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.

Native-Like Animations for Page Transitions on the Web

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

Here’s what we’ll be building:

Demo Site

GitHub Repo

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

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

Why?

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

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

Prerequisite Knowledge

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

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

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

Getting Started

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

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

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

Setting Up Our Hooks

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

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

Step One: Vuex Store

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

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

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

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

Step Two: Middleware

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

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

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

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

Step Three: Register Our Navigation

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

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

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

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

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

Step Four: Create our Transitions in the Navigation Component

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

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

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

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

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

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

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

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

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

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

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

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

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

View GitHub Repo

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

It’s the same concept:

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

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

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

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

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

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

VuePress Static Site Generator

VuePress is a new tool from Vue creator Evan You that spins up Vue projects that are more on the side of websites based on content and markup than progressive web applications and does it with a few strokes of the command line.

We talk a lot about Vue around here, from a five-part series on getting started with it to a detailed implementation of a serverless checkout cart

But, like anything new, even the basics of getting started can feel overwhelming and complex. A tool like VuePress can really lower the barrier to entry for many who (like me) are still wrapping our heads around the basics and tinkering with the concepts.

There are alternatives, of course! For example, Nuxt is already primed for this sort of thing and also makes it easy to spin up a Vue project. Sarah wrote up a nice intro to Nuxt and it’s worth checking out, particularly if your project is a progressive web application. If you’re more into React but love the idea of static site generating, there is Gatsby.

Direct Link to Article — Permalink

The post VuePress Static Site Generator appeared first on CSS-Tricks.

Methods, Computed, and Watchers in Vue.js

One of the reasons I love working with Vue is because of how useful methods, computed, and watchers are, and the legibility of their distinction. Until understanding all three, it’s difficult to leverage the functionality of Vue to its full potential. Still, the majority of people I see confused about this framework tend to also be confused about the differences here, so let’s dig in.

In case you need a quick answer and don’t have time to read through the entire article, here’s a small TL;DR:

  • Methods: These are exactly what they sound like they might be (yay, naming!). They’re functions that hang off of an object—typically the Vue instance itself or a Vue component.
  • Computed: These properties may at first look like they’d be used like a method, but are not. In Vue, we use data to track changes to a particular property that we’d like to be reactive. Computed properties allow us to define a property that is used the same way as data, but can also have some custom logic that is cached based on its dependencies. You can consider computed properties another view into your data.
  • Watchers: These are allowing you a peek into the reactivity system. We’re offered some hooks with which to observe any properties that are stored by Vue. If we want to add a bit of functionality each time something changes, or respond to a particular change, we could watch a property and apply some logic. This means that the name of the watcher has to match what we’re trying to observe.

If any of this sounds confusing, don’t worry! We’ll dive in further below and hopefully address any confusion. If you’re familiar with vanilla JavaScript already, methods may be pretty obvious to you, aside from one or two caveats. It might then behoove you (I love that phrase) to skip to the Computed and Watchers sections.

Methods

Methods are likely something you’re going to use a lot while working with Vue. They’re aptly named as, in essence, we’re hanging a function off of an object. They’re incredibly useful for connecting functionality to directives for events, or even just creating a small bit of logic to be reused like any other function. You can call a method within another method, for example. You can also call a method inside a lifecycle hook. They’re very versatile.

Here’s a simple demo to demonstrate:

See the Pen Slim example of methods by Sarah Drasner (@sdras) on CodePen.

<code class="language-css"><div id="app"> <button @click="tryme">Try Me</button> <p>{{ message }}</p>
</div>
new Vue({ el: '#app', data() { return { message: null } }, methods: { tryme() { this.message = Date() } }
})

We could have also executed the logic in the directive itself like <button @click="message = Date()">Try Me</button>, which works very well for this small example. However, as the complexity of our application grows, it’s more common to do as we see above to break it out to keep it legible. There’s also a limit to the logic that Vue will allow you to express in a directive—for instance, expressions are allowed but statements are not.

You may notice that we’re be able to access this method within that component or Vue instance, and we can call any piece of our data here, in this case, this.message. You don’t have to call a method like you’d call a function within a directive. For example, @click=”methodName()” is unnecessary. You can reference it with @click=”methodName”, unless you need to pass a parameter, such as @click=”methodName(param)”.

