Crud dApp con NEAR y VUE Capítulo 2

Phoenix Pulsar
18 min readMay 31, 2022

En este tutorial vamos a escribir el código web de para nuestro contrato inteligente. Usaremos VUE CLI para crear nuestro proyecto.

Código Fuente: https://github.com/phoenixpulsar/vehicles-on-the-block-front-end

DEMO: https://recordit.co/1RQAM9kmOp

Este tutorial sigue el historial Git a continuación.

Antes de comenzar — Disclaimer:
El objetivo de este tutorial es para fines educativos. El código que se presenta no es código terminado o listo para producción. Se presentará solo una de las muchas formas en la que se puede construir una aplicación descentralizada usando NEAR. Por último, retroalimentación es bienvenida con el objetivo de crear un tutorial que sea útil para la comunidad.

— — — — — — — — — —

  1. Crear un nuevo proyecto usandoVUE
    También puedes usar el proyecto directo de Github

    Desde la terminal:
vue create <name-of-your-project>

Vamos a seleccionar las opciones manualmente:

Versión:

Router:

SCSS

ESLint + Prettier

Archivos de configuración:

Cuando nos pregunte si queremos guardar la configuración para futuros proyectos, recomendamos elegir la opción no, dado que cada proyecto es diferente.

Una vez que el proyecto termine su inicialización, cd al proyecto y comenzamos el servidor de desarrollo local con: yarn serve

Usaremos Git y Github para poder tener control de nuestro progreso del proyecto. Yo lo nombré vehicles-on-the-block-front-end. (Otra alternativa, es crear este proyecto dentro del contrato inteligente en su propio archivo)

Después de crear un proyecto en Github, tendrán la opciones de como empujar el código a un proyecto ya existente, que es lo usaremos.(Push to an existing repository)

— — — — — — — — — — — — — — — —
2. Configuración inicial

Para mantener el código organizado usaremos Prettier. Ahora creamos el archivo .prettierrc y adentro de este archivo agregamos un objeto vacío. Esto usa la configuración default de prettier.

Ahora agregaremos un nuestro archivo principal de configuración donde agregaremos unas variables específicas de nuestro contrato inteligente. En src agrega el archivoconfig.js.Asegúrate de cambiar de cambiar “SMART_CONTRACT_NAME” al nombre del contrato que creaste en el capítulo uno

const CONTRACT_NAME = 
process.env.CONTRACT_NAME || "SMART_CONTRACT_NAME";
function getConfig(env) {
switch (env) {
case "development":
case "production":
case "testnet":
return {
networkId: "testnet",
nodeUrl: "https://rpc.testnet.near.org",
contractName: CONTRACT_NAME,
walletUrl: "https://wallet.testnet.near.org",
helperUrl: "https://helper.testnet.near.org",
explorerUrl: "https://explorer.testnet.near.org",
};
default:
throw Error(
`Unconfigured environment '${env}'. Can be configured in src/config.js.`
);
}
}
module.exports = getConfig;

— — — — — — — — — — — — — — — —
3. Estructura

Ahora vamos a crear los componentes que usaremos para interactuar con nuestro contrato inteligente. Los dejaremos bastante vacíos y escribiremos más código mientras avancemos en el tutorial.

LEER (READ)
Vehicle.vue
VehicleService.vue

CREAR (CREATE)
AddVehicleForm.vue
Add ServiceForm.vue

EDITAR (EDIT)
EditVehicleForm.vue
EditVehicleServiceForm.vue

OTROS (OTHER)
Login.vue
ActionMessage.vue

El H1 del componente y el nombre del componente los pondremos igual. Login.vue quedaría como mostramos a continuación. Haz lo mismo para todos los componentes.

<template>
<div class="login">
<h1>Login</h1>
</div>
</template>
<script>
// @ is an alias to /src
export default {
name: "Login",
data() {
return {};
},
methods: {},
};
</script>
<style lang="scss" scoped></style>

— — — — — — — — — — — — — — — —
3. Init Home
Mayoría del código que vamos a escribir va ser en Home.vue. Revisa de que tu servidor de desarrollo local este corriendo (yarn serve ) y navega a localhost:portAppIsServed. En este paso vamos a importar todos nuestros componentes.

Navega Home.vue y en nuestro template agrega los componentes para poder visualizarlos en el web.

<template>
<div class="home">
<Login></Login>
<Vehicle></Vehicle>
<ActionMessage></ActionMessage>
<VehicleService></VehicleService>
<AddVehicleForm></AddVehicleForm>
<AddServiceForm></AddServiceForm>
<EditVehicleForm></EditVehicleForm>
<EditVehicleServiceForm></EditVehicleServiceForm>
</div>
</template><script>
// @ is an alias to /src
import Login from "@/components/Login.vue";
import Vehicle from "@/components/Vehicle.vue";
import ActionMessage from "@/components/ActionMessage.vue";
import VehicleService from "@/components/VehicleService.vue";
import AddVehicleForm from "@/components/AddVehicleForm.vue";
import AddServiceForm from "@/components/AddServiceForm.vue";
import EditVehicleForm from "@/components/EditVehicleForm.vue";
import EditVehicleServiceForm from "@/components/EditVehicleServiceForm.vue";export default {
name: "Home",
components: {
Login,
Vehicle,
ActionMessage,
VehicleService,
AddVehicleForm,
AddServiceForm,
EditVehicleForm,
EditVehicleServiceForm,
},
};
</script>

