Friday | 29 MAR 2024
[ previous ]
[ next ]

Using Vue Without a Build Step

Title:
Date: 2022-02-23
Tags:  

One of things I really like about web development is that everything is still pretty simple at it's core. We can make web development as complex as we want but ultimately it's still the same old, same old. I stick with html, css and javascript, using jquery if I need to do anything fancy. I like this because it keeps the code small and plain and there is no set up or maintenance headaches. Anyone can use the browser's inspect tool and start looking at the code. I also try to write code that is simple and straightforward in what it does. After all the goal is that someone else can start making changes if they need to. This system has served me well but now I write more and more applications for the web rather than just websites. The issue now is that code often ends up in a spaghetti state over time. I have tried using various frameworks but such react, angular, svelte and a few others but usually I give them up because of the pain of getting started with them.

These frameworks have a build step and require a build toolchain which usually is an extra load. I can see how they are useful and development is quite fun once you get everything going but getting to that step is a barrier than I would rather just not have. Now that I'm giving vue a shot, I think I've found a framework that does pretty much exactly what I want!

It can be used without a build step and it enforces a structure to your code that makes sense. There is the cost of shipping over an entire framework but I'm lucky enough all the applications I work on are internal projects in the first world.

This is still only the first few days of me using Vue and I haven't done too much with it yet but I think it will be helpful to lay out how I've built my test project. It'll be interesting to come back later and see how I feel about vue.

The github has the code:

https://github.com/Krowemoh/vue3-without-build

Table of Contents

  1. Hello World
  2. Tables
  3. Fetching Data
  4. Searching a Table
  5. Sorting
  6. Conditional Styling
  7. Vue Components without a Build Step
  8. Vue Components without a Build Step - Better Way

First Steps

The first step is to get our base index.html page running.

<!DOCTYPE html>
<html lang="en">
  <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Vue3 Test</title>
        <link rel="icon" href="data:;base64,=">
  </head>
  <body>
        <h1>Hello, World!</h1>
  </body>
</html>

Here we have our base html page.

Now let's add vue. The great thing is we can simply include the vue file like jquery and we can begin using the framework.

    <body>
        <script src="https://unpkg.com/vue@3"></script>

        <div id="app">
            <h1>Hello, {{name}}!</h1>
        </div>

        <script>
            Vue.createApp({
                data() {
                    return {
                        name: 'Nivethan'
                    }
                }
            }).mount('#app')
        </script>
    </body>

Voila! We actually have a pretty good showing of how vue works now. We use the createApp function in Vue to set up the application and one of the things we do is we set up the data function. This data function is where variables relevant to our code will exist. We then mount our Vue application to the specific element in out html by using the id.

Magically, the name attribute in data is now matched to the name in out h1 tag. The curly brackets are used for interpolation which means that the stuff inside the brackets is evaluated and replaced with its true value. This is very much traditional templating.

A Vue3 Tutorial - 02 Tables

Now that we have the basics, let's wire up a table. The scenario would be that we get data from some data source and we want to have it display in a table. We also want to search on this table, sort this table and maybe even make a few selections about what to display.

For now, however let's just get the table working.

The Data

The first step is to wire in the data. We are going to fake the data for now, so we can just add in a list into our data function.

(The data was take from the datatables site, great library.:))

data() {
    return {
        name: 'Nivethan',
        workers: [
            { name: "Airi Satou", position: "Accountant", office: "Tokyo", age: 33},
            { name: "Angelica Ramos", position: "Chief Executive Officer (CEO)", office: "London", age: 47 },
            { name: "Cedric Kelly", position: "Senior Javascript Developer", office: "Edinburgh", age: 22 },
            { name: "Jennifer Chang", position: "Regional Director", office: "Singapore", age: 28 },
        ],
    }
}

We create an array called workers and now we can use this in the main html.

The View

Now that we have the data available, it's time to display it.

<table>
    <thead>
        <th>Name</th>
        <th>Position</th>
        <th>Office</th>
        <th>Age</th>
    </thead>
    <tbody>
        <tr v-for="worker in workers">
            <td>{{worker.name}}</td>
            <td>{{worker.position}}</td>
            <td>{{worker.office}}</td>
            <td>{{worker.age}}</td>
        </tr>
    </tbody>
