How to Build and Publish Web Components with Vue CLI 3

How to Build and Publish Web Components with Vue CLI 3

In this post, I'll discuss the reasons why you might want to create web components and show you how you make your first one with nothing more than a basic knowledge of Vue.

Are web components "the future" for the web platform? There are many opinions both for and against. What is a fact, though, is that browser support is emerging for web components and there are a growing number of tools and resources for authors interested in creating and publishing web components of their own.

A great tool for creating web components is Vue.js, and it's been made even easier with the release of Vue CLI 3 and the new @vue/web-component-wrapper library.

In this post, I'll discuss the reasons why you might want to create web components and show you how you make your first one with nothing more than a basic knowledge of Vue.

What Are Web Components?

You're familiar, of course, with HTML elements like divs, spans, tables, etc. Web components are custom HTML elements that can be used and reused in web apps and web pages.

For example, you might create a custom element called video-player so you can provide a reusable video interface that has UI features beyond what's available with the standard HTML 5 video element. This element could provide an attribute "src" for the video file and events like "play," "pause," etc., to allow a consumer to programmatically control it:

<div>
  <video-player src="..." onpause="..."></video-player>
</div>

This is probably sounding a lot like what regular Vue components can do! The difference is that web components are native to the browser (or at least, will be as the specs are incrementally implemented) and can be used like normal HTML elements can. Regardless of what tools you use to create your web component, you can consume it in React, Angular etc., or even with no framework at all.

function ReactComponent() {
  return(
    <h1>A Vue.js web component used in React!</h1>
    <video-player></video-player>
  );
}
How Do You Create a Web Component?

On the inside, web components are made by standard HTML elements that the browser already knows, e.g. divs, spans, etc. So video-player might look like this internally:

<div>
  <video src="..."></video>
  <div class="buttons">
    <button class="play-button"></button>
    <button class="pause-button"></button>
    ...
  </div>
  ...
</div>

Web components can include CSS and JavaScript, too. Using new browser standards like Shadow DOM, these aspects are fully encapsulated within your custom component so that a consumer doesn't need to worry about how their CSS might overwrite rules in the web component, for example.

There are, of course, APIs you will use to declare web components natively. But we don't need to know about those right now as we will use Vue as a layer of abstraction.

Creating Web Components With the @vue/web-component-Wrapper

Creating web components is easy with Vue CLI 3 and the new @vue/web-component-wrapper library.

The @vue/web-component-wrapper library provides a wrapper around a Vue component that interfaces it with web component APIs. The wrapper automatically proxies properties, attributes, events, and slots. This means you can write a working web component with nothing more than your knowledge of Vue components!

Another great Vue library for creating web components is vue-custom-element.

To create a web component, be sure to have Vue CLI 3 installed and create a new project with any environment settings you like:

$ vue create vue-web-component-project

Now create a new Vue component that you want to use as a web component. This component will be compiled by Webpack before publishing, so you can use any JavaScript features for this. We'll just make something really simple as a proof-of-concept, though:

src/components/VueWebComponent.vue

<template>
  <div>
    <h1>My Vue Web Component</h1>
    <div>{{ msg }}</div>
  </div>
</template>
<script>
  export default {
    props: ['msg'] 
  }
</script>

To prepare a component for wrapping by @vue/web-component-wrapper, make sure your entry file, src/main.js, looks like this:

src/main.js

import Vue from 'vue';
import wrap from '@vue/web-component-wrapper';
import VueWebComponent from './components/VueWebComponent';

const CustomElement = wrap(Vue, VueWebComponent);

window.customElements.define('my-custom-element', CustomElement);
Build a Web Component With Vue CLI 3

Vue CLI 3 includes a lot of great new features (check out this article for a rundown). One of them is the CLI Service which uses Webpack for a variety of tasks including building your app code for production. This can be done with the simple vue-cli-service build command. By adding the --target wc switch, you can create a bundle that's perfect for building a web component:

$ vue-cli-service build --target wc --name my-custom-element ./src/main.js

Behind the scenes, this will use Webpack to produce a single JavaScript file with everything needed for your web component inlined. When included on a page, this script registers <my-custom-element>, which has wrapped the target Vue component using @vue/web-component-wrapper.

Consuming Your Vue Web Component in a Web Page

With your component now built, you or anyone else can use it in a non-Vue project without any Vue.js code (although you will need to import the Vue library as this is intentionally not added to the bundle to avoid repetition in the case where you use multiple Vue-based web components). The custom element acts exactly like a native HTML element once you've loaded the script that defines it on the page.

Note that it's essential to include a polyfill since most browsers don't natively support all the web component specs. Here I'm using webcomponents.js (v1 spec polyfills).

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>My Non-Vue App</title>
  </head>
  <body>
    <!--Load Vue-->
    <script src="https://unpkg.com/vue"></script>
    <!--Load the web component polyfill-->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.2.0/webcomponents-loader.js"></script>
    <!--Load your custom element-->
    <script src="./my-custom-element.js"></script>
    <!--Use your custom element-->
    <my-custom-element msg="Hello..."></my-custom-element>
  </body>
</html>

It works! If you want to use the code I've been referring to as a template, I've put it in a repo here.

Publishing

Finally, if you want to share your web component with the world, there's no better place than webcomponents.org. This site features a browsable collection of web components free for download. The showcased components have been built from a variety of frameworks including Vue, Polymer, Angular, etc.

Thank you for reading!

Angular 9 Tutorial: Learn to Build a CRUD Angular App Quickly

What's new in Bootstrap 5 and when Bootstrap 5 release date?

What’s new in HTML6

How to Build Progressive Web Apps (PWA) using Angular 9

What is new features in Javascript ES2020 ECMAScript 2020

Async Vue.js Components - Vue.js Tutorials

Async Vue.js Components - Vue.js Tutorials

In this tutorial, you'll see how to build and lazy load these async components in Vue.js

In this tutorial, you'll see how to build and lazy load these async components in Vue.js

As your application grows, you start to look for performance patterns to make it faster. On the way, you’ll find code splitting and lazy loading to be two of them that make your initial bundle smaller by deferring the loading of code chunks until needed.

Lazy loading makes a lot of sense to be applied to the app routes and has a great impact because each route is a different section of an app.

Another case where lazy loading makes sense is when you have components which rendering is deferred. These components can be tooltips, popovers, modals, etc, and can be used as async components.

Let’s see how to build and lazy load these async components in Vue.

Lazy Loading a Component

Before we start by lazy loading a component, let’s first remember how we usually load a component. For that, let’s create a Tooltip.vue component:

<!-- Tooltip.vue --> 
<template> 
  <h2>Hi from Tooltip!</h2> 
</template>

Nothing special here, it’s just a simple component. We can use it in another component by doing local registration, importing the Tooltip component, and adding it to the components component option. For instance, in an App.vue component:

<!-- App.vue --> 
<template> 
  <div> 
    <tooltip></tooltip> 
  </div> 
</template> 

<script> 
import Tooltip from "./Tooltip"; 

export default { 
  components: { 
    Tooltip 
  } 
}; 
</script>

The Tooltip component is imported, used and loaded as long as the App is imported, probably on the initial load. But think: wouldn’t make sense to load that component only when we’re going to use it? It’s likely that the user navigates through the whole sit without the need of a tooltip.

Why should we spend precious resources on loading the component at the beginning of the application? We can apply a combination of lazy loading and code splitting in order to improve that. Lazy loading is the technique of loading something at a later phase when it’s going to be used.

While code splitting is about separating a piece of code in a separate file, known as chunk, so that the initial bundle of your application gets reduced, increasing the initial load.