Así es como se debe de ver en el navegador.

— — — — — — — — — — — — — — — —
4. Vuex

Vamos a usar VUEX para almacenar toda la información que vamos a querer compartir a través de nuestros componentes en nuestra aplicación. VUEX es inspirado de la arquitectura Flux. Existen varias implementaciones como Redux en React, NGRX en Angular y Mobx.

En el folder store/index.js Importamos todo de near-api-js que nos servirá para integrar NEAR con nuestra aplicación. Para agregar este paquete, vas a tener que correr el siguiente comando desde la terminal.

yarn add near-api-js

Usando config.js ynearAPI vamos a establecer el ‘keystore’ al ‘browser local storage’. Esto agrega las llaves necesarias para poder interactuar con nuestro contrato inteligente.

import { createStore } from "vuex";
import getConfig from "../config";
import * as nearAPI from "near-api-js";
export default createStore({
state: {
nearConfig: null,
},
mutations: {
SET_NEAR_CONFIG: (state, nearConfig) => {
state.nearConfig = nearConfig;
},
},
actions: {
_setConfig: ({ commit }) => {
try {
let config = getConfig(process.env.NODE_ENV || "development");
let nearConfig = {
...config,
keyStore: new nearAPI.keyStores.BrowserLocalStorageKeyStore(),
};
commit("SET_NEAR_CONFIG", nearConfig);
} catch (error) {
console.error("Error setting NEAR Config", error);
}
},
initStore: async ({ dispatch }) => {
console.log("Init Store In progres...");
dispatch("_setConfig");
},
},
modules: {},
});

En Home.vue, llamamos la funcióninitStore , importamosmapActionsde vuexy cuando el componente se monté, llamamos nuestra función.

<template>
<div class="home">
<Login></Login>
<Vehicle></Vehicle>
<ActionMessage></ActionMessage>
<VehicleService></VehicleService>
<AddVehicleForm></AddVehicleForm>
<AddServiceForm></AddServiceForm>
<EditVehicleForm></EditVehicleForm>
<EditVehicleServiceForm></EditVehicleServiceForm>
</div>
</template><script>
// @ is an alias to /src
import { mapActions } from "vuex";
import Login from "@/components/Login.vue";
import Vehicle from "@/components/Vehicle.vue";
import ActionMessage from "@/components/ActionMessage.vue";
import VehicleService from "@/components/VehicleService.vue";
import AddVehicleForm from "@/components/AddVehicleForm.vue";
import AddServiceForm from "@/components/AddServiceForm.vue";
import EditVehicleForm from "@/components/EditVehicleForm.vue";
import EditVehicleServiceForm from "@/components/EditVehicleServiceForm.vue";export default {
name: "Home",
components: {
Login,
Vehicle,
ActionMessage,
VehicleService,
AddVehicleForm,
AddServiceForm,
EditVehicleForm,
EditVehicleServiceForm,
},
methods: {
...mapActions(["initStore"]),
},
mounted() {
this.initStore();
},
};
</script>

Es recomendable agregar las herramientas de desarrollo de Vue a tu navegador. Después de instalar estas herramientas, abre las herramientas de desarrollo y navega a la pestaña de Vue y da click en VUEX.

Estamos listos para crear una conexión con NEAR y empezar a interactuar con nuestro contrato inteligente.

— — — — — — — — — — — — — — — —
5. Conexión con NEAR
Ahora es momento de conectar nuestra aplicación con NEAR. Usado connect denearAPI y pasarle nuestro objeto de configuración que escribimos en el paso anterior. Esta conexión va ser necesaria para poder leer el estado de nuestro contrato e interactuar con el.

_connectToNear: async ({ commit, state }) => {
try {
let nearConnection = await nearAPI.connect(state.nearConfig);
commit("SET_NEAR_CONNECTION", nearConnection);
} catch (error) {
console.error("Error connecting to NEAR", error);
}
},

Las funciones que usan _ es solo una convención que usamos para saber que esa función solo va ser utilizada dentro de este archivo. Si tienes experiencia con otros lenguajes de programación, muchos usan esta convención para crear funciones o variables privadas.

