A practice that has gained significant traction recently is displaying a UI full of placeholder styles that look like the fully-loaded page before the entirety of the content loads. This tactic, used by Slack, Medium, and Facebook, among others, helps to avoid unexpected jumps in the page as it loads, and makes the user feel like the page is loading faster. Accomplishing this is fairly easy with Vue.js. We’ll explore several methods to do this here.
The most basic method might be to have different styles for elements that are empty and elements that have content. Well, you can actually do that with plain CSS.
<template>
<p class="has-placeholder">{{content}}</p>
</template>
...
<style>
.has-placeholder {
filter: none;
transition: all 200ms;
background-color: transparent;
}
.has-placeholder:empty {
width: 5vw;
height: 20px;
background-color: rgba(0, 0, 0, 0.2);
filter: blur(7px);
}
</style>
Works fairly well, doesn’t it! However, it doesn’t work well with values that might not exist, such as properties on objects currently undefined.
To make matters worse, the :empty pseudo-selector only matches elements that are entirely empty with no whitespace in the source. That means no multi-line or even spaced elements:
<p></p> <!-- Empty -->
<p> </p> <!-- Not Empty -->
<p>
</p> <!-- Not Empty -->
So, we’ll try the next method.
Using Vue to toggle classes is almost as simple as the previous method, and has the added bonus of letting us be able to use multi-line tags.
<template>
<p class="has-placeholder" :class="{empty: !content}">
{{content}}
</p>
</template>
...
<style>
.has-placeholder {
filter: none;
transition: all 200ms;
background-color: transparent;
}
.has-placeholder.empty {
width: 5vw;
height: 20px;
background-color: rgba(0, 0, 0, 0.2);
filter: blur(7px);
}
</style>
However, this still has the issue of accessing values that might not exist on an object, and it’s not all that idiomatic, so we need to go deeper.
This is probably the first solution that might come to mind when trying to deal with a problem of this sort. Simply have two execution paths, one for when the data is loaded, and one for the placeholder.
<template>
<div class="wrapper">
<p v-if="content">
{{content}}
</p>
<p v-else class="has-placeholder empty"></p>
</div>
</template>
...
<style>
.has-placeholder.empty {
width: 5vw;
height: 20px;
background-color: rgba(0, 0, 0, 0.2);
filter: blur(7px);
}
</style>
Unfortunately, with this method, we increase the verbosity, decrease the performance, and lose transitions in the process. The code is very explicit, but looks pretty messy. We also have to introduce conditionals for every property, unless we split the entire layout into two paths…
… Which is what we’re going to do now! This method has the disadvantage of being much more verbose and potentially difficult to maintain, but it also allows for a lot of customization and dead-simple usage.
<template>
<div class="wrapper">
<!-- Render the live template -->
<div v-if="contentObject" class="user-container">
<img class="avatar" :src="contentObject.avatar"/>
<h2>My Name is {{contentObject.name}}</h2>
<h4>Biography</h4>
<p>{{contentObject.bio}}</p>
</div>
<!-- Render the placeholder template -->
<div v-else class="user-container placeholder">
<img class="avatar" src="/path/to/default-avatar.png"/>
<h2></h2>
<h4>Biography</h4>
<p></p>
</div>
</div>
</template>
...
<style>
.user-container.placeholder > *:not(img) {
background-color: rgba(0, 0, 0, 0.2);
filter: blur(7px);
}
.user-container.placeholder h2 {
height: 40px;
width: 50%;
}
.user-container.placeholder p {
height: 400px;
width: 30%;
}
</style>
Throw some transitions and clean it up a bit, and you’ve got a fairly workable solution for your site! Just, not a really nice one.
My personal favorite solution is to combine the Classy Toggles method with some mock data before the real data loads. That way you can still have a component that is fairly easy to maintain while not having to worry as much about possibly invalid data. (Assuming you check your mock data, that is.)
We’ll work with this schema for the example:
const person:Object = {
name: String,
avatar: String,
bio: String
};
const people: Array<person>;
<template>
<div class="user-container">
<img class="avatar" :src="contentObject.avatar"/>
<h2 :class="{placeholder: !contentObject.name}">My Name is {{contentObject.name}}</h2>
<h4>Biography</h4>
<p :class="{placeholder: !contentObject.bio}">
{{contentObject.bio}}
</p>
</div>
</template>
<script>
export default {
props: {
contentObject: {
type: Object,
default: {
avatar: '/path/to/default/avatar.png',
name: '',
bio: ''
}
}
}
}
</script>
<style>
.user-container * {
transition: all 200ms;
background-color: transparent;
color: inherit;
filter: none;
}
.user-container .placeholder {
color: transparent;
background-color: rgba(0, 0, 0, 0.2);
filter: blur(7px);
}
.user-container h2.placeholder {
height: 40px;
width: 50%;
}
.user-container p.placeholder {
height: 400px;
width: 30%;
}
</style>
This method has the advantage of having a fairly simple template, still having CSS transitions, and being able to maintain a decent data model (though more checks are needed). It does, however, require you to keep close tabs on your template and data model to make sure nothing odd is getting through.
A classic error that plagues almost every new Vue.js developer is that of attempting to access a property on an object that has yet to be initialized. Unlike Angular 2+, Vue does not have the ?.prop ?[prop] existential operator, so there’s no simple, safe way to access a property on an object that might not exist.
As a workaround, you could use a component method that recursively checks level to see if it is valid. Lo-dash comes with the _.get method, or you could use one of the many solutions here.
However, a better solution would be to maintain a consistent model. In many cases, if a property may or may not exist, that’s usually a sign of code smell. Plus, Vue’s reactivity system isn’t good at picking up properties that suddenly exist that didn’t previously.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!