Vue makes it easy to apply these techniques by using the language standard dynamic import, a JavaScript feature likely landing on the ES2018 version of the language that allows loading a module in runtime. We’ll have a separate article to dive deep into these concepts, but let’s see it from a practical and simple perspective.

Modern bundlers, such as Webpack (since version 2), Rollup and Parcel will understand this syntax and automatically create a separate file for that module which will load when it’s required.

I can imagine you’re already familiar with importing a module the usual static way. However the dynamic import is a function that returns a promise, containing the module as its payload. The following example shows how to import the utils module, both in a static and lazy-loaded dynamic way:

// static import 
import utils from "./utils"; 

// dynamic import 
import("./utils").then(utils => { 
  // utils module is available here... 
});

Lazy loading a component in Vue is as easy as importing the component using dynamic import wrapped in a function. In the previous example, we can lazy load the Tooltip component as follows:

export default { 
  components: { 
    Tooltip: () => import("./Tooltip") 
  } 
};

That’s it, just by passing () => import("./tooltip") instead of the previous import Tooltip from "./tooltip" Vue.js will lazy load that component as soon as is requested to be rendered.

Not only that, but it will apply code splitting as well. You can test that by running that code using any of the bundlers mentioned. An easy way is by using the vue-cli, but at the end of the article, you’ll find an already built demo. Once running, open the dev tools and you’ll see a JavaScript file with a name like 1.chunk.js:

In the previous example, even though we’re lazily loading the Tooltip component, it’ll be loaded as soon as it is required to be rendered, which happens right away when the App component gets mounted.

However, in practice, we’d like to defer the Tooltip component loading until it is required, which usually happens conditionally after a certain event has been triggered, for example when hovering a button or text.

For simplicity, let’s take the previous App component and add a button to make the Tooltip render conditionally using a v-if condition:

<!-- App.vue --> 
<template> 
  <div> 
    <button @click="show = true">Load Tooltip</button> 
    <div v-if="show"> 
      <tooltip></tooltip> 
    </div> 
  </div> 
</template> 

<script> 
export default { 
  data: () => ({ show: false }), 
  components: { 
    Tooltip: () => import("./Tooltip") 
  } 
}; 
</script>

Keep in mind that Vue doesn’t use a component until it needs to be rendered. Meaning that the component will not be required until that point and that’s when the component will be lazy loaded.

You can see a demo of this example running in this Codesandbox. Keep in mind that Codesandbox doesn’t do code splitting, so if you want to check that in the dev tools you can download the demo and run it locally on your machine.

User Experience on Async Components

Most of the times, async components load quite fast since they’re small pieces of code chunks stripped out from the main bundle. But imagine you’re lazy loading a big modal component under a very slow connection. That could probably take some seconds to get loaded and rendered.

Sure you can use some optimizations like HTTP caching or resource hints to load it in memory with low priority beforehand, in fact, the new vue-cli applies prefetch to these lazily loaded chunks. Still, in a few cases, it can take some time to load.

From the UX point of view, if a task takes more than 1 second to happen, you start losing user’s attention.

However, the attention could be kept by providing feedback to the user. There are several progress indicator components we could use while loading, in order to engage user’s attention, but how could we use a nice spinner or progress bar while an async component is loading?

Loading Component

Do you remember we’ve used a function with the dynamic import to lazy load an async component?

export default { 
  components: { 
    Tooltip: () => import("./Tooltip"); 
  } 
};

There is a long-hand way to define async components by returning an object instead of the result of the dynamic import. In that object, we can define a loading component:

const Tooltip = () => ({ 
  component: import("./Tooltip"), 
  loading: AwesomeSpinner 
  }
);

In that way, after a default delay of 200ms, the component AwesomeSpinner will be shown. You can customize the delay:

const Tooltip = () => ({ 
  component: import("./Tooltip"), 
  loading: AwesomeSpinner, 
  delay: 500 
});&nbsp;

The component you should use as your loading component must be as small as posible, so that it loads almost instantly.

Error Component

In the same way, we can define an error component in the long-hand form of the lazily loaded component:

const Tooltip = () => ({ 
  component: import("./Tooltip"), 
  loading: AwesomeSpinner, 
  error: SadFaceComponent 
});

The SadFaceComponent will be shown when there is an error loading the "./Tooltip" component. That could happen in several cases:

  • The internet is down
  • That component doesn’t exist (this is a good way to try it, by intentionally deleting it yourself)
  • A timeout is met

By default, there is no timeout, but we can configure it ourselves:

const Tooltip = () => ({ 
  component: import("./Tooltip"), 
  loading: AwesomeSpinner, 
  error: SadFaceComponent, 
  timeout: 5000 
});&nbsp;

Now, if after 5000 milliseconds the component hasn’t load, the error component will be shown.

Wrapping Up

You’ve seen how a component is split in its own chunk file and how it’s lazily loaded using the dynamic import. We’ve also deferred the chunk loading by conditionally rendering it.

While async components can improve an app loading time by splitting and deferring the loading of their chunks, they could have an impact on UX especially when they are big. Having control over the loading state allows us to provide feedback and engage the user in the case when that slowness is noticeable.

Node.js, ExpressJs, MongoDB and Vue.js (MEVN Stack) Application Tutorial

Node.js, ExpressJs, MongoDB and Vue.js (MEVN Stack) Application Tutorial

In this tutorial, you'll learn how to integrate Vue.js with Node.js backend (using Express framework) and MongoDB and how to build application with Node.js, ExpressJs, MongoDB and Vue.js

In this tutorial, you'll learn how to integrate Vue.js with Node.js backend (using Express framework) and MongoDB and how to build application with Node.js, ExpressJs, MongoDB and Vue.js

Vue.js is a JavaScript framework with growing number of users. Released 4 years ago, it’s now one of the most populare front-end frameworks. There are some reasons why people like Vue.js. Using Vue.js is very simple if you are already familiar with HTML and JavaScript. They also provide clear documentation and examples, makes it easy for starters to learn the framework. Vue.js can be used for both simple and complex applications. If your application is quite complex, you can use Vuex for state management, which is officially supported. In addition, it’s also very flexible that yu can write template in HTML, JavaScript or JSX.

This tutorial shows you how to integrate Vue.js with Node.js backend (using Express framework) and MongoDB. As for example, we’re going to create a simple application for managing posts which includes list posts, create post, update post and delete post (basic CRUD functionality). I divide this tutorial into two parts. The first part is setting up the Node.js back-end and database. The other part is writing Vue.js code including how to build .vue code using Webpack.

Dependencies

There are some dependencies required for this project. Add the dependencies below to your package.json. Then run npm install to install these dependencies.

  "dependencies": {
    "body-parser": "~1.17.2",
    "dotenv": "~4.0.0",
    "express": "~4.16.3",
    "lodash": "~4.17.10",
    "mongoose": "~5.2.9",
    "morgan": "~1.9.0"
  },
  "devDependencies": {
    "axios": "~0.18.0",
    "babel-core": "~6.26.3",
    "babel-loader": "~7.1.5",
    "babel-preset-env": "~1.7.0",
    "babel-preset-stage-3": "~6.24.1",
    "bootstrap-vue": "~2.0.0-rc.11",
    "cross-env": "~5.2.0",
    "css-loader": "~1.0.0",
    "vue": "~2.5.17",
    "vue-loader": "~15.3.0",
    "vue-router": "~3.0.1",
    "vue-style-loader": "~4.1.2",
    "vue-template-compiler": "~2.5.17",
    "webpack": "~4.16.5",
    "webpack-cli": "^3.1.0"
  },

Project Structure

Below is the overview of directory structure for this project.

  app
    config
    controllers
    models
    queries
    routes
    views
  public
    dist
    src

The app directory contains all files related to server-side. The public directory contains two sub-directories: dist and src. dist is used for the output of build result, while src is for front-end code files.