import { createStore } from "vuex";
import getConfig from "../config";
import * as nearAPI from "near-api-js";export default createStore({
state: {
nearConfig: null,
nearConnection: null
},
mutations: {
SET_NEAR_CONFIG: (state, nearConfig) => {
state.nearConfig = nearConfig;
},
SET_NEAR_CONNECTION: (state, nearConnection) => {
state.nearConnection = nearConnection;
},
},
actions: {
_setConfig: ({ commit }) => {
try {
let config = getConfig(process.env.NODE_ENV || "development");
let nearConfig = {
...config,
keyStore: new nearAPI.keyStores.BrowserLocalStorageKeyStore(),
};
commit("SET_NEAR_CONFIG", nearConfig);
} catch (error) {
console.error("Error setting NEAR Config", error);
}
},
_connectToNear: async ({ commit, state }) => {
try {
let nearConnection = await nearAPI.connect(state.nearConfig);
commit("SET_NEAR_CONNECTION", nearConnection);
} catch (error) {
console.error("Error connecting to NEAR", error);
}
},
initStore: async ({ dispatch }) => {
console.log("Init Store In progres...");
dispatch("_setConfig");
await dispatch("_connectToNear");
},
},
modules: {},
});

— — — — — — — — — — — — — — — —
6. Leer la información del contrato inteligente y guardarla en VUEX

Aquí es donde empieza lo divertido. Vamos a empezar leyendo el estado del contrato y guardarlo en VUEX. En la función que mostramos a continuación estamos haciendo bastante, si lo partimos en pasos, vemos que primero usamos nearConnection del paso anterior para poder leer el estado del contrato. Esta información viene codificada en base64. Para decodificarla usamos la función atob de JavaScript. Después separamos vehículos y servicios para poder tener más control en nuestra aplicación. Por último, lo guardamos en VUEX.

_fetchState: async ({ commit, state }) => {
try {
// Fetch State
const response = await state.nearConnection.connection.provider.query({
prefix_base64: "",
finality: "final",
account_id: state.nearConfig.contractName,
request_type: "view_state",
}); // Decode
let storage = {};
response.values.forEach((v) => {
let decodedKey = atob(v.key);
let decodedVal = atob(v.value);
storage[decodedKey] = JSON.parse(decodedVal);
}); // Data Structures
let vehicles = [];
let services = []; // Populate Data Structures
for (const [key, value] of Object.entries(storage)) {
if (key.startsWith("v:")) {
let vehicleToAdd = {
fullid: key,
...value,
};
vehicles.push(vehicleToAdd);
}
if (key.startsWith("vs:")) {
let serviceToAdd = {
fullid: key,
...value,
};
services.push(serviceToAdd);
}
} // Create object to store
let accountState = {
vehicles: vehicles,
services: services,
}; // Update Vuex State
commit("SET_ACCOUNT_STATE", accountState);
} catch (error) {
console.error("Error connecting to NEAR", error);
}
},

Si no agregaste un vehículo o servicio en el capítulo 1 de este tutorial, revisa el paso 6 de cómo agregar un vehículo desde la terminal. Más adelante vamos hacerlo todo por medio del navegador, pero por ahora, agrega un vehículo o dos desde la terminal para poder revisar que podemos LEER el contrato.

En las herramientas del navegador, en la pestaña de Vuex:

— — — — — — — — — — — — — — — —
7. Usar el navegador para ver la información de vehículos que hemos guardado

Vamos a usar unos ‘getters’ para que Home.vue pueda leer el estado de Vuex. Cómo la información que vamos a mostrar puede cambiar, vamos a usar ‘computed properties’ para leer el estado de nuestros vehículos y pasar esta información a cada uno de nuestros vehículos.

Home.vue

<template>
<div class="home">
<Login></Login>
<div
v-for="(vehicle, index) in contractState.vehicles"
:key="index"
class="vehicle-box"
>
<Vehicle :vehicle="vehicle"></Vehicle>
</div>
<ActionMessage></ActionMessage>
<VehicleService></VehicleService>
<AddVehicleForm></AddVehicleForm>
<AddServiceForm></AddServiceForm>
<EditVehicleForm></EditVehicleForm>
<EditVehicleServiceForm></EditVehicleServiceForm>
</div>
</template><script>
// @ is an alias to /src
import { mapActions, mapGetters } from "vuex";
import Login from "@/components/Login.vue";
import Vehicle from "@/components/Vehicle.vue";
import ActionMessage from "@/components/ActionMessage.vue";
import VehicleService from "@/components/VehicleService.vue";
import AddVehicleForm from "@/components/AddVehicleForm.vue";
import AddServiceForm from "@/components/AddServiceForm.vue";
import EditVehicleForm from "@/components/EditVehicleForm.vue";
import EditVehicleServiceForm from "@/components/EditVehicleServiceForm.vue";export default {
name: "Home",
components: {
Login,
Vehicle,
ActionMessage,
VehicleService,
AddVehicleForm,
AddServiceForm,
EditVehicleForm,
EditVehicleServiceForm,
},
computed: {
...mapGetters(["GET_CONTRACT_STATE"]),
contractState() {
return this.GET_CONTRACT_STATE;
},
},
methods: {
...mapActions(["initStore"]),
},
mounted() {
this.initStore();
},
};
</script>

