useStateInCustomProperties

In my recent “Custom Properties as State” post, one of the things I mentioned was that theoretically, UI libraries, like React and Vue, could automatically map the state they manage over to CSS Custom Properties so we could use that state right there if we wanted.

Someone should make a useStateWithCustomProperties hook or something to do that. #freeidea

Andrew Bloyce took me up on that idea.

It works just like I had hoped. The hook returns a component that is the Custom Property “boundary” and any state you pass it is mapped to those custom properties. Basic demo:

This is clever and useful already, but I’m tellin’ ya, this will be extremely useful should the concept of higher level custom properties land. The idea is that you could flip one custom property and have a whole block of styling change, which is is what we already enjoy with media queries and you know how useful those are.


The post useStateInCustomProperties appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

What if you could cut your hosting costs by 80%? Webiny Serverless CMS makes it possible.

Are you hosting one or more websites and are using a headless CMS? Are you hosting your CMS on a virtual machine or a container, or using a SaaS solution? If so, then you’re paying for the uptime, regardless if the server or service is serving requests or not. Essentially, you are paying for stuff you are not using. And in this article look at how how you can change that and save up to 80% of your hosting cost along the way.

Serverless — what’s that about?

If you’re new to serverless, in short, serverless is set of services you’re consuming without worrying about the underlying infrastructure. There are services for compute, like AWS Lambda that allow you to run Node.js code, services for storage like S3, database as a service like DynamoDb and many others.

The benefits of serverless are:

  1. You are billed based on your consumption
  2. There are no servers for you to manage
  3. Services scale automatically
  4. Services are more secure than your regular server

Servers are still there, but they are abstracted away — out of sight, out of mind.

Out of all the benefits, the first one plays a big role. Picture an API on a regular server or a virtual machine. If that server is not handling a new request every few seconds, there is a lot of idle time where the server is not doing anything, but you’re still paying for it.

With serverless you pay per your consumption, if your API is not handling any request at that point in time, your cost is $ 0. To further back this case, a research made by Deloitte found that a larger system can save anywhere between 60-80% in infrastructure costs and up to 60% in management costs just by switching to serverless.

Although serverless sounds great, there is a down side to it. It’s quite complex and time consuming to create new solutions from scratch and existing solutions are not designed for such environments. This is where Webiny comes in.

Webiny Serverless CMS

To help you adopt serverless and build websites on top of this modern infrastructure, there is one solution you can use today, for free. Webiny Serverless CMS is an open source solution that comes with a few apps, including a GraphQL-based Headless CMS.

Some of its features:

  1. GraphQL API
  2. Content versioning and modeling through a UI
  3. Multi-tenancy & Multi-language support
  4. Powerful user access control
  5. Built-in image optimization and image editor
  6. Works with existing static page generators like Gatsby and others

It’s important to note that Webiny Serverless CMS is completely free and self-hosted — all you need is an AWS account.

The system is self-hosted on top of the AWS serverless offering, and your sites will benefit from it in the following ways:

  • High-availability and fault tolerance for your API
  • 99.999999999% (11 9’s) of data durability
  • Enterprise-grade secure and scalable ACL
  • Event-driven scalability — pay for what you use
  • Great performance using a global CDN
  • DDoS Protection of your APIs

All this is in the box and it takes less than 10 minutes to get up and running.

Comparing Webiny to other solutions on the market — this is what it looks like:

Get started with Webiny Serverless CMS and stop overpaying for your infrastructure.


The post What if you could cut your hosting costs by 80%? Webiny Serverless CMS makes it possible. appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

How to Play and Pause CSS Animations with CSS Custom Properties

Let’s have a look CSS @keyframes animations, and specifically about how you can pause and otherwise control them. There is a CSS property specifically for it, that can be controlled with JavaScript, but there is plenty of nuance to get into in the details. We’ll also look at my preferred way of setting this up which gives lots of control. Hint: it involves CSS custom properties.

The importance of pausing animations

Recently, while working on the CSS-powered slideshow you’ll see later in this article, I was inspecting the animations in the Layers panel of DevTools. I noticed something interesting I’d never thought about before: animations not currently in the viewport were still running!