Model

First, we define a model for Post using Mongoose. To make it simple, it only has two properties: title and content.

app/models/Post.js

  const mongoose = require('mongoose');

  const { Schema } = mongoose;

  const PostSchema = new Schema(
    {
      title: { type: String, trim: true, index: true, default: '' },
      content: { type: String },
    },
    {
      collection: 'posts',
      timestamps: true,
    },
  );

  module.exports = mongoose.model('Post', PostSchema);

Queries

After defining the model, we write some queries that will be needed in the controllers.

app/queries/posts.js

  const Post = require('../models/Post');

  /**
   * Save a post.
   *
   * @param {Object} post - Javascript object or Mongoose object
   * @returns {Promise.}
   */
  exports.save = (post) => {
    if (!(post instanceof Post)) {
      post = new Post(post);
    }

    return post.save();
  };

  /**
   * Get post list.
   * @param {object} [criteria] - Filter options
   * @returns {Promise.<Array.>}
   */
  exports.getPostList = (criteria = {}) => Post.find(criteria);

  /**
   * Get post by ID.
   * @param {string} id - Post ID
   * @returns {Promise.}
   */
  exports.getPostById = id => Post.findOne({ _id: id });

  /**
   * Delete a post.
   * @param {string} id - Post ID
   * @returns {Promise}
   */
  exports.deletePost = id => Post.findByIdAndRemove(id);

Controllers

We need API controllers for handling create post, get post listing, get detail of a post, update a post and delete a post.

app/controllers/api/posts/create.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.save(req.body)
    .then((post) => {
      if (!post) {
        return Promise.reject(new Error('Post not created'));
      }

      return res.status(200).send(post);
    })
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to create post');
    });

app/controllers/api/posts/delete.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.deletePost(req.params.id)
    .then(() => res.status(200).send())
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to delete post');
    });

app/controllers/api/posts/details.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.getPostById(req.params.id)
    .then((post) => {
      if (!post) {
        return Promise.reject(new Error('Post not found'));
      }

      return res.status(200).send(post);
    })
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to get post');
    });

app/controllers/api/posts/list.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.getPostList(req.params.id)
    .then(posts => res.status(200).send(posts))
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to get post list');
    });

app/controllers/api/posts/update.js

  const _ = require('lodash');

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.getPostById(req.params.id)
    .then(async (post) => {
      if (!post) {
        return Promise.reject(new Error('Post not found'));
      }

      const { title, content } = req.body;

      _.assign(post, {
        title, content
      });

      await postQueries.save(post);

      return res.status(200).send({
        success: true,
        data: post,
      })
    })
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to update post');
    });

Routes

We need to have some pages for user interaction and some API endpoints for processing HTTP requests. To make the app scalable, it’s better to separate the routes for pages and APIs.

app/routes/index.js

  const express = require('express');

  const routes = express.Router();

  routes.use('/api', require('./api'));
  routes.use('/', require('./pages'));

  module.exports = routes;


Below is the API routes.

app/routes/api/index.js

  const express = require('express');

  const router = express.Router();

  router.get('/posts/', require('../../controllers/api/posts/list'));
  router.get('/posts/:id', require('../../controllers/api/posts/details'));
  router.post('/posts/', require('../../controllers/api/posts/create'));
  router.patch('/posts/:id', require('../../controllers/api/posts/update'));
  router.delete('/posts/:id', require('../../controllers/api/posts/delete'));

  module.exports = router;


For the pages, in this tutorial, we use plain HTML file. You can easily replace it with any HTML template engine if you want. The HTML file contains a div whose id is app. Later, in Vue.js application, it will use the element with id app for rendering the content. What will be rendered on each pages is configured on Vue.js route on part 2 of this tutorial.

