Viewport units have been around for several years now, with near-perfect support in the major browsers, but I keep finding new and exciting ways to use them. I thought it would be fun to review the basics, and then round-up some of my favorite use-cases.
What are viewport units?
Four new „viewport-relative” units appeared in the CSS specifications between 2011 and 2015, as part of the W3C’s CSS Values and Units Module Level 3. The new units – vw, vh, vmin, and vmax – work similarly to existing length units like px or em, but represent a percentage of the current browser viewport.
Viewport Width (vw) – A percentage of the full viewport width. 10vw will resolve to 10% of the current viewport width, or 48px on a phone that is 480px wide. The difference between % and vw is most similar to the difference between em and rem. A % length is relative to local context (containing element) width, while a vw length is relative to the full width of the browser window.
Viewport Height (vh) – A percentage of the full viewport height. 10vh will resolve to 10% of the current viewport height.
Viewport Minimum (vmin) – A percentage of the viewport width or height, whichever is smaller. 10vmin will resolve to 10% of the current viewport width in portrait orientations, and 10% of the viewport height on landscape orientations.
Viewport Maximum (vmax) – A percentage of the viewport width or height, whichever is larger. 10vmin will resolve to 10% of the current viewport height in portrait orientations, and 10% of the viewport width on landscape orientations. Sadly, and strangely, vmax units are not yet available on Internet Explorer or Edge.
While these units are derived from viewport height or width, they can all be used everywhere lengths are accepted – from font-size to positioning, margins, padding, shadows, borders, and so on. Let’s see what we can do!
Responsive Typography
It’s become very popular to use viewport units for responsive typography – establishing font-sizes that grow and shrink depending on the current viewport size. Using simple viewport units for font-size has an interesting (dangerous) effect. As you can see, fonts scale very quickly – adjusting from unreadably small to extra large in a very small range.
This direct scaling is clearly too dramatic for daily use. We need something more subtle, with minimums and maximums, and more control of the growth rate. That’s where calc() becomes useful. We can combine a base size in more steady units (say 16px) with a smaller viewport-relative adjustment (0.5vw), and let the browser do the math: calc(16px + 0.5vw)
By changing the relationship between your base-size and viewport-relative adjustment, you can change how dramatic the growth-rate is. Use higher viewport values on headings, and watch them grow more quickly than the surrounding text. This allows for a more dynamic typographic scale on larger screens, while keeping fonts constrained on a mobile device – no media-queries required. You can also apply this technique to your line-height, allowing you to adjust leading at a different rate than the font-size.
body {
// font grows 1px for every 100px of viewport width
font-size: calc(16px + 1vw);
// leading grows along with font,
// with an additional 0.1em + 0.5px per 100px of the viewport
line-height: calc(1.1em + 0.5vw);
}
For me, this is enough complexity. If I need to constrain the top-end for rapid-growth headings, I can do that with one single media-query wherever the text becomes too large:
Suddenly I wish there was a max-font-size property.
Others have developed more complex calculations and Sass mixins to specify the exact text-size ranges at specific media-queries. There are several existing CSS-Tricks articles that explain the technique and provide snippets to help you get started:
I think that’s overkill in most cases, but your milage will absolutely vary.
Full-Height Layouts, Hero Images, and Sticky Footers
There are many variations on full-height (or height-constrained) layouts – from desktop-style interfaces to hero images, spacious designs, and sticky footers. Viewport-units can help with all of these.
In a desktop-style full-height interface, the page is often broken into sections that scroll individually – with elements like headers, footers, and sidebars that remains in place at any size. This is common practice for many web-apps these days, and vh units make it much simpler. Here’s an example using the new CSS Grid syntax:
A single declaration on the body element, height: 100vh, constrains your application to the height of the viewport. Make sure you apply overflow values on internal elements, so your content isn’t cut off. You can also achieve this layout using flexbox or floats.Note that full-height layouts can cause problems on some mobile browsers. There’s a clever fix for iOs Safari, that we use to handle one of the most noticeable edge-cases.
Sticky-footers can be created with a similar technique. Change your body height: 100vh to min-height: 100vh and the footer will stay in place at the bottom of your screen until it’s pushed down by content.
Apply vh units to the height, min-height, or max-height of various elements to create full-screen sections, hero images, and more. In the new OddBird redesign, we constrained our hero images with max-height: 55vh so they never push headlines off the page. On my personal website, I went with max-height: 85vh for a more image-dominated look. On other sites, I’ve applied min-height: 90vh to sections.
It can also be useful to constrain the height-to-width ratio of an element. This is especially useful for embeded content, like videos. Chris has written about this before. In the good-old-days, we would do that with %-based padding on a container element, and absolute positioning on the inner element. Now we can sometimes use viewport units to achieve that effect without the extra markup.
If we can count on the video being full-screen, we can set our height relative to the full viewport width:
That math doesn’t have to happen in the browser with calc. If you are using a pre-processor like Sass, it will work just as well to do the math there: height: 100vw * (9/16). If you need to constrain the max-width, you can constrain the max-height as well:
Here’s a demonstration showing both options, with CSS custom properties (variables) to make the math more semantic. Play with the numbers to see how things move, keeping the proper ratio at all times:
Chris takes this one step farther in his pre-viewport-units article, so we will too. What if we need actual HTML content to scale inside a set ratio – like presentation slides often do?
We can set all our internal fonts and sizes using the same viewport units as the container. In this case I used vmin for everything, so the content would scale with changes in both container height and width:
For years now, it’s been popular to mix constrained text with full-width backgrounds. Depending on your markup or CMS, that can become difficult. How do you break content outside of a restricted container, so that it fills the viewport exactly?
Again, viewport units can come in handy. This is another trick we’ve used on the new OddBird site, where a static-site generator sometimes limits our control of the markup. It only takes a few lines of code to make this work.
There are more in-depth articles about the technique, both at Cloud Four and here on CSS Tricks.
Getting Weird
Of course, there’s much more you can do with viewport units, if you start experimenting. Check out this pure CSS scroll-indicator (made by someone named Mike) using viewport units on a background image:
I came across a couple suchanimations a while ago and this gave me the idea of creating my own versions with as little code as possible, no external libraries, using various methods, some of which take advantage of more recent features we can use these days, such as CSS variables. This article is going to guide you through the process of building these demos.
Before anything else, this is the animation we’re trying to achieve here:
The desired result: a rotating ☯ symbol, with its two lobes increasing and decreasing in size.
No matter what method we choose to use to recreate the above animation, we always start from the static yin and yang shape which looks as illustrated below:
First off, we have a big circle of diameter d. Inside this circle, we tightly fit two smaller circles, each one of them having a diameter that’s half the diameter of our initial big circle. This means that the diameter for each of these two smaller circles is equal to the big circle’s radius r (or .5*d). Inside each of these circles of diameter r we have an even smaller concentric circle. If we are to draw a diameter for the big circle that passes through all the central points of all these circles – the line segment AB in the illustration above, the intersections between it and the inner circles split it into 6 equal smaller segments. This means that the diameter of one of the smallest circles is r/3 (or d/6) and its radius is r/6.
Knowing all of this, let’s get started with the first method!
Plain old HTML + CSS
In this case, we can do it with one element and its two pseudo-elements. The how behind building the symbol is illustrated by the following animation (since the whole thing is going to rotate, it doesn’t matter if we switch axes):
The actual element is the big circle and it has a top to bottom gradient with a sharp transition right in the middle. The pseudo-elements are the smaller circles we place over it. The diameter of one of the smaller circles is half the diameter of the big circle. Both smaller circles are vertically middle-aligned with the big circle.
So let’s start writing the code that can achieve this!
First of all, we decide upon a diameter $d for the big circle. We use viewport units so that everything scales nicely on resize. We set this diameter value as its width and height, we make the element round with border-radius and we give it a top to bottom gradient background with a sharp transition from black to white in the middle.
Now let’s move on to the smaller circles which we create with pseudo-elements. We give our element display: flex and make its children (or pseudo-elements in our case) middle aligned with it vertically by setting align-items: center. We make these pseudo-elements have half the height (50%) of their parent element and make sure that, horizontally, they each cover half of the big circle. Finally, we make them round with border-radius, give them a dummy background and set the content property just so that we can see them:
.☯ {
display: flex;
align-items: center;
/* same styles as before */
&:before, &:after {
flex: 1;
height: 50%;
border-radius: 50%;
background: #f90;
content: '';
}
}
All that’s left to do before we get the static symbol is to give these two pseudo-elements borders. The black one should get a white border, while the white one should get a black border. These borders should be a third of the pseudo-element’s diameter, which is a third of half the diameter of the big circle – that gives us $d/6.
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
border: solid $d/6 white;
}
&:after {
/* same styles as before */
border-color: black;
}
}
This is because, vertically, the border adds up to the height instead of being subtracted out of it. Horizontally, we haven’t set a width, so it gets subracted from the available space. We have two fixes possible here. One would be to set box-sizing: border-box on the pseudo-elements. The second one would be to change the height of the pseudo-elements to $d/6 – we’ll go with this one:
We now have the basic shape, so let’s move on to the animation! This animation involves going from the state where the first pseudo-element has shrunk to let’s say half its original size (which would mean a scaling factor $f of .5) while the second pseudo-element has expanded to take up all available space left – meaning to the diameter of the big circle (which is twice its original size) minus the diameter of the first circle (which is $f of its original size) to the state where the second pseudo-element has shrunk to $f of its original size and the first pseudo-element has expanded to 2 - $f of its original size. The first pseudo-element circle scales relative to its leftmost point (so we need to set a transform-origin of 0 50%), while the second one scales relative to its rightmost point (100% 50%).
$f: .5;
$t: 1s;
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
transform-origin: 0 50%;
transform: scale($f);
animation: s $t ease-in-out infinite alternate;
}
&:after {
/* same styles as before */
transform-origin: 100% 50%;
animation-delay: -$t;
}
}
@keyframes s { to { transform: scale(2 - $f) } }
We now have the shape changing animation we’ve been after:
However, there’s still one more thing we can do to make the compiled CSS more efficient: eliminate redundancy with CSS variables!
white can be written in HSL format as hsl(0, 0%, 100%). The hue and the saturation don’t matter, any value that has the lightness 100% is white, so we just set them both to 0 to make our life easier. Similarly, black can be written as hsl(0, 0%, 0%). Again, the hue and saturation don’t matter, any value that has the lightness 0% is black. Given this, our code becomes:
the x component of our transform-origin is calc(0*100%) for the first pseudo-element and calc(1*100%) for the second one
our border-color is hsl(0, 0%, calc((1 - 0)*100%)) for the first pseudo-element and hsl(0, 0%, calc((1 - 1)*100%)) for the second one
our background is hsl(0, 0%, calc(0*100%)) for the first pseudo-element and hsl(0, 0%, calc(1*100%)) for the second one
our animation-delay is calc(0*#{-$t}) for the first pseudo-element and calc(1*#{-$t}) for the second one
This means we can use a custom property that acts as a switch and is 0 for the first pseudo-element and 1 for the second:
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
--i: 0;
border: solid $d/6 hsl(0, 0%, calc((1 - var(--i))*100%));
transform-origin: calc(var(--i)*100%) 50%;
background: hsl(0, 0%, calc(var(--i)*100%));
animation: s $t ease-in-out calc(var(--i)*#{-$t}) infinite alternate;
}
&:after { --i: 1 }
}
This eliminates the need for witing all these rules twice: all we need to do now is flip the switch! Sadly, this only works in WebKit browsers for now because Firefox and Edge don’t support using calc() as an animation-delay value and Firefox doesn’t support using it inside hsl() either.
Canvas + JavaScript
While some people might think this method is overkill, I really like it because it requires about the same amount of code as the CSS one, it has good support and good performance.
We start with a canvas element and some basic styles just to put it in the middle of its container (which is the body element in our case) and make it visible. We also make it circular with border-radius so that we simplify our job when drawing on the canvas.
Alright, now let’s move on to the JavaScript part! Before anything else, we need to get the canvas element, the 2D context and set the canvas element’s width and height attributes (things we draw on the canvas would appear stretched otherwise). Then, we’re going to need to have a radius for our big circle. We get this radius to be half the computed size of the canvas element and, after we do that, we translate our context such that we bring the 0,0 point of our canvas dead in the middle (it’s originally in the top left corner). We make sure we recompute the radius and the width and height attributes on each resize because, in the CSS, we made the canvas dimensions depend on the viewport.
const _C = document.querySelector('canvas'),
CT = _C.getContext('2d');
let r;
function size() {
_C.width = _C.height = Math.round(_C.getBoundingClientRect().width);
r = .5*_C.width;
CT.translate(r, r);
};
size();
addEventListener('resize', size, false);
After we’ve done this, we can move on to drawing on the canvas. Draw what? Well, a shape made out of three arcs, as shown in the illustration below:
In order to draw an arc on a 2D canvas, we need to know a few things. First off, it’s the coordinates of the central point of the circle this arc belongs to. Then we need to know the radius of this circle and the angles (relative to the x axis of the local coordinate system of the circle) at which the start and end points of the arc are located. Finally, we need to know if we go from the start point to the end point clockwise or not (if we don’t specify this, the default is clockwise).
The first arc is on the big circle whose diameter is equal to the canvas dimensions and, since we’ve placed the 0,0 point of the canvas right in the middle of this circle, this means we know both the first set of coordinates (it’s 0,0) and the circle radius (it’s r). The start point of this arc is the leftmost point of this circle – this point is at -180° (or -π). The end point is the rightmost point of the circle, which is at 0° (also 0 in radians). If you need a refresher of angles on a circle, check out this helper demo.
This means we can create a path and add this arc to it and, in order to see what we have so far, we can close this path (which in this case means connecting the end point of our arc to the start point with a straight line) and fill it (using the default fill, which is black):
Now let’s move on to the second arc. The coordinates of the central point of the circle it’s on are .5*r,0 and its radius is .5*r (half the radius of the big circle). It goes from 0 to π, moving clockwise in doing so. So the arc we add to out path before closing it is:
Now we have one more arc left to add. The radius is the same as for the previous one (.5*r) and the first set of coordinates is -.5*r,0. This arc goes from 0 to -π and it’s the first arc not to go clockwise, so we need to change that flag:
Next, we’re going to add the black circle to this path. We’re not going to create another path because the aim is to group all shapes with the same fill into the same path for better performance. Calling fill() is expensive, so we don’t want to do it more often than we really need to.
A circle is just an arc from 0° to 360° (or from 0 to 2*π). The central point of this circle coincides to that for the last arc we’ve drawn (-.5*r, 0) and its radius is a third of that of the previous two arcs.
CT.arc(-.5*r, 0, .5*r/3, 0, 2*Math.PI);
Now we’re getting really close to having the full symbol:
All that’s left to do is create a white circle, symmetrical to the black one with respect to the y axis. This means need to switch to a white fill, start a new path and then add an arc to it using almost the same command we used to add the black circle shape – the only difference is that we reverse the sign of the x coordinate (this time, it’s +, not -). After that, we close that path and fill it.
For the animation, we want to go from the state where the first of the smaller arcs has shrunk to half is original radius (so we use a scaling factor F of .5) and the other one has expanded accordingly to the state where these initial radii are reversed.
In the initial state, given that the radius of the smaller arcs is initially .5*r, then the radius of the first of them after being scaled down by a factor F is r1 = F*.5*r. Since the radii of the smaller circles need to add up to the radius of the big circle r, we have that the radius of the second one of the smaller circles is r2 = r - r1 = r - F*.5*r.
In order to get the x coordinate of the origin of the first smaller arc for the initial state, we need to subtract its radius from the x coordinate of the point it starts at. This way, we get that this coordinate is r - r1 = r2. Similarly, in order to get the x coordinate of the origin of the second smaller arc, we need to add up its radius to the coordinate of the point it ends at. This way, we get that this coordinate is -r + r2 = -(r - r2) = -r1.
The initial vs. the final state of the animation (live demo).
For the final state, the values of the two radii are reversed. The second one is F*.5*r, while the first one is r - F*.5*r.
With every frame of our animation, we increase the current radius of the first smaller arc from the minimum value (F*.5*r) to the maximum value (r - F*.5*r) and then we start decreasing it to the minimum value and then the cycle repeats while also scaling the radius of the other smaller arc accordingly.
In order to do this, we first set the minimum and maximum radius in the size() function:
const F = .5;
let rmin, rmax;
function size() {
/* same as before */
rmin = F*.5*r;
rmax = r - rmin;
};
At any moment in time, the current radius of the first of the smaller arcs is k*rmin + (1 - k)*rmax, where this k factor keeps going from 1 to 0 and then back up to 1. This sounds similar to the cosine function on the [0, 360°] interval. At 0°, the value of the cosine is 1. Then it starts decreasing and it keeps doing so until it gets to 180°, when it reaches its minimum value of -1, after which the value of the function starts increasing again until it gets to 360°, where it’s again 1:
Alright, but the values of the cosine function are in the [-1, 1] interval and we need a function that gives us values in the [0, 1] interval. Well, if we add 1 to the cosine, then we shift the whole graph up and the values are now in the [0, 2] interval:
[0, 2] isn’t [0, 1], so what we still need to do here is divide the whole thing by 2 (or multiply it with .5, same thing). This squishes our graph to the desired interval.
Good, but what’s up with that angle? We don’t have an angle going from 0° to 360°. If we’re going to use requestAnimationFrame, we just have the number of the current frame, which starts at 0 and then keeps going up. Well, at the beginning, we set a total number of frames T for one animation cycle (the first arc going from the minimum radius value to the maximum radius value and then back again).
For every frame, we compute the ratio between the number of the current frame (t) and the total numeber of frames. For one cycle, this ratio goes from 0 to 1. If we multiply this ratio with 2*Math.PI (which is the same as 360°), then the result goes from 0 to 2*Math.PI over the course of a cycle. So this is going to be our angle.
const T = 120;
(function ani(t = 0) {
let k = .5*(1 + Math.cos(t/T*2*Math.PI)),
cr1 = k*rmin + (1 - k)*rmax, cr2 = r - cr1;
})();
The next step is to put inside this function the code that actually draws our symbol. The code for beginning, closing, filling paths, changing fills stays the same, as does the code needed for creating the big arc. The things that change are:
the radii of the smaller arcs – they’re cr1 and cr2 respectively
the x coordinates of the central points for the smaller arcs – they’re at cr2 and -cr1 respectively
the radii of the black and white circles – they’re cr2/3 and cr1/3 respectively
the x coordinates of the central points of these circles – they’re at -cr1 and cr2 respectively
Before we actually start animating the radii of the arcs, we still need to take care of a couple more things. First of all, if we start the animation right now, we’re just going to be drawing how the shape looks for each frame over what we’ve drawn for the previous frames, which is going to create one big mess. In order to avoid this, we need to clear the canvas for each frame, before drawing anything on it. What we clear is the visible part, which is inside the rectangle of canvas dimensions whose top left corner is at -r,-r:
CT.clearRect(-r, -r, _C.width, _C.width);
The second little problem we need to fix is that we’re switching to a white fill, but at the start of the next frame, we need a black one. So we need to make this switch for each frame before the beginning of the first path:
CT.fillStyle = 'black';
Now we can actually start the animation:
requestAnimationFrame(ani.bind(this, ++t));
This gives us the morphing animation, but we still need to rotate the whole thing. Before tackling that, let’s look at the formula for k once more:
let k = .5*(1 + Math.cos(t/T*2*Math.PI))
T and 2*Math.PI are constant throughout the animation, so we can just take that part out and store it as a constant angle A:
const T = 120, A = 2*Math.PI/T;
(function ani(t = 0) {
let k = .5*(1 + Math.cos(t*A));
/* same as before */
})();
Now for every frame, we can also rotate the context by A after clearing the canvas.
Not too exciting, so let’s move on to drawing something on the SVG canvas. Just like in the canvas case, we’ll be drawing a path made up of the same three arcs (the big one with a radius that’s half the size of the SVG viewBox and the two smaller ones with a radius that’s half of that of the big arc in the static case) and two small circles (with a radius that’s a third of that of the smaller arc they share their central point with).
So we start by picking a radius r value and using it to set the viewBox on the svg element:
- var r = 1500;
svg(viewBox=[-r, -r, 2*r, 2*r].join(' '))
The next step is to add the path made up of the three arcs. Creating a path in SVG is a bit different from canvas. Here, the shape is described by the path data d attribute, which, in our case, is made up of:
a „move to” (M) command after which we specify the coordinates of the start point of our path (also the start point of the big arc in this case)
an „arc to” (A) command for each of our arcs after which we describe our arcs; each of these arcs starts from the end point of the previous arc or, in the case of the first arc, from the start point of our path
Let’s take a closer look at the components of an „arc to” (A) command:
the radius of our arc along the x axis of its system of coordinates – this is equal to r in the case of the big arc and to .5*r in the case of the two smaller ones
the radius of our arc along the y axis of its system of coordinates – this is equal to the one along the x axis in the case of circular arcs as we have here (it’s only different for elliptical arcs, but that’s beyond the scope of this article)
the rotation of our arc’s system of coordinates – this only influences the arc’s shape in the case of elliptical arcs, so we can safely always take it 0 to simplify things for circular arcs
the large arc flag – this is 1 if our arc is greater than half a circle and 0 otherwise; since our arcs are exactly half a circle, they’re not greater than haf a circle, so this is always 0 in our case
the sweep flag – this is 1 if the arc goes clockwise between its start and its end point and 0 otherwise; in our case, the first two arcs go clockwise, while the third doesn’t, so the values we use for the three arcs are 1, 1 and 0
the x coordinate of the arc’s end point – this is something we need to determine for each arc
the y coordinate of the arc’s end point – also something we need to determine for each arc
At this point, we already know most of what we need. All we still have to figure out are the coordinates of the arcs’ endpoints. So let’s consider the following illustration:
The structure of the three arc shape with coordinates of arc endpoints (live demo).
From the illustration above we can see that the first arc (the big one) starts at (-r,0) and ends at (r,0), the second one ends at 0,0 and the third one ends at (-r,0) (also the start point of our path). Note that the y coordinates of all these points remain 0 even if the smaller arcs’ radii change, but the x coordinate of the second arc’s endpoint only happens to be 0 in this case when the radii of the smaller arcs are exactly half of the big one. In the general case, it’s r - 2*r1, where r1 is the radius of the second arc (the first of the smaller ones). This means we can now create our path:
Next, we’re going to animate the shape of our path and the size and position of our two small circles using JavaScript. This means that the first thing we do is get these elements, get the radius R of the big circle and set a scaling factor F that gives us the minimum radius (RMIN) down to which the arcs can be scaled. We also set a total number of frames (T) and a unit angle (A).
const _P = document.querySelector('path'),
_C = document.querySelectorAll('circle'),
_SVG = document.querySelector('svg'),
R = -1*_SVG.getAttribute('viewBox').split(' ')[0],
F = .25, RMIN = F*R, RMAX = R - RMIN,
T = 120, A = 2*Math.PI/T;
The animation function is pretty much the same as in the canvas case. The only thing that’s different is the fact that now, in order to change the path shape, we change its d attribute and, in order to change the small circles’ radii and positions, we change their r and cx attributes. But everything else works exactly the same way:
There’s one more method of doing this, although it involves changing things like the path data from CSS, which is something only Blink browsers support at this point (and they’re not even matching the latest spec).
It’s also a bit breakable because we need to have the same radius value both in the SVG viewBox attribute and as a Sass variable.
- var r = 1500;
svg(viewBox=[-r, -r, 2*r, 2*r].join(' '))
path
circle
circle
We could access the value of this radius from the CSS, but only as a custom property, if we were to do something like this:
- var r = 1500;
svg(viewBox=[-r, -r, 2*r, 2*r].join(' '))
style :root { --r: #{r} }
However, while this may be very helpful in some cases, it is useless here, as we currently have no way of putting CSS variables into the path data string we build with Sass. So we’re stuck with having to set the same value both in the viewBox attribute and in the Sass code.
The basic styles are the same and we can create the path data with Sass in a way that’s similar to the Pug method:
For the two small circles, we set their radii and positions along the x axis. We also need to make sure one of them is white:
circle {
r: $r1/3;
cx: $r2;
&:nth-child(2) { fill: white }
&:nth-child(3) {
r: $r2/3;
cx: -$r1
}
}
We now have the static shape:
The static yin and yang shape (live demo, Blink only).
In order to get the effect we’re after, we need the following animations:
a morphing animation of the path shape, where the radius of the first of the smaller arcs goes from the minimum possible radius ($rmin: .25*$r) to the maximum possible one ($rmax: $r - $rmin) and then back, while the radius of the last arc goes from $rmax to $rmin and back again; this can be done with a keyframe animation from one extreme to the other and then using the alternate value for animation-direction
another alternating animation that scales the radius of the first small circle from $rmin/3 up to $rmax/3 and then back down to $rmin/3 again; the second small circle uses the same animation only delayed by the value of a normal animation-duration
a third alternating animation that moves the central points of the two small circles back and forth; in the case of the first (white) small circle, it moves from $rmax down to $rmin; in the case of the second (black) circle, it goes from -$rmin down to -$rmax; what we can do here to unify them is use a CSS variable as a switch (it only works in WebKit browsers, but setting the path data or the circle radii or offsets from the CSS doesn’t have better support either)
So let’s first see the morphing @keyframes. These are created by setting pretty much the same path data as before, only replacing $r1 with $rmin and $r2 with $rmax for the 0% keyframe and the other way around for the 100% one:
The three arcs shape morphing (live demo, Blink only).
Next step is to move on to scaling and moving the two small circles. The scaling @keyframes follow the same pattern as the morphing ones. The radius value is $rmin/3 at 0% and $rmax/3 at 100%:
circle { animation: s $t ease-in-out infinite alternate }
And now the radii of the two small circles are animated:
The three arcs shape morphing and the small circles scaling (live demo, Blink only).
It’s a start, but we have a number of problems here. First of all, the second small circle should decrease in size when the first one is growing bigger and the other way around. We fix this by setting an animation-delay that depends on a CSS variable we initially set to 0 and then switch to 1 on the second small circle:
circle {
--i: 0;
animation: s $t ease-in-out calc(var(--i)*#{$t}) infinite alternate
&:nth-child(3) { --i: 1 }
}
As mentioned before, using calc() as an animation-delay value only works in WebKit browsers, but setting r from the CSS has even poorer support, so the animation-delay is not the biggest problem we have here. The result can be seen below:
The three arcs shape morphing and the small circles scaling (live demo, Blink only).
This is much better, but we still nedd to animate the positions of the small circles along the x axis. The way we do this is with a set of @keyframes that make cx go from $rmax to $rmin and back again for the first small circle and from -$rmin to -$rmax and back for the second one. In these two cases, we have both a different order and a different sign, so we need to come up with a keyframe animation that satisfies both.
Getting around the order problem is the easy part – we use the same animation-delay as we did for the scaling radii animation.
But what about the sign? Well, we use our custom property --i again. This is 0 for the first small circle and 1 for the second one, so we need a function that takes in --i and gives us 1 when this variable’s value is 0 and -1 for a value of 1. The simplest one that comes to mind is raising -1 to the power --i. Sadly, that’s not possible with CSS – we can only have arithmetic operations inside calc(). However, calc(1 - 2*var(--i)) is another solution that works and it’s not much more complicated. Using this, our code becomes:
What we have looks like a sudden flip at 50% in between the two end values, not a smooth animation. This is not what we wanted, so it looks like we need to abandon this tactic.
We have another option here though: combining cx with transform. The two small circles are always positioned such that the distance between their central points is $r. So what we can do is position the second of the small circles at -$r, then translate them both by a distance that’s between $rmax and $rmin:
One more thing we can do here to simplify the code is get rid of the initial $r1 and $r2 values and replace them with those in the 0% keyframe of each animation:
The final step is to make the SVG element itself rotate infinitely:
svg { animation: r 2*$t linear infinite }
@keyframes r { to { transform: rotate(1turn) } }
The finished loading animation can be seen in this Pen.
So there you have it – one loading animation, four different methods of recreating it from scratch for the web. Not everything we’ve explored in here is usable in practice today. For example, the support for the last method is really poor and the performance is awful. However, exploring the limits of what’s becoming possible these days was a fun exercise and a great learning opportunity.
When you name lines, you can optionally name them as *-start and *-end and this gives you a little more grid magic. We get a named grid area of the main name used. Sounds odd? Take a look at the diagram below, it shows 4 named grid lines, main-start and main-end both for columns and rows. The area marked out by the intersection of these lines can now be referenced by the name main. If we had named the lines foo-start and foo-end then we would have a named area called foo.
Rachel’s post stood out to me for a number of reasons. First, I love blog posts as responses to other blog posts. Second, it’s an excellent reminder that sharing how a concept works is equally as important as showing that it works. Lastly, the concept of implicitly named grid areas based on named grid lines is as good a reason as any to roll up our sleeves and get cozy with the spec. In fact, following Rachel’s series on the CSS Grid spec is a good starting point.
As a side note, Tyler’s clever use of named Grid lines reminded me of Dave Rupert’s equally crafty use of :not to achieve a similar full bleed effect.
Filters are an interesting way to deal with data rendering in Vue but are only useful in a small amount of cases. The first thing to understand about filters is that they aren’t replacements for methods, computed values, or watchers, because filters don’t transform the data, just the output that the user sees. As of Vue 2.0, there are no built-in filters, we need to construct them ourselves.
We can use filters locally or globally, but it’s worth mentioning that if you declare a Vue filter globally it should come before the Vue instance. In both cases, we would pass the value in as a parameter.
//global
Vue.filter('filterName', function(value) {
return // thing to transform
});
//locally, like methods or computed
filters: {
filterName(value) {
return // thing to transform
}
}
Filters are used with a pipe, following the piece of data you’d like to be altered upon render. So we would show the piece of data we want to alter, followed by the filter
You can also use filters in v-bind directives rather than just the mustache template. Filters can also be chained. Keep in mind if you’re going to chain filters: ordering matters. The first filter will be applied first, the second will be applied to the completed first, and so on.
{{ data | filterA | filterB }}
We can also pass additional arguments into filters like so:
{{ data | filterName(arg1, arg2) }}
// locally, like methods or computed
filters: {
filterName(value, arg1, arg2) {
return //thing to transform
}
}
Now, you might think, based on the name, that filters would be great for forms when we want to show only some bits of data and not others. However, filters need to rerun on every single update, so if you have something like an input that updates every time you type, it’s not very performant. Better to use computed for something like this as it’s less overhead. The results will be cached based on their dependencies and won’t be rerun on every update. Computed properties will only be reevaluated when those dependencies change, but can also handle complex logic. This makes them excellent candidates for filtering information based on input. There are, however, circumstances where you do need to update based on changes in time, and for these instances, better to use a method.
Let’s take a look at building something using Firebase and React. We’ll be building something called Fun Food Friends, a web application for planning your next potluck, which hopefully feels like something rather „real world”, in that you can imagine using these technologies in your own production projects. The big idea in this app is that you and your friends will be able to log in and be able to see and post information about what you’re planning to bring to the potlock.
When we’re finished, it will look like this:
Our example app: Fun Food Friends
This article assumes you already have some basic knowledge of how React works and maybe built a few small apps with React. If you haven’t, I would recommend checking out a series like Wes Bos’ React for Beginners first before continuing on.
What is Firebase?
Google’s Firebase is a cloud-based database hosting service that will set up a database for you and host it, as well as offer you the tools to interact with it. You can use it to store and retrieve data in real time. That’s not all Firebase does, it can do more things like handle user authentication and store files, but we’ll be mainly focusing on data storage.
The data storage ability of Firebase make it a perfect fit for React. A persistent, real-time backend for your application to plug in to!
How does Firebase store data?
Firebase stores data as a giant object with key-value pairs. Unlike JSON or JavaScript objects, there are no arrays in Firebase.
A Firebase database might look something like this:
We’ll start by using the incredibly handy `create-react-app` package in order to quickly set up a new React project without having to worry about any build configuration. Open up your command line, and type the following:
This will boot up your app in the browser, and start a watch task in your terminal so that we can begin hacking away at the project. We’re also installing the `firebase` package here as we’ll need it for the next step.
Creating our Firebase Database
Now that our app is set up, we’ll need to create an account and database on Firebase so that we can link up our application to it.
This will take you to a page where you’ll be asked to authenticate with your Google account. Select the account that you’d like this project to be affiliated with, and press OK.
This should take you to the Firebase console, which looks something like this:
Now let’s create our project’s database. Click Add Project. Let’s call it „fun-food-friends” and press OK.
This will take you to your app’s dashboard, which looks like this:
Since we’ll be building a web app, select Add Firebase to your web app. This will trigger a popup with some code that looks like this:
Since we’ll be importing Firebase into our project using ES6 modules, we won’t need those script tags. That config object is important though: it’s how we authenticate our React application with our Firebase database.
Hooking up our App to Firebase
Copy that whole config object, and head back over to your React project. Find your `src` folder, and create a file called `firebase.js`. Inside of it, let’s import firebase, our config, and initialize our app:
One last thing we’ll need to do before we can dive into roughing out our App. We need to temporarily disable authentication requirements on our app so that we can add and remove items without needing to have any kind of user authentication flow.
From the Firebase Dashboard, on the left-hand side of the screen, you’ll notice that there is a Database tab. Click on it. Then, on the right-hand side, under the subheading Realtime Database, you’ll see a Rules tab. This will cause an object to appear that looks something like this:
We need to set .read and .write to both be equal to true, otherwise later, when we try to add data to our database from our application, Firebase won’t let us. When you’re finished, it should look something like this:
Make sure to click the Publish button.
And that’s all there is to hooking up our database! Anytime we need a component of our application to connect with our Firebase database, we simply need to import our firebase module and we’ll have direct reference to it.
Building out our App’s Rough Skeleton
Let’s build out a rough HTML skeleton for our application. We’ll build a simple form with two inputs:
A field where the user can submit their name
A field where the user can enter what food they’re bringing to the potluck.
Since our app is quite simple, we’ll keep everything inside of one main component, `App.js`. Open up `src/App.js`, and remove the `App` component, replacing it with this basic skeleton:
I’ve prepared a little bit of CSS for you to paste into the `App.css` file, just so that our app doesn’t look totally bland. If you want to grab it, just go here and copy and paste the raw contents you find there into your `src/App.css` file!
We’ll also need to embed a link to Google Fonts and Font Awesome, so go ahead and open up `public/index.html` and add the following lines below the favicon:
Before we can start adding data into our Firebase database, we need to connect our inputs to our component’s state, so that React can keep track of them.
First, let’s carve out some space in our component’s state – a space to keep track of the user using our app (username) and the item they intend to bring (currentItem). We’ll do this by creating a constructor() hook for our app and setting a default value for our input’s state there:
We’ll add a onChange event handlers to our inputs, as well as providing them with a value derived from our state (this is called a „controlled input”), like this:
<section className="add-item">
<form>
<input type="text" name="username" placeholder="What's your name?" onChange={this.handleChange} value={this.state.username} />
<input type="text" name="currentItem" placeholder="What are you bringing?" onChange={this.handleChange} value={this.state.currentItem} />
<button>Add Item</button>
</form>
</section>
And finally, we’ll create a catch-all handleChange method that receives the event from our inputs, and updates that input’s corresponding piece of state:
If you aren’t familiar with using brackets to dynamically determine key name in an object literal, check out the MDN docs on computed properties.
Since we’re using ES6 classes and need access to this in our handleChange method, we’ll also need to bind it back in our constructor() component like this:
If you now use the React DevTools to inspect your App component’s state, you’ll see that both of your inputs are now successfully hooked up and being tracked in your component’s state:
Adding a new Potluck Item to your Database
Now that we’re tracking our inputs, let’s make it so that we can add a new item to our database so that Firebase can keep track of it.
First we’ll need to connect to Firebase in order to do this, we’ll start by importing our firebase module that we created earlier. We’ll also delete the logo.svg import, since it’s just an unneeded part of the create-react-app boiler plate and will cause warnings if we don’t:
import React, { Component } from 'react';
import logo from './logo.svg'; // <--- remove this line
import './App.css';
import firebase from './firebase.js'; // <--- add this line
Once that’s done, we’ll need to make our 'Add Item’ button let Firebase know what we’d like to add to our database and where we’d like to put it.
First we’ll attach a submit event listener for our form, and have it call a handleSubmit method we’ll write in a minute:
<form onSubmit={this.handleSubmit}>
<input type="text" name="username" placeholder="What's your name?" onChange={this.handleChange} value={this.state.username} />
<input type="text" name="currentItem" placeholder="What are you bringing ?" onChange={this.handleChange} value={this.state.currentItem} />
<button>Add Item</button>
</form>
e.preventDefault() – we need to prevent the default behavior of the form, which if we don’t will cause the page to refresh when you hit the submit button.
const itemsRef = firebase.database().ref('items'); – we need to carve out a space in our Firebase database where we’d like to store all of the items that people are bringing to the potluck. We do this by calling the ref method and passing in the destination we’d like them to be stored (items).
const item = { /* .. */ } here we grab the item the user typed in (as well as their username) from the state, and package it into an object so we ship it off to our Firebase database.
itemsRef.push(item) similar to the Array.push method, this sends a copy of our object so that it can be stored in Firebase.
Finally this.setState({ currentItem: '', username:''}); is just so that we can clear out the inputs so that an additional item can be added.
Now try adding a new item, and hitting submit! If you don’t have any errors in your console, you should be able to head on over to the Firebase dashboard, where you’ll see something like this inside your Database tab:
If you click the little + next to items you’ll be able to look inside, like this:
That strange looking -Kk8lHSMqC5oP6Qai0Vx key you see is a programmatically generated key created by Firebase when we called the push method, but inside you’ll find whatever item you added to the Potluck.
You’ll notice that all of our records are stored as objects with properties that have the generated names you see above – just another quick reminder that there are no arrays in Firebase!
Try adding more items and see what happens.
Way to go! We’re almost there, but we still have one more step: getting our potluck items to appear on the page.
Retrieving our Potluck Items from the database
Just like in a traditional React app, we need to find some way to keep track of all of the potluck dishes so that we can display what people are planning to bring on to the page.
Without a database, this poses an issue, since every time we refresh the page any new dishes that were added to the potluck would get lost. But with Firebase, this is a snap to fix!
First, let’s create a variable called items inside of default state. This will eventually hold all of the potluck items that are currently being tracked inside of our Firebase database.
Next, we need to actually grab those items from our Firebase database so that we can store them into our state.
The Firebase API offers us an incredibly easy way to not only grab this kind information from our database, but also to update us when new values get added to our database. It accomplishes this using the value custom event listener.
The callback here, which we’ve called snapshot, provides you with a bird’s eye overview of the items ref inside of your database. From here, you can easily grab a list of all of the properties inside of that items ref, using the .val() method which you can call on the snapshot.
This value automatically fires on two occassions:
Any time a new item is added or removed from our items reference inside of our database
The first time the event listener is attached
This makes it especially useful for initially grabbing a list of all of the items inside of our database, and then subsequently tracking when new items get added and removed.
We’ll attach this event listener inside of our componentDidMount, so that we start tracking our Potluck items as soon as our component loads on to the page:
componentDidMount() {
const itemsRef = firebase.database().ref('items');
itemsRef.on('value', (snapshot) => {
let items = snapshot.val();
let newState = [];
for (let item in items) {
newState.push({
id: item,
title: items[item].title,
user: items[item].user
});
}
this.setState({
items: newState
});
});
}
Here, we instantiate a new array and populate it with the results that come back from our value listener. We for…in over each key, and push the result into an object inside our newState array. Finally, once all the keys are iterated over (and therefore all items are grabbed from our database), we update the state with this list of items from our database.
Inspect your App using the React Dev Tools – you’ll notice that you now have an items property inside of your state with all of the items people have submitted for your potluck!
Displaying Potluck Items on the Page
Now let’s get these potluck items to actually display on the page. This is relatively easy, now that we have a list of all of our items being grabbed from Firebase and stored inside of our state. We just map over it and print the results on to the page, like so:
Try adding a new item through your form. You’ll notice that it automatically causes a new list item to appear on the page!
It’s not magic, Firebase’s value event is firing when you push the new item into your database, and sending back a new snapshot with a list of all of the items currently in your database, which ultimate updates your component through a setState which triggers a re-render and displays the new item on the page.
But we digress. There’s still one more step! We need to make it so that we can remove an item from the page.
Removing Items from the Page
We’ll need to create a new method on our component for this: removeItem. This method will need to be passed that unique key which serves as the identifier for each one of the items inside of our Firebase database.
Here, instead of grabbing all of the items as we did before when adding a new item, we instead look up a specific item by its key (that strange –Kk8lHSMqC5oP6Qai0Vx key from before). We can then call firebase.database()’s remove method, which strips it from the page.
Finally, we’ll need to add a button to our UI with an onClick that calls our removeItem method and passes it the item’s key, like follows:
And that’s all there is to it! Just like our addItem method, our UI and component state automatically update when an item is removed from the database.
Here’s what our completed `App.js` should look like:
Now you can truly see how Firebase and React play beautifully together. Firebase’s ability to persist data on the fly, coupled with React’s component lifecycle, makes for an incredibly simple and powerful way to quickly build up simple applications.
This article just scratches the surface of what the Firebase API can provide us. For example, with just a few more steps (and perhaps we will go over this in a future article), it would be incredibly easy to expand this application so that users could log in and out, be able to have a display photo next to the item that they are bringing, and only be able to remove their own items.
I’m sure most of you understand how you work with a framework like Bootstrap, Foundation, or Materialize. You use their CSS and JavaScript. You also use their chunks of HTML, piecing together and applying classes as needed to do what you need to do.
You’re on your own piecing the HTML together. That’s good, because it’s flexible. People use frameworks like this in all kinds of CMS’s and backend systems. But what if you want to apply some structure to this, making actual components out of the components given to you in the framework?
We could abstract that into a reusable component by:
Pass in the type of alert (second half of the second class)
Pass in the content inside the alert
I’m sure you could imagine doing that in the backend or templating language of your choice. A single PHP file in which you set variables representing those things before you include it. A Rails partial in which you pass locals to it. A literal React component in JSX where you pass the stuff as props. This kind of thing makes these patterns a lot easier to reuse.
Morgan did this with Nunjucks:
{% macro alert(class="success", text="<strong>Well done!</strong> You successfully read this important alert message.") %}
<div class="alert alert-{{ class }}" role="alert">
{{ text | safe }}
</div>
{% endmacro %}
I think this is super compelling and the kind of thing we’ll be doing more and more as design systems are becoming more of a standard practice.
My favorite kind of software products are the ones that very clearly make life simpler. Being able to legally sign a document by clicking a button in an email and squiggling my mouse to make my signature is definitely one of those things.
You can provide that to your users with HelloSign! You can set up your documents there (it supports all the formats you’d need, like PDF, Microsoft Word, Powerpoint, etc) and start collecting the signatures you need very easily. Set up templates of your commonly used documents. Make sure your branding is present during the signing process. Get notifications when documents are reviewed and signed.
There are a bunch more killer features you should be aware of. For example, like I mentioned, you can sign documents without ever leaving your email with their Chrome browser extension for Gmail. Same with Google Docs and Salesforce!
Perhaps most importantly, you can use HelloSign right from your own interface through their API. That’s great for all us developers interested in building seamless useful experiences right in our own products. You can embed documents directly on your website with just a few lines of code!
Last month Chris Coyier wrote a post investigating the question, „When Does a Project Need React?” In other words, when do the benefits of using React (acting as a stand-in for data-driven web frameworks in general), rather than server-side templates and jQuery, outweigh the added complexity of setting up the requisite tooling, build process, dependencies, etc.? A week later, Sacha Greif wrote a counterpoint post arguing why you should always use such a framework for every type of web project. His points included future-proofing, simplified workflow from project to project (a single architecture; no need to keep up with multiple types of project structures), and improved user experience because of client-side re-rendering, even when the content doesn’t change very often.
In this pair of posts, I delve into a middle ground: writing reactive-style UI’s in plain old JavaScript – no frameworks, no preprocessors.
Article Series:
Pure Functional Style (You are here!)
Class Based Components (Coming soon!)
There are two very different ways to write React components.
You can write them as classes. Stateful objects with lifecycle hooks and internal data.
Or, you can write them as functions. Just a piece of HTML that gets constructed and updated based on parameters that are passed in.
The former is often more useful for large, complex applications with lots of moving parts, while the latter is a more elegant way to display information if you don’t have a lot of dynamic state. If you’ve ever used a templating engine like Handlebars or Swig, their syntax looks pretty similar to function-style React code.
In this pair of posts, our target use case is websites that might otherwise be static, but would benefit from JavaScript-based rendering were it not for the overhead of setting up a framework like React. Blogs, forums, etc. Therefore, this first post will focus on the functional approach to writing a component-based UI, because it’ll be more practical for that type of scenario. The second post will be more of an experiment; I’ll really push the limit on how far we can take things without a framework, trying to replicate React’s class-based component pattern as closely as possible with only Vanilla JavaScript, probably at the expense of some practicality.
About functional programming
Functional programming has soared in popularity over the last couple years, driven primarily by Clojure, Python, and React. A full explanation of functional programming is outside the scope of this post, but the part that’s relevant to us right now is the concept of values that are functions of other values.
Let’s say your code needs to represent the concept of a rectangle. A rectangle has width and height, but it also has area, perimeter, and other attributes. At first, one might think to represent a rectangle with the following object:
But, it would quickly become apparent that there’s a problem. What happens if the width gets changed? Now we have to also change the area and perimeter, or they’d be wrong. It’s possible to have conflicting values, where you can’t just change one without the possibility of having to update something else. This is called having multiple sources of truth.
In the rectangle example, the functional programming-style solution is to make area and perimeter into functions of a rectangle:
This way, if width or height changes, we don’t have to manually modify anything else to reflect that fact. The area and perimeter just are correct. This is called having a single source of truth.
This idea is powerful when you substitute the rectangle with whatever data your application may have, and the area and perimeter with HTML. If you can make your HTML a function of your data, then you only have to worry about modifying data – not DOM – and the way it gets rendered on the page will be implicit.
UI components as functions
We want to make our HTML a function of our data. Let’s use the example of a blog post:
var blogPost = {
author: 'Brandon Smith',
title: 'A CSS Trick',
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};
function PostPage(postData) {
return '<div class="page">' +
'<div class="header">' +
'Home' +
'About' +
'Contact' +
'</div>' +
'<div class="post">' +
'<h1>' + postData.title + '</h1>' +
'<h3>By ' + postData.author + '</h3>' +
'<p>' + postData.body + '</p>' +
'</div>' +
'</div>';
}
document.querySelector('body').innerHTML = PostPage(blogPost);
Okay. We’ve made a function of a post object, which returns an HTML string that renders our blog post. It’s not really „componentized” though. It’s all one big thing. What if we also wanted to render all our blog posts in a sequence on the home page? What if we wanted to reuse that header across different pages? Luckily, it’s really easy to build functions out of other functions. This is called composing functions:
var blogPost = {
author: 'Brandon Smith',
title: 'A CSS Trick',
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};
function Header() {
return '<div class="header">' +
'Home' +
'About' +
'Contact' +
'</div>';
}
function BlogPost(postData) {
return '<div class="post">' +
'<h1>' + postData.title + '</h1>' +
'<h3>By ' + postData.author + '</h3>' +
'<p>' + postData.body + '</p>' +
'</div>';
}
function PostPage(postData) {
return '<div class="page">' +
Header() +
BlogPost(postData) +
'</div>';
}
function HomePage() {
return '<div class="page">' +
Header() +
'<h1>Welcome to my blog!</h1>' +
'<p>It\'s about lorem ipsum dolor sit amet, consectetur ad...</p>' +
'</div>';
}
document.querySelector('body').innerHTML = PostPage(blogPost);
That’s so much nicer. We didn’t have to duplicate the header for the home page; we have a single source of truth for that HTML code. If we wanted to display a post in a different context, we could do so easily.
Prettier syntax with template literals
Okay, but all those plus signs are horrible. They’re a pain to type, and they make it harder to read what’s going on. There has to be a better way, right? Well, the folks at W3C are way ahead of you. They created template literals – which, while still relatively new, have pretty good browser support at this point. Simply wrap your string in backticks instead of quotes, and it will get a couple of extra superpowers.
The first superpower is the ability to span multiple lines. So our BlogPost component up above can become:
That’s nice. But the other power is even nicer: variable substitution. Variables (or any JavaScript expression, including function calls!) can be inserted directly into the string if they’re wrapped in ${ }:
Much better. It almost looks like JSX now. Let’s see our full example again, with template literals:
var blogPost = {
author: 'Brandon Smith',
title: 'A CSS Trick',
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};
function Header() {
return `<div class="header">
HomeAboutContact
</div>`;
}
function BlogPost(postData) {
return `<div class="post">
<h1>${postData.title}</h1>
<h3>By ${postData.author}</h3>
<p>${postData.body}</p>
</div>`;
}
function PostPage(postData) {
return `<div class="page">
${Header()}
${BlogPost(postData)}
</div>`;
}
function HomePage() {
return `<div class="page">
${Header()}
<h1>Welcome to my blog!</h1>
<p>It's about lorem ipsum dolor sit amet, consectetur ad...</p>
</div>`;
}
document.querySelector('body').innerHTML = PostPage(blogPost);
More than just filling in blanks
So we can fill in variables, and even other components through functions, but sometimes more complex rendering logic is necessary. Sometimes we need to loop over data, or respond to a condition. Let’s go over some JavaScript language features that make it easier to do more complex rendering in a functional style.
The ternary operator
We’ll start with the simplest logic: if-else. Of course, since our UI components are just functions, we could use an actual if-else if we wanted to. Let’s see what that would look like:
var blogPost = {
isSponsored: true,
author: 'Brandon Smith',
title: 'A CSS Trick',
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};
function BlogPost(postData) {
var badgeElement;
if(postData.isSponsored) {
badgeElement = `<img src="badge.png">`;
} else {
badgeElement = '';
}
return `<div class="post">
<h1>${postData.title} ${badgeElement}</h1>
<h3>By ${postData.author}</h3>
<p>${postData.body}</p>
</div>`;
}
That’s… not ideal. It adds a whole lot of lines for something that isn’t that complicated, and it separates part of our rendering code from its place within the rest of the HTML. This is because a classical if-else statement decides which lines of code to run, rather than which value to evaluate to. This is an important distinction to understand. You can only stick an expression into a template literal, not a series of statements.
The ternary operator is like an if-else, but for an expression instead of a set of statements:
It takes the form [conditional] ? [valueIfTrue] : [valueIfFalse]. So, the blog post example above becomes:
var blogPost = {
isSponsored: true,
author: 'Brandon Smith',
title: 'A CSS Trick',
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};
function BlogPost(postData) {
return `<div class="post">
<h1>
${postData.title} ${postData.isSponsored ? '<img src="badge.png">' : ''}
</h1>
<h3>By ${postData.author}</h3>
<p>${postData.body}</p>
</div>`;
}
Much better.
Array.map()
On to loops. Anytime we have an array of data that we want to render, we’re going to need to loop over those values to generate the corresponding HTML. But if we used a for-loop, we’d run into the exact same issue we had with the if-else statement above. A for loop doesn’t evaluate to a value, it executes a series of statements in a certain way. Luckily, ES6 added some very helpful methods to the Array type which serve this specific need.
Array.map() is an Array method that takes a single argument, which is a callback function. It loops over the array it’s called on (similar to Array.forEach()), and calls the supplied callback once for each item, passing the array element to it as an argument. The thing that makes it different from Array.forEach() is that the callback is supposed to return a value – presumably one that’s based on the corresponding item in the array – and the full expression returns the new array of all the items returned from the callback. For example:
var myArray = [ 'zero', 'one', 'two', 'three' ];
// evaluates to [ 'ZERO', 'ONE', 'TWO', 'THREE' ]
var capitalizedArray = myArray.map(function(item) {
return item.toUpperCase();
});
You might be able to guess why this is so useful for what we’re doing. Earlier we established the concept of a value being a function of another value. Array.map() allows us to get an entire array, for which each item is a function of the corresponding item in another array. Let’s say we have an array of blog posts that we want to display:
function BlogPost(postData) {
return `<div class="post">
<h1>${postData.title}</h1>
<h3>By ${postData.author}</h3>
<p>${postData.body}</p>
</div>`;
}
function BlogPostList(posts) {
return `<div class="blog-post-list">
${posts.map(BlogPost).join('')}
</div>`
}
var allPosts = [
{
author: 'Brandon Smith',
title: 'A CSS Trick',
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
},
{
author: 'Chris Coyier',
title: 'Another CSS Trick',
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
},
{
author: 'Bob Saget',
title: 'A Home Video',
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
}
]
document.querySelector('body').innerHTML = BlogPostList(allPosts);
Each object containing the info for a single blog post is passed, one by one, to the BlogPost function, and the returned HTML strings are placed into a new array. We then just call join() on that new array to combine the array of strings into a single string (separated by an empty string), and we’re done. No for-loops, just a list of objects converted to a list of HTML elements.
Re-rendering
We can now implicitly generate HTML for given data, in a way that’s reusable and composable, all within the browser. But, how do we update when the data changes? How do we even know when to trigger an update? This subject is one of the most complex and hotly debated in the JavaScript framework community today. Making large numbers of DOM updates efficiently is an amazingly difficult problem, one which engineers at Facebook and Google have spent years working on.
Luckily, our proverbial website is just a blog. The content pretty much only changes when we look at a different blog post. There aren’t a ton of interactions to detect, we don’t have to optimize our DOM operations. When we load a new blog post, we can just scrap the DOM and rebuild it.
We could make this a little nicer by wrapping it in a function:
function update() {
document.querySelector('body').innerHTML = PostPage(postData);
}
Now whenever we load a new blog post, we can just call update() and it will appear. If our application were complicated enough that it needed to re-render frequently – maybe a couple times per second in certain situations – it would get choppy really fast. You could write complex logic to figure out which sections of the page truly need to update given a particular change in data and only update those, but that’s the point where you should just use a framework.
Not just for content
At this point pretty much all our rendering code has been used to determine the actual HTML and text content inside the elements, but we don’t have to stop there. Since we’re just creating an HTML string, anything inside there is fair game. CSS classes?
Check. Feel free to get really creative with this. Think about your data, and think about how all the different aspects of it should be represented in markup, and write expressions that turn one into the other.
Summary
Hopefully, this post gives you a good set of tools for writing simple reactive, data-driven web interfaces without the overhead of any tools or frameworks. This type of code is far easier to write and maintain than jQuery spaghetti, and there’s no hurdle at all to using it right now. Everything we’ve talked about here comes free with all reasonably modern browsers, without so much as a library.
Part 2 will focus on class-based, stateful components, which will get near the territory of too-complicated-to-reasonably-do-in-VanillaJS. But by golly, we’re going to try anyway, and it’s going to be interesting.
I think, as front-end developers, we’re well aware that different browsers (and versions) support different things. We make choices based on web features we’d like to use balanced with what statistics tell us about our users and what browsers they use. Only 0.01% of users left on IE 9, we might see from our Google Analytics, OK, let’s start using Flexbox and .classList.
Enter Autoprefixer. Autoprefixer, over time, became a nearly ubiquitously part of CSS build processes because it helped us with cross browser support almost effortlessly. Even though IE 10 only supported an older Flexbox syntax, we didn’t have to worry about that because Autoprefixer did it’s best to port the modern syntax to the older one, and it did a great job at that.
Autoprefixer allows you to configure what browsers you wanted to target with the prefixing. This means you don’t have to generate prefixes for every browser ever (resulting in potentially more code output than you want) but instead only generate prefixes for the browsers you’ve decided to support. There are lots of ways to use Autoprefixer, but let’s say it’s a part of your Grunt build:
As you might guess, that Autoprefixer configuration will process based on the last 2 versions of all major browsers as well as specifically do what is needed for IE 8 and IE 9.
That’s great, but Autoprefixer isn’t the only tool out there making choices about browser versions.
Indeed.
I bet many of you have worked with Babel or at least heard of it. You write as modern of JavaScript as you like, and it processes it into JavaScript that will run in older browsers. There is a project called babel-preset-env which allows you to configure what browsers Babel will compile down to. For example:
There we are specifically targeting Edge 15. Just as one little example, Babel won’t even bother converting anything in const a = `string`; because Edge 15 supports const and `backticks`. But if we told it to also target IE 10, we’d get var a = "string";.
Browserslist is about a single configuration for all tools that need to know what browsers you support.
We just looked at two major tools that can be configured based on which browsers to support: Autoprefixer and Babel. Doesn’t it make sense to be targeting the same list of browsers? (Yes.)
That’s an example of having the configuration stored in your `package.json` file. There are other ways to make sure a Browserslist configuration is available though, like having a BROWSERSLIST environment variable or a `.browserslistrc` config dotfile.
There are other interesting tools using Browserslist.
For example, your linting setup can be configured to warn you if you use code that isn’t supported outside of your Browserslist setup. This is done with the eslint-plugin-compat plugin for ESLint.
Those things feel like a natural extension of Browserslist, and it’s really cool they exist already. Perhaps slightly more surprising is PostCSS Normalize, which actually builds a CSS „reset” (Normalize isn’t really a reset, but you know what I mean, it handles cross browser differences in CSS) based on the browsers your target.
One of the reasons I wanted to share this approach was to see what kind of response it would generate. Since then I’ve received some interesting feedback from other developers, with some raising valid shortcomings about this approach that would have never otherwise occurred to me.
In this article, I’ll be providing some solutions to these shortcomings, as well as baking in more features and general improvements to make our reusable function even more powerful.
Managing State in CSS with Reusable JavaScript Functions (You are here!)
For reference, here’s the JavaScript from Part 1 for our reusable function as it stands:
// Grab all elements with required attributes
var elems = document.querySelectorAll("[data-class][data-class-element]");
// closestParent helper function
closestParent = function(child, match) {
if (!child || child == document) {
return null;
}
if (child.classList.contains(match) || child.nodeName.toLowerCase() == match) {
return child;
}
else {
return closestParent(child.parentNode, match);
}
}
// Loop through if any are found
for(var i = 0; i < elems.length; i++){
// Add event listeners to each one
elems[i].addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Grab classes list and convert to array
var dataClass = this.getAttribute('data-class');
dataClass = dataClass.split(", ");
// Grab linked elements list and convert to array
var dataClassElement = this.getAttribute('data-class-element');
dataClassElement = dataClassElement.split(", ");
// Grab data-class-behaviour list if present and convert to array
if(this.getAttribute("data-class-behaviour")) {
var dataClassBehaviour = this.getAttribute("data-class-behaviour");
dataClassBehaviour = dataClassBehaviour.split(", ");
}
// Grab data-scope list if present and convert to array
if(this.getAttribute("data-class-scope")) {
var dataClassScope = this.getAttribute("data-class-scope");
dataClassScope = dataClassScope.split(", ");
}
// Loop through all our dataClassElement items
for(var b = 0; b < dataClassElement.length; b++) {
// Grab elem references, apply scope if found
if(dataClassScope && dataClassScope[b] !== "false") {
// Grab parent
var elemParent = closestParent(this, dataClassScope[b]),
// Grab all matching child elements of parent
elemRef = elemParent.querySelectorAll("." + dataClassElement[b]);
// Convert to array
elemRef = Array.prototype.slice.call(elemRef);
// Add parent if it matches the data-class-element and fits within scope
if(dataClassScope[b] === dataClassElement[b] && elemParent.classList.contains(dataClassElement[b])) {
elemRef.unshift(elemParent);
}
}
else {
var elemRef = document.querySelectorAll("." + dataClassElement[b]);
}
// Grab class we will add
var elemClass = dataClass[b];
// Grab behaviour if any exists
if(dataClassBehaviour) {
var elemBehaviour = dataClassBehaviour[b];
}
// Do
for(var c = 0; c < elemRef.length; c++) {
if(elemBehaviour === "add") {
if(!elemRef[c].classList.contains(elemClass)) {
elemRef[c].classList.add(elemClass);
}
}
else if(elemBehaviour === "remove") {
if(elemRef[c].classList.contains(elemClass)) {
elemRef[c].classList.remove(elemClass);
}
}
else {
elemRef[c].classList.toggle(elemClass);
}
}
}
});
}
Going forward, this is going to serve as the base for our improvements.
Let’s get started!
Accessibility
The most common piece of feedback I’ve received from other developers in response to Part 1 was the approach’s lack of consideration for accessibility. More specifically, it’s lack of support for ARIA attributes (or ARIA states if you prefer) and its failure to provide keyboard events for triggering our reusable function.
Let’s see how we can integrate both.
ARIA attributes
ARIA attributes form part of the WAI-ARIA specification. In the words of the specification they…
…are used to support platform accessibility APIs on various operating system platforms. Assistive technologies may access this information through an exposed user agent DOM or through a mapping to the platform accessibility API. When combined with roles, the user agent can supply the assistive technologies with user interface information to convey to the user at any time. Changes in states or properties will result in a notification to assistive technologies, which could alert the user that a change has occurred.
Revisiting the accordion example from part 1, an aria-expanded attribute set to true when the component is expanded, and vice versa when at its default state, would allow assistive technologies such as screen readers to better assess the component.
In addition to providing these benefits, as Ben Frain explores in his article, we can drop stateful classes and instead rely on ARIA attributes as our CSS hooks for styling some component state:
Adopting this approach results in what is (cringingly) referred to as a „Win Win” situation. We get to improve the accessibility of our web application, while also gaining a clearly defined, well-considered lexicon for communicating the states we need in our application logic.
Coming back to our reusable function, we’ll build in support so the data-class attribute can also accept an ARIA attribute reference. Since we’re now manipulating attributes rather than just classes, it would make sense semantically to rename data-class and all of its associated attributes to data-state:
In the above example, clicking c-mycomponent should toggle aria-expanded on itself. Whilst in the below example, in addition to the previous behaviour my-class would be removed from c-myothercomponent.
Our reusable function currently assumes that everything passed to it via our newly renamed data-state attribute is a class. It then acts accordingly based on either what’s defined in data-state-behaviour or its default toggle behaviour:
// Cycle through target elements
for(var c = 0; c < elemRef.length; c++) {
if(elemBehaviour === "add") {
if(!elemRef[c].classList.contains(elemClass)) {
elemRef[c].classList.add(elemClass);
}
}
else if(elemBehaviour === "remove") {
if(elemRef[c].classList.contains(elemClass)) {
elemRef[c].classList.remove(elemClass);
}
}
else {
elemRef[c].classList.toggle(elemClass);
}
}
To support ARIA attributes, we’ve simply added a check to first see if the given ARIA attribute exists on the element, and if not, assume it’s a class and process it as before. This way we can support both ARIA attributes and classes to cover all eventualities. Also removed are the classList.contains() checks, as in current spec classList.add() and classList.remove() are smart enough to account for this.
Keyboard Events
For a website to be considered accessible, it’s important that it can be easily navigated and interacted with through just the use of a keyboard. As far as the developer is concerned, this often involves the usage of the tabindex attribute and leveraging keyboard events.
In most browsers, elements such as the anchor already have these properties by default. You can tab to them and when in focus they can be activated on the press of the enter key. However for many components built with a combination of semantic elements and divs, this is not the case.
Let’s make our reusable function pick up this slack by writing logic to automatically add a keyboard event to the trigger element so it can be activated – like an anchor – with a press of the enter key.
Implementation
At the moment, as the function logic is triggered by clicking an element with data-state and data-state-element attributes, everything is wrapped in a click event listener:
elems[i].addEventListener("click", function(e){
// Function logic
});
As a press of the enter key is going to need to trigger the same function logic as a click, it makes sense to seperate out this logic into it’s own function so it can be triggered from either. We’ll call it processChange():
// Assign click event
elem.addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Run state function
processChange(this);
});
// Add keyboard event for enter key to mimic anchor functionality
elem.addEventListener("keypress", function(e){
// e.which refers to the key pressed, 13 being the enter key.
if(e.which === 13) {
// Prevent default action of element
e.preventDefault();
// Run state function
processChange(this);
}
});
In addition to the existing click event listener, we’ve added an extra listener to react when the enter key is pressed. When a matching keypress event occurs on a focused trigger element, it’s just a matter of running our new processChange() function and passing the element.
You will also notice there’s no logic to automatically add a tabIndex attribute. This is because it may conflict with any tabIndex hierarchy already defined on the page and interfere with developer intent.
Example
Here’s a modified version of the accordion example from part 1, but fully updated to leverage ARIA attributes and keyboard events to make it a more accessible component. You can see the full reusable function as it now stands in the JavaScript panel.
In Part 1, an issue was raised in the comments section:
I think this would have some issues for elements added to the DOM at a later point. In that case you would need to repeat assigning the click event. Am I correct?
That is correct! Any elements with data-state and data-state-element attributes added after the initial render of the DOM won’t have any event listeners assigned to them. So when they are clicked or swiped, nothing will happen.
Why? This is because in our Javascript, once the initial round of assigning event listeners to elements with data-state and data-state-element is complete, there’s no functionality in place to say „Hey! Watch out for any new elements withdata-state and data-state-element attributes and make them work.”
Implementation
To fix this, we’ll leverage something called MutationObservers. Whilst they can be explained much better in David Walsh’s great overview of the API, MutationObservers essentially allow us to track any nodes added or removed from the DOM (also known as „DOM mutations”).
We can setup one up like so:
// Setup mutation observer to track changes for matching elements added after initial DOM render
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
for(var d = 0; d < mutation.addedNodes.length; d++) {
// Check if we're dealing with an element node
if(typeof mutation.addedNodes[d].getAttribute === 'function') {
if(mutation.addedNodes[d].getAttribute("data-state") && mutation.addedNodes[d].getAttribute("data-state-element")) {
// Create click and keyboard event listeners etc
}
}
}
});
});
// Define type of change our observer will watch out for
observer.observe(document.body, {
childList: true,
subtree: true
});
This is what our MutationObserver is doing:
Recording any DOM mutation to the body element which are immediate children childList: true or its decendents subtree: true
Checking if that DOM mutation is a new element node, rather than a text node
If true, then check if the new element node has data-state and data-state-element attributes
The next step, assuming these 3 checks pass, would be to setup our click and keypress event listeners. Like with the implementation of keyboard events, let’s seperate out this setup logic into its own function so we can reuse it both on page load and when an element with data-state and data-state-element attributes is detected by our MutationObserver.
We’ll call this new function initDataState().
// Init function
initDataState = function(elem){
// Add event listeners to each one
elems.addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Run state function
processChange(this);
});
// Add keyboard event for enter key to mimic anchor functionality
elems.addEventListener("keypress", function(e){
if(e.which === 13) {
// Prevent default action of element
e.preventDefault();
// Run state function
processChange(this);
}
});
}
Then it’s just a matter of hooking everything up correctly:
// Run when DOM has finished loading
document.addEventListener("DOMContentLoaded", function() {
// Grab all elements with required attributes
var elems = document.querySelectorAll("[data-state][data-state-element]");
// Loop through if any are found
for(var a = 0; a < elems.length; b++){
initDataState(elems[a]);
}
// Setup mutation observer to track changes for matching elements added after initial DOM render
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
for(var d = 0; d < mutation.addedNodes.length; d++) {
// Check if we're dealing with an element node
if(typeof mutation.addedNodes[d].getAttribute === 'function') {
if(mutation.addedNodes[d].getAttribute("data-state")) {
initDataState(mutation.addedNodes[d]);
}
}
}
});
});
// Define type of change our observer will watch out for
observer.observe(document.body, {
childList: true,
subtree: true
});
});
Example
Click the „Add” button to insert more elements into the page (example Pen below):
At the moment, our reusable function is using click and keyboard events to trigger state logic. This is fine at desktop level, but on touch devices for some UI components (Such as closing a sliding navigation menu for instance) it’s often more useful to have this logic trigger on detection of a swipe.
Let’s build in optional swipe support for our reusable function. This will require adding a new data attribute to compliment our existing set:
data-state-swipe
The purpose of this new attribute is to allow us to define the swipe direction which should trigger our state logic. These directions should be:
up
right
down
left
Let’s also build in the option to specify if or not the swipe event should replace the click event, or if both should coexist. We can add a comma separated boolean to data-state-swipe to trigger this behaviour:
false – Swipe event listener and click event listener are both added (Default)
So for example, when the div below detects a left swipe, the aria-expanded attribute on js-elem would be changed to true. The swipe event listener would also in this instance replace the click event listener, as we’re passing true in data-state-swipe:
Swipe is managed in just the same way as you would manage clicks and keyboard input – via event listeners. To keep the article focused on our reusable function, I’ll be using a helper function called swipeDetect() which will handle all the calculations required for accurate swipe detection. However feel free to use your own preferred method of detecting swipe direction in place of it.
We’re building swipe into our reusable function as another way of triggering function logic, so it makes sense that it should sit with the click and keyboard event listeners in initDateState() and then trigger processChange() once our requirements for a desired swipe direction are met.
Though we also have to account for the behaviour flag passed in data-state-swipe that determines if swipe should replace click. Let’s refactor initDataState() to add some scaffolding to properly support all of this:
// Init function
initDataState = function(elem){
// Detect data-swipe attribute before we do anything, as its optional
// If not present, assign click event like before
if(elem.getAttribute("data-state-swipe")){
// Grab swipe specific data from data-state-swipe
var elemSwipe = elem.getAttribute("data-state-swipe"),
elemSwipe = elemSwipe.split(", "),
swipeDirection = elemSwipe[0],
elemSwipeBool = elemSwipe[1],
currentElem = elem;
// If the behaviour flag is set to "false", or not set at all, then assign our click event
if(elemSwipeBool === "false" || !elemSwipeBool) {
// Assign click event
elem.addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Run state function
processChange(this);
});
}
// Use our swipeDetect helper function to determine if the swipe direction matches our desired direction
swipeDetect(elem, function(swipedir){
if(swipedir === swipeDirection) {
// Run state function
processChange(currentElem);
}
})
}
else {
// Assign click event
elem.addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Run state function
processChange(this);
});
}
// Add keyboard event for enter key to mimic anchor functionality
elem.addEventListener("keypress", function(e){
if(e.which === 13) {
// Prevent default action of element
e.preventDefault();
// Run state function
processChange(this);
}
});
};
These amends to initDataState now give it 3 different outcomes:
If there’s a data-state-swipe attribute on the trigger element, and its behaviour boolean is set to true, then only swipe and keyboard events are assigned.
If there’s a data-state-swipe attribute on the trigger element, but its behaviour boolean is set to false, then swipe, click and keyboard events are all assigned.
If there’s nodata-state-swipe attribute on the trigger element altogether, only click and keyboard event listeners are assigned.
Example
Here’s a very barebones example of the new swipe functionality in practice. Click the button to toggle the menu and then swipe right on the menu whilst on a touch device (or your preferred browser inspector) to close it. Simple.
Finally, we’ll be looking into ways we can refine our reusable function to make it more efficient and easier to use.
Targeting the trigger element
Say for example I have an element named c-btn which on click would need to toggle aria-pressed on itself. With our reusable function as it stands, the HTML would look something like this:
The problem here is that on click, aria-pressed would be toggled on all instances of c-btn everywhere, which isn’t the behaviour we’re looking for.
This was the problem which data-state-scope was created to resolve. By scoping our data-state instance to the nearest c-btn (which in this case would be itself) then we are creating the desired toggle behaviour.
Whilst the above snippet works fine, it’s a bit jarring having all these attributes all referencing the same c-btn element. Ideally if data-state-element and data-state-scope aren’t defined, then the function should default to the element triggering it. This would allow easy targeting of our trigger element. Like so:
data-scope-element is currently a required attribute. If it isn’t present, the function will not be able to assign any event listeners. This is because in our reusable function as it stands, the initial scan of the document is looking for elements with both the data-scopeanddata-scope-element attributes:
// Grab all elements with required attributes
var elems = document.querySelectorAll("[data-state][data-state-element]");
We need to tweak this so we’re only looking for elements with data-state, as data-state-element will shortly be relegated to an optional attribute.
// Grab all elements with required attributes
var elems = document.querySelectorAll("[data-state]");
In addition, we need to add an if-statement to processChange() which wraps around the retrieval of the data-state-element value, as if it’s not present, the function will throw an error when trying to call getAttribute() on something which doesn’t exist.
// Grab data-state-element list and convert to array
if(elem.getAttribute("data-state-element")) {
var dataStateElement = elem.getAttribute("data-state-element");
dataStateElement = dataStateElement.split(", ");
}
Next, let’s implement the logic which makes data-state-element and data-state-scope default to the trigger element if they are not explicitly defined. We can build on our previous amends to processChange() and add an else block to our data-state-element check to manually declare our target element and scope.
// Grab data-state-element list and convert to array
// If data-state-element isn't found, pass self, set scope to self if none is present, essentially replicating "this"
if(elem.getAttribute("data-state-element")) {
var dataStateElement = elem.getAttribute("data-state-element");
dataStateElement = dataStateElement.split(", ");
}
else {
var dataStateElement = [];
dataStateElement.push(elem.classList[0]);
if(!dataStateScope) {
var dataStateScope = dataStateElement;
}
}
Another consequence of making data-state-element no longer required is that in processChange(), it’s length is used in the for loop to make sure all the elements defined in data-state-element receive their changes of state. This is the loop as it stands:
// Loop through all our dataStateElement items
for(var b = 0; b < dataStateElement.length; b++) {
[...]
}
Thankfully, all we need to do here is swap out our now optional data-state-element attribute for our still required data-state element as the base for this loop.
// Loop through all our dataStateElement items
for(var b = 0; b < dataState.length; b++) {
[...]
}
This is because in instances where multiple values have been passed to each data-state attribute (For example: <div data-state="is-active, is-disabled" data-state-element="my-elem, my-elem-2" data-state-behaviour="add, remove">) the length of the array which is derived from each of these is always going to match, so we’re always going to get the same amount of loops in the for block.
Simplifying repeated values
Another improvement which we could make relates to assigning similar types of logic in a single data-state use. Consider the below example:
Whilst this a legitimate use of our reusable function, one thing you will notice is that we have a lot of repeated values in many of the data-state attributes. Ideally, if we want to assign similar types of logic many times over, we should be able to just write a value once and have the function interpret that as repeat values.
For example, the below HTML snippet should perform the same action as the one above.
The first thing we need to consider is the for loop in processChange() which we last amended in the previous section. As it uses the length of data-state as a base for it’s loop amount, implementing these changes would expose a bug in scenarios where we have one class being applied to many elements.
What would happen here is because data-state only has a single value, the for loop in processChange() would only loop a single time, meaning our intended logic for c-btn-2 would never be assigned.
To fix this, we need to compare data-state and data-state-element. Whichever has the most values then becomes the base for our loop. Like so:
// Find out which has the biggest length between states and elements and use that length as loop number
// This is to make sure situations where we have one data-state-element value and many data-state values are correctly setup
var dataLength = Math.max(dataStateElement.length, dataState.length);
// Loop
for(var b = 0; b < dataLength; b++) {
[...]
}
As for the rest of the implementation, it’s now a matter of adding logic in the for loop for each attribute which says „If a matching value can’t be found, use the last valid one”.
Let’s use data-state value as an example. Currently, the code in the for loop which grabs the state value looks like:
// Grab state we will add
var elemState = dataState[b];
The problem now is if we have 3 data-state-element values, but only 1 data-state value, on loops 2 and 3 elemState would be undefined.
What we need to do is only redefine elemState if we have a value to give it. Like so:
// Grab state we will add
// If one isn't found, keep last valid one
if(dataState[b] !== undefined) {
var elemState = dataState[b];
}
This would ensure elemState would always have a value, including inheriting any previous values if one can’t initially be found.
Example
Here’s a final example showing all of our function refinements:
In this article, we’ve covered how to build upon the reusable function created in Part 1 to make it more accessible and easier to use.
In addition, we’ve also added swipe support for trigger elements and have made sure any data-state elements added after the initial render of the DOM are no longer ignored.
As before, any comments or constructive feedback are welcome. I’ll leave you will the full reusable function which we’ve developed over the last 2 articles:
(function(){
// SWIPE DETECT HELPER
//----------------------------------------------
var swipeDetect = function(el, callback){
var touchsurface = el,
swipedir,
startX,
startY,
dist,
distX,
distY,
threshold = 100, //required min distance traveled to be considered swipe
restraint = 100, // maximum distance allowed at the same time in perpendicular direction
allowedTime = 300, // maximum time allowed to travel that distance
elapsedTime,
startTime,
eventObj,
handleswipe = callback || function(swipedir, eventObj){}
touchsurface.addEventListener('touchstart', function(e){
var touchobj = e.changedTouches[0]
swipedir = 'none'
dist = 0
startX = touchobj.pageX
startY = touchobj.pageY
startTime = new Date().getTime() // record time when finger first makes contact with surface
eventObj = e;
}, false)
touchsurface.addEventListener('touchend', function(e){
var touchobj = e.changedTouches[0]
distX = touchobj.pageX - startX // get horizontal dist traveled by finger while in contact with surface
distY = touchobj.pageY - startY // get vertical dist traveled by finger while in contact with surface
elapsedTime = new Date().getTime() - startTime // get time elapsed
if (elapsedTime <= allowedTime){ // first condition for awipe met
if (Math.abs(distX) >= threshold && Math.abs(distY) <= restraint){ // 2nd condition for horizontal swipe met
swipedir = (distX < 0)? 'left' : 'right' // if dist traveled is negative, it indicates left swipe
}
else if (Math.abs(distY) >= threshold && Math.abs(distX) <= restraint){ // 2nd condition for vertical swipe met
swipedir = (distY < 0)? 'up' : 'down' // if dist traveled is negative, it indicates up swipe
}
}
handleswipe(swipedir, eventObj)
}, false)
}
// CLOSEST PARENT HELPER FUNCTION
//----------------------------------------------
closestParent = function(child, match) {
if (!child || child == document) {
return null;
}
if (child.classList.contains(match) || child.nodeName.toLowerCase() == match) {
return child;
}
else {
return closestParent(child.parentNode, match);
}
}
// REUSABLE FUNCTION
//----------------------------------------------
// Change function
processChange = function(elem){
// Grab data-state list and convert to array
var dataState = elem.getAttribute("data-state");
dataState = dataState.split(", ");
// Grab data-state-behaviour list if present and convert to array
if(elem.getAttribute("data-state-behaviour")) {
var dataStateBehaviour = elem.getAttribute("data-state-behaviour");
dataStateBehaviour = dataStateBehaviour.split(", ");
}
// Grab data-scope list if present and convert to array
if(elem.getAttribute("data-state-scope")) {
var dataStateScope = elem.getAttribute("data-state-scope");
dataStateScope = dataStateScope.split(", ");
}
// Grab data-state-element list and convert to array
// If data-state-element isn't found, pass self, set scope to self if none is present, essentially replicating "this"
if(elem.getAttribute("data-state-element")) {
var dataStateElement = elem.getAttribute("data-state-element");
dataStateElement = dataStateElement.split(", ");
}
else {
var dataStateElement = [];
dataStateElement.push(elem.classList[0]);
if(!dataStateScope) {
var dataStateScope = dataStateElement;
}
}
// Find out which has the biggest length between states and elements and use that length as loop number
// This is to make sure situations where we have one data-state-element value and many data-state values are correctly setup
var dataLength = Math.max(dataStateElement.length, dataState.length);
// Loop
for(var b = 0; b < dataLength; b++) {
// If a data-state-element value isn't found, use last valid one
if(dataStateElement[b] !== undefined) {
var dataStateElementValue = dataStateElement[b];
}
// If scope isn't found, use last valid one
if(dataStateScope && dataStateScope[b] !== undefined) {
var cachedScope = dataStateScope[b];
}
else if(cachedScope) {
dataStateScope[b] = cachedScope;
}
// Grab elem references, apply scope if found
if(dataStateScope && dataStateScope[b] !== "false") {
// Grab parent
var elemParent = closestParent(elem, dataStateScope[b]);
// Grab all matching child elements of parent
var elemRef = elemParent.querySelectorAll("." + dataStateElementValue);
// Convert to array
elemRef = Array.prototype.slice.call(elemRef);
// Add parent if it matches the data-state-element and fits within scope
if(elemParent.classList.contains(dataStateElementValue)) {
elemRef.unshift(elemParent);
}
}
else {
var elemRef = document.querySelectorAll("." + dataStateElementValue);
}
// Grab state we will add
// If one isn't found, keep last valid one
if(dataState[b] !== undefined) {
var elemState = dataState[b];
}
// Grab behaviour if any exists
// If one isn't found, keep last valid one
if(dataStateBehaviour) {
if(dataStateBehaviour[b] !== undefined) {
var elemBehaviour = dataStateBehaviour[b];
}
}
// Do
for(var c = 0; c < elemRef.length; c++) {
// Find out if we're manipulating aria-attributes or classes
var toggleAttr;
if(elemRef[c].getAttribute(elemState)) {
toggleAttr = true;
}
else {
toggleAttr = false;
}
if(elemBehaviour === "add") {
if(toggleAttr) {
elemRef[c].setAttribute(elemState, true);
}
else {
elemRef[c].classList.add(elemState);
}
}
else if(elemBehaviour === "remove") {
if(toggleAttr) {
elemRef[c].setAttribute(elemState, false);
}
else {
elemRef[c].classList.remove(elemState);
}
}
else {
if(toggleAttr) {
if(elemRef[c].getAttribute(elemState) === "true") {
elemRef[c].setAttribute(elemState, false);
}
else {
elemRef[c].setAttribute(elemState, true);
}
}
else {
elemRef[c].classList.toggle(elemState);
}
}
}
}
},
// Init function
initDataState = function(elem){
// Detect data-swipe attribute before we do anything, as its optional
// If not present, assign click event like before
if(elem.getAttribute("data-state-swipe")){
// Grab swipe specific data from data-state-swipe
var elemSwipe = elem.getAttribute("data-state-swipe"),
elemSwipe = elemSwipe.split(", "),
direction = elemSwipe[0],
elemSwipeBool = elemSwipe[1],
currentElem = elem;
// If the behaviour flag is set to "false", or not set at all, then assign our click event
if(elemSwipeBool === "false" || !elemSwipeBool) {
// Assign click event
elem.addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Run state function
processChange(this);
});
}
// Use our swipeDetect helper function to determine if the swipe direction matches our desired direction
swipeDetect(elem, function(swipedir){
if(swipedir === direction) {
// Run state function
processChange(currentElem);
}
})
}
else {
// Assign click event
elem.addEventListener("click", function(e){
// Prevent default action of element
e.preventDefault();
// Run state function
processChange(this);
});
}
// Add keyboard event for enter key to mimic anchor functionality
elem.addEventListener("keypress", function(e){
if(e.which === 13) {
// Prevent default action of element
e.preventDefault();
// Run state function
processChange(this);
}
});
};
// Run when DOM has finished loading
document.addEventListener("DOMContentLoaded", function() {
// Grab all elements with required attributes
var elems = document.querySelectorAll("[data-state]");
// Loop through our matches and add click events
for(var a = 0; a < elems.length; a++){
initDataState(elems[a]);
}
// Setup mutation observer to track changes for matching elements added after initial DOM render
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
for(var d = 0; d < mutation.addedNodes.length; d++) {
// Check if we're dealing with an element node
if(typeof mutation.addedNodes[d].getAttribute === 'function') {
if(mutation.addedNodes[d].getAttribute("data-state")) {
initDataState(mutation.addedNodes[d]);
}
}
}
});
});
// Define type of change our observer will watch out for
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}());