Skip to content

14. Example [nuxt-11]: Customizing the loading image

By default, the loading image for [nuxt] is a progress bar. The example [nuxt-11] shows that you can replace it with your own loading image:

Image

The example [nuxt-11] also shows how to handle loading errors.

Image

The example [nuxt-11] is initially obtained by copying the example [nuxt-10]:

Image

In [1], we will add a client plugin whose role will be to manage events between components.

14.1. The [event-bus] plugin

The [event-bus] plugin will be executed by both the client and the server, but we will see that it does not work on the server side. Its code is as follows:


// create an event bus between views
import Vue from 'vue'
export default (context, inject) => {
  // the event bus
  const eventBus = new Vue()
  // injection of a [eventBus] function into the context
  inject('eventBus', () => eventBus)
}
  • line 5: the event bus is an instance of the [Vue] class. This class provides methods for handling events:
    • [$emit]: to emit an event;
    • [$on]: to listen for a specific event;

This event bus will handle only one event, [loading], which will be used by the pages to start/stop the animation that indicates waiting for the completion of an asynchronous function;

  • Line 7: We create a function [$eventBus] (first argument) whose role will be to return the object [eventBus] that we just created (second argument). This function is injected into the context so that it is available in the [context.app] and [this] objects of the pages;

14.2. The layout [default.vue]

The layout [default.vue] evolves as follows:


<template>
  <div class="container">
    <b-card>
      <!-- a message -->
      <b-alert show variant="success" align="center">
        <h4>[nuxt-11] : personnalisation de l'attente, gestion des erreurs</h4>
      </b-alert>
      <!-- the current routing view -->
      <nuxt />
      <!-- loading -->
      <b-alert v-if="showLoading" show variant="light">
        <strong>Requête au serveur de données en cours...</strong>
        <div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
      </b-alert>
      <!-- loading error -->
      <b-alert v-if="showErrorLoading" show variant="danger">
        <strong>La requête au serveur de données a échoué : {{ errorLoadingMessage }}</strong>
      </b-alert>
    </b-card>
  </div>
</template>
 
<script>
/* eslint-disable no-console */
export default {
  name: 'App',
  data() {
    return {
      showLoading: false,
      showErrorLoading: false
    }
  },
  // life cycle
  beforeCreate() {
    console.log('[default beforeCreate]')
  },
  created() {
    console.log('[default created]')
    // listen to evt [loading]
    this.$eventBus().$on('loading', this.mShowLoading)
    // and the [errorLoadingMessage] event
    this.$eventBus().$on('errorLoading', this.mShowErrorLoading)
  },
  beforeMount() {
    console.log('[default beforeMount]')
  },
  mounted() {
    console.log('[default mounted]')
  },
  methods: {
    // load management
    mShowLoading(value) {
      console.log('[default mShowLoading], showLoading=', value)
      this.showLoading = value
    },
    // loading error
    mShowErrorLoading(value, errorLoadingMessage) {
      console.log('[default mShowErrorLoading], showErrorLoading=', value, 'errorLoadingMessage=', errorLoadingMessage)
      this.showErrorLoading = value
      this.errorLoadingMessage = errorLoadingMessage
    }
  }
}
</script>
  • lines 11–14: the loading animation. It is displayed only if the [showLoading] property is true (line 29);
  • lines 16–18: the loading error message. It is displayed only if the property [showErrorLoading] (line 30) is true;
  • lines 29-30: upon initial loading of the component, the loading animation is hidden, as is the error message;
  • lines 37–43: when created, the page listens for the [loading] event (1st argument) on the event bus created by the plugin. Upon receiving it, it executes the [mShowLoading] method in lines 52–55 (2nd argument);
  • lines 52–55: The value received by the [mShowLoading] method will be a Boolean true/false. It is used to show/hide the loading message;
  • lines 41–42: When created, the page listens for the [errorLoading] event (first argument) on the event bus created by the plugin. Upon receiving it, it executes the [mShowErrorLoading] method in lines 57–61 (second argument);
  • line 57: the [mShowErrorLoading] method receives two arguments:
    • the first argument is a Boolean (true/false) to show or hide the error message;
    • the second argument is only present if an error occurred. It represents the error message to be displayed;
  • The logs on lines 53 and 58 show that the methods [showLoading] and [showErrorLoading] are not executed on the server side;