app/routes/pages/index.js

  const express = require('express');

  const router = express.Router();

  router.get('/posts/', (req, res) => {
    res.sendFile(`${__basedir}/views/index.html`);
  });

  router.get('/posts/create', (req, res) => {
    res.sendFile(`${__basedir}/views/index.html`);
  });

  router.get('/posts/:id', (req, res) => {
    res.sendFile(`${__basedir}/views/index.html`);
  });

  module.exports = router;

Below is the HTML file

app/views/index.html

  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>VueJS Tutorial by Woolha.com</title>
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css" type="text/css" media="all" />
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
      <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    </head>
    <body>
      <div id="app"></div>
      <script src="/dist/js/main.js"></script>
    </body>
  </html>

Below is the main script of the application, you need to run this for starting the server-side application.

app/index.js

  require('dotenv').config();

  const bodyParser = require('body-parser');
  const express = require('express');
  const http = require('http');
  const mongoose = require('mongoose');
  const morgan = require('morgan');
  const path = require('path');

  const dbConfig = require('./config/database');
  const routes = require('./routes');

  const app = express();
  const port = process.env.PORT || 4000;

  global.__basedir = __dirname;

  mongoose.Promise = global.Promise;

  mongoose.connect(dbConfig.url, dbConfig.options, (err) => {
    if (err) {
      console.error(err.stack || err);
    }
  });

  /* General setup */
  app.use(morgan('dev'));
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: true }));
  app.use(morgan('dev'));

  app.use('/', routes);

  const MAX_AGE = 86400000;

  // Select which directories or files under public can be served to users
  app.use('/', express.static(path.join(__dirname, '../public'), { maxAge: MAX_AGE }));

  // Error handler
  app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
    res.status(err.status || 500);

    if (err.status === 404) {
      res.locals.page = {
        title: 'Not Found',
        noIndex: true,
      };

      console.error(`Not found: ${req.url}`);

      return res.status(404).send();
    }

    console.error(err.stack || err);

    return res.status(500).send();
  });

  http
    .createServer(app)
    .listen(port, () => {
      console.info(`HTTP server started on port ${port}`);
    })
    .on('error', (err) => {
      console.error(err.stack || err);
    });

  process.on('uncaughtException', (err) => {
    if (err.name === 'MongoError') {
      mongoose.connection.emit('error', err);
    } else {
      console.error(err.stack || err);
    }
  });

  module.exports = app;

That’s all for the server side preparation. On the next part, we’re going to set up the Vue.js client-side application and build the code into a single JavaScript file ready to be loaded from HTML.

Then, we build the code using Webpack, so that it can be loaded from HTML. In this tutorial, we’re building a simple application with basic CRUD functionality for managing posts.

Create Vue.js Components

For managing posts, there are three components we’re going to create. The first one is for creating a new post. The second is for editing a post. The other is for managing posts (displaying list of posts and allow post deletion)

First, this is the component for creating a new post. It has one method createPost which validate data and send HTTP request to the server. We use axios for sending HTTP request.

public/src/components/Posts/Create.vue

  <template>
    <b-container>
      <h1 class="d-flex justify-content-center">Create a Post</h1>
      <p v-if="errors.length">
        <b>Please correct the following error(s):</b>
        <ul>
          <li v-for="error in errors">{{ error }}</li>
        </ul>
      </p>
      <b-form @submit.prevent>
        <b-form-group>
          <b-form-input type="text" class="form-control" placeholder="Title of the post" v-model="post.title"></b-form-input>
        </b-form-group>
        <b-form-group>
          <b-form-textarea class="form-control" placeholder="Write the content here" v-model="post.content"></b-form-textarea>
        </b-form-group>
        <b-button variant="primary" v-on:click="createPost">Create Post</b-button>
      </b-form>
    </b-container>
  </template>

  <script>
    import axios from 'axios';

    export default {
      data: () => ({
        errors: [],
        post: {
          title: '',
          content: '',
        },
      }),
      methods: {
        createPost(event) {
          if (event) {
            event.preventDefault();
          }

          if (!this.post.title) {
            this.errors = [];

            if (!this.post.title) {
              this.errors.push('Title required.');
            }

            return;
          }

          const url = 'http://localhost:4000/api/posts';
          const param = this.post;

          axios
            .post(url, param)
            .then((response) => {
              console.log(response);
              window.location.href = 'http://localhost:4000/posts';
            }).catch((error) => {
              console.log(error);
            });
        },
      }
    }
  </script>