Estamos pasando el vehículo(prop) al componente Vehicle. Modifica Vehículo de la siguiente manera para que pueda recibir la información del vehículo.

<template>
<div class="vehicle">
<h1>Vehicle</h1>
<div>Make: {{ vehicle.make }}</div>
<div>Model:{{ vehicle.model }}</div>
<div>Year: {{ vehicle.year }}</div>
<div>Owner:{{ vehicle.owner }}</div>
<div>Acquired:{{ vehicle.vehicleNotes }}</div>
<div>Notes: {{ vehicle.dateAcquired }}</div>
</div>
</template>
<script>
// @ is an alias to /src
export default {
name: "Vehicle",
props: {
vehicle: Object,
},
data() {
return {};
},
methods: {},
};
</script>
<style lang="scss" scoped></style>

En el navegador podrás ver el vehículo que agregaste a tu contrato.

Es tu turno, haz lo mismo para los servicios.

— — — — — — — — — — — — — — — —
8. Crear un sesión de usuario.

Crear una sesión con un usuario es muy simple con NEAR. En este paso vamos a explorar cómo podemos dejar que usuarios se conecten a nuestra aplicación. Conectaremos la carteraWalletConnection usando nearAPI .

_connectToWallet: ({ commit, state }) => {
try {
const wallet = new nearAPI.WalletConnection(state.nearConnection);
commit("SET_WALLET_CONNECTION", wallet);
} catch (error) {
console.error("Error connecting to NEAR wallet");
}
},

Mutación-Mutation:

SET_WALLET_CONNECTION: (state, walletConnection) => {
state.walletConnection = walletConnection;
},

Agregamos lo siguiente al estado para poder tener control del usuario que creé una sesión

Estado-State:

state: {
nearConfig: null,
nearConnection: null,
accountState: {
vehicles: [],
services: [],
},
walletConnection: null,
isUserLoggedIn: null,
accountDetails: null,
},

Acciones - Actions:

signIn: ({ state }) => {
// redirects user to wallet to authorize your dApp
// this creates an access key that will be stored in the browser's
// local storage
// access key can then be used to connect to NEAR and sign
// transactions via keyStore let config = getConfig(process.env.NODE_ENV || "development");
state.walletConnection.requestSignIn(
config.contractName, // contract requesting access
"Vehicles On The Block" // optional
// "http://YOUR-URL.com/success", // optional
// "http://YOUR-URL.com/failure" // optional
);
},
signOut: ({ state }) => {
state.walletConnection.signOut();
state.isUserLoggedIn = false;
state.accountDetails = null;
},
getAccountDetails: async ({ state }) => {
state.accountDetails = await state.walletConnection.account();
},
checkIfUserLoggedIn: ({ state, dispatch }) => {
if (!state.walletConnection.getAccountId()) {
state.isUserLoggedIn = false;
} else {
state.isUserLoggedIn = true;
dispatch("getAccountDetails");
}
},

En initStore disparamos la función para conectar la cartera

dispatch("_connectToWallet")

En Home.vue agrega lo siguiente

Template:

<button @click="signInUsingStore()">
Sign In with NEAR Wallet
</button>
<button @click="signOutUsingStore()">
Sign Out
</button>

Métodos - Methods:

...mapActions([
"initStore",
"signIn",
"signOut",
]),
signInUsingStore() {
this.signIn();
},
signOutUsingStore() {
this.signOut();
},

En el navegador veras:

Haz click en Sign In y serás redirigido para que autorices la cuenta NEAR que quieres usar para crear una sesión.

Después de seleccionar la cuenta con la que quieres iniciar la sesión, ve a las herramientas del navegador, haz click en la pestaña de VUEX, y revisa el estado. Revisa quewalletConnection tenga el valor del usuario que seleccionaste.

Ya casi terminamos en este paso, agregamos lo siguiente para ver al usuario en el navegador.

En VUEX

Getters:

GET_IS_USER_LOGGED_IN: (state) => {
return state.isUserLoggedIn;
},
GET_USER_ACCOUNT_DETAILS: (state) => {
return state.accountDetails;
},

Acciones-Actions:

getAccountDetails: async ({ state }) => {
state.accountDetails = await state.walletConnection.account();
},

Asegúrate de disparar(dispatch) eninitStore

dispatch("checkIfUserLoggedIn");

Hacemos los siguientes cambios en Home.vue

Template

<div v-if="!GET_IS_USER_LOGGED_IN">
<button @click="signInUsingStore()">
Sign In with NEAR Wallet
</button>
</div>
<div v-if="GET_IS_USER_LOGGED_IN">
<button @click="signOutUsingStore()">Sign Out</button>
</div>
<div v-if="GET_IS_USER_LOGGED_IN">
Welcome, {{ GET_USER_ACCOUNT_DETAILS?.accountId }}
</div>