Maybe it’s not that unexpected. We know videos do that. Videos just go on until you pause them. But it made me wonder if these playing animations still use the CPU/GPU? Do they consume unnecessary processing power, slowing down other parts of the page?

Inspecting frames in the Performance panel in DevTools didn’t shed any more light on this since I couldn’t see “offscreen”-frames. But, when I scrolled away from my “CSS Only Slideshow” at the first slide, then waited and scrolled back, it was at slide five. The animation hadn’t paused. Animations just run and run, until you pause them.

So I began to look into how, why and when animations should pause. Performance is an obvious reason, given the findings above. Another reason is control. Users not only love to have control, but they should have control. A couple of years ago, my wife had a really bad concussion. Since then, she has avoided webpages with too many animations, as they make her dizzy. As a result, I consider accessibility perhaps the most important reason for allowing animations to pause.

All together, this is important stuff. We’re talking specifically about CSS keyframe animations, but broadly, that means we’re talking about:

  1. Performance
  2. Control
  3. Accessibility

The basics of pausing an animation

The only way to truly pause an animation in CSS is to use the animation-play-state property with a paused value.

.paused {   animation-play-state: paused; }

In JavaScript, the property is “camelCased” as animationPlayState and set like this:

element.style.animationPlayState = 'paused';

We can create a toggle that plays and pauses the animation by reading the current value of animationPlayState:

const running = element.style.animationPlayState === 'running';

…and then setting it to the opposite value:

element.style.animationPlayState = running ? 'paused' : 'running';

Setting the duration

Another way to pause animations is to set animation-duration to 0s. The animation is actually running, but since it has no duration, you won’t see any action.

But if we change the value to 3s instead:

It works, but has a major caveat: the animations are technically still running. The animation is merely toggling between its initial position, and where it is next in the sequence.

Straight up removing the animation

We can remove the animation entirely and add it back via classes, but like animation-duration, this doesn’t actually pause the animation.

.remove-animation {   animation: none !important; }

Since true pausing is really what we’re after here, let’s stick with animation-play-state and look into other ways of using it.

Using data attributes and CSS custom properties

Let’s use a data-attribute as a selector in our CSS. We can call those whatever we want, so I’m going to use a [data-animation]-attribute on all the elements where I’d like to play/pause animations. That way, it can be distinguished from other animations:

<div data-animation></div>

That attribute is the selector, and the animation shorthand is the property where we’re setting everything. We’ll toss in a bunch of CSS custom properties *(*using Emmet-abbreviations) as values:

[data-animation] {   animation:     var(--animn, none)     var(--animdur, 1s)     var(--animtf, linear)     var(--animdel, 0s)     var(--animic, infinite)     var(--animdir, alternate)     var(--animfm, none)     var(--animps, running); }

With that in place, any animation with this data-attribute will be perfectly ready to accept animations, and we can control individual aspects of the animation with custom properties. Some animations are going to have something in common (like duration, easing-type, etc.), so fallback values are set on the custom properties as well.

Why CSS custom properties? First of all, they can be read and set in both CSS and JavaScript. Secondly, they help significantly reduce the amount of CSS we need to write. And, since we can set them within @keyframes (at least in Chrome at the time of writing), they offer new and exiting ways to work with animations!

For the animations themselves, I’m using class selectors and updating the variables from the [data-animation]-selector:

<div class="circle a-slide" data-animation></div>

Why a class and a data-attribute? At this stage, the data-animation attribute might as well be a regular class, but we’re going to use it in more advanced ways later. Note that the .circle class name actually has nothing to do with the animation — it’s just a class for styling the element.

/* Animation classes */ .a-pulse {   --animn: pulse; } .a-slide {   --animdur: 3s;   --animn: slide; }  /* Keyframes */ @keyframes pulse {   0% { transform: scale(1); }   25% { transform: scale(.9); }   50% { transform: scale(1); }   75% { transform: scale(1.1); }   100% { transform: scale(1); } } @keyframes slide {   from { margin-left: 0%; }   to { margin-left: 150px; } }

We only need to update the values that will change, so if we use some common values in the fallback values for the data-animation selector, we only need to update the name of the animation’s custom property, --animn.

Example: Pausing with the checkbox hack

To pause all the animations using the ol’ checkbox hack, let’s create a checkbox before the animations:

<input type="checkbox" data-animation-pause />

And update the --animps property when checked:

[data-animation-pause]:checked ~ [data-animation] {   --animps: paused; }

That’s it! The animations toggle between played and paused when clicking the checkbox — no JavaScript required.

CSS-only slideshow

Let’s put some of these ideas to work!

I‘ve played with the <details>-tag a lot recently. It’s the obvious candidate for accordions, but it can also be used for tooltips, toggle-tips, drop-downs (styled <select>-look-a-likes), mega-menus… you name it. It is the official HTML disclosure element, after all. Apart from the global attributes and global events that all HTML elements accept, <details> has a single open attribute, and a single toggle event. So, like the checkbox hack, it’s perfect for toggling state — but even simpler:

details[open] {   --state: 1; } details:not([open]) {   --state: 0; }

I decided to do a slideshow, where the slides change automatically via a primary animation called autoplay, and each individual slide has its own unique secondary animation. The animation-play-state is controlled by the --animps-property. Each individual slide can have it’s own, unique animation, defined in a --animn-property:

<figure style="--animn:kenburns-top;--index:0;">   <img src="some-slide-image.jpg" />   <figcaption>Caption</figcaption> </figure>

The animation-play-state of the secondary animations are controlled by the --img-animps-property. I found a bunch of nice Ken Burns-esque animations at Animista and switched between them in the --animn-properties of the slides.

Pausing an animation from another animation

In order to prevent GPU overload, it would be ideal for the primary animation to pause any secondary animations. We noted it briefly earlier, but only Chrome (at the time of writing, and it is a bit shaky) can update a CSS Custom Property from an @keyframe animation — which you can see in the following example where the --bgc-property and --counter-properties are modified at different frames:

The initial state of the secondary animation, the --img-animps -property, needs to be paused, even if the primary animation is running:

details[open] ~ .c-mm__inner .c-mm__frame {   --animps: running;   --img-animps: paused; }

Then, in the main animation @keyframes, the property is updated to running:

@keyframes autoplay {   0.1% {     --img-animps: running; /* START */     opacity: 0;     z-index: calc(var(--z) + var(--slides))   }   5% { opacity: 1 }   50% { opacity: 1 }   51% { --img-animps: paused } /* STOP! */   100% {     opacity: 0;     z-index: var(--z)   } }

To make this work in browsers other than Chrome, the initial value needs to be running, as they cannot update a CSS custom property from a @keyframe.

Here’s the slideshow, with a “details hack” play/pause-button — no JavaScript required:

Enabling prefers-reduced-motion

Some people prefer no animations, or at least reduced motion. It might just be a personal preference, but can also be because of a medical condition. We talked about the importance of accessibility with animations at the very top of this post.

Both macOS and Windows have options that allow users to inform browsers that they prefer reduced motion on websites. This enables us to reach for the prefers-reduced-motion feature query, which Eric Bailey has written all about.

@media (prefers-reduced-motion) { ... }

Let’s use the [data-animation]-selector for reduced motion by giving it different values that are applied when prefers-reduced-motion is enabled*:*

  • alternate = run a different animation
  • once = set the animation-iteration-count to 1
  • slow = change the animation-duration-property
  • stop = set animation-play-state to paused

These are just suggestions and they can be anything you want, really.

<div class="circle a-slide" data-animation="alternate"></div> <div class="circle a-slide" data-animation="once"></div> <div class="circle a-slide" data-animation="slow"></div> <div class="circle a-slide" data-animation="stop"></div>

And the updated media query:

@media (prefers-reduced-motion) {   [data-animation="alternate"] {    /* Change animation duration AND name */     --animdur: 4s;     --animn: opacity;   }   [data-animation="slow"] {     /* Change animation duration */     --animdur: 10s;   }   [data-animation="stop"] {     /* Stop the animation */     --animps: paused;   } }

If this is too generic, and you prefer having unique, alternate animations per animation class, group the selectors like this:

.a-slide[data-animation="alternate"] { /* etc. */ }

Here’s a Pen with a checkbox simulating prefers-reduced-motion. Scroll down within the Pen to see the behavior change for each circle:

Pausing with JavaScript