</table>

This is pretty straightforward html, the only thing that should give you pause is the v-for tag. This is like the curly brackets from before. The v-for tag takes in a loop and inside it looks like you can use javascript variable names. This is a bit weird as it's inside the quotes so I'm curious if traditional ranges and calculations work inside the quotes. It could be that v-for attribute is quite limited.

The v-for tag is going to create as many rows as there are workers. We can get the index if we need it by doing the following.

    <tr v-for="(worker, index) in workers">

Now if we refresh the page we should see a table with out users! Usually I would use a function to hold a template string and then loop and build up that string and then append it to the dom. This is already loads better than that strategy. I really like this style of being able to embed the template logic directly into the html rather than have it be in the javascript.

A Vue3 Tutorial - 03 Fetching Data

Now that we can display the data, we have one issue. The data is hardcoded into our code. In the real world, we fetch the data and only display the data if it's available. We aren't going to set up an actual server and do a real fetch but we can abstract everything so that it will be simple to add a fetch statement later.

The first thing we need to do is move our workers variable elsewhere where we can pretend to fetch it. We then need to get this fetched data once our application loads.

Vue.createApp({
    data() {
        return {
            name: 'Nivethan',
            workers: [],
        }
    },
    methods: {
        async getWorkers() {
            const workers = [
                { name: "Airi Satou", position: "Accountant", office: "Tokyo", age: 33},
                { name: "Angelica Ramos", position: "Chief Executive Officer (CEO)", office: "London", age: 47 },
                { name: "Cedric Kelly", position: "Senior Javascript Developer", office: "Edinburgh", age: 22 },
                { name: "Jennifer Chang", position: "Regional Director", office: "Singapore", age: 28 },
            ];
            this.workers = workers;
        }
    },
    mounted() {
        this.getWorkers();
    }
}).mount('#app')

The first thing we do is change our workers variable in data to be an empty array. We still need to initialize it.

The next thing we do is add a new parameter to our vue application called methods. This is an object that will hold functions we can call from within the application.

We can write the async function getWorkers which we will contain the fetching code. Here we have the workers still hardcoded but we can easily swap this out for an awaited fetch call.

Finally we add one more function our vue object. This is the mounted function, this goes outside the methods as this is a vue specific function like data. The mounted function will run once our application is ready and it will call getWorkers.

Once getWorkers runs, it will populate the workers variable and voila! Our screen should update with our new data.

If we refresh the screen, not much will have changed but we'll know, deep down, inside we'll know!

Before we wrap up this section, it would be nice to be able to show a message for when we don't have workers yet.

We can do this by using conditionals.

<div v-if="workers.length === 0">No workers available.</div>

<div v-else>
    <table>
        ...
    </table>
</div>

We check to see if the array has anything in it and then print out a message if there is none. Otherwise, we can display the table.

The v-else could go on the table but I prefer using divs.

To test this logic, you can remove the assignment of workers to this.workers in our getWorkers function.

With that, we have our application ready to fetch data and display it!

A Vue3 Tutorial - 04 Searching a Table

Now that we fetched our data and displayed it, it's time to add a search bar.

<div v-else>
    <input v-model="searchString" placeholder="search" class="mb-1">
    <table>
    ...
    <table>
</div>

We use the v-model tag to bind a form element to a javascript variable. The variable name searchString in the input tag will correspond with a variable called searchString in our Vue application.

data() {
    return {
        searchString: "",
        workers: [],
    }
},

Now as we type things in or delete things, the variable in the vue application will also be updated. We want our search to happen instantly as the person types, so now we are going to use the computed property of our vue application.

Like the methods property, computed is also a property but functions defined inside this object will be run anytime anything inside the function changes value.

data() {
    ...
},
computed: {
    filteredWorkers() {
        const filteredWorkers = this.searchString === ""
            ? this.workers
            : this.workers.filter(wo => Object.values(wo).join("").indexOf(this.searchString) !== -1);
        return filteredWorkers;
    },
},
methods: {
    ...
}

Here we write a function called filteredWorkers which will reference the searchString variable in our application. We then do a very poor search of this string in the values of our array of objects.