14.3. The [page1] page

The code for the [page1] page changes as follows:


<!-- view n° 1 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="primary"> Page 1 -- result={{ result }} </b-alert>
  </Layout>
</template>
 
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
 
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
 
export default {
  name: 'Page1',
  // components used
  components: {
    Layout,
    Navigation
  },
  // asynchronous data
  asyncData(context) {
    // log
    console.log('[page1 asyncData started]')
    // start waiting
    context.app.$eventBus().$emit('loading', true)
    // no error
    context.app.$eventBus().$emit('errorLoading', false)
    // we make a promise
    return new Promise(function(resolve, reject) {
      // we simulate an asynchronous function
      setTimeout(function() {
        // end waiting
        context.app.$eventBus().$emit('loading', false)
        // log
        console.log('[page1 asyncData finished]')
        // we make the result asynchronous - a random number here
        resolve({ result: Math.floor(Math.random() * Math.floor(100)) })
      }, 5000)
    })
  },
 
  // life cycle
  beforeCreate() {
    console.log('[page1 beforeCreate]')
  },
  created() {
    console.log('[page1 created]')
  },
  beforeMount() {
    console.log('[page1 beforeMount]')
  },
  mounted() {
    console.log('[page1 mounted]')
  }
}
</script>
  • The changes occur in the [asyncData] function on lines 26–47;
  • lines 29–30: Before the asynchronous function begins, the [loading] event is sent to the other pages in the application. Note that in [asyncData], we do not yet have access to the [this] object, which has not yet been created. We therefore use the context passed as an argument to the [asyncData] function (line 26);
  • line 30: the event bus is used to indicate that loading is about to begin;
  • line 38: the event bus is used to indicate that loading is complete;

Note: At runtime, when the [page1] page is requested from the server, the loading image is not displayed. The logs show that on the server side, the [default.mShowLoading] method is not called. In any case, seeing the loading image makes no sense when the page is requested from the server. The server only sends the page to the client browser once the [asyncData] function has finished. The loading image is therefore unnecessary. This will be the case for all pages in the application requested directly from the server.

14.4. The [index] page

The code for the [index] page is as follows:


<!-- main page -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning">
      Home
    </b-alert>
  </Layout>
</template>
 
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
/* eslint-disable nuxt/no-timing-in-fetch-data */
 
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
 
export default {
  name: 'Home',
  // components used
  components: {
    Layout,
    Navigation
  },
  // asynchronous data
  asyncData(context) {
    // log
    console.log('[page1 asyncData started]')
    // start waiting
    context.app.$eventBus().$emit('loading', true)
    // no error
    context.app.$eventBus().$emit('errorLoading', false)
    // we make a promise
    return new Promise(function(resolve, reject) {
      // we simulate an asynchronous function
      setTimeout(function() {
        // end waiting
        context.app.$eventBus().$emit('loading', false)
        // log
        console.log('[page1 asyncData finished]')
        // we return an error
        reject(new Error("le serveur n'a pas répondu assez vite"))
      }, 5000)
    }).catch((e) => context.error({ statusCode: 500, message: e.message }))
  },
  // life cycle
  beforeCreate() {
    console.log('[home beforeCreate]')
  },
  created() {
    console.log('[home created]')
  },
  beforeMount() {
    console.log('[home beforeMount]')
  },
  mounted() {
    console.log('[home mounted]')
    // no error
    this.$eventBus().$emit('errorLoading', false)
  }
}
</script>
  • Lines 30–49: The function [asyncData] is identical to that of the page [page1] with one exception: in line 46, the asynchronous function is terminated on failure (using the method [reject]);
  • line 46: the parameter of the [reject] function is an instance of the [Error] class. The parameter of the [Error] constructor is the error message;
  • line 48: this error is intercepted by the [catch] method of [Promise], which receives the error as a parameter. The [context.error] function is then used to report the error. The parameter of the [context.error] function is an object with two properties here:
    • [statusCode]: an error code;
    • [message]: an error message;

Whether [asyncData] is executed by the client or the server, in the event of an error [context.error], [nuxt] displays the page [layouts / error.vue]:

Image

Although it is a page, the [error.vue] page is looked for in the [layouts] folder (perhaps to prevent it from being included in the application routes?). Here, the [error.vue] page is as follows:


<!-- definition HTML of the view -->
<template>
  <!-- layout -->
  <Layout :left="true" :right="true">
    <!-- alert in the right-hand column -->
    <template slot="right">
      <!-- message on yellow background -->
      <b-alert show variant="danger" align="center">
        <h4>L'erreur suivante s'est produite : {{ JSON.stringify(error) }}</h4>
      </b-alert>
    </template>
    <!-- navigation menu in the left-hand column -->
    <Navigation slot="left" />
  </Layout>
</template>
 
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
 
import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
 
export default {
  name: 'Error',
  // components used
  components: {
    Layout,
    Navigation
  },
  // property [props]
  props: { error: { type: Object, default: () => 'waiting ...' } },
  // life cycle
  beforeCreate() {
    // client and server
    console.log('[error beforeCreate]')
  },
  created() {
    // client and server
    console.log('[error created, error=]', this.error)
  },
  beforeMount() {
    // customer only
    console.log('[error beforeMount]')
  },
  mounted() {
    // customer only
    console.log('[error mounted]')
  }
}
</script>

When [nuxt] displays the page [error.vue], it passes the error that occurred (line 33) to it as the property [props]. If the error was caused by [context.error(objet1)], the [props] property of the [error.vue] page will have the value [objet1]. The [nuxt] documentation states that [objet1] must have at least the attributes of [statusCode, message]. Line 9 displays the string jSON from the received object [objet1].

14.5. The [page2] page

The page [page2] shows another way to handle the error:

  • in [page1], the error is displayed on a separate page, [error.vue];
  • in [page2], the error will be displayed on the page [page2] that caused the error;

The code for [page2] is as follows:


<!-- view n° 2 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message -->
    <b-alert slot="right" show variant="secondary">
      Page 2
    </b-alert>
  </Layout>
</template>
 
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
 
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
 
export default {
  name: 'Page2',
  // components used
  components: {
    Layout,
    Navigation
  },
  // asynchronous data
  asyncData(context) {
    // log
    console.log('[page2 asyncData started]')
    // start waiting
    context.app.$eventBus().$emit('loading', true)
    // no error
    context.app.$eventBus().$emit('errorLoading', false)
    // we make a promise
    return new Promise(function(resolve, reject) {
      // we simulate an asynchronous function
      setTimeout(function() {
        // end waiting
        context.app.$eventBus().$emit('loading', false)
        // arbitrarily generate an error
        const errorLoadingMessage = "le serveur n'a pas répondu assez vite"
        // successful completion
        resolve({ showErrorLoading: true, errorLoadingMessage })
        // log
        console.log('[page2 asyncData finished]')
      }, 5000)
    })
  },
  // life cycle
  beforeCreate() {
    console.log('[page2 beforeCreate]')
  },
  created() {
    console.log('[page2 created]')
  },
  beforeMount() {
    console.log('[page2 beforeMount]')
  },
  mounted() {
    console.log('[page2 mounted]')
    // customer
    if (this.showErrorLoading) {
      console.log('[page2 mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
  }
}
</script>

Once again, we insert a [asyncData] function into the page code, and like [index], [page2] will generate an error that we will handle differently this time.

  • Line 44: Both the server and the client resolve the promise successfully by returning the result [{ showErrorLoading: true, errorLoadingMessage }]. We know that this will result in the properties [showerrorLoading, errorLoadingMessage] being included in the page’s properties [data] and that the client will receive these properties;
  • lines 60–67: we know that the [mounted] function is executed only by the client;
  • line 63: the client checks whether the [showErrorLoading] property has been set (by the server or the client, as applicable). If so, it triggers the [‘errorLoading’] event (line 65) so that the [default] page displays the [this.errorLoadingMessage] error message. Ultimately, the server sends a page without an error message displayed. The error message is displayed at the last moment by the client when the page is "loaded";

14.6. Execution

14.6.1. [nuxt.config]

The [nuxt.config.js] execution file is as follows:


export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    title: 'Introduction à [nuxt.js]',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: 'ssr routing loading asyncdata middleware plugins store'
      }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: false,
 
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [{ src: '@/plugins/event-bus' }],
  /*
   ** Nuxt.js dev-modules
   */
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module'
  ],
  /*
   ** Nuxt.js modules
   */
  modules: [
    // Doc: https://bootstrap-vue.js.org
    'bootstrap-vue/nuxt',
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios'
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {},
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  },
  // source code directory
  srcDir: 'nuxt-11',
  // router
  router: {
    // application URL root
    base: '/nuxt-11/'
  },
  // server
  server: {
    // service port, default 3000
    port: 81,
    // network addresses listened to, default localhost: 127.0.0.1
    // 0.0.0.0 = all the machine's network addresses
    host: 'localhost'
  }
}
  • line 22: set the [loading] property to [false] so that [nuxt] does not use its default idle image;
  • line 31: the plugin that defines the event bus;

