This page looks best with JavaScript enabled

Create a simple app using Vue from CDN

 ·   ·  ☕ 10 min read

I had to work on a MVP where there were specific instructions to use Vue directly from a CDN. The Vue build available in this way is also called UMD (Universal Module Definition) build since you can use Vue from anywhere and the project does not need specific setup to build and package your code. Here’s a demo of how a simple Vue setup from CDN can be used for quick demo projects.

But, why use Vue from CDN? There may be a few cases where such an arrangement can help-

  1. demo simple functionality (and do not want to spend time on setup)
  2. create quick MVPs that can be demonstrated using simple HTML and JS files
  3. make everything portable; enable others to easily change stuff without the full Vue setup

Rather than building a simple “hello world”, we will incorporate two components and router, and axios to call external services - and get all this working within a single HTML page and a couple of JS files.

simple-vue-html-axios-demo

Setup

There is nothing to setup when using Vue from CDN. Create a new index.html page and include Vue (and Vue router).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<html>
  <head>
    <title>The Coolest Vue - Axios Demo</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <script src="https://unpkg.com/vue-router@2.0.0"></script>
  </head>
  <body>
    Sample page
  </body>
</html>

You are all set to start coding.

Add Bootstrap for future styling - we will use it for nav pane, and restrict ourselves to simple styles elsewhere.

Also introduce a footer while we are at it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<html>
  <head>
    <title>The Coolest Vue - Axios Demo</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <script src="https://unpkg.com/vue-router@2.0.0"></script>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    />
    <style>
      .section {
        margin-top: 2em;
      }
    </style>
  </head>
  <body>
    <div class="wrapper">
      <footer
        style="
          background-color: rgb(236, 236, 236);
          position: fixed;
          width: 100%;
          font-size: 80%;
          margin-top: 5em;
          padding-top: 5px;
          padding-bottom: 5px;
          bottom: 0;
        "
      >
        <div class="container">
          <i>© 2020.</i>
        </div>
      </footer>
    </div>
  </body>
</html>

Add Script in index.html

Initiate vue and use a couple of routes using router. Include axios from CDN.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

<script>
  Vue.component("Navigation", Navigation);

  const routes = [
    {
      path: "/",
      component: Access,
    },
    {
      path: "/content",
      component: Content,
    },
  ];

  const router = new VueRouter({
    routes: routes,
    mode: "history",
    base: "/",
  });

  var app = new Vue({
    el: "#app",
    router: router,
    data() {
      return {
        authstatus: false,
        apiurls: {
          authget: "http://localhost:9000/auth",
          authpost: "http://localhost:9000/auth",
          postget: "http://localhost:9000/posts",
        },
      };
    },
    methods: {
      setAuthStatus(val) {
        console.log("Setting authstatus through event to: ", val);
        this.authstatus = val;
        console.log("New value: ", app.authstatus);
      },
    },
  });
</script>

As you can see, we created a couple of variables and a method. These will be used by more than one components that we will create next.

Also note that we used mode: "history". This will necessitate using a server to serve the index.html file. If you don’t want that, use mode: "hash".

Create Components

We will not quite use .vue files - although it can be done by using vue-http-loader package when using Vue UMD build. Instead, we will create “pseudo-components” by just defining the entire Vue template structure in an object.

Let us create three components -

  1. Get access details and authenticate
  2. Show content
  3. Navigation: this can be included in the main HTML page or in both of the above components.

Create a new file and call it Navigation.vue.js.

  1. Create the Vue template as an object (use Bootstrap for styling)
    • Show access and content links
    • Display Content links only if user has “logged in”
  2. data (and computed, methods etc.) will be objects as well, and used where relevant (almost represents real Vue single file components)

The value to check whether user has logged in comes from the root.

1
this.$root.authstatus;