Using directives to call methods is also nice because we have some existing modifiers. One such example that’s very useful is .prevent, which will keep a submit event from reloading the page, used like this:

<form v-on:submit.prevent="onSubmit"></form>

There are many more, here are just a few.

Computed

Computed properties are very valuable for manipulating data that already exists. Anytime you’re building something where you need to sort through a large group of data and you don’t want to rerun those calculations on every keystroke, think about using a computed value.

Some good candidates include, but are not limited to:

  • Updating a large amount of information while a user is typing, such as filtering a list
  • Gathering information from your Vuex store
  • Form validation
  • Data visualizations that change depending on what the user needs to see

Computed properties are a vital part of Vue to understand. They are calculations that will be cached based on their dependencies and will only update when needed. They’re extremely performant when used well and extraordinarily useful. There are many large libraries that handle this kind of logic that you can now eliminate with only a few lines of code.

Computed properties aren’t used like methods, though at first, they might look similar- you’re stating some logic in a function and returning- but the name of that function becomes a property that you’d then use in your application like data.

If we needed to filter this big list of names of heroes based on what the user was typing, here’s how we would do it. We’re keeping this really simple so you can get the base concepts down. Originally our list would output in our template using names, which we store in data:

new Vue({ el: '#app', data() { return { names: [ 'Evan You', 'John Lindquist', 'Jen Looper', 'Miriam Suzanne', ... ] } }
})
<div id="app"> <h1>Heroes</h1> <ul> <li v-for="name in names"> {{ name }} </li> </ul>
</div>

Now let’s create a filter for those names. We’ll start by creating an input with v-model that will originally be an empty string, but we’ll eventually use to match and filter through our list. We’ll call this property findName and you can see it referenced both on the input and in the data.

<label for="filtername">Find your hero:</label>
<input v-model="findName" id="filtername" type="text" />
data() { return { findName: '', names: [ 'Evan You', 'John Lindquist', ... ] }
}

Now, we can create the computed property that will filter all of the names based on what the user has typed into the input, so anything in our findName property. You’ll note that I’m using regex here to make sure that mismatched capitalization doesn’t matter, as users will typically not capitalize as they type.

computed: { filteredNames() { let filter = new RegExp(this.findName, 'i') return this.names.filter(el => el.match(filter)) }
}

And now we’ll update what we’re using in the template to output from this:

<ul> <li v-for="name in names"> {{ name }} </li>
</ul>

…to this:

<ul> <li v-for="name in filteredNames"> {{ name }} </li>
</ul>

And it filters for us on every keystroke! We only had to add a couple of lines of code to make this work, and didn’t have to load any additional libraries.

See the Pen Filter a list with Computed- end by Sarah Drasner (@sdras) on CodePen.

I can’t tell you how much time I save by using them. If you’re using Vue and haven’t explored them yet, please do, you’ll thank yourself.

Watchers

Vue has nice abstractions, and anyone who has been a programmer for a while will usually tell you that abstractions can be a pain because you’ll eventually get to a use case they can’t solve. However, this situation is accounted for, because Vue grants us some deeper access to into the reactivity system, which we can leverage as hooks to observe anything that’s changing. This can be incredibly useful because, as application developers, most of what we’re responsible for are things that change.

Watchers also allow us to write much more declarative code. You’re no longer tracking everything yourself. Vue is already doing it under the hood, so you can also have access to changes made to any properties it’s tracking, in data, computed, or props, for example.

Watchers are incredibly good for executing logic that applies to something else when a change on a property occurs (I first heard this way of putting it from Chris Fritz, but he says he might have also heard it from someone else ☺️). This isn’t a hard rule- you can absolutely use watchers for logic that refers to the property itself, but it’s a nice way of looking at how watchers are immediately different from computed properties, where the change will be in reference to the property we intend to use.

Let’s run through the most simple example possible so you get a taste of what’s happening here.

new Vue({ el: '#app', data() { return { counter: 0 } }, watch: { counter() { console.log('The counter has changed!') } }
})

