This is part of a series explaining how to use VueJS to create a transplation-free client-side templating architecture. I'm not claiming this is The Best Way to create a UI, but it's a good thought experiment for minimizing ceremony.
- Part 1: VueJS introduction -- Progressive, observables, loops, & ifs
- Part 2: Components, Templates Types, and SPAs (unedited)
- Part 3: Using Render Functions & HTML Files (to come)
I heard about VueJS a while back on from Shawn Wildermuth guesting on Yet Another Podcast, and figured I'd take a look. The most interesting thing about it was its claim to be a "progressive framework"...
From vuejs.org:
Vue (pronounced /vjuː/, like view) is a progressive framework for building user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable. The core library is focused on the view layer only, and is easy to pick up and integrate with other libraries or existing projects. On the other hand, Vue is also perfectly capable of powering sophisticated Single-Page Applications when used in combination with modern tooling and supporting libraries.
Incremental adoption, you say? As in, I could use its templating without any other libraries? RLY? I mean, Vue bills itself to be a pretty mature lib. Heck, you can go full-bore, Babel and JSX with VueJS if you want to. That marks a sort of max progression. Can we find a similarly minimally progressive stack?
Once burned...
Last year, I spent most of my time working on a system that used React, JSX, and MobX using Babel for transpilation. It wasn't a horrible client-side stack, but I've never wasted so much time on build systems since I was new to ant. We updated and debugged with each new release of anything of those three, and it drove me crazy with what Shawn Wildermuth calls the "ceremony" of coding -- all the magic incantations and rituals you have to go through and maintain before working code comes out.
Using transpilation and riding the bleeding edge instead of writing deployable code means everything you do costs you -- let's say -- an extra 10%, and, in the wrong hands, more. Do you really get that time back with transpilation?
Just because everyone else is putting together complex JavaScript build processes (and they are. Projects that don't include "the dev who maintains the build" are becoming few and far between, and that dev is always front and center on interview calls pretending their role is absolutely critical to a project's success), do we have to go full Cranberries? (pro tip: see album title, young'uns)
I don't think so. I'm not against transpilation, if it's well managed. But if I can use a library that doesn't require transpilation, but can still give me a good templating system, I'm curious. Just for fun, let's see if VueJS can keep us transpilation-free.
How progressive is VueJS, exactly?
NOTE: If you already know Vue, stay with me. Skim the balance of this so you know where I'm coming from, and then stop at the end, where I start talking about what's missing. Then tune in next time, for Part 2, where I fill in what's missing & seal the deal.
Let's take the minimal VueJS setup as described in their introductory guide, where we assume zero libraries other than Vue are present.
What's below is the minimal Vue setup from their guide, with just enough changed so that we...
- Have something that'll work in a live example
- Include a form element to test our binding
- Have a button to access our data programmatically (standing in for a call back to the server)
- Wait until the DOM has loaded before initializing to begin templating
- If we were using true Vue templates, we wouldn't have to do this. More on that in Part 2.
- Some gymnastics to pass JSLint.
Excuse the last. It's an unshakeable addiction.
(That said, you should use JSLint too. ;^D)
/*global Vue, window */
document.addEventListener("DOMContentLoaded", function() {
"use strict";
window.app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
methods: {
report: function () {
// Note that it's app.property, not app.data.property
console.log(JSON.stringify(window.app.message, null, " "));
}
}
});
});
And the HTML needed to match is super simple.
<html>
<head>
<title>Look how simple</title>
<script src="../vue.js"></script>
<script src="./app.js"></script>
</head>
<body>
<div id="app">
{{ message }}<br />
<input
type="text"
v-model="message"
/><br />
<button
type="button"
v-on:click.prevent="report"
>Add new</button>
</div>
</body>
</html>
And poof! We've got an interactive, VueJS-templated web page.
That's... great. Right?
Vue does observables right
Actually, it is great, kinda. data
is just a datastore. Vue wraps it in observable overhead, so in many ways, it reminds me, at this level, of KnockoutJS. It's advantage? That we're not wasting time dealing with any of that overhead ourselves, in code or our mental model. Knockout had this horrible convention where you had to call a function to get a "raw" observable, which often threw people for loops).
Vue beats out KnockoutJS easily here. You can very simply put in properties to serve as hooks in your data
object to hang all of your datastores, and when you set them, they iterate through your object models and make everything an observable too. If you set message
, above, to some giant json model, all of that model would be observable. And each property would be accessible in plain old javascript object ("POJsO") notation.
/*global Vue, window */
document.addEventListener("DOMContentLoaded", function() {
"use strict";
window.app = new Vue({
el: '#app',
data: {
payload: {
b: { c: 2 }
}
},
methods: {
report: function () {
// Note that it's app.property, not app.data.property
console.log(JSON.stringify(payload.b.c, null, " "));
}
}
});
// Pretend some event caused this.
window.app.payload = {
a: 1,
b: {
a:"a",
b:"b",
c:"c"
},
c: 2
};
});
And then a quick change to our html...
<div id="app">
{{ payload.b.c }}
</div>
That, I like. That's how you should do observables. It's the same mental model as a plain ole JavaScript object. There's no "oops, that's an observable! Remember to treat it like a function!" overhead. It's simply magic.
Okay, okay, I haven't looked, but it's almost certainly some getter and setter overhead, which works like magic here. That's why it's important to note that window.app.data
is inaccessible. What we don't want to do is blow up our observable chain.
How not to observe...
NOTE: The follow-on from that is that you can't do this:
window.app.payload.d = "spam"
and expect spam
to be observable. You have to set something that's already an observable, or the extra setter logic won't fire. That just makes sense.
But you can pick up the observable train wherever you want...
NOTE: You really shouldn't be updating your data
object's object model for the most part, however. That's a code smell, imo. You'll be getting nice large JSON payloads from your server, and you might have some navigation and convenience objects for managing views, but that's about it. If you make changes to data, that should be going back to the server. And if you're changing the data model on the client, that's probably a business rule that should've been handled on your server-side, not the client-side, where your interest should be almost exclusively presenting views.
Keep your interests separated.
Moving towards an SPA
There are really only two more things you need to know to get pretty close to making minimalistic single page apps (SPAs) with VueJS: conditionals and loops.
Loops with v-for
Loops are easy. If you have an array of objects in your datastore, you can loop through them easily with v-for
, repeating the markup within that marked node as many times as you have objects.
So if you had data = { stooges: ["Larry", "Moe", "Curly", "Shemp"] }
, your markup would be something similar to:
<div id="app">
<ul>
<li v-for="stooge in stooges">
{{ stooge }}
</li>
</ul>
</div>
Easy. And if your stooges were objects instead of strings, that's done just like you'd expect, thanks to the POJsO setup Vue supports.
data = {
stooges: [
{ name: "Larry", desc: "the curly-headed one." },
{ name: "Moe", desc: "the relatively smart one." },
{ name: "Curly", desc: "the bald one. See what they did there, with Larry having the curly hair?" },
{ name: "Shemp", desc: "the one nobody knew in the 70s, but somehow everyone did starting around 1990." }
]
};
And change the markup to match:
<div id="app">
<ul>
<li v-for="stooge in stooges">
{{ stooge.name }} is {{ stooge.desc }}
</li>
</ul>
</div>
Conditionals with v-if
Now let's look at conditionals. If you only want something to be rendered when some condition is true, you plant that portion of your markup in a conditional.
<html>
<head>
<title>Animal types and examples</title>
<script src="../vue.js"></script>
<script src="./app.js"></script>
</head>
<body>
<div id="app">
Display all animals of type: <input
type="text"
v-model="activeUi"
/><br />
<div v-if="activeUi === 'cats'">
<h3>Cats</h3>
<ul>
<li v-for="cat in cats">
{{ cat }}
</li>
</ul>
</div>
<div v-else-if="activeUi === 'dogs'">
<h3>Dogs</h3>
<ol>
<li v-for="dog in dogs">
{{ dog }}
</li>
</ol>
</div>
<div v-else>
<h3>Neither dogs nor cats</h3>
<ul>
<li v-for="x in others">
{{x.name}}, a {{x.type}}
<!-- And note that we can embed ad infinitum, natch -->
<ul v-if="x.type === 'rooster'">
<li>"That's a joke, I say, that's a joke son!"</li>
</ul>
</li>
</ul>
</div>
</div>
</body>
</html>
You're half-way there. To an SPA, that is.
If you use conditionals with reckless aplomb, you could force your way to a complete SPA. No, really, you're done. You could have every portion of your app in some div marked with the proper conditional, and have everything appear iff it's supposed to.
There're just two problems. At some point, it's going to be very difficult to keep this DRY. You will eventually want to use the same markup (HTML) and business logic (JavaScript) in two places that are each embedded deep within some other portion of HTML.
And, well, that's HTML is already a mess. It's sprawling and difficult to grok quickly, which means it'll be difficult to maintain.
Wouldn't it be nice if we could cut that previous html down with some custom components, like this?
<html>
<head>
<title>Animal types and examples</title>
<script src="../vue.js"></script>
<script src="../components.js"></script>
<script src="./app.js"></script>
</head>
<body>
<div id="app">
Display all animals of type: <input
type="text"
v-model="activeUi"
/><br />
<cats store="cats" v-if="activeUi === 'cats'" />
<dogs store="dogs" v-else-if="activeUi === 'dogs'" />
<other store="others" v-else />
</div>
</body>
</html>
... and to do it without transpilation?
We can. And we will. In Part 2.