Once we have our list filtered we can then return this array.

Now instead of displaying all of the workers, what we want to display is this filtered list. When the search is blank, we'll then display everything.

    <tr v-for="worker in filteredWorkers">

We can now reference our computed function instead of our workers variable and everything should be good to go!

As we type, out searchString variable is being updated, which in turn will trigger the computed functions that reference searchString to be re-run, which ultimately leads to the table being re-rendered each time we type. Quite magical!

A Vue3 Tutorial - 05 Sorting

Now that we have searching done, the next step is to add sorting. We're going to use the same logic from before. We want to be able to click on a header and update a sortColumn variable which will in trigger a new compute. We can actually use our filteredWorkers function as we want the search and sort to stack on top of each other.

data() {
    return {
        sortColumn: "",
        order: "ASC",
        searchString: "",
        workers: [],
    }
},

We also want to keep track of the order as we want to reverse the sort if someone clicks the same header twice.

Now the next step is to add our click events.

<thead>
    <th @click="setSortColumn('name')">Name</th>
    <th @click="setSortColumn('position')">Position</th>
    <th @click="setSortColumn('office')">Office</th>
    <th @click="setSortColumn('age')">Age</th>
</thead>

The @ is a shorthand for the v-on:click which binds the event to a function. We are going to run a function called setSortColumn passing in the column's name.

methods: {
    setSortColumn(column) {
        if (this.sortColumn === column) {
            this.order = this.order === "ASC" ? "DESC" : "ASC";
        } else {
            this.order = "ASC";
            this.sortColumn = column;
        }
    },
    ...
}

We put the setSortColumn in the methods section instead of the computed section because this is something we want to manually control.

Here is also where we have logic that checks to see what the current sortColumn is before changing the ordering of the results.

Now we have everything ready to actually implement our sort!

computed: {
    filteredWorkers() {
        const filteredWorkers = this.searchString === ""
            ? this.workers
            : this.workers.filter(wo => Object.values(wo).join("").indexOf(this.searchString) !== -1);

        const column = this.sortColumn
        const order = this.order;

        filteredWorkers.sort(function(a, b) {
            var nameA = a[column]+"".toUpperCase();
            var nameB = b[column]+"".toUpperCase();
            if (order === "DESC" && nameA > nameB) {
                return -1;
            }
            if (order === "DESC" && nameA < nameB) {
                return 1;
            }
            if (nameA < nameB) {
                return -1;
            }
            if (nameA > nameB) {
                return 1;
            }
            return 0;
        });

        return filteredWorkers;
    },
},

We do our filtering first with our search string. We then run our sort function and our ordering of the results. With that we are done!

We have now wired up our sort to work when we click the headers and we can also search as well.

A Vue3 Tutorial - 06 Conditional Styling

Now that we have sorting done, let's add a sort icon to the header. Before we do that we should clean up the header as it's starting to get unwieldy. Ideally, we want the header to also be dynamic.

We can update our getWorkers function to also get the headers.

async getWorkers() {
    const workers = [
        { name: "Airi Satou", position: "Accountant", office: "Tokyo", age: 33},
        { name: "Angelica Ramos", position: "Chief Executive Officer (CEO)", office: "London", age: 47 },
        { name: "Cedric Kelly", position: "Senior Javascript Developer", office: "Edinburgh", age: 22 },
        { name: "Jennifer Chang", position: "Regional Director", office: "Singapore", age: 28 },
    ];

    const headers = [
        { key: "name", value: "Name" },
        { key: "position", value: "Position" },
        { key: "office", value: "Office" },
        { key: "age", value: "Age" },
    ];

    this.headers = headers;
    this.workers = workers;
}

Next, we update the data variable to have a headers variable.

data() {
    return {
        sortColumn: "",
        order: "ASC",
        searchString: "",
        headers: [],
        workers: [],
    }
}

Finally we can update the html to use our new variable!

<thead>
    <th v-for="header in headers" @click="setSortColumn(header.key)">
        {{header.value}}
    </th>
</thead>

Now that we have that done, we can now add some arrows to show the sort.

