The Power of Named Transitions in Vue

Vue offers several ways to control how an element or component visually appears when inserted into the DOM. Examples can be fading in, sliding in, or other visual effects. Almost all of this functionality is based around a single component: the transition component.

A simple example of this is with a single v-if based on a Boolean. When the Boolean is true, the element appears. When the Boolean is false, the element disappears. Normally, this element would just pop in and out of existence, but with the transition component you can control the visual effect.

<transition> <div v-if="isVisible">is this visible?</div>
</transition>

Several articles have been written that cover the transition component quite well, like articles from Sarah Drasner, Nicolas Udy, and Hassan Djirdeh. Each article covers different aspects of Vue’s transition component in detail. This article will expand on the topic by focusing on one aspect of the transition component; the fact that they can be “named.”

<transition name="fade"> <div v-if="isVisible">is this visible?</div>
</transition>

The initial change this attribute offers is that the CSS classes injected onto the element during the transition sequence will be prefixed by the given name. Basically, it would be fade-enter instead of v-enter from the example above. This single attribute can go well beyond this simple option. It can be used to leverage certain features of Vue and CSS which allows for some interesting outcomes.

Another thing to consider is that the name attribute can be bound:

<transition v-bind:name="currentTransition"> <div v-if="isVisible">is this visible?</div>
</transition>

In this example, the transition will be named the value currentTransition resolves to. This simple change provides another level of options and features to an app’s animations. With static and dynamic named transitions, a project can have a series of prebuilt transitions ready to apply throughout the entire app, components that can extend existing transitions applied to them, switch a transition being used before or after being applied, allowing users to choose transitions, and control how individual elements of a list transition into place based on the current state of that list.

This article is intended to explore these features and explain how to use them.

What happens when transitions are named?

By default, when a transition component is used, it applies specific classes in a specific sequence to the element. These classes can be leveraged in CSS. Without any CSS, these classes, in essence, do nothing for the element. Therefore, there is a need for CSS of this nature:

.v-enter,
.v-leave-to { opacity: 0;
} .v-enter-active,
.v-leave-active { transition: 0.5s;
}

This causes the element to fade in and out with a duration of half a second. A minor change to the transition provides for elegant visual feedback to the user. Still, there is an issue to consider. But first, what’s different with a named transition?

.fade-enter,
.fade-leave-to { opacity: 0;
} .fade-enter-active,
.fade-leave-active { transition: 0.5s;
}

Essentially the same CSS but with fade- prefixed instead of v-. This naming addresses the potential issue that can happen when using the default class names of the transition component. The v- prefix makes the classes global in effect, especially if the CSS is placed in the style block of the app’s root level. This would, in effect, make *all* transitions without a name attribute throughout the entire app use the same transition effect. For small apps this may suffice, but in larger, more complex apps, it may lead to undesirable visual effects, as not everything should fade in and out over half a second.

Naming transitions provides a level of control for developers throughout the project as to how different elements or components are inserted or removed visually. It is suggested that all transitions be named — even if there is just one — to establish the habit of doing so. Even if an app has only one transition effect, there may be a need to add a new one at a future point. Having already named existing transitions in the project eases the effort of adding a new one.

Building a collection of transition effects

Naming transitions provides for a simple yet very useful process. A common practice might be to create the transition classes as part of the component that is using them. If another common practice of scoping styles for a component is done, those classes will only be available to that particular component. If two different components have similar transitions in their style blocks, then we are just duplicating code.

So, let’s consider keeping CSS for transitions in the style block of the root of the app, typically the app.vue file. For most of my projects, I place them as the last section of the style block, making them easy to locate for adjustments and additions. Keeping the CSS in this location makes the transition effects available to every use of the transition component throughout the entire app. Here are examples from some of my projects.

.fade-enter,
.fade-leave-to { opacity: 0; }
.fade-enter-active,
.fade-leave-active { transition: 0.5s; } .slide-enter { opacity: 0; transform: scale3d(2, 0.5, 1) translate3d(400px, 0, 0);
} .slide-enter-to { transform: scale3d(1, 1, 1); }
.slide-enter-active,
.slide-leave-active { transition: 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); }
.slide-leave { transform: scale3d(1, 1, 1); } .slide-leave-to { opacity: 0; transform: scale3d(2, 0.5, 1) translate3d(-400px, 0, 0);
} .rotate-enter { transform: perspective(500px) rotate3d(0, 1, 0, 90deg); }
.rotate-enter-active,
.rotate-leave-active { transition: 0.5s; }
.rotate-leave-to { transform: perspective(500px) rotate3d(0, 1, 0, -90deg); }

There are multiple ways to store these transition classes depending on your preferences and the needs of the project. The first, as mentioned earlier, is to keep it all in the style block of the app.vue file. You can also keep a Sass partial of all the transitions in the project’s assets folder and import it into the app’s style block.

<style lang="scss"> @import "assets/_transitions.scss";
</style>

This method allows for adjustments and additions to the collection of transitions outside of the Vue files. Another benefit of this setup is that such a file can be easily transferred between projects if they share transition effects. If one project gets a new transition, then it’s easy enough to transfer the addition to another project without having to touch main project files.

If you’re using CSS instead of Sass, then you can include the file as a requirement of the project. You can accomplish this by keeping the file in the assets folder of the project and placing a require statement in the main.js file.

require("@/assets/transitions.css");

Another option is keep the transition styles in a static CSS file that can be stored elsewhere, either in the public folder of the project or just on the server itself. Since this is a regular CSS file, no building or deployment would be required — just include a link reference in the index.html file.

<link rel="stylesheet" type="text/css" href="/css/transitions.css">

This file could also potentially be stored in a CDN for all projects to share. Whenever the file is updated, the changes are immediately available everywhere it is referenced. If a new transition name is created, then existing projects can start using the new name as needed.

Now, let’s slow down a minute

While we’re building a collection of transitions to use throughout our project, let’s consider users out there who may not want abrupt animations, or who may want no animations at all. Some people could consider our animations over-the-top and unnecessary, but for some, they can actually cause problems. Some time ago, WebKit introduced the prefers-reduced-motion media query to assist with possible Vestibular Spectrum Disorder issues. Eric Bailey also posted a nice introduction to the media query as well.

In most cases, adding the media query as part of our collection of transitions is quite easy and should be considered. We can either reduce the amount of motion involved in the transition to reduce the negative effects or simply turn them off.

Here’s a simple example from one of my demos below:

.next-enter { opacity: 0; transform: scale3d(2, 0.5, 1) translate3d(400px, 0, 0);
} .next-enter-to { transform: scale3d(1, 1, 1); }
.next-enter-active,
.next-leave-active { transition: 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); }
.next-leave { transform: scale3d(1, 1, 1); } .next-leave-to { opacity: 0; transform: scale3d(2, 0.5, 1) translate3d(-400px, 0, 0);
} /* If animations are reduced at the OS level, use simpler transitions */
@media screen and (prefers-reduced-motion: reduce) { .next-enter { opacity: 0; transform: translate3d(100px, 0, 0); } .next-enter-active, .next-leave-active { transition: 0.5s; } .next-leave-to { opacity: 0; transform: translate3d(-100px, 0, 0); }
}

In that example, I took what was a rather exaggerated transition and made it simpler. The animation is a slide that moves to the left with an elastic ease, then scales down and fades out as it moves away. If someone has the reduce motion preference set, then the animation becomes a much simpler transition with a shorter distance (which gives it a slower velocity) and keeps the fade. If we had wanted to turn them off, then we’d only need to reference the classes with the transition property and set their value to none.

To test this requires finding and selecting a checkbox on your respective OS. On Windows, you will find it in Control Panel > Ease of Access Center > Make the computer easier to see section; look for “Turn off all unnecessary animations (when possible).” On a Mac, look under System Preferences > Accessibility > Display; look for “Reduce motion.” The latest iOS devices have a similar setting under Accessibility as well.

Let’s stay flexible with our transitions collection

With this collection of transitions, there is the potential snag of a lack of flexibility with the effects. For instance, what if one element needs a slightly slower fade time? Let’s say that everything else in the effect can stay the same, only the transition-duration needs to be different. There are ways to adjust for that without having to create a whole new transition name.

The easiest method is to use an inline style directly on the element within the transition component.

<transition name="fade"> <div style="transition-duration: 6s;" v-if="isVisible">this has a different duration</div>
</transition>

Such a change can also be done through the various ways Vue offers handling styles and classes.

Let’s say you are using the component element with the is attribute for dynamic components such as this:

<transition name="fade" mode="out-in"> <component :is="currentComponent"></component>
</transition>

Even with this dynamic component, we have options to adjust properties of the transition effect. Again, we can apply an inline style on the component element, which will be placed on the root element of the component. The root element also receives the transition classes, so we would directly override their properties.

<transition name="fade" mode="out-in"> <component :is="currentComponent" style="transition-duration: 6s;"></component>
</transition>

Another option is to pass in props to our components. That way, the desired changes can be applied through the component’s code to its root element.

<transition name="fade" mode="out-in"> <component :is="currentComponent" duration="6s"></component>
</transition>
<template> <div :style="`transition-duration: ${duration}`">component one</div>
</template> <script>
export default { name: "component-one", props: { duration: String }
};
</script>

We can also override the properties of the transition’s classes inside the component’s style block, especially if it is scoped.

<style scoped> .fade-enter-active, .fade-leave-active { transition-duration: 1s; }
</style>

In this case, the component will have a fade duration of one second instead of the global duration of half-a-second. We can even take it a step further and have different durations for each side of the sequence.

<style scoped> .fade-enter-active { transition-duration: 1s; } .fade-leave-active { transition-duration: 2s; }
</style>

Any of the global transition classes can be altered within the component when needed. Although this isn’t quite as flexible as changing the property outside of a class structure, it can still be quite useful in certain circumstances.

As you can see, even with our collection of prebuilt transitions, we still have options for flexibility.

Dynamic transitions

Even after all these interesting things we can do with Vue’s transition component, yet another interesting feature waits to be explored. The name attribute on the transition component can be dynamic in nature, meaning we can change the current transition in use at will.

This means that the transition can be changed to have different animation effects with different situations, based in code. For example, we could have a transition change because of the answer to a question, transitions decided from user interaction, and have a list use different transitions based on the current state of the list itself.

Let’s look into these three examples.

Example 1: Change transition based on an answer

In this example, we have a simple math question that must be answered. Two numbers are randomly selected and we are expected to provide the sum. Then the button is clicked to check the answer against the expected answer. A small notification appears above the equation that indicates whether the answer is true or false. If the answer is correct, the notification is given a transition that suggests a head nodding yes with an up and down animation. If your answer is incorrect, the notification goes side-to-side suggesting a head shaking no.

See the Pen
VueJS Dynamic Transitions: Change Transition Based on an Answer
by Travis Almand (@talmand)
on CodePen.

The logic behind this is not overly complicated, nor is the setup of the transition. Here’s the HTML:

<transition :name="currentTransition"> <div id="notification" :class="response.toString()" v-if="answerChecked">{{ response }}</div>
</transition>

Rather simple in nature. We have a bound name on the transition and then a v-if on the notification div. We also apply a true or false class to decorate the notification based on the response.

