POSTing an Indeterminate Checkbox Value

There is a such thing as an indeterminate checkbox value. It’s a checkbox (<input type="checkbox">) that isn’t checked. Nor is it not checked. It’s indeterminate.

We can even select a checkbox in that state and style it with CSS!

Some curious points though:

  1. It’s only possible to set via JavaScript. There is no HTML attribute or value for it.
  2. It doesn’t POST (or GET or whatever else) or have a value. It’s like being unchecked.

So, say you had a form like this:

<form action="" method="POST" id="form"> <input name="name" type="text" value="Chris" /> <input name="vegetarian" type="checkbox" class="veg"> <input type="submit" value="Submit"> </form>

And, for whatever reason, you make that checkbox indeterminate:

let veg = document.querySelector(".veg");
veg.indeterminate = true;

If you serialize that form and take a look at what will POST, you’ll get "name=Chris". No value for the checkbox. Conversely, had you checked the checkbox in the HTML and didn’t touch it in JavaScript, you’d get "name=Chris&vegetarian=on".

Apparently, this is by design. Checkboxes are meant to be boolean, and the indeterminate value is just an aesthetic thing meant to indicate that visual “child” checkboxes are in a mixed state (some checked, some not). That’s fine. Can’t change it now without serious breakage of websites.

But say you really need to know on the server if a checkbox is in that indeterminate state. The only way I can think of is to have a buddy hidden input that you keep in sync.

<input name="vegetarian" type="checkbox" class="veg">
<input name="vegetarian-value" type="hidden" class="veg-value">
let veg = document.querySelector(".veg");
let veg_value = document.querySelector(".veg-value"); veg.indeterminate = true;
veg_value.value = "indeterminate";

I’ve set the indeterminate value of one input and I’ve set another hidden input value to "indeterminate", which I can POST. Serialized means it looks like "name=Chris&vegetarian-value=indeterminate". Good enough.

See the Pen Can you POST an intermediate value? by Chris Coyier (@chriscoyier) on CodePen.

The post POSTing an Indeterminate Checkbox Value appeared first on CSS-Tricks.

Native-Like Animations for Page Transitions on the Web

Some of the most inspiring examples I’ve seen of front-end development have involved some sort of page transitions that look slick like they do in mobile apps. However, even though the imagination for these types of interactions seem to abound, their presence on actual sites that I visit do not. There are a number of ways to accomplish these types of movement!

Here’s what we’ll be building:

Demo Site

GitHub Repo

We’ll build out the simplest possible distillation of these concepts so that you can apply them to any application, and then I’ll also provide the code for this more complex app if you’d like to dive in.

Today we’ll be discussing how to create them with Vue and Nuxt. There are a lot of moving parts in page transitions and animations (lol I kill me), but don’t worry! Anything we don’t have time to cover in the article, we’ll link off to with other resources.

Why?

The web has come under critique in recent years for appearing “dated” in comparison to native iOS and Android app experiences. Transitioning between two states can reduce cognitive load for your user, as when someone is scanning a page, they have to create a mental map of everything that’s contained on it. When we move from page to page, the user has to remap that entire space. If an element is repeated on several pages but altered slightly, it mimics the experience we’ve been biologically trained to expect — no one just pops into a room or changes suddenly; they transition from another room into this one. Your eyes see someone that’s smaller relative to you. As they get closer in proximity to you, they get bigger. Without these transitions, changes can be startling. They force the user to remap placement and even their understanding of the same element. It is for this reason that these effects become critical in an experience that helps the user feel at home and gather information quickly on the web.

The good news is, implementing these kind of transitions is completely doable. Let’s dig in!

Prerequisite Knowledge

If you’re unfamiliar with Nuxt and how to work with it to create Vue.js applications, there’s another article I wrote on the subject here. If you’re familiar with React and Next.js, Nuxt.js is the Vue equivalent. It offers server-side rendering, code splitting, and most importantly, hooks for page transitions. Even though the page transition hooks it offers are excellent, that’s not how we’re going to accomplish the bulk of our animations in this tutorial.

In order to understand how the transitions we’re working with today do work, you’ll also need to have basic knowledge around the <transition /> component and the difference between CSS animations and transitions. I’ve covered both in more detail here. You’ll also need basic knowledge of the <transition-group /> component and this Snipcart post is a great resource to learn more about it.

Even though you’ll understand everything in more detail if you read these articles, I’ll give you the basic gist of what’s going on as we encounter things throughout the post.

Getting Started

First, we want to kick off our project by using the Vue CLI to create a new Nuxt project:

# if you haven’t installed vue cli before, do this first, globally:
npm install -g @vue/cli
# or
yarn global add @vue/cli # then
vue init nuxt/starter my-transitions-project
npm i
# or
yarn # and
npm i vuex node-sass sass-loader
# or
yarn add vuex node-sass sass-loader

Great! Now the first thing you’ll notice is that we have a pages directory. Nuxt will take any .vue files in that directory and automatically set up routing for us. Pretty awesome. We can make some pages to work with here, in our case: about.vue, and users.vue.

Setting Up Our Hooks

As mentioned earlier, Nuxt offers some page hooks which are really nice for page to page transitions. In other words, we have hooks for a page entering and leaving. So if we wanted to create an animation that would allow us to have a nice fade from page to page, we could do it because the class hooks are already available to us. We can even name new transitions per page and use JavaScript hooks for more advanced effects.

But what if we have some elements that we don’t want to leave and re-enter, but rather transition positions? In mobile applications, things don’t always leave when they move from state to state. Sometimes they transition seamlessly from one point to another and it makes the whole application feel very fluid.

Step One: Vuex Store

The first thing we’ll have to do is set up a centralized state management store with Vuex because we’re going to need to hold what page we’re currrently on.

Nuxt will assume this file will be in the store directory and called index.js:

import Vuex from 'vuex' const createStore = () => { return new Vuex.Store({ state: { page: 'index' }, mutations: { updatePage(state, pageName) { state.page = pageName } } })
} export default createStore

We’re storing both the page and we create a mutation that allows us to update the page.

Step Two: Middleware

Then, in our middleware, we’ll need a script that I’ve called pages.js. This will give us access to the route that’s changing and being updated before any of the other components, so it will be very efficient.

export default function(context) { // go tell the store to update the page context.store.commit('updatePage', context.route.name)
}

We’ll also need to register the middleware in our nuxt.config.js file:

module.exports = { ... router: { middleware: 'pages' }, ...
}

Step Three: Register Our Navigation

Now, we’ll go into our layouts/default.vue file. This directory allows you to set different layouts for different page structures. In our case, we’re not going to make a new layout, but alter the one that we’re reusing for every page. Our template will look like this at first:

<template> <div> <nuxt/> </div>
</template>

And that nuxt/ tag will insert anything that’s in the templates in our different pages. But rather than reusing a nav component on every page, we can add it in here and it will be presented consistently on every page:

<template> <div> <app-navigation /> <nuxt/> </div>
</template>
<script>
import AppNavigation from '~/components/AppNavigation.vue' export default { components: { AppNavigation }
}
</script>

This is also great for us because it won’t rerender every time the page is re-routed. It will be consistent on every page and, because of this, we cannot plug into our page transition hooks but instead we can build our own with what we built between Vuex and the Middleware.

Step Four: Create our Transitions in the Navigation Component

Now we can build out the navigation, but I’m also going to use this SVG here to do a small demo of basic functionality we’re going to implement for a larger application

<template> <nav> <h2>Simple Transition Group For Layout: {{ page }}</h2> <!--simple navigation, we use nuxt-link for routing links--> <ul> <nuxt-link exact to="/"><li>index</li></nuxt-link> <nuxt-link to="/about"><li>about</li></nuxt-link> <nuxt-link to="/users"><li>users</li></nuxt-link> </ul> <br> <!--we use the page to update the class with a conditional--> <svg :class="{ 'active' : (page === 'about') }" xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 447 442"> <!-- we use the transition group component, we need a g tag because it’s SVG--> <transition-group name="list" tag="g"> <rect class="items rect" ref="rect" key="rect" width="171" height="171"/> <circle class="items circ" key="circ" id="profile" cx="382" cy="203" r="65"/> <g class="items text" id="text" key="text"> <rect x="56" y="225" width="226" height="16"/> <rect x="56" y="252" width="226" height="16"/> <rect x="56" y="280" width="226" height="16"/> </g> <rect class="items footer" key="footer" id="footer" y="423" width="155" height="19" rx="9.5" ry="9.5"/> </transition-group> </svg> </nav>
</template>
<script>
import { mapState } from 'vuex' export default { computed: mapState(['page'])
}
</script>