Although I am not a fan of accessing variables like this, it serves its purpose in a simple setup. Use Vuex or pass values through props/events if it becomes more complex.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const Navigation = {
  template: `
      <nav class="navbar navbar-expand-md" style="background-color: #ffffff;">
        <div class="nav-brand font-weight-bold">Vue-Axios Demo</div>

        <button
          class="navbar-toggler"
          type="button"
          data-toggle="collapse"
          data-target="#navbarSupportedContent"
        >
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav ml-auto mt-2 mt-lg-0 font-weight-bold">
            <li>
              <router-link class="nav-link" to="/">
                Access
              </router-link>
            </li>
            <li v-if="authstatus">
              <router-link class="nav-link" to="content">
                    Content
              </router-link>
            </li>
           
           
          </ul>
        </div>
      </nav>
  `,
  computed: {
    authstatus() {
      //  used to show/hide `content` link
      return this.$root.authstatus;
    },
  },
};

Access

Create a new file access.vue.js.

  1. Collect email
  2. Check whether email has access through an Axios call to our dummy service
  3. Disable form on successful authentication
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
const Access = {
  template: `
    <div>
        <Navigation/>
        <h1>Access</h1>

        <div class="error" v-if="validationErrors.length > 0" style = "color:red; margin-top: 2em; margin-bottom: 1em;">
            {{ validationErrors }}
        </div>
        <div v-else style="margin-top: 2em; margin-bottom: 1em;">&nbsp;</div>

        <form @submit.prevent="validateAndSubmit" id="formLogin">
        
          <div class="field section" style="margin-top:3em">
              <label for="email" >Email:</label>
              <input type="text"  name="email" v-model="email" @input="validate" :disabled="authstatus"/>
              <button style="margin-left:1em" type="submit" :disabled="authstatus">Access</button>
          </div>

          <div class="section">
              <i v-if="authstatus">You have access! </i>
              <i v-else>Verify email to gain access.</i>
          </div>

        </form>
    </div>`,

  data() {
    return {
      email: "",
      validationErrors: [],
    };
  },
  computed: {
    authstatus() {
      // this way of sharing values is ok for small projects.
      // large projects must use Vuex
      return this.$root.authstatus;
    },

    urls() {
      // fetch URLs from root
      return this.$root.apiurls;
    },
  },
  mounted() {
    this.fetchAuthStatus(); // check auth on mount
  },
  methods: {
    async fetchAuthStatus() {
      // triggered at load. Relevant only when you have a session.
      // for JWT and similar: check against token, and pass refresh token if expired
      console.log("Fetching auth status..");
      const res = await axios.get(this.urls.authget, {
        crossdomain: true,
      });

      const authData = res.data;
      console.log(`GET auth done!`, authData);

      if (authData["auth-status"]) {
        console.log("Auth approved!");
      } else {
        // this is silent error since user will ..
        // .. most likely not have access the first time the screen loads
        console.log("Auth denied..! Re-login.");
      }
      this.$emit("auth-verified", authData["auth-status"]);
    },
    async login() {
      try {
        // main login flow
        const res = await axios.post(this.urls.authpost, { email: this.email });
        const authData = res.data;

        console.log(`POST auth done!`, authData);

        if (!authData["auth-status"]) {
          this.validationErrors.push(
            "Could not verify email. Validate and resubmit."
          );
        }
        this.$emit("auth-verified", authData["auth-status"]);
      } catch (e) {
        this.validationErrors.push[e.message];
        console.error(e);
      }
    },
    validateAndSubmit() {
      // .. validate before sending the request off to server.
      this.validationErrors = [];
      if (this.validate()) {
        this.login();
      }
    },
    validate() {
      const errors = [];
      const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
      if (!pattern.test(this.email)) errors.push("Invalid email.");

      if (errors) this.validationErrors = errors;
      else this.validationErrors = [];

      return !this.validationErrors.length;
    },
  },
};

Content

Create a new file content.vue.js.

Include -

  1. template, data and methods
  2. logic to call an external service to get posts