Here’s the CSS for the transitions:

.positive-enter-active { animation: positive 1s; }
@keyframes positive { 0% { transform: translate3d(0, 0, 0); } 25% { transform: translate3d(0, -20px, 0); } 50% { transform: translate3d(0, 20px, 0); } 75% { transform: translate3d(0, -20px, 0); } 100% { transform: translate3d(0, 0, 0); }
} .negative-enter-active { animation: negative 1s; }
@keyframes negative { 0% { transform: translate3d(0, 0, 0); } 25% { transform: translate3d(-20px, 0, 0); } 50% { transform: translate3d(20px, 0, 0); } 75% { transform: translate3d(-20px, 0, 0); } 100% { transform: translate3d(0, 0, 0); }
}

You’ll see that I’m using CSS animations to accomplish the up-and-down and side-to-side effects.

Here’s some of the JavaScript:

methods: { randomProblem: function () { this.a = Math.floor(Math.random() * Math.floor(10)); this.b = Math.floor(Math.random() * Math.floor(10)); }, check: function () { this.response = this.a + this.b === parseInt(this.answer); this.answerChecked = true; this.currentTransition = this.response ? 'positive' : 'negative'; }, reset: function () { this.answer = null; this.answerChecked = false; this.randomProblem(); }
}

There’s the randomProblem method that sets up our equation. The check method that decides on which transition effect to use based on comparing the provided answer with the correct answer. Then the simple reset method that just, well, resets everything.

This is just a simple example. Another possible example is having a notification that has two different effects based on whether the notification is important or not. If the message is not overly important, then we can have a subtle animation that doesn’t drive the user’s eyes away from the current task. If it is important, we could use an animation that is more direct in nature in an effort to force the eyes up to the notification.

Example 2: Change transition based on user interaction

Another thing we can build is a carousel of some sort. This could be presentation slides, an image gallery, or a series of instructions. The basic idea is that we have a need to present information to the user in a sequence. In this presentation, the user gets to decide when to proceed and whether to move forward or to go backward.

See the Pen
VueJS Dynamic Transitions: Change Transition Based on User Interaction
by Travis Almand (@talmand)
on CodePen.

This, again, is a rather simple setup. The example, more or less, is a slide presentation type of situation. The two buttons at the bottom shift between two components with a sliding transition. A real project would have more components or perhaps logic to change the contents of the components based on the current slide. This example shall stay simple to demonstrate the idea.

Here’s the HTML:

<transition :name="currentTransition" mode="out-in"> <component :is="slides[currentSlide]"></component>
</transition>

You’ll see that we’re merely transitioning whenever the component is switched out by a bound is attribute on the component element.

Here’s the CSS:

.next-enter { opacity: 0; transform: scale3d(2, 0.5, 1) translate3d(400px, 0, 0);
} .next-enter-to { transform: scale3d(1, 1, 1); }
.next-enter-active,
.next-leave-active { transition: 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); }
.next-leave { transform: scale3d(1, 1, 1); } .next-leave-to { opacity: 0; transform: scale3d(2, 0.5, 1) translate3d(-400px, 0, 0);
} .prev-enter { opacity: 0; transform: scale3d(2, 0.5, 1) translate3d(-400px, 0, 0);
} .prev-enter-to { transform: scale3d(1, 1, 1); }
.prev-enter-active,
.prev-leave-active { transition: 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); }
.prev-leave { transform: scale3d(1, 1, 1); } .prev-leave-to { opacity: 0; transform: scale3d(2, 0.5, 1) translate3d(400px, 0, 0);
} /* If animations are reduced at the OS level, use simpler transitions */
@media screen and (prefers-reduced-motion: reduce) { .next-enter { opacity: 0; transform: translate3d(100px, 0, 0); } .next-enter-active, .next-leave-active { transition: 0.5s; } .next-leave-to { opacity: 0; transform: translate3d(-100px, 0, 0); } .prev-enter { opacity: 0; transform: translate3d(-100px, 0, 0); } .prev-enter-active, .prev-leave-active { transition: 0.5s; } .prev-leave-to { opacity: 0; transform: translate3d(100px, 0, 0); }
}

Here we have two transitions, one for when the user clicks on the “next” button and the other is for the “prev” button. Each essentially slides the component in the appropriate direction with the transform property, but with a few extras to create a kind of squeezing effect for a cartoonish feel. We also make use of prefers-reduced-motion to change the animation to be a simpler fade with a small slide to the side in the appropriate direction.

Now, for the JavaScript:

methods: { changeSlide: function (dir) { this.currentSlide = dir === 'next' ? this.currentSlide + 1 : this.currentSlide - 1; this.currentTransition = dir; }
}

Each button calls the changeSlide method on its click event and passes which direction it represents. Then we have some logic to keep track of what the current slide happens to be. A single line controls which transition to use. Since the “next” button passes “next” as the direction it corresponds to the “next” transition in the CSS. Same for the “prev” button. Each time the user clicks a button, the app automatically knows which transition to use. Thus, we have nice transition effects that provide context as to which direction the user is progressing through the sequence.

Example 3: Change transition based on list state

For our final example, we’ll see how to change transitions based on the current state of a list inside a transition-group component. The idea here is a list to be updated an item at a time with a different transition each time.

See the Pen
VueJS Dynamic Transitions: Change Transition Based on List State
by Travis Almand (@talmand)
on CodePen.

In this example, we are presented with a list of cities on the right and a blank list on the left. As cities are chosen on the right, they fill in the blanks on the left. The first city slides in from above while fading into view. The next cities before the last will slide in either from the right or the left, depending on the previous transition, and the last city slides in from below.

Here’s the HTML:

<transition-group :name="currentListTransition" tag="ul" class="list"> <li v-for="(item, index) in selectedItems" :key="item">{{ item }}</li>
</transition-group>

As usual, a rather simple setup. Here are the transitions in CSS:

.top-enter-active,
.top-leave-active { transition: 0.5s; }
.top-enter,
.top-leave-to { opacity: 0; transform: translate3d(0, -40px, 0);
} .top-move { opacity: 0.5; transition: 0.5s;
} .left-enter-active,
.left-leave-active { transition: 0.5s; }
.left-enter,
.left-leave-to { opacity: 0; transform: translate3d(-40px, 0, 0);
} .left-move { opacity: 0.5; transition: 0.5s;
} .right-enter-active,
.right-leave-active { transition: 0.5s; }
.right-enter,
.right-leave-to { opacity: 0; transform: translate3d(40px, 0, 0);
} .right-move { opacity: 0.5; transition: 0.5s;
} .bottom-enter-active,
.bottom-leave-active { transition: 0.5s; }
.bottom-enter,
.bottom-leave-to { opacity: 0; transform: translate3d(0, 30px, 0);
} .bottom-move { opacity: 0.5; transition: 0.5s;
} /* If animations are reduced at the OS level, turn off transitions */
@media screen and (prefers-reduced-motion: reduce) { .top-enter-active, .top-leave-active { transition: none; } .top-move { transition: none; } .left-enter-active, .left-leave-active { transition: none; } .left-move { transition: none; } .right-enter-active, .right-leave-active { transition: none; } .right-move { transition: none; } .bottom-enter-active, .bottom-leave-active { transition: none; } .bottom-move { transition: none; }
}

As you can see, a transition for each possible direction of the cities appearing the blank list.

Now, for our JavaScript:

methods: { chooseCity: function (index) { let selectedLength = this.selectedItems.length; let citiesLength = this.cities.length; let clt = this.currentListTransition; if (selectedLength === 0) { clt = 'top'; } else if (selectedLength > 0 && selectedLength < citiesLength - 1) { clt = clt === 'top' || clt === 'left' ? 'right' : 'left'; } else if (selectedLength === citiesLength - 1) { clt = 'bottom'; } this.currentListTransition = clt; this.selectedItems.push(this.cities[index]); document.querySelector(`.city:nth-child(${index + 1})`).classList.add('selected'); }, clearSelection: function () { this.currentListTransition = 'right'; this.selectedItems = []; document.querySelectorAll('.city.selected').forEach(element => { element.classList.remove('selected'); }); }
}

The chooseCity method handles what happens as you choose each city. What we mostly care about is the series of if and if/else statements in the middle of the method. As cities are selected, the logic looks at the current length of the selectedItems array that the selected cities eventually get pushed into. If the length is zero, then that’s the first city, so the transition should have it come in from the top. If the length is between zero and the total number of our cities list, then the transition should be right or left. The new direction used is based on the direction of the previous transition direction. Then, finally, if we’re on the last city to be chosen, it’ll change to the bottom transition. Again we use prefers-reduced-motion, in this case to turn off the transitions altogether.

Another option to change transitions for a list is changing according to the type of items chosen; such as east coast versus west coast cities, each having different transitions. Consider changing the transition based on the current number of items added to the list; for instance, a different transition for every five items.

So long, and thanks for all the transitions

After all these examples and ideas, I hope that you will consider leveraging Vue’s transition component in your own projects. Exploring the possibilities of adding transitions and animations to your apps to provide context and interest for your users. In many cases, additions such as these are rather simple to implement, almost to the point of it being a shame not to add them. Vue offers an exciting and highly useful feature, the transition component, out of the box and I can only encourage its usage.

Cheers.

The post The Power of Named Transitions in Vue appeared first on CSS-Tricks.

Advanced Tooling for Web Components

Over the course of the last four articles in this five-part series, we’ve taken a broad look at the technologies that make up the Web Components standards. First, we looked at how to create HTML templates that could be consumed at a later time. Second, we dove into creating our own custom element. After that, we encapsulated our element’s styles and selectors into the shadow DOM, so that our element is entirely self-contained.

We’ve explored how powerful these tools can be by creating our own custom modal dialog, an element that can be used in most modern application contexts regardless of the underlying framework or library. In this article, we will look at how to consume our element in the various frameworks and look at some advanced tooling to really ramp up your Web Component skills.

Article Series:

  1. An Introduction to Web Components
  2. Crafting Reusable HTML Templates
  3. Creating a Custom Element from Scratch
  4. Encapsulating Style and Structure with Shadow DOM
  5. Advanced Tooling for Web Components (This post)

Framework agnostic

Our dialog component works great in almost any framework or even without one. (Granted, if JavaScript is disabled, the whole thing is for naught.) Angular and Vue treat Web Components as first-class citizens: the frameworks have been designed with web standards in mind. React is slightly more opinionated, but not impossible to integrate.

Angular

First, let’s take a look at how Angular handles custom elements. By default, Angular will throw a template error whenever it encounters an element it doesn’t recognize (i.e. the default browser elements or any of the components defined by Angular). This behavior can be changed by including the CUSTOM_ELEMENTS_SCHEMA.

…allows an NgModule to contain the following:

  • Non-Angular elements named with dash case (-).
  • Element properties named with dash case (-). Dash case is the naming convention for custom elements.

Angular Documentation