To re-create the “Pause all animations”-checkbox in JavaScript, iterate all the [data-animation]-elements and toggle the same --animps custom property:

<button id="js-toggle" type="button">Toggle Animations</button>
const animations = document.querySelectorAll('[data-animation'); const jstoggle = document.getElementById('js-toggle');  jstoggle.addEventListener('click', () => {   animations.forEach(animation => {     const running = getComputedStyle(animation).getPropertyValue("--animps") || 'running';     animation.style.setProperty('--animps', running === 'running' ? 'paused' : 'running');   }) });

It’s exactly the same concept as the checkbox hack, using the same custom property: --animps, only set by JavaScript instead of CSS. If we want to support older browsers, we can toggle a class, that will update the animation-play-state.

Using IntersectionObserver

To play and pause all [data-animation]-animations automatically — and thus not unnecessarily overloading the GPU — we can use an IntersectionObserver.

First, we need to make sure that no animations are running at all:

[data-animation] {   /* Change 'running' to 'paused' */   animation: var(--animps, paused);  }

Then, we’ll create the observer and trigger it when an element is 25% or 75% in viewport. If the latter is matched, the animation starts playing; otherwise it pauses.

By default, all elements with a [data-animation]-attribute will be observed, but if prefers-reduced-motion is enabled (set to “reduce”), the elements with [data-animation="stop"] will be ignored.

const IO = new IntersectionObserver((entries) => {   entries.forEach((entry) => {     if (entry.isIntersecting) {       const state = (entry.intersectionRatio >= 0.75) ? 'running' : 'paused';       entry.target.style.setProperty('--animps', state);     }   }); }, {   threshold: [0.25, 0.75] });  const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); const elements = mediaQuery?.matches ? document.querySelectorAll(`[data-animation]:not([data-animation="stop"]`) : document.querySelectorAll('[data-animation]');  elements.forEach(animation => {   IO.observe(animation); });

You have to play around with the threshold-values, and/or whether you need to unobserve some animations after they’ve triggered, etc. If you load new content or animations dynamically, you might need to re-write parts of the observer as well. It’s impossible to cover all scenarios, but using this as a foundation should get you started with auto-playing and pausing CSS animations!

Bonus: Adding <audio> to the slideshow with minimal JavaScript

Here’s an idea to add music to the slideshow we built. First, add an audio-tag:

<audio src="/asset/audio/slideshow.mp3" hidden loop></audio>

Then, in Javascript:

const audio = document.querySelector('your-audio-selector'); const details = document.querySelector('your-details-selector'); details.addEventListener('toggle', () => {   details.open ? audio.play() : audio.pause(); })

Pretty simple, huh?

I did a “Silent Movie” (with audio)-demo here, where you get to know my geeky past. 🙂


The post How to Play and Pause CSS Animations with CSS Custom Properties appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

The types of websites you can create with WooCommerce

[Top]

Scrollbars on Hover

First, scrollbars are a usability and accessibility thing. Second, a rule of thumb: if an area scrolls, it should have a visible scrollbar. But the web is a big place and I like tricks, so I’m going to cover the idea of only revealing them on hover. Even macOS itself¹ hides scrollbars by default, revealing them contextually and on interaction. Same on iOS, leading to confusing momements.

All that aside, here’s a way to hide scrollbars by default, only revealing them when the element is hovered. It was created by Thomas Gladdines, who also emailed me about it:

In quick testing on my machine, it works across Chrome, Firefox, and Safari, regardless of my macOS settings. So pretty robust.

The trick is that mask covers the scrollbar! So, if you create a mask that is exactly as wide as the scrollbar (here, I’m just guessing that 17px will cover it) and super duper tall (both of which should probably be calculated by a script), it can perfectly cover the scrollbar. You can even transition the position of the mask, faking a fading in/out effect. Very clever.

Notably, this is the real scrollbar of the element, and not a faked one. Faking one could be another approach. Ben Nadel covered how Slack does that. Their trick is to force the scrollbar to render in an area hidden by overflow, and make a virtual scrollbar that mimics the native one (which you’d then have more direct control over). It’s not forcing the scrollbar either, which is something else you can do if so motivated. And nothing about this prevents you from styling the scrollbar, which might actually have some benefits like specifying the exact width of it.

  1. As I write: If your device allows gestures, scroll bars are hidden until you start scrolling. Otherwise, they’re visible. ↩️