Like earlier, we will fetch variables from the root by using this.$root.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
const Content = {
  template: `
    <div>
      <Navigation/>
      <h1>Content</h1>
      
      <div class="error" v-if="validationErrors.length > 0" style = "color:red; margin-top: 2em; margin-bottom: 1em;">
        {{ validationErrors }}
      </div>
      <div v-else style="margin-top: 2em; margin-bottom: 1em;">&nbsp;</div>

      <div class="field section" style="margin-top:3em">
          <label for="keywords" >Keywords:</label>
          <input type="text"  name="keywords" v-model="keywords"/>
          <button style="margin-left:1em" @click="">Search</button>
      </div>
      
      <div class="field section">
        <label for="posts"><b>Posts</b></label>
      </div>

      <div class="posts" style="margin-left:30%; text-align: left!important">
        <div v-for="(post,index) in posts" :key="index" >
          {{post.id}}. {{post.description}}
        </div>
        <i v-if="!posts || posts.length == 0">No content found.</i>
      </div>
      
      <div class="section" style="margin-top:1em">
        
      </div>

        
    </div>`,
  data() {
    return {
      keywords: "",
      posts: [],
      validationErrors: [],
      currentPage: 0,
      totalPages: 0,
    };
  },
  computed: {
    authstatus() {
      // large projects must used Vuex. This will work for small components and data sets.
      return this.$root.authstatus;
    },

    urls() {
      // a simple way to standardize URLs from root
      return this.$root.apiurls;
    },
  },
  mounted() {
    this.fetchPosts();
  },

  methods: {
    async fetchPosts() {
      if (!this.authstatus)
        // authstatus check here is not quite required, but is just a fall-back
        this.validationErrors.push(
          "Not logged in. You have to login to fetch posts."
        );
      else {
        console.log("Fetching posts..");
        // keywords passed as query - as-is.
        // verify whether server requires any specific format
        const res = await axios.get(
          `${this.urls.postget}?query=${this.keywords}&page=${
            this.currentPage + 1
          }`,
          {
            crossdomain: true,
          }
        );

        const postData = res.data;
        console.log(`GET posts done!`, postData);

        if (!postData["auth-status"]) {
          // auth-status is returned by post requests too!
          console.log("Auth denied..! Re-login.");
          this.validationErrors.push("Access is denied.");
        }

        this.posts = postData["posts"] ? postData["posts"] : [];
        this.currentPage = postData["currentpage"]
          ? postData["currentpage"]
          : 0;
        this.totalPages = postData["totalpages"] ? postData["totalpages"] : 0;

        // pagination logic yet to be implemented to navigate to subsequent pages
        console.log(".. posts fetched!");
      }
    },
  },

  validateAndSubmit() {
    //  there are no validations at this time.

    this.validationErrors = [];

    this.fetchPosts();
  },
};

Index.html

We will change index.html to include -

  1. the newly developed components
  2. common variables used across components

Change the script section with following code -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<script src="./navigation.vue.js"></script>
<script src="./access.vue.js"></script>
<script src="./content.vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

<script>
  Vue.component("Navigation", Navigation);
  Vue.component("Content", Content);
  Vue.component("Access", Access);

  const routes = [
    {
      path: "/",
      component: Access,
    },
    {
      path: "/content",
      component: Content,
    },
  ];

  const router = new VueRouter({
    routes: routes,
    mode: "history",
    base: "/",
  });

  var app = new Vue({
    el: "#app",
    router: router,
    data() {
      return {
        authstatus: false,
        apiurls: {
          authget: "http://localhost:9000/auth",
          authpost: "http://localhost:9000/auth",
          postget: "http://localhost:9000/posts",
        },
      };
    },
    methods: {
      setAuthStatus(val) {
        console.log("Setting authstatus through event to: ", val);
        this.authstatus = val;
        console.log("New value: ", app.authstatus);
      },
    },
  });
</script>

And ta da.. your application is ready. Just serve the directory that has index.html. For e.g. we can use a quick http server using http-server:

npm i -g http-server
http-server

If you use mode: "hash", you can simply open index.html to see the magic.

How it works?

  • Parent HTML page instantiates Vue and mounts in on a div.
  • Three JS files that define Vue templates, methods, et al. as objects.
  • Component objects defined in the external JS files are imported and used in parent HTML page.

Not quite beautiful or modular, but it works!

See this code on Github

Conclusion

Using Vue from CDN can certainly play a role in super simple projects when there are people in the ecosystem who are not quite onboard with Vue, and want to understand how things work by seeing a simple project.

But, I find it cleaner to just use Vue CLI and its single file components. While Vue CLI does bring in complexity in build processes, the overall structure is easier to understand and demonstrate to a technical audience.

Stay in touch!
Share on

Prashanth Krishnamurthy
WRITTEN BY
Prashanth Krishnamurthy
Technologist | Creator of Things