One place for hosting & domains

      Vuex

      How To Manage State in a Vue.js Application with Vuex


      The author selected Open Sourcing Mental Illness to receive a donation as part of the Write for DOnations program.

      Introduction

      Vuex is the first-party development state management library for Vue.js. It was created by Evan You and is currently maintained by the Vue.js Core Team. Like many other state management libraries, Vuex follows the principle that Redux has popularized over the past years: Data flows in one direction, with actions and mutations modifying data in a single source of truth called the store.

      A Vuex store is a collection of different methods and data. Some of these methods, such as actions, can fetch and process data before it is sent to a mutation. A mutation is a method that mutates or updates the store property with the value provided. Getters are methods that can modify or combine data to create a new state property. These getters are read-only and do not mutate data. These are similar to computed properties in a Vue.js component. The last component to Vuex is the state, or the dataset that acts as your single source of truth.

      In this tutorial, you will create an application that renders a list of cards with airport information in them. When clicked, these cards will execute the Vuex workflow to add the selected airport to a list of favorites. By running through this example, you will make actions and mutations to manage state and getters to retrieve computed data.

      Prerequisites

      Step 1 — Setting Up the Example Application

      In order to help visualize how state is managed with Vuex, set up a project with some data to display in the view. You will use this project and throughout the tutorial.

      Once the favorite-airports project is created as described in the Prerequisites section, create a directory to hold all of your local data for this project. Open your terminal and run the following commands in the project root directory (favorite-airports):

      • mkdir src/data
      • touch src/data/airports.js

      This will create the data directory and an empty airports.js file inside it.

      In your text editor of choice, open the newly created airports.js file and add in the following:

      favorite-airports/src/data/airports.js

      export default [
        {
          name: 'Cincinnati/Northern Kentucky International Airport',
          abbreviation: 'CVG',
          city: 'Hebron',
          state: 'KY'
        },
        {
          name: 'Seattle-Tacoma International Airport',
          abbreviation: 'SEA',
          city: 'Seattle',
          state: 'WA',
        },
        {
          name: 'Minneapolis-Saint Paul International Airport',
          abbreviation: 'MSP',
          city: 'Bloomington',
          state: 'MN',
        },
        {
          name: 'Louis Armstrong New Orleans International Airport',
          abbreviation: 'MSY',
          city: 'New Orleans',
          state: 'LA',
        },
        {
          name: `Chicago O'hare International Airport`,
          abbreviation: 'ORD',
          city: 'Chicago',
          state: 'IL',
        },
        {
          name: `Miami International Airport`,
          abbreviation: 'MIA',
          city: 'Miami',
          state: 'FL',
        }
      ]
      

      This is an array of objects consisting of a few airports in the United States. In this application, you are going to iterate through this data to generate cards consisting of the name, abbreviation, city, and state properties. When the user clicks on a card, you will execute a dispatch method, which will add that airport to your Vuex state as a favorite airport.

      Save data/airports.js and return to the terminal.

      When you’ve completed that step, create a single-file component (SFC) with the name AirportCard.vue. This file will live in the components directory of your project. This component will contain all the styles and logic for the airport card. In your terminal, create the .vue file using the touch command:

      • touch src/components/AirportCard.vue

      Open AirportCard.vue in your text editor and add the following:

      favorite-airports/src/components/AirportCard.vue

      <template>
        <div class="airport">
          <p>{{ airport.abbreviation }}</p>
          <p>{{ airport.name }}</p>
          <p>{{ airport.city }}, {{ airport.state }}</p>
        </div>
      </template>
      
      <script>
      export default {
        props: {
          airport: {
            type: Object,
            required: true
          }
        }
      }
      </script>
      
      <style scoped>
      .airport {
        border: 3px solid;
        border-radius: .5rem;
        padding: 1rem;
        margin-bottom: 1rem;
      }
      
      .airport p:first-child {
        font-weight: bold;
        font-size: 2.5rem;
        margin: 1rem 0;
      }
      
      .airport p:last-child {
        font-style: italic;
        font-size: .8rem;
      }
      </style>
      

      You may notice that there is some CSS included in this code snippet. In the AirportCard.vue component, the wrapper <div> contains the class of airport. This CSS adds some styling to the generated HTML by adding borders to give each airport the appearance of a “card”. The :first-child and :last-child are pseudo selectors that apply different styling to the first and last p tags in the HTML inside the div with the class of airport. In addition to that, you may also notice that this component contains a prop, which in Vue.js is a way to pass data down from a parent component to a child component.

      Save and exit from the file.

      Before wrapping up the setup, replace the existing App.vue component with the following code:

      favorite-airports/src/App.vue

      <template>
        <div class="wrapper">
          <div v-for="airport in airports" :key="airport.abbreviation">
            <airport-card :airport="airport" />
          </div>
        </div>
      </template>
      
      <script>
      import { ref } from 'vue'
      import allAirports from '@/data/airports.js'
      import AirportCard from '@/components/AirportCard.vue'
      
      export default {
        components: {
          AirportCard
        },
        setup() {
          const airports = ref(allAirports)
          return { airports }
        }
      }
      </script>
      
      <style>
      #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
      }
      
      .wrapper {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        grid-column-gap: 1rem;
        max-width: 960px;
        margin: 0 auto;
      }
      
      p,
      h3 {
        grid-column: span 3;
      }
      </style>
      

      This code contains a v-for loop that iterates through the airports.js data and renders a series of AirportCards.vue components with airport data passed in via the prop :airport. Save this code and return to the command line.

      With the project set up, run the local development server using the npm run serve command in your terminal:

      This will start a server on your localhost, usually at port 8080. Open your web browser of choice and visit localhost:8080to see the following:

      A list of cards rendered in Vue using the v-for directive.

      Now that your example application is set up, in the next step you are going to install the Vuex library and create a store. This store is a collection of a number of different Vuex items including: state, mutations, actions, and getters. To illustrate this, you will be executing dispatch methods, which will add an airport to a favorites section of your app.

      Step 2 — Installing Vuex

      When working on web-based applications, you will often work with state. State is a collection of data at a given time. This state can be changed with user interactions via dispatch and commit methods. When the user modifies data, a dispatch event is executed, which passes data to a mutation and updates the state object.

      There are a few ways to approach updating state. Some developers skip actions and go straight to mutatations. However, for the sake of this tutorial, you will always execute an action that in turn calls a mutation. This way you can have multiple mutations inside of an action. The cardinal rule of Vuex is that mutations have one job and one job only: update the store. Actions can do a number of different things including combining data, fetching data, and running JavaScript logic.

      In addition to actions, there are also getters. A getter is a way to combine multiple state values into a single value. If you are familiar with computed properties in Vue.js, getters can be thought of as state-specific computed properties.

      With the Vuex terminology covered, start installing and integrating Vuex. Open your terminal and run the following command:

      • npm install vuex@next --save

      This command will install the version of Vuex that is the most compatible with Vue.js 3.x and saves it in your package.json file. Next, create a directory and an index file for your store. You will use the mkdir command to make a directory and touch to create a new file:

      • mkdir src/store
      • touch src/store/index.js

      Open your text editor and in your store/index.js file, initialize your Vuex store. To do this, you need to leverage the createStore function from Vuex:

      airport-favorites/src/store/index.js

      import { createStore } from 'vuex'
      
      export default createStore({
      
      })
      

      You also export this, as you will later import it into your main.js file.

      At this point, you have your Vuex store set up, but the application is not yet aware of it or how to use it. To fully initialize the store, import it into your main.js file. In your text editor, open the src/main.js file.

      Immediately after createApp(App), chain the use method and pass into the store that you are importing, as shown in the following highlighted code:

      favorite-airports/src/main.js

      import { createApp } from 'vue'
      import App from './App.vue'
      import store from './store'
      
      createApp(App).use(store).mount('#app')
      

      Once you’ve chained the use method, save this file. The use method tells the Vue application which code to bundle together when the application is built. In this case, you are telling Vue to “use” or bundle the Vuex store.

      Before you move on to the next section, add a state value into your store and reference it in the App.vue file. Open your store/index.js file and add the following objects and values:

      favorite-airports/src/store/index.js

      import { createStore } from 'vuex'
      
      export default createStore({
      state: {
          firstName: 'John',
          lastName: 'Doe'
        },
      mutations: {
      
      },
      actions: {
      
      },
      getters: {
      
      }
      })
      

      These properties reflect the type of data the store holds: state for state (global data), mutations (commits that mutate data), actions (dispatches that call mutations), and getters (store computed properties).

      Save store/index.js, then open your App.vue file in your text editor and add the following:

      favorite-airports/src/App.vue

      <template>
        <div class="wrapper">
          <p>{{ $store.state.firstName }} {{ $store.state.lastName }}</p>
          <div v-for="airport in airports" :key="airport.abbreviation">
            <airport-card :airport="airport" />
          </div>
        </div>
      </template>
      ...
      

      The $store in this case is the global store that you initialized in the main.js file. If you were to log this.$store into the console, you would see the store object. From there, the code accesses the property you want to display via dot notation.

      Save App.vue then open your web browser. Above the airport cards, you will see the first and last name that you saved to your Vuex store. These are the default values of firstName and lastName, respectively.

      Data from the Vuex store displayed in the view.

      In this step, you installed Vuex and created a Vuex store. You added some default store data and displayed it in the view with the $store object using dot notion. In the next step, you will be updating your Vuex store via actions and mutations, and you will get combined data with getters.

      Step 3 — Creating Actions, Mutations, and Getters

      In Step 2, you installed Vuex manually and integrated it into your project. In this step you still have the first and last name rendered in your browser, but you will create a Vuex getter to render the data as one string. As mentioned before, you can think of Vuex getters as computed properties for your Vuex store.

      To create a getter, open your src/store/index.js file in your text editor of choice. Once open, create a property in the getters object with a function as its value. The name of the property is how you will access the getter later.

      Add the following highlighted code:

      favorite-airports/src/store/index.js

      import { createStore } from 'vuex'
      
      export default createStore({
        state: {
          firstName: 'John',
          lastName: 'Doe'
        },
        ...
        getters: {
          fullName: function () {
      
          }
        }
      })
      

      In this case, you will use the function to combine the first and last names and store the resulting property as fullName. Inside the function, you will need to pass in the state object that is inside your Vuex store. From there, return a string with the first and last name interpolated:

      favorite-airports/src/store/index.js

      import { createStore } from 'vuex'
      
      export default createStore({
        state: {
          firstName: 'John',
          lastName: 'Doe'
        },
        ...
        getters: {
          fullName: function (state) {
            return `${state.firstName} ${state.lastName}`
          }
        }
      })
      

      You are using template literals here to put the firstName and lastName into one string.

      Save this file, then move back to App.vue. In this file, remove the first and last values and replace them with the getter:

      favorite-airports/src/App.vue

      <template>
        <div class="wrapper">
          <p>{{ $store.getters.fullName }}</p>
          <div v-for="airport in airports" :key="airport.abbreviation">
            <airport-card :airport="airport" />
          </div>
        </div>
      </template>
      ...
      

      Once you make this change and save the file, your browser will hot reload. You will see the first and last name in your browser as before, but now you are leveraging the getter. If you change one of the names in your Vuex store, the getter will be updated automatically.

      Moving on from getters, there are actions. As mentioned in the last step, for the sake of this tutorial, you will always use an action rather than mutating the data directly.

      In this project, you will add an airport’s data to a “favorites” list when a user clicks on the card. You are going to first get the action and mutation created, then later assign it to a click event using the v-on directive.

      To create an action, open the src/store/index.js file in your text editor. In the actions section of the store, create a function. Like the getter, the function name will be how you reference the action later. Name this function addToFavorites:

      favorite-airports/src/store/index.js

      import { createStore } from 'vuex'
      
      export default createStore({
        state: {
          firstName: 'John',
          lastName: 'Doe',
          favorites: [] // will store favorites here
        },
        mutations: {
      
        },
        actions: {
          addToFavorites() {
      
          }
        },
        getters: {
          fullName: function (state) {
            return `${state.firstName} ${state.lastName}`
          }
      }
      })
      

      An action accepts two arguments: the context, or the Vue app itself, and the payload, or the data that you want to add to the store. The context has a commit method associated with it that you will use to call a mutation you will make later:

      favorite-airports/src/store/index.js

      import { createStore } from 'vuex'
      
      export default createStore({
        state: {
          firstName: 'John',
          lastName: 'Doe',
          favorites: []
        },
        mutations: {
      
        },
        actions: {
          addToFavorites(context, payload) {
            context.commit('UPDATE_FAVORITES', payload)
          }
        },
        getters: {
          fullName: function (state) {
            return `${state.firstName} ${state.lastName}`
          }
       }
      })
      

      The commit method also accepts two arguments: the name of the mutation to call and the payload or the data that the mutation will replace the state with.

      In this code, you are naming the mutation UPDATE_FAVORITES. Mutation names should be agnostic and not named after a specific action. For example, a mutation like ADD_FAVORITE and REMOVE_FAVORITE implies a logic, like removing or adding a piece of data. This is not ideal, since mutations should have one job and one job only: update the state. To differentiate between adding and removing data, you could have two different actions that remove or add a favorite airport from the array, which then execute a single mutation called UPDATE_FAVORITES that updates the array with whatever was passed in. Minimizing the amount of mutations you have in your store will help make your Vuex store easier to manage as it grows larger in complexity and size.

      Next, add some logic to this action. When you add an airport as a “favorite”, you will add that payload (the airport data) to the existing array. To do that, you can use the push method in JavaScript:

      favorite-airports/src/store/index.js

      import { createStore } from 'vuex'
      
      export default createStore({
        state: {
          firstName: 'John',
          lastName: 'Doe',
          favorites: []
        },
        mutations: {
      
        },
        actions: {
          addToFavorites(context, payload) {
            const favorites = context.state.favorites
            favorites.push(payload)
            context.commit('UPDATE_FAVORITES', favorites)
          }
        },
        getters: {
          fullName: function (state) {
            return `${state.firstName} ${state.lastName}`
          }
       }
      })
      

      At this point, your action is set up to to add the payload to your favorites array then call a mutation with the mutated array as the new data. Next, you will define the UPDATE_FAVORITES mutation. Add the following code to set the favorites array as a new array:

      favorite-airports/src/store/index.js

      import { createStore } from 'vuex'
      
      export default createStore({
        state: {
            firstName: 'John',
          lastName: 'Doe',
          favorites: []
          },
        mutations: {
          UPDATE_FAVORITES(state, payload) {
            state.favorites = payload
          }
        },
        actions: {
          addToFavorites(context, payload) {
            const favorites = context.state.favorites
            favorites.push(payload)
            context.commit('UPDATE_FAVORITES', favorites)
          }
        },
        getters: {
          fullName: function (state) {
            return `${state.firstName} ${state.lastName}`
          }
       }
      })
      

      Now that you have your action and your mutation, you can save this file.

      To execute this action, you can call a dispatch event when the user clicks on a card. You will do this with the v-on directive.

      Open the App.vue file in your text editor. On the <airport-card /> component, add the v-on directive shorthand syntax (@) with the event being click:

      favorite-airports/src/App.vue

      <template>
        <div class="wrapper">
          <p>{{ $store.getters.fullName }}</p>
          <div v-for="airport in airports" :key="airport.abbreviation">
            <airport-card :airport="airport" @click="$store.dispatch('addToFavorites', airport)" />
          </div>
          <h2 v-if="$store.state.favorites.length">Favorites</h2>
          <div v-for="airport in $store.state.favorites" :key="airport.abbreviation">
            <airport-card :airport="airport"  />
          </div>
        </div>
      </template>
      ...
      

      The dispatch function accepts two arguments: the action name and the payload data that you are sending to the action.

      Save this file and open it in your browser. Now, when you click on an airport card, the action will call the mutation that updates the state and adds the airport to the favorites property.

      A favorite airport added to the favorite airports section after a Vuex mutation was executed.

      In this step, you expanded on the Vuex store that you created earlier. You created an action that copies an array and pushes a new item to that array. That action called a mutation that in turn updated the state. In addition to that, you learned about getters and how they can be leveraged to create new properties by combining or modifying read-only values in the Vuex store.

      In the final step, you will implement Vuex modules. Modules are a great way to break up your Vuex store into smaller Vuex stores. This is useful as your Vuex store growers larger in size and complexity.

      Step 4 — Composing Vuex Modules

      Modules are smaller Vuex stores that are combined into a single Vuex store. This is similar to how multiple Vue.js components are imported into a single .vue file, such as App.vue. In this step, you are going to separate this Vuex store into two separate modules. One module will be for a user state, and the other will be specific to airport state, actions, and mutations.

      In your terminal, cd into the store directory and use the touch command to create two separate files.

      • touch src/store/user.module.js
      • touch src/store/airports.module.js

      Inside of the user.module.js file, create an object that will be exported by default by adding the following code:

      favorite-airports/src/store/user.module.js

      export default {
        namespaced: true
      }
      

      You are also adding the property namespaced with its value as true. The namespace property will make it so you can reference the module name while accessing a property with dot notation later.

      Inside of this object, you will add the state and getter information that is associated with a user:

      favorite-airports/src/store/user.module.js

      export default {
        namespaced: true,
        state: {
          firstName: 'John',
          lastName: 'Doe'
        },
        getters: {
          fullName: function (state) {
            return `${state.firstName} ${state.lastName}`
          }
        }
      }
      

      The user module contains everything that you need for user information. Save and exit from the file.

      Go ahead and do the same thing in the airports.module.js file. Open the airports.module.js file in your text editor and add the following:

      favorite-airports/src/store/airports.module.js

      export default {
        state: {
          favorites: []
        },
        mutations: {
          UPDATE_FAVORITES(state, payload) {
            state.favorites = payload
          }
        },
        actions: {
          addToFavorites(context, payload) {
            const favorites = context.state.favorites
            favorites.push(payload)
            context.commit('UPDATE_FAVORITES', favorites)
          }
        },
      }
      

      Now that you have put the airport-related mutations, actions, and state, you can save your airports.module.js.

      Next, import these two files into the main store/index.js file. In your text editor, open the store/index.js file and remove the state, mutations, actions, and getters properties. Your file will resemble the following snippet:

      favorite-airports/src/store/index.js

      import { createStore } from 'vuex'
      
      export default createStore({
      
      })
      

      To register modules, you will need to import them into this index.js file with the following highlighted code:

      favorite-airports/src/store/index.js

      import { createStore } from 'vuex'
      import UserModule from './user.module.js'
      import AirportsModule from './airports.module.js'
      
      export default createStore({
      
      })
      

      From here, you will need to have a property called modules with an object as its value. The property names inside of this object will be the names of the Vuex modules. The value of the module name is the imported module itself:

      favorite-airports/src/store/index.js

      import { createStore } from 'vuex'
      import UserModule from './user.module.js'
      import AirportsModule from './airports.module.js'
      
      export default createStore({
      modules: {
        user: UserModule,
        airports: AirportsModule
      }
      })
      

      Once you save this file, your modules have now been registered and combined into your single Vuex store. Save store/index.js, then open the App.vue file and update it to reference the newly created modules:

      favorite-airports/src/App.vue

      <template>
        <div class="wrapper">
          <p>{{ $store.getters['user/fullName'] }}</p>
          <div v-for="airport in airports" :key="airport.abbreviation">
            <airport-card :airport="airport" @click="$store.dispatch('addToFavorites', airport)" />
          </div>
          <h2 v-if="$store.state.airports.favorites.length">Favorites</h2>
          <div v-for="airport in $store.state.airports.favorites" :key="airport.abbreviation">
            <airport-card :airport="airport"  />
          </div>
        </div>
      </template>
      ...
      

      Now you have a modular version of your Vuex setup.

      In this step, you segmented your existing Vuex store into smaller chunks called modules. These modules are a great way to group related store properties into a smaller Vuex store. You also updated your App.vue to reference the state and dispatch events in each module.

      Conclusion

      At a high level, state management is all about updating data. In this setup, the data in the state is global throughout your entire application and acts as a single source of truth, which can only be updated with explicit functions in the form of actions and mutations. In this tutorial, you ran through examples of state, mutations, actions, and getters and saw how each of these properties have their own purpose in the update cycle.

      To learn more about Vuex, actions, mutations, and modules, review the official Vuex documentation written by the Vue.js Core Team. For more tutorials on Vue, check out the How To Develop Websites with Vue.js series page.



      Source link

      How To Build a Shopping Cart with Vue 3 and Vuex


      The author selected the Open Source Initiative to receive a donation as part of the Write for DOnations program.

      Introduction

      Vue.js is a performant and progressive Javascript framework. It is a popular framework on GitHub and has an active and helpful community.

      In order to show the capabilities of the Vue web framework, this tutorial will lead you through building the shopping cart of an e-commerce app. This app will store product information and hold the products that the customer wants to buy for checkout later. To store the information, you will explore a widely used state management library for Vue.js: Vuex. This will allow the shopping cart application to persist data to a server. You will also handle asynchronous task management using Vuex.

      Once you finish the tutorial, you will have a functioning shopping cart application like the following:

      Animation of user adding and deleting products from the shopping cart application

      Prerequisites

      Step 1 — Setting Up the Application with Vue CLI

      As of version 4.5.0, Vue CLI now provides a built-in option to choose the Vue 3 preset when creating a new project. The latest version of Vue CLI allows you to use Vue 3 out of the box and to update your existing Vue 2 project to Vue 3. In this step, you will use the Vue CLI to make your project, then install the front-end dependencies.

      First, install the latest version of Vue CLI by executing the following command from the terminal:

      This will install Vue CLI globally on your system.

      Note: On some systems, installing an npm package globally can result in a permission error, which will interrupt the installation. Since it is a security best practice to avoid using sudo with npm install, you can instead resolve this by changing npm’s default directory. If you encounter an EACCES error, follow the instructions at the official npm documentation.

      Check you have the right version with this command:

      You will get output like the following:

      Output

      @vue/cli 4.5.10

      Note: If you already have the older version of Vue CLI installed globally, execute the following command from the terminal to upgrade:

      Now, you can create a new project:

      • vue create vuex-shopping-cart

      This uses the Vue CLI command vue create to make a project named vuex-shopping-cart. For more information on the Vue CLI, check out How To Generate a Vue.js Single Page App With the Vue CLI.

      Next, you will receive the following prompt:

      Output

      Vue CLI v4.5.10 ? Please pick a preset: (Use arrow keys) ❯ Default ([Vue 2] babel, eslint) Default (Vue 3 Preview) ([Vue 3] babel, eslint) Manually select features

      Choose the Manually select features option from this list.

      Next, you will encounter the following prompt to customize your Vue app:

      Output

      ... ◉ Choose Vue version ◯ Babel ◯ TypeScript ◯ Progressive Web App (PWA) Support ◉ Router ◉ Vuex ◯ CSS Pre-processors ◯ Linter / Formatter ❯◯ Unit Testing ◯ E2E Testing

      From this list, select Choose Vue version, Router, and Vuex. This will allow you to choose your version of Vue and use Vuex and Vue Router.

      Next, choose 3.x (Preview) for your version of Vue, answer no (N) to history mode, and select the option to have your configurations In dedicated config file. Finally, answer N to avoid saving the setup for a future project.

      At this point, Vue will create your application.

      After the project creation, move into the folder using the command:

      To start, you’ll install Bulma, a free, open-source CSS framework based on Flexbox. Add Bulma to your project by running the following command:

      To use Bulma CSS in your project, open up your app’s entry point, the main.js file:

      Then add the following highlighted import line:

      vuex-shopping-cart/src/main.js

      import { createApp } from 'vue'
      import App from './App.vue'
      import router from './router'
      import store from './store'
      import './../node_modules/bulma/css/bulma.css'
      
      createApp(App).use(store).use(router).mount('#app')
      

      Save and close the file.

      In this app, you’ll use the Axios module to make requests to your server. Add the Axios module by running the following command:

      Now, run the app to make sure it is working:

      Navigate to http://localhost:8080 in your browser of choice. You will find the Vue app welcome page:

      Default Vue page when running your app in development mode

      Once you have confirmed that Vue is working, stop your server with CTRL+C.

      In this step, you globally installed Vue CLI in your computer, created a Vue project, installed the required npm packages Axios and Bulma, and imported Bulma to the project in the main.js file. Next, you will set up a back-end API to store data for your app.

      Step 2 — Setting Up the Backend

      In this step, you will create a separate backend to work with your Vue project. This will be in a different project folder from your front-end Vue application.

      First, move out of your Vue directory:

      Make a separate directory named cart-backend:

      Once you have your back-end folder, make it your working directory:

      You will get started by initializing the project with the necessary file. Create the file structure of your app with the following commands:

      • touch server.js
      • touch server-cart-data.json
      • touch server-product-data.json

      You use the touch command here to create empty files. The server.js file will hold your Node.js server, and the JSON will hold data for the shop’s products and the user’s shopping cart.

      Now run the following command to create a package.json file:

      For more information on npm and Node, check out our How To Code in Node.js series.

      Install these back-end dependencies into your Node project:

      • npm install concurrently express body-parser

      Express is a Node framework for web applications, which will provide useful abstractions for handling API requests. Concurrently will be used to run the Express back-end server and the Vue.js development server simulteneously. Finally, body-parser is an Express middleware that will parse requests to your API.

      Next, open a server.js file in the root of your application:

      Then add the following code:

      cart-backend/server.js

      const express = require('express');
      const bodyParser = require('body-parser');
      const fs = require('fs');
      const path = require('path');
      
      const app = express();
      const PRODUCT_DATA_FILE = path.join(__dirname, 'server-product-data.json');
      const CART_DATA_FILE = path.join(__dirname, 'server-cart-data.json');
      
      app.set('port', (process.env.PORT || 3000));
      app.use(bodyParser.json());
      app.use(bodyParser.urlencoded({ extended: true }));
      app.use((req, res, next) => {
        res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
        res.setHeader('Pragma', 'no-cache');
        res.setHeader('Expires', '0');
        next();
      });
      
      app.listen(app.get('port'), () => {
        console.log(`Find the server at: http://localhost:${app.get('port')}/`);
      });
      

      This snippet first adds the Node modules to your backend, including the fs module to write to your filesystem and the path module to make defining filepaths easier. You then initialize the Express app and save references to your JSON files as PRODUCT_DATA_FILE and CART_DATA_FILE. These will be used as data repositories. Finally, you created an Express server, set the port, created a middleware to set the response headers, and set the server to listen on your port. For more information on Express, see the official Express documentation.

      The setHeader method sets the header of the HTTP responses. In this case, you are using Cache-Control to direct the caching of your app. For more information on this, check out the Mozilla Developer Network article on Cache-Control.

      Next, you will create an API endpoint that your frontend will query to add an item to the shopping cart. To do this, you will use app.post to listen for an HTTP POST request.

      Add the following code to server.js just after the last app.use() middleware:

      cart-backend/server.js

      ...
      app.use((req, res, next) => {
        res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
        res.setHeader('Pragma', 'no-cache');
        res.setHeader('Expires', '0');
        next();
      });
      
      app.post('/cart', (req, res) => {
          fs.readFile(CART_DATA_FILE, (err, data) => {
            const cartProducts = JSON.parse(data);
            const newCartProduct = { 
              id: req.body.id,
              title: req.body.title,
              description: req.body.description,
              price: req.body.price,
              image_tag: req.body.image_tag, 
              quantity: 1 
            };
            let cartProductExists = false;
            cartProducts.map((cartProduct) => {
              if (cartProduct.id === newCartProduct.id) {
                cartProduct.quantity++;
                cartProductExists = true;
              }
            });
            if (!cartProductExists) cartProducts.push(newCartProduct);
            fs.writeFile(CART_DATA_FILE, JSON.stringify(cartProducts, null, 4), () => {
              res.setHeader('Cache-Control', 'no-cache');
              res.json(cartProducts);
            });
          });
        });
      
      app.listen(app.get('port'), () => {
        console.log(`Find the server at: http://localhost:${app.get('port')}/`);
      });
      

      This code receives the request object containing the cart items from the frontend and stores them in the server-cart-data.json file in the root of your project. Products here are JavaScript objects with id, title, description, price, image_tag, and quantity properties. The code also checks if the cart already exists to ensure that requests for a repeated product only increase the quantity.

      Now, add code to create an API endpoint to remove an item from the shopping cart. This time, you will use app.delete to listen for an HTTP DELETE request.

      Add the following code to server.js just after the previous endpoint:

      cart-backend/server.js

      ...
            fs.writeFile(CART_DATA_FILE, JSON.stringify(cartProducts, null, 4), () => {
              res.setHeader('Cache-Control', 'no-cache');
              res.json(cartProducts);
            });
          });
        });
      
      app.delete('/cart/delete', (req, res) => {
        fs.readFile(CART_DATA_FILE, (err, data) => {
          let cartProducts = JSON.parse(data);
          cartProducts.map((cartProduct) => {
            if (cartProduct.id === req.body.id && cartProduct.quantity > 1) {
              cartProduct.quantity--;
            } else if (cartProduct.id === req.body.id && cartProduct.quantity === 1) {
              const cartIndexToRemove = cartProducts.findIndex(cartProduct => cartProduct.id === req.body.id);
              cartProducts.splice(cartIndexToRemove, 1);
            }
          });
          fs.writeFile(CART_DATA_FILE, JSON.stringify(cartProducts, null, 4), () => {
            res.setHeader('Cache-Control', 'no-cache');
            res.json(cartProducts);
          });
        });
      });
      
      app.listen(app.get('port'), () => {
        console.log(`Find the server at: http://localhost:${app.get('port')}/`); // eslint-disable-line no-console
      });
      

      This code receives the request object containing the item to be removed from the cart and checks the server-cart-data.json file for this item via its id. If it exists and the quantity is greater than one, then the quantity of the item in the cart is deducted. Otherwise, if the item’s quantity is less than 1, it will be removed from the cart and the remaining items will be stored in the server-cart-data.json file.

      To give your user additional functionality, you can now create an API endpoint to remove all items from the shopping cart. This will also listen for a DELETE request.

      Add the following highlighted code to server.js after the previous endpoint:

      cart-backend/server.js

      ...
          fs.writeFile(CART_DATA_FILE, JSON.stringify(cartProducts, null, 4), () => {
            res.setHeader('Cache-Control', 'no-cache');
            res.json(cartProducts);
          });
        });
      });
      
      app.delete('/cart/delete/all', (req, res) => {
        fs.readFile(CART_DATA_FILE, () => {
          let emptyCart = [];
          fs.writeFile(CART_DATA_FILE, JSON.stringify(emptyCart, null, 4), () => {
            res.json(emptyCart);
          });
        });
      });
      
      app.listen(app.get('port'), () => {
        console.log(`Find the server at: http://localhost:${app.get('port')}/`); // eslint-disable-line no-console
      });
      

      This code is responsible for removing all the items from the cart by returning an empty array.

      Next, you will create an API endpoint to retrieve all the products from the product storage. This will use app.get to listen for a GET request.

      Add the following code to server.js after the previous endpoint:

      cart-backend/server.js

      ...
      app.delete('/cart/delete/all', (req, res) => {
        fs.readFile(CART_DATA_FILE, () => {
          let emptyCart = [];
          fs.writeFile(CART_DATA_FILE, JSON.stringify(emptyCart, null, 4), () => {
            res.json(emptyCart);
          });
        });
      });
      
      app.get("https://www.digitalocean.com/products", (req, res) => {
        fs.readFile(PRODUCT_DATA_FILE, (err, data) => {
          res.setHeader('Cache-Control', 'no-cache');
          res.json(JSON.parse(data));
        });
      });
      ...
      

      This code uses the file system’s native readFile method to fetch all the data in the server-product-data.json file and returns them in JSON format.

      Finally, you will create an API endpoint to retrieve all the items from the cart storage:

      cart-backend/server.js

      ...
      app.get("https://www.digitalocean.com/products", (req, res) => {
        fs.readFile(PRODUCT_DATA_FILE, (err, data) => {
          res.setHeader('Cache-Control', 'no-cache');
          res.json(JSON.parse(data));
        });
      });
      
      app.get('/cart', (req, res) => {
        fs.readFile(CART_DATA_FILE, (err, data) => {
          res.setHeader('Cache-Control', 'no-cache');
          res.json(JSON.parse(data));
        });
      });
      ...
      

      Similarly, this code uses the file system’s native readFile method to fetch all the data in the server-cart-data.json file and returns them in JSON format.

      Save and close the server.js file.

      Next, you will add some mock data to your JSON files for testing purposes.

      Open up the server-cart-data.json file you created earlier:

      • nano server-cart-data.json

      Add the following array of product objects:

      cart-backend/server-cart-data.json

      [
          {
              "id": 2,
              "title": "MIKANO Engine",
              "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis! ",
              "price": 650.9,
              "image_tag": "diesel-engine.png",
              "quantity": 1
          },
          {
              "id": 3,
              "title": "SEFANG Engine",
              "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis!",
              "price": 619.9,
              "image_tag": "sefang-engine.png",
              "quantity": 1
          }
      ]
      

      This shows two engines that will start out in the user’s shopping cart.

      Save and close the file.

      Now open the server-product-data.json file:

      • nano server-product-data.json

      Add the following data in server-product-data.json file:

      cart-backend/server-product-data.json

      [
          {
            "id": 1,
            "title": "CAT Engine",
            "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis!",
            "product_type": "power set/diesel engine",
            "image_tag": "CAT-engine.png",
            "created_at": 2020,
            "owner": "Colton",
            "owner_photo": "image-colton.jpg",
            "email": "colt@gmail.com",
            "price": 719.9
          },
          {
            "id": 2,
            "title": "MIKANO Engine",
            "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis! ",
            "product_type": "power set/diesel engine",
            "image_tag": "diesel-engine.png",
            "created_at": 2020,
            "owner": "Colton",
            "owner_photo": "image-colton.jpg",
            "email": "colt@gmail.com",
            "price": 650.9
          },
          {
            "id": 3,
            "title": "SEFANG Engine",
            "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis!",
            "product_type": "power set/diesel engine",
            "image_tag": "sefang-engine.png",
            "created_at": 2017,
            "owner": "Anne",
            "owner_photo": "image-anne.jpg",
            "email": "anne@gmail.com",
            "price": 619.9
          },
          {
            "id": 4,
            "title": "CAT Engine",
            "description": "Lorem ipsum dolor sit amet, consectetur  dignissimos suscipit voluptatibus distinctio, error nostrum expedita omnis ipsum sit inventore aliquam sunt quam quis!",
            "product_type": "power set/diesel engine",
            "image_tag": "lawn-mower.png",
            "created_at": 2017,
            "owner": "Irene",
            "owner_photo": "image-irene.jpg",
            "email": "irene@gmail.com",
            "price": 319.9
          }
      
        ]
      

      This will hold all the possible products that the user can put in their cart.

      Save and close the file.

      Finally, execute this command to run the server:

      You will receive something like this on your terminal:

      Output

      Find the server at: http://localhost:3000/

      Leave this server running in this window.

      Finally, you will set up a proxy server in your Vue app. This will enable the connection between the frontend and backend.

      Go to the root directory of your Vue app:

      In the terminal, run this command to create a Vue configuration file:

      Then, add this code:

      vuex-shopping-cart/vue.config.js

      module.exports = {
        devServer: {
          proxy: {
            '/api': {
              target: 'http://localhost:3000/',
              changeOrigin: true,
              pathRewrite: {
                '^/api': ''
              }
            }
          }
        }
      }
      

      This will send requests from your frontend to your back-end server at http://localhost:3000/. For more information on proxy configuration, review the Vue devServer.proxy documentation.

      Save and close the file.

      In this step, you wrote server-side code that will handle API endpoints for your shopping cart. You started by creating the file structure and ended with adding necessary code in the server.js file and data in your JSON files. Next, you will set up the state storage for your frontend.

      Step 3 — Setting Up State Management with Vuex

      In Vuex, the store is where the state of the application is kept. The application state can only be updated by dispatching actions within a component that will then trigger mutations in the store. The Vuex store is made up of the state, mutations, actions, and getters.

      In this step, you’re going to build each of these pieces, after which you will couple everything together into a Vuex store.

      State

      Now you will create a place to store state for your application.

      The store folder in the root directory src of your project is automatically created at the time of the project setup. Locate the store folder in the src directory of your project then create a new folder named modules:

      Inside this folder, create the product and cart folders:

      • mkdir src/store/modules/product
      • mkdir src/store/modules/cart

      These will hold all the state files for your product inventory and your user’s cart. You will build these two files up at the same time, each open in a separate terminal. This way, you will be able to compare your mutations, getters, and actions side-by-side.

      Finally, open an index.js file in the product folder:

      • nano src/store/modules/product/index.js

      Add the following code to create a state object containing your productItems:

      vuex-shopping-cart/src/store/modules/product/index.js

      import axios from 'axios';
      const state = {
        productItems: [] 
      }
      

      Save the file and keep it open.

      Similarly, in a new terminal, add an index.js file to the cart directory with the following:

      • nano src/store/modules/cart/index.js

      Then add code for the cartItems:

      vuex-shopping-cart/src/store/modules/cart/index.js

      import axios from 'axios';
      const state = {
        cartItems: []
      }
      

      Save this file, but keep it open.

      In these code snippets, you imported the Axios module and set the state. The state is a store object that holds the application-level data that needs to be shared between components.

      Now that you’ve set the states, head over to mutations.

      Mutations

      Mutations are methods that modify the store state. They usually consist of a string type and a handler that accepts the state and payload as parameters.

      You will now create all the mutations for your application.

      Add the following code in the product/index.js file just after the state section:

      vuex-shopping-cart/src/store/modules/product/index.js

      ...
      const mutations = {
        UPDATE_PRODUCT_ITEMS (state, payload) {
          state.productItems = payload;
        }
      }
      

      This creates a mutations object that holds an UPDATE_PRODUCT_ITEMS method that sets the productItems array to the payload value.

      Similarly, add the following code in the cart/index.js file just after the state section:

      vuex-shopping-cart/src/store/modules/cart/index.js

      ...
      const mutations = {
        UPDATE_CART_ITEMS (state, payload) {
          state.cartItems = payload;
        }
      }
      

      This creates a similar UPDATE_CART_ITEMS for your user’s shopping cart. Note that this follows the Flux architecture style of making references to mutations in capital letters.

      Actions

      Actions are methods that will handle mutations, so that mutations are insulated from the rest of your application code.

      In product/index.js, create an actions object with all the actions for your application:

      vuex-shopping-cart/src/store/modules/product/index.js

      ...
      const actions = {
        getProductItems ({ commit }) {
          axios.get(`/api/products`).then((response) => {
            commit('UPDATE_PRODUCT_ITEMS', response.data)
          });
        }
      }
      

      Here the getProductItems method sends an asynchronous GET request to the server using the Axios package that you installed earlier. When the request is successful, the UPDATE_PRODUCT_ITEMS mutation is called with the response data as the payload.

      Next, add the following actions object to cart/index.js:

      vuex-shopping-cart/src/store/modules/cart/index.js

      ...
      const actions = {
        getCartItems ({ commit }) {
          axios.get('/api/cart').then((response) => {
            commit('UPDATE_CART_ITEMS', response.data)
          });
        },
        addCartItem ({ commit }, cartItem) {
          axios.post('/api/cart', cartItem).then((response) => {
            commit('UPDATE_CART_ITEMS', response.data)
          });
        },
        removeCartItem ({ commit }, cartItem) {
          axios.delete('/api/cart/delete', cartItem).then((response) => {
            commit('UPDATE_CART_ITEMS', response.data)
          });
        },
        removeAllCartItems ({ commit }) {
          axios.delete('/api/cart/delete/all').then((response) => {
            commit('UPDATE_CART_ITEMS', response.data)
          });
        }
      }
      
      

      In this file, you create the getCartItems method, which sends an asynchronous GET request to the server. When the request is successful, the UPDATE_CART_ITEMS mutation is called with the response data as the payload. The same happens with the removeAllCartItems method, although it makes a DELETE request to the server. The removeCartItem and addCartItem methods receives the cartItem object as a parameter for making a DELETE or POST request. After a successful request, the UPDATE_CART_ITEMS mutation is called with the response data as the payload.

      You used ES6 destructuring to decouple the commit method from the Vuex context object. This is similar to using context.commit.

      Getters

      Getters are to an application store what computed properties are to a component. They return computed information from store state methods that involve receiving computed state data.

      Next, create a getters object to get all the information for the product module:

      vuex-shopping-cart/src/store/modules/product/index.js

      ...
      const getters = {
        productItems: state => state.productItems,
        productItemById: (state) => (id) => {
          return state.productItems.find(productItem => productItem.id === id)
        }
      }
      

      Here, you made a method productItems that returns the list of product items in the state, followed by productItemById, a higher order function that returns a single product by its id.

      Next, create a getters object in cart/index.js:

      vuex-shopping-cart/src/store/modules/cart/index.js

      ...
      const getters = {
        cartItems: state => state.cartItems,
        cartTotal: state => {
          return state.cartItems.reduce((acc, cartItem) => {
            return (cartItem.quantity * cartItem.price) + acc;
          }, 0).toFixed(2);
        },
        cartQuantity: state => {
          return state.cartItems.reduce((acc, cartItem) => {
            return cartItem.quantity + acc;
          }, 0);
        }
      }
      

      In this snippet, you made the cartItems method, which returns the list of cart items in the state, followed by cartTotal, which returns the computed value of the total amount of cart items available for checkout. Finally, you made the cartQuantity method, which retuns the quantity of items in the cart.

      Exporting the Module

      The final part of the product and cart modules will export the state, mutations, actions, and getters objects so that other parts of the application can access them.

      In product/index.js, add the following code at the end of the file:

      vuex-shopping-cart/src/store/modules/product/index.js

      ...
      const productModule = {
        state,
        mutations,
        actions,
        getters
      }
      
      export default productModule;
      

      This collects all your state objects into the productModule object, then exports it as a module.

      Save product/index.js and close the file.

      Next, add similar code to cart/index.js:

      vuex-shopping-cart/src/store/modules/product/index.js

      ...
          const cartModule = {
        state,
        mutations,
        actions,
        getters
      }
      export default cartModule;
      

      This exports the module as cartModule.

      Setting up the Store

      With the state, mutations, actions, and getters all set up, the final part of integrating Vuex into your application is creating the store. Here you will harness the Vuex modules to split your application store into two manageable fragments.

      To create your store, open up the index.js file in your store folder:

      Add the following highlighted lines:

      vuex-shopping-cart/src/store/index.js

      import { createStore } from 'vuex'
      import product from'./modules/product';
      import cart from './modules/cart';
      
      export default createStore({
        modules: {
          product,
          cart
        }
      })
      

      Save the file, then exit the text editor.

      You have now created the methods needed for state management and have created the store for your shopping cart. Next you will create user interface (UI) components to consume the data.

      Step 4 — Creating Interface Components

      Now that you have the store for your shopping cart set up, you can move onto making the components for the user interface (UI). This will include making some changes to the router and making front-end components for your navigation bar and list and item views of your products and your cart.

      First, you will update your vue-router setup. Remember that when you used the Vue CLI tool to scaffold your application, you chose the router option, which allowed Vue to automatically set up the router for you. Now you can re-configure the router to provide paths for Cart_List.vue and Product_List.vue, which are Vue components you will make later.

      Open up the router file with the following command:

      • nano vuex-shopping-cart/src/router/index.js

      Add the following highlighted lines:

      vuex-shopping-cart/src/router/index.js

      import { createRouter, createWebHashHistory } from 'vue-router'
      import CartList from '../components/cart/Cart_List.vue';
      import ProductList from '../components/product/Product_List.vue';
      
      const routes = [
        {
          path: '/inventory',
          component: ProductList
        },
        {
          path: '/cart',
          component: CartList
        },
        {
          path: '/',
          redirect: '/inventory'
        },
      ]
      const router = createRouter({
        history: createWebHashHistory(),
        routes
      })
      
      export default router
      

      This creates the /inventory route for your products and the /cart route for the items in your cart. It also redirects your root path / to the product view.

      Once you have added this code, save and close the file.

      Now you can set up your UI component directories. Run this command on your terminal to move to the component’s directory:

      Run this command to create three new sub-folders under the component’s directory:

      core will hold essential parts of your application, such as the navigation bar. cart and product will hold the item and list views of the shopping cart and the total inventory.

      Under the core directory, create the Navbar.vue file by running this command:

      Under the cart directory, create the files Cart_List_Item.vue and Cart_List.vue:

      • touch cart/Cart_List_Item.vue cart/Cart_List.vue

      Finally, under the product directory, create these two files:

      • touch product/Product_List_Item.vue product/Product_List.vue

      Now that the file structure has been outlined, you can move on to creating the individual components of your front-end app.

      In the navbar, the cart navigation link will display the quantity of items in your cart. You will use the Vuex mapGetters helper method to directly map store getters with component computed properties, allowing your app to get this data from the store’s getters to the Navbar component.

      Open the navbar file:

      Replace the code with the following:

      vuex-shopping-cart/src/components/core/Navbar.vue

      <template>
          <nav class="navbar" role="navigation" aria-label="main navigation">
            <div class="navbar-brand">
              <a
                role="button"
                class="navbar-burger burger"
                aria-label="menu"
                aria-expanded="false"
                data-target="navbarBasicExample"
              >
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
              </a>
            </div>
            <div id="navbarBasicExample" class="navbar-menu">
              <div class="navbar-end">
                <div class="navbar-item">
                  <div class="buttons">
                    <router-link to="/inventory" class="button is-primary">
                     <strong> Inventory</strong>
                    </router-link>
                    <router-link to="/cart"  class="button is-warning">   <p>
          Total cart items:
          <span> {{cartQuantity}}</span> </p>
                    </router-link>
                  </div>
                </div>
              </div>
            </div>
          </nav>
      </template>
      <script>
      import {mapGetters} from "vuex"
      export default {
          name: "Navbar",
          computed: {
          ...mapGetters([
            'cartQuantity'
          ])
        },
        created() {
          this.$store.dispatch("getCartItems");
        }
      }
      </script>
      

      As a Vue component, this file starts out with a template element, which holds the HTML for the component. This snippet includes multiple navbar classes that use pre-made styles from the Bulma CSS framework. For more information, check out the Bulma documentation.

      This also uses the router-link elements to connect the app to your products and cart, and uses cartQuantity as a computed property to dynamically keep track of the number of items in your cart.

      The JavaScript is held in the script element, which also handles state management and exports the component. The getCartItems action gets dispatched when the navbar component is created, updating the store state with all the cart items from the response data received from the server. After this, the store getters recompute their return values and the cartQuantity gets rendered in the template. Without dispatching the getCartItems action on the created life cycle hook, the value of cartQuantity will be 0 until the store state is modified.

      Save and close the file.

      Product_List Component

      This component is the parent to the Product_List_Item component. It will be responsible for passing down the product items as props to the Product_List_Item (child) component.

      First, open up the file:

      • nano product/Product_List.vue

      Update Product_List.vue as follows:

      vuex-shopping-cart/src/components/product/Product_List.vue

      <template>
        <div class="container is-fluid">
          <div class="tile is-ancestor">
            <div class="tile is-parent" v-for="productItem in productItems" :key="productItem.id">
            <ProductListItem :productItem="productItem"/>
            </div>
          </div>
        </div>
      </template>
      <script>
      import { mapGetters } from 'vuex';
      import Product_List_Item from './Product_List_Item'
      export default {
        name: "ProductList",
        components: {
          ProductListItem:Product_List_Item
        },
        computed: {
          ...mapGetters([
            'productItems'
          ])
        },
        created() {
          this.$store.dispatch('getProductItems');
        }
      };
      </script>
      

      Similar to the Navbar component logic discussed earlier, here the Vuex mapGetters helper method directly maps store getters with component computed properties to get the productItems data from the store. The getProductItems action gets dispatched when the ProductList component is created, updating the store state with all the product items from the response data received from the server. After this, the store getters re-computes their return values and the productItems gets rendered in the template. Without dispatching the getProductItems action on the created life cycle hook, there will be no product item displayed in the template until the store state is modified.

      Product_List_Item Component

      This component will be the direct child component to the Product_List component. It will receive the productItem data as props from its parent and render them in the template.

      Open Product_List_Item.vue:

      • nano product/Product_List_Item.vue

      Then add the following code:

      vuex-shopping-cart/src/components/product/Product_List_Item.vue

      <template>
          <div class="card">
            <div class="card-content">
              <div class="content">
                <h4>{{ productItem.title }}</h4>
                <a
                  class="button is-rounded is-pulled-left"
                  @click="addCartItem(productItem)"
                >
                  <strong>Add to Cart</strong>
                </a>
                <br />
                <p class="mt-4">
                  {{ productItem.description }}
                </p>
              </div>
              <div class="media">
                <div class="media-content">
                  <p class="title is-6">{{ productItem.owner }}</p>
                  <p class="subtitle is-7">{{ productItem.email }}</p>
                </div>
                <div class="media-right">
                  <a class="button is-primary is-light">
                    <strong>$ {{ productItem.price }}</strong>
                  </a>
                </div>
              </div>
            </div>
          </div>
      </template>
      <script>
      import {mapActions} from 'vuex'
      export default {
        name: "ProductListItem",
        props: ["productItem"],
        methods: {
          ...mapActions(["addCartItem"]),
        },
      };
      </script>
      

      In addition to the mapGetters helper function used in the previous components, Vuex also provides you with the mapActions helper function to directly map the component method to the store’s actions. In this case, you use the mapAction helper function to map the component method to the addCartItem action in the store. Now you can add items to the cart.

      Save and close the file.

      Cart_List Component

      This component is responsible for displaying all the product items added to the cart and also the removal of all the items from the cart.

      To create this component, first open the file:

      Next, update Cart_List.vue as follows:

      vuex-shopping-cart/src/components/cart/Cart_List.vue

      <template>
        <div id="cart">
          <div class="cart--header has-text-centered">
            <i class="fa fa-2x fa-shopping-cart"></i>
          </div>
          <p v-if="!cartItems.length" class="cart-empty-text has-text-centered">
            Add some items to the cart!
          </p>
          <ul>
            <li class="cart-item" v-for="cartItem in cartItems" :key="cartItem.id">
                <CartListItem :cartItem="cartItem"/>
            </li>
            <div class="notification is-success">
              <button class="delete"></button>
              <p>
                Total Quantity:
                <span class="has-text-weight-bold">{{ cartQuantity }}</span>
              </p>
            </div>
            <br>
          </ul>
          <div class="buttons">
          <button :disabled="!cartItems.length" class="button is-info">
            Checkout (<span class="has-text-weight-bold">${{ cartTotal }}</span>)
          </button>
      
       <button class="button is-danger is-outlined" @click="removeAllCartItems">
          <span>Delete All items</span>
          <span class="icon is-small">
            <i class="fas fa-times"></i>
          </span>
        </button>
             </div>
        </div>
      </template>
      <script>
      import { mapGetters, mapActions } from "vuex";
      import CartListItem from "./Cart_List_Item";
      export default {
        name: "CartList",
        components: {
          CartListItem
        },
        computed: {
          ...mapGetters(["cartItems", "cartTotal", "cartQuantity"]),
        },
        created() {
          this.$store.dispatch("getCartItems");
        },
        methods: {
          ...mapActions(["removeAllCartItems"]),
        }
      };
      </script>
      

      This code uses a v-if statement in the template to conditionally render a message if the cart is empty. Otherwise, it iterates through the store of cart items and renders them to the page. You also loaded in the cartItems, cartTotal, and cartQuantity getters to compute the data properties, and brought in the removeAllCartItems action.

      Save and close the file.

      Cart_List_Item Component

      This component is the direct child component of the Cart_List component. It receives the cartItem data as props from its parent and renders them in the template. It is also responsible for incrementing and decrementing the quantity of items in the cart.

      Open up the file:

      • nano cart/Cart_List_Item.vue

      Update Cart_List_Item.vue as follows:

      vuex-shopping-cart/src/components/cart/Cart_List_Item.vue

      <template>
        <div class="box">
          <div class="cart-item__details">
            <p class="is-inline">{{cartItem.title}}</p>
            <div>
              <span class="cart-item--price has-text-info has-text-weight-bold">
                ${{cartItem.price}} X {{cartItem.quantity}}
              </span>
      
              <span>
                <i class="fa fa-arrow-circle-up cart-item__modify" @click="addCartItem(cartItem)"></i>
                <i class="fa fa-arrow-circle-down cart-item__modify" @click="removeCartItem(cartItem)"></i>
              </span>
            </div>
      
          </div>
        </div>
      </template>
      <script>
      import { mapActions } from 'vuex';
      export default {
        name: 'CartListItem',
        props: ['cartItem'],
        methods: {
          ...mapActions([
            'addCartItem',
            'removeCartItem'
          ])
        }
      }
      </script>
      

      Here, you are using the mapAction helper function to map the component method to the addCartItem and removeCartItem actions in the store.

      Save and close the file.

      Lastly, you will update the App.vue file to bring these components into your app. First, move back to the root folder of your project:

      Now open the file:

      Replace the contents with the following code:

      vuex-shopping-cart/src/App.vue

      <template>
        <div>
          <Navbar/>
          <div class="container mt-6">
            <div class="columns">
              <div class="column is-12 column--align-center">
                <router-view></router-view>
              </div>
            </div>
          </div>
        </div>
      </template>
      <script>
      import Navbar from './components/core/Navbar'
      export default {
        name: 'App',
        components: {
          Navbar
        }
      }
      </script>
      <style>
      html,
      body {
        height: 100%;
        background: #f2f6fa;
      }
      </style>
      

      App.vue is the root of your application defined in Vue component file format. Once you have made the changes, save and close the file.

      In this step, you set up the frontend of your shopping cart app by creating components for the navigation bar, the product inventory, and the shopping cart. You also used the store actions and getters that you created in a previous step. Next, you will get your application up and running.

      Step 5 — Running the Application

      Now that your app is ready, you can start the development server and try out the final product.

      Run the following command in the root of your front-end project:

      This will start a development server that allows you to view your app on http://localhost:8080. Also, make sure that your backend is running in a separate terminal; you can do this by running the following command in your cart-backend project:

      Once your backend and your frontend are running, navigate to http://localhost:8080 in your browser. You will find your functioning shopping cart application:

      Animation of user adding and deleting products from the shopping cart application

      Conclusion

      In this tutorial, you built an online shopping cart app using Vue.js and Vuex for data management. These techniques can be reused to form the basis of an e-commerce shopping application. If you would like to learn more about Vue.js, check out our Vue.js topic page.



      Source link

      Handling Authentication In Vue Using Vuex


      Introduction

      Traditionally, many people use local storage to manage tokens generated through client-side authentication. A big concern is always a better way to manage authorization tokens to allow us to store even more information on users.

      This is where Vuex comes in. Vuex manages states for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.

      Sounds like a better alternative to always checking localStorage? Let’s explore it.

      Prerequisites

      1. Node installed on your local system
      2. Knowledge of JavaScript and Vue
      3. Install Vue CLI on your local system.
      4. Read through Vue Authentication And Route Handling Using Vue-router

      If you want to jump straight to the demo code: Go to vue-auth-vuex on GitHub

      Setting up the application modules

      For this project, we want to create a vue application that has vuex and vue-router. We will use the vue cli 3.0 to create a new vue project and select router and vuex from the options.

      Run the following command to set it up:

      $ vue create vue-auth
      

      Follow the dialogue that shows up, add the necessary information and select the options we need and complete the installation.

      Next, install axios:

      $ npm install axios --save
      

      Setup Axios

      We will need axios across many of our components. Let’s set it up at the entry level so we do not have to import it every time we need it.

      Open the ./src/main.js file and add the following:

      [...]
      import store from './store'
      import Axios from 'axios'
      
      Vue.prototype.$http = Axios;
      const token = localStorage.getItem('token')
      if (token) {
        Vue.prototype.$http.defaults.headers.common['Authorization'] = token
      }
      [...]
      

      Now, when we want to use axios inside our component, we can do this.$http and it will be like calling axios directly. We also set the Authorization on axios header to our token, so our requests can be processed if a token is required. This way, we do not have to set token anytime we want to make a request.

      When that is done, let’s set up the server to handle authentication.

      Setting up the server for authentication

      I already wrote about this when explaining how to handle authentication with vue-router. Check out the Setup Node.js Server section of this

      Setup Components

      The Login Component

      Create a file Login.vue in the ./src/components directory. Then, add the template for the login page:

      <template>
       <div>
         <form class="login" @submit.prevent="login">
           <h1>Sign in</h1>
           <label>Email</label>
           <input required v-model="email" type="email" placeholder="Name"/>
           <label>Password</label>
           <input required v-model="password" type="password" placeholder="Password"/>
           <hr/>
           <button type="submit">Login</button>
         </form>
       </div>
      </template>
      

      When you are done, add the data attributes that would bind to the HTML form:

      [...]
      <script>
        export default {
          data(){
            return {
              email : "",
              password : ""
            }
          },
        }
      </script>
      

      Now, let’s add the method for handling login:

      [...]
      <script>
        export default {
          [...]
          methods: {
            login: function () {
              let email = this.email 
              let password = this.password
              this.$store.dispatch('login', { email, password })
             .then(() => this.$router.push('/'))
             .catch(err => console.log(err))
            }
          }
        }
      </script>
      

      We are using a vuex action — login to handle this authentication. We can resolve actions into promises so we can do cool things with them inside our component.

      The Register Component

      Like the component for login, let’s make one for registering users. Start by creating a file Register.vue in the components directory and add the following to it:

      <template>
        <div>
          <h4>Register</h4>
          <form @submit.prevent="register">
            <label for="name">Name</label>
            <div>
                <input id="name" type="text" v-model="name" required autofocus>
            </div>
      
            <label for="email" >E-Mail Address</label>
            <div>
                <input id="email" type="email" v-model="email" required>
            </div>
      
            <label for="password">Password</label>
            <div>
                <input id="password" type="password" v-model="password" required>
            </div>
      
            <label for="password-confirm">Confirm Password</label>
            <div>
                <input id="password-confirm" type="password" v-model="password_confirmation" required>
            </div>
      
            <div>
                <button type="submit">Register</button>
            </div>
          </form>
        </div>
      </template>
      

      Let define the data attributes we will bind to the form:

      [...]
      <script>
        export default {
          data(){
            return {
              name : "",
              email : "",
              password : "",
              password_confirmation : "",
              is_admin : null
            }
          },
        }
      </script>
      

      Now, let’s add the method for handling login:

      [...]
      <script>
        export default {
          [...]
          methods: {
            register: function () {
              let data = {
                name: this.name,
                email: this.email,
                password: this.password,
                is_admin: this.is_admin
              }
              this.$store.dispatch('register', data)
             .then(() => this.$router.push('/'))
             .catch(err => console.log(err))
            }
          }
        }
      </script>
      

      The Secure Component

      Let’s make a simple component that would only display if our user is authenticated. Create the component file Secure.vue and add the following to it:

      <template>
        <div>
          <h1>This page is protected by auth</h1>
        </div>
      </template>
      

      Update The App Component

      Open ./src/App.vue file and add the following to it:

      <template>
        <div id="app">
          <div id="nav">
            <router-link to="/">Home</router-link> |
            <router-link to="/about">About</router-link><span v-if="isLoggedIn"> | <a @click="logout">Logout</a></span>
          </div>
          <router-view/>
        </div>
      </template>
      

      Can you see the Logout link we set to only show up if a user is logged in? Great.

      Now, let’s add the logic behind the log out:

      <script>
        export default {
          computed : {
            isLoggedIn : function(){ return this.$store.getters.isLoggedIn}
          },
          methods: {
            logout: function () {
              this.$store.dispatch('logout')
              .then(() => {
                this.$router.push('/login')
              })
            }
          },
        }
      </script>
      

      We are doing two things — computing the authentication state of the user and dispatching a logout action to our vuex store when a user clicks the logout button. After the log out, we send the user to login page using this.$router.push('/login'). You can change where the user gets sent to if you want.

      That’s it. Let’s make the auth module using vuex.

      Vuex Auth Module

      If you read past the Setup Node.js Server section, you would notice we had to store user auth token in localStorage and we had to retrieve both the token and user information anytime we wanted to check if the user is authenticated. This works, but it is not really elegant. We will rebuild the authentication to use vuex.

      First, let’s setup our store.js file for vuex:

      import Vue from 'vue'
      import Vuex from 'vuex'
      import axios from 'axios'
      
      Vue.use(Vuex)
      
      export default new Vuex.Store({
        state: {
          status: '',
          token: localStorage.getItem('token') || '',
          user : {}
        },
        mutations: {
      
        },
        actions: {
      
        },
        getters : {
      
        }
      })
      

      If you noticed, we have imported vue, vuex and axios, then asked vue to use vuex. This is because we mean serious business here.

      We have defined the attributes of the state. Now the vuex state would hold our authentication status, jwt token and user information.

      Create The Vuex login Action

      Vuex actions are used to commit mutations to the vuex store. We will create a login action that would authenticate a user with the server and commit user credentials to the vuex store. Open the ./src/store.js file and add the following to actions object:

      login({commit}, user){
          return new Promise((resolve, reject) => {
            commit('auth_request')
            axios({url: 'http://localhost:3000/login', data: user, method: 'POST' })
            .then(resp => {
              const token = resp.data.token
              const user = resp.data.user
              localStorage.setItem('token', token)
              axios.defaults.headers.common['Authorization'] = token
              commit('auth_success', token, user)
              resolve(resp)
            })
            .catch(err => {
              commit('auth_error')
              localStorage.removeItem('token')
              reject(err)
            })
          })
      },
      

      The login action passes vuex commit helper that we will use to trigger mutations. Mutations make changes to vuex store.

      We are making a call to the server’s login route and returning the necessary data. We store the token on localStorage, then pass the token and user information to auth_success to update the store’s attributes. We also set the header for axios at this point as well.

      We could store the token in vuex store, but if the user leaves our application, all of the data in the vuex store disappears. To ensure we allow the user to return to the application within the validity time of the token and not have to log in again, we have to keep the token in localStorage.

      It’s important you know how these work so you can decide what exactly it is you want to achieve.

      We return a promise so we can return a response to a user after login is complete.

      Create The Vuex register Action

      Like the login action, the register action will work almost the same way. In the same file, add the following in the actions object:

      register({commit}, user){
        return new Promise((resolve, reject) => {
          commit('auth_request')
          axios({url: 'http://localhost:3000/register', data: user, method: 'POST' })
          .then(resp => {
            const token = resp.data.token
            const user = resp.data.user
            localStorage.setItem('token', token)
            axios.defaults.headers.common['Authorization'] = token
            commit('auth_success', token, user)
            resolve(resp)
          })
          .catch(err => {
            commit('auth_error', err)
            localStorage.removeItem('token')
            reject(err)
          })
        })
      },
      

      This works similarly to login action, calling the same mutators as our login and register actions have the same simple goal — get a user into the system.

      Create The Vuex logout Action

      We want the user to have the ability to log out of the system, and we want to destroy all data created during the last authenticated session. In the same actions object, add the following:

      logout({commit}){
        return new Promise((resolve, reject) => {
          commit('logout')
          localStorage.removeItem('token')
          delete axios.defaults.headers.common['Authorization']
          resolve()
        })
      }
      

      Now, when the user clicks to log out, we will remove the jwt token we stored along with the axios header we set. There is no way they can perform a transaction requiring a token now.

      Create The Mutations

      Like I mentioned earlier, mutators are used to change the state of a vuex store. Let’s define the mutators we had used throughout our application. In the mutators object, add the following:

      mutations: {
        auth_request(state){
          state.status="loading"
        },
        auth_success(state, token, user){
          state.status="success"
          state.token = token
          state.user = user
        },
        auth_error(state){
          state.status="error"
        },
        logout(state){
          state.status=""
          state.token = ''
        },
      },
      

      Create The Getters

      We use getter to get the value of the attributes of vuex state. The role of our getter in the situation is to separate application data from application logic and ensure we do not give away sensitive information.

      Add the following to the getters object:

      getters : {
        isLoggedIn: state => !!state.token,
        authStatus: state => state.status,
      }
      

      You would agree with me that this is a neater way to access data in the store.

      Hide Pages Behind Auth

      The whole purpose of this article is to implement authentication and keep certain pages away from a user who is not authentication. To achieve this, we need to know the page the user wants to visit and equally have a way to check if the user is authenticated. We also need a way to say if the page is reserved for only authenticated user or unauthenticated user alone or both. These things are important considerations which, luckily, we can achieve with vue-router.

      Defiing Routes For Authenticated And Unauthenticated Pages

      Open the ./src/router.js file and import what we need for this setup:

      import Vue from 'vue'
      import Router from 'vue-router'
      import store from './store.js'
      import Home from './views/Home.vue'
      import About from './views/About.vue'
      import Login from './components/Login.vue'
      import Secure from './components/Secure.vue'
      import Register from './components/Register.vue'
      
      Vue.use(Router)
      

      As you can see, we have imported vue, vue-router and our vuex store setup. We also imported all the components we defined and set vue to use our router.

      Let’s define the routes:

      [...]
      let router = new Router({
        mode: 'history',
        routes: [
          {
            path: '/',
            name: 'home',
            component: Home
          },
          {
            path: '/login',
            name: 'login',
            component: Login
          },
          {
            path: '/register',
            name: 'register',
            component: Register
          },
          {
            path: '/secure',
            name: 'secure',
            component: Secure,
            meta: { 
              requiresAuth: true
            }
          },
          {
            path: '/about',
            name: 'about',
            component: About
          }
        ]
      })
      
      export default router
      

      Our route definition is simple. For routes requiring authentication, we add extra data to it to enable us identify it when the user tries to access it. This is the essence of the meta attribute added to the route definition. If you are asking ”Can I add more data to this meta and use it?” then I’m pleased to tell you that you are absolutely right ?.

      Handling Unauthorized Access Cases

      We have our routes defined. Now, let’s check for unauthorized access and take action.
      In the router.js file, add the following before the export default router:

      router.beforeEach((to, from, next) => {
        if(to.matched.some(record => record.meta.requiresAuth)) {
          if (store.getters.isLoggedIn) {
            next()
            return
          }
          next('/login') 
        } else {
          next() 
        }
      })
      

      From the article on using vue router for authentication, you can recall we had a really complex mechanism here that grew very big and got very confusing. Vuex has helped us simplify that completely, and we can go on to add any condition to our route. In our vuex store, we can then define actions to check these conditions and getters to return them.

      Handling Expired Token Cases

      Because we store our token in localStorage, it can remain there perpetually. This means that whenever we open our application, it would automatically authenticate a user even if the token has expired. What would happen at most is that our requests would keep failing because of an invalid token. This is bad for user experience.

      Now, open ./src/App.vue file and in the script, add the following to it:

      export default {
        [...]
        created: function () {
          this.$http.interceptors.response.use(undefined, function (err) {
            return new Promise(function (resolve, reject) {
              if (err.status === 401 && err.config && !err.config.__isRetryRequest) {
                this.$store.dispatch(logout)
              }
              throw err;
            });
          });
        }
      }
      

      We are intercepting axios call to determine if we get 401 Unauthorized response. If we do, we dispatch the logout action and the user gets logged out of the application. This takes them to the login page like we designed earlier and they can log in again.

      We can agree that this will greatly improve the user’s experience.

      Conclusion

      Using vuex allows us to store and manage authentication state and proceed to check state in our application using only a few lines of code.



      Source link