14.6.2. The [index] page executed by the server

Let’s request the page [index] from the server (we manually enter URL [http://localhost:81/nuxt-11/]). The page displayed by the client browser is as follows:

Image

The logs are as follows:

Image

  • In [3], we see that the server sends the page [error.vue];
  • in [4], we see that the client also displays the page [error] with the same error as the server;
  • we can see that the [mShowLoading] method of the [default] page was not called on the server side, even though the [index] page had triggered a wait. This method is called upon receipt of an event, and clearly event handling is not implemented on the server side;

Let’s examine the source code of the page received by the client browser:


<!doctype html>
<html data-n-head-ssr>
<head>
  <title>Introduction à [nuxt.js]</title>
  <meta data-n-head="ssr" charset="utf-8">
  <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
  <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <base href="/nuxt-11/">
  <link rel="preload" href="/nuxt-11/_nuxt/runtime.js" as="script">
  <link rel="preload" href="/nuxt-11/_nuxt/commons.app.js" as="script">
  <link rel="preload" href="/nuxt-11/_nuxt/vendors.app.js" as="script">
  <link rel="preload" href="/nuxt-11/_nuxt/app.js" as="script">
  ...
</head>
<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
            <h4>[nuxt-11] : personnalisation de l'attente, gestion des erreurs</h4>
              </div>
            <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-11/" target="_self" class="nav-link active nuxt-link-active">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-11/page1" target="_self" class="nav-link">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-11/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div> <div class="col-10"><div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-danger">
                <h4>L'erreur suivante s'est produite : {&quot;statusCode&quot;:500,&quot;message&quot;:&quot;le serveur n'a pas répondu assez vite&quot;}</h4>
                    </div>
                </div>
              </div>
            </div>  
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>window.__NUXT__ = (function (a, b, c, d) {
  d.statusCode = 500; d.message = "le serveur n'a pas répondu assez vite";
  return {
    layout: "default", data: [d], error: d, serverRendered: true,
    logs: [
      { date: new Date(1575047424168), args: ["[event-bus créé]"], type: a, level: b, tag: c },
      { date: new Date(1575047424175), args: ["[page1 asyncData started]"], type: a, level: b, tag: c },
      { date: new Date(1575047429455), args: ["[page1 asyncData finished]"], type: a, level: b, tag: c },
      { date: new Date(1575047429515), args: ["[default beforeCreate]"], type: a, level: b, tag: c },
      { date: new Date(1575047429675), args: ["[default created]"], type: a, level: b, tag: c },
      { date: new Date(1575047430157), args: ["[error beforeCreate]"], type: a, level: b, tag: c },
      { date: new Date(1575047430246), args: ["[error created, error=]", "{ statusCode: 500,\n  message: 'le serveur n\\'a pas répondu assez vite' }"], type: a, level: b, tag: c }]
  }
    }("log", 2, "", {}));</script>
  <script src="/nuxt-11/_nuxt/runtime.js" defer></script>
  <script src="/nuxt-11/_nuxt/commons.app.js" defer></script>
  <script src="/nuxt-11/_nuxt/vendors.app.js" defer></script>
  <script src="/nuxt-11/_nuxt/app.js" defer></script>
</body>
</html>
  • line 57: we see that the server sent an object [d] representing the error that occurred on the server side;
  • line 59: we see a property [error] with a value of the object [d]. We can assume that it is the presence of the [error] property in the page sent by the server that causes the client-side scripts to display the [error.vue] page with the [error] error;

14.6.3. The [page1] page executed by the server

We manually enter URL [http://localhost:81/nuxt-11/page1]. After 5 seconds, the browser displays the following page:

Image

The displayed logs are as follows:

Image

  • in [1], the server logs. Note that the [mShowLoading] method of the [default] page was not called;
  • in [2], the client logs;

14.6.4. The [page2] page executed by the server

We manually enter URL [http://localhost:81/nuxt-11/page2]. After 5 seconds, the browser displays the following page:

Image

Let’s examine the logs displayed in the browser:

Image

  • in [1], the server logs. Recall that the server included the [showErrorLoading, errorLoadingMessage] properties in the page sent to the client browser. We know that these properties will then be incorporated into the [data] of the page displayed by the client
  • In [3], when the [page2] page is loaded, it finds the [showErrorLoading] property set to true. It then sends an event to the [default] page, so that it displays the error message sent by the [4] server;

14.6.5. The [index] page executed by the client

We now use the links from navigation to display the three pages. All pages displayed by the client are identical to those displayed by the server. The only difference is that the 5-second wait image is displayed each time.

We start with the [index] page. The loading image is then displayed:

Image

then after 5 seconds, the following page appears:

Image

The final page is therefore identical to the one obtained on the server side.

Image

Recall the [asyncData] function from the [index] page:


asyncData(context) {
    // log
    console.log('[page1 asyncData started]')
    // start waiting
    context.app.$eventBus().$emit('loading', true)
    // no error
    context.app.$eventBus().$emit('errorLoading', false)
    // we make a promise
    return new Promise(function(resolve, reject) {
      // we simulate an asynchronous function
      setTimeout(function() {
        // end waiting
        context.app.$eventBus().$emit('loading', false)
        // log
        console.log('[page1 asyncData finished]')
        // we return an error
        reject(new Error("le serveur n'a pas répondu assez vite"))
      }, 5000)
    }).catch((e) => context.error({ statusCode: 500, message: e.message }))
}

The client logs are as follows:

Image

  • in [1], the [asyncData] function starts;
  • in [2], the loading image is displayed;
  • in [2-3], we see that page [default] received events [loading, true], [2], and [errorLoading, false] sent by function [asyncData] from page [index] (lines 5 and 7);
  • in [4], end of wait. Page [default] received event [loading, false] sent by page [index] (line 13);
  • in [5], the function [asyncData] has finished its work;
  • because the function [asyncData] generated an error with [context.error] (line 19), the page [error] is displayed as [6];

14.6.6. The [page1] page executed by the client

After a 5-second wait, the client displays the following page:

Image

Let’s review the code for the [asyncData] function from [page1]:


asyncData(context) {
    // log
    console.log('[page1 asyncData started]')
    // start waiting
    context.app.$eventBus().$emit('loading', true)
    // no error
    context.app.$eventBus().$emit('errorLoading', false)
    // we make a promise
    return new Promise(function(resolve, reject) {
      // we simulate an asynchronous function
      setTimeout(function() {
        // end waiting
        context.app.$eventBus().$emit('loading', false)
        // log
        console.log('[page1 asyncData finished]')
        // we make the result asynchronous - a random number here
        resolve({ result: Math.floor(Math.random() * Math.floor(100)) })
      }, 5000)
    })
},

The logs are as follows:

Image

14.6.7. The [page2] page executed by the client

After a 5-second wait, the client displays the following page:

Image

Let’s review the code for the [asyncData] and [mounted] functions in [page2]:


asyncData(context) {
    // log
    console.log('[page2 asyncData started]')
    // start waiting
    context.app.$eventBus().$emit('loading', true)
    // no error
    context.app.$eventBus().$emit('errorLoading', false)
    // we make a promise
    return new Promise(function(resolve, reject) {
      // we simulate an asynchronous function
      setTimeout(function() {
        // end waiting
        context.app.$eventBus().$emit('loading', false)
        // arbitrarily generate an error
        const errorLoadingMessage = "le serveur n'a pas répondu assez vite"
        // successful completion
        resolve({ showErrorLoading: true, errorLoadingMessage })
        // log
        console.log('[page2 asyncData finished]')
      }, 5000)
    })
  }
 
mounted() {
    console.log('[page2 mounted]')
    // customer
    if (this.showErrorLoading) {
      console.log('[page2 mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
}

The logs are as follows:

Image

  • In [1], the page [default] received the event [showErrorLoading, true] sent by [page2] (line 29), which instructs it to display the error message;