As you can see in the code above, we’re storing counter in data, and by using the name of the property as the function name, we’re able to watch it. When we reference that counter in watch, we can observe any change to that property.

Transitioning State With Watchers

If the state is similar enough, you can even simply transition the state with watchers. Here’s an example I built from scratch of a chart with Vue. As the data changes, the watchers will update it and simply transition between them.

SVG is also good for a task like this because it’s built with math.

See the Pen Chart made with Vue, Transitioning State by Sarah Drasner (@sdras) on CodePen.

watch: { selected: function(newValue, oldValue) { var tweenedData = {} var update = function () { let obj = Object.values(tweenedData); obj.pop(); this.targetVal = obj; } var tweenSourceData = { onUpdate: update, onUpdateScope: this } for (let i = 0; i < oldValue.length; i++) { let key = i.toString() tweenedData[key] = oldValue[i] tweenSourceData[key] = newValue[i] } TweenMax.to(tweenedData, 1, tweenSourceData) }
}

What happened here?

  • First we created a dummy object that will get updated by our animation library.
  • Then we have an update function that is invoked on each tween step. We use this to push the data.
  • Then we create an object to hold the source data to be tweened and the function pointer for update events.
  • We create a for loop, and turn the current index into a string
  • Then we can tween over the our target dummy object, but we’ll only do this for the specific key

We could also use animation in watchers to create something like this time difference dial. I travel a bit and all my coworkers are in different areas, so I wanted a way to track what local time we were all in, as well as some signification of the change from daytime/nighttime as well.

See the Pen Vue Time Comparison by Sarah Drasner (@sdras) on CodePen.

Here we’re watching the checked property, and we’ll fire different methods that contain timeline animations that change the hue and saturation and some other elements based on the relative association to the current time. As mentioned earlier- the change occurs on the dropdown, but what we’re executing is logic that’s applied elsewhere.

watch: { checked() { let period = this.timeVal.slice(-2), hr = this.timeVal.slice(0, this.timeVal.indexOf(':')); const dayhr = 12, rpos = 115, rneg = -118; if ((period === 'AM' && hr != 12) || (period === 'PM' && hr == 12)) { this.spin(`${rneg - (rneg / dayhr) * hr}`) this.animTime(1 - hr / dayhr, period) } else { this.spin(`${(rpos / dayhr) * hr}`) this.animTime(hr / dayhr, period) } }
},

There are also a number of other interesting things about watchers, for instance: we’re given access to both the new and old versions of the property as parameters, we can specify deep if we’d like to watch a nested object. For more detailed information, there’s a lot of good information in the guide.

You can see how watchers can be incredibly useful for anything that’s updating—be it form inputs, asynchronous updates, or animations. If you’re curious how reactivity in Vue works, this part of the guide is really helpful. If you’d like to know more about reactivity in general, I really enjoyed Andre Staltz’ post and the Reactivity section of Mike Bostock’s A Better Way to Code.

Wrapping Up

I hope this was a helpful breakdown on how to use each, and speeds up your application development process by using Vue efficiently. There’s a stat out there that we spend 70% of our time as programmers reading code and 30% writing it. Personally, I love that, as a maintainer, I can look at a codebase I’ve never seen before and know immediately what the author has intended by the distinction made from methods, computed, and watchers.

The post Methods, Computed, and Watchers in Vue.js appeared first on CSS-Tricks.

Vue Design System

We talk a lot about Vue around here, including some practical applications of it, but haven’t gotten deep into designing for it. In this post, Viljami Salminen describes his Vue design process and the thinking that led him to build the Vue Design System:

A design system can help establish a common vocabulary between everyone in an organization and ease the collabo­ration between different disciplines. I’ve seen it go the other way around too when important decisions have been made in a rush. To avoid that, Vue Design System introduces the following framework for naming that I’ve found working well in the past…

Viljami lists Design Principles, Tokens, Elements, Patterns, and Templates as the way in which he structures a system and I think it’s a pretty interesting approach and a parallel to Lucas Lemonnier’s post on creating design systems in Sketch, using Atomic Design as the structure. I particularly like how Viljami fits everything together in the example style guide that’s provided.

Direct Link to Article — Permalink

The post Vue Design System appeared first on CSS-Tricks.