Agrega los getter:

...mapGetters([
"GET_CONTRACT_STATE",
"GET_IS_USER_LOGGED_IN",
"GET_USER_ACCOUNT_DETAILS",
]),

En el navegador veras:

Intenta iniciar sesión y cerrar sesión para asegurar que todo este funcionando.

— — — — — — — — — — — — — — — —
9. Interactuar con nuestro contrato inteligente

En los pasos anteriores, escribimos código que nos permite leer el estado del contrato. En este paso vamos escribir código que permita a un usuario agregar un vehículo y su servicio. Empezaremos en nuestra tienda VUEX.

Acciones-Actions

getContract: async ({ commit, state }) => {
let config = getConfig(process.env.NODE_ENV || "development");
let contract = new nearAPI.Contract(
// the account object that is connecting
state.accountDetails,
// name of contract you're connecting to
config.contractName,
{
// view methods do not change state but can return a value
viewMethods: [],
// change methods modify state
changeMethods: [
"add_vehicle",
"update_vehicle",
"delete_vehicle",
"add_vehicle_service",
"update_vehicle_service",
"delete_vehicle_service",
],
// account object to initialize and sign transactions.
sender: state.account,
}
);
commit("SET_CONTRACT", contract);
},

Mutaciones - Mutations

SET_CONTRACT: (state, contract) => {
state.contract = contract;
},

State Prop

contract: null,

Otras acciones necesarias

addVehicle: async ({ state, dispatch }, vehicleToAdd) => {
if (state.contract === null) {
await dispatch("getContract");
}
let res = await state.contract.add_vehicle(vehicleToAdd);
console.log("res from adding", res);
dispatch("_fetchState");
},updateVehicle: async ({ state, dispatch }, vehicleToUpdate) => {
if (state.contract === null) {
await dispatch("getContract");
}
let res = await state.contract.update_vehicle(vehicleToUpdate);
console.log("res from updating", res);
dispatch("_fetchState");
},updateVehicleService: async ({ state, dispatch }, serviceToUpdate) => {
if (state.contract === null) {
await dispatch("getContract");
}
let res = await state.contract.update_vehicle_service(serviceToUpdate);
console.log("res from updating service", res);
dispatch("_fetchState");
},deleteVehicle: async ({ state, dispatch }, vehicleToDelete) => {
if (state.contract === null) {
await dispatch("getContract");
}
let res = await state.contract.delete_vehicle({
vehicleId: vehicleToDelete.id,
});
console.log("res from deleting vehicle", res);
dispatch("_fetchState");
},deleteService: async ({ state, dispatch }, serviceToDeleteId) => {
if (state.contract === null) {
await dispatch("getContract");
} let res = await state.contract.delete_vehicle_service({
vehicleServiceId: serviceToDeleteId,
}); console.log("res from deleting service", res); dispatch("_fetchState");
},addService: async ({ state, dispatch }, serviceToAdd) => {
if (state.contract === null) {
await dispatch("getContract");
}
await state.contract.add_vehicle_service({
vehicleId: serviceToAdd.vehicleId,
serviceDate: serviceToAdd.serviceDate,
serviceNotes: serviceToAdd.serviceNotes,
}); dispatch("_fetchState");
}

Para ayudarnos, usaremos console.log, pero la mejor práctica es borrarlos antes de hacer un commit.

Agregar un vehículo AddVehicleForm

<template>
<div class="add-vehicle-service-form">
<h1>Add Vehicle Form</h1>
<div>
<input type="text" v-model="make" placeholder="Make" />
</div>
<div>
<input type="text" v-model="year" placeholder="Year" />
</div>
<div>
<input type="text" v-model="model" placeholder="Model" />
</div>
<div>
<input type="text" v-model="owner" placeholder="Owner" />
</div>
<div>
<input type="text" v-model="dateAcquired" placeholder="Date Acquired" />
</div>
<div>
<textarea type="text" v-model="vehicleNotes" placeholder="Notes" />
</div>
<button @click="callAddVehicle()">Add Vehicle</button>
</div>
</template>
<script>
import { mapActions } from "vuex";
// @ is an alias to /src
export default {
name: "AddVehicleServiceForm",
data() {
return {
year: "",
make: "",
model: "",
owner: "",
vehicleNotes: "",
dateAcquired: "",
};
},
methods: {
...mapActions(["addVehicle"]),
callAddVehicle() {
let vehicleToAdd = {
year: this.year,
make: this.make,
model: this.model,
owner: this.owner,
dateAcquired: this.dateAcquired,
vehicleNotes: this.vehicleNotes,
};
this.addVehicle(vehicleToAdd);
this.resetForm();
},
resetForm() {
this.year = "";
this.make = "";
this.model = "";
this.owner = "";
this.vehicleNotes = "";
this.dateAcquired = "";
},
},
};
</script>
<style lang="scss" scoped></style>