Consuming this schema is as simple as adding it to a module:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @NgModule({ /** Omitted */ schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class MyModuleAllowsCustomElements {}

That’s it. After this, Angular will allow us to use our custom element wherever we want with the standard property and event bindings:

<one-dialog [open]="isDialogOpen" (dialog-closed)="dialogClosed($event)"> <span slot="heading">Heading text</span> <div> <p>Body copy</p> </div>
</one-dialog>

Vue

Vue’s compatibility with Web Components is even better than Angular’s as it doesn’t require any special configuration. Once an element is registered, it can be used with Vue’s default templating syntax:

<one-dialog v-bind:open="isDialogOpen" v-on:dialog-closed="dialogClosed"> <span slot="heading">Heading text</span> <div> <p>Body copy</p> </div>
</one-dialog>

One caveat with Angular and Vue, however, is their default form controls. If we wish to use something like reactive forms or [(ng-model)] in Angular or v-model in Vue on a custom element with a form control, we will need to set up that plumbing for which is beyond the scope of this article.

React

React is slightly more complicated than Angular. React’s virtual DOM effectively takes a JSX tree and renders it as a large object. So, instead of directly modifying attributes on HTML elements like Angular or Vue, React uses an object syntax to track changes that need to be made to the DOM and updates them in bulk. This works just fine in most cases. Our dialog’s open attribute is bound to its property and will respond perfectly well to changing props.

The catch comes when we start to look at the CustomEvent dispatched when our dialog closes. React implements a series of native event listeners for us with their synthetic event system. Unfortunately, that means that controls like onDialogClosed won’t actually attach event listeners to our component, so we have to find some other way.

The most obvious means of adding custom event listeners in React is by using DOM refs. In this model, we can reference our HTML node directly. The syntax is a bit verbose, but works great:

import React, { Component, createRef } from 'react'; export default class MyComponent extends Component { constructor(props) { super(props); // Create the ref this.dialog = createRef(); // Bind our method to the instance this.onDialogClosed = this.onDialogClosed.bind(this); this.state = { open: false }; } componentDidMount() { // Once the component mounds, add the event listener this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed); } componentWillUnmount() { // When the component unmounts, remove the listener this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed); } onDialogClosed(event) { /** Omitted **/ } render() { return <div> <one-dialog open={this.state.open} ref={this.dialog}> <span slot="heading">Heading text</span> <div> <p>Body copy</p> </div> </one-dialog> </div> }
}

Or, we can use stateless functional components and hooks:

import React, { useState, useEffect, useRef } from 'react'; export default function MyComponent(props) { const [ dialogOpen, setDialogOpen ] = useState(false); const oneDialog = useRef(null); const onDialogClosed = event => console.log(event); useEffect(() => { oneDialog.current.addEventListener('dialog-closed', onDialogClosed); return () => oneDialog.current.removeEventListener('dialog-closed', onDialogClosed) }); return <div> <button onClick={() => setDialogOpen(true)}>Open dialog</button> <one-dialog ref={oneDialog} open={dialogOpen}> <span slot="heading">Heading text</span> <div> <p>Body copy</p> </div> </one-dialog> </div>
}

That’s not bad, but you can see how reusing this component could quickly become cumbersome. Luckily, we can export a default React component that wraps our custom element using the same tools.

import React, { Component, createRef } from 'react';
import PropTypes from 'prop-types'; export default class OneDialog extends Component { constructor(props) { super(props); // Create the ref this.dialog = createRef(); // Bind our method to the instance this.onDialogClosed = this.onDialogClosed.bind(this); } componentDidMount() { // Once the component mounds, add the event listener this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed); } componentWillUnmount() { // When the component unmounts, remove the listener this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed); } onDialogClosed(event) { // Check to make sure the prop is present before calling it if (this.props.onDialogClosed) { this.props.onDialogClosed(event); } } render() { const { children, onDialogClosed, ...props } = this.props; return <one-dialog {...props} ref={this.dialog}> {children} </one-dialog> }
} OneDialog.propTypes = { children: children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), PropTypes.node ]).isRequired, onDialogClosed: PropTypes.func
};

…or again as a stateless, functional component:

import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types'; export default function OneDialog(props) { const { children, onDialogClosed, ...restProps } = props; const oneDialog = useRef(null); useEffect(() => { onDialogClosed ? oneDialog.current.addEventListener('dialog-closed', onDialogClosed) : null; return () => { onDialogClosed ? oneDialog.current.removeEventListener('dialog-closed', onDialogClosed) : null; }; }); return <one-dialog ref={oneDialog} {...restProps}>{children}</one-dialog>
}

Now we can use our dialog natively in React, but still keep the same API across all our applications (and still drop classes, if that’s your thing).

import React, { useState } from 'react';
import OneDialog from './OneDialog'; export default function MyComponent(props) { const [open, setOpen] = useState(false); return <div> <button onClick={() => setOpen(true)}>Open dialog</button> <OneDialog open={open} onDialogClosed={() => setOpen(false)}> <span slot="heading">Heading text</span> <div> <p>Body copy</p> </div> </OneDialog> </div>
}

Advanced tooling

There are a number of great tools for authoring your own custom elements. Searching through npm reveals a multitude of tools for creating highly-reactive custom elements (including my own pet project), but the most popular today by far is lit-html from the Polymer team and, more specifically for Web Components, LitElement.

LitElement is a custom elements base class that provides a series of APIs for doing all of the things we’ve walked through so far. It can be run in a browser without a build step, but if you enjoy using future-facing tools like decorators, there are utilities for that as well.

Before diving into how to use lit or LitElement, take a minute to familiarize yourself with tagged template literals, which are a special kind of function called on template literal strings in JavaScript. These functions take in an array of strings and a collection of interpolated values and can return anything you might want.

function tag(strings, ...values) { console.log({ strings, values }); return true;
}
const who = 'world'; tag`hello ${who}`; /** would log out { strings: ['hello ', ''], values: ['world'] } and return true **/

What LitElement gives us is live, dynamic updating of anything passed to that values array, so as a property updates, the element’s render function would be called and the resulting DOM would be re-rendered

