Categories
Nuxt.js

Nuxt.js — Error Pages, Async, and Request Data

Nuxt.js is an app framework that’s based on Vue.js.

We can use it to create server-side rendered apps and static sites.

In this article, we’ll look at how to add error pages, get async data, and get request data with Nuxt.js.

Error Page

We can add an error page to our Nuxt app by putting pages files in the layouts folder.

For example, if we want to show a page when we get a 404 error, we add a layouts/error.js file:

<template>
  <div>
    <h1 v-if="error.statusCode === 404">Page not found</h1>
  </div>
</template>

<script>
export default {
  props: ["error"],
};
</script>

It takes an error prop and we can check the statusCode property for the error status code.

Pages

Pages have special attributes and functions to make the development of our universal app.

For example, our pages may look like:

<template>
  <div>hello {{ name }}</div>
</template>

<script>
export default {
  asyncData(context) {
    return { name: "world" };
  },
  fetch() {
    //...
  },
  head() {
    return { foo: "bar" };
  },
};
</script>

The asyncData method lets us set the state before loading the component.

The returned object will be merged with our data object.

The fetch method lets us fill the store before rendering the page.

head lets us set the meta tags for the page.

loading prevents a page from automatically calling this.$nuxt.$loading.finish() as we enter it and this.$nuxt.$loading.start() as we leave it.

This lets us manually control the behavior.

transition defines a specific transition for the page.

scrollToTop is a boolean that specifies if we want to scroll to the top before rendering the page.

validate is a validator function for dynamic routes.

middleware defines the middleware for the page.

Async Data

We can use the asyncData method for getting data.

If we use Axios interceptors in our code, then we have to create an instance of it first.

For example, we can write:

import axios from 'axios'
const myAxios = axios.create({
  // ...
})
myAxios.interceptors.response.use(
  function (response) {
    return response.data
  },
  function (error) {
    // ...
  }
)

In our asyncData method, we can return a promise with it.

For example, we can write:

<template>
  <div>{{ title }}</div>
</template>

<script>
import axios from "axios";

export default {
  async asyncData({ params }) {
    const { data } = await axios.get(
      `https://jsonplaceholder.typicode.com/posts/${params.id}`
    );
    return { title: data.title };
  },
};
</script>

We have the asyncData method that takes an object with the params property.

It has the URL parameters for our page.

Then we can use axios to get the data we want and return the resolved value.

The resolved object can be retrieved from our component.

The Context

The context parameter for asyncData also has the req and res properties.

req has the request object.

And res has the response.

We use the process.server to check if the page is server-side rendered before using the request data.

To do that, we write:

pages/hello.vue

<template>
  <div class="container">{{host}}</div>
</template>

<script>
export default {
  async asyncData({ req, res }) {
    if (process.server) {
      return { host: req.headers.host };
    }
    return {};
  },
};
</script>

If process.server is true , then we can use the req object to get the request data.

Conclusion

We can get request data with Nuxt in our pages.

Also, we can create our own error page.

And we can initial our page with async data.

Categories
Nuxt.js

Nuxt.js — Error Handling and Plugins

Nuxt.js is an app framework that’s based on Vue.js.

We can use it to create server-side rendered apps and static sites.

In this article, we’ll look at how to handle async data errors and plugins with Nuxt.js.

Handling Errors

We can handle errors in our pages when we get async data.

To do that, we write:

<template>
  <div>{{ title }}</div>
</template>

<script>
import axios from "axios";

export default {
  async asyncData({ params, error }) {
    try {
      const { data } = await axios.get(
        `https://jsonplaceholder.typicode.com/posts/${params.id}`
      );
      return { title: data.title };
    } catch {
      error({ statusCode: 404, message: "Post not found" });
    }
  },
};
</script>

We have an asyncData method with an object that has the error property.

error is a function that we can to render the error.

Assets

Nuxt uses vue-loader, file-loader, and url-loader Webpack loaders for asset serving.

Also, we can use the static folder for static assets.

To add assets, we can write:

<template>
  <div class="container">
    <img src="~/assets/kitten.jpg" />
  </div>
</template>

<script>
export default {};
</script>

The ~ is a shorthand for the Nuxt root folder.

Static

We can also serve assets from the static folder.

For example, we can move our file to the static folder and write:

<template>
  <div class="container">
    <img src="/kitten.jpg" />
  </div>