The post Scrollbars on Hover appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

,
[Top]

New in Chrome 88: aspect-ratio

And it was released yesterday! The big news for us in CSS Land is that the new release supports the aspect-ratio property. This comes right on the heels of Safari announcing support for it in Safari Technology Preview 118, which released January 6. That gives us something to look forward to as it rolls out to Edge, Firefox and other browsers.

Here’s the release video skipped ahead to the aspect-ratio support:

For those catching up:

  • An aspect ratio defines the proportion of an element’s dimensions. For example, a box with an aspect ratio of 1/1 is a perfect square. An aspect ratio of 3/1 is a wide rectangle. Many videos aim for a 16/9 aspect ratio.
  • Some elements, like images and iframes, have an intrinsic aspect ratio. That means if either the width or the height is declared, the other is automatically calculated in a way that maintains its proportion.
  • Non-replaced elements, like divs, don’t have an intrinsic aspect ratio. We’ve resorted to a padding hack to get the same sort of effect.
  • Support for an aspect-ratio property in CSS allows us to maintain the aspect ratio of non-replaced elements.
  • There are some tricks for using it. For example, defining width on an element with aspect-ratio will result in the property using that width value to calculate the element’s height. Same goes for defining the height instead. And if we define both the width and height of an element? The aspect-ratio is completely ignored.

Seems like now is a good time to start brushing up on it!

Direct Link to ArticlePermalink


The post New in Chrome 88: aspect-ratio appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

,
[Top]

5 Reasons Why Every Freelance Web Designer Needs to Have a Contract

[Top]

Lightweight Form Validation with Alpine.js and Iodine.js

Many users these days expect instant feedback in form validation. How do you achieve this level of interactivity when you’re building a small static site or a server-rendered Rails or Laravel app? Alpine.js and Iodine.js are two minimal JavaScript libraries we can use to create highly interactive forms with little technical debt and a negligible hit to our page-load time. Libraries like these prevent you from having to pull in build-step heavy JavaScript tooling which can complicate your architecture.

I‘m going to iterate through a few versions of form validation to explain the APIs of these two libraries. If you want to copy and paste the finished product here‘s what we’re going to build. Try playing around with missing or invalid inputs and see how the form reacts:

A quick look at the libraries

Before we really dig in, it’s a good idea to get acquainted with the tooling we’re using.

Alpine is designed to be pulled into your project from a CDN. No build step, no bundler config, and no dependencies. It only needs a short GitHub README for its documentation. At only 8.36 kilobytes minfied and gzipped, it’s about a fifth of the size of a create-react-app hello world. Hugo Di Fracesco offers a complete and thorough overview of what it is an how it works. His initial description of it is pretty great:

Alpine.js is a Vue template-flavored replacement for jQuery and vanilla JavaScript rather than a React/Vue/Svelte/WhateverFramework competitor.

Iodine, on the other hand, is a micro form validation library, created by Matt Kingshott who works in the Laravel/Vue/Tailwind world. Iodine can be used with any front-end-framework as a form validation helper. It allows us to validate a single piece of data with multiple rules. Iodine also returns sensible error messages when validation fails. You can read more in Matt’s blog post explaining the reasoning behind Iodine.

A quick look at how Iodine works

Here’s a very basic client side form validation using Iodine. We‘ll write some vanilla JavaScript to listen for when the form is submitted, then use DOM methods to map through the inputs to check each of the input values. If it‘s incorrect, we’ll add an “invalid” class to the invalid inputs and prevent the form from submitting.

We’ll pull in Iodine from this CDN link for this example:

<script src="https://cdn.jsdelivr.net/gh/mattkingshott/iodine@3/dist/iodine.min.js" defer></script>

Or we can import it into a project with Skypack:

import kingshottIodine from "https://cdn.skypack.dev/@kingshott/iodine";

We need to import kingshottIodine when importing Iodine from Skypack. This still adds Iodine to our global/window scope. In your user code, you can continue to refer to the library as Iodine, but make sure to import kingshottIodine if you’re grabbing it from Skypack.