We’re doing a few things here. In the script, we bring in the page name from the store as a computed value. mapState will let us bring in anything else from the store, which will handy later when we deal with a lot of user information.

In the template, we have a regular nav with nuxt-links, which is what we use for routing links in Nuxt. We also have class that will be updated on a conditional based on the page (it will change to .active when it’s the about page.

We’re also using the <transition-group> component around a number of elements that will change positions. The <transition-group> component is a bit magical because it applies the concepts of FLIP under the hood. If you’ve heard of FLIP before, you’re going to be super excited to hear this because it’s a really performant way of animating on the web but usually takes a lot of calculations to implement. If you haven’t heard of FLIP before, it’s definitely good to read up to understand how it works, and maybe more importantly, all of the stuff you no longer have to do to make this kind of effect work! Can I get a “Hell yeah!”

Here is the CSS that makes this work. We basically state how we’d like all of the elements to be positioned on that “active” hook that we made. Then we tell the elements to have a transition applied if something changes. You’ll notice I’m using 3D transforms even if I’m just moving something along one X or Y axis because transforms are better for performance than top/left/margin for reducing paint and I want to enable hardware acceleration.

.items,
.list-move { transition: all 0.4s ease;
} .active { fill: #e63946; .rect { transform: translate3d(0, 30px, 0); } .circ { transform: translate3d(30px, 0, 0) scale(0.5); } .text { transform: rotate(90deg) scaleX(0.08) translate3d(-300px, -35px, 0); } .footer { transform: translateX(100px, 0, 0); }
}

Here is a reduced codepen without the page transitions, but just to show the movement:

See the Pen layout transition-group by Sarah Drasner (@sdras) on CodePen.

I want to point out that any implementations I use here are choices that I’ve made for placement and movement- you can really create any effect you like! I am choosing SVG here because it communicates the concept of layout in a small amount of code, but you don’t need to use SVG. I’m also using transitions instead of animation because of how declarative they are by nature- you are in essence stating: “I want this to be repositioned here when this class is toggled in Vue”, and then the transition’s only job is to describe the movement as anything changes. This is great for this use-case because it’s very flexible. I can then decide to change it to any other conditional placement and it will still work.

Great! This will now give us the effect, smooth as butter between pages, and we can still give the content of the page a nice little transition as well:

.page-enter-active { transition: opacity 0.25s ease-out;
} .page-leave-active { transition: opacity 0.25s ease-in;
} .page-enter,
.page-leave-active { opacity: 0;
}

I’ve also added in one of the examples from the Nuxt site to show that you can still do internal animations within the page as well:

View GitHub Repo

Ok, that works for a small demo, but now let’s apply it to something more real-world, like our example from before. Again, the demo site is here and the repo with all of the code is here.

It’s the same concept:

  • We store the name of the page in the Vuex store.
  • Middleware commits a mutation to let the store know the page has changed.
  • We apply a special class per page, and nest transitions for each page.
  • The navigation stays consistent on each page but we have different positions and apply some transitions.
  • The content of the page has a subtle transition and we build in some interactions based on user events

The only difference is that this is a slightly more involved implementation. The CSS that’s applied to the elements will stay the same in the navigation component. We can tell the browser what position we want all the elements to be in, and since there’s a transition applied to the element itself, that transition will be applied and it will move to the new position every time the page has changed.

// animations
.place { .follow { transform: translate3d(-215px, -80px, 0); } .profile-photo { transform: translate3d(-20px, -100px, 0) scale(0.75); } .profile-name { transform: translate3d(140px, -125px, 0) scale(0.75); color: white; } .side-icon { transform: translate3d(0, -40px, 0); background: rgba(255, 255, 255, 0.9); } .calendar { opacity: 1; }
}

That’s it! We keep it nice and simple and use flexbox, grid and absolute positioning in a relative container to make sure everything translates easily across all devices and we have very few media queries through this project. I’m mainly using CSS for the nav changes because I can declaratively state the placement of the elements and their transitions. For the micro-interactions of any user-driven event, I’m using JavaScript and GreenSock, because it allows me to coordinate a lot of movement very seamlessly and stabilizes transform-origin across browsers, but there are so many ways you could implement this. There are a million ways I could improve this demo application, or build on these animations, it’s a quick project to show some possibilities in a real-life context.

Remember to hardware accelerate and use transforms, and you can achieve some beautiful, native-like effects. I’m excited to see what you make! The web has so much potential for beautiful movement, placement, and interaction that reduces cognitive load for the user.

The post Native-Like Animations for Page Transitions on the Web appeared first on CSS-Tricks.

Let’s Build a Custom Vue Router

Plenty of tutorials exist that do a great job in explaining how Vue’s official routing library, vue-router, can be integrated into an existing Vue application. vue-router does a fantastic job by providing us with the items needed to map an application’s components to different browser URL routes.

But, simple applications often don’t need a fully fledged routing library like vue-router. In this article, we’ll build a simple custom client-side router with Vue. By doing so, we’ll gather an understanding of what needs to be handled to construct client-side routing as well as where potential shortcomings can exist.

Though this article assumes basic knowledge in Vue.js; we’ll be explaining things thoroughly as we start to write code!

Routing

First and foremost: let’s define routing for those who may be new to the concept.

In web development, routing often refers to splitting an application’s props to dictate the information that should be displayed. Here’s a Pen that shows just this:

See the Pen Vue Pokemon by Hassan Dj (@itslit) on CodePen.

Though the app would functionally work, it misses a substantial feature that’s expected from most web applications—responding to browser navigation events. We’d want our Pokémon app to be accessible and to show different details for different pathnames: /charizard, /blastoise, and /venusaur. This would allow users to refresh different pages and keep their location in the app, bookmark the URLs to come back to later, and potentially share the URL with others. These are some of the main benefits of creating routes within an application.

Now that we have an idea of what we’ll be working on, let’s start building!

Preparing the App

The easiest way to follow along step-by-step (if you wish to do so) is to clone the GitHub repo I’ve set up.

Download on GitHub

When cloned, install the project dependencies with:

npm install

Let’s take a brief look within the project directory.

$ ls
README.md
index.html
node_modules/
package.json
public/
src/
static/
webpack.config.js

There also exists the hidden files, .babelrc and .gitignore within the project scaffold.

This project is a simple webpack-configured application scaffolded with vue-cli, the Vue command line interface.

index.html is where we declare the DOM element—#app— with which we’ll use to mount our Vue application:

<!DOCTYPE html>
<html lang="en"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.3/css/bulma.css"> <link rel="stylesheet" href="../public/styles.css" /> <title>Pokémon - Routing</title> </head> <body> <div id="app"></div> <script src="/dist/build.js"></script> </body>
</html>

In the <head> tag of the index.html file, we introduce Bulma as our application’s CSS framework and our own styles.css file that lives in the public/ folder.

Since our focus is on the usage of Vue.js, the application already has all the custom CSS laid out.

The src/ folder is where we’ll be working directly from:

$ ls src/
app/
main.js

src/main.js represents the starting point of our Vue application. It’s where our Vue instance is instantiated, where we declare the parent component that is to be rendered, and the DOM element #app with which our app is to be mounted to:

import Vue from 'vue';
import App from './app/app'; new Vue({ el: '#app', render: h => h(App)
});

We’re specifying the App component, from the src/app/app.js file, to be the main parent component of our application.

In the src/app directory, there exists two other files – app-custom.js and app-vue-router.js:

$ ls src/app/
app-custom.js
app-vue-router.js
app.js

app-custom.js denotes the completed implementation of the application with a custom Vue router (i.e. what we’ll be building in this article). app-vue-router.js is a completed routing implementation using the vue-router library.

For the entire article, we’ll only be introducing code to the src/app/app.js file. With that said, let’s take a look at the starting code within src/app/app.js:

const CharizardCard = { name: 'charizard-card', template: ` <div class="card card--charizard has-text-weight-bold has-text-white"> <div class="card-image"> <div class="card-image-container"> <img src="../../static/charizard.png"/> </div> </div> <div class="card-content has-text-centered"> <div class="main"> <div class="title has-text-white">Charizard</div> <div class="hp">hp 78</div> </div> <div class="stats columns is-mobile"> <div class="column">&#x1f525;<br> <span class="tag is-warning">Type</span> </div> <div class="column center-column">199 lbs<br> <span class="tag is-warning">Weight</span> </div> <div class="column">1.7 m <br> <span class="tag is-warning">Height</span> </div> </div> </div> </div> `
}; const App = { name: 'App', template: ` <div class="container"> <div class="pokemon"> <pokemon-card></pokemon-card> </div> </div> `, components: { 'pokemon-card': CharizardCard }
}; export default App;

Currently, two components exist: CharizardCard and App. The CharizardCard component is a simple template that displays details of the Charizard Pokémon. The App component declares the CharizardCard component in its components property and renders it as <pokemon-card></pokemon-card> within its template.

We currently only have static content with which we’ll be able to see if we run our application:

npm run dev

And launch localhost:8080:

To get things started, let’s introduce two new components: BlastoiseCard and VenusaurCard that contains details of the Blastoise and Venusaur Pokémon respectively. We can lay out these components right after CharizardCard:

const CharizardCard = { // ... }; const BlastoiseCard = { name: 'blastoise-card', template: ` <div class="card card--blastoise has-text-weight-bold has-text-white"> <div class="card-image"> <div class="card-image-container"> <img src="../../static/blastoise.png"/> </div> </div> <div class="card-content has-text-centered"> <div class="main"> <div class="title has-text-white">Blastoise</div> <div class="hp">hp 79</div> </div> <div class="stats columns is-mobile"> <div class="column">&#x1f4a7;<br> <span class="tag is-light">Type</span> </div> <div class="column center-column">223 lbs<br> <span class="tag is-light">Weight</span> </div> <div class="column">1.6 m<br> <span class="tag is-light">Height</span> </div> </div> </div> </div> `
}; const VenusaurCard = { name: 'venusaur-card', template: ` <div class="card card--venusaur has-text-weight-bold has-text-white"> <div class="card-image"> <div class="card-image-container"> <img src="../../static/venusaur.png"/> </div> </div> <div class="card-content has-text-centered"> <div class="main"> <div class="title has-text-white">Venusaur</div> <div class="hp hp-venusaur">hp 80</div> </div> <div class="stats columns is-mobile"> <div class="column">&#x1f343;<br> <span class="tag is-danger">Type</span> </div> <div class="column center-column">220 lbs<br> <span class="tag is-danger">Weight</span> </div> <div class="column">2.0 m<br> <span class="tag is-danger">Height</span> </div> </div> </div> </div> `
}; const App = { // ... }; export default App;

With our application components established, we can now begin to think how we’ll create routing between these components.

router-view

To establish routing, we’ll start by buiding a new component that holds the responsibility to render a specified component based on the app’s location. We’ll create this component in a constant variable named View.

Before we create this component, let’s see how we might use it. In the template of the App component, we’ll remove the declaration of <pokemon-card> and instead render the upcoming router-view component. In the components property; we’ll register the View component constant as <router-view> to be declared in the template.

const App = { name: 'App', template: ` <div class="container"> <div class="pokemon"> <router-view></router-view> </div> </div> `, components: { 'router-view': View }
}; export default App;

The router-view component will match the correct Pokémon component based on the URL route. This matching will be dictated in a routes array that we’ll create. We’ll create this array right above the App component:

const CharizardCard = { // ... };
const BlastoiseCard = { // ... };
const VenusaurCard = { // ... }; const routes = [ {path: '/', component: CharizardCard}, {path: '/charizard', component: CharizardCard}, {path: '/blastoise', component: BlastoiseCard}, {path: '/venusaur', component: VenusaurCard}
]; const App = { // ... }; export default App;

We’ve set each Pokémon path to their own respective component (e.g. /blastoise will render the BlastoiseCard component). We’ve also set the root path / to the CharizardCard component.

Let’s now begin to create our router-view component.

The router-view component will essentially be a mounting point to dynamically switch between components. One way we can do this in Vue is by using the reserved <component> element to establish Dynamic Components.

Let’s create a starting point for router-view to get an understanding of how this works. As mentioned earlier; we’ll create router-view within a constant variable named View. So with that said, let’s set up View right after our routes declaration:

const CharizardCard = { // ... };
const BlastoiseCard = { // ... };
const VenusaurCard = { // ... }; const routes = [ // ...
]; const View = { name: 'router-view', template: `<component :is="currentView"></component>`, data() { return { currentView: CharizardCard } }
}; const App = {
// ... }; export default App;

The reserved <component> element will render whatever component the is attribute is bound to. Above, we’ve attached the is attribute to a currentView data property that simply maps to the CharizardCard component. As of now, our application resembles the starting point by displaying CharizardCard regardless of what the URL route is.

Though router-view is now appropriately rendered within App, it’s not currently dynamic. We need router-view to display the correct component based on the URL pathname upon page load. To do this, we’ll use the created() hook to filter the routes array and return the component that has a path that matches the URL path. This would make View look something like this:

const View = { name: 'router-view', template: `<component :is="currentView"></component>`, data() { return { currentView: {} } }, created() { this.currentView = routes.find( route => route.path === window.location.pathname ).component; }
};

In the data function, we’re now instantiating currentView with an empty object. In the created() hook, we’re using JavaScript’s native find() method to return the first object from routes that matches route.path === window.location.pathname. We can then get the component with object.component (where object is the returned object from find()).

Inside a browser environment, window.location is a special object containing the properties of the browser’s current location. We grab the pathname from this object which is the path of the URL.

At this stage; we’ll be able to see the different Pokémon Card components based on the state of our browser URL!

The BlastoiseCard component now renders at the /blastoise route.

There’s something else we should consider. If a random URL pathname is entered, our app will currently error and present nothing to the view.

To avoid this, let’s introduce a simple check to display a “Not Found” template if the URL pathnamedoesn’t match any path existing in the routes array. We’ll separate out the find() method to a component method named getRouteObject() to avoid repetition. This updates the View object to:

const View = { name: 'router-view', template: `<component :is="currentView"></component>`, data() { return { currentView: {} } }, created() { if (this.getRouteObject() === undefined) { this.currentView = { template: ` <h3 class="subtitle has-text-white"> Not Found :(. Pick a Pokémon from the list below! </h3> ` }; } else { this.currentView = this.getRouteObject().component; } }, methods: { getRouteObject() { return routes.find( route => route.path === window.location.pathname ); } }
};

If the getRouteObject() method returns undefined, we display a “Not Found” template. If getRouteObject()returns an object from routes, we bind currentView to the component of that object. Now if a random URL is entered, the user will be notified:

The “Not Found” view is rendered if the URL pathname does not match any of the values in the routes array.

The “Not Found” template tells the user to pick a Pokémon from a list. This list will be the links we’ll create to allow the user to navigate to different URL routes.

Awesome! Our app is now responding to some external state, the location of the browser. router-view determines which component should be displayed based on the app’s location. Now, we need to construct links that will change the location of the browser without making a web request. With the location updated, we want to re-render our Vue app and rely on router-view to appropriately determine which component to render.

We’ll label these links as router-link components.

router-link

In web interfaces, we use HTML <a> tags to create links. What we want here is a special type of <a> tag. When the user clicks on this tag, we’ll want the browser to skip its default routine of making a web request to fetch the next page. Instead, we just want to manually update the browser’s location.

Let’s compose a router-link component that produces an <a> tag with a special click binding. When the user clicks on the router-link component, we’ll use the browser’s history API to update the browser’s location.

Just like we did with router-view, let’s see how we’ll use this component before we build it.

In the template of the App component, let’s create three <router-link> elements within a parent <div class="pokemon-links"></div> element. Rather than using the href attribute in <router-link>, we’ll specify the desired location of the link using a to attribute. We’ll also register the upcoming router-link component (from a Link constant variable) in the App components property:

const App = { name: 'App', template: ` <div class="container"> <div class="pokemon"> <router-view></router-view> <div class="pokemon-links has-text-centered"> <router-link to="/charizard"></router-link> <router-link to="/blastoise"></router-link> <router-link to="/venusaur"></router-link> </div> </div> </div> `, components: { 'router-view': View, 'router-link': Link }
};

We’ll create the Link object that represents router-link right above the App component. We’ve established the router-link component should always be given a to attribute (i.e. prop) that has a value of the target location. We can enforce this prop validation requirement like so:

const CharizardCard = { // ... };
const BlastoiseCard = { // ... };
const VenusaurCard = { // ... }; const routes = [ // ... ]; const View = { // ... }; const Link = { name: 'router-link', props: { to: { type: String, required: true } }
}; const App = { // ... }; export default App;

We can create the template of router-link to consist of an <a> tag with an @click handler attribute. Upon trigger, the @click handler will call a component method, labeled navigate(), that navigates the browser to the desired location. This navigation will occur with the use of the history.pushState() method. With that said, the Link constant object will be updated to:

const Link = { name: 'router-link', props: { to: { type: String, required: true } }, template: `<a @click="navigate" :href="to">{{ to }}</a>`, methods: { navigate(evt) { evt.preventDefault(); window.history.pushState(null, null, this.to); } }
};

Within the <a> tag, we’ve bound the value of the to prop to the element text content with {{ to }}.

When navigate() is triggered, it first calls preventDefault() on the event object to prevent the browser from making a web request for the new location. The history.pushState() method is then called to direct the user to the desired route location. history.pushState() takes three arguments:

  • a state object to pass serialized state information
  • a title
  • the target URL

In our case, there is no state information that’s needed to be passed, so we’ve left the first argument as null. Some browsers (e.g. Firefox) currently ignore the second parameter, title, hence we’ve left that as null as well.

The target location, the to prop, is passed in to the third and last parameter. Since the to prop contains the target location in a relative state, it will be resolved relative to the current URL. In our case, /blastoise will resolve to http://localhost:8080/blastoise.

If we click any of the links now, we’ll notice our browser updates to the correct location without a full page reload. However, our app will not update and render the correct component.

This unexpected behaviour happens because when router-link is updating the location of the browser, our Vue app is not alerted of the change. We’ll need to trigger our app (or simply just the router-view component) to re-render whenever the location changes.

Though there’s a few ways to accomplish this behaviour, we’ll do this by using a custom EventBus. An EventBus is a Vue instance responsible in allowing isolated components to subscribe and publish custom events between each other.

At the beginning of the file, we’ll import the vue library and create an EventBus with a new Vue() instance:

import Vue from 'vue'; const EventBus = new Vue();

When a link has been clicked, we need to notify the necessary part of the application (i.e. router-view) that the user is navigating to a particular route. The first step is to create an event emitter using the EventBus‘s events interface in the navigate() method of router-link. We’ll give this custom event a name of navigate:

const Link = { // ..., methods: { navigate(evt) { evt.preventDefault(); window.history.pushState(null, null, this.to); EventBus.$emit('navigate'); } }
};

We can now set the event listener/trigger in the created() hook of router-view. By setting the custom event listener outside of the if/else statement, the created() hook of View will be updated to:

const View = { // ..., created() { if (this.getRouteObject() === undefined) { this.currentView = { template: ` <h3 class="subtitle has-text-white"> Not Found :(. Pick a Pokémon from the list below! </h3> ` }; } else { this.currentView = this.getRouteObject().component; } // Event listener for link navigation EventBus.$on('navigate', () => { this.currentView = this.getRouteObject().component; }); }, // ...
};

When the browser’s location changes by clicking a <router-link> element, this listening function will be invoked, re-rendering router-view to match against the latest URL!

Great! Our app now navigates appropriately as we click each of the links.

There’s one last thing we need to consider. If we try to use the browser back/forward buttons to navigate through the browser history, our application will not currently re-render correctly. Although unexpected, this occurs because no event notifier is emitted when the user clicks browser back or browser forward.

To make this work, we’ll use the onpopstate event handler.

The onpopstate event is fired each time the active history entry changes. A history change is invoked by clicking the browser back or browser forward buttons, or calling history.back() or history.forward() programmatically.

Right after our EventBus creation, let’s set up the onpopstate event listener to emit the navigate event when a history change is invoked:

window.addEventListener('popstate', () => { EventBus.$emit('navigate'); });

Our application will now respond appropriately even when the browser navigation buttons are used!

And there we have it! We’ve just built a custom Vue router using an EventBus and dynamic components. Even with the tiny size of our app we can enjoy a noticeable performance improvement. Avoiding a full page load also saves hundreds of milliseconds and prevents our app from “blinking” during the page change.

Conclusion

I love Vue. One reason as to why – it’s incredibly easy to use and manipulate Vue components just like we saw in this article.

In the introduction, we mentioned how Vue provides the vue-router library as the official routing library of the framework. We’ve just created simple versions of the same main items that are used in vue-router:

  • routes: the array responsible in mapping components to respective URL pathnames.
  • router-view: the component that renders a specified app component based on the app’s location
  • router-link: the component that allows the user to change the location of the browser without making a web request.

For very simple applications, the routing we’ve built (or a variation thereof like this one built by Chris Fritz) can do the minimal amount of work needed to route our applications.

The vue-router library, on the other hand, is built in a more complicated manner and introduces incredibly useful capabilities, often needed in larger applications like:

  • Consistency between different browsers
  • Nested Routes
  • Navigation Guards
  • Transition Effects

Though the vue-router library does come with additional boilerplate, it’s fairly easy to integrate once your application is composed of well isolated and distinct components. If you’re interested, you can see the components of vue-router being used to enable routing in this application here.

Hopefully this was as enjoyable to you as it was for me in compiling this post! Thanks for reading!


This article is an adapted (and summarized) segment from the upcoming book, Fullstack Vue, that I’m working on with the Fullstack.io team! Having the opportunity to work with the folks at Fullstack has been nothing short of being a blast. In true Fullstack fashion, the book covers numerous facets of Vue including but not restricted to routing, simple state management, form handling, Vuex, server persistence, and testing. If this is something that piques your interest or if you have any questions at all, follow (or message) me on twitter (@djirdehh)! If the above doesn’t pique your interest, you can still follow me anyway. 😛


Let’s Build a Custom Vue Router is a post from CSS-Tricks

Making CSS Animations Feel More Natural

It used to be that designers designed and coders coded. There was no crossover, and that’s the way it was. But with the advent of CSS transitions and animations, those lines are blurring a bit. It’s no longer as simple as the designer dictating the design and the coder transcribing—designers must now know something about code, and coders must know something about design in order to effectively collaborate.

As an example, let’s say a designer asks a developer to make a box bounce. That’s it—no additional instruction. Without some cross-knowledge and a common vocabulary, both sides are a little lost in this communication: the developer doesn’t have enough information to fully realize the designer’s vision, but the designer doesn’t really know what the options are and how to communicate them. With a very basic interpretation, you might end up with something that looks like this:

See the Pen Bouncing Box 1 by Brandon Gregory (@pulpexploder) on CodePen.

Not very exciting. Although, to be fair, this does meet all of the criteria given. We can definitely do better than this, though.

The first thing to look at is the timing function. In the above example, we’re using a linear timing function, which means that the box is constantly moving at the same speed. In some cases, this is desirable; however, in the real world, motion usually doesn’t work like that.

An easy fix is to simply change the timing function to ease. This makes the beginning and ending of each animation a little slower than the middle part, which adds a more natural look to some animations. Here’s the box with the easing function turned on:

See the Pen Bouncing Box 2 by Brandon Gregory (@pulpexploder) on CodePen.

This is a slight improvement, but there’s still a lot of work to be done. The box still looks mechanical and stiff, with the same animation occurring in the same timeframe over and over. Adding a slight delay between bounces adds some visual contrast that seems a little more natural:

See the Pen Bouncing Box 3 by Brandon Gregory (@pulpexploder) on CodePen.

The box now looks like it’s jumping rather than simply moving up and down. There’s a little wind-up and cool-down between jumps that mimics what a live creature might do if given the same instruction. Even though we have no reference for what a jumping box would look like, we all have a pretty good idea of what a jumping creature would look like. Because we know what would happen in nature, by mimicking that, the animation feels more natural. But we can do more to make that wind-up feel a little more weighty.

If you watch cartoons, you’ll notice that natural movements are often exaggerated, creating a caricature of real life. When done well, this can feel just as natural as something in the real world, with the added bonus of infusing a little charm and character into the animation.

At this stage, collaboration between the designer and developer is crucial — but many designers may not even be aware that these options exist. It may be up to the developer to pitch this possibility to the designer.

By adding some subtle distortion to the scale of the box, we can add a lot to the animation:

See the Pen Bouncing Box 4 by Brandon Gregory (@pulpexploder) on CodePen.

Now, the box has character. It feels alive. There are many things to tweak, but this is already moving much farther than the original instruction — in a very good way!

We’re going to go a step further and add a little rebound at the end of the jump:

See the Pen Bouncing Box 5 by Brandon Gregory (@pulpexploder) on CodePen.

The second bounce is making this feel more alive, but something still seems off. The bounce looks stiff compared to the rest of the animation. We need to add another bit of distortion like we did for the wind-up:

See the Pen Bouncing Box 6 by Brandon Gregory (@pulpexploder) on CodePen.

That subtle distortion at the end makes the rebound seem much more natural. Overall, a huge improvement from our basic linear bounce in the first example.

That right there may be exactly what we’re looking for, but further tweaks to the rate of movement can be made with a custom cubic Bézier curve:

See the Pen Bouncing Box 7 by Brandon Gregory (@pulpexploder) on CodePen.

Without both the designer and the developer aware of basic animation principles and controls, this level of customization is impossible. Really, this article just scratches the surface of both fields. If you’re a web designer or a web developer who works with designers, I’d strongly urge you to read up on both.

For animation principles, The Illusion of Life: Disney Animation by Ollie Johnston and Frank Thomas is a great primer on how to make that caricature of real life seem alive and real. With that common language in place, communication and collaboration between designers and developers becomes much easier.

For the technical controls and variations of CSS animation, the possibilities are nearly endless. Delay and timing are simple to adjust. As mentioned, if you don’t like the out-of-the-box ease timing function, it’s very possible to create your own using a cubic-bezier(). You can also adjust the level of distortion you want to bring the animation closer to or further from reality. The important thing is that both the designer and developer are thinking about these variations rather than blindly taking everything without customization. Shared knowledge and collaboration can make even simple animations into great ones.

More Resources

  • 12 basic principles of animation – Wikipedia post outlining the concepts introduced in The Illusion of Life.
  • The Guide to CSS Animation: Principles and Examples – Smashing Magazine article providing a comprehensive guide to CSS animations.
  • Animation in Design Systems – 24 Ways article by Sarah Drasner
  • Animation property – CSS-Tricks almanac entry covering the property and its values
  • Transition property – CSS-Tricks almanac entry covering the property and its values
  • CubicBezier.com – Resource to create custom animation curves with a user interface.

Making CSS Animations Feel More Natural is a post from CSS-Tricks

Making your web app work offline, Part 2: The Implementation

This two-part series is a gentle, high-level introduction to offline web development. In Part 1 we got a basic service worker running, which caches our application resources. Now let’s extend it to support offline.

Article Series:

  1. The Setup
  2. The Implementation (you are here!)

Making an `offline.htm` file

Next, lets add some code to detect when the application is offline, and if so, redirect our users to a (cached) `offline.htm`.

But wait, if the service worker file is generated automatically, how do we go about adding in our own code, manually? Well, we can add an entry for importScripts, which tells our service worker to import the scripts we specify. It does this through the service worker’s native importScripts function, which is well-named. And we’ll also add our `offline.htm` file to our statically cached list of files. The new files are highlighted below:

new SWPrecacheWebpackPlugin({ mergeStaticsConfig: true, filename: "service-worker.js", importScripts: ["../sw-manual.js"], staticFileGlobs: [ //... "offline.htm" ], // the rest of the config is unchanged })

Now, let’s go in our `sw-manual.js` file, and add code to load the cached `offline.htm` file when the user is offline.

toolbox.router.get(/books$/, handleMain);
toolbox.router.get(/subjects$/, handleMain);
toolbox.router.get(/localhost:3000\/$/, handleMain);
toolbox.router.get(/mylibrary.io$/, handleMain); function handleMain(request) { return fetch(request).catch(() => { return caches.match("react-redux/offline.htm", { ignoreSearch: true }); });
}

We’ll use the toolbox.router object we saw before to catch all our top-level routes, and if the main page doesn’t load from the network, send back the (hopefully cached) `offline.htm` file.

This is one of the few times in this post you’ll see promises being used directly, instead of with the async syntax, mainly because in this case it’s actually easier to just tack on a .catch(), rather than set up a try{} catch{} block.

The `offline.htm` file will be pretty basic, just some HTML that reads cached books from IndexedDB, and displays them in a rudimentary table. But before showing that, let’s walk through how to actually use IndexedDB (if you want to just see it now, it’s here)

Hello World, IndexedDB

IndexedDB is an in-browser database. It’s ideal for enabling offline functionality since it can be accessed without network connectivity, but it’s by no means limited to that.

The API pre-dates Promises, so it’s callback based. We’ll go through everything with the native API, but in practice, you’ll likely want to wrap and simplify it, either with your own helper methods which wrap the functionality with Promises, or with a third-party utility.

Let me repeat: the API for IndexedDB is awful. Here’s Jake Archibald saying he wouldn’t even teach it directly

We’ll still go over it because I really want you to see everything as it is, but please don’t let it scare you away. There’s plenty of simplifying abstractions out there, for example dexie and idb.

Setting up our database

Let’s add code to sw-manual that subscribes to the service worker’s activate event, and checks to see if we already have an IndexedDB setup; if not, we’ll create, and then fill it with data.

First, the creating bit.

self.addEventListener("activate", () => { //1 is the version of IDB we're opening let open = indexedDB.open("books", 1); //should only be called the first time, when version 1 does not exist open.onupgradeneeded = evt => { let db = open.result; //this callback should only ever be called upon creation of our IDB, when an upgrade is needed //for version 1, but to be doubly safe, and also to demonstrade this, we'll check to see //if the stores exist if (!db.objectStoreNames.contains("books") || !db.objectStoreNames.contains("syncInfo")) { if (!db.objectStoreNames.contains("books")) { let bookStore = db.createObjectStore("books", { keyPath: "_id" }); bookStore.createIndex("imgSync", "imgSync", { unique: false }); } if (!db.objectStoreNames.contains("syncInfo")) { db.createObjectStore("syncInfo", { keyPath: "id" }); evt.target.transaction .objectStore("syncInfo") .add({ id: 1, lastImgSync: null, lastImgSyncStarted: null, lastLoadStarted: +new Date(), lastLoad: null }); } evt.target.transaction.oncomplete = fullSync; } };
});

The code’s messy and manual; as I said, you’ll likely want to add some abstractions in practice. Some of the key points: we check for the objectStores (tables) we’ll be using, and create them as needed. Note that we can even create indexes, which we can see on the books store, with the imgSync index. We also create a syncInfo store (table) which we’ll use to store information on when we last synced our data, so we don’t pester our servers too frequently, asking for updates.

When the transaction has completed, at the very bottom, we call the fullSync method, which loads all our data. Let’s see what that looks like.

Performing an initial sync

Below is the relevant portion of the syncing code, which makes repeated calls to our endpoint to load our books, page by page, adding each result to IDB along the way. Again, this is using zero abstractions, so expect a lot of bloat.

See this GitHub gist for the full code, which includes some additional error handling, and code which runs when the last page is finished.

function fullSyncPage(db, page) { let pageSize = 50; doFetch("/book/offlineSync", { page, pageSize }) .then(resp => resp.json()) .then(resp => { if (!resp.books) return; let books = resp.books; let i = 0; putNext(); function putNext() { //callback for an insertion, with indicators it hasn't had images cached yet if (i < pageSize) { let book = books[i++]; let transaction = db.transaction("books", "readwrite"); let booksStore = transaction.objectStore("books"); //extend the book with the imgSync indicated, add it, and on success, do this for the next book booksStore.add(Object.assign(book, { imgSync: 0 })).onsuccess = putNext; } else { //either load the next page, or call loadDone() } } });
}

The putNext() function is where the real work is done. This serves as the callback for each successful insertion’s success. In real life we’d hopefully have a nice method that adds each book, wrapped in a promise, so we could do a simple for of loop, and await each insertion. But this is the “vanilla” solution or at least one of them.

We modify each book before inserting it, to set the imgSync property to 0, to indicate that this book has not had its image cached, yet.

And after we’ve exhausted the last page, and there are no more results, we call loadDone(), to set some metadata indicating the last time we did a full data sync.

In real life, this would be a good time to sync all those images, but let’s instead do it on-demand by the web app itself, in order to demonstrate another feature of service workers.

Communicating between the web app, and service worker

Let’s just pretend it would be a good idea to have the books’ covers load the next time the user visits our page when the service worker is running. Let’s have our web app send a message to the service worker, and we’ll have the service worker receive it, and then sync the book covers.

From our app code, we attempt to send a message to a running service worker, instructing it to sync images.

In the web app:

if ("serviceWorker" in navigator) { try { navigator.serviceWorker.controller.postMessage({ command: "sync-images" }); } catch (er) {}
}

In `sw-manual.js`:

self.addEventListener("message", evt => { if (evt.data && evt.data.command == "sync-images") { let open = indexedDB.open("books", 1); open.onsuccess = evt => { let db = open.result; if (db.objectStoreNames.contains("books")) { syncImages(db); } }; }
});

In sw-manual we have code to catch that message, and call the syncImages() method. Let’s look at that, next.

function syncImages(db) { let tran = db.transaction("books"); let booksStore = tran.objectStore("books"); let idx = booksStore.index("imgSync"); let booksCursor = idx.openCursor(0); let booksToUpdate = []; //a cursor's onsuccess callback will fire for EACH item that's read from it booksCursor.onsuccess = evt => { let cursor = evt.target.result; //if (!cursor) means the cursor has been exhausted; there are no more results if (!cursor) return runIt(); let book = cursor.value; booksToUpdate.push({ _id: book._id, smallImage: book.smallImage }); //read the next item from the cursor cursor.continue(); }; async function runIt() { if (!booksToUpdate.length) return; for (let book of booksToUpdate) { try { //fetch, and cache the book's image await preCacheBookImage(book); let tran = db.transaction("books", "readwrite"); let booksStore = tran.objectStore("books"); //now save the updated book - we'll wrap the IDB callback-based opertion in //a manual promise, so we can await it await new Promise(res => { let req = booksStore.get(book._id); req.onsuccess = ({ target: { result: bookToUpdate } }) => { bookToUpdate.imgSync = 1; booksStore.put(bookToUpdate); res(); }; req.onerror = () => res(); }); } catch (er) { console.log("ERROR", er); } } }
}

We’re cracking open the imageSync index from before, and reading all books that have a zero, which means they haven’t had their images sync’d yet. The booksCursor.onsuccess will be called over and over again, until there are no books left; I’m using this to put them all into an array, at which point I call the runIt() method, which runs through them, calling preCacheBookImage() for each. This method will cache the image, and if there are no unforeseen errors, update the book in IDB to indicate that imgSync is now 1.

If you’re wondering why in the world I’m going through the trouble to save all the books from the cursor into an array, before calling runIt(), rather than just walking through the results of the cursor, and caching and updating as I go, well — it turns out transactions in IndexedDB are a bit weird. They complete when you yield to the event loop unless you yield to the event loop in a method provided by the transaction. So if we leave the event loop to go do other things, like make a network request to pull down an image, then the cursor’s transaction will complete, and we’ll get an error if we try to continue reading from it later.

Manually updating the cache.

Let’s wrap this up, and look at the preCacheBookImage method which actually pulls down a cover image, and adds it to the relevant cache, (but only if it’s not there already.)

async function preCacheBookImage(book) { let smallImage = book.smallImage; if (!smallImage) return; let cachedImage = await caches.match(smallImage); if (cachedImage) return; if (/https:\/\/s3.amazonaws.com\/my-library-cover-uploads/.test(smallImage)) { let cache = await caches.open("local-images1"); let img = await fetch(smallImage, { mode: "no-cors" }); await cache.put(smallImage, img); }
}

If the book has no image, we’re done. Next, we check if it’s cached already — if so, we’re done. Lastly, we inspect the URL, and figure out which cache it belongs in.

The local-images1 cache name is the same from before, which we set up in our dynamic cache. If the image in question isn’t already there, we fetch it, and add it to cache. Each cache operation returns a promise, so the async/await syntax simplifies things nicely.

Testing it out

The way it’s set up, if we clear our service worker either in dev tools, below, or by just opening a fresh incognito window…

…then the first time we view our app, all our books will get saved to IndexedDB.

When we refresh, the image sync will happen. So if we start on a page that’s already pulling down these images, we’ll see our normal service worker saving them to cache (ahem, assuming we delay the ajax call to give our Service Worker a chance to install), which is what these events are in our network tab.

Then, if we navigate elsewhere and refresh, we won’t see any network requests for those image, since our sync method is already finding everything in cache.

If we clear our service workers again, and start on this same page, which is not otherwise pulling these images down, then refresh, we’ll see the network requests to pull down, and sync these images to cache.

Then if we navigate back to the page that uses these images, we won’t see the calls to cache these images, since they’re already cached; moreover, we’ll see these images being retrieved from cache by the service worker.

Both our runtimeCaching provided by sw-toolbox, and our own manual code are working together, off of the same cache.

It works!

As promised, here’s the `offline.htm` page

<div style="padding: 15px"> <h1>Offline</h1> <table class="table table-condescend table-striped"> <thead> <tr> <th></th> <th>Title</th> <th>Author</th> </tr> </thead> <tbody id="booksTarget"> <!--insertion will happen here--> </tbody> </table>
</div>
let open = indexedDB.open("books");
open.onsuccess = evt => { let db = open.result; let transaction = db.transaction("books", "readonly"); let booksStore = transaction.objectStore("books"); var request = booksStore.openCursor(); let rows = ``; request.onsuccess = function(event) { var cursor = event.target.result; if(cursor) { let book = cursor.value; rows += ` <tr> <td><img src="${book.smallImage}" /></td> <td>${book.title}</td> <td>${Array.isArray(book.authors) ? book.authors.join("<br/>") : book.authors}</td> </tr>`; cursor.continue(); } else { document.getElementById("booksTarget").innerHTML = rows; } };
}

Now let’s tell Chrome to pretend to be offline, and test it out:

Cool!

Where to, from here?

We’re barely scratching the surface. Your users can update these data from multiple devices, and each one will need to keep in sync somehow. You could either periodically wipe your IDB tables and re-sync; have the user manually trigger a re-sync when they want; or you could get really ambitious and try to log all your mutations on your server, and have each service worker on each device request all changes that happened since the last time it ran, in order to sync up.

The most interesting solution here is PouchDB, which does this syncing for you; the catch is it’s designed to work with CouchDB, which you may or may not be using.

Syncing local changes

For one last piece of code, let’s consider an easier problem to solve: syncing your IndexedDB with changes that are made right this minute, by your user who’s using your web app. We can already intercept fetch requests in the service worker, so it should be easy to listen for the right mutation endpoint, run it, then then peak at the results and update IndexedDB accordingly. Let’s take a look.

toolbox.router.post(/graphql/, request => { //just run the request as is return fetch(request).then(response => { //clone it by necessity let respClone = response.clone(); //do this later - get the response back to our user NOW setTimeout(() => { respClone.json().then(resp => { //this graphQL endpoint is for lots of things - inspect the data response to see //which operation we just ran if (resp && resp.data && resp.data.updateBook && resp.data.updateBook.Book) { syncBook(resp.data.updateBook.Book); } }, 5); }); //return the response to our user NOW, before the IDB syncing return response; });
}); function syncBook(book) { let open = indexedDB.open("books", 1); open.onsuccess = evt => { let db = open.result; if (db.objectStoreNames.contains("books")) { let tran = db.transaction("books", "readwrite"); let booksStore = tran.objectStore("books"); booksStore.get(book._id).onsuccess = ({ target: { result: bookToUpdate } }) => { //update the book with the new values ["title", "authors", "isbn"].forEach(prop => (bookToUpdate[prop] = book[prop])); //and save it booksStore.put(bookToUpdate); }; } };
}

This may seem a bit more involved than you were hoping. We can only read the fetch response once, and our application thread will also need to read it, so we’ll first clone the response. Then, we’ll run a setTimeout() so we can return the original response to the web application/user as quickly as possible, and do what we need thereafter. Don’t just rely on the promise in respClone.json() to do this, since promises use microtasks. I’ll let Jake Archibald explain what exactly that means, but the short of it is that they can starve the main event loop. I’m not quite smart enough to be certain whether that applies here, so I just went with the safe approach of setTimeout.

Since I’m using GraphQL, the responses are in a predictable format, and it’s easy to see if I just performed the operation I’m interested in, and if so I can re-sync the affected data.

Further reading

Literally everything here is explained in wonderful depth in this book by Tal Ater. If you’re interested in learning more, you can’t beat that as a learning resource.

For some more immediate, quick resources, here’s an MDN article on IndexedDB, and a service workers introduction, and offline cookbook, both from Google.

Parting thoughts

Giving your user useful things to do with your web app when they don’t even have network connectivity is an amazing new ability web developers have. As you’ve seen though, it’s no easy task. Hopefully this post has given you a realistic idea of what to expect, and a decent introduction to the things you’ll need to do to accomplish this.

Article Series:

  1. The Setup
  2. The Implementation (you are here!)

Making your web app work offline, Part 2: The Implementation is a post from CSS-Tricks

Making your web app work offline, Part 1: The Setup

This two-part series is a gentle introduction to offline web development. Getting a web application to do something while offline is surprisingly tricky, requiring a lot of things to be in place and functioning correctly. We’re going to cover all of these pieces from a high level, with working examples. This post is an overview, but there are plenty of more-detailed resources listed throughout.

Article Series:

  1. The Setup (you are here!)
  2. The Implementation

Basic approach

I’ll be making heavy use of JavaScript’s async/await syntax. It’s supported in all major browsers and Node, and greatly simplifies Promise-based code. The link above explains async well, but in a nutshell they allow you to resolve a promise, and access its value directly in code with await, rather than calling .then and accessing the value in the callback, which often leads to the dreaded “rightward drift.”

What are we building?

We’ll be extending an existing booklist project to sync the current user’s books to IndexedDB, and create a simplified offline page that’ll show even when the user has no network connectivity.

Starting with a service worker

The one non-negotiable thing you need for offline development is a service worker. A service worker is a background process that can, among other things, intercept network requests; redirect them; short circuit them by returning cached responses; or execute them as normal and do custom things with the response, like caching.

Basic caching

Probably the first, most basic, yet high impact thing you’ll do with a service worker is have it cache your application’s resources. Service worker and the cache it uses are extremely low-level primitives; everything is manual. In order to properly cache your resources you’ll need to fetch and add them to a cache, but then you’ll also need to track changes to these resources. You’ll track when they change, remove the prior version, and fetch and update the new one.

In practice, this means your service worker code will need to be generated as part of a build step, which hashes your files, and generates a file that’s smart enough to record these changes between versions, and update caches as needed.

Abstractions to the rescue

This is extremely tedious and error-prone code that you’d likely never want to write yourself. Luckily some smart people have written abstractions to help, namely sw-precache, and sw-toolbox by the great people at Google. Note, Google has since deprecated these tools in favor of the newer Workbox. I’ve yet to move my code over since sw-* works so well, but in any event the ideas are the same, and I’m told the conversion is easy. And it’s worth mentioning that sw-precache currently has about 30,000 downloads per day, so it’s still widely used.

Hello World, sw-precache

Let’s jump right in. We’re using webpack, and as webpack goes, there’s a plugin, so let’s check that out first.

// inside your webpack config
new SWPrecacheWebpackPlugin({ mergeStaticsConfig: true, filename: "service-worker.js", staticFileGlobs: [ //static resources to cache "static/bootstrap/css/bootstrap-booklist-build.css", ... ], ignoreUrlParametersMatching: /./, stripPrefixMulti: { //any paths that need adjusting "static/": "react-redux/static/", ... }, ...
})

By default ALL of the bundles webpack makes will be precached. We’re also manually providing some paths to static resources I want cached in the staticFileGlobs property, and I’m adjusting some paths in stripPrefixMulti.

// inside your webpack config
const getCache = ({ name, pattern, expires, maxEntries }) => ({ urlPattern: pattern, handler: "cacheFirst", options: { cache: { maxEntries: maxEntries || 500, name: name, maxAgeSeconds: expires || 60 * 60 * 24 * 365 * 2 //2 years }, successResponses: /0|[123].*/ }
}); new SWPrecacheWebpackPlugin({ ... runtimeCaching: [ //pulls in sw-toolbox and caches dynamically based on a pattern getCache({ pattern: /^https:\/\/images-na.ssl-images-amazon.com/, name: "amazon-images1" }), getCache({ pattern: /book\/searchBooks/, name: "book-search", expires: 60 * 7 }), //7 minutes ... ]
})

Adding the runtimeCaching section to our SWPrecacheWebpackPlugin pulls in sw-toolbox and lets us cache urls matching a certain pattern, dynamically, as needed—with getCache helping keep the boilerplate to a minimum.

Hello World, sw-toolbox

The entire service worker file that’s generated is pretty big, but let’s just look at a small piece, namely one of the dynamic caches from above:

toolbox.router.get(/^https:\/\/images-na.ssl-images-amazon.com/, toolbox.cacheFirst, { cache: { maxEntries: 500, name: "amazon-images1", maxAgeSeconds: 63072000 }, successResponses: /0|[123].*/
});

sw-toolbox has provided us with a nice, high-level router object we can use to hook into various URL requests, MVC-style. We’ll use this to setup offline shortly.

Don’t forget to register the service worker

And, of course, the existence of the service worker file that’s generated above is of no use by itself; it needs to be registered. The code looks like this, but be sure to either have it inside an onload listener, or some other place that’ll be guaranteed to run after the page has loaded.

if ("serviceWorker" in navigator) { navigator.serviceWorker.register("https://cdn.css-tricks.com/service-worker.js");
}

There we have it! We got a basic service worker running, which caches our application resources. Tune in tomorrow when we extend it to support offline.

Article Series:

  1. The Setup (you are here!)
  2. The Implementation

Making your web app work offline, Part 1: The Setup is a post from CSS-Tricks

Animating Border

Transitioning border for a hover state. Simple, right? You might be unpleasantly surprised.

The Challenge

The challenge is simple: building a button with an expanding border on hover.

This article will focus on genuine CSS tricks that would be easy to drop into any project without having to touch the DOM or use JavaScript. The methods covered here will follow these rules

  • Single element (no helper divs, but psuedo-elements are allowed)
  • CSS only (no JavaScript)
  • Works for any size (not restricted to a specific width, height, or aspect ratio)
  • Supports transparent backgrounds
  • Smooth and performant transition

I proposed this challenge in the Animation at Work Slack and again on Twitter. Though there was no consensus on the best approach, I did receive some really clever ideas by some phenomenal developers.

Method 1: Animating border

The most straightforward way to animate a border is… well, by animating border.

.border-button { border: solid 5px #FC5185; transition: border-width 0.6s linear;
} .border-button:hover { border-width: 10px; }

See the Pen by Shaw (@shshaw) on CodePen.

Nice and simple, but there are some big performance issues.

Since border takes up space in the document’s layout, changing the border-width will trigger layout. Nearby elements will shift around because of the new border size, making browser reposition those elements every frame of the animation unless you set an explicit size on the button.

As if triggering layout wasn’t bad enough, the transition itself feels “stepped”. I’ll show why in the next example.

Method 2: Better border with outline

How can we change the border without triggering layout? By using outline instead! You’re probably most familiar with outline from removing it on :focus styles (though you shouldn’t), but outline is an outer line that doesn’t change an element’s size or position in the layout.

.border-button { outline: solid 5px #FC5185; transition: outline 0.6s linear; margin: 0.5em; /* Increased margin since the outline expands outside the element */
} .border-button:hover { outline-width: 10px; }

See the Pen by Shaw (@shshaw) on CodePen.

A quick check in Dev Tools’ Performance tab shows the outline transition does not trigger layout. Regardless, the movement still seems stepped because browsers are rounding the border-width and outline-width values so you don’t get sub-pixel rendering between 5 and 6 or smooth transitions from 5.4 to 5.5.

See the Pen by Shaw (@shshaw) on CodePen.

Strangely, Safari often doesn’t render the outline transition and occasionally leaves crazy artifacts.

border artifact in safari

Method 3: Cut it with clip-path

First implemented by Steve Gardner, this method uses clip-path with calc to trim the border down so on hover we can transition to reveal the full border.

.border-button { /* Full width border and a clip-path visually cutting it down to the starting size */ border: solid 10px #FC5185; clip-path: polygon( calc(0% + 5px) calc(0% + 5px), /* top left */ calc(100% - 5px) calc(0% + 5px), /* top right */ calc(100% - 5px) calc(100% - 5px), /* bottom right */ calc(0% + 5px) calc(100% - 5px) /* bottom left */ ); transition: clip-path 0.6s linear;
} .border-button:hover { /* Clip-path spanning the entire box so it's no longer hiding the full-width border. */ clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}

See the Pen by Shaw (@shshaw) on CodePen.

clip-path technique is the smoothest and most performant method so far, but does come with a few caveats. Rounding errors may cause a little unevenness, depending on the exact size. The border also has to be full size from the start, which may make exact positioning tricky.

Unfortunately there’s no IE/Edge support yet, though it seems to be in development. You can and should encourage Microsoft’s team to implement those features by voting for masks/clip-path to be added.

Method 4: linear-gradient background

We can simulate a border using a clever combination of multiple linear-gradient backgrounds properly sized. In total we have four separate gradients, one for each side. The background-position and background-size properties get each gradient in the right spot and the right size, which can then be transitioned to make the border expand.

.border-button { background-repeat: no-repeat; /* background-size values will repeat so we only need to declare them once */ background-size: calc(100% - 10px) 5px, /* top & bottom */ 5px calc(100% - 10px); /* right & left */ background-position: 5px 5px, /* top */ calc(100% - 5px) 5px, /* right */ 5px calc(100% - 5px), /* bottom */ 5px 5px; /* left */ /* Since we're sizing and positioning with the above properties, we only need to set up a simple solid-color gradients for each side */ background-image: linear-gradient(0deg, #FC5185, #FC5185), linear-gradient(0deg, #FC5185, #FC5185), linear-gradient(0deg, #FC5185, #FC5185), linear-gradient(0deg, #FC5185, #FC5185); transition: all 0.6s linear; transition-property: background-size, background-position;
} .border-button:hover { background-position: 0 0, 100% 0, 0 100%, 0 0; background-size: 100% 10px, 10px 100%, 100% 10px, 10px 100%;
}

See the Pen by Shaw (@shshaw) on CodePen.

This method is quite difficult to set up and has quite a few cross-browser differences. Firefox and Safari animate the faux-border smoothly, exactly the effect we’re looking for. Chrome’s animation is jerky and even more stepped than the outline and border transitions. IE and Edge refuse to animate the background at all, but they do give the proper border expansion effect.

Method 5: Fake it with box-shadow

Hidden within box-shadow‘s spec is a fourth value for spread-radius. Set all the other length values to 0px and use the spread-radius to build your border alternative that, like outline, won’t affect layout.

.border-button { box-shadow: 0px 0px 0px 5px #FC5185; transition: box-shadow 0.6s linear; margin: 0.5em; /* Increased margin since the box-shado expands outside the element, like outline */
} .border-button:hover { box-shadow: 0px 0px 0px 10px #FC5185; }

See the Pen by Shaw (@shshaw) on CodePen.

The transition with box-shadow is adequately performant and feels much smoother, except in Safari where it’s snapping to whole-values during the transition like border and outline.

Pseudo-Elements

Several of these techniques can be modified to use a pseudo-element instead, but pseudo-elements ended up causing some additional performance issues in my tests.

For the box-shadow method, the transition occasionally triggered paint in a much larger area than necessary. Reinier Kaper pointed out that a pseudo-element can help isolate the paint to a more specific area. As I ran further tests, box-shadow was no longer causing paint in large areas of the document and the complication of the pseudo-element ended up being less performant. The change in paint and performance may have been due to a Chrome update, so feel free to test for yourself.

I also could not find a way to utilize pseudo-elements in a way that would allow for transform based animation.

Why not transform: scale?

You may be firing up Twitter to helpfully suggest using transform: scale for this. Since transform and opacity are the best style properties to animate for performance, why not use a pseudo-element and have the border scale up & down?

.border-button { position: relative; margin: 0.5em; border: solid 5px transparent; background: #3E4377;
} .border-button:after { content: ''; display: block; position: absolute; top: 0; right: 0; bottom: 0; left: 0; border: solid 10px #FC5185; margin: -15px; z-index: -1; transition: transform 0.6s linear; transform: scale(0.97, 0.93);
} .border-button:hover::after { transform: scale(1,1); }

See the Pen by Shaw (@shshaw) on CodePen.

There are a few issues:

  1. The border will show through a transparent button. I forced a background on the button to show how the border is hiding behind the button. If your design calls for buttons with a full background, then this could work.
  2. You can’t scale the border to specific sizes. Since the button’s dimensions vary with the text, there’s no way to animate the border from exactly 5px to 10px using only CSS. In this example I’ve done some magic-numbers on the scale to get it to appear right, but that won’t be universal.
  3. The border animates unevenly because the button’s aspect ratio isn’t 1:1. This usually means the left/right will appear larger than the top/bottom until the animation completes. This may not be an issue depending on how fast your transition is, the button’s aspect ratio, and how big your border is.

If your button has set dimensions, Cher pointed out a clever way to calculate the exact scales needed, though it may be subject to some rounding errors.

Beyond CSS

If we loosen our rules a bit, there are many interesting ways you can animate borders. Codrops consistently does outstanding work in this area, usually utilizing SVGs and JavaScript. The end results are very satisfying, though they can be a bit complex to implement. Here are a few worth checking out:

  • Creative Buttons
  • Button Styles Inspiration
  • Animated Checkboxes
  • Distorted Button Effects
  • Progress Button Styles

Conclusion

There’s more to borders than simply border, but if you want to animate a border you may have some trouble. The methods covered here will help, though none of them are a perfect solution. Which you choose will depend on your project’s requirements, so I’ve laid out a comparison table to help you decide.

See the Pen by Shaw (@shshaw) on CodePen.

My recommendation would be to use box-shadow, which has the best overall balance of ease-of-implementation, animation effect, performance and browser support.

Do you have another way of creating an animated border? Perhaps a clever way to utilize transforms for moving a border? Comment below or reach me on Twitter to share your solution to the challenge.

Special thanks to Martin Pitt, Steve Gardner, Cher, Reinier Kaper, Joseph Rex, David Khourshid, and the Animation at Work community.


Animating Border is a post from CSS-Tricks