Por simplicidad no estamos usando HTML forms ni estamos agregando validación, esto es un paso importante pero lo dejaremos como un ejercicio para el lector.

Cuando agregamos un vehículo y esperamos la respuesta 200 podrás notar que a veces no se refleja el cambio en el navegador. NEAR blockchain es muy rápido (1 segundo por bloque ~). Como nuestra aplicación es probable que sea más rápida que lo que se tarda en terminar un bloque, agregaremos un timeout para esperar que se complete la transacción. En VUEX cambia add_vehicle de la siguiente manera:

// wait for block (1 sec~), to be safe we wait for 2 sec
setTimeout(() => {
dispatch("_fetchState");
}, 2000);

— — — — — — — — — — — — — — — —

10. Agregar un servicio a un vehículo

Para poder agregar un servicio, vamos a necesitar el número de identificación del vehículo. En Home agregamos el componente AddServiceForm y pasamos el ‘id’.

<div v-for="(vehicle, index) in contractState.vehicles" :key="index">
<Vehicle :vehicle="vehicle"></Vehicle>
<AddServiceForm :vehicleId="vehicle.id"></AddServiceForm>
</div>

En AddServiceForm modificamos el código con unos input para poder agregar un servicio

<template>
<div class="add-service-form">
<h1>Add Service Form</h1>
<div>
<input
type="text"
v-model="serviceDate"
placeholder="Service Date" />
</div>
<div>
<textarea
type="text"
rows="5"
v-model="serviceNotes"
placeholder="Service Notes"
/>
</div>
</div>
<button @click="callAddService()">Add Service</button>
</div>
</template>
<script>
import { mapActions } from "vuex";
// @ is an alias to /src
export default {
name: "AddServiceForm",
data() {
return {
serviceDate: "",
serviceNotes: "",
};
},
methods: {
...mapActions(["addService"]),
callAddService() {
let serviceToAdd = {
vehicleId: this.vehicleId,
serviceDate: this.serviceDate,
serviceNotes: this.serviceNotes,
};
this.addService(serviceToAdd);
},
},
props: {
vehicleId: String,
},
};
</script>
<style lang="scss" scoped></style>

— — — — — — — — — — — — — — — —

11. Editar Vehículo y Servicio

Ya terminamos de agregar y poder leer cuando agregamos vehículos y servicios. Ahora vamos a agregar lógica para poder editarlos.

Vamos a ordenar un poco nuestro template en Home.vue .

Template

<template>
<div class="home">
<Login></Login>
<div v-if="!GET_IS_USER_LOGGED_IN">
<button @click="signInUsingStore()">
Sign In with NEAR Wallet
</button>
</div>
<div v-if="GET_IS_USER_LOGGED_IN">
<button @click="signOutUsingStore()">Sign Out</button>
</div>
<div v-if="GET_IS_USER_LOGGED_IN">
Welcome, {{ GET_USER_ACCOUNT_DETAILS?.accountId }}
</div>
<AddVehicleForm></AddVehicleForm>
<div v-for="(vehicle, index) in contractState.vehicles" :key="index">
<Vehicle :vehicle="vehicle"></Vehicle>
<AddServiceForm :vehicleId="vehicle.id"></AddServiceForm>
<EditVehicleForm :vehicle="vehicle"></EditVehicleForm>
</div>
<div v-for="(vehicleService, index) in contractState.services" :key="index">
<VehicleService :vehicleService="vehicleService"></VehicleService>
<EditVehicleServiceForm :vehicleService="vehicleService"></EditVehicleServiceForm>
</div>
<ActionMessage></ActionMessage>
</div>
</template>

EditVehicleForm

<template>
<div class="edit-vehicle-form">
<h1>Edit Vehicle Form</h1>
<div>
<input
type="text"
:value="vehicle?.make"
@change="make = $event.target.value"
placeholder="Make"
/>
</div>
<div>
<input
type="text"
:value="vehicle?.year"
@change="year = $event.target.value"
placeholder="Year"
/>
</div>
<div>
<input
type="text"
:value="vehicle?.model"
@change="model = $event.target.value"
placeholder="Model"
/>
</div>
<div>
<input
type="text"
:value="vehicle?.owner"
@change="owner = $event.target.value"
placeholder="Owner"
/>
</div>
<div>
<input
type="text"
:value="vehicle?.vehicleNotes"
@change="vehicleNotes = $event.target.value"
placeholder="Date Acquired"
/>
</div>
<div>
<textarea
type="text"
:value="vehicle?.dateAcquired"
@change="dateAcquired = $event.target.value"
placeholder="Notes"
/>
</div>
</div><button @click="callUpdateVehicle()">Update Vehicle</button>
</template>
<script>
import { mapActions } from "vuex";
// @ is an alias to /src
export default {
name: "EditVehicleForm",
props: {
vehicle: Object,
},
data() {
return {
make: null,
year: null,
model: null,
owner: null,
vehicleNotes: null,
dateAcquired: null,
};
},
methods: {
...mapActions(["updateVehicle"]),
callUpdateVehicle() {
let vehicleToUpdate = {
vehicleId: this.vehicle.id,
make: this.make !== null ? this.make : this.vehicle.make,
year: this.year !== null ? this.year : this.vehicle.year,
model: this.model !== null ? this.model : this.vehicle.model,
owner: this.owner !== null ? this.owner : this.vehicle.owner,
vehicleNotes:
this.vehicleNotes !== null
? this.vehicleNotes
: this.vehicle.vehicleNotes,
dateAcquired:
this.dateAcquired !== null
? this.dateAcquired
: this.vehicle.dateAcquired,
};this.updateVehicle(vehicleToUpdate);
},
},
};
</script>
<style lang="scss" scoped></style>