<thead>
    <th v-for="header in headers" @click="setSortColumn(header.key)">
        {{ header.value }}
        <span class="arrow" :class="{ active: this.sortColumn === header.key && this.order === 'ASC'}">
            ↑
        </span>
        <span class="arrow" :class="{ active: this.sortColumn === header.key && this.order === 'DESC'}">
            ↓
        </span>
    </th>
</thead>

Here we are using the unicode characters for the up and down arrows.

We also have :class binding now which will conditionally add a class to an element. In this case, we check to see which column we are sorting on and the order when we set the active flag.

We can also include styling in the html file which will be specific to this component.

<style>
th { cursor: pointer; }
.arrow { color: gray; }
.active { color: black; }
</style>

With that! We have a decent enough header. When we click on a column, we'll see our active class flipping between the two states of ordering and it will also tell us which column we are sorting on.

A Vue3 Tutorial - 07 Vue Components without a Build System

Edit - This section lets you write a .vue file and use the from the browser. This works but a better or at least cheaper way to do this would be to to use the vuejs way of importing things in the next chapter.

Now we are at the point where we can be dangerous. We know enough about Vue to do something useful however there is one more thing I really wanted from a web framework besides having no build system. I really want something that is modular and composable.

One of the things I really liked about react was the ability to write self contained code and build up my own custom tags where all you need to do is pass in props and you'll get a well formed and working set of elements. Vue has this as well and I imagine most frameworks do. You could also make this in plain javascript but ultimately you'll end up create your own custom framework.

Ideally I want to be able to create Vue components and use them in the browser. Unfortunately there isn't a way to do that through vue. ! This chapter wouldn't exist if that was impossible though.

Someone has created a small library to load vue components. This makes it pretty easy to create components and pass props to them. I'll need to dig into it to do more complex things but it works pretty well.

https://github.com/FranckFreiburger/vue3-sfc-loader

In this chapter, we're going to take our table and create a component out of it!

Clean Up

The first thing we need to do is remove the code that is specific to the table and move it all into a new file called table.vue. This file will be slightly different than what we've been doing. Instead of calling createApp, our vue file simply export everything that would go inside a createApp regularly.

export default {
    props: ["headers", "workers"],
    data() {
        return {
            sortColumn: "",
            order: "ASC",
            searchString: "",
        }
    },
    computed: {
        filteredWorkers() {
            ...
        },
    },
    methods: {
        setSortColumn(column) {
            ...
        },
    },
}

Here we have the data, computed and methods properties being set but now we only keep the stuff that is relevant to the table.We also have a new property called props which will contain a string of the keys we want to pass through. The parent component will pass in a variable called headers and a variable called workers when this table component is used.

Next we add the template code to our vue component.

<template>
    <div v-if="workers.length === 0">No workers available.</div>

    <div v-else>
        <input v-model="searchString" placeholder="search" class="mb-1">
        <table>
            ...
        </table>
    </div>
</template>

Finally we also move the styles over to table.vue.

<style>
th { cursor: pointer; }
.arrow { color: gray; }
.active { color: black; }
</style>

Now our table component has everything it needs to work. The next step is to now clean up the index.html file. Once the index file only contains what it needs, we can work on the code to load in the table component.

<body>
    <script src="https://unpkg.com/vue@3"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader/dist/vue3-sfc-loader.js"></script>
    <div id="app">
        <h1>People!</h1>
    </div>
    <script>
        Vue.createApp({
            data() {
                return {
                    headers: [],
                    workers: [],
                }
            },
            methods: {
                async getWorkers() {
                    ...
                }
            },
            mounted() {
                this.getWorkers();
            }
        }).mount('#app')
    </script>
</body>

Using vue3-sfc-loader

The first step is to include the vue3-sfc-loader. This will let us use .vue files directly in the browser.

<body>
    <script src="https://unpkg.com/vue@3"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue3-sfc-loader/dist/vue3-sfc-loader.js"></script>
    ...
</body>

Next we need to set up the options and import in the loadModule function.