Below is the component for editing a post. Of course, we need the current data of the post before editing it. Therefore, there’s fetchPost method called when the component is created. There’s also updatePost method which validate data and call the API for updating post.

public/src/components/Posts/Edit.vue

  <template>
    <b-container>
      <h1 class="d-flex justify-content-center">Edit a Post</h1>
      <p v-if="errors.length">
        <b>Please correct the following error(s):</b>
        <ul>
          <li v-for="error in errors">{{ error }}</li>
        </ul>
      </p>
      <b-form @submit.prevent>
        <b-form-group>
          <b-form-input type="text" class="form-control" placeholder="Title of the post" v-model="post.title"></b-form-input>
        </b-form-group>
        <b-form-group>
          <b-form-textarea class="form-control" placeholder="Write the content here" v-model="post.content"></b-form-textarea>
        </b-form-group>
        <b-button variant="primary" v-on:click="updatePost">Update Post</b-button>
      </b-form>
    </b-container>
  </template>

  <script>
    import axios from 'axios';

    export default {
      data: () => ({
        errors: [],
        post: {
          _id: '',
          title: '',
          content: '',
        },
      }),
      created: function() {
        this.fetchPost();
      },
      methods: {
        fetchPost() {
          const postId = this.$route.params.id;
          const url = `http://localhost:4000/api/posts/${postId}`;

          axios
            .get(url)
            .then((response) => {
              this.post = response.data;
              console.log('this.post;');
              console.log(this.post);
          });
        },
        updatePost(event) {
          if (event) {
            event.preventDefault();
          }

          if (!this.post.title) {
            this.errors = [];

            if (!this.post.title) {
              this.errors.push('Title required.');
            }

            return;
          }

          const url = `http://localhost:4000/api/posts/${this.post._id}`;
          const param = this.post;

          axios
            .patch(url, param)
            .then((response) => {
                console.log(response);
              window.alert('Post successfully saved');
            }).catch((error) => {
              console.log(error);
            });
        },
      }
    }
  </script>


For managing posts, we need to fetch the list of post first. Similar to the edit component, in this component, we have fetchPosts method called when the component is created. For deleting a post, there’s also a method deletePost. If post successfully deleted, the fetchPosts method is called again to refresh the post list.

public/src/components/Posts/List.vue

  <template>
    <b-container>
      <h1 class="d-flex justify-content-center">Post List</h1>
      <b-button variant="primary" style="color: #ffffff; margin: 20px;"><a href="/posts/create" style="color: #ffffff;">Create New Post</a></b-button>
      <b-container-fluid v-if="posts.length">
        <table class="table">
          <thead>
            <tr class="d-flex">
              <td class="col-8">Titleqqqqqqqqq</td>
              <td class="col-4">Actions</td>
            </tr>
          </thead>
          <tbody>
            <tr v-for="post in posts" class="d-flex">
              <td class="col-8">{{ post.title }}</td>
              <td class="col-2"><a v-bind:href="'http://localhost:4000/posts/' + post._id"><button type="button" class="btn btn-primary"><i class="fa fa-edit" aria-hidden="true"></i></button></a></td>
              <td class="col-2"><button type="button" class="btn btn-danger" v-on:click="deletePost(post._id)"><i class="fa fa-remove" aria-hidden="true"></i></button></td>
            </tr>
          </tbody>
        </table>
      </b-container-fluid>
    </b-container>
  </template>

  <script>
    import axios from 'axios';

    export default {
      data: () => ({
        posts: [],
      }),
      created: () => {
        this.fetchPosts();
      },
      methods: {
        fetchPosts() {
          const url = 'http://localhost:4000/api/posts/';

          axios
            .get(url)
            .then((response) => {
              console.log(response.data);
              this.posts = response.data;
          });
        },
        deletePost(id) {
          if (event) {
            event.preventDefault();
          }

          const url = `http://localhost:4000/api/posts/${id}`;
          const param = this.post;

          axios
            .delete(url, param)
            .then((response) => {
              console.log(response);
              console.log('Post successfully deleted');

              this.fetchPosts();
            }).catch((error) => {
              console.log(error);
            });
        },
      }
    }
  </script>