EditServiceForm

<template>
<div class="edit-vehicle-service-form"> <div>
<input
type="text"
:value="vehicleService.serviceDate"
@change="serviceDate = $event.target.value"
placeholder="Service Date"
/>
</div> <div>
<textarea
type="text"
rows="3"
:value="vehicleService.serviceNotes"
@change="serviceNotes = $event.target.value"
placeholder="Service Notes"
/>
</div>
</div><button @click="callUpdateVehicleService()">Update Service</button>
</template><script>
import { mapActions } from "vuex";
export default {
name: "EditVehicleServiceForm",
props: {
vehicleService: Object,
},
data() {
return {
serviceDate: null,
serviceNotes: null,
};
},
methods: {
...mapActions(["updateVehicleService"]),
callUpdateVehicleService() {
let serviceToUpdate = {
vehicleServiceId: this.vehicleService.id,
vehicleId: this.vehicleService.vehicleId,
serviceDate:
this.serviceDate !== null
? this.serviceDate
: this.vehicleService.serviceDate,
serviceNotes:
this.serviceNotes !== null
? this.serviceNotes
: this.vehicleService.serviceNotes,
};
this.updateVehicleService(serviceToUpdate);
},
},
};
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss"></style>

— — — — — — — — — — — — — — — —

12. Borrar Vehículo y Servicio

Ya estamos en la recta final. Ahora agregaremos código para borrar un vehículo y servicio.

Home.vue

<template>
<div class="home">
<Login></Login>
<div v-if="!GET_IS_USER_LOGGED_IN">
<button @click="signInUsingStore()">Sign In with NEAR Wallet</button>
</div>
<div v-if="GET_IS_USER_LOGGED_IN">
<button @click="signOutUsingStore()">Sign Out</button>
</div>
<div v-if="GET_IS_USER_LOGGED_IN">
Welcome, {{ GET_USER_ACCOUNT_DETAILS?.accountId }}
</div>
<AddVehicleForm></AddVehicleForm>
<div v-for="(vehicle, index) in contractState.vehicles" :key="index">
<Vehicle :vehicle="vehicle"></Vehicle>
<button @click="callDeleteVehicle(vehicle)">Delete Vehicle</button>
<AddServiceForm :vehicleId="vehicle.id"></AddServiceForm>
<EditVehicleForm :vehicle="vehicle"></EditVehicleForm>
</div>
<div v-for="(vehicleService, index) in contractState.services" :key="index">
<VehicleService :vehicleService="vehicleService"></VehicleService>
<button @click="callDeleteService(vehicleService.id)">
Delete Service
</button>
<EditVehicleServiceForm
:vehicleService="vehicleService"
></EditVehicleServiceForm>
</div>
<ActionMessage></ActionMessage>
</div>
</template><script>
// @ is an alias to /src
import { mapActions, mapGetters } from "vuex";
import Login from "@/components/Login.vue";
import Vehicle from "@/components/Vehicle.vue";
import ActionMessage from "@/components/ActionMessage.vue";
import VehicleService from "@/components/VehicleService.vue";
import AddVehicleForm from "@/components/AddVehicleForm.vue";
import AddServiceForm from "@/components/AddServiceForm.vue";
import EditVehicleForm from "@/components/EditVehicleForm.vue";
import EditVehicleServiceForm from "@/components/EditVehicleServiceForm.vue";export default {
name: "Home",
components: {
Login,
Vehicle,
ActionMessage,
VehicleService,
AddVehicleForm,
AddServiceForm,
EditVehicleForm,
EditVehicleServiceForm,
},
computed: {
...mapGetters([
"GET_CONTRACT_STATE",
"GET_IS_USER_LOGGED_IN",
"GET_USER_ACCOUNT_DETAILS",
]),
contractState() {
return this.GET_CONTRACT_STATE;
},
},
methods: {
...mapActions([
"initStore",
"signIn",
"signOut",
"deleteVehicle",
"deleteService",
]),
signInUsingStore() {
this.signIn();
},
signOutUsingStore() {
this.signOut();
},
callDeleteVehicle(vehicle) {
this.deleteVehicle(vehicle);
},
callDeleteService(serviceId) {
this.deleteService(serviceId);
},
},
mounted() {
this.initStore();
},
};
</script>