const options = {
    moduleCache: {
        vue: Vue
    },
    async getFile(url) {
        const res = await fetch(url);
        if ( !res.ok )
            throw Object.assign(new Error(res.statusText + ' ' + url), { res });
        return {
            getContentData: asBinary => asBinary ? res.arrayBuffer() : res.text(),
        }
    },
    addStyle(textContent) {
        const style = Object.assign(document.createElement('style'), { textContent });
        const ref = document.head.getElementsByTagName('style')[0] || null;
        document.head.insertBefore(style, ref);
    },
}

const { loadModule } = window['vue3-sfc-loader'];

Vue.createApp({
    ...
}).mount('#app');

I'm guessing that the reason we have getFile and addStyle here is that we may want to customize these functions but they work as is.

Now that we have vue3-sfc-loader ready, we can now start using components!

Vue.createApp({
    data() {
        return {
            headers: [],
            workers: [],
        }
    },
    components: {
        'Table': Vue.defineAsyncComponent( () => loadModule('./table.vue', options) )
    },
    template: `<Table :headers="headers" :workers="workers"></Table>`,
    methods: {
        ...
    },
    mounted() {
        ...
    }
}).mount('#app');

We specify the component we want to use in the components attribute and then in the template attribute we actually reference it. It's curious that it works with an uppercase Table name even though I didn't specify it. For now I'll choose to ignore it but if anyone has answer, please comment!

Now we can pass in props by using the colon followed by the property to set up a binding. In our component, because we have the props attribute set up, we can then start using these variables that we passed through.

Voila! If everything was done properly, you should now have a single component file that you can include and use from the browser.

We can now use vue SFCs without a build step!

At this point, this is pretty much everything I know about Vue. So not much but enough to get started. Much of this might be the wrong way of doing things but I certainly like this. There is no build step involved and everything is in a file that is well structured. The only drawback is the filesize of the things that are transferred.

Vue is 600kb and vue3-sfc-loader is 1.4mb. So to make applications with the core idea being that there is no build step means shipping 2mb of javascript to the clients machine. This is their raw size, zipped, this comes out to 800kb which is still quite a bit. All that code needs to still be read and compiled before my application even starts.

I'll need to think about it for bit and try it out some more before I can really go all into it.

Overall, Vue was pretty easy to get and start using which was nice, React really did require more effort but it could be that react set me up well to pick up vue.

! Until next time.

A Vue3 Tutorial - 08 Vue Components without a Build System 2 (A Better Way)

Once again bit by the bug of not reading the manual enough! I ended up spending time at the car dealership with time to kill and read the vue guide. Eventually I got to the part about components and found that you could already load vue components in the browser without a build system!

After giving it look, it seemed like exactly what I want with 1 major drawback from what I can tell. Instead of writing .vue file you'd need to write a js file. Besides that, it seems pretty much the same. The big gain here would be the fact that you don't need to ship vue3-sfc-loader and it would cut out some of the code in the main file.

First, we can convert our table.vue file to table.js. Next we move the styles in table.js to the index file. I haven't figured out how to do styles scoped to a component yet.

Now the next step is to put the template tag inside a javascript variable. We can easily do this by using template literal strings.

const template = `
    <div v-if="workers.length === 0">No workers available.</div>

    <div v-else>
        ...
    </div>
`;

export default {
    props: ["headers", "workers",],
    data() {
        return {
            sortColumn: "",
            order: "ASC",
            searchString: "",
        }
    },
    template: template,
    ...
}

We also set the template property on our Vue config object that we export out.

With that, we are done setting up our component for use in the browser. The changes are pretty superficial, so changing this to be a .vue file would be very easy.

The next thing to do is we'll load our component in our index file.

<script>
    Vue.createApp({
        data() {
            return {
                headers: [],
                workers: [],
            }
        },
        components: {
            'Table': Vue.defineAsyncComponent( () => import('./table.js'))
        },
        template: `<Table :headers="headers" :workers="workers"></Table>`,
        methods: {
            ...
        },
        mounted() {
            this.getWorkers();
        }
    }).mount('#app')
</script>

Here we can remove all the code relevant to vue3-sfc-loader and just set up the components variable to reference our table.js file.

With that we have the ability to use components in the web!

This is much simpler! Vue has a great guide, not quite at the level of some of the best documentation but its pretty thorough and explains things well. I should have read it earlier as I did learn quite a bit.