import { LitElement, html } from 'lit-element'; class SomeComponent { static get properties() { return { now: { type: String } }; } connectedCallback() { // Be sure to call the super super.connectedCallback(); this.interval = window.setInterval(() => { this.now = Date.now(); }); } disconnectedCallback() { super.disconnectedCallback(); window.clearInterval(this.interval); } render() { return html`<h1>It is ${this.now}</h1>`; }
} customElements.define('some-component', SomeComponent);

See the Pen
LitElement now example
by Caleb Williams (@calebdwilliams)
on CodePen.

What you will notice is that we have to define any property we want LitElement to watch using the static properties getter. Using that API tells the base class to call render whenever a change is made to the component’s properties. render, in turn, will update only the nodes that need to change.

So, for our dialog example, it would look like this using LitElement:

See the Pen
Dialog example using LitElement
by Caleb Williams (@calebdwilliams)
on CodePen.

There are several variants of lit-html available, including Haunted, a React hooks-style library for Web Components that can also make use of virtual components using lit-html as a base.

At the end of the day, most of the modern Web Components tools are various flavors of what LitElement is: a base class that abstracts common logic away from our components. Among the other flavors are Stencil, SkateJS, Angular Elements and Polymer.

What’s next

Web Components standards are continuing to evolve and new features are being discussed and added to browsers on an ongoing basis. Soon, Web Component authors will have APIs for interacting with web forms at a high level (including other element internals that are beyond the scope of these introductory articles), like native HTML and CSS module imports, native template instantiation and updating controls, and many more which can be tracked on the W3C/web components issues board on GitHub.

These standards are ready to adopt into our projects today with the appropriate polyfills for legacy browsers and Edge. And while they may not replace your framework of choice, they can be used alongside them to augment you and your organization’s workflows.

The post Advanced Tooling for Web Components appeared first on CSS-Tricks.

What Hooks Mean for Vue

Not to be confused with Lifecycle Hooks, Hooks were introduced in React in v16.7.0-alpha, and a proof of concept was released for Vue a few days after. Even though it was proposed by React, it’s actually an important composition mechanism that has benefits across JavaScript framework ecosystems, so we’ll spend a little time today discussing what this means.

Mainly, Hooks offer a more explicit way to think of reusable patterns — one that avoids rewrites to the components themselves and allows disparate pieces of the stateful logic to seamlessly work together.

The initial problem

In terms of React, the problem was this: classes were the most common form of components when expressing the concept of state. Stateless functional components were also quite popular, but due to the fact that they could only really render, their use was limited to presentational tasks.

Classes in and of themselves present some issues. For example, as React became more ubiquitous, stumbling blocks for newcomers did as well. In order to understand React, one had to understand classes, too. Binding made code verbose and thus less legible, and an understanding of this in JavaScript was required. There are also some optimization stumbling blocks that classes present, discussed here.

In terms of the reuse of logic, it was common to use patterns like render props and higher-order components, but we’d find ourselves in similar “pyramid of doom” — style implementation hell where nesting became so heavily over-utilized that components could be difficult to maintain. This led me to ranting drunkenly at Dan Abramov, and nobody wants that.

Hooks address these concerns by allowing us to define a component’s stateful logic using only function calls. These function calls become more compose-able, reusable, and allows us to express composition in functions while still accessing and maintaining state. When hooks were announced in React, people were excited — you can see some of the benefits illustrated here, with regards to how they reduce code and repetition:

In terms of maintenance, simplicity is key, and Hooks provide a single, functional way of approaching shared logic with the potential for a smaller amount of code.

Why Hooks in Vue?

You may read through this and wonder what Hooks have to offer in Vue. It seems like a problem that doesn’t need solving. After all, Vue doesn’t predominantly use classes. Vue offers stateless functional components (should you need them), but why would we need to carry state in a functional component? We have mixins for composition where we can reuse the same logic for multiple components. Problem solved.

I thought the same thing, but after talking to Evan You, he pointed out a major use case I missed: mixins can’t consume and use state from one to another, but Hooks can. This means that if we need chain encapsulated logic, it’s now possible with Hooks.

Hooks achieve what mixins do, but avoid two main problems that come with mixins:

  • They allows us to pass state from one to the other.
  • They make it explicit where logic is coming from.

If we’re using more than one mixin, it’s not clear which property was provided by which mixin. With Hooks, the return value of the function documents the value being consumed.

So, how does that work in Vue? We mentioned before that, when working with Hooks, logic is expressed in function calls that become reusable. In Vue, this means that we can group a data call, a method call, or a computed call into another custom function, and make them freely compose-able. Data, methods, and computed now become available in functional components.

Example

Let’s go over a really simple hook so that we can understand the building blocks before we move on to an example of composition in Hooks.

useWat?

OK, here’s were we have, what you might call, a crossover event between React and Vue. The use prefix is a React convention, so if you look up Hooks in React, you’ll find things like useState, useEffect, etc. More info here.

In Evan’s live demo, you can see where he’s accessing useState and useEffect for a render function.

If you’re not familiar with render functions in Vue, it might be helpful to take a peek at that.

But when we’re working with Vue-style Hooks, we’ll have — you guessed it — things like: useData, useComputed, etc.

So, in order for us look at how we’d use Hooks in Vue, I created a sample app for us to explore.

In the src/hooks folder, I’ve created a hook that prevents scrolling on a useMounted hook and reenables it on useDestroyed. This helps me pause the page when we’re opening a dialog to view content, and allows scrolling again when we’re done viewing the dialog. This is good functionality to abstract because it would probably be useful several times throughout an application.

import { useDestroyed, useMounted } from "vue-hooks"; export function preventscroll() { const preventDefault = (e) => { e = e || window.event; if (e.preventDefault) e.preventDefault(); e.returnValue = false; } // keycodes for left, up, right, down const keys = { 37: 1, 38: 1, 39: 1, 40: 1 }; const preventDefaultForScrollKeys = (e) => { if (keys[e.keyCode]) { preventDefault(e); return false; } } useMounted(() => { if (window.addEventListener) // older FF window.addEventListener('DOMMouseScroll', preventDefault, false); window.onwheel = preventDefault; // modern standard window.onmousewheel = document.onmousewheel = preventDefault; // older browsers, IE window.touchmove = preventDefault; // mobile window.touchstart = preventDefault; // mobile document.onkeydown = preventDefaultForScrollKeys; }); useDestroyed(() => { if (window.removeEventListener) window.removeEventListener('DOMMouseScroll', preventDefault, false); //firefox window.addEventListener('DOMMouseScroll', (e) => { e.stopPropagation(); }, true); window.onmousewheel = document.onmousewheel = null; window.onwheel = null; window.touchmove = null; window.touchstart = null; document.onkeydown = null; });
} 

And then we can call it in a Vue component like this, in AppDetails.vue:

<script>
import { preventscroll } from "./../hooks/preventscroll.js";
... export default { ... hooks() { preventscroll(); }
}
</script>

We’re using it in that component, but now we can use the same functionality throughout the application!

Two Hooks, understanding each other

We mentioned before that one of the primary differences between hooks and mixins is that hooks can actually pass values from one to another. Let’s look at that with a simple, albeit slightly contrived, example.

Let’s say in our application we need to do calculations in one hook that will be reused elsewhere, and something else that needs to use that calculation. In our example, we have a hook that takes the window width and passes it into an animation to let it know to only fire when we’re on larger screens.

In the first hook:

import { useData, useMounted } from 'vue-hooks'; export function windowwidth() { const data = useData({ width: 0 }) useMounted(() => { data.width = window.innerWidth }) // this is something we can consume with the other hook return { data }
}

Then, in the second we use this to create a conditional that fires the animation logic:

// the data comes from the other hook
export function logolettering(data) { useMounted(function () { // this is the width that we stored in data from the previous hook if (data.data.width > 1200) { // we can use refs if they are called in the useMounted hook const logoname = this.$refs.logoname; Splitting({ target: logoname, by: "chars" }); TweenMax.staggerFromTo(".char", 5, { opacity: 0, transformOrigin: "50% 50% -30px", cycle: { color: ["red", "purple", "teal"], rotationY(i) { return i * 50 } } }, ...

Then, in the component itself, we’ll pass one into the other:

<script>
import { logolettering } from "./../hooks/logolettering.js";
import { windowwidth } from "./../hooks/windowwidth.js"; export default { hooks() { logolettering(windowwidth()); }
};
</script>

Now we can compose logic with Hooks throughout our application! Again, this is a contrived example for the purposes of demonstration, but you can see how useful this might be for large scale applications to keep things in smaller, reusable functions.

Future plans

Vue Hooks are already available to use today with Vue 2.x, but are still experimental. We’re planning on integrating Hooks into Vue 3, but will likely deviate from React’s API in our own implementation. We find React Hooks to be very inspiring and are thinking about how to introduce its benefits to Vue developers. We want to do it in a way that complements Vue’s idiomatic usage, so there’s still a lot of experimentation to do.

You can get started by checking out the repo here. Hooks will likely become a replacement for mixins, so although the feature still in its early stages, it’s probably a concept that would be beneficial to explore in the meantime.

(Sincere thanks to Evan You and Dan Abramov for proofing this article.)

The post What Hooks Mean for Vue appeared first on CSS-Tricks.

Storing and Using the Last Known Route in Vue

There are situations where keeping a reference to the last route a user visited can come in handy. For example, let’s say we’re working with a multi-step form and the user proceeds from one step to the next. It would be ideal to have the route of that previous step in hand so we know where the user left off, in the event that they navigate away and come back later to complete the form later.

We’re going to cover how to store the last known route and then fetch it when we need it. We’ll be working in Vue in this example and put vue-router to use for routing and localStorage to keep the information about last visited route.

Here’s an example of what we’ll be working with:

First, let’s outline the route structure

Our example has a grand total of three routes:

  • /home
  • /hello
  • /goodbye

Each route needs to be assigned a name property, so let’s add that to our router.js file:

// router.js import Vue from "vue";
import Router from "vue-router";
import Hello from "@/components/Hello";
import Goodbye from "@/components/Goodbye"; import { HELLO_URL, GOODBYE_URL
} from "@/consts"; Vue.use(Router); const router = new Router({ mode: "history", routes: [ { path: "/", name: "home" }, { path: HELLO_URL, name: "hello", component: Hello }, { path: GOODBYE_URL, name: "goodbye", component: Goodbye } ]
}); export default router;

Next, let’s go over the requirements

We know the first requirement is to store the last visited route in localStorage. And, secondly, we need to be able to retrieve it. But what conditions should the route be fetched and applied? That gives us two additional requirements.

  • the user enters the main route (/home), navigates away from it, then wants to return to it.
  • the user has been inactive for a specific time period, the session expires, and we want to return the user to the last screen they were on after restarting the session.

These four requirements are what we need to meet in order to proceed with the redirection.

Now let’s jump into the code.

Requirement 1: Save the last route name in localStorage

We want to keep the reference to our last visited route in localStorage. For example, if a user is at /checkout and then leaves the site, we want to save that so the purchase can be completed later.

To do that, we want to save the route name when the user enters any new route. We’ll use a navigation guard called afterEach that’s fired each time the route transition is finished. It provides a to object which is the target Route Object. In that hook, we can extract the name of that route and save it in localStorage using a setItem method.

// router.js const router = new Router( ... ); router.afterEach(to => { localStorage.setItem(LS_ROUTE_KEY, to.name);
}); ...
export default router;

Requirement 2: Fetch the last route name from localStorage and redirect

Now that the name of the last route is saved, we need to be able to fetch it and trigger a redirect to it when it’s needed. We want to check if we should redirect before we enter a new route, so we will use another navigation guard called beforeEach. This guard receives three arguments:

  • to: the target route object
  • from: the current route navigated from
  • next: the function that must be called in the guard to resolve the hook

In that guard, we read the name of the last visited route by using a localStorage.getItem() method. Then, we determine if the user should be redirected. At this point, we check that the target route (to) is our main route (/home) and if we do indeed have a last route in localStorage.

If those conditions are met, we fire the next method that contains the name of the last visited route. That, in turn, will trigger a redirect to that route.

If any condition fails, then we’ll fire next without any arguments. That will move the user on to the next hook in the pipeline and proceed with ordinary routing without redirection.

// router.js const router = new Router( ... ); router.beforeEach((to, from, next) => { const lastRouteName = localStorage.getItem(LS_ROUTE_KEY); const shouldRedirect = Boolean( to.name === "home" && lastRouteName ); if (shouldRedirect) next({ name: lastRouteName }); else next();
}); ...
export default router;

That covers two out of four requirements! Let’s proceed with requirement number three.

Requirement 3: The first visit condition

Now, we need to check if the user is visiting the main route for the first time (coming from a different source) or is navigating there from another route within the application. We can do that by adding a flag that is set to true when the Router is created and set it to false after first transition is finished.

// router.js const router = new Router( ... ); let isFirstTransition = true; router.beforeEach((to, from, next) => { const lastRouteName = localStorage.getItem(LS_ROUTE_KEY); const shouldRedirect = Boolean( to.name === "home" && && lastRouteName && isFirstTransition ); if (shouldRedirect) next({ name: lastRouteName }); else next(); isFirstTransition = false;
}); ...
export default router;

OK, there is one more requirement we need to meet: we want to redirect the user to the last known route if the user has been inactive for longer that a specific period of time.

Requirement 4: The activity time condition

Again, we will use localStorage to keep the information about user’s last visited route.

In the beforeEach guard, we will get the route from localStorage and check if the time passed from that moment is within our threshold (defined by hasBeenActiveRecently). Then, in our shouldRedirect, we’ll determine whether a route redirect should happen or not.

We also need to save that information, which we will do in the afterEach guard.

// router.js const router = new Router( ... ); let isFirstTransition = true; router.beforeEach((to, from, next) => { const lastRouteName = localStorage.getItem(LS_ROUTE_KEY); const lastActivityAt = localStorage.getItem(LS_LAST_ACTIVITY_AT_KEY); const hasBeenActiveRecently = Boolean( lastActivityAt && Date.now() - Number(lastActivityAt) < MAX_TIME_TO_RETURN ); const shouldRedirect = Boolean( to.name === "home" && && lastRouteName && isFirstTransition && hasBeenActiveRecently ); if (shouldRedirect) next({ name: lastRouteName }); else next(); isFirstTransition = false;
}); router.afterEach(to => { localStorage.setItem(LS_ROUTE_KEY, to.name); localStorage.setItem(LS_LAST_ACTIVITY_AT_KEY, Date.now());
}); ...
export default router;

We met the requirements!

That’s it! We covered all four of requirements, namely:

  • We store the last visited route in localStorage
  • We have a method to retrieve the last visited route from localStorage
  • We redirect a user back to the main route if they’re coming into the application on an initial visit
  • We provide the user with a redirect to the last known route within a certain time period

Of course, we can extend this further by adding more complexity to the app and new conditions to the shouldRedirect variable, but this gives us more than we need to have an understanding of how to keep the last visited route persistent and retrieve it when it’s needed.

The post Storing and Using the Last Known Route in Vue appeared first on CSS-Tricks.

Swipeable card stack using Vue.js and interact.js

I recently had an opportunity to work on a fantastic research and development project at Netguru. The goal of project (codename: “Wordguru”) was to create a card game that anyone can play with their friends. You can see the outcome here.

One element of the development process was to create an interactive card stack. The card stack had a set of requirements, including:

  • It should contain a few cards from the collection.
  • The first card should be interactive.
  • The user should be able to swipe the card in different directions that indicate an intent to accept, reject or skip the card.

This article will explain how to create that and make it interactive using Vue.js and interact.js. I created an example for you to refer to as we go through the process of creating a component that is in charge of displaying that card stack and a second component that is responsible for rendering a single card and managing user interactions in it.

View Demo

Step 1: Create the GameCard component in Vue

Let’s start by creating a component that will show a card, but without any interactions just yet. We’ll call this file GameCard.vue and, in the component template, we’ll render a card wrapper and the keyword for a specific card. This is the file we’ll be working in throughout this post.

// GameCard.vue
<template> <div class="card" :class="{ isCurrent: isCurrent }" > <h3 class="cardTitle">{{ card.keyword }}</h3> </div>
</template>

In the script section of the component, we receive the prop card that contains our card content as well as an isCurrent prop that gives the card a distinct look when needed.

export default { props: { card: { type: Object, required: true }, isCurrent: { type: Boolean, required: true } }
},