To check each input, we call the is method on Iodine. We pass the value of the input as the first parameter, and an array of strings as the second parameter. These strings are the rules the input needs to follow to be valid. A list of built-in rules can be found in the Iodine documentation.

Iodine’s is method either returns true if the value is valid, or a string that indicates the failed rule if the check fails. This means we‘ll need to use a strict comparison when reacting to the output of the function; otherwise, JavaScript assesses the string as true. What we can do is store an array of strings for the rules for each input as JSON in HTML data attributes. This isn’t built into either Alpine or Iodine, but I find it a nice way to co-locate inputs with their constraints. Note that if you do this you’ll need to surround the JSON with single quotes and use double quotes inside the attribute to follow the JSON spec.

Here’s how this looks in our HTML:

<input name="email" type="email" id="email" data-rules='["required","email"]'>

When we‘re mapping through the DOM to check the validity of each input, we call the Iodine function with the element‘s input value, then the JSON.encode() result of the input’s dataset.rules. This is what this looks like using vanilla JavaScript DOM methods:

let form = document.getElementById("form");  // This is a nice way of getting a list of checkable input elements // And converting them into an array so we can use map/filter/reduce functions: let inputs = [...form.querySelectorAll("input[data-rules]")];  function onSubmit(event) {   inputs.map((input) => {     if (Iodine.is(input.value, JSON.parse(input.dataset.rules)) !== true) {       event.preventDefault();       input.classList.add("invalid");     }   }); } form.addEventListener("submit", onSubmit);

Here’s what this very basic implementation looks like:

As you can tell this is not a great user experience. Most importantly, we aren’t telling the user what is wrong with the submission. The user also has to wait until the form is submitted before finding out anything is wrong. And frustratingly, all of the inputs keep the “invalid” class even after the user has corrected them to follow our validation rules.

This is where Alpine comes into play

Let’s pull it in and use it to provide nice user feedback while interacting with the form.

A good option for form validation is to validate an input when it’s blurred or on any changes after it has been blurred. This makes sure we‘re not yelling at the user before they’ve finished writing, but still give them instant feedback if they leave an invalid input or go back and correct an input value.

We’ll pull Alpine in from the CDN:

<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.7.3/dist/alpine.min.js" defer></script>

Or we can import it into a project with Skypack:

import alpinejs from "https://cdn.skypack.dev/alpinejs";

Now there’s only two pieces of state we need to hold for each input:

  • Whether the input has been blurred
  • The error message (the absence of this will mean we have a valid input)

The validation that we show in the form is going to be a function of these two pieces of state.

Alpine lets us hold this state in a component by declaring a plain JavaScript object in an x-data attribute on a parent element. This state can be accessed and mutated by its children elements to create interactivity. To keep our HTML clean, we can declare a JavaScript function that returns all the data and/or functions the form would need. Alpine will look for the this function in the global/window scope of our JavaScript code if we add this function to the x-data attribute. This also provides a reusable way to share logic as we can use the same function in multiple components or even multiple projects.

Let’s initialize the form data to hold objects for each input field with two properties: an empty string for the errorMessage and a boolean called blurred. We’ll use the name attribute of each element as their keys.

 <form id="form" x-data="form()" action="">   <h1>Log In</h1>    <label for="username">Username</label>   <input name="username" id="username" type="text" data-rules='["required"]'>    <label for="email">Email</label>   <input name="email" type="email" id="email" data-rules='["required","email"]'>    <label for="password">Password</label>   <input name="password" type="password" id="password" data-rules='["required","minimum:8"]'>    <label for="passwordConf">Confirm Password</label>   <input name="passwordConf" type="password" id="passwordConf" data-rules='["required","minimum:8"]'>    <input type="submit"> </form>

And here’s our function to set up the data. Note that the keys match the name attribute of our inputs:

window.form = () => {    return {     username: {errorMessage:'', blurred:false},     email: {errorMessage:'', blurred:false},     password: {errorMessage:'', blurred:false},     passwordConf: {errorMessage:'', blurred:false},   } }

Now we can use Alpine’s x-bind:class attribute on our inputs to add the “invalid” class if the input has blurred and a message exists for the element in our component data. Here’s how this looks in our username input:

<input name="username" id="username" type="text"  x-bind:class="{'invalid':username.errorMessage && username.blurred}" data-rules='["required"]'>

Responding to input changes

Now we need our form to respond to input changes and on blurring input states. We can do this by adding event listeners. Alpine gives a concise API to do this either using x-on or, similar to Vue, we can use an @ symbol. Both ways of declaring these act the same way.

On the input event we need to change the errorMessage in the component data to an error message if the value is invalid; otherwise, we’ll make it an empty string.

On the blur event we need to set the blurred property as true on the object with a key matching the name of the blurred element. We also need to recalculate the error message to make sure it doesn’t use the blank string we initialized as the error message.

So we’re going to add two more functions to our form to react to blurring and input changes, and use the name value of the event target to find what part of our component data to change. We can declare these functions as properties in the object returned by the form() function.

Here’s our HTML for the username input with the event listeners attached:

<input    name="username" id="username" type="text"   x-bind:class="{'invalid':username.errorMessage && username.blurred}"    @blur="blur" @input="input"   data-rules='["required"]' >

And our JavaScript with the functions responding to the event listeners:

window.form = () => {   return {     username: {errorMessage:'', blurred:false},     email: {errorMessage:'', blurred:false},     password:{ errorMessage:'', blurred:false},     passwordConf: {errorMessage:'', blurred:false},     blur: function(event) {       let ele = event.target;       this[ele.name].blurred = true;       let rules = JSON.parse(ele.dataset.rules)       this[ele.name].errorMessage = this.getErrorMessage(ele.value, rules);     },     input: function(event) {       let ele = event.target;       let rules = JSON.parse(ele.dataset.rules)       this[ele.name].errorMessage = this.getErrorMessage(ele.value, rules);     },     getErrorMessage: function() {     // to be completed     }   } }

Getting and showing errors

Next up, we need to write our getErrorMessage function.

If the Iodine check returns true, we‘ll set the errorMessage property to an empty string. Otherwise, we’ll pass the rule that has broken to another Iodine method: getErrorMessage. This will return a human-readable message. Here’s what this looks like:

getErrorMessage:function(value, rules){   let isValid = Iodine.is(value, rules);   if (isValid !== true) {     return Iodine.getErrorMessage(isValid);   }   return ''; }

Now we also need to show our error messages to the user.

Let’s add <p> tags with an error-message class below each input. We can use another Alpine attribute called x-show on these elements to only show them when their error message exists. The x-show attribute causes Alpine to toggle display: none; on the element based on whether a JavaScript expression resolves to true. We can use the same expression we used in the the show-invalid class on the input.

To display the text, we can connect our error message with x-text. This will automatically bind the innertext to a JavaScript expression where we can use our component state. Here’s what this looks like:

<p x-show="username.errorMessage && username.blurred" x-text="username.errorMessage" class="error-message"></p>

One last thing we can do is re-use the onsubmit code from before we pulled in Alpine, but this time we can add the event listener to the form element with @submit and use a submit function in our component data. Alpine lets us use $ el to refer to the parent element holding our component state. This means we don’t have to write lengthier DOM methods:

<form id="form" x-data="form()" @submit="submit" action="">   <!-- inputs...  --> </form>
submit: function (event) {   let inputs = [...this.$  el.querySelectorAll("input[data-rules]")];   inputs.map((input) => {     if (Iodine.is(input.value, JSON.parse(input.dataset.rules)) !== true) {       event.preventDefault();     }   }); }

This is getting there:

  • We have real-time feedback when the input is corrected.
  • Our form tells the user about any issues before they submit the form, and only after they’ve blurred the inputs.
  • Our form does not submit when there are invalid properties.

Validating on the client side of a server-side rendered app

There are still some problems with this version, though some won‘t be immediately obvious in the Pen as they‘re related to the server. For example, it‘s difficult to validate all errors on the client side in a server-side rendered app. What if the email address is already in use? Or a complicated database record needs to be checked? Our form needs to have a way to show errors found on the server. There are ways to do this with AJAX, but we’ll look at a more lightweight solution.

We can store the server side errors in another JSON array data attribute on each input. Most back-end frameworks will provide a reasonably easy way to do this. We can use another Alpine attribute called x-init to run a function when the component initializes. In this function we can pull the server-side errors from the DOM into each input’s component data. Then we can update the getErrorMessage function to check whether there are server errors and return these first. If none exist, then we can check for client-side errors.

<input name="username" id="username" type="text"  x-bind:class="{'invalid':username.errorMessage && username.blurred}"  @blur="blur" @input="input" data-rules='["required"]'  data-server-errors='["Username already in use"]'>

And to make sure the server side errors don’t show the whole time, even after the user starts correcting them, we’ll replace them with an empty array whenever their input gets changed.

Here’s what our init function looks like now:

init: function () {   this.inputElements = [...this.$  el.querySelectorAll("input[data-rules]")];   this.initDomData(); }, initDomData: function () {   this.inputElements.map((ele) => {   this[ele.name] = {     serverErrors: JSON.parse(ele.dataset.serverErrors),     blurred: false     };   }); }

Handling interdependent inputs

Some of the form inputs may depend on others for their validity. For example, a password confirmation input would depend on the password it is confirming. Or a date you started a job field would need to hold a value later than your date-of-birth field. This means it’s a good idea to check all the inputs of the form every time an input gets changed.

We can map through all of the input elements and set their state on every input and blur event. This way, we know that inputs that rely on each other will not be using stale data.

To test this out, let’s add a matchingPassword rule for our password confirmation. Iodine lets us add new custom rules with an addRule method.

Iodine.addRule(   "matchingPassword",   value => value === document.getElementById("password").value );

Now we can set a custom error message by adding a key to the messages property in Iodine:

Iodine.messages.matchingPassword="Password confirmation needs to match password";

We can add both of these calls in our init function to set up this rule.

In our previous implementation, we could have changed the “password” field and it wouldn’t have made the “password confirmation” field invalid. But now that we’re mapping through all the inputs on every change, our form will always make sure the password and the password confirmation match.

Some finishing touches

One little refactor we can do is to make the getErrorMessage function only return a message if the input has been blurred — this can make out HTML slightly shorter by only needing to check one value before deciding whether to invalidate an input. This means our x-bind attribute can be as short as this:

x-bind:class="{'invalid':username.errorMessage}"

Here’s what our functions look like to map through the inputs and set the errorMessage data now:

updateErrorMessages: function () {   // Map through the input elements and set the 'errorMessage'   this.inputElements.map((ele) => {     this[ele.name].errorMessage = this.getErrorMessage(ele);   }); }, getErrorMessage: function (ele) {   // Return any server errors if they're present   if (this[ele.name].serverErrors.length > 0) {     return input.serverErrors[0];   }   // Check using Iodine and return the error message only if the element has not been blurred   const error = Iodine.is(ele.value, JSON.parse(ele.dataset.rules));   if (error !== true && this[ele.name].blurred) {     return Iodine.getErrorMessage(error);   }   // Return empty string if there are no errors   return ""; },

We can also remove the @blur and @input events from all of our inputs by listening for these events in the parent form element. However, there is a problem with this: the blur event does not bubble (parent elements listening for this event will not be passed it when it fires on their children). Luckily, we can replace blur with the focusout event, which is basically the same event, but this one bubbles, so we can listen for it in our form parent element.

Finally, our code is growing a lot of boilerplate. If we were to change any input names we would have to rewrite the data in our function every time and add new event listeners. To prevent rewriting the component data every time, we can map through the form’s inputs that have a data-rules attribute to generate our initial component data in the init function. This makes the code more reusable for additional forms. All we’d need to do is include the JavaScript and add the rules as a data attribute and we’re good to go.

Oh, and hey, just because it’s so easy to do with Alpine, let’s add a fade-in transition that brings attention to the error messaging:

<p class="error-message" x-show.transition.in="username.errorMessage" x-text="username.errorMessage"></p>

And here’s the end result. Reactive, reusable form validation at a minimal page-load cost.

If you want to use this in your own application, you can copy the form function to reuse all the logic we’ve written. All you’d need to do is configure your HTML attributes and you’d be ready to go.


The post Lightweight Form Validation with Alpine.js and Iodine.js appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

How To Make The Best Decision When Choosing The Right Brand Colours

[Top]

5 Top Gallery Plugins for Photographers

[Top]