@urbandoor/press

Progressive enhancement with VueJS

Usage no npm install needed!

<script type="module">
  import urbandoorPress from 'https://cdn.skypack.dev/@urbandoor/press';
</script>

README

PRESS (@urbandoor/press)

license standard-readme compliant npm (scoped) npm

Dependabot badge dependencies Status devDependencies Status semantic-release

CircleCI

PRESS is a client-side library and set of server-side patterns for progressively adding interactivity to server-rendered HTML.

Table of Contents

Install

The easiest way to get started with PRESS is to drop the script tag (and dependencies) onto your page.

<link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/@urbandoor/press@latest/press.css"
/>
<script src="https://cdn.jsdelivr.net/jquery/latest/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@urbandoor/press@latest/press.min.js"></script>

Yes, mixing jQuery and Vue seems a bit odd. Our site necessarily uses jQuery for other things, so from our point of view, it's not a huge addition and it saves us a lot of time by leveraging prior art. Eventually (in a semver-major), we'll release a version of PRESS that makes our custom components optional and removes the jQuery dependency.

If you intend to use the autocomplete component, you may need to polyfill window.fetch(). The easiest way to do so is with polyfill.io

<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=fetch|gate"></script>

But following the instructions for whatwg-fetch is probably a better long-term option.

Of course, PRESS is also available as an npm module:

npm install @urbandoor/press

Note the peer dependency warnings; PRESS has a number of peers for setting up webpack.

The npm version is intended for use with module bundlers: the following should also work, but is untested:

<link rel="stylesheet" href="//node_modules/@urbandoor/press/press.css" />
<script src="https://cdn.jsdelivr.net/jquery/latest/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="//node_modules/@urbandoor/press/press.min.js"></script>

Integration with webpack

We're building PRESS from npm in our main application project using webpacker.

Since the nature of Vue makes it a little tricky to distribute a fully-functional library that still tree shakes effectively, you'll need to make some changes to your webpack and postcss configs to use press. The following webpack configuration should add the PRESS JavaScript and CSS to your app.

This config externalizes Vue and jQuery so they can be loaded directly from their CDNs rather than building them into your bundle.

// webpack.config.js

'use strict';

const VueLoaderPlugin = require('vue-loader/lib/plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');

module.exports = {
    externals: {
        jQuery: 'jquery',
        vue: 'Vue'
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin(),
        new LodashModuleReplacementPlugin({paths: true})
    ]
};
// postcss.config.js

'use strict';

module.exports = function({
    env,
    file,
    options: {autoprefixer = {}, cssnano = {}}
}) {
    return {
        plugins: {
            'postcss-import': {root: file.dirname},
            autoprefixer: env === 'production' ? autoprefixer : false,
            cssnano: env === 'production' ? cssnano : false
        }
    };
};

Entrypoints

package.json defines a number of different entry points:

  • main: CommonJS entrypoint. The code specified by main has been fully compiled to meet the compatibility required by .browserslistrc. This is almost certainly the entrypoint preferred by your bundler, unless it's configured to look for module.
  • module: Like main this is fully compiled according to .browserslistrc, but uses EcmaScript modules instead of CommonJS requires so that your bundler can treeshake more effectively. Webpack and the like may prefer this over main automatically.
  • jsdelivr: Identifies the bundle we create for the CDN.
  • style: A defacto standard for exporting css from node_modules. A reasonably standard postcss configuration should automatically target this entrypoint if you use `@import "@urbandoor/press" in your css.
  • source: raw source code. You almost certainly don't want to use this, but it if you're really concerned about filesize and want to, for example, supply your own set of values to browserslist, you might want to configure your bundle to target this entrypoint.

If you're bundling assets yourself and you use one of the npm versions, make sure you make the full version of Vue available, not just the runtime. Since PRESS is intended to upgrade server-render html with Vue directives, you'll need to version of Vue that includes the template compiler. See the dist README in the Vue package for details on configuring your bundler.

Usage

  1. Initialize the PRESS JavaScript

    If using the CDN version, PRESS will automatically annotate your page once the script finishes loading. If you're using the version from npm, make sure to

    require('@urbandoor/press');
    

    or

    import '@urbandoor/press';
    
  2. Add the CSS

    If you're using the postcss-import plugin, you should be able to simply

    @import '@urbandoor/press';
    
  3. Vue refuses to work if its bound to the body tag, so add [data-press-app] to the boundary of interactive content on your page.

    <html>
        <body>
            <main data-press-app></main>
        </body>
    </html>
    

Interactive Pages

PRESS effectively does two things:

  1. Alter input[type=date] elements to be replaced by a custom Vue date picker component if those elements find themselves inside a Vue instance.
  2. Instantiate a Vue app for every [data-press-app] element on the page.

This means that within a server-rendered page, you can use Vue bindings for any element that is a child of a [data-press-app].

In most cases, it should be adequate to add data-press-app to the first child of the body tag, but if you've got multiple pieces of functionality on a page, it may make sense to create several apps to prevent data sharing

<div data-press-app>
    <div class="left">
        <form><input name="count" type="number" /></form>
    </div>
    <div class="right">{{count}}</div>
</div>

We use a few tricks to avoid rendering the templates until Vue takes over:

  1. Combine v-if="false" with a sibling template tag
<p v-if="false">This text will be visible until Vue takes over</p>
<template
    ><p>This text will be visible after Vue takes over</p></template
>
  1. Use v-cloak. Let's say you have a complicated v-if. You don't want its contents to render until after Vue is running and the conditional can be evaluated.

    <div v-if="magic()" v-cloak></div>
    

Previously, we recommended the class .press-hide-until-mount rather than [v-cloak]. This is still supported but is deprecated behavior that may be removed in a future version.

Components

In addition to providing a framework for progressively enhancing server-rendered HTML, PRESS includes its own components.

Eventually, there will be a sem-ver major release that makes the components optional, but for now, they're required as are their dependencies

Each component in ./src/components/ has a README explaining its usage.

How It Works

PRESS executes a series of phases, decorating or replacing HTML as appropriate:

  1. Register PRESS components

  2. Infer intended components using the infer() method of each registered component. e.g. Find all input[type="date"] elements and add [data-press-component="datepicker"]

  3. For every element el that matches [data-press-component], call the enhance() method of the specified component on el.

  4. For every element el that matches [data-press-app] but is not itself a child of an element that matches [data-press-app], do the following

    1. create an empty object data
    2. for every child child of el matching [name]:not([v-model]), add a v-model attribute with the same value as the name attribute.
    3. for every child of child of el with a v-model, add an entry to data at the keypath specified by the child's v-model; if the child's value attibute is defined, use that, otherwise, use null.
    4. call the constructor specified by the value of data-press-app.

Testing

Test are implemented using Gherkin syntax and Cucumber JS via WebdriverIO. We're using WebdriverIO mostly for its ability to launch multiple browsers and Gherkin for its ability to narrowly scope failures (Reasonable but unfortunate implementation decisions in other JavaScript selenium runners make it straightforward to know what test failed, but not what step of the test failed).

Gherkin feature files are stored in ./features. Step definitions are stored in ./features/steps. The files given.js, then.js, and when.js as well as most everything in ./features/support are taken pretty much directly from the WDIO Cucumber Boilerplate (with adjustments made to support CommonJS instead of ESM).

Maintainer

Ian Remmel

Contribute

PRs Welcome.

You can use npm run plop -- component to scaffold out a new PRESS component.

License

MIT © Urbandoor Inc. 2018 until at least now