Quench Vue
Simple, tiny, client-side hydration of pre-rendered Vue.js apps
Quench Vue allows server-rendered/static markup to be used as a Vue app's data
, template
and local components' template
s. It's great for when you can't/don't want to use "real" server-side rendering.
All of Vue's existing features will work as normal when the app is initialised in the browser.
Table of Contents
- Demo
- Installation
- Usage
- Defining the app
data
andtemplate
- Defining local component
template
s - Hiding elements in the pre-rendered HTML
- Embedding additional app templates
- Benefits
Demo
A complete demo is available here: https://codesandbox.io/s/quench-vue-demo-foe55
Installation
npm
npm install quench-vue --save
<script>
include
Direct
Note: You will need to use the full build of Vue.js, which includes the compiler.
Usage
data
and template
Defining the app There are 2 ways of defining and using data
for the app:
- With a stringified JSON object in the app container's
q-data
attribute; and/or - With an inline
q-binding
attribute on an element, whenq-convert-bindings
is added to the app container.
Both techniques can be used together or on their own, but the q-data
is preferred as it's faster, simpler and more versatile.
Let's look at some examples:
data
with [q-data]
Method 1: Defining the This method allows you to easily specify the data
for the app, including arrays and objects.
…
[v-text]
Rendering the data with We obviously duplicate the "data" in the markup, and inform Vue which elements are bound to which data
properties using a v-text
attribute whose value points to a property name, such as:
Hello, World!2018 js <!-- <q> --> library <!-- </q> --> Matt <!-- <q> --> Stow <!-- </q> --> JS 4 <!-- <q> --> CSS 5 <!-- </q> -->
For iterating over lists, we also need to use another syntax, <!-- <q> --> … <!-- </q> -->
, which we'll describe later.
Note:
- You can also use
v-html
to render HTML, but ensure that it's sanitized and trusted. - You only need to output the
v-for
and thev-text
/q-binding
attributes on the first iteration of the loop.
data
with inline [q-binding]
bindings
Method 2: Defining the While we don't recommend the following approach for anything but the simplest of apps, when q-convert-bindings
is set on the app's container, we can also use the q-binding
attribute to create a data
variable that is equal to the value of the element's .textContent
.
Note:
- Bindings specified in the global
q-data
object take precedence over inline bindings. - Do not nest elements inside a
q-binding
element, or you'll have unexpected results.
The following examples all perfectly re-create the global q-data
object from before.
Simple bindings
Hello, World! 2018
Array and Object bindings
Vue supports iterating over arrays and objects via the v-for
directive with the syntax item in items
, where items
is the source data list and item
is an alias for the array element being iterated on.
To inline bind with Quench, we need to use another special syntax itemsSource as item
.
Array
To replicate the tags
array from above, we would:
js <!-- <q> --> library <!-- </q> -->
where itemsSource
is the name of the array (tags
) plus the index in the array [0]
/[1]
which we wish to populate, and tag
is the item
alias in the v-for
.
Object
To replicate the author
object from above, we would:
Matt <!-- <q> --> Stow <!-- </q> -->
where itemsSource
is the name of the object (author
) plus the relevant object key .firstName
/.lastName
which we wish to populate, and key
is the item
alias in the v-for
.
Array of Objects
Both of the above techniques can be combined, so to replicate the skills
array from above, we would:
JS 4 <!-- <q> --> CSS 5 <!-- </q> -->
where itemsSource
is the name of the array and index (skills[0]
) plus the relevant object key .name
/.level
which we wish to populate, and skill.name
/skill.level
is the item
alias in the v-for
plus the object key.
Hopefully you'll agree that using inline bindings to set the data
is more complicated than using the q-data
method, but it can still have its uses.
Note: When using inline bindings, arrays and objects are limited to a depth of 1 level.
Non-element bindings
Since v0.7.0, you can also create data
from bindings using specially formatted comments, thus not requiring to have actual elements to attach to. This can be useful if you need to create extra data
that is "invisible" to the user, or if you need to create more complex values, such as nested arrays and objects.
Using the syntax <!-- q-binding:[dataPropertyName] = [value] -->
we can easily recreate the data
from the previous examples:
<!-- q-binding:title = "Hello, World!" --> <!-- q-binding:year = 2018 --> <!-- q-binding:tags[0] = "js" --> <!-- q-binding:tags[1] = "library" --> <!-- q-binding:author.firstName = "Matt" --> <!-- q-binding:author.lastName = "Stow" --> <!-- q-binding:skills[0].name = "JS" --> <!-- q-binding:skills[0].level = 4 --> <!-- q-binding:skills[1].name = "CSS" --> <!-- q-binding:skills[1].level = 5 -->
However, we can also create more complex data
values. The following recreates the above arrays and objects succinctly.
<!-- q-binding:tags = ["js", "library"] --> <!-- q-binding:author = { "firstName": "Matt", "lastName": "Stow" } --> <!-- q-binding:skills = [{ "name": "JS", "level": 4 }, { "name": "CSS", "level": 5 }] -->
Note: Non-element bindings must be JSON-serializable and written on one line.
data
properties
Referencing global variables as You can also pass global variables (on window
) to be used as data properties. Similarly to q-data
, we can pass a stringified JSON object of key/value pairs to a q-r-data
attribute, where key is the name of the data
's property and value the name of the global variable to be used, which can also use dot notation to access properties of an object.
which will produce the following data
:
env: 'dev' port: 3000 foo: 'bar' baz: 'qux'
Note: Bindings specified in the q-r-data
object take precedence over those in q-data
.
Excluding elements from the app template compiler
In the previous sections, we introduced the <!-- <q> --> … <!-- </q> -->
syntax. These are a pair of opening and closing comments that exclude the contents within from being passed to the template compiler.
The most obvious use case (and necessary when using inline bindings) is to strip all but the first element of a v-for
loop as demonstrated earlier.
Another use case is to replace static markup for a component, such as:
<!-- <q> -->I will be stripped in the app and "replaced" with the component version below<!-- </q> -->
Note: Nesting comments is not supported.
Instantiating the app
Very little needs to change from the way you'd normally instantiate an app.
With a module bundler, such as webpack
;; var appEl = document;var data = ;var template = ; var app = el: appEl data: data template: template;
<script>
include
For direct
var appEl = document;var data = quenchVue;var template = quenchVue; var app = el: appEl data: data template: template;
template
s
Defining local component Vue applications are often comprised of multiple components, which promotes reuse and reduces repetition. However, the more components you have, the greater your JavaScript bundle will be. While complex components, such as interactive UI widgets should probably be defined in JavaScript, simpler, more display-only components can easily be defined from existing, pre-rendered HTML with Quench Vue.
Defining local component template
s from existing markup suits situations such as an infinite scroll of news cards, where the original "page" of cards are pre-rendered, and as a user scrolls, your Vue app needs to fetch and append more cards from a JSON API response.
Note: You cannot use Quench Vue to specify the template
of global components created with Vue.component()
. We consider global components an anti-pattern anyway.
[q-component]
Specifying a component with Any element within your app's el
can be used as the markup for a component by adding an attribute of q-component="NAME"
, where "NAME"
is the name of the local component defined in your Vue app.
Typically, this would be on a <div>
or similar, which sets the outerHTML
of the element to be used as the template
. However, you can also use a <template>
element as the component definition, in which case the innerHTML
becomes the template
.
For instance:
<!-- <q> --> Card<!-- </q> -->
will create a template
of:
Card
but:
<!-- <q> --> Card<!-- </q> -->
will create a template
of:
Card
Note:
- Only the first instance of
q-component="NAME"
will be used as the component'stemplate
. - Components need to be wrapped in
<!-- <q> --> … <!-- </q> -->
comments to prevent them from being included in the app's template. - We opted for a custom,
q-component
syntax (compared with Vue's nativeinline-template
syntax) for various reasons, but mainly because it allows for greater flexibility in the types of templates that can be defined (such as<tr>
s with multiple children), and we can supplement with additional features as described below.
template
Specifying a component's The previous templates aren't particularly useful, but as you'd expect, any Vue directives or template syntax can be used and will be converted, similar to the app's template above (with the exception of not supporting Quench Vue's q-binding
).
Take the following HTML:
Mario Bros And Other Nintendo Arcade Games Coming To Nintendo Switch September 13, 2017 Doom 2016 Is Coming To Nintendo Switch September 12, 2017
which will define the card
component's template
as:
template
's logic
Handling a While the previous template will handle content and style differences, it's not uncommon for a component's markup to also change based on certain conditions. There a 3 ways in which we can handle these logic requirements:
- Add the logic within the markup (often using
<template>
so they're invisible in the pre-rendered markup). - Add the logic within the component's JavaScript using a proprietary
partials
object and reference it with a special<q-component-partial name="NAME"></q-component-partial>
syntax; or - Define a completely different component.
Let's look at some examples:
Method 1: Add the logic within the markup
<!-- <q-component> --> <!-- </q-component> --> Mario Bros And Other Nintendo Arcade Games Coming To Nintendo Switch September 13, 2017
This will most likely be the primary method used to handle template logic, however, with complex conditions, you may like to consider the following 2 approaches.
partials
Method 2: Add the logic to the component's JavaScript using When defining the component in JavaScript, add a partials
object with a name and template string key/value pair, like so:
components: card: partials: image: ` <template v-if="props.image"> <img v-bind:src="props.image" v-bind:alt="props.alt" /> </template> <template v-else> <div class="fallback"></div> </template> ` // Using ES6 template literals, but any string concatenation method works template: 'local'
and reference it in the pre-rendered component with <q-component-partial name="image"></q-component-partial>
:
<!-- <q-component> --> <!-- </q-component> --> Mario Bros And Other Nintendo Arcade Games Coming To Nintendo Switch September 13, 2017
When the template is compiled, this <q-component-partial name="image">
will be converted to the value of the app's components.partials.image
property.
This method allows you to move more complex or repetitive logic into the JavaScript to reduce the size of the pre-rendered HTML.
Note: Having a <q-component-partial>
element within your markup could affect your layout. There are 2 solutions to this problem:
- Add
q-component-partial { display: none; }
to your CSS; or - Wrap HTML comments around the tag
<!-- <q-component-partial></q-component-partial> -->
, which reduces the need for extra CSS, but may make the HTML less obvious in your editor.
Define a completely different component
Instead of handling the logic in the front-end, you could define separate component variations in the pre-rendered HTML. The downside to this approach is, that if your original markup didn't contain a component variation which was later used, Vue would throw an error. A workaround to this is to output a <template q-component="NAME">
as a fallback for all possible variations that weren't in the original data.
Here we define 2 components, card--default
and card--fallback
, and remove all the v-if
logic from the markup.
Mario Bros And Other Nintendo Arcade Games Coming To Nintendo Switch September 13, 2017 Doom 2016 Is Coming To Nintendo Switch September 12, 2017
Excluding elements from the component template compiler
Similarly to excluding elements from the app template compiler, elements within a component can be excluded from its template by being wrapped in a pair of <!-- <q-component> --> … <!-- </q-component> -->
comments as demonstrated in the earlier examples.
Rendering future components dynamically
While components are normally rendered with their name in angled brackets (<navigation></navigation>
), Vue also supports a meta component (<component></component>
) which allows us to programmatically render a component of our choice.
Assuming we had 4 card variations: card--default
, card--fallback
, card--twitter
and card--instagram
, we can use a unique variable (often a property on the card's props
object such as type
) to selectively render future components of that type using the is
attribute.
Here we iterate over our cards
array, dynamically render the correct card component based on card.type
and pass the card's data to the props
prop.
type: 'card--twitter' title: 'Arguing on the Internet still rampant' … type: 'card--default' title: 'Mario Bros And Other Nintendo Arcade Games Coming To Nintendo Switch' … type: 'card--instagram' title: 'Check out this selfie of me and my avocado' … type: 'card--fallback' title: 'Doom 2016 Is Coming To Nintendo Switch' …
Updating our app initialization to support pre-rendered components
Very little needs to change from our earlier example.
With a module bundler, such as webpack
;; // import createComponentTemplates var appEl = document;var data = ;var components = 'card--default': // Register all possible components for this app props: 'props' // Define props as you normally would template: 'local' // Specify that the component's template is "local" 'card--fallback': props: 'props' template: 'local' 'card--instagram': props: 'props' template: 'local' 'card--twitter': props: 'props' template: 'local' ;components = ; // Convert and add templates to your componentsvar template = ; // createAppTemplate has to be called after createComponentTemplates var app = el: appEl components: components data: data template: template;
Note: camelCase component names are converted to kebab-case, so you can use object shorthand notation to define your components after import
ing them
<script>
include
For direct
var appEl = document;var data = quenchVue;var components = 'card--default': // Register all possible components for this app props: 'props' // Define props as you normally would template: 'local' // Specify that the component's template is "local" 'card--fallback': props: 'props' template: 'local' 'card--instagram': props: 'props' template: 'local' 'card--twitter': props: 'props' template: 'local' ;components = quenchVue; // Convert and add templates to your componentsvar template = quenchVue; // createAppTemplate has to be called after createComponentTemplates var app = el: appEl components: components data: data template: template;
Hiding elements in the pre-rendered HTML
To prevent layout jumping and repositioning when the app's template gets compiled, it can be beneficial to visually (and accessibly) hide elements and content that is inappropriate without JavaScript, such as a <button>
.
By adding a class on your app container and an appropriate CSS rule, this can be achieved easily:
I'm a button that only works with JS
When Quench compiles our template for Vue, it removes any pre-quench
classes and adds a quenched
class, thus providing the ability to style elements based on the pre and post-quenched state.
Embedding additional app templates
You may also like to embed an additional JavaScript string template within the compiled, pre-rendered template. This can easily be achieved by passing the template as the second parameter to createAppTemplate()
, and describing where it will appear with a <q-template></q-template>
tag.
A possible use case for this technique is when you need to create multiple apps of simple components (such as a video player), but you don't want to have to continually repeat the <video-player />
component definition.
var baseTemplate = '<video-player v-bind:autoplay="autoplay" v-bind:id="id"></video-player>';var template = ;
Note: You can also wrap HTML comments around the tag <!-- <q-template></q-template> -->
, to hide the placeholder from the browser's parser.
Benefits
Hopefully you've recognized that you're now able to render fast, SEO-friendly static markup (either from a CMS, static-site generator or component library such as Fractal) and have it quickly and easily converted into a fully dynamic, client-side Vue.js application, without having to set up more complicated server-side rendering processes.
Copyright (c) 2018 Matt Stow
Licensed under the MIT license (see LICENSE for details)