— — — — — — — — — — — — — — — —

12. Action Message (Opcional)

A lo mejor te estas preguntado para qué es el componente ActionMessage. Usaremos este componente para informar al usuario que estamos trabajando en su orden. Como mencionamos en el paso 9, si el lector tiene experiencia trabajando con APIs y REST podrá ver que NEAR (aunque es muy muy muy rápido comparando con otras tecnologías de bloques) no es tan rápido como muchos de los servidores REST. Como agregar algo a la cadena de bloques no es instantaneo, es buena idea de decirle al usuario que estamos trabajando en su orden y/o esperando a que se complete su orden. Para hacer esto mandaremos un mensaje de el componente hijo(child) al componente papá(parent) usando $emit para mostrar un mensaje.

AddVehicleForm component

<AddVehicleForm @openActionMssg="openActionMssg"></AddVehicleForm>

AddServiceForm component

<AddServiceForm 
:vehicleId="vehicle.id"
@openActionMssg="openActionMssg"
></AddServiceForm>

EditVehicleForm

<EditVehicleForm
:vehicle="vehicle"
@openActionMssg="openActionMssg"
></EditVehicleForm>

EditVehicleServiceForm

<EditVehicleServiceForm
@openActionMssg="openActionMssg"
:vehicleService="vehicleService"
></EditVehicleServiceForm>

Agrega showActionMessage para controlar cuando mostrar ActionMessage

data() {
return {
showActionMessage: false,
};
},

Usando un v-if para que solo mostremosActionMessage y ni un otro de los componentes.

<div v-if="!showActionMessage">
// All components inside home to display
</div><ActionMessage
v-if="showActionMessage"
@closeActionMssg="closeActionMssg"
></ActionMessage>

Agregamos uno métodos para controlar cuando lo mostramos

closeActionMssg() {
this.showActionMessage = false;
},
openActionMssg() {
this.showActionMessage = true;
},

Para nuestras funciones para borrar, podemos mostrar el mensaje desde Home.vue component sin necesidad de usar $emit. Modifica las siguientes funciones a:

callDeleteVehicle(vehicle) {
this.deleteVehicle(vehicle);
this.showActionMessage = true;
},
callDeleteService(serviceId) {
this.deleteService(serviceId);
this.showActionMessage = true;
},

Ahora en cualquier otro componente dónde interactuamos con nuestro contrato inteligente, modifica el código para mostrar el mensaje. En AddVehicleForm modifica la funcióncallAddVehicle a:

callAddVehicle() {
let vehicleToAdd = {
year: this.year,
make: this.make,
model: this.model,
owner: this.owner,
dateAcquired: this.dateAcquired,
vehicleNotes: this.vehicleNotes,
};
this.addVehicle(vehicleToAdd);
this.resetForm();
this.$emit("openActionMssg");
},

Haz lo mismo para todos los otros componentes.

Por último, modifica ActionMessage a:

<template>
<div class="action-message">
<h1>Action Message</h1>
<h2>Your Request is Being Processed</h2>
<p>It might take a bit to see the changes reflected in the blockchain.</p>
<button @click="close">Close</button>
</div>
</template>
<script>
// @ is an alias to /src
export default {
name: "ActionMessage",
data() {
return {};
},
methods: {
close() {
this.$emit("closeActionMssg");
},
},
};
</script>
<style lang="scss" scoped></style>

Felicidades, hemos terminado con un producto mínimo viable de nuestra aplicación(MVP). Esperamos que este tutorial sirva como inspiración para el lector para crear otras aplicaciones ¿Qué vas crear?

— — — — — — — — — — — — — — — —

13. SCSS (Opcional)

No vamos a ir paso por paso en cómo agregar estilos(SCSS) en este tutorial, lo dejaremos como un ejercicio al lector. En Github podrás encontrar el SCSS del proyecto pero invitamos al lector en crear su propios estilos. Hasta la próxima!

— — — — — — — — — — — — — — — —

14. Retroalimentación

Hemos terminado con la funcionalidad de nuestra aplicación de nuestro dApp. Felicidades por completar el tutorial. Si llegaste a este paso por favor dejar retroalimentación en los comentarios. También menciona todas las cuentas que creaste en NEAR. (Estamos considerando dar un premio en NEAR a todos aquellos que muestren qué terminaron el tutorial). Queremos poder crear un tutorial que sea útil para la comunidad y la única forma de mejorar es por medio de retroalimentación.

--

--

Phoenix Pulsar
0 Followers

“Think deeply about things, don’t just ago along because that is the way things are” — Aaron Swartz