</template>

<script>
export default {};
</script>

The / is the static folder’s shorthand.

Plugins

We can add plugins into our app.

They may come as external packages like Axios.

Or we can use Vue plugins.

To use Vue plugins, we have to add them manually.

For example, if we want to add a tooltip to our app, we install the v-tooltip package by running:

npm install --save v-tooltip

Then we create the plugins/vue-tooltip.js and add:

import Vue from 'vue'
import VTooltip from 'v-tooltip'

Vue.use(VTooltip)

Then in nuxt.config.js , we add:

plugins: [
  '@/plugins/vue-tooltip.js'
],

Then we can use it as usual by writing:

<template>
  <div class="container">
    <button v-tooltip="'hello world'">hello</button>
  </div>
</template>

<script>
export default {};
</script>

Inject in $root & context

We can also make functions available across all of our app.

To do that, we can call the inject function.

We can create our own plugin by adding plugins/hello.js :

export default (context, inject) => {
  const hello = msg => console.log(`hello ${msg}!`)
  inject('hello', hello)
}

Then we can add our plugin in nuxt.config.js :

plugins: [
  '@/plugins/hello.js'
],

Then we can call our hello function by writing:

<template>
  <div class="container"></div>
</template>

<script>
export default {
  mounted() {
    this.$hello("james");
  },
};
</script>

We also need:

context.$hello = hello

in the function we exported if we use Nuxt 2.12 or earlier.

We can also use the $hello function with the asyncData function by writing:

<template>
  <div class="container"></div>
</template>

<script>
export default {
  mounted() {
    this.$hello("james");
  },
  asyncData({ $hello }) {
    $hello("asyncData");
  },
};
</script>

The $hello is part of the context parameter of asyncData .

Conclusion

We can handle errors when we get data.

Also, we can create our own plugins and functions and inject them to our app.

Categories
Nuxt.js

Getting Started with Nuxt.js

Nuxt.js is an app framework that’s based on Vue.js.

We can use it to create server-side rendered apps and static sites.

In this article, we’ll look at how to get started with Nuxt.js.

Installation

We can create our Nuxt app with the create-nuxt-app program.

To do that, we run:

npx create-nuxt-app <project-name>

or:

yarn create nuxt-app <project-name>

Then we run:

npm run dev

to run our app.

Directory Structure

The directory structure for a Nuxt app follows some conventions.

The assets folder as static assets like styles, images, and fonts.

The layouts folder has the layout components for laying out contents on pages.

The middlewares folder has application middleware.

Middleware lets us define custom functions that can be run before rendering.

The pages folder has the views and routes.

It should only contain .vue files.

The folder can’t be renamed without changing our app’s configuration.

The plugins folder has the JavaScript plugins that we want to run before instantiating the root Vue instance.

The static folder serves static files directly to the public.

nuxt.config.js has the Nuxt.js custom configuration.

Routing

Nuxt does routing automatically by following the structure of the pages folder.

For instance, we can create the pages/hello.vue file:

<template>
  <div class="container">hello world</div>
</template>

<script>
export default {};
</script>

Then when we go to http://localhost:3000/hello, we see ‘hello world’ displayed.

If we want to accept URL parameters in our pages, we add a _ before the name of our file.

For example, we create a pages/users/_id.vue file and write:

<template>
  <div class="container">{{$route.params.id}}</div>
</template>

<script>
export default {};
</script>

We get the URL parameters from the $route.params object.

We can validate route params with the validate method.

To add it, we write:

<template>
  <div class="container">{{$route.params.id}}</div>
</template>

<script>
export default {
  validate({ params }) {
    return /^d+$/.test(params.id);
  },
};
</script>

We added the validate method with an object with the params property as the property.

Then we can return the condition for validation.

Also, we can add a _.vue file to handle URLs that don’t match any other URLs.

Named Views

We can use <nuxt name="top"/> or <nuxt-child name="top"/> in our layout or page to add the named views.

Views

The view defines the parts of our pages.

The default template for a Nuxt page is:

<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head {{ HEAD_ATTRS }}>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>

Default Layout

We can change the default layout by editing the layouts/default.vue file.

By default, it has:

<template>
  <div>
    <Nuxt />
  </div>
</template>

to render the page.

Custom Layout

Also, we can add custom layouts.

For example, we can create a layouts/blog.vue file and write:

<template>
  <div>
    <div>My blog</div>
    <Nuxt />
  </div>
</template>

to display a heading and the Nuxt component for displaying the page content.

Then to use the layout, we can create a file in the pages folder called pages/post.vue and add:

<template>
  <div>hello world</div>
</template>

<script>
export default {
  layout: "blog",
};
</script>

We set the layout property so that we can select the layout we want to use.

Now we should see the ‘My blog’ heading above ‘hello world’.

Conclusion

We can use Nuxt.js to create simple server-side rendered apps.

It’s based on Vue.js.

Categories
Vue

Getting Started with Storybook for Vue

Storybook is an open-source tool for developing UI components in isolation.

It works with React, Vue, Angular, and other frameworks.

In this article, we’ll look at how to add Storybook to our Vue project and create our components.

Install Storybook

To get started with Storybook, we create a new Vue project with Vue CLI.

We run npx vue create . in an empty project folder to create a Vue project.

Then we follow the instructions to complete the process.

Once we did that, we run:

npx sb init

to create the Storybook project.

Once we did that, we run:

npm run storybook

in our Vue project folder to run Storybook in our Vue project.

Then we should see the Storybook screen in our browser.

It has a collection of links to the Storybook components and we can preview them.

Story

Once we have Storybook set up, we can create our stories.

A story is a collection of components that we can preview in Storybook.

The preview includes the possible states and props that it can take and what it looks like with them.

It also includes the docs.

In the preview screen, we can edit the props and preview what it’ll look like with them in the Controls tab.

Write Stories

We can put stories in the strotries folder.

To start, we add a .js or .ts file for our story.

For example, we can create a button.

In the stories folder, we add a Button.vue file to create our Storybook component:

<template>
  <button type="button" :class="classes" @click="onClick" :style="style">{{ label }}</button>
</template>

<script>
import './button.css';

export default {
  name: 'my-button',

props: {
    label: {
      type: String,
      required: true,
    },
    primary: {
      type: Boolean,
      default: false,
    },
    size: {
      type: String,
      default: 'medium',
      validator (value) {
        return ['small', 'medium', 'large'].includes(value);
      },
    },
    backgroundColor: {
      type: String,
    },
  },

  computed: {
    classes() {
      return {
        'button': true,
        'button-primary': this.primary,
        [`button-${this.size}`]: true,
      };
    },
    style() {
      return {
        backgroundColor: this.backgroundColor,
      };
    },
  },

  methods: {
    onClick() {
      this.$emit('onClick');
    },
  },
};
</script>

Our component takes a few props to let us style our button.

label is for the label text.

primary is a boolean for changing the colors of the button.

size is the size of the button.

backgroundColor is the background color.

We imported button.css in the same folder, which has:

.button {
  border-radius: 3em;
  cursor: pointer;
  display: inline-block;
}
.button-primary {
  color: white;
  background-color: lightblue;
}
.button-small {
  font-size: 12px;
}
.button-medium {
  font-size: 16px;
}
.button-large {
  font-size: 20px;
}

To make it show in Storybook, we’ve to add a Button.stories.js file with the following code:

import MyButton from './Button.vue';

export default {
  title: 'Example/Button',
  component: MyButton,
  argTypes: {
    backgroundColor: { control: 'color' },
    size: { control: { type: 'select', options: ['small', 'medium', 'large'] } },
  },
};

const Template = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { MyButton },
  template: '<my-button @onClick="onClick" v-bind="$props" />',
});

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};

export const Large = Template.bind({});
Large.args = {
  size: 'large',
  label: 'Button',
};

export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};

All the exported items will be shown by Storybook.

The default export has the title, component, and argTypes .

title is the title shown in Storybook.

component is the component we want to preview.

argTypes define the controls that we can set to change prop values.

The names are the same as the prop names.

We also have the Template object that sets the props to the argType keys.

components with the component to preview.

template for the component to preview.

v-bind='$props' passes all props to the my-button component.

Then 3 exports at the bottom define the preset styles that we want to show the button with.

We pass in some args and they’ll be set as props.

Now we should see the component displayed in the sidebar.

Conclusion

We can create a Storybook with Vue by creating a Vue CLI project and then add Storybook code to it.

Then we can add our components and stories.

Categories
Vue 3

Vuex 4 — Modules Namespace

Vuex 4 is in beta and it’s subject to change.

