Tag: Form

Form design

A very digestable guide from Geri Reid on building forms. Not the code, but the design and UX principles that should guide the code.

Working on a design system for a bank has taught [me] a lot about forms. I’ve watched testing in our labs. I’ve worked alongside experts from specialist accessibility organisations. I’ve seen forms tested by disabled people and users of assistive technology. I’ve also read a lot of research.

From all this learning I’ve formed my own forms best-practice guidelines. 

I always think about one code-related thing when it comes to general form advice: all inputs need an attached label.

<label for="name">Name:</label> <input type="text" id="name" name="name">  <!-- or -->  <label>   Name:   <input type="text" name="name"> </label>

It’s HTML 101 stuff, but so many forms fail to do it. I once heard a story about blind college-bound high school students who were unable to apply to college specifically because they couldn’t figure out what the inputs wanted on a form. They started second-guessing if they could do college on their own after that experience.

You know how The Onion prints the article “‘No Way To Prevent This,’ Says Only Nation Where This Regularly Happens” literally every single time there is a mass shooting? I feel like someone should make a website that publishes an article pointing to every single website that fails this one test with a headline like “‘No Way To Prevent This’, Says Website Where The Fix Would Be Typing A Handful Of Characters.”

Direct Link to ArticlePermalink


The post Form design appeared first on CSS-Tricks.

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

CSS-Tricks

,

Chrome 83 Form Element Styles

There have been some aesthetic changes to what form elements look like as of Chrome 83. Anything with gradient colorization is gone (notably the extra-shiny <meter> stuff). The consistency across the board is nice, particularly between inputs and textareas. Not a big fan of the new <select> styling, but I hear a lot of accessibility research went into this, so it’s hard to complain there — plus you can always change it.

Hakim has a nice comparison tweet:

The Jetpack plugin for WordPress has a new comparison block and I’m going to try it out here. You can swipe between the items, just for fun (drag the slider in the middle):

This is not accompanied by new standardized ways to change the look of form elements with CSS, although browsers are well aware of that and seem to draw nearer and nearer all the time. I believe is was a step along that path.

I also see there is a new <input type="time"> as well. The old version looked like this and offered no UI controls:

Now we get this beast with controls:

There are no visual indicators or buttons, but you can scroll those columns.

Reddit notes that it uses the same pseudo element that date pickers use, so if you want it gone, you can scope it to these types of inputs (or not) and remove it.

input[type="time"]::-webkit-calendar-picker-indicator {   display: none; }

I’d call it an improvement (I like UI controls for things), but it does continue to highlight the need to be able to style these things, particularly if the goal is to have people actually use them and not (poorly) rebuild them.

The post Chrome 83 Form Element Styles appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

How to Redirect a Search Form to a Site-Scoped Google Search

This is just a tiny little trick that might be helpful on a site where you don’t have the time or desire to build out a really good on-site search solution. Google.com itself can perform searches scoped to one particular site. The trick is getting people there using that special syntax without them even knowing it.

Make a search form:

<form action="https://google.com/search" target="_blank" type="GET">    <label>      Search CSS-Tricks on Google:       <input type="search" name="q">    </label>    <input type="submit" value="Go">  </form>

When that form is submitted, we’ll intercept it and change the value to include the special syntax:

var form = document.querySelector("form");  form.addEventListener("submit", function (e) {   e.preventDefault();   var search = form.querySelector("input[type=search]");   search.value = "site:css-tricks.com " + search.value;   form.submit(); }); 

That’s all.

The post How to Redirect a Search Form to a Site-Scoped Google Search appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Better Form Inputs for Better Mobile User Experiences

Here’s one simple, practical way to make apps perform better on mobile devices: always configure HTML input fields with the correct type, inputmode, and autocomplete attributes. While these three attributes are often discussed in isolation, they make the most sense in the context of mobile user experience when you think of them as a team. 

There’s no question that forms on mobile devices can be time-consuming and tedious to fill in, but by properly configuring inputs, we can ensure that the data entry process is as seamless as possible for our users. Let’s take a look at some examples and best practices we can use to create better user experiences on mobile devices.

Use this demo to experiment on your own, if you’d like.

Using the correct input type

This is the easiest thing to get right. Input types, like email, tel, and url, are well-supported across browsers. While the benefit of using a type, like tel over the more generic text, might be hard to see on desktop browsers, it’s immediately apparent on mobile.

Choosing the appropriate type changes the keyboard that pops up on Android and iOS devices when a user focuses the field. For very little effort, just by using the right type, we will show custom keyboards for email, telephone numbers, URLs, and even search inputs

Text input type on iOS (left) and Android (right)
Email input type on iOS (left) and Android (right)
URL input type on iOS (left) and Android (right)
Search input type on iOS (left) and Android (right)

One thing to note is that both input type="email" and input type="url" come with validation functionality, and modern browsers will show an error tooltip if their values do not match the expected formats when the user submits the form. If you’d rather turn this functionality off, you can simply add the novalidate attribute to the containing form.

A quick detour into date types

HTML inputs comprise far more than specialized text inputs — you also have radio buttons, checkboxes, and so on. For the purposes of this discussion, though, I’m mostly talking about the more text-based inputs

There is a type of input that sits in the liminal space between the more free-form text inputs and input widgets like radio buttons: date. The date input type comes in a variety of flavors that are well-supported on mobile, including date, time, datetime-local, and month. These pop up custom widgets in iOS and Android when they are focused. Instead of triggering a specialized keyboard, they show a select-like interface in iOS, and various different types of widgets on Android (where the date and time selectors are particularly slick). 

I was excited to start using native defaults on mobile, until I looked around and realized that most major apps and mobile websites use custom date pickers rather than native date input types. There could be a couple reasons for this. First, I find the native iOS date selector to be less intuitive than a calendar-type widget. Second, even the beautifully-designed Android implementation is fairly limited compared to custom components — there’s no easy way to input a date range rather than a single date, for instance. 

Still, the date input types are worth checking out if the custom datepicker you’re using doesn’t perform well on mobile. If you’d like to try out the native input widgets on iOS and Android while making sure that desktop users see a custom widget instead of the default dropdown, this snippet of CSS will hide the calendar dropdown for desktop browsers that implement it:

::-webkit-calendar-picker-indicator {   display: none; }
Date input type on iOS (left) and Android (right)
Time input type on iOS (left) and Android (right)

One final thing to note is that date types cannot be overridden by the inputmode attribute, which we’ll discuss next.

Why should I care about inputmode?

The inputmode attribute allows you to override the mobile keyboard specified by the input’s type and directly declare the type of keyboard shown to the user. When I first learned about this attribute, I wasn’t impressed — why not just use the correct type in the first place? But while inputmode is often unnecessary, there are a few places where the attribute can be extremely helpful. The most notable use case that I’ve found for inputmode is building a better number input.

While some HTML5 input types, like url and email, are straightforward, input type="number" is a different matter. It has some accessibility concerns as well as a somewhat awkward UI. For example, desktop browsers, like Chrome, show tiny increment arrows that are easy to trigger accidentally by scrolling.

So here’s a pattern to memorize and use going forwards. For most numeric inputs, instead of using this: 

<input type="number" />

…you actually want to use this:

<input type="text" inputmode="decimal" />

Why not inputmode="numeric" instead of inputmode="decimal"

The numeric and decimal attribute values produce identical keyboards on Android. On iOS, however, numeric displays a keyboard that shows both numbers and punctuation, while decimal shows a focused grid of numbers that almost looks exactly like the tel input type, only without extraneous telephone-number focused options. That’s why it’s my preference for most types of number inputs.

iOS numeric input (left) and decimal input (right)
Android numeric input (left) and decimal input (right)

Christian Oliff has written an excellent article dedicated solely to the inputmode attribute.

Don’t forget autocomplete

Even more important than showing the correct mobile keyboard is showing helpful autocomplete suggestions. That can go a long way towards creating a faster and less frustrating user experience on mobile.

While browsers have heuristics for showing autocomplete fields, you cannot rely on them, and should still be sure to add the correct autocomplete attribute. For instance, in iOS Safari, I found that an input type="tel" would only show autocomplete options if I explicitly added a autocomplete="tel" attribute.

You may think that you are familiar with the basic autocomplete options, such as those that help the user fill in credit card numbers or address form fields, but I’d urge you to review them to make sure that you are aware of all of the options. The spec lists over 50 values! Did you know that autocomplete="one-time-code" can make a phone verification user flow super smooth?

Speaking of autocomplete…

I’d like to mention one final element that allows you to create your own custom autocomplete functionality: datalist. While it creates a serviceable — if somewhat basic — autocomplete experience on desktop Chrome and Safari, it shines on iOS by surfacing suggestions in a convenient row right above the keyboard, where the system autocomplete functionality usually lives. Further, it allows the user to toggle between text and select-style inputs.

On Android, on the other hand, datalist creates a more typical autocomplete dropdown, with the area above the keyboard reserved for the system’s own typeahead functionality. One possible advantage to this style is that the dropdown list is easily scrollable, creating immediate access to all possible options as soon as the field is focused. (In iOS, in order to view more than the top three matches, the user would have to trigger the select picker by pressing the down arrow icon.)

You can use this demo to play around with datalist:

And you can explore all the autocomplete options, as well as input type and inputmode values, using this tool I made to help you quickly preview various input configurations on mobile.

In summary

When I’m building a form, I’m often tempted to focus on perfecting the desktop experience while treating the mobile web as an afterthought. But while it does take a little extra work to ensure forms work well on mobile, it doesn’t have to be too difficult. Hopefully, this article has shown that with a few easy steps, you can make forms much more convenient for your users on mobile devices.

The post Better Form Inputs for Better Mobile User Experiences appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Custom Styling Form Inputs With Modern CSS Features

It’s entirely possible to build custom checkboxes, radio buttons, and toggle switches these days, while staying semantic and accessible. We don’t even need a single line of JavaScript or extra HTML elements! It’s actually gotten easier lately than it has been in the past. Let’s take a look.

Here’s where we’ll end up:

Things sure have gotten easier than they were!

The reason is that we can finally style the ::before and ::after pseudo-elements on the <input> tag itself. This means we can keep and style an <input> and won’t need any extra elements. Before, we had to rely on the likes of an extra <div> or <span>, to pull off a custom design.

Let’s look at the HTML

Nothing special here. We can style our inputs with just this HTML:

<!-- Checkbox --> <input type="checkbox">  <!-- Radio --> <input type="radio">  <!-- Switch --> <input type="checkbox" class="switch">

That’s it for the HTML part, but of course it’s recommended to have name and id attributes, plus a matching <label> element:

<!-- Checkbox --> <input type="checkbox" name="c1" id="c1"> <label for="c1">Checkbox</label>  <!-- Radio --> <input type="radio" name="r1" id="r1"> <label for="r1">Radio</label>  <!-- Switch --> <input type="checkbox" class="switch" name="s1" id="s1"> <label for="s1">Switch</label>

Getting into the styling 

First of all, we check for the support of appearance: none;, including it’s prefixed companions. The appearance property is key because it is designed to remove a browser’s default styling from an element. If the property isn’t supported, the styles won’t apply and default input styles will be shown. That’s perfectly fine and a good example of progressive enhancement at play.

@supports(-webkit-appearance: none) or (-moz-appearance: none) {   input[type='checkbox'],   input[type='radio'] {     -webkit-appearance: none;     -moz-appearance: none;   } }

As it stands today, appearance  is a working draft, but here’s what support looks like:

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

Chrome Firefox IE Edge Safari
82* 74* No 79* TP*

Mobile / Tablet

Android Chrome Android Firefox Android iOS Safari
79* 68* 76* 13.3*

Like links, we’ve gotta consider different interactive states with form elements. We’ll consider these when styling our elements:

  • :checked
  • :hover
  • :focus
  • :disabled

For example, here’s how we can style our toggle input, create the knob, and account for the :checked state:

/* The toggle container */ .switch {   width: 38px;   border-radius: 11px; }  /* The toggle knob */ .switch::after {   left: 2px;   top: 2px;   border-radius: 50%;   width: 15px;   height: 15px;   background: var(--ab, var(--border));   transform: translateX(var(--x, 0)); }  /* Change color and position when checked */ .switch:checked {   --ab: var(--active-inner);   --x: 17px; }  /* Drop the opacity of the toggle knob when the input is disabled */ .switch:disabled:not(:checked)::after {   opacity: .6; }

We are using the <input> element like a container. The knob inside of the input is created with the ::after pseudo-element. Again, no more need for extra markup!

If you crack open the styles in the demo, you’ll see that we’re defining some CSS custom properties because that’s become such a nice way to manage reusable values in a stylesheet:

@supports(-webkit-appearance: none) or (-moz-appearance: none) {   input[type='checkbox'],   input[type='radio'] {     --active: #275EFE;     --active-inner: #fff;     --focus: 2px rgba(39, 94, 254, .25);     --border: #BBC1E1;     --border-hover: #275EFE;     --background: #fff;     --disabled: #F6F8FF;     --disabled-inner: #E1E6F9;   } }

But there’s another reason we’re using custom properties — they work well for updating values based on the state of the element! We won’t go into full detail here, but here’s an example how we can use custom properties for different states.

/* Default */ input[type='checkbox'], input[type='radio'] {   --active: #275EFE;   --border: #BBC1E1;   border: 1px solid var(--bc, var(--border)); }  /* Override defaults */ input[type='checkbox']:checked, input[type='radio']:checked {   --b: var(--active);   --bc: var(--active); }    /* Apply another border color on hover if not checked & not disabled */ input[type='checkbox']:not(:checked):not(:disabled):hover, input[type='radio']:not(:checked):not(:disabled):hover {   --bc: var(--border-hover); }

For accessibility, we ought to add a custom focus style. We are removing the default outline because it can’t be rounded like the rest of the things we’re styling. But a border-radius along with a box-shadow can make for a rounded style that works just like an outline.

input[type='checkbox'], input[type='radio'] {   --focus: 2px rgba(39, 94, 254, .25);   outline: none;   transition: box-shadow .2s; }  input[type='checkbox']:focus, input[type='radio']:focus {   box-shadow: 0 0 0 var(--focus); }

It’s also possible to align and style the <label> element which directly follows the <input> element in the HTML:

<input type="checkbox" name="c1" id="c1"> <label for="c1">Checkbox</label>
input[type='checkbox'] + label, input[type='radio'] + label {   display: inline-block;   vertical-align: top;   /* Additional styling */ }  input[type='checkbox']:disabled + label, input[type='radio']:disabled + label {     cursor: not-allowed; }

Here’s that demo again:

Hopefully, you’re seeing how nice it is to create custom form styles these days. It requires less markup, thanks to pseudo-elements that are directly on form inputs. It requires less fancy style switching, thanks to custom properties. And it has pretty darn good browser support, thanks to @supports.

All in all, this is a much more pleasant developer experience than we’ve had to deal with in the past!

The post Custom Styling Form Inputs With Modern CSS Features appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Demonstrating Reusable React Components in a Form

Components are the building blocks of React applications. It’s almost impossible to build a React application and not make use of components. It’s widespread to the point that some third-party packages provide you with components you can use to integrate functionality into your application.

These third-party components tend to be reusable. The difference between them and components you probably have in your application has to do with specificity.

Here’s what I mean. Say you run a company that sells polo shirts. There are two things you can do:

  1. You can produce polos that already have a design on them, or
  2. You can have the buyer choose the design they want.

Some fundamental things will be consistent, like all polos should be short-sleeved. But the user can pick variations of the shirts, such as the color and size. A short-sleeve polo would make a good React component in that case: it’s the same item with different variations.

Now let’s say you’re working on a login form. Like polo shirts, the form has consistent characteristics, but instead of size and color variations, we’d be looking at input fields, a submit button, perhaps even a lost password link. This can be componentized and implemented with variations in the inputs, buttons, links, etc.

Input Element Example

Let’s look at it from the perspective of creating an input field for your form. Here is how a typical text input will look like as a React component:

class Form extends React.Component {   this.state = {     username: ''   }      handleChange = (event) => {     this.setSate({ username: event.target.value })   }    render() {     return (       <input         name="username         type={type}         placeholder="Enter username"         onChange={this.handleChange}         value={this.state.username}       />     )   } }

To make this input element reusable in other places and projects, we’ll have to extract it into its own component. Let’s call it FormInput.

const FormInput = ({   name,   type,   placeholder,   onChange,   className,   value,   error,   children,   label,   ...props }) => {      return (     <React.Fragment>       <label htmlFor={name}>{label}</label>       <input         name={name}         type={type}         placeholder={placeholder}         onChange={onChange}         value={value}         className={className}         style={error && {border: 'solid 1px red'}}       />       { error && <p>{ error }</p>}     </React.Fragment>   ) }  FormInput.defaultProps = {   type: "text",   className: "" }  FormInput.propTypes = {   name: PropTypes.string.isRequired,   type: PropTypes.string,   placeholder: PropTypes.string.isRequired,   type: PropTypes.oneOf(['text', 'number', 'password']),   className: PropTypes.string,   value: PropTypes.any,   onChange: PropTypes.func.isRequired }

The component accepts certain props, such as the attributes that we need to make the input with valid markup, including the placeholder, value and name. We set up the input element in the render function, setting the attribute values as the props that are passed to the component. We even bind the input to a label to ensure they’re always together. You can see that we’re not making assumptions by predefining anything. The idea is to ensure that the component can be used in as many scenarios as possible.

This makes for a good component because it enforces good markup (something Brad Frost calls dumb React) which goes to show that not every component has to be some highly complex piece of functionality. Then again, if we were talking about something super basic, say a static heading, then reaching for a React component might be overkill. The possible yardstick for making something a reusable component probably ought to be when you need the same functionality in other parts of an application. There’s generally no need for a “reusable” component if that component is only used once.

We can make use of our input component in another component, the LoginPage.

class LoginPage extends React.Component {   state = {     user: {       username: "",       password: ""     },     errors: {},     submitted: false   };    handleChange = event => {     const { user } = this.state;     user[event.target.name] = event.target.value;     this.setState({ user });   };    onSubmit = () => {     const {       user: { username, password }     } = this.state;     let err = {};      if (!username) {       err.username = "Enter your username!";     }      if (password.length < 8) {       err.password = "Password must be at least 8 characters!";     }      this.setState({ errors: err }, () => {       if (Object.getOwnPropertyNames(this.state.errors).length === 0) {         this.setState({ submitted: true });       }     });   };    render() {     const {       submitted,       errors,       user: { username, password }     } = this.state;     return (       <React.Fragment>         {submitted ? (           <p>Welcome onboard, {username}!</p>         ) : (           <React.Fragment>             <h3>Login!</h3>             <FormInput               label="Username"               name="username"               type="text"               value={username}               onChange={this.handleChange}               placeholder="Enter username..."               error={errors.username}               required               className="input"             />              <FormInput               label="Password"               name="password"               type="password"               value={password}               onChange={this.handleChange}               placeholder="Enter password..."               error={errors.password}               className="input"               required             />              <Button               type="submit"               label="Submit"               className="button"               handleClick={this.onSubmit}             />           </React.Fragment>         )}       </React.Fragment>     );   } }

See how LoginPage uses the FormInput twice? We’re using it both as the text input for a username and another text input for a password. If we want to make any changes to how the input functions, we can make those changes inside the FormInput component file we created and they will be applied to every instance where the input component is used. That’s the fundamental upside to having reusable components: you don’t have to repeat yourself.

Even the errors are displayed from the FormInput component.

The onSubmit function first validates the user object which we get from the form, ensuring that it fits the structure that there is a value for username. Notice that we can even extend the input’s functionality, like we did to check that the password contains at least eight characters.

If you look at the code, you’ll see a have a Button component in there. That’s not the same as a HTML <button> element, but instead another component that takes the props that define the type of button we want (submit, reset, button), its class name, what to do on click, and the label. There are lots of other button attributes we could integrate to enforce whatever standard is needed.

const Button = (props) => (   <button     type={props.type}     className={props.className}     onClick={props.handleClick}   >     {props.label}   </button> )

Here’s our final login form when all of our components are put together.

See the Pen
Reusable Button Component
by Kingsley Silas Chijioke (@kinsomicrote)
on CodePen.


Wanna give this a try yourself? Try working on a reusable <select> element. If that’s too difficult, you can start with a <textarea> element, then perhaps a checkbox before hopping over to <select>. The key idea is to make it generic. I’ll like to see what you came up with, so link up your work in the comment section!

The post Demonstrating Reusable React Components in a Form appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

A Comparison of Static Form Providers

Let’s attempt to coin a term here: “Static Form Provider.” You bring your HTML <form>, but don’t worry about the back-end processing that makes it work. There are a lot of these services out there!

Static Form Providers do all tasks like validating, storing, sending notifications, and integrating with other APIs. It’s lovely when you can delegate big responsibilities like this. The cost? Typically a monthly or annual subscription, except for a few providers and limited plans. The cost is usually less than fancier “form builders” that help you build the form itself and process it.

In this article, we are going to review some of the most popular static form providers:

Before moving forward, just a note that the information for these comparisons came from visiting the site for each product and learning about the included features. Once I got all the information, I sent an email to each provider to confirm the list of features. Some of them confirmed, some didn’t. Special thanks to Kwes, FormKeep, Formspree, FormSubmit, formX, and Netlify Forms teams for confirming.

Form building components and validation

Name Custom Components Front-End Validation Back-End Validation
Kwes
Basin
FieldGoal Unable to confirm Unable to confirm Unable to confirm
FormCarry
FormKeep
Formspree
FormSubmit
formX
Getform
Netlify Forms

Components for building a form are HTML input elements, like text inputs, textareas, checkboxes, and radio buttons. When using a static form, most providers require adding custom HTML attributes. By providing the custom URL in the form action attribute, the form gets submitted on the provider’s side where it gets stored.

If you are looking for a form builder, FormKeep has a form designer feature. That means you embed custom HTML and JavaScript files in the page, and you get a styled form. Otherwise, you have to style the form by yourself.

If you need custom components, like a date picker or card inputs, Kwes and FormKeep are the only providers with this feature. If you want to validate input fields in the browser, you might use third-party libraries or writing your code which means adding additional JavaScript code to the site. Kwes is the only provider that supports front-end validation based on the rules you set in each input component. To enable this feature, you should include additional JavaScript file, which you might do nevertheless. Other static form providers rely only on HTML5 validation.

Kwes is the only provider with back-end validation, too. The rules you set in the front end are passed to the back end side. In case when front-end validation fails, you are safe, the backend validation would work. Other providers don’t have this feature; they rely only on spam protection.

Spam protection

Name Spam Protection
Kwes Artificial intelligence
Automatic Honeypot
Blacklists
Proprietary technology
Basin Akismet
CleanTalk
reCAPTCHA
Honeypot
FieldGoal Provided, but unable to confirm what powers it
FormCarry Akismet
reCAPTCHA
FormKeep Akismet
reCAPTCHA
Honeypot
Proprietary technology
Formspree reCaptcha
Profanity filter
Automated classification
FormSubmit reCaptcha
Honeypot
formX reCAPTCHA
Honeypot
Getform Akismet
reCAPTCHA
Netlify Forms Akismet
reCAPTCHA
Honeypot

Kwes advertises a 99.6% spam block success rate with no setup required.

Once your form is ready for submissions, you might find it hard to deal with spam. That’s why spam protection is essential, especially if you want to keep your sanity and serenity. All providers provide spam protection in this way or another. Some rely on Google reCAPTCHA or Akismet, some on Honeypot techniques, and some use artificial intelligence to get the job done. It is worth noting that adding an additional step to your form, like adding reCAPTCHA might affect the conversion rates on form submissions.

Email notifications

Name Confirmations Notifications Email Routing Logic
Kwes
Basin
FieldGoal Unable to confirm Unable to confirm Unable to confirm
FormCarry
FormKeep
Formspree
FormSubmit
formX
Getform
Netlify Forms

Email confirmations are essential if you want to provide a fast response to your users. With a contact form, for example, you want to get an email for every new submission. That way, you’re able to respond to the submission quickly and efficiently.

All providers, except FieldGoal, have confirmation, notification, and email routing logic features. You could set up an email form element which would be used to send an email automatically to the user with confirmation about the submission.

Some providers have other sorts of notifications besides email, like push notifications or Slack messages, which might be handy.

White labeling

Name White Label
Kwes
Basin
FieldGoal Unable to confirm
FormCarry
FormKeep
Formspree
FormSubmit
formX
Getform
Netlify Forms

When communicating via email notifications with your clients, you might want to use your brand and style. It creates better awareness and that way you familiarize your clients with your product. All providers offer this feature, with the exception of FieldGoal, which I was unable to confirm (although it might be under paid plans).

Custom redirects

Name Custom Redirects
Kwes
Basin
FieldGoal Unable to confirm
FormCarry
FormKeep
Formspree
FormSubmit
formX
Getform
Netlify Forms

Once you have captured a response from your user, you may want to let the user continue using your site. Also, you might want to communicate to the user that the submission was received. This feature is called “custom redirect,” and every provider has this feature (with another exception for FieldGoal because I was unable to confirm). Note that this feature might not be available in a free plan and require a paid or upgraded account.

Upload storage

Name Upload Storage
Kwes 200MB per file
20GB storage
Basin 100MB per submission
FieldGoal Amazon S3
FormCarry 5MB per file
5GB storage
FormKeep 2.5G storage
Formspree 10MB per file
10GB storage
FormSubmit Included, but unconfirmed storage amounts
formX
Getform 25MB per submission
25GB storage
Netlify Forms 1GB storage

Not every static form provider provides file storage. For example, formX doesn’t provide it at all. In most cases, this feature is available under paid plans. You might want to invest additional time to find out which provider offers the best service for you. Be sure to look specifically at single file size and form submission size limitations.

Data export

Name Data Export
Kwes
Basin
FieldGoal Unable to confirm
FormCarry
FormKeep
Formspree
FormSubmit
formX
Getform
Netlify Forms

Data export is important feature if you want to use it for analysis or for import to third-party software. Most providers offers CSV and JSON exports, which are the most commonly used ones.

API access

Name API Access
Kwes
Basin
FieldGoal Unable to confirm
FormCarry
FormKeep
Formspree
FormSubmit
formX On demand
Getform
Netlify Forms

If you want to control your data submissions by building custom application or script, you might benefit from having API access. Most providers have this feature, except Getform. formX offers it, but only on demand.

Webhooks/Zapier

Name Webhooks Zapier
Kwes
Basin
FieldGoal Unable to confirm
FormCarry
FormKeep
Formspree
FormSubmit
formX
Getform
Netlify Forms

When building a custom application or a script is out of budget, you might want to use webhooks to integrate data submissions with third-party software. Zapier is one of the most commonly used services for this, and only FormSubmit doesn’t support it (though it does support webhooks).

Analytics

Name Analytics
Kwes
Basin
FieldGoal Unable to confirm
FormCarry
FormKeep
Formspree
FormSubmit
formX
Getform
Netlify Forms

Analytics for static forms is a neat feature that could offer beneficial insight into how your form is performing. It may help you understand how your users interact with it, and you may spot ways to improve the form submission experience as a result. This feature is the least supported of all other features. Only Basin, FormKeep, and formX provide it.

Plan comparison

Name Plan 1 Plan 2 Plan 3 Plan 4
Kwes Free Tier
$ 0/mo.
Build spam-protected, and validated forms quicker than ever.

1 Website
Unlimited Forms
50 Spam Blocks

Bronze Tier
$ 9/mo.
Unlimited spam blocks, more form tools, and submissions.
1 Website
Unlimited Forms
Unlimited Spam Blocks
Silver Tier
$ 29/mo.
Build more powerful forms with integrations and webhooks.
3 Websites
Unlimited Forms
Unlimited Spam Blocks
4 Users
Gold Tier
$ 79/mo.
Enjoy more team members and everything with increased limits.
10 Websites
Unlimited Forms
Unlimited Spam Blocks
11 Users
Basin Standard Tier
$ 4.17/mo. (billed annually)
Premium Tier
$ 12.50/mo. (billed annually)
FieldGoal Single Tier
1 form
$ 5/mo.
Freelancer Tier
5 forms
$ 15/mo.
Studio Tier
15 forms
$ 39/mo.
Agency Tier
50 forms
$ 79/mo.
FormCarry Baby Tier
Free
Basic Tier
$ 15/mo.
Growth Tier
$ 40/mo.
FormKeep Starter Tier
$ 4.99/mo.
Starter Pack
$ 7.50 per form per month
Freelancer Tier
$ 5.90 per form per month
Agency Tier
$ 3.30 per form per month
Formspree Free Tier
$ 0/mo.
Gold Tier
$ 10/mo.
Platform Tier
$ 40/mo.
FormSubmit Unlimited
formX Free Tier
$ 0/mo.
100 submissions max.
Starter Tier
$ 4.99/mo.
SMBs & Freelancers
$ 49.99/mo.
Business & Agencies
$ 99.99/mo.
Getform Free Tier
$ 0/mo.
Basic Tier
Perfect for small businesses
$ 7.5/mo.
$ 90 per year
Agency Tier
$ 24/mo.
$ 290 annually
Enterprise Tier
$ 57.5/mo.
$ 690 annually
Netlify Forms Level 0
$ 0
100 submissions/mo.
10MB uploads/mo.
Level 1
$ 19/mo. per site
1,000 submissions/mo.
1GB uploads/mo.
Level 2
Custom pricing and limits

Static form providers have different plans, from entirely free plans and trials, to enterprise plans for every business need. Depending on a plan, you might have different features enabled. For example, FormSubmit is the only provider that offers all of its features for free, though it doesn’t support every feature we’ve covered here. You will want to invest some time to learn about which features that are most important for you and your product or business. Then go ahead and decide on which provider is most suitable for your needs.

Wrapping up

Having a form of any kind is a must-have for a large number of sites. When you use a static site generator, you might discover that static form providers make adding forms to a site almost trivial. By following a few rules for enabling static forms, you could benefit from essential features like spam protection and email notifications.

I have been using Kwes for a while now and I can honestly tell you it is a great product that fulfills all of my needs. It has smart spam protection, an easy-to-use dashboard, and impressive validation, both on the front end and back end.

Before choosing your static form providers, be sure to put down all requirements to the paper, and then find your perfect provider.

The post A Comparison of Static Form Providers appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

Form Validation in Under an Hour with Vuelidate

Form validation has a reputation for being tricky to implement. In this tutorial, we’ll break things down to alleviate some of that pain. Creating nice abstractions for forms is something that Vue.js excels at and Vuelidate is personally my favorite option for validations because it doesn’t require a lot of hassle. Plus, it’s really flexible, so we don’t even have to do it how I’m going to cover it here. This is just a launching point.

If you simply want to copy and paste my full working example, it’s at the end. Go ahead. I won’t tell. Then your time spent is definitely under an hour and more, like, two minutes amirite?! Ahh, the internet is a beautiful place.

You may find you need to modify the form we’re using in this post so, in that case, you can read the full thing. We’ll start with a simple case and gradually build out a more concrete example. Finally, we’ll go through how to show form errors when the user has completed the form.

Simplest case: showing the entry once you’re done with the input

First, let’s show how we’d work with Vuelidate. We’ll need to create an object called validations that will mirror the data structure of what we’re trying to capture in the form. In the simplest terms, it would look like this:

data: {   name: ‘’ }, validations: {   name: {     required   } }

This would create an object within computed properties that we can find with $ v. It looks like this in Vue DevTools:

computed properties when empty

A couple things to note here: $ v is a computed property. This is great because that means it’s cached until something updates, which is a very performant way to deal with these state changes. Check out my article here if you want more background on this concept.

Another thing to note: there are two objects — one general object about all validations (there’s only one here currently) and one about the property name in specific. This is great because if we’re looking for general information about all fields, we have that information. And if we need to gather specific data, we have that too.

Let’s take a look at what happens when we start typing in that input:

random typing shown in computed properties

We can see in data that we have… well, me typing like a lunatic. But let’s check out some of these other fields. $ dirty, in this case, refers to whether the form has been touched at all. We can also see that the $ model field is now filled in for the name object, which mirrors what’s in data.

$ error and $ invalid sound the same but are actually a little different. $ invalid is checking if it passes validation, but $ error checks both for something that’s $ invalid and whether or not it’s $ dirty (whether the form has been touched yet or not). If this all seems like a lot to parse (haha get it? parse?), don’t worry, we’ll walk through many of these pieces step by step.

Installing Vuelidate and creating our first form validation

OK, so that was a very simple example. Let’s build something real out of it. We’ll bring this into our application and this time we’ll make the field required and give it a minimum length requirement. In the Vue app, we’ll first add Vuelidate:

yarn add vuelidate

Now, let’s go into the main.js file and update it as follows:

import Vue from 'vue'; import Vuelidate from "vuelidate"; import App from './App.vue'; import store from './store';  Vue.use(Vuelidate); Vue.config.productionTip = false  new Vue({  store,  render: h => h(App) }).$ mount('#app')

Now, in whatever component holds the form element, let’s first import the validators we’ll need:

import { required, minLength } from 'vuelidate/lib/validators'

Then, we’ll put the data inside of a function so we can reuse the component. You likely know about that one. Next, we’ll put our name form field in an object, because typically, we’d want to capture all of the form data together.

We’ll also need to include the validations, which will mirror our data. We’ll use required again, but this time we’ll also add a key/value pair for the minimum length of the characters, minLength(x), which will look something like this:

<script> import { required, minLength } from 'vuelidate/lib/validators'  export default {  data() {    return {      formResponses: {        name: '',      }    }  },  validations: {    formResponses: {      name: {        required,         minLength: minLength(2)      },    }  } } </script>

Next, in the template, we’ll create a label for accessibility purposes. Instead of using what’s in the data to create the relationship in v-model, we’ll use that computed property ($ model) that we saw earlier in the validations object.

<template>  <div id="app">    <label for="fname">Name*</label>    <input id="fname" class="full" v-model="$ v.formResponses.name.$ model" type="text">  </div> </template>

Finally, beneath the form input, we’ll place some text beneath the form. We can use required attached to formResponses.name to see if it evaluates correctly and whether it’s provided at all. We can also see if there’s more than the minimum length of characters. We even have a params object that will tell us the number of characters we specified. We’ll use all of this to create informative error messages for our user.

<p class="error" v-if="!$ v.formResponses.name.required">this field is required</p> <p class="error" v-if="!$ v.formResponses.name.minLength">Field must have at least {{ $ v.formResponses.name.$ params.minLength.min }} characters.</p>

And we’ll style our error class so it’s clear at a glance that they’re errors.

.error {   color: red; }

Be a little lazy

You may have noticed in that last demo that the errors are present right away and update while typing. Personally, I don’t like to show form validations that way because I think it’s distracting and confusing. What I like to do is wait to evaluate until typing has completed. For that kind of interaction, Vue comes equipped with a modifier for v-model: v-model.lazy. This will only evaluate the two-way binding once the user has completed the task with the input.

We can now improve on our single form input like this:

<label for="fname">Name*</label> <input id="fname" class="full" v-model.lazy="$ v.formResponses.name.$ model" type="text">

Creating custom validators

Vuelidate comes with a lot of validators out of the box, which is really helpful. However, there are times when we need something a little more custom. Let’s make a custom validator for a strong password, and check that it matches with Vuelidate’s sameAs validator

The first thing we’ll do is make a label attached to an input, and the input will be type="password".

<section>   <label for="fpass1">Password*</label>   <input id="fpass1" v-model="$ v.formResponses.password1.$ model" type="password"> </section>

In our data, we’ll create password1 and password2 (which we’ll use these in a moment to validate matching passwords) in our formResponses object, and import what we need from the validators.

import { required, minLength, email, sameAs } from "vuelidate/lib/validators";  export default {  data() {    return {      formResponses: {        name: null,        email: null,        password1: null,        password2: null      }    };  },

Then, we’ll create our custom validator. In the code below you can see that we’re using regex for different types of evaluation. We’ll create a strongPassword method, passing in our password1, and then we can check it several ways with .test(), which works as you might expect: it has to pass true if it is passing and false if not.

validations: {   formResponses: {     name: {       required,       minLength: minLength(3)     },     email: {       required,       email     },     password1: {       required,       strongPassword(password1) {         return (           /[a-z]/.test(password1) && // checks for a-z           /[0-9]/.test(password1) && // checks for 0-9           /W|_/.test(password1) && // checks for special char           password1.length >= 6         );       }     },  }

I am separating out each line so you can see what’s going on, but we could also write the whole thing as a one-liner like this:

const regex = /^[a-zA-Z0-9!@#$ %^&*)(+=._-]{6,}$ /g

I prefer to break it out because it is easier to modify.

This allows us to make the error text for our validation. We can make it say whatever we like, or even take this out of a v-if and make it present on the page. Up to you!

<section>   <label for="fpass1">Password*</label>   <input id="fpass1" v-model="$ v.formResponses.password1.$ model" type="password">   <p class="error" v-if="!$ v.formResponses.password1.required">this field is required</p>   <p class="error" v-if="!$ v.formResponses.password1.strongPassword">Strong passwords need to have a letter, a number, a special character, and be more than 8 characters long.</p> </section>

Now we can check if the second password matches the first with Vuelidate’s sameAs method:

validations: {   formResponses: {     password1: {       required,       strongPassword(password1) {         return (           /[a-z]/.test(password1) && // checks for a-z           /[0-9]/.test(password1) && // checks for 0-9           /W|_/.test(password1) && // checks for special char           password1.length >= 6         );       }     },     password2: {       required,       sameAsPassword: sameAs("password1")     }   } }

And we can create our second password field:

<section>   <label for="fpass2">Please re-type your Password</label>   <input id="fpass2" v-model="$ v.formResponses.password2.$ model" type="password">   <p class="error" v-if="!$ v.formResponses.password2.required">this field is required</p>   <p class="error" v-if="!$ v.formResponses.password2.sameAsPassword">The passwords do not match.</p> </section>

Now you can see the whole thing in action all together:

Evaluate on completion

You can see how noisy that last example is until the form has been completed. In my opinion, a better route is to evaluate when the entire form is completed so the user isn’t interrupted in the process. Here’s how we can do that.

Remember when we looked at the computed properties $ v contained? It had objects for all the individual properties, but also one for all validations as well. Inside, there were three very important values:

  • $ anyDirty: if the form was touched at all or left blank
  • $ invalid: if there are any errors in the form
  • $ anyError: if there are any errors at all (even one), this will evaluate to true

You can use $ invalid, but I prefer $ anyError, because it doesn’t require us to check if it’s dirty as well.

Let’s improve on our last form. We’ll put in a submit button, and a uiState string to keep track of, well, the UI state! This is incredibly useful as we can keep track of whether we’ve attempted submission, and whether we’re ready to send what we’ve collected. We’ll also make a small style improvement: position the error on the form so that it’s not moving around to in order to show the errors.

First, let’s add a few new data properties:

data() {   return {     uiState: "submit not clicked",     errors: false,     empty: true,     formResponses: {       ...     }   } }

Now, we’ll add in a submit button at the end of the form. The .prevent modifier at the end of the @click directive acts like preventDefault, and keeps the page from reloading:

<section>   <button @click.prevent="submitForm" class="submit">Submit</button> </section>

We’ll handle some different states in the submitForm method. We’re going to use that computed property from Vuelidate ($ anyDirty) to see if the form is empty. Remember, we can gather that information from this.$ v. We used the formResponses object to hold all the form responses, so what we’ll use is this.$ v.formResponses.$ anyDirty. We’ll map that value to our “empty” data property. We’ll also do the same with errors and we’ll change the uiState to "submit clicked":

submitForm() {   this.formTouched = !this.$ v.formResponses.$ anyDirty;   this.errors = this.$ v.formResponses.$ anyError;   this.uiState = "submit clicked";   if (this.errors === false && this.formTouched === false) {     //this is where you send the responses     this.uiState = "form submitted";   } }

If the form has no errors and it’s not empty, we’ll send the responses and change the uiState to “form submitted” as well.

Now, we can handle some states for errors and empty states as well and, finally, if the form is submitted, we’ll evaluate a success.

<section>   <button @click.prevent="submitForm" class="submit">Submit</button>   <p v-if="errors" class="error">The form above has errors,     <br>please get your act together and resubmit   </p>   <p v-else-if="formTouched && uiState === 'submit clicked'" class="error">The form above is empty,     <br>cmon y'all you can't submit an empty form!   </p>   <p v-else-if="uiState === 'form submitted'" class="success">Hooray! Your form was submitted!</p> </section>

In this form, we’ve given each section relative positioning and added a little padding at the bottom. That will allow us to give absolute positioning to the error state, which will prevent the form from moving around.

.error {   color: red;    font-size: 12px;   position: absolute;   text-transform: uppercase; }

There’s one last thing we need to do: now that we’ve placed the errors in the form absolutely, they’ll stack on top of each other unless we place them next to each other instead. We also want to check if the form is in the error state, which will be true only after the submit button is clicked. This can be a useful way of doing things- we won’t show the errors until the user is done with the form, which can be less invasive. It’s up to you if you’d like to do it this way or the v-model.lazy example used in previous sections.

Our previous errors looked like this:

<section>   ...   <p class="error" v-if="!$ v.formResponses.password2.required">this field is required</p>   <p class="error" v-if="!$ v.formResponses.password2.sameAsPassword">The passwords do not match.</p>  </section>

Now, they’ll be contained together like this:

<p v-if="errors" class="error">   <span v-if="!$ v.formResponses.password1.required">this field is required.</span>   <span v-if="!$ v.formResponses.password1.strongPassword">Strong passwords need to have a letter, a number, a special character, and be more than 8 characters long.</span> </p>

To make things even easier on you, there’s a library that dynamically figures out what error to display based on your validation. Super cool! If you’re doing something simple, it’s probably too much overhead, but if you have a really complex form, it might save you time 🙂

And there we have it! Our form is validated and we have both errors and empty states when we need them, but none while we’re typing.

Sincere thanks to Damian Dulisz, one of the maintainers for Vuelidate, for proofing this article.

The post Form Validation in Under an Hour with Vuelidate appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Using React and XState to Build a Sign In Form

To make a sign in form with good UX requires UI state management, meaning we’d like to minimize the cognitive load to complete it and reduce the number of required user actions while making an intuitive experience. Think about it: even a relatively simple email and password sign in form needs to handle a number of different states, like empty fields, errors, password requirements, loading and success.

Thankfully, state management is what React was made for and I was able to create a sign in form with it using an approach that features XState, a JavaScript state management library using finite machines.

State management? Finite machines? We’re going to walk through these concepts together while putting together a solid sign in form.

Jumping ahead, here’s what we’re going to build together:

First, let’s set up

We’ll need a few tools before getting started. Here’s what to grab:

Once those are in hand, we can make sure our project folder is set up for development. Here’s an outline of how the files should be structured:

public/   |--src/     |--Loader/     |--SignIn/       |--contactAuthService.js       |--index.jsx       |--isPasswordShort.js       |--machineConfig.js       |--styles.js     |--globalStyles.js     |--index.jsx package.json

A little background on XState

We already mentioned that XState is a state management JavaScript library. Its approach uses finite state machines which makes it ideal for this sort of project. For example:

  • It is a thoroughly tried and tested approach to state management. Finite state machines have been around for 30+ years.
  • It is built accordance to specification.
  • It allows logic to be completely separated from implementation, making it easily testable and modular.
  • It has a visual interpreter which gives great feedback of what’s been coded and makes communicating the system to another person that much easier.

For more information on finite-state machines check out David Khourshid’s article.

Machine Config

The machine config is the core of XState. It is a statechart and it will define the logic of our form. I have broken it down into the following parts, which we’ll go over one by one.

1. The States

We need a way to control what to show, hide, enable and disable. We will control this using named-states, which include:

dataEntry: This is the state when the user can enter an email and password into the provided fields. We can consider this the default state. The current field will be highlighted in blue.

awaitingResponse: This is after the browser makes a request to the authentication service and we are waiting for the response. We’ll disable the form and replace the button with a loading indicator when the form is in this state.

emailErr: Whoops! This state is thrown when there is a problem with the email address the user has entered. We’ll highlight that field, display the error, and disable the other field and button.

passwordErr: This is another error state, this time when there is a problem with the password the user has entered. Like the previous error, we’ll highlight the field, display the error, and disable the rest of the form.

serviceErr: We reach this state when we are unable contact the authentication service, preventing the submitted data to be checked. We’ll display an error along with a “Retry” button to re-attempt a service connection.

signedIn: Success! This is when the user has successfully authenticated and proceeds past the sign in form. Normally, this would take the user to some view, but we’ll simply confirm authentication since we’re focusing solely on the form.

See the machinConfig.js file in the SignIn directory? Crack that open so we can define our states. We list them as properties of a states object. We also need to define an initial state, which mentioned earlier, will be the dataEntry state, allowing the user to enter data into the form fields.

const machineConfig = {   id: 'signIn',   initial: 'dataEntry',   states: {     dataEntry: {},     awaitingResponse: {},     emailErr: {},     passwordErr: {},     serviceErr: {},     signedIn: {},   } }  export default machineConfig

Each part of this article will show the code of machineConfig.js along with a diagram produced from the code using XState’s visualizer.

2. The Transitions

Now that we have defined our states, we need to define how to change from one state to another and, in XState, we do that with a type of event called a transition. We define transitions within each state. For example, If the ENTER_EMAIL transition is triggered when we’re in the emailErr state, then the system will move to state dataEntry.

emailErr: {   on: {     ENTER_EMAIL: {       target: 'dataEntry'     }   } }

Note that nothing would happen if a different type of transition was triggered (such as ENTER_PASSWORD) while in the emailErr state. Only transitions that are defined within the state are valid.

When a transition has no target, it is an external (by default) self-transition. When triggered, the state will exit and re-enter itself. As an example, the machine will change from dataEntry back to dataEntry when the ENTER_EMAIL transition is triggered.

Here’s how that is defined:

dataEntry: {   on: {     ENTER_EMAIL: {}   } }

Sounds weird, I know, but we’ll explain it a little later. Here’s the machineConfig.js file so far.

const machineConfig = {   id: 'signIn',   initial: 'dataEntry',   states: {     dataEntry: {       on: {         ENTER_EMAIL: {},         ENTER_PASSWORD: {},         EMAIL_BLUR: {},         PASSWORD_BLUR: {},         SUBMIT: {           target: 'awaitingResponse',         },       },     },     awaitingResponse: {},     emailErr: {       on: {         ENTER_EMAIL: {           target: 'dataEntry',         },       },     },     passwordErr: {       on: {         ENTER_PASSWORD: {           target: 'dataEntry',         },       },     },     serviceErr: {       on: {         SUBMIT: {           target: 'awaitingResponse',         },       },     },     signedIn: {},   }, };  export default machineConfig;

3. Context

We need a way to save what the user enters into the input fields. We can do that in XState with context, which is an object within the machine that enables us to store data. So, we’ll need to define that in our file as well.

Email and password are both empty strings by default. When the user enters their email or password, this is where we’ll store it.

const machineConfig = {   id: 'signIn',   context: {     email: '',     password: '',   },   ...

4. Hierarchical States

We will need a way to be more specific about our errors. Instead of simply telling the user there is an email error, we need to tell them what kind of error happened. Perhaps it’s email with the wrong format or there is no account linked to the entered email — we should let the user know so there’s no guessing. This is where we can use hierarchical states which are essentially state machines within state machines. So, instead of having a emailErr state, we can add sub-states, such as emailErr.badFormat or emailErr.noAccount.

For the emailErr state, we have defined two sub-states: badFormat and noAccount. This means the machine can no longer only be in the emailErr state; it would be either in the emailErr.badFormat state or the emailErr.noAccount state and having them parsed out allows us to provide more context to the user in the form of unique messaging in each sub-state.

const machineConfig = {   ...   states: {     ...     emailErr: {       on: {         ENTER_EMAIL: {           target: 'dataEntry',         },       },       initial: 'badFormat',       states: {         badFormat: {},         noAccount: {},       },     },     passwordErr: {       on: {         ENTER_PASSWORD: {           target: 'dataEntry',         },       },       initial: 'tooShort',       states: {         tooShort: {},         incorrect: {},       },     },     ...

5. Guards

When the user blurs an input or clicks submit, we need to check if the email and/or password are valid. If even one of those values is in a bad format, we need to prompt the user to change it. Guards allows us to transition to a state depending on those types of conditions.

Here, we’re using the EMAIL_BLUR transition to change the state to emailErr.badFormat only if the condition isBadEmailFormat returns true. We are doing a similar thing to PASSWORD_BLUR.

We’re also changing the SUBMIT transition’s value to an array of objects with a target and condition property. When the SUBMIT transition is triggered, the machine will go through each of the conditions, from first to last, and change the state of the first condition that returns true. For example, if isBadEmailFormat returns true, the machine will change to state emailErr.badFormat. However, if isBadEmailFormat returns false, the machine will move to the next condition statement and check if it returns true.

const machineConfig = {   ...   states: {     ...     dataEntry: {       ...       on: {         EMAIL_BLUR: {           cond: 'isBadEmailFormat',           target: 'emailErr.badFormat'         },         PASSWORD_BLUR: {           cond: 'isPasswordShort',           target: 'passwordErr.tooShort'         },         SUBMIT: [           {             cond: 'isBadEmailFormat',             target: 'emailErr.badFormat'           },           {             cond: 'isPasswordShort',             target: 'passwordErr.tooShort'           },           {             target: 'awaitingResponse'           }         ],       ...

6. Invoke

All of the work we’ve done so far would be for nought if we didn’t make a request to an authentication service. The result of what’s entered and submitted to the form will inform many of the states we defined. So, invoking that request should result in one of two states:

  • Transition to the signedIn state if it returns successfully, or
  • transition to one of our error states if it fails.

The invoke method allows us to declare a promise and transition to different states, depending on what that promise returns. The src property takes a function that has two parameters: context and event (but we’re only using context here). We return a promise (our authentication request) with the values of email and password from the context. If the promise returns successfully, we will transition to the state defined in the onDone property. If an error is returned, we will transition to the state defined in the onError property.

const machineConfig = {   ...   states: {     ...     // We’re in a state of waiting for a response     awaitingResponse: {       // Make a call to the authentication service             invoke: {         src: 'requestSignIn',         // If successful, move to the signedIn state         onDone: {           target: 'signedIn'         },         // If email input is unsuccessful, move to the emailErr.noAccount sub-state         onError: [           {             cond: 'isNoAccount',             target: 'emailErr.noAccount'           },           {             // If password input is unsuccessful, move to the passwordErr.incorrect sub-state             cond: 'isIncorrectPassword',             target: 'passwordErr.incorrect'           },           {             // If the service itself cannot be reached, move to the serviceErr state             cond: 'isServiceErr',             target: 'serviceErr'           }         ]       },     },     ...

7. Actions

We need a way to save what the user enters into the email and password fields. Actions enable side effects to be triggered when a transition occurs. Below, we have defined an action (cacheEmail) within the ENTER_EMAIL transition of the dataEntry state. This means if the machine is in dataEntry and the transition ENTER_EMAIL is triggered, the action cacheEmail will also be triggered.

const machineConfig = {   ...   states: {     ...     // On submit, target the two fields     dataEntry: {       on: {         ENTER_EMAIL: {           actions: 'cacheEmail'         },         ENTER_PASSWORD: {           actions: 'cachePassword'         },       },       ...     },     // If there’s an email error on that field, trigger email cache action     emailErr: {       on: {         ENTER_EMAIL: {           actions: 'cacheEmail',           ...         }       }     },     // If there’s a password error on that field, trigger password cache action     passwordErr: {       on: {         ENTER_PASSWORD: {           actions: 'cachePassword',           ...                 }       }     },     ...

8. Final State

We need to way to indicate if the user has successfully authenticated and, depending on the result, trigger the next stage of the user journey. Two things are required for this:

  • We declare that one of the states is the final state, and
  • define an onDone property that can trigger actions when that final state is reached.

Within the signedIn state, we add type: final. We also add an onDone property with action onAuthentication. Now, when the state signedIn is reached, the action onAuthentication will be triggered and the machine will be done (no longer executable).

const machineConfig = {   ...   states: {     ...     signedIn: {       type: 'final'     },     onDone: {       actions: 'onAuthentication'     },     ...

9. Test

A great feature of XState is that the machine configuration is completely independent of the actual implementation. This means we can test it now and get confidence with what we’ve made before connecting it to the UI and backend service. We can copy and paste the machine config file into XState’s visualizer and get a auto-generated statechart diagram that not only outlines all the defined states with arrows that illustrate how they’re all connected, but allows us to interact with the chart as well. This is built-in testing!

Connecting the machine to a React component

Now that we’ve written our statechart, it’s time to connect it to our UI and backend service. An XState machine options object allows us to map strings we declared in the config to functions.

We’ll begin by defining a React class component with three refs:

// SignIn/index.jsx  import React, { Component, createRef } from 'react'  class SignIn extends Component {   emailInputRef = createRef()   passwordInputRef = createRef()   submitBtnRef = createRef()      render() {     return null   } }  export default SignIn

Map out the actions

We declared the following actions in our machine config:

  • focusEmailInput
  • focusPasswordInput
  • focusSubmitBtn
  • cacheEmail
  • cachePassword
  • onAuthentication

Actions are mapped in the machine config’s actions property. Each function takes two arguments: context (ctx) and event (evt).

focusEmailInput and focusPasswordInput are pretty straightforward, however, there is a bug. These elements are being focused when coming from a disabled state. The function to focus these elements is firing right before the elements are re-enabled. The delay function gets around that.

cacheEmail and cachePassword need to update the context. To do this, we use the assign function (provided by XState). Whatever is returned by the assign function is added to our context. In our case, it is reading the input’s value from the event object and then adding that value to the context’s email or password. From there property.assign is added to the context. Again, in our case, it is reading the input’s value from the event object and adding that value to the context’s email or password property.

// SignIn/index.jsx  import { actions } from 'xstate' const { assign } = actions    const delay = func => setTimeout(() => func())  class SignIn extends Component {   ...   machineOptions = {     actions: {       focusEmailInput: () => {         delay(this.emailInputRef.current.focus())       },       focusPasswordInput: () => {         delay(this.passwordInputRef.current.focus())       },       focusSubmitBtn: () => {         delay(this.submitBtnRef.current.focus())       },       cacheEmail: assign((ctx, evt) => ({         email: evt.value       })),       cachePassword: assign((ctx, evt) => ({         password: evt.value       })),       // We’ll log a note in the console to confirm authentication       onAuthentication: () => {         console.log('user authenticated')       }     },   } }

Put up our guards

We declared the following guards in our machine config:

  • isBadEmailFormat
  • isPasswordShort
  • isNoAccount
  • isIncorrectPassword
  • isServiceErr

Guards are mapped in the machine configuration’s guards property. The isBadEmailFormat and isPasswordShort guards make use of the context to read the email and password entered by the user then pass them on to the appropriate functions. isNowAccount, isIncorrectPassword and isServiceErr make use of the event object to read what kind of error was returned from the call to the authentication service.

// isPasswordShort.js  const isPasswordShort = password => password.length < 6  export default isPasswordShort
// SignIn/index.jsx  import { isEmail } from 'validator' import isPasswordShort from './isPasswordShort'  class SignIn extends Component {   ...   machineOptions = {     ...     guards: {       isBadEmailFormat: ctx => !isEmail(ctx.email),       isPasswordShort: ctx => isPasswordShort(ctx.password),       isNoAccount: (ctx, evt) => evt.data.code === 1,       isIncorrectPassword: (ctx, evt) => evt.data.code === 2,       isServiceErr: (ctx, evt) => evt.data.code === 3     },     },   ... }

Hook up the services

We declared the following service in our machine configuration (within our invoke definition): requestSignIn.

Services are mapped in the machine configuration’s services property. In this case, the function is a promise and is passed to the email password from the context.

// contactAuthService.js // error code 1 - no account // error code 2 - wrong password // error code 3 - no response  const isSuccess = () => Math.random() >= 0.8 const generateErrCode = () => Math.floor(Math.random() * 3) + 1  const contactAuthService = (email, password) =>   new Promise((resolve, reject) => {     console.log(`email: $ {email}`)     console.log(`password: $ {password}`)     setTimeout(() => {       if (isSuccess()) resolve()       reject({ code: generateErrCode() })     }, 1500) })  export default contactAuthService
// SignIn/index.jsx ... import contactAuthService from './contactAuthService.js'  class SignIn extends Component {   ...   machineOptions = {     ...     services: {       requestSignIn: ctx => contactAuthService(ctx.email, ctx.password)     }   },   ... }

react-xstate-js connects React and XState

Now that we have our machine config and options at the ready, we can create the actual machine! In order to use XState in a real world scenario, that requires an interpreter. react-xstate-js is an interpreter that connects React with XState using the render props approach. (Full disclosure, I developed this library.) It takes two props — config and options — and returns an XState service and state object.

// SignIn/index.jsx ... import { Machine } from 'react-xstate-js' import machineConfig from './machineConfig'  class SignIn extends Component {   ...   render() {     <Machine config={machineConfig} options={this.machineOptions}>       {({ service, state }) => null}     </Machine>   } }

Let’s make the UI!

OK, we have a functional machine but the user needs to see the form in order to use it. That means it’s time to create the markup for the UI component. There are two things we need to do to communicate with our machine:

1. Read the state

To determine what state we are in, we can use the state’s matches method and return a boolean. For example: state.matches('dataEntry').

2. Fire a transition

To fire a transition, we use the service’s send method. It takes an object with the transitions type we want to trigger as well as any other key and value pairs we want in the evt object. For example: service.send({ type: 'SUBMIT' }).

// SignIn/index.jsx  ... import {   Form,   H1,   Label,   Recede,   Input,   ErrMsg,   Button,   Authenticated,   MetaWrapper,   Pre } from './styles'  class SignIn extends Component {   ...   render() {     <Machine config={machineConfig} options={this.machineOptions}>       {({ service, state }) => {         const disableEmail =           state.matches('passwordErr') ||           state.matches('awaitingResponse') ||           state.matches('serviceErr')                    const disablePassword =           state.matches('emailErr') ||           state.matches('awaitingResponse') ||           state.matches('serviceErr')                  const disableSubmit =           state.matches('emailErr') ||           state.matches('passwordErr') ||           state.matches('awaitingResponse')                  const fadeHeading =           state.matches('emailErr') ||           state.matches('passwordErr') ||           state.matches('awaitingResponse') ||           state.matches('serviceErr')          return (           <Form             onSubmit={e => {               e.preventDefault()               service.send({ type: 'SUBMIT' })             }}             noValidate           >             <H1 fade={fadeHeading}>Welcome Back</H1>              <Label htmlFor="email" disabled={disableEmail}>               email             </Label>             <Input               id="email"               type="email"               placeholder="charlie@gmail.com"               onBlur={() => {                 service.send({ type: 'EMAIL_BLUR' })               }}               value={state.context.email}               err={state.matches('emailErr')}               disabled={disableEmail}               onChange={e => {                 service.send({                   type: 'ENTER_EMAIL',                   value: e.target.value                 })               }}               ref={this.emailInputRef}               autoFocus             />             <ErrMsg>               {state.matches({ emailErr: 'badFormat' }) &&                 "email format doesn't look right"}               {state.matches({ emailErr: 'noAccount' }) &&                 'no account linked with this email'}             </ErrMsg>                          <Label htmlFor="password" disabled={disablePassword}>               password <Recede>(min. 6 characters)</Recede>             </Label>             <Input               id="password"               type="password"               placeholder="Passw0rd!"               value={state.context.password}               err={state.matches('passwordErr')}               disabled={disablePassword}               onBlur={() => {                 service.send({ type: 'PASSWORD_BLUR' })               }}               onChange={e => {                 service.send({                   type: 'ENTER_PASSWORD',                   value: e.target.value                 })               }}               ref={this.passwordInputRef}             />             <ErrMsg>               {state.matches({ passwordErr: 'tooShort' }) &&                 'password too short (min. 6 characters)'}               {state.matches({ passwordErr: 'incorrect' }) &&                 'incorrect password'}             </ErrMsg>                          <Button               type="submit"               disabled={disableSubmit}               loading={state.matches('awaitingResponse')}               ref={this.submitBtnRef}             >               {state.matches('awaitingResponse') && (                 <>                   loading                   <Loader />                 </>               )}               {state.matches('serviceErr') && 'retry'}               {!state.matches('awaitingResponse') &&                 !state.matches('serviceErr') &&                 'sign in'               }             </Button>             <ErrMsg>               {state.matches('serviceErr') && 'problem contacting server'}             </ErrMsg>              {state.matches('signedIn') && (               <Authenticated>                 <H1>authenticated</H1>               </Authenticated>             )}           </Form>         )       }}     </Machine>   } }

We have a form!

And there you have it. A sign in form that has a great user experience controlled by XState. Not only were we able to create a form a user can interact with, we also put a lot of thought into the many states and types of interactions that’s need to be considered, which is a good exercise for any piece of functionality that would go into a component.

Hit up the comments form if there’s something that doesn’t make sense or if there’s something else you think might need to be considered in the form. Would love to hear your thoughts!

More resources

The post Using React and XState to Build a Sign In Form appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]