All of the components above are wrapped into a root component which roles as the basic template. The root component renders the navbar which is same across all components. The component for each routes will be rendered on router-view.

public/src/App.vue

  <template>
    <div>
      <b-navbar toggleable="md" type="dark" variant="dark">
        <b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
        <b-navbar-brand to="/">My Vue App</b-navbar-brand>
        <b-collapse is-nav id="nav_collapse">
          <b-navbar-nav>
            <b-nav-item to="/">Home</b-nav-item>
            <b-nav-item to="/posts">Manage Posts</b-nav-item>
          </b-navbar-nav>
        </b-collapse>
      </b-navbar>
      <!-- routes will be rendered here -->
      <router-view />
    </div>
  </template>

  <script>

  export default {
    name: 'app',
    data () {},
    methods: {}
  }
  </script>


For determining which component should be rendered, we use Vue.js’ router. For each routes, we need to define the path, component name and the component itself. A component will be rendered if the current URL matches the path.

public/src/router/index.js

  import Vue from 'vue'
  import Router from 'vue-router'

  import CreatePost from '../components/Posts/Create.vue';
  import EditPost from '../components/Posts/Edit.vue';
  import ListPost from '../components/Posts/List.vue';

  Vue.use(Router);

  let router = new Router({
    mode: 'history',
    routes: [
      {
        path: '/posts',
        name: 'ListPost',
        component: ListPost,
      },
      {
        path: '/posts/create',
        name: 'CreatePost',
        component: CreatePost,
      },
      {
        path: '/posts/:id',
        name: 'EditPost',
        component: EditPost,
      },
    ]
  });

  export default router;


Lastly, we need a main script as the entry point which imports the main App component and the router. Inside, it creates a new Vue instance

webpack.config.js

  import BootstrapVue from 'bootstrap-vue';
  import Vue from 'vue';

  import App from './App.vue';
  import router from './router';

  Vue.use(BootstrapVue);
  Vue.config.productionTip = false;
  new Vue({
    el: '#app',
    router,
    render: h => h(App),
  });

Configure Webpack

For building the code into a single JavaSript file. Below is the basic configuration for Webpack 4.

webpack.config.js

  const { VueLoaderPlugin } = require('vue-loader');

  module.exports = {
    entry: './public/src/main.js',
    output: {
      path: `${__dirname}/public/dist/js/`,
      filename: '[name].js',
    },
    resolve: {
      modules: [
        'node_modules',
      ],
      alias: {
        // vue: './vue.js'
      }
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            'vue-style-loader',
            'css-loader'
          ]
        },
        {
          test: /\.vue$/,
          loader: 'vue-loader',
          options: {
            loaders: {
            }
            // other vue-loader options go here
          }
        },
        {
          test: /\.js$/,
          loader: 'babel-loader',
          exclude: /node_modules/
        },
      ]
    },
    plugins: [
      new VueLoaderPlugin(),
    ]

After that, run ./node_modules/webpack/bin/webpack.js. You can add the command to the scripts section of package.json, so you can run Webpack with a shorter command npm run build, as examplified below.

  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
  },
  "scripts": {
    "build": "./node_modules/webpack/bin/webpack.js",
    "start": "node app/index.js"
  },

Finally, you can start to try the application. This code is also available on Woolha.com’s Github.