Crud dApp con NEAR y VUE Capítulo 2
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.
— — — — — — — — — —
- 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
, importamosmapActions
de vuex
y 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.