Vuex is a popular state management library for Vue.

Vuex 4 is the version that’s made to work with Vue 3.

In this article, we’ll look at how to use Vuex 4 with Vue 3.

Accessing Global Assets in Namespaced Modules

We can access global assets in namespaced modules.

For example, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vuex@4.0.0-beta.4/dist/vuex.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <button @click="this['moduleA/increment']">increment</button>
      <p>{{this['moduleA/doubleCount']}}</p>
    </div>
    <script>
      const moduleA = {
        state: () => ({
          count: 0
        }),
        mutations: {
          increment(state) {
            state.count++;
          }
        },
        actions: {
          increment({ commit, dispatch }) {
            commit("increment");
            commit("someAction", null, { root: true });
          }
        },
        getters: {
          doubleCount(state) {
            return state.count * 2;
          }
        }
      };

      const store = new Vuex.Store({
        mutations: {
          someAction(state) {
            console.log("someAction");
          }
        },
        modules: {
          moduleA: {
            namespaced: true,
            ...moduleA
          }
        }
      });
      const app = Vue.createApp({
        methods: {
          ...Vuex.mapActions(["moduleA/increment"])
        },
        computed: {
          ...Vuex.mapGetters(["moduleA/doubleCount"])
        }
      });
      app.use(store);
      app.mount("#app");
    </script>
  </body>
</html>

We have the increment action that commits the someAction mutation from the root namespace.

Therefore, when we dispatch the moduleA/increment action, we should see 'someAction' logged.

We called commit with an object with the root: true property to set make it dispatch the root mutation.

We can do the same with actions. For example, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vuex@4.0.0-beta.4/dist/vuex.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <button @click="this['moduleA/increment']">increment</button>
      <p>{{this['moduleA/doubleCount']}}</p>
    </div>
    <script>
      const moduleA = {
        state: () => ({
          count: 0
        }),
        mutations: {
          increment(state) {
            state.count++;
          }
        },
        actions: {
          increment({ commit, dispatch }) {
            commit("increment");
            dispatch("someOtherAction", null, { root: true });
          }
        },
        getters: {
          doubleCount(state) {
            return state.count * 2;
          }
        }
      };

      const store = new Vuex.Store({
        mutations: {
          someAction(state) {
            console.log("someAction");
          }
        },
        actions: {
          someOtherAction({ commit }) {
            commit("someAction");
          }
        },
        modules: {
          moduleA: {
            namespaced: true,
            ...moduleA
          }
        }
      });
      const app = Vue.createApp({
        methods: {
          ...Vuex.mapActions(["moduleA/increment"])
        },
        computed: {
          ...Vuex.mapGetters(["moduleA/doubleCount"])
        }
      });
      app.use(store);
      app.mount("#app");
    </script>
  </body>
</html>

We called dispatch with an object with the root: true property to set make it dispatch the root action.

We can access root getters with the rootGetters property.

To do that, we write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vuex@4.0.0-beta.4/dist/vuex.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <button @click="this['moduleA/increment']">increment</button>
      <p>{{this['moduleA/doubleCount']}}</p>
    </div>
    <script>
      const moduleA = {
        state: () => ({
          count: 0
        }),
        mutations: {
          increment(state) {
            state.count++;
          }
        },
        actions: {
          increment({ commit, dispatch, rootGetters }) {
            console.log("increment", rootGetters.one);
            commit("increment");
          }
        },
        getters: {
          doubleCount(state, getters, rootState, rootGetters) {
            console.log("doubleCount", rootGetters.one);
            return state.count * 2;
          }
        }
      };

      const store = new Vuex.Store({
        getters: {
          one(state) {
            return 1;
          }
        },
        modules: {
          moduleA: {
            namespaced: true,
            ...moduleA
          }
        }
      });
      const app = Vue.createApp({
        methods: {
          ...Vuex.mapActions(["moduleA/increment"])
        },
        computed: {
          ...Vuex.mapGetters(["moduleA/doubleCount"])
        }
      });
      app.use(store);
      app.mount("#app");
    </script>
  </body>
</html>

We have a one root getter method in the store’s root.

Then we have the rootGetters property in the object parameter of the increment method.

We can get the getter’s return value with from the rootGetters.one property.

Other getters can get the value from the rootGetters parameter.

Conclusion

We can namespace our store so that we divide our store into smaller chunks that have their own states.