Step 2: Create the GameCardStack component in Vue

Now that we have a single card, let’s create our card stack.

This component will receive an array of cards and render the GameCard for each card. It’s also going to mark the first card as the current card in the stack so a special styling is applied to it.

// GameCardsStack.vue
<template> <div class="cards"> <GameCard v-for="(card, index) in cards" :key="card" :card="card" :is-current="index === 0" /> </div>
</template> <script> import GameCard from "@/components/GameCard"; export default { components: { GameCard }, props: { cards: { type: Array, required: true } } };
</script>

Here’s what we’re looking at so far, using the styles pulled from the demo:

At this point, our card looks complete, but isn’t very interactive. Let’s fix that in the next step!

Step 3: Add interactivity to GameCard component

All our interactivity logic will live in the GameCard component. Let’s start by allowing the user to drag the card. We will use interact.js to deal with dragging.

We’ll set the interactPosition initial values to 0 in the script section. These are the values that indicate a card’s order in the stack when it’s moved from its original position.

<script>
import interact from "interact.js"; data() { return { interactPosition: { x: 0, y: 0 }, };
},
// ...
</script>

Next, we create a computed property that’s responsible for creating a transform value that’s applied to our card element.

// ...
computed: { transformString() { const { x, y } = this.interactPosition; return `translate3D(${x}px, ${y}px, 0)`; }
},
// ...

In the mounted lifecycle hook, we make use of the interact.js and its draggable method. That method allows us to fire a custom function each time the element is dragged (onmove). It also exposes an event object that carries information about how far the element is dragged from its original position. Each time user drags the card, we calculate a new position of the card and set it on the interactPosition property. That triggers our transformString computed property and sets new value of transform on our card.

We use the interact onend hook that allows us to listen when the user releases the mouse and finishes the drag. At this point, we will reset the position of our card and bring it back to its original position: { x: 0, y: 0 }.

We also need to make sure to remove the card element from the Interactable object before it gets destroyed. We do that in the beforeDestroy lifecycle hook by using interact(target).unset(). That removes all event listeners and makes interact.js completely forget about the target.

// ...
mounted() { const element = this.$refs.interactElement; interact(element).draggable({ onmove: event => { const x = this.interactPosition.x + event.dx; const y = this.interactPosition.y + event.dy; this.interactSetPosition({ x, y }); }, onend: () => { this.resetCardPosition(); } });
},
// ...
beforeDestroy() { interact(this.$refs.interactElement).unset();
},
// ...
methods: { interactSetPosition(coordinates) { const { x = 0, y = 0 } = coordinates; this.interactPosition = {x, y }; }, resetCardPosition() { this.interactSetPosition({ x: 0, y: 0 }); },
},
// ...

We need to add one thing in our template to make this work. As our transformString computed property returns a string, we need to apply it to the card component. We do that by binding to the :style attribute and then passing the string to the transform property.

<template> <div class="card" :class="{ isCurrent: isCurrent }" :style="{ transform: transformString }" > <h3 class="cardTitle">{{ card.keyword }}</h3> </div>
</template>

With that done, we have created interaction with our card — we can drag it around!

You may have noticed that the behavior isn’t very natural, specifically when we drag the card and release it. The card immediately returns to its original position, but it would be more natural if the card would go back to initial position with animation to smooth the transition.

That’s where transition comes into play! But adding it to our card introduces another issue: there’s a lag in the card following as it follows the cursor because transition is applied to the element at all times. We only want it applied when the drag ends. We can do that by binding one more class (isAnimating) to the component.

<template> <div class="card" :class="{ isAnimating: isInteractAnimating, isCurrent: isCurrent }" > <h3 class="cardTitle">{{ card.keyword }}</h3> </div>
</template>

We can add and remove the animation class by changing the isInteractAnimating property.

The animation effect should be applied initially and we do that by setting our property in data.

In the mounted hook where we initialize interact.js, we use one more interact hook (onstart) and change the value of isInteractAnimating to false so that the animation is disabled when the during the drag.

We’ll enable the animation again in the onend hook, and that will make our card animate smoothly to its original position when we release it from the drag.

We also need to update transformString computed property and add a guard to recalculate and return a string only when we are dragging the card.

data() { return { // ... isInteractAnimating: true, // ... };
}, computed: { transformString() { if (!this.isInteractAnimating) { const { x, y } = this.interactPosition; return `translate3D(${x}px, ${y}px, 0)`; } return null; }
}, mounted() { const element = this.$refs.interactElement; interact(element).draggable({ onstart: () => { this.isInteractAnimating = false; }, // ... onend: () => { this.isInteractAnimating = true; }, });
},

Now things are starting to look nice!

Our card stack is ready for second set of interactions. We can drag the card around, but nothing is actually happening — the card is always coming back to its original place, but there is no way to get to the second card.

This will change when we add logic that allows the user to accept and rejecting cards.

Step 4: Detect when the card is accepted, rejected, or skipped

The card has three types of interactions:

  • Accept card (on swipe right)
  • Reject card (on swipe left)
  • Skip card (on swipe down)

We need to find a place where we can detect if the card was dragged from its initial position. We also want to be sure that this check will happen only when we finish dragging the card so the interactions do not conflict with the animation we just finished.

We used that place earlier smooth the transition during animation — it’s the onend hook provided by the interact.draggable method.

Let’s jump into the code.

First, we need to store our threshold values. Those values are the distances as the card is dragged from its original position and allows us to determine if the card should be accepted, rejected, or skipped. We use X axis for right (accept) and left (reject), then use the Y axis for downward movement (skip).

We also set coordinates where we want to place a card after it gets accepted, rejected or skipped (coordinates out of user’s sight).

Since those values will not change, we will keep them in the static property of our component, which can be accessed with this.$options.static.interactYThreshold.

export default { static: { interactYThreshold: 150, interactXThreshold: 100 },

We need to check if any of our thresholds were met in our onend hook and then fire the appropriate method that happened. If no threshold is met, then we reset the card to its initial position.

mounted() { const element = this.$refs.interactElement; interact(element).draggable({ onstart: () => {...}, onmove: () => {...}, onend: () => { const { x, y } = this.interactPosition; const { interactXThreshold, interactYThreshold } = this.$options.static; this.isInteractAnimating = true; if (x > interactXThreshold) this.playCard(ACCEPT_CARD); else if (x < -interactXThreshold) this.playCard(REJECT_CARD); else if (y > interactYThreshold) this.playCard(SKIP_CARD); else this.resetCardPosition(); } });
}

OK, now we need to create a playCard method that’s responsible for handling those interactive actions.

Step 5: Establish the logic to accept, reject, and skip cards

We will create a method that accepts a parameter telling us the user’s intended action. Depending on that parameter, we will set the final position of the current card and emit the accept, reject, or skip event. Let’s go step by step.

First, our playCard method will remove the card element from the Interactable object so that it stops tracking drag events. We do that by using interact(target).unset().
Secondly, we set the final position of the active card depending on the user’s intention. That new position allows us to animate the card and remove it from the user’s view.

Next, we emit an event up to the parent component so we can deal with our cards (e.g. change the current card, load more cards, shuffle the cards, etc.). We want to follow the DDAU principle that states a component should refrain from mutating data it doesn’t own. Since our cards are passed down to our component, it should emit an event up to the place from where those cards come.

Lastly, we hide the card that was just played and add a timeout that allow the card to animate out of view.

methods: { playCard(interaction) { const { interactOutOfSightXCoordinate, interactOutOfSightYCoordinate, } = this.$options.static; this.interactUnsetElement(); switch (interaction) { case ACCEPT_CARD: this.interactSetPosition({ x: interactOutOfSightXCoordinate, }); this.$emit(ACCEPT_CARD); break; case REJECT_CARD: this.interactSetPosition({ x: -interactOutOfSightXCoordinate, }); this.$emit(REJECT_CARD); break; case SKIP_CARD: this.interactSetPosition({ y: interactOutOfSightYCoordinate }); this.$emit(SKIP_CARD); break; } this.hideCard(); }, hideCard() { setTimeout(() => { this.isShowing = false; this.$emit("hideCard", this.card); }, 300); }, interactUnsetElement() { interact(this.$refs.interactElement).unset(); this.interactDragged = true; },
}

And, there we go!

Summary

Let’s recap what we just accomplished:

  • First we created a component for a single card.
  • Next we created another component that renders the cards in a stack.
  • Thirdly, we implemented interact.js to allow interactive dragging.
  • Then we detected when the user wants takes an action with the current card.
  • Finally, we established the to handle those actions.

Phew, we covered a lot! Hopefully this gives you both a new trick in your toolbox as well as a hands-on use case for Vue. And, if you’ve ever had to build something similar, please share in the comments because it would be neat to compare notes.

The post Swipeable card stack using Vue.js and interact.js appeared first on CSS-Tricks.

Getting Started with Vue Plugins

In the last months, I’ve learned a lot about Vue. From building SEO-friendly SPAs to crafting killer blogs or playing with transitions and animations, I’ve experimented with the framework thoroughly.

But there’s been a missing piece throughout my learning: plugins.

Most folks working with Vue have either comes to rely on plugins as part of their workflow or will certainly cross paths with plugins somewhere down the road. Whatever the case, they’re a great way to leverage existing code without having to constantly write from scratch.

Many of you have likely used jQuery and are accustomed to using (or making!) plugins to create anything from carousels and modals to responsive videos and type. We’re basically talking about the same thing here with Vue plugins.

So, you want to make one? I’m going to assume you’re nodding your head so we can get our hands dirty together with a step-by-step guide for writing a custom Vue plugin.

First, a little context…

Plugins aren’t something specific to Vue and — just like jQuery — you’ll find that there’s a wide variety of plugins that do many different things. By definition, they indicate that an interface is provided to allow for extensibility.

Brass tacks: they’re a way to plug global features into an app and extend them for your use.

The Vue documentation covers plugins in great detail and provides an excellent list of broad categories that plugins generally fall into:

  1. Add some global methods or properties.
  2. Add one or more global assets: directives/filters/transitions etc.
  3. Add some component options by global mixin.
  4. Add some Vue instance methods by attaching them to Vue.prototype.
  5. A library that provides an API of its own, while at the same time injecting some combination of the above.

OK, OK. Enough prelude. Let’s write some code!

What we’re making

At Spektrum, Snipcart’s mother agency, our designs go through an approval process, as I’m sure is typical at most other shops and companies. We allow a client to comment and make suggestions on designs as they review them so that, ultimately, we get the green light to proceed and build the thing.

We generally use InVision for all this. The commenting system is a core component in InVision. It lets people click on any portion of the design and leave a comment for collaborators directly where that feedback makes sense. It’s pretty rad.

As cool as InVision is, I think we can do the same thing ourselves with a little Vue magic and come out with a plugin that anyone can use as well.

The good news here is they’re not that intimidating. A basic knowledge of Vue is all you need to start fiddling with plugins right away.

Step 1. Prepare the codebase

A Vue plugin should contain an install method that takes two parameters:

  1. The global Vue object
  2. An object incorporating user-defined options

Firing up a Vue project is super simple, thanks to Vue CLI 3. Once you have that installed, run the following in your command line:

$ vue create vue-comments-overlay
# Answer the few questions
$ cd vue-comments-overlay
$ npm run serve

This gives us the classic “Hello World” start we need to crank out a test app that will put our plugin to use.

Step 2. Create the plugin directory

Our plugin has to live somewhere in the project, so let’s create a directory where we can cram all our work, then navigate our command line to the new directory:

$ mkdir src/plugins
$ mkdir src/plugins/CommentsOverlay
$ cd src/plugins/CommentsOverlay

Step 3: Hook up the basic wiring

A Vue plugin is basically an object with an install function that gets executed whenever the application using it includes it with Vue.use().

The install function receives the global Vue object as a parameter and an options object:

// src/plugins/CommentsOverlay/index.js
// export default { install(vue, opts){ console.log('Installing the CommentsOverlay plugin!') // Fun will happen here }
}

Now, let’s plug this in our “Hello World” test app:

// src/main.js
import Vue from 'vue'
import App from './App.vue'
import CommentsOverlay from './plugins/CommentsOverlay' // import the plugin Vue.use(CommentsOverlay) // put the plugin to use! Vue.config.productionTip = false new Vue({ render: createElement => createElement(App)}).$mount('#app')

Step 4: Provide support for options

We want the plugin to be configurable. This will allow anyone using it in their own app to tweak things up. It also makes our plugin more versatile.

We’ll make options the second argument of the install function. Let’s create the default options that will represent the base behavior of the plugin, i.e. how it operates when no custom option is specified:

// src/plugins/CommentsOverlay/index.js const optionsDefaults = { // Retrieves the current logged in user that is posting a comment commenterSelector() { return { id: null, fullName: 'Anonymous', initials: '--', email: null } }, data: { // Hash object of all elements that can be commented on targets: {}, onCreate(created) { this.targets[created.targetId].comments.push(created) }, onEdit(editted) { // This is obviously not necessary // It's there to illustrate what could be done in the callback of a remote call let comments = this.targets[editted.targetId].comments comments.splice(comments.indexOf(editted), 1, editted); }, onRemove(removed) { let comments = this.targets[removed.targetId].comments comments.splice(comments.indexOf(removed), 1); } }
}

Then, we can merge the options that get passed into the install function on top of these defaults:

// src/plugins/CommentsOverlay/index.js export default { install(vue, opts){ // Merge options argument into options defaults const options = { ...optionsDefaults, ...opts } // ... }
}

Step 5: Create an instance for the commenting layer

One thing you want to avoid with this plugin is having its DOM and styles interfere with the app it is installed on. To minimize the chances of this happening, one way to go is making the plugin live in another root Vue instance, outside of the main app’s component tree.

Add the following to the install function:

// src/plugins/CommentsOverlay/index.js export default { install(vue, opts){ ... // Create plugin's root Vue instance const root = new Vue({ data: { targets: options.data.targets }, render: createElement => createElement(CommentsRootContainer) }) // Mount root Vue instance on new div element added to body root.$mount(document.body.appendChild(document.createElement('div'))) // Register data mutation handlers on root instance root.$on('create', options.data.onCreate) root.$on('edit', options.data.onEdit) root.$on('remove', options.data.onRemove) // Make the root instance available in all components vue.prototype.$commentsOverlay = root ... }
}

Essential bits in the snippet above:

  1. The app lives in a new div at the end of the body.
  2. The event handlers defined in the options object are hooked to the matching events on the root instance. This will make sense by the end of the tutorial, promise.
  3. The $commentsOverlay property added to Vue’s prototype exposes the root instance to all Vue components in the application.

Step 6: Make a custom directive

Finally, we need a way for apps using the plugin to tell it which element will have the comments functionality enabled. This is a case for a custom Vue directive. Since plugins have access to the global Vue object, they can define new directives.

Ours will be named comments-enabled, and it goes like this:

// src/plugins/CommentsOverlay/index.js export default { install(vue, opts){ ... // Register custom directive tha enables commenting on any element vue.directive('comments-enabled', { bind(el, binding) { // Add this target entry in root instance's data root.$set( root.targets, binding.value, { id: binding.value, comments: [], getRect: () => el.getBoundingClientRect(), }); el.addEventListener('click', (evt) => { root.$emit(`commentTargetClicked__${binding.value}`, { id: uuid(), commenter: options.commenterSelector(), clientX: evt.clientX, clientY: evt.clientY }) }) } }) }
}

The directive does two things:

  1. It adds its target to the root instance’s data. The key defined for it is binding.value. It enables consumers to specify their own ID for target elements, like so : <img v-comments-enabled="imgFromDb.id" src="imgFromDb.src" />.
  2. It registers a click event handler on the target element that, in turn, emits an event on the root instance for this particular target. We’ll get back to how to handle it later on.

The install function is now complete! Now we can move on to the commenting functionality and components to render.

Step 7: Establish a “Comments Root Container” component

We’re going to create a CommentsRootContainer and use it as the root component of the plugin’s UI. Let’s take a look at it:

<!-- src/plugins/CommentsOverlay/CommentsRootContainer.vue --> <template> <div> <comments-overlay v-for="target in targets" :target="target" :key="target.id"> </comments-overlay> </div>
</template> <script>
import CommentsOverlay from "./CommentsOverlay"; export default { components: { CommentsOverlay }, computed: { targets() { return this.$root.targets; } }
};
</script>

What’s this doing? We’ve basically created a wrapper that’s holding another component we’ve yet to make: CommentsOverlay. You can see where that component is being imported in the script and the values that are being requested inside the wrapper template (target and target.id). Note how the target computed property is derived from the root component’s data.

Now, the overlay component is where all the magic happens. Let’s get to it!

Step 8: Make magic with a “Comments Overlay” component

OK, I’m about to throw a lot of code at you, but we’ll be sure to walk through it:

<!-- src/plugins/CommentsOverlay/CommentsRootContainer.vue --> <template> <div class="comments-overlay"> <div class="comments-overlay__container" v-for="comment in target.comments" :key="comment.id" :style="getCommentPostition(comment)"> <button class="comments-overlay__indicator" v-if="editing != comment" @click="onIndicatorClick(comment)"> {{ comment.commenter.initials }} </button> <div v-else class="comments-overlay__form"> <p>{{ getCommentMetaString(comment) }}</p> <textarea ref="text" v-model="text" /> <button @click="edit" :disabled="!text">Save</button> <button @click="cancel">Cancel</button> <button @click="remove">Remove</button> </div> </div> <div class="comments-overlay__form" v-if="this.creating" :style="getCommentPostition(this.creating)"> <textarea ref="text" v-model="text" /> <button @click="create" :disabled="!text">Save</button> <button @click="cancel">Cancel</button> </div> </div>
</template> <script>
export default { props: ['target'], data() { return { text: null, editing: null, creating: null }; }, methods: { onTargetClick(payload) { this._resetState(); const rect = this.target.getRect(); this.creating = { id: payload.id, targetId: this.target.id, commenter: payload.commenter, ratioX: (payload.clientX - rect.left) / rect.width, ratioY: (payload.clientY - rect.top) / rect.height }; }, onIndicatorClick(comment) { this._resetState(); this.text = comment.text; this.editing = comment; }, getCommentPostition(comment) { const rect = this.target.getRect(); const x = comment.ratioX <em> rect.width + rect.left; const y = comment.ratioY <em> rect.height + rect.top; return { left: `${x}px`>, top: `${y}px` }; }, getCommentMetaString(comment) { return `${ comment.commenter.fullName } - ${comment.timestamp.getMonth()}/${comment.timestamp.getDate()}/${comment.timestamp.getFullYear()}`; }, edit() { this.editing.text = this.text; this.editing.timestamp = new Date(); this._emit("edit", this.editing); this._resetState(); }, create() { this.creating.text = this.text; this.creating.timestamp = new Date(); this._emit("create", this.creating); this._resetState(); }, cancel() { this._resetState(); }, remove() { this._emit("remove", this.editing); this._resetState(); }, _emit(evt, data) { this.$root.$emit(evt, data); }, _resetState() { this.text = null; this.editing = null; this.creating = null; } }, mounted() { this.$root.$on(`commentTargetClicked__${this.target.id}`, this.onTargetClick ); }, beforeDestroy() { this.$root.$off(`commentTargetClicked__${this.target.id}`, this.onTargetClick ); }
};
</script>

I know, I know. A little daunting. But it’s basically only doing a few key things.

First off, the entire first part contained in the <template> tag establishes the markup for a comment popover that will display on the screen with a form to submit a comment. In other words, this is the HTML markup that renders our comments.

Next up, we write the scripts that power the way our comments behave. The component receives the full target object as a prop. This is where the comments array and the positioning info is stored.

Then, the magic. We’ve defined several methods that do important stuff when triggered:

  • Listens for a click
  • Renders a comment box and positions it where the click was executed
  • Captures user-submitted data, including the user’s name and the comment
  • Provides affordances to create, edit, remove, and cancel a comment

Lastly, the handler for the commentTargetClicked events we saw earlier is managed within the mounted and beforeDestroy hooks.

It’s worth noting that the root instance is used as the event bus. Even if this approach is often discouraged, I judged it reasonable in this context since the components aren’t publicly exposed and can be seen as a monolithic unit.

Aaaaaaand, we’re all set! After a bit of styling (I won’t expand on my dubious CSS skills), our plugin is ready to take user comments on target elements!

Demo time!

Live Demo

GitHub Repo

Getting acquainted with more Vue plugins

We spent the bulk of this post creating a Vue plugin but I want to bring this full circle to the reason we use plugins at all. I’ve compiled a short list of extremely popular Vue plugins to showcase all the wonderful things you gain access to when putting plugins to use.

  • Vue-router – If you’re building single-page applications, you’ll without a doubt need Vue-router. As the official router for Vue, it integrates deeply with its core to accomplish tasks like mapping components and nesting routes.
  • Vuex – Serving as a centralized store for all the components in an application, Vuex is a no-brainer if you wish to build large apps with high maintenance.
  • Vee-validate – When building typical line of business applications, form validation can quickly become unmanageable if not handled with care. Vee-validate takes care of it all in a graceful manner. It uses directives, and it’s built with localization in mind.

I’ll limit myself to these plugins, but know that there are many others waiting to help Vue developers, like yourself!

And, hey! If you can’t find a plugin that serves your exact needs, you now have some hands-on experience crafting a custom plugin. 😀

The post Getting Started with Vue Plugins appeared first on CSS-Tricks.

Vue + TypeScript: A Match Made in Your Code Editor

Vue is so hot right now and I’ve been thinking of doing a serious project with it since quite a while, so when the opportunity popped up, I hopped in. But there was a little problem — one of the requirements of the project was to write it in TypeScript. At first, I was super stressed about how I was going to ever get started on this combo, but vue-cli made it so easy.

I’d be lying if I said this ride was super smooth. There were frustrations, hours of staring at the screen and some fistbumps with my table but after working with Vue + TypeScript for over a month now, I can say it was worth it — and if I had to code another app with Vue, I wouldn’t do it without TypeScript.

Prerequisites

This article is about pairing Vue and TypeScript and assumes some basic knowledge of both. If you haven’t had a chance to play with them yet and are curious, Vue has a great guide, and the TypeScript docs are a great place to start.

We need to have vue-cli installed globally, so we can quickly spin up Vue project. Install vue-cli by running the following command in your terminal:

npm install -g @vue/cli

Once we have that installed, we’re good to go. If you don’t have TypeScript installed, we don’t need to do that beforehand, as vue-cli takes care of that when you start a new project and choose TypeScript there.

Getting Started

Now that we have vue-cli installed, all we need to do to get a project with Vue + TypeScript started is to run vue create. While creating a new project, choose TypeScript and we’re good to go.

vue create <app-name>

Here’s the result once our project spins up:

vue-cli also provides us the ability to choose Babel along with TypeScript for polyfills, CSS preprocessors, linter, unit testing libraries (I picked Jest, go Jest!) along with other config. You can even save your choices in a preset, to use it later, for another project.

Here’s a rundown of the handy questions you’ll get asked to configure the project:

One thing I’d like to mention is that vue-cli 3.0 comes with a user interface which makes it even more easy to create a new project. Run vue ui in terminal and vue-cli opens a vue-property-decorator package exposes Vue properties and makes them available to use as decorators. In my application, I ended up using only @Component, @Prop, @Watch but there are others such as @Emit, @Inject and @Model, that make your code much more verbose when used extensively.

Vuex

Vuex has typings…nuff said! Vuex supports TypeScript to boot and, at first, I didn’t even know it. I started to look for proper ways to combine Vuex with TypeScript and stumbled upon Alex Jover Morales’ egghead.io course on Vue.js State Management with Vuex and TypeScript. It helped me understand the correct way of managing Vuex state when using TypeScript.

For example:

// actions.ts
import { ActionTree } from 'vuex';
import { RootState, ModuleState } from '@/types'; const actions: ActionTree<ModuleState, RootState> = { // all your actions go here
};

Vuex-class

This is yet another thing that I didn’t know existed when I first started but know wish I had found it sooner. I was creating getters for almost everything, but that didn’t feel right. I started looking for better ways to do this and found an interesting article by Francesco Vitullo which cleared up a few things for me. That’s where I found out about vuex-class which provides decorators for all vuex mappers.

So, now instead of writing a new getter for simply accessing a property in state, I could do this:

import { State,
} from 'vuex-class' @Component
export class MyComp extends Vue { @State(state => state.bar) stateBar
}

Development Experience With VS Code

With TypeScript, the coding experience on VS Code is so much better. There is no going back and forth to check what mutation types I declared in mutation-types.ts because VS Code can recognize them and suggest correct ones as I type.

The same goes for modifying state in mutations — with TypeScript, the editor knew what my state structure looks like and suggests correct properties.

If you’re using VS Code, I strongly recommend using the Vetur plugin because it provides Vue tooling and comes with other bells and whistles like syntax highlighting (this works great with Vue single file components) and linting right out of the box.

Final Thoughts

Just like everything else in the JavaScript ecosystem, Vue + TypeScript still has a long way to go. For example, I could not use vuelidate because it doesn’t have typings. But thankfully vee-validate provided a workaround, so I didn’t have to go down the difficult road of writing those myself.

In conclusion, I found the development experience to be much smoother and VS Code is a totally different beast when you work with TypeScript. I don’t really need to sing the praises of Vue — it is very easy to pick up and start building and saves days trying to wrap your head around the inner workings of a framework.

The post Vue + TypeScript: A Match Made in Your Code Editor appeared first on CSS-Tricks.

Building a RSS Viewer With Vue: Part 2

Welcome to Part 2 of this mini-series on building a RSS viewer with Vue. In the last post, I walked through how I built my demo using Vue.js and Vuetify on the front end and Webtask on the back end. When I built that initial version, I knew it was exactly thatmdash;an “initial” version. I took some time to work on a few updates, and while I won’t dare call this a “perfect” version, I do think I’ve made some improvements and I’d like to share them with you.

Article Series:

  1. Setup and first iteration
  2. Refinements and final version (This Post)

Before I get started, here are links to the completed demo and source code.

View Demo View Code

Feel free to fork, file PRs, and report bugs to your heart’s content!

The Plan

When I shared the initial version in Part 1, I outlined some ideas to improve the RSS reader, including:

  • Moving to Vuex.
  • Starting to switch to components in the layout. (Well, I was already using Vuetify components, but I meant custom components for my application.)
  • Using IndexedDB to store feed items for quicker access and offline support.

That was the plan, and like most plans, I wasn’t necessarily able to hit everything in this update (and I’ll explain why at the end). But hopefully you’ll see the improvements as a general “moving in the right direction” for the application. With that out of the way, let’s get started!

Implementing Vuex

I’ll start off discussing the biggest change to the application, the addition of Vuex. As I said in the previous post, Vuex describes itself as a “state management pattern + library” on their “What is Vuex” page. No offense to their documentation, but I had a difficult time wrapping my head around exactly what this meant, from a practical sense.

After having using it in a few small projects now, I’m coming to appreciate what it provides. To me, the core benefit is providing a central interface to your data. If I’ve got a basic Vue app working with an array of values, I may have multiple different methods that modify it. What happens when I begin to have certain rules that must be applied before the data changes? As a simple example, imagine an array of RSS feeds. Before I add a new one, I want to ensure it doesn’t already exist in the list. If I have one method that adds to the feed list, that isn’t a problem, but if I have more, it may become cumbersome to keep that logic in sync across the different methods. I could simply build a utility to do this, but what happens when I have other components in play as well?

While it is absolutely not a one-to-one comparison, I feel like Vuex reminds me of how Providers or Services work in Angular. If I ever want to do work with any data, I’ll ensure I use a central provider to handle all access to that data. That’s how I look at Vuex.

So the big change in this application was to migrate all the data related items to a store. I began by adding the library to my HTML:

<script src="https://unpkg.com/vuex"></script>

Woot! Half-way done! (OK maybe not.)

I then created an instance of my store in my JavaScript file:

const feedStore = new Vuex.Store({ // lots of stuff here
});

and included it in my Vue app:

let app = new Vue({ el: '#app', store:feedStore, // lots of stuff here too...
});

Now comes the interesting part. Any time my Vue application needs data, which primarily consists of the list of feeds and the items from those feeds, it’s going to ask the store for them. So, for example, my feeds value is now computed:

feeds() { return feedStore.state.feeds;
},

This is now defined in the state portion of my store:

state: { allItems: [], feeds: [], selectedFeed: null
},

Notice that feeds defaults to an empty array. I had previously used the created event of my Vue app to read in the data from localStorage. Now, I ask the store to do that:

created() { feedStore.dispatch('restoreFeeds');
},

Back in the store, the logic is pretty much the same:

restoreFeeds(context) { let feedsRaw = window.localStorage.getItem('feeds'); if(feedsRaw) { try { let feeds = JSON.parse(feedsRaw); context.state.feeds = feeds; context.state.feeds.forEach(f => { context.dispatch('loadFeed', f); }); } catch(e) { console.error('Error restoring feed json'+e); // bad json or other issue, nuke it window.localStorage.removeItem('feeds'); } }
},

I say “pretty much the same” except now I’m doing a bit of error-checking on the value read in from localStorage. But here’s the crucial bit. I already said I failed in terms of switching to IndexedDB, but in theory, I could build a third version of this application with an updated store and my Vue app won’t know the difference. And that’s where I started to get really excited. The more I worked, the more “dumb” my Vue app became and the less tied it was to any particular implementation of storage. Let’s look at the complete Vue app now:

let app = new Vue({ el: '#app', store:feedStore, data() { return { drawer:true, addFeedDialog:false, addURL:'', urlError:false, urlRules:[], selectedFeed:null } }, computed: { showIntro() { return this.feeds.length == 0; }, feeds() { return feedStore.state.feeds; }, items() { return feedStore.getters.items; } }, created() { feedStore.dispatch('restoreFeeds'); }, methods:{ addFeed() { this.addFeedDialog = true; }, allFeeds() { feedStore.dispatch('filterFeed', null); }, addFeedAction() { this.urlError = false; this.urlRules = []; feedStore.dispatch('addFeed', {url:this.addURL}) .then(res => { this.addURL = ''; this.addFeedDialog = false; }) .catch(e =>{ console.log('err to add', e); this.urlError = true; this.urlRules = ["URL already exists."]; }); }, deleteFeed(feed) { feedStore.dispatch('deleteFeed', feed); }, filterFeed(feed) { feedStore.dispatch('filterFeed', feed); } }
})

What you’ll notice is that pretty much all of the actual logic is now gone and all I’m really doing here is here, although I apologize for lumping everything together in one file.

Adding a Component

One of the other changes I mentioned was beginning to “component-ize” the view layer. I ended up only making one component, feed-item. This reduced the total number of lines in the HTML a bit:

<v-flex xs12 v-for="item in items" :key="item.link"> <feed-item :title="item.title" :content="item.content" :link="item.link" :feedtitle="item.feedTitle" :color="item.feedColor" :posted="item.pubDate"></feed-item>
</v-flex>

It isn’t a huge change by any means, but it did make it bit easier for me when I started working on the feed display. As I’m not using a fancy builder yet, I defined my component straight in JavaScript like so:

Vue.component('feed-item', { props:[ 'color','title','content','link','feedtitle', 'posted' ], template: ` <v-card :color="color"> <v-card-title primary-title> <div class="headline">{{title}} ({{posted | dtFormat}})</div> </v-card-title> <v-card-text> {{content | maxText }} </v-card-text> <v-card-actions> <v-btn flat target="_new" :href="link">Read on {{feedtitle}}</v-btn> </v-card-actions> </v-card> `
});

I’m not doing anything at all fancy in heremdash;there’s no dynamic logic or events or anything like that, but I could certainly add that later where it makes sense. I did finally get around to adding the date and time of posting. If you’re curious about how I built the formatter used for it, read my article Build A i18n Filter Using Vue.js & Native Web Specs.”

The Power of Delete!

Oh, and I finally added a way to delete feeds:

Trash can icon, FTW!

This just fires off a method on the Vue object that, in turn, fires off a call to the store that takes care of removing the feed and items from the UI and then persisting it. A small thing, but, wow, did I wish I had that in the first version when testing. And here is a final shot of everything:

The awesome app in all it’s awesomeness

Next Steps… and What Happened to IndexedDB?

As I said in the beginning, this version is still not perfect but I definitely feel better about it. I highly encourage you to share tips, suggestions, and bug reports in the comments below or on the GitHub repo.

So what happened to IndexedDB support? The issue I ran into was how to properly initialize the database. Vuex stores don’t have a concept of a created process. I could have done something like this:

// dummy code for getting feeds
dispatch('getDB')
.then(() => // do stuff
);

Where the getDB action returns a promise and handles doing a one-time IndexedDB opening and storing the value in the state. I may give this a shot later, and again, what I love about Vuex is that I know I can safely do that without interfering with the rest of the application.


Article Series:

  1. Setup and first iteration
  2. Refinements and final version (This Post)

The post Building a RSS Viewer With Vue: Part 2 appeared first on CSS-Tricks.

Building a RSS Viewer With Vue: Part 1

As I explore, learn, and most importantly, play with Vue.js, I’ve been building different types of apps as a way to get practice with and improve my use of it. A few weeks ago, I was reading about the shut down of Digg’s RSS Reader and while great alternatives exist, I thought it would be fun to build my own with Vue. In this article, I’m going to explain how I put it together and also what’s wrong with it. I knew getting into this that I was going to make some compromises, so the plan is to follow up this version with a nicer one in a follow-up post.

Article Series:

  1. Setup and first iteration (This Post)
  2. Refinements and final version

Let’s start by looking at the app and explaining the various components.

View Demowebpack for this application — just a simple script include with no build process.

The Vuetify, a very nice material design framework that is easy to use. I’m still learning it, so you can be sure that my design could be better, though I’m really happy with how it looks now.

Persistence is done via localStorage. I store the feed metadata retrieved from the RSS feed. This typically includes things like the name of the site, the main URL, and a description. I do not store feed items which means every time you load the site, I re-fetch items. The next version will keep items locally using IndexedDB.

So, how do I load feed information? I could just make a network request to the URL, but most RSS feeds aren’t making use of CORS which means the browser would be blocked from loading it. To get around this, I wrote a quick serverless function with Webtask. It handles both creating a CORS-friendly endpoint as well as parsing the feeds’ XML into friendly JSON.

Now that I’ve covered the various parts of the application, let’s start looking at the code!

The Layout

Let’s start with the layout. As I said, I’m using Vuetify for the UI. I started off using the dark sample layout. This is what creates the header, footer, and left column used for the menu.

Application template

I used the card component for individual feed items. I’m not quite happy with the layout here. For example, I don’t have publication dates rendered yet because I had trouble finding a nice way to render it. I decided to simply punt and wait till the next version, which we’ll **see in Part 2 of this series.

Instead of dumping the entire source code on you at once, let’s look at the individual parts. First, here’s the introductory/help text before any feeds have been added:

<div v-if="showIntro"> <p> Welcome to the RSS Reader, a simple way to manage RSS feeds and read content. To begin using the RSS Reader, add your first feed by clicking the button below. </p> <p> <v-btn color="primary" large @click="addFeed"> <v-icon>add</v-icon> Add Feed </v-btn> </p>
</div>

When you do have feeds, items are displayed as a list of cards:

<v-container fluid grid-list-lg> <v-layout row wrap> <v-flex xs12 v-for="item in items"> <v-card :color="item.feedColor"> <v-card-title primary-title> <div class="headline">{{item.title}}</div> </v-card-title> <v-card-text> {{item.content | maxText }} </v-card-text> <v-card-actions> <v-btn flat target="_new" :href="item.link">Read on {{item.feedTitle}}</v-btn> </v-card-actions> </v-card> </v-flex> </v-layout>
</v-container>

Note the button for reading a feed item uses a target to open it up in a new tab.

To display feeds, I use a list component:

<v-list dense> <v-list-tile @click="allFeeds"> <v-list-tile-action> <v-icon>dashboard</v-icon> </v-list-tile-action> <v-list-tile-content> <v-list-tile-title>All Feeds</v-list-tile-title> </v-list-tile-content> </v-list-tile> <v-list-tile @click="filterFeed(feed)" v-for="feed in feeds" :value="feed == selectedFeed"> <v-list-tile-action> <v-icon :color="feed.color">bookmark</v-icon> </v-list-tile-action> <v-list-tile-content> <v-list-tile-title>{{ feed.title }} </v-list-tile-title> </v-list-tile-content> </v-list-tile> <v-list-tile @click="addFeed"> <v-list-tile-action> <v-icon>add</v-icon> </v-list-tile-action> <v-list-tile-content> <v-list-tile-title>Add Feed</v-list-tile-title> </v-list-tile-content> </v-list-tile>
</v-list>

Finally, here is the modal layout:

<v-dialog v-model="addFeedDialog" max-width="500px"> <v-card> <v-card-title>Add Feed</v-card-title> <v-card-text> Add the RSS URL for a feed below, or the URL for the site and I'll try to auto-discover the RSS feed. <v-text-field v-model="addURL" label="URL" :error="urlError" :rules="urlRules"></v-text-field> </v-card-text> <v-card-actions> <v-btn color="primary" @click.stop="addFeedAction">Add</v-btn> <v-btn color="primary" flat @click.stop="addFeedDialog=false">Close</v-btn> </v-card-actions> </v-card>
</v-dialog>

The Logic

Now for the fun part — JavaScript! As before, I’m not going to dump the entire file on you. Instead, let’s tackle it bit by bit.

On start up, I load any existing feeds that may have defined and then display the introduction text, if needed:

created() { this.restoreFeeds(); if (this.feeds.length === 0) this.showIntro = true;
},

The restoreFeeds method handles checking LocalStorage for existing feeds.

restoreFeeds() { let feeds = localStorage.getItem('feeds'); if (feeds) { this.feeds = JSON.parse(feeds); this.feeds.forEach((feed,idx) => { feed.color = colors[idx % (colors.length-1)]; this.loadFeed(feed); }); }
},

Note that this method handles assigning a color (which is a simple array) and then loading feed data.

Speaking of that, how do I handle loading RSS information? Currently there are two times where this happens. First is when you initially add the feed and second when you reload the application and the feed was already defined. In both cases, I call one URL — the serverless task defined with Webtask. This task will return everything — the metadata about the feed and the items itself. I only care about the metadata on the *first* call and, in theory, I could have made the code a bit quicker by removing the metadata at the server side and strip that out but it didn’t seem like it was worth the effort.

That serverless function is rather simple:

'use strict'; const Parser = require('rss-parser');
const parser = new Parser(); module.exports = function(context, cb) { let url = ''; if(context.body && context.body.url) url = context.body.url; if(context.query && context.query.url) url = context.query.url; if(url === '') cb(new Error('URL parameter not passed.')); console.log('gonna parse '+url); parser.parseURL(url) .then(feed => { console.log(feed); cb(null, {feed:feed}); }) .catch(e => { cb(e); }); }

All I’m doing here is wrapping the npm package rss-parser and that handles all the converting for me. The if statements you see in the beginning handle looking for the url parameter. When calling my webtask, I can either pass a query string variable or send it as part of a HTTP body. Either way, I simply use the rss-parser module and return the result.

The endpoint for this function is:

https://wt-c2bde7d7dfc8623f121b0eb5a7102930-0.sandbox.auth0-extend.com/getRss

You’re welcome to try it out yourself. You can see this being used in the method handling adding a feed:

addFeedAction() { this.urlError = false; this.urlRules = []; //first, see if new if(this.feeds.findIndex((feed) => { return (feed.rsslink === this.addURL); }) >= 0) { this.urlError = true; this.urlRules = ["URL already exists."]; return; } else { fetch(rssAPI+encodeURIComponent(this.addURL)) .then(res => res.json()) .then(res => { // ok for now, assume no error, cuz awesome this.addURL = ''; //assign a color first res.feed.color = colors[this.feeds.length % (colors.length-1)]; // ok, add the items (but we append the url as a fk so we can filter later) res.feed.items.forEach(item => { item.feedPk = this.addURL; item.feedColor = res.feed.color; this.allItems.push(item); }); // delete items delete res.feed.items; // add the original rss link res.feed.rsslink = this.addURL; this.feeds.push(res.feed); this.addFeedDialog = false; //always hide intro this.showIntro = false; //persist the feed, but not the items this.storeFeeds(); }); } },

This method first checks if a feed already exists and, if it doesn’t, it hits the serverless endpoint to get the details. I’ve got a bit of data duplication going on when I store items. I didn’t want to store items “under” a feed object and instead use a global Vue data value, allItems. Therefore, I copy the feed identifier and color into each item. The idea was to make it easier to do item display and filtering later. This feels “wrong” to me, but again, this is my first draft. I’m using a computed property for items and you can see that logic here:

items:function() { if(this.allItems.length === 0) return []; // filter let items = []; if(this.selectedFeed) { console.log('filtered'); items = this.allItems.filter(item => { return item.feedPk == this.selectedFeed.rsslink; }); } else { items = this.allItems; } items = items.sort((a, b) => { return new Date(b.isoDate) - new Date(a.isoDate); }); return items;
}

Looking at it now, I could gather my items from each feed instead of storing one global array, though I could address this later, if I want. I love that Vue gives me options for how to solve things like this.

Where Next?

When I started this article, I explicitly thought *this* *is* a first draft. I’ve pointed out things here and there that I like and don’t like, but what exactly do I plan for the next version?

  • I want to move all the data access into Vuex. Vuex is described as a “state management pattern + library” for Vue. If that doesn’t make much sense to you, don’t worry. I had no idea what it meant at first either. To me, Vuex provides a way to handle more complex data in an encapsulated manner. This becomes even more important as you start building more components that need to share data.
  • Speaking of components, I should consider making “item” a proper Vue component. That’s an easy win.
  • I want to start storing feed items in IndexedDB so you’ll get content the second you open the application. This will make the application much more performant and provide basic offline support. Of course, you can’t read the full entries if you’re offline, but at least *something* could be provided.
  • …and anything you suggest! Take a look at the code and feel free to make suggestions and point out mistakes!

Stay tuned for the second post!

The post Building a RSS Viewer With Vue: Part 1 appeared first on CSS-Tricks.

What does the ‘h’ stand for in Vue’s render method?

If you’ve been working with Vue for a while, you may have come across this way of rendering your app — this is the default in the latest version of the CLI, in main.js:

new Vue({ render: h => h(App)
}).$mount('#app')

Or, if you’re using a render function, possibly to take advantage of JSX:

Vue.component('jsx-example', { render (h) { return <div id="foo">bar</div> }
})

You may be wondering, what does that h do? What does it stand for? The h stands for hyperscript. It’s a riff of HTML, which means Hypertext Markup Language: since we’re dealing with a script, it’s become convention in virtual DOM implementations to use this substitution. This definition is also addressed in the documentation of other frameworks as well. Here it is, for example, in Cycle.js.

In this issue, Evan describes that:

Hyperscript itself stands for “script that generates HTML structures”

This is shortened to h because it’s easier to type. He also describes it a bit more in his Advanced Vue workshop on Frontend Masters.

Really, you can think of it as being short for createElement. Here would be the long form:

render: function (createElement) { return createElement(App);
}

If we replace that with an h, then we first arrive at:

render: function (h) { return h(App);
}

…which can then be shortened with the use of ES6 to:

render: h => h (App)

The Vue version takes up to three arguments:

render(h) { return h('div', {}, [...])
}
  1. The first is type of the element (here shown as div).
  2. The second is the data object. We nest some fields here, including: props, attrs, dom props, class and style.
  3. The third is an array of child nodes. We’ll then have nested calls and eventually return a tree of virtual DOM nodes.

There’s more in-depth information in the Vue Guide here.

The name hyperscript may potentially be confusing to some people, given the fact that hyperscript is actually the name of a library (what isn’t updated these days) and it actually has a small ecosystem. In this case, we’re not talking about that particular implementation.

Hope that clears things up for those who are curious!

The post What does the ‘h’ stand for in Vue’s render method? appeared first on CSS-Tricks.