Tag: Form

Generate a Pull Request of Static Content With a Simple HTML Form

Jamstack has been in the website world for years. Static Site Generators (SSGs) — which often have content that lives right within a GitHub repo itself — are a big part of that story. That opens up the idea of having contributors that can open pull requests to add, change, or edit content. Very useful!

Examples of this are like:

Why built with a static site approach?

When we need to build content-based sites like this, it’s common to think about what database to use. Keeping content in a database is a time-honored good idea. But it’s not the only approach! SSGs can be a great alternative because…

  • They are cheap and easy to deploy. SSGs are usually free, making them great for an MVP or a proof of concept.
  • They have great security. There is nothing to hack through the browser, as all the site contains is often just static files.
  • You’re ready to scale. The host you’re already on can handle it.

There is another advantage for us when it comes to a content site. The content of the site itself can be written in static files right in the repo. That means that adding and updating content can happen right from pull requests on GitHub, for example. Even for the non-technically inclined, it opens the door to things like Netlify CMS and the concept of open authoring, allowing for community contributions.

But let’s go the super lo-fi route and embrace the idea of pull requests for content, using nothing more than basic HTML.

The challenge

How people contribute adding or updating a resource isn’t always perfectly straightforward. People need to understand how to fork your repository, how to and where to add their content, content formatting standards, required fields, and all sorts of stuff. They might even need to “spin up” the site themselves locally to ensure the content looks right.

People who seriously want to help our site sometimes will back off because the process of contributing is a technological hurdle and learning curve — which is sad.

You know what anybody can do? Use a <form>

Just like a normal website, the easy way for people to submit a content is to fill out a form and submit it with the content they want.

What if we can make a way for users to contribute content to our sites by way of nothing more than an HTML <form> designed to take exactly the content we need? But instead of the form posting to a database, it goes the route of a pull request against our static site generator? There is a trick!

The trick: Create a GitHub pull request with query parameters

Here’s a little known trick: We can pre-fill a pull request against our repository by adding query parameter to a special GitHub URL. This comes right from the GitHub docs themselves.

Let’s reverse engineer this.

If we know we can pre-fill a link, then we need to generate the link. We’re trying to make this easy remember. To generate this dynamic data-filled link, we’ll use a touch of JavaScript.

So now, how do we generate this link after the user submits the form?

Demo time!

Let’s take the Serverless site from CSS-Tricks as an example. Currently, the only way to add a new resource is by forking the repo on GitHub and adding a new Markdown file. But let’s see how we can do it with a form instead of jumping through those hoops.

The Serverless site itself has many categories (e.g. for forms) we can contribute to. For the sake of simplicity, let’s focus on the “Resources” category. People can add articles about things related to Serverless or Jamstack from there.

The Resources page of the CSS-Tricks Serverless site. The site is primarily purple in varying shades with accents of orange. The page shows a couple of serverless resources in the main area and a list of categories in the right sidebar.

All of the resource files are in this folder in the repo.

Showing the main page of the CSS-Tricks Serverless repo in GitHub, displaying all the files.

Just picking a random file from there to explore the structure…

--- title: "How to deploy a custom domain with the Amplify Console" url: "https://read.acloud.guru/how-to-deploy-a-custom-domain-with-the-amplify-console-a884b6a3c0fc" author: "Nader Dabit" tags: ["hosting", "amplify"] ---  In this tutorial, we’ll learn how to add a custom domain to an Amplify Console deployment in just a couple of minutes.

Looking over that content, our form must have these columns:

  • Title
  • URL
  • Author
  • Tags
  • Snippet or description of the link.

So let’s build an HTML form for all those fields:

<div class="columns container my-2">   <div class="column is-half is-offset-one-quarter">   <h1 class="title">Contribute to Serverless Resources</h1>    <div class="field">     <label class="label" for="title">Title</label>     <div class="control">       <input id="title" name="title" class="input" type="text">     </div>   </div>      <div class="field">     <label class="label" for="url">URL</label>     <div class="control">       <input id="url" name="url" class="input" type="url">     </div>   </div>        <div class="field">     <label class="label" for="author">Author</label>     <div class="control">       <input id="author" class="input" type="text" name="author">     </div>   </div>      <div class="field">     <label class="label" for="tags">Tags (comma separated)</label>     <div class="control">       <input id="tags" class="input" type="text" name="tags">     </div>   </div>        <div class="field">     <label class="label" for="description">Description</label>     <div class="control">       <textarea id="description" class="textarea" name="description"></textarea>     </div>   </div>       <!-- Prepare the JavaScript function for later -->   <div class="control">     <button onclick="validateSubmission();" class="button is-link is-fullwidth">Submit</button>   </div>        </div> </div>

I’m using Bulma for styling, so the class names in use here are from that.

Now we write a JavaScript function that transforms a user’s input into a friendly URL that we can combine as GitHub query parameters on our pull request. Here is the step by step:

  • Get the user’s input about the content they want to add
  • Generate a string from all that content
  • Encode the string to format it in a way that humans can read
  • Attach the encoded string to a complete URL pointing to GitHub’s page for new pull requests

Here is the Pen:

After pressing the Submit button, the user is taken right to GitHub with an open pull request for this new file in the right location.

GitHub pull request screen showing a new file with content.

Quick caveat: Users still need a GitHub account to contribute. But this is still much easier than having to know how to fork a repo and create a pull request from that fork.

Other benefits of this approach

Well, for one, this is a form that lives on our site. We can style it however we want. That sort of control is always nice to have.

Secondly, since we’ve already written the JavaScript, we can use the same basic idea to talk with other services or APIs in order to process the input first. For example, if we need information from a website (like the title, meta description, or favicon) we can fetch this information just by providing the URL.

Taking things further

Let’s have a play with that second point above. We could simply pre-fill our form by fetching information from the URL provided for the user rather than having them have to enter it by hand.

With that in mind, let’s now only ask the user for two inputs (rather than four) — just the URL and tags.

How does this work? We can fetch meta information from a website with JavaScript just by having the URL. There are many APIs that fetch information from a website, but you might the one that I built for this project. Try hitting any URL like this:

https://metadata-api.vercel.app/api?url=https://css-tricks.com

The demo above uses that as an API to pre-fill data based on the URL the user provides. Easier for the user!

Wrapping up

You could think of this as a very minimal CMS for any kind of Static Site Generator. All you need to do is customize the form and update the pre-filled query parameters to match the data formats you need.

How will you use this sort of thing? The four sites we saw at the very beginning are good examples. But there are so many other times where might need to do something with a user submission, and this might be a low-lift way to do it.


The post Generate a Pull Request of Static Content With a Simple HTML Form appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , ,

How to Create a Contact Form With Next.js and Netlify

We’re going to create a contact form with Next.js and Netlify that displays a confirmation screen and features enhanced spam detection.

Next.js is a powerful React framework for developing performant React applications that scale. By integrating a Next.js site with Netlify’s technology, we can quickly get a working contact form up and running without having to write any server-side code.

Not only is it a relatively fast process to set up forms to be processed by Netlify, but it’s also free to get started (with up to 100 free submissions/per site hosted on Netlify). Form submissions automatically go through Netlify’s built-in spam filter which uses Akismet and there are also options that can be configured to increase the level of spam detection.

Creating the contact form

Within the Next.js application we should create a ContactForm component to render the contact form inside of the contact page. If you’d like for this form to render at /contact, then the ContactForm component below with labels and input fields should be used within the pages/contact.js file.

const ContactForm = (   <form     name="contact-form"     method="POST"     action="contact/?success=true"   >     <label htmlFor="name">Name *</label>     <input       id="name"       name="name"       required       type="text"     />     <label htmlFor="company">Company *</label>     <input id="company" name="company" required type="text" />     <label htmlFor="email">E-mail Address *</label>     <input id="email" type="email" name="email" required />     <label htmlFor="message">Message *</label>     <textarea id="message" name="message" required></textarea>     <button type="submit">Submit</button>   </form> );

The above markup is required to render a form with a field for Name, Company, Email address and message with a submit button. When submitting the form, based on the value of the form’s action, it should redirect to contact/?success=true from /contact. Right now there is not yet a difference between the page’s appearance with and without the success query parameter, but we will update that later.

Our Contact.js file looks like this so far:

import React from "react"; const ContactPage = () => {  const ContactForm = (/* code in above code sample*/)    return (    <div>      <h1>Contact Us</h1>      {ContactForm}    </div>  ); };   export default ContactPage;

Now that we have the basic form set up, the real magic will happen after we add additional information for Netlify to auto-recognize the form during future site deployments. To accomplish this we should update the form to have the attribute data-netlify="true" and a hidden input field that contains the name of our contact form. In Netlify, once we navigate to our site in the dashboard and then click on the “forms” tab  we will be able to view our form responses based on the name that we’ve put in our hidden field. It’s important that if you have multiple forms within a site that they have unique names so that they are recorded properly in Netlify.

<form   method="POST"   name="contact-form"   action="contact/?success=true"   data-netlify="true" > <input type="hidden" name="form-name" value="contact-form" />

After successfully deploying the site to Netlify with the data-netlify attribute and the form-name field  then we can go to the deployed version of the site and fill out the form. Upon submitting the form and navigating to https://app.netlify.com/sites/site-name/forms (where site-name is the name of your site) then our most recent form submission should appear if we have successfully set up the form. 

Redirect to confirmation screen 

In order to improve the user experience, we should add some logic to redirect to a confirmation screen on form submission when the URL changes to /contact/?success=true. There is also the option to redirect to an entirely different page as the action when the form is submitted but using query params we can achieve something similar with the Next Router. We can accomplish this by creating a new variable to determine if the confirmation screen or the form should be visible based on the query parameter. The next/router which is imported with import { useRouter } from "next/router"; can be used to retrieve the current query params. 

const router = useRouter();   const confirmationScreenVisible = router.query?.success && router.query.success === "true";

In our case, the confirmation screen and form can never be visible at the same time; therefore, the following statement can be used to determine if the form is visible or not.

const formVisible = !confirmationScreenVisible; 

To give users the option to resubmit the form, we can add a button to the confirmation screen to reset the form by clearing the query params. Using router.replace (instead of router.push) not only updates the page but replaces the current page in the history to the version without query params. 

<button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>

We can then conditionally render the form based on whether or not the form is visible with:

{formVisible ? ContactForm : ConfirmationMessage}

Putting it all together, we can use the following code to conditionally render the form based on the query params (which are updated when the form is submitted):

import React, { useState } from "react"; import { useRouter } from "next/router";   const ContactPage = () => {  const [submitterName, setSubmitterName] = useState("");  const router = useRouter();  const confirmationScreenVisible =    router.query?.success && router.query.success === "true";  const formVisible = !confirmationScreenVisible;    const ConfirmationMessage = (    <React.Fragment>      <p>        Thank you for submitting this form. Someone should get back to you within 24-48 hours.      </p>        <button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>    </React.Fragment>  );    const ContactForm = (/* code in first code example */);    return (    <div>      <h1>Contact Us</h1> {formVisible ? ContactForm : ConfirmationMessage}    </div>  ); };   export default ContactPage;

Adding a hidden bot field

Now that the core functionality of our form is working, we can add additional spam detection to our form in addition to the base spam detection because Akismet is included with all Netlify Forms by default. We can enable this by adding data-netlify-honeypot="bot-field" to our form.

<form   className="container"   method="POST"   name="contact-form"   action="contact/?success=true"   data-netlify="true"   data-netlify-honeypot="bot-field" >

We also need to create a new hidden paragraph that contains a label named bot-field that contains the input. This field is “visible” to bots, but not humans. When this honeypot form field is filled, Netlify detects a bot and then the submission is flagged as spam.

<p hidden>   <label>     Don’t fill this out: <input name="bot-field" />   </label> </p>

Further customizations

  • We could explore another spam prevention option that Netlify supports by adding reCAPTCHA 2 to a Netlify form.
  • We could update the form to allow uploaded files with input <input type="file">.
  • We could set up notifications for form submissions. That happens over at https://app.netlify.com/sites/[your-site-name]/settings/forms where we can include a custom subject field (which can be hidden) for email notifications.

Full code

The code for the full site code is available over at GitHub.

 Bonus

The following code includes everything we covered as well as the logic for setting a custom subject line with what was submitted in the name field.

import React, { useState } from "react"; import { useRouter } from "next/router";   const ContactPage = () => {  const [submitterName, setSubmitterName] = useState("");  const router = useRouter();  const confirmationScreenVisible =    router.query?.success && router.query.success === "true";  const formVisible = !confirmationScreenVisible;    const ConfirmationMessage = (    <React.Fragment>      <p>        Thank you for submitting this form. Someone should get back to you        within 24-48 hours.      </p>        <button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>    </React.Fragment>  );    const ContactForm = (    <form      className="container"      method="POST"      name="contact-form"      action="contact/?success=true"      data-netlify="true"      data-netlify-honeypot="bot-field"    >      <input        type="hidden"        name="subject"        value={`You've got mail from $ {submitterName}`}      />      <input type="hidden" name="form-name" value="contact-form" />      <p hidden>        <label>          Don’t fill this out: <input name="bot-field" />        </label>      </p>        <label htmlFor="name">Name *</label>      <input        id="name"        name="name"        required        onChange={(e) => setSubmitterName(e.target.value)}        type="text"      />      <label htmlFor="company">Company *</label>      <input id="company" name="company" required type="text" />      <label htmlFor="email">E-mail Address *</label>      <input id="email" type="email" name="email" required />      <label htmlFor="message">Message *</label>      <textarea id="message" name="message" required/>      <button type="submit">Submit</button>    </form>  );    return (    <div>      <h1>Contact Us</h1> {formVisible ? ContactForm : ConfirmationMessage}    </div>  ); };   export default ContactPage;

The post How to Create a Contact Form With Next.js and Netlify appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Building a Form in PHP Using DOMDocument

Templating makes the web go round. The synthesis of data and structure into content. It’s our coolest superpower as developers — grab some data, then make it work for us, in whatever presentation we need. An array of objects can become a table, a list of cards, a chart, or whatever we think is most useful to the user. Whether the data is our own blog posts in Markdown files, or on-the-minute global exchange rates, the markup and resulting UX are up to us as front-end developers.

PHP is an amazing language for templating, providing many ways to merge data with markup. Let’s get into an example of using data to build out an HTML form in this post.

Want to get your hands dirty right away? Jump to the implementation.

In PHP, we can inline variables into string literals that use double quotes, so if we have a variable $ name = 'world', we can write echo "Hello, {$ name}", and it prints the expected Hello, world. For more complex templating, we can always concatenate strings, like: echo "Hello, " . $ name . ".".

For the old-schoolers, there’s printf("Hello, %s", $ name). For multiline strings, you can use Heredoc (the one that starts like <<<MYTEXT). And, last but certainly not least, we can sprinkle PHP variables inside HTML, like <p>Hello, <?= $ name ?></p>.

All of these options are great, but things can get messy when a lot of inline logic is required. If we need to build compound HTML strings, say a form or navigation, the complexity is potentially infinite, since HTML elements can nest inside each other.

What we’re trying to avoid

Before we go ahead and do the thing we want to do, it’s worth taking a minute to consider what we don’t want to do. Consider the following abridged passage from the scripture of WordPress Core, class-walker-nav-menu.php, verses 170-270:

<?php // class-walker-nav-menu.php // ... $  output .= $  indent . '<li' . $  id . $  class_names . '>'; // ... $  item_output  = $  args->before; $  item_output .= '<a' . $  attributes . '>'; $  item_output .= $  args->link_before . $  title . $  args->link_after; $  item_output .= '</a>'; $  item_output .= $  args->after; // ... $  output .= apply_filters( 'walker_nav_menu_start_el', $  item_output, $  item, $  depth, $  args ); // ... $  output .= "</li>{$  n}";

In order to build out a navigation <ul> in this function, we use a variable, $ output, which is a very long string to which we keep adding stuff. This type of code has a very specific and limited order of operations. If we wanted to add an attribute to the <a>, we must have access to $ attributes before this runs. And if we wanted to optionally nest a <span> or an <img> inside the <a>, we’d need to author a whole new block of code that would replace the middle of line 7 with about 4-10 new lines, depending on what exactly we want to add. Now imagine you need to optionally add the <span> , and then optionally add the <img>, either inside the <span> or after it. That alone is three if statements, making the code even less legible.

It’s very easy to end up with string spaghetti when concatenating like this, which is as fun to say as it is painful to maintain.

The essence of the problem is that when we try to reason about HTML elements, we’re not thinking about strings. It just so happens that strings are what the browser consumes and PHP outputs. But our mental model is more like the DOM — elements are arranged into a tree, and each node has many potential attributes, properties, and children.

Wouldn’t it be great if there were a structured, expressive way to build our tree?

Enter…

The DOMDocument class

PHP 5 added the DOM module to it’s roster of Not So Strictly Typed™ types. Its main entry point is the DOMDocument class, which is intentionally similar to the Web API’s JavaScript DOM. If you’ve ever used document.createElement or, for those of us of a certain age, jQuery’s $ ('<p>Hi there!</p>') syntax, this will probably feel quite familiar.

We start out by initializing a new DOMDocument:

$  dom = new DOMDocument();

Now we can add a DOMElement to it:

$  p = $  dom->createElement('p');

The string 'p' represents the type of element we want, so other valid strings would be 'div', 'img' , etc.

Once we have an element, we can set its attributes:

$  p->setAttribute('class', 'headline');

We can add children to it:

$  span = $  dom->createElement('span', 'This is a headline'); // The 2nd argument populates the element's textContent $  p->appendChild($  span);

And finally, get the complete HTML string in one go:

$  dom->appendChild($  p); $  htmlString = $  dom->saveHTML(); echo $  htmlString;

Notice how this style of coding keeps our code organized according to our mental model — a document has elements; elements can have any number of attributes; and elements nest inside one another without needing to know anything about each other. The whole “HTML is just a string” part comes in at the end, once our structure is in place.

The “document” here is a bit different from the actual DOM, in that it doesn’t need to represent an entire document, just a block of HTML. In fact, if you need to create two similar elements, you could save a HTML string using saveHTML(), modify the DOM “document” some more, and then save a new HTML string by calling saveHTML() again.

Getting data and setting the structure

Say we need to build a form on the server using data from a CRM provider and our own markup. The API response from the CRM looks like this:

{   "submit_button_label": "Submit now!",   "fields": [     {       "id": "first-name",       "type": "text",       "label": "First name",       "required": true,       "validation_message": "First name is required.",       "max_length": 30     },     {       "id": "category",       "type": "multiple_choice",       "label": "Choose all categories that apply",       "required": false,       "field_metadata": {         "multi_select": true,         "values": [           { "value": "travel", "label": "Travel" },           { "value": "marketing", "label": "Marketing" }         ]       }     }   ] }

This example doesn’t use the exact data structure of any specific CRM, but it’s rather representative.

And let’s suppose we want our markup to look like this:

<form>   <label class="field">     <input type="text" name="first-name" id="first-name" placeholder=" " required>     <span class="label">First name</span>     <em class="validation" hidden>First name is required.</em>   </label>   <label class="field checkbox-group">     <fieldset>       <div class="choice">         <input type="checkbox" value="travel" id="category-travel" name="category">         <label for="category-travel">Travel</label>       </div>       <div class="choice">         <input type="checkbox" value="marketing" id="category-marketing" name="category">         <label for="category-marketing">Marketing</label>       </div>     </fieldset>     <span class="label">Choose all categories that apply</span>   </label> </form>

What’s that placeholder=" "? It’s a small trick that allows us to track in CSS whether the field is empty, without needing JavaScript. As long as the input is empty, it matches input:placeholder-shown, but the user doesn’t see any visible placeholder text. Just the kind of thing you can do when we control the markup!

Now that we know what our desired result is, here’s the game plan:

  1. Get the field definitions and other content from the API
  2. Initialize a DOMDocument
  3. Iterate over the fields and build each one as required
  4. Get the HTML output

So let’s stub out our process and get some technicalities out of the way:

<?php function renderForm ($  endpoint) {   // Get the data from the API and convert it to a PHP object   $  formResult = file_get_contents($  endpoint);   $  formContent = json_decode($  formResult);   $  formFields = $  formContent->fields;    // Start building the DOM   $  dom = new DOMDocument();   $  form = $  dom->createElement('form');    // Iterate over the fields and build each one   foreach ($  formFields as $  field) {     // TODO: Do something with the field data   }    // Get the HTML output   $  dom->appendChild($  form);   $  htmlString = $  dom->saveHTML();   echo $  htmlString; }

So far, we’ve gotten the data and parsed it, initialized our DOMDocument and echoed its output. What do we want to do for each field? First off, let’s build the container element which, in our example, should be a <label>, and the labelling <span> which is common to all field types:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  label = null;    // Add a `<span>` for the label if it is set   if ($  field->label) {     $  label = $  dom->createElement('span', $  field->label);     $  label->setAttribute('class', 'label');   }    // Add the label to the `<label>`   if ($  label) $  element->appendChild($  label); }

Since we’re in a loop, and PHP doesn’t scope variables in loops, we reset the $ label element on each iteration. Then, if the field has a label, we build the element. At the end, we append it to the container element.

Notice that we set classes using the setAttribute method. Unlike the Web API, there unfortunately is no special handing of class lists. They’re just another attribute. If we had some really complex class logic, since It’s Just PHP™, we could create an array and then implode it:
$ label->setAttribute('class', implode($ labelClassList)).

Single inputs

Since we know that the API will only return specific field types, we can switch over the type and write specific code for each one:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     case 'text':     case 'email':     case 'telephone':       $  input = $  dom->createElement('input');       $  input->setAttribute('placeholder', ' ');       if ($  field->type === 'email') $  input->setAttribute('type', 'email');       if ($  field->type === 'telephone') $  input->setAttribute('type', 'tel');       break;   } }

Now let’s handle text areas, single checkboxes and hidden fields:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     //...       case 'text_area':       $  input = $  dom->createElement('textarea');       $  input->setAttribute('placeholder', ' ');       if ($  rows = $  field->field_metadata->rows) $  input->setAttribute('rows', $  rows);       break;      case 'checkbox':       $  element->setAttribute('class', 'field single-checkbox');       $  input = $  dom->createElement('input');       $  input->setAttribute('type', 'checkbox');       if ($  field->field_metadata->initially_checked === true) $  input->setAttribute('checked', 'checked');       break;      case 'hidden':       $  input = $  dom->createElement('input');       $  input->setAttribute('type', 'hidden');       $  input->setAttribute('value', $  field->field_metadata->value);       $  element->setAttribute('hidden', 'hidden');       $  element->setAttribute('style', 'display: none;');       $  label->textContent = '';       break;   } }

Notice something new we’re doing for the checkbox and hidden cases? We’re not just creating the <input> element; we’re making changes to the container <label> element! For a single checkbox field we want to modify the class of the container, so we can align the checkbox and label horizontally; a hidden <input>‘s container should also be completely hidden.

Now if we were merely concatenating strings, it would be impossible to change at this point. We would have to add a bunch of if statements regarding the type of element and its metadata in the top of the block. Or, maybe worse, we start the switch way earlier, then copy-paste a lot of common code between each branch.

And here is the real beauty of using a builder like DOMDocument — until we hit that saveHTML(), everything is still editable, and everything is still structured.

Nested looping elements

Let’s add the logic for <select> elements:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     //...       case 'select':       $  element->setAttribute('class', 'field select');       $  input = $  dom->createElement('select');       $  input->setAttribute('required', 'required');       if ($  field->field_metadata->multi_select === true)         $  input->setAttribute('multiple', 'multiple');            $  options = [];            // Track whether there's a pre-selected option       $  optionSelected = false;            foreach ($  field->field_metadata->values as $  value) {         $  option = $  dom->createElement('option', htmlspecialchars($  value->label));              // Bail if there's no value         if (!$  value->value) continue;              // Set pre-selected option         if ($  value->selected === true) {           $  option->setAttribute('selected', 'selected');           $  optionSelected = true;         }         $  option->setAttribute('value', $  value->value);         $  options[] = $  option;       }            // If there is no pre-selected option, build an empty placeholder option       if ($  optionSelected === false) {         $  emptyOption = $  dom->createElement('option');              // Set option to hidden, disabled, and selected         foreach (['hidden', 'disabled', 'selected'] as $  attribute)           $  emptyOption->setAttribute($  attribute, $  attribute);         $  input->appendChild($  emptyOption);       }            // Add options from array to `<select>`       foreach ($  options as $  option) {         $  input->appendChild($  option);       }   break;   } }

OK, so there’s a lot going on here, but the underlying logic is the same. After setting up the outer <select>, we make an array of <option>s to append inside it.

We’re also doing some <select>-specific trickery here: If there is no pre-selected option, we add an empty placeholder option that is already selected, but can’t be selected by the user. The goal is to place our <label class="label"> as a “placeholder” using CSS, but this technique can be useful for all kinds of designs. By appending it to the $ input before appending the other options, we make sure it is the first option in the markup.

Now let’s handle <fieldset>s of radio buttons and checkboxes:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     // ...       case 'multiple_choice':       $  choiceType = $  field->field_metadata->multi_select === true ? 'checkbox' : 'radio';       $  element->setAttribute('class', "field {$  choiceType}-group");       $  input = $  dom->createElement('fieldset');            // Build a choice `<input>` for each option in the fieldset       foreach ($  field->field_metadata->values as $  choiceValue) {         $  choiceField = $  dom->createElement('div');         $  choiceField->setAttribute('class', 'choice');              // Set a unique ID using the field ID + the choice ID         $  choiceID = "{$  field->id}-{$  choiceValue->value}";              // Build the `<input>` element         $  choice = $  dom->createElement('input');         $  choice->setAttribute('type', $  choiceType);         $  choice->setAttribute('value', $  choiceValue->value);         $  choice->setAttribute('id', $  choiceID);         $  choice->setAttribute('name', $  field->id);         $  choiceField->appendChild($  choice);              // Build the `<label>` element         $  choiceLabel = $  dom->createElement('label', $  choiceValue->label);         $  choiceLabel->setAttribute('for', $  choiceID);         $  choiceField->appendChild($  choiceLabel);              $  input->appendChild($  choiceField);       }   break;   } }

So, first we determine if the field set should be for checkboxes or radio button. Then we set the container class accordingly, and build the <fieldset>. After that, we iterate over the available choices and build a <div> for each one with an <input> and a <label>.

Notice we use regular PHP string interpolation to set the container class on line 21 and to create a unique ID for each choice on line 30.

Fragments

One last type we have to add is slightly more complex than it looks. Many forms include instruction fields, which aren’t inputs but just some HTML we need to print between other fields.

We’ll need to reach for another DOMDocument method, createDocumentFragment(). This allows us to add arbitrary HTML without using the DOM structuring:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     //...       case 'instruction':       $  element->setAttribute('class', 'field text');       $  fragment = $  dom->createDocumentFragment();       $  fragment->appendXML($  field->text);       $  input = $  dom->createElement('p');       $  input->appendChild($  fragment);       break;   } }

At this point you might be wondering how we found ourselves with an object called $ input, which actually represents a static <p> element. The goal is to use a common variable name for each iteration of the fields loop, so at the end we can always add it using $ element->appendChild($ input) regardless of the actual field type. So, yeah, naming things is hard.

Validation

The API we are consuming kindly provides an individual validation message for each required field. If there’s a submission error, we can show the errors inline together with the fields, rather than a generic “oops, your bad” message at the bottom.

Let’s add the validation text to each element:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;   $  validation = null;    // Add a `<span>` for the label if it is set   // ...    // Add a `<em>` for the validation message if it is set   if (isset($  field->validation_message)) {     $  validation = $  dom->createElement('em');     $  fragment = $  dom->createDocumentFragment();     $  fragment->appendXML($  field->validation_message);     $  validation->appendChild($  fragment);     $  validation->setAttribute('class', 'validation-message');     $  validation->setAttribute('hidden', 'hidden'); // Initially hidden, and will be unhidden with Javascript if there's an error on the field   }    // Build the input element   switch ($  field->type) {     // ...   } }

That’s all it takes! No need to fiddle with the field type logic — just conditionally build an element for each field.

Bringing it all together

So what happens after we build all the field elements? We need to add the $ input, $ label, and $ validation objects to the DOM tree we’re building. We can also use the opportunity to add common attributes, like required. Then we’ll add the submit button, which is separate from the fields in this API.

<?php function renderForm ($  endpoint) {   // Get the data from the API and convert it to a PHP object   // ...    // Start building the DOM   $  dom = new DOMDocument();   $  form = $  dom->createElement('form');    // Iterate over the fields and build each one   foreach ($  formFields as $  field) {     // Build the container `<label>`     $  element = $  dom->createElement('label');     $  element->setAttribute('class', 'field');        // Reset input values     $  input = null;     $  label = null;     $  validation = null;        // Add a `<span>` for the label if it is set     // ...        // Add a `<em>` for the validation message if it is set     // ...        // Build the input element     switch ($  field->type) {       // ...     }        // Add the input element     if ($  input) {       $  input->setAttribute('id', $  field->id);       if ($  field->required)         $  input->setAttribute('required', 'required');       if (isset($  field->max_length))         $  input->setAttribute('maxlength', $  field->max_length);       $  element->appendChild($  input);          if ($  label)         $  element->appendChild($  label);          if ($  validation)         $  element->appendChild($  validation);          $  form->appendChild($  element);     }   }      // Build the submit button   $  submitButtonLabel = $  formContent->submit_button_label;   $  submitButtonField = $  dom->createElement('div');   $  submitButtonField->setAttribute('class', 'field submit');   $  submitButton = $  dom->createElement('button', $  submitButtonLabel);   $  submitButtonField->appendChild($  submitButton);   $  form->appendChild($  submitButtonField);    // Get the HTML output   $  dom->appendChild($  form);   $  htmlString = $  dom->saveHTML();   echo $  htmlString; }

Why are we checking if $ input is truthy? Since we reset it to null at the top of the loop, and only build it if the type conforms to our expected switch cases, this ensures we don’t accidentally include unexpected elements our code can’t handle properly.

Hey presto, a custom HTML form!

Bonus points: rows and columns

As you may know, many form builders allow authors to set rows and columns for fields. For example, a row might contain both the first name and last name fields, each in a single 50% width column. So how would we go about implementing this, you ask? By exemplifying (once again) how loop-friendly DOMDocument is, of course!

Our API response includes the grid data like this:

{   "submit_button_label": "Submit now!",   "fields": [     {       "id": "first-name",       "type": "text",       "label": "First name",       "required": true,       "validation_message": "First name is required.",       "max_length": 30,       "row": 1,       "column": 1     },     {       "id": "category",       "type": "multiple_choice",       "label": "Choose all categories that apply",       "required": false,       "field_metadata": {         "multi_select": true,         "values": [           { "value": "travel", "label": "Travel" },           { "value": "marketing", "label": "Marketing" }         ]       },       "row": 2,       "column": 1     }   ] }

We’re assuming that adding a data-column attribute is enough for styling the width, but that each row needs to be it’s own element (i.e. no CSS grid).

Before we dive in, let’s think through what we need in order to add rows. The basic logic goes something like this:

  1. Track the latest row encountered.
  2. If the current row is larger, i.e. we’ve jumped to the next row, create a new row element and start adding to it instead of the previous one.

Now, how would we do this if we were concatenating strings? Probably by adding a string like '</div><div class="row">' whenever we reach a new row. This kind of “reversed HTML string” is always very confusing to me, so I can only imagine how my IDE feels. And the cherry on top is that thanks to the browser auto-closing open tags, a single typo will result in a gazillion nested <div>s. Just like fun, but the opposite.

So what’s the structured way to handle this? Thanks for asking. First let’s add row tracking before our loop and build an additional row container element. Then we’ll make sure to append each container $ element to its $ rowElement rather than directly to $ form.

<?php function renderForm ($  endpoint) {   // Get the data from the API and convert it to a PHP object   // ...    // Start building the DOM   $  dom = new DOMDocument();   $  form = $  dom->createElement('form');    // init tracking of rows   $  row = 0;   $  rowElement = $  dom->createElement('div');   $  rowElement->setAttribute('class', 'field-row');    // Iterate over the fields and build each one   foreach ($  formFields as $  field) {     // Build the container `<label>`     $  element = $  dom->createElement('label');     $  element->setAttribute('class', 'field');     $  element->setAttribute('data-row', $  field->row);     $  element->setAttribute('data-column', $  field->column);          // Add the input element to the row     if ($  input) {       // ...       $  rowElement->appendChild($  element);       $  form->appendChild($  rowElement);     }   }   // ... }

So far we’ve just added another <div> around the fields. Let’s build a new row element for each row inside the loop:

<?php // ... // Init tracking of rows $  row = 0; $  rowElement = $  dom->createElement('div'); $  rowElement->setAttribute('class', 'field-row');  // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // ...   // If we've reached a new row, create a new $  rowElement   if ($  field->row > $  row) {     $  row = $  field->row;     $  rowElement = $  dom->createElement('div');     $  rowElement->setAttribute('class', 'field-row');   }    // Build the input element   switch ($  field->type) {     // ...       // Add the input element to the row       if ($  input) {         // ...         $  rowElement->appendChild($  element);          // Automatically de-duped         $  form->appendChild($  rowElement);       }   } }

All we need to do is overwrite the $ rowElement object as a new DOM element, and PHP treats it as a new unique object. So, at the end of every loop, we just append whatever the current $ rowElement is — if it’s still the same one as in the previous iteration, then the form is updated; if it’s a new element, it is appended at the end.

Where do we go from here?

Forms are a great use case for object-oriented templating. And thinking about that snippet from WordPress Core, an argument might be made that nested menus are a good use case as well. Any task where the markup follows complex logic makes for a good candidate for this approach. DOMDocument can output any XML, so you could also use it to build an RSS feed from posts data.

Here’s the entire code snippet for our form. Feel free it adapt it to any form API you find yourself dealing with. Here’s the official documentation, which is good for getting a sense of the available API.

We didn’t even mention DOMDocument can parse existing HTML and XML. You can then look up elements using the XPath API, which is kinda similar to document.querySelector, or cheerio on Node.js. There’s a bit of a learning curve, but it’s a super powerful API for handling external content.

Fun(?) fact: Microsoft Office files that end with x (e.g. .xlsx) are XML files. Don’t tell the marketing department, but it’s possible to parse Word docs and output HTML on the server.

The most important thing is to remember that templating is a superpower. Being able to build the right markup for the right situation can be the key to a great UX.


The post Building a Form in PHP Using DOMDocument appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

16px or Larger Text Prevents iOS Form Zoom

This was a great “Today I Learned” for me from Josh W. Comeau. If the font-size of an <input> is 16px or larger, Safari on iOS will focus into the input normally. But as soon as the font-size is 15px or less, the viewport will zoom into that input. Presumably, because it considers that type too small and wants you to see what you are doing. So it zooms in to help you. Accessibility. If you don’t want that, make the font big enough.

Here’s Josh’s exact Pen if you want to have a play yourself.

In general, I’d say I like this feature. It helps people see what they are doing and discourages super-tiny font sizes. What is a slight bummer — and I really don’t blame anyone here — is that not all typefaces are created equal in terms of readability at different sizes. For example, here’s San Francisco versus Caveat at 16px.

San Francisco on the left, Cavet on the right. Caveat looks visually much smaller even though the font-size is the same.

You can view that example in Debug Mode to see for yourself and change the font size to see what does and doesn’t zoom.


The post 16px or Larger Text Prevents iOS Form Zoom appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , ,
[Top]

Headless Form Submission With the WordPress REST API

If you’re building a WordPress site, you need a good reason not to choose a WordPress form plugin. They are convenient and offer plenty of customizations that would take a ton of effort to build from scratch. They render the HTML, validate the data, store the submissions, and provide integration with third-party services.

But suppose we plan to use WordPress as a headless CMS. In this case, we will be mainly interacting with the REST API (or GraphQL). The front-end part becomes our responsibility entirely, and we can’t rely anymore on form plugins to do the heavy lifting in that area. Now we’re in the driver’s seat when it comes to the front end.

Forms were a solved problem, but now we have to decide what to do about them. We have a couple of options:

  • Do we use our own custom API if we have such a thing? If not, and we don’t want to create one, we can go with a service. There are many good static form providers, and new ones are popping up constantly.
  • Can we keep using the WordPress plugin we already use and leverage its validation, storage, and integration?

The most popular free form plugin, Contact Form 7, has a submission REST API endpoint, and so does the well-known paid plugin, Gravity Forms, among others.

From a technical standpoint, there’s no real difference between submitting the form‘s data to an endpoint provided by a service or a WordPress plugin. So, we have to decide based on different criteria. Price is an obvious one; after that is the availability of the WordPress installation and its REST API. Submitting to an endpoint presupposes that it is always available publicly. That’s already clear when it comes to services because we pay for them to be available. Some setups might limit WordPress access to only editing and build processes. Another thing to consider is where you want to store the data, particularly in a way that adheres to GPDR regulations.

When it comes to features beyond the submission, WordPress form plugins are hard to match. They have their ecosystem, add-ons capable of generating reports, PDFs, readily available integration with newsletters, and payment services. Few services offer this much in a single package.

Even if we use WordPress in the “traditional” way with the front end based on a WordPress theme, using a form plugin’s REST API might make sense in many cases. For example, if we are developing a theme using a utility-first CSS framework, styling the rendered form with fixed markup structured with a BEM-like class convention leaves a sour taste in any developer’s mouth.

The purpose of this article is to present the two WordPress form plugins submission endpoints and show a way to recreate the typical form-related behaviors we got used to getting out of the box. When submitting a form, in general, we have to deal with two main problems. One is the submission of the data itself, and the other is providing meaningful feedback to the user.

So, let’s start there.

The endpoints

Submitting data is the more straightforward part. Both endpoints expect a POST request, and the dynamic part of the URL is the form ID.

Contact Form 7 REST API is available immediately when the plugin is activated, and it looks like this:

https://your-site.tld/wp-json/contact-form-7/v1/contact-forms/<FORM_ID>/feedback

If we’re working with Gravity Forms, the endpoint takes this shape:

https://your-site.tld/wp-json/gf/v2/forms/<FORM_ID>/submissions

The Gravity Forms REST API is disabled by default. To enable it, we have to go to the plugin’s settings, then to the REST API page, and check the “Enable access to the API” option. There is no need to create an API key, as the form submission endpoint does not require it.

The body of the request

Our example form has five fields with the following rules:

  • a required text field
  • a required email field
  • a required date field that accepts dates before October 4, 1957
  • an optional textarea
  • a required checkbox

For Contact Form 7’s request’s body keys, we have to define them with the form-tags syntax:

{   "somebodys-name": "Marian Kenney",   "any-email": "marian2210@geocities.com",   "before-space-age": "1922-03-11",   "optional-message": "",   "fake-terms": "1" }

Gravity Forms expects the keys in a different format. We have to use an auto-generated, incremental field ID with the input_ prefix. The ID is visible when you are editing the field.

{   "input_1": "Marian Kenney",   "input_2": "marian2210@geocities.com",   "input_3": "1922-03-11",   "input_4": "",   "input_5_1": "1" }

Submitting the data

We can save ourselves a lot of work if we use the expected keys for the inputs’ name attributes. Otherwise, we have to map the input names to the keys.

Putting everything together, we get an HTML structure like this for Contact Form 7:

<form action="https://your-site.tld/wp-json/contact-form-7/v1/contact-forms/<FORM_ID>/feedback" method="post">   <label for="somebodys-name">Somebody's name</label>   <input id="somebodys-name" type="text" name="somebodys-name">   <!-- Other input elements -->   <button type="submit">Submit</button> </form>

In the case of Gravity Forms, we only need to switch the action and the name attributes:

<form action="https://your-site.tld/wp-json/gf/v2/forms/<FORM_ID>/submissions" method="post">   <label for="input_1">Somebody's name</label>   <input id="input_1" type="text" name="input_1">   <!-- Other input elements -->   <button type="submit">Submit</button> </form>

Since all the required information is available in the HTML, we are ready to send the request. One way to do this is to use the FormData in combination with the fetch:

const formSubmissionHandler = (event) => {   event.preventDefault();    const formElement = event.target,     { action, method } = formElement,     body = new FormData(formElement);    fetch(action, {     method,     body   })     .then((response) => response.json())     .then((response) => {       // Determine if the submission is not valid       if (isFormSubmissionError(response)) {         // Handle the case when there are validation errors       }       // Handle the happy path     })     .catch((error) => {       // Handle the case when there's a problem with the request     }); };  const formElement = document.querySelector("form");  formElement.addEventListener("submit", formSubmissionHandler);

We can send the submission with little effort, but the user experience is subpar, to say the least. We owe to users as much guidance as possible to submit the form successfully. At the very least, that means we need to:

  • show a global error or success message,
  • add inline field validation error messages and possible directions, and
  • draw attention to parts that require attention with special classes.

Field validation

On top of using built-in HTML form validation, we can use JavaScript for additional client-side validation and/or take advantage of server-side validation.

When it comes to server-side validation, both Contact Form 7 and Gravity Forms offer that out of the box and return the validation error messages as part of the response. This is convenient as we can control the validation rules from the WordPress admin.

For more complex validation rules, like conditional field validation, it might make sense to rely only on the server-side because keeping the front-end JavaScript validation in sync with the plugins setting can become a maintenance issue.

If we solely go with the server-side validation, the task becomes about parsing the response, extracting the relevant data, and DOM manipulation like inserting elements and toggle class-names.

Response messages

The response when there is a validation error for Contact Form 7 look like this:

{   "into": "#",   "status": "validation_failed",   "message": "One or more fields have an error. Please check and try again.",   "posted_data_hash": "",   "invalid_fields": [     {       "into": "span.wpcf7-form-control-wrap.somebodys-name",       "message": "The field is required.",       "idref": null,       "error_id": "-ve-somebodys-name"     },     {       "into": "span.wpcf7-form-control-wrap.any-email",       "message": "The field is required.",       "idref": null,       "error_id": "-ve-any-email"     },     {       "into": "span.wpcf7-form-control-wrap.before-space-age",       "message": "The field is required.",       "idref": null,       "error_id": "-ve-before-space-age"     },     {       "into": "span.wpcf7-form-control-wrap.fake-terms",       "message": "You must accept the terms and conditions before sending your message.",       "idref": null,       "error_id": "-ve-fake-terms"     }   ] }

On successful submission, the response looks like this:

{   "into": "#",   "status": "mail_sent",   "message": "Thank you for your message. It has been sent.",   "posted_data_hash": "d52f9f9de995287195409fe6dcde0c50" }

Compared to this, Gravity Forms’ validation error response is more compact:

{   "is_valid": false,   "validation_messages": {     "1": "This field is required.",     "2": "This field is required.",     "3": "This field is required.",     "5": "This field is required."   },   "page_number": 1,   "source_page_number": 1 }

But the response on a successful submission is bigger:

{   "is_valid": true,   "page_number": 0,   "source_page_number": 1,   "confirmation_message": "<div id='gform_confirmation_wrapper_1' class='gform_confirmation_wrapper '><div id='gform_confirmation_message_1' class='gform_confirmation_message_1 gform_confirmation_message'>Thanks for contacting us! We will get in touch with you shortly.</div></div>",   "confirmation_type": "message" }

While both contain the information we need, they don‘t follow a common convention, and both have their quirks. For example, the confirmation message in Gravity Forms contains HTML, and the validation message keys don’t have the input_ prefix — the prefix that’s required when we send the request. On the other side, validation errors in Contact Form 7 contain information that is relevant only to their front-end implementation. The field keys are not immediately usable; they have to be extracted.

In a situation like this, instead of working with the response we get, it’s better to come up with a desired, ideal format. Once we have that, we can find ways to transform the original response to what we see fit. If we combine the best of the two scenarios and remove the irrelevant parts for our use case, then we end up with something like this:

{   "isSuccess": false,   "message": "One or more fields have an error. Please check and try again.",   "validationError": {     "somebodys-name": "This field is required.",     "any-email": "This field is required.",     "input_3": "This field is required.",     "input_5": "This field is required."   } }

And on successful submission, we would set isSuccess to true and return an empty validation error object:

{   "isSuccess": true,   "message": "Thanks for contacting us! We will get in touch with you shortly.",   "validationError": {} }

Now it’s a matter of transforming what we got to what we need. The code to normalize the Contact Forms 7 response is this:

const normalizeContactForm7Response = (response) => {   // The other possible statuses are different kind of errors   const isSuccess = response.status === 'mail_sent';   // A message is provided for all statuses   const message = response.message;   const validationError = isSuccess     ? {}     : // We transform an array of objects into an object     Object.fromEntries(       response.invalid_fields.map((error) => {         // Extracts the part after "cf7-form-control-wrap"         const key = /cf7[-a-z]*.(.*)/.exec(error.into)[1];          return [key, error.message];       })     );    return {     isSuccess,     message,     validationError,   }; };

The code to normalize the Gravity Forms response winds up being this:

const normalizeGravityFormsResponse = (response) => {   // Provided already as a boolean in the response   const isSuccess = response.is_valid;   const message = isSuccess     ? // Comes wrapped in a HTML and we likely don't need that       stripHtml(response.confirmation_message)     : // No general error message, so we set a fallback       'There was a problem with your submission.';   const validationError = isSuccess     ? {}     : // We replace the keys with the prefixed version;       // this way the request and response matches       Object.fromEntries(         Object.entries(             response.validation_messages         ).map(([key, value]) => [`input_$ {key}`, value])       );    return {     isSuccess,     message,     validationError,   }; };

We are still missing a way to display the validation errors, success messages, and toggling classes. However, we have a neat way of accessing the data we need, and we removed all of the inconsistencies in the responses with a light abstraction. When put together, it’s ready to be dropped into an existing codebase, or we can continue building on top of it.

There are many ways to tackle the remaining part. What makes sense will depend on the project. For situations where we mainly have to react to state changes, a declarative and reactive library can help a lot. Alpine.js was covered here on CSS-Tricks, and it’s a perfect fit for both demonstrations and using it in production sites. Almost without any modification, we can reuse the code from the previous example. We only need to add the proper directives and in the right places.

Wrapping up

Matching the front-end experience that WordPress form plugins provide can be done with relative ease for straightforward, no-fuss forms — and in a way that is reusable from project to project. We can even accomplish it in a way that allows us to switch the plugin without affecting the front end.

Sure, it takes time and effort to make a multi-page form, previews of the uploaded images, or other advanced features that we’d normally get baked right into a plugin, but the more unique the requirements we have to meet, the more it makes sense to use the submission endpoint as we don’t have to work against the given front-end implementation that tries to solve many problems, but never the particular one we want.

Using WordPress as a headless CMS to access the REST API of a form plugin to hit the submissions endpoints will surely become a more widely used practice. It’s something worth exploring and to keep in mind. In the future, I would not be surprised to see WordPress form plugins designed primarily to work in a headless context like this. I can imagine a plugin where front-end rendering is an add-on feature that’s not an integral part of its core. What consequences that would have, and if it could have commercial success, remains to be explored but is a fascinating space to watch evolve.


The post Headless Form Submission With the WordPress REST API appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]

Creating Custom Form Controls with ElementInternals

Ever since the dawn of time, humanity has dreamed of having more control over form elements. OK, I might be overselling it a tiny bit, but creating or customizing form components has been a holy grail of front-end web development for years.

One of the lesser-heralded, but most powerful features of custom elements (e.g. <my-custom-element>) has quietly made its way into Google Chrome as of version 77 and is working its way into other browsers. The ElementInternals standard is a very exciting set of features with a very unassuming name. Among the features internals adds are the ability to participate in forms and an API around accessibility controls.

In this article, we’re going to look at how to create a custom form control, integrate constraint validation, introduce the basics of internal accessibility and see a way to combine these features to create a highly-portable macro form control.

Let’s start by creating a very simple custom element that matches our design system. Our element will hold all of its styles within the shadow DOM and ensure some basic accessibility. We’ll use the wonderful LitElement library from the Polymer team at Google for our code examples and, although you definitely don’t need it, it does provide a great abstraction for writing custom elements.

In this Pen, we’ve created a <rad-input> that has some basic design to it. We have also added a second input to our form that is a vanilla HTML input, and added a default value (so you can simply press submit and see it work).

When we click our submit button a few things happen. First, the submit event’s preventDefault method is called, in this case, to ensure our page doesn’t reload. After this, we create a FormData object which gives us access to information about our form which we use to construct a JSON string and append it to an <output> element. Notice, however, that the only value added to our output is from the element with name="lame".

That’s because our element doesn’t know how to interact with the form just yet, so let’s set up our <rad-input> with an ElementInternals instance to help it live up to its name. To start, we’ll need to call our method’s attachInternals method in the element’s constructor, we’ll also be importing an ElementInternals polyfill into our page to work with browsers that don’t support the spec yet.

The attachInternals method returns a new element internals instance which contains some new APIs we can use in our method. In order to let our element take advantage of these APIs, we need to add a static formAssociated getter that returns true.

class RadInput extends LitElement {   static get formAssociated() {     return true;   }    constructor() {     super();     this.internals();   } }

Let’s take a look at some of the APIs in our element’s internals property:

  • setFormValue(value: string|FormData|File, state?: any): void — This method will set the element’s value on its parent form if one is present. If the value is null, the element will not participate in the form submission process.
  • form — A reference to our element’s parent form, if one exists.
  • setValidity(flags: Partial<ValidityState>, message?: string, anchor?: HTMLElement): void — The setValidity method will help control our element’s validity state within the form. If the form is invalid, a validation message must be present.
  • willValidate — Will be true if the element will be evaluated when the form is submitted.
  • validity — A validity object that matches the APIs and semantics attached to HTMLInputElement.prototype.validity.
  • validationMessage — If the control has been set as invalid with setValidity, this is the message that was passed in describing the error.
  • checkValidity — Will return true if the element is valid, otherwise this will return false and fire an invalid event on the element.
  • reportValidity — Does the same as checkValidity, and will report problems to the user if the event isn’t cancelled.
  • labels — A list of elements that label this element using the label[for] attribute.
  • A number of other controls used to set aria information on the element.

Setting a custom element’s value

Let’s modify our <rad-input> to take advantage of some of these APIs:

Here we’ve modified the element’s _onInput method to include a call to this.internals.setFormValue. This tells the form our element wants to register a value with the form under its given name (which is set as an attribute in our HTML). We’ve also added a firstUpdated method (loosely analogous with connectedCallback when not using LitElement) which sets the element’s value to an empty string whenever the element is done rendering. This is to make sure our element always has a value with the form (and though it is not necessary, you may want to exclude your element from the form by passing in a null value).

Now when we add a value to our input and submit the form, we will see that we have a radInput value in our <output> element. We can also see our element has been added to the HTMLFormElement’s radInput property. One thing you might have noticed, however, is that despite the fact that despite the fact that our element doesn’t have a value, it will still allow the form submission to take place. Let’s add some validation to our element next.

Adding constraint validation

In order to set our field’s validation, we need to modify our element a little bit to make use of the setValidity method on our element internals object. This method will take in three arguments (the second one is only required if the element is invalid, the third is always optional). The first argument is a partial ValidityState object. If any flag is set to true the control will be marked as invalid. If one of the built-in validity keys doesn’t meet your needs, there is a catch-all customError key that should work. Lastly, if the control is valid, we pass in an object literal ({}) to reset the control’s validity.

The second argument here is the control’s validity message. This argument is required if the control is invalid, and not allowed if the control is valid. The third argument is an optional validation target that will control the user’s focus if and when the form is submitted as invalid or reportValidity is called.

We’re going to introduce a new method to our <rad-input> that will take care of this logic for us:

_manageRequired() {   const { value } = this;   const input = this.shadowRoot.querySelector('input');   if (value === '' && this.required) {     this.internals.setValidity({       valueMissing: true     }, 'This field is required', input);   } else {     this.internals.setValidity({});   } }

This function gets the control’s value and input. If the value is equal to an empty string and the element is marked as required, we’ll call the internals.setValidity and toggle the control’s validity. Now we all we need to do is call this method in our firstUpdated and _onInput methods and we’ll have added some basic validation to our element.

Clicking the submit button before a value is entered into our <rad-input> will now display an error message in browsers that support the ElementInternals spec. Unfortunately, displaying validation errors is still not supported by the polyfill as there isn’t any reliable way to trigger the built-in validation popup in non-supporting browsers.

We’ve also added some basic accessibility information to our example by using our internals object. We’ve added an additional property to our element, _required, which will serve as a proxy for this.required and as a getter/setter for required.

get required() {   return this._required; }  set required(isRequired) {   this._required = isRequired;   this.internals.ariaRequired = isRequired; } 

By passing the required property to internals.ariaRequired, we are alerting screen readers that our element is currently expecting a value. In the polyfill, this is done by adding an aria-required attribute; however, in supporting browsers, the attribute won’t be added to the element because that property is inherent to the element.

Creating a micro-form

Now that we have a working input that meets our design system, we might want to begin composing our elements into patterns that we can reuse throughout several applications. One of the most compelling features for ElementInternals is that the setFormValue method can take not only string and file data, but also FormData objects. So let’s say we want to create a common address form that might be used in multiple organizations, we can do that easily with our newly-created elements.

In this example, we have a form created inside our element’s shadow root where we have composed four <rad-input> elements to make an address form. Instead of calling setFormValue with a string, this time we’ve chosen to pass along the entire value of our form. As a result, our element passes along the values of each individual element inside its child form to the outer form.


Adding constraint validation to this form would be a fairly straightforward process, as would providing additional styles, behaviors and slotting in content. Using these newer APIs finally allows developers to unlock a ton of potential inside custom elements and finally gives us free-range on controlling our user experiences.


The post Creating Custom Form Controls with ElementInternals appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]

Don’t put pointer-events: none on form labels

Bruce Lawson with the tip of the day, warning against the use of pointer-events: none on forms labels. We know that pointer-events is used to change how elements respond to click, tap, hover, and active states. But it apparently borks form labels, squashing their active hit target size to something small and tough to interact with. Bruce includes examples in his post.

That’s not the striking part of the post though. It’s that the issue was pinned to an implementation of Material Design’s floating labels component. Bruce fortunately had pointer events expert Patrick Lauke’s ear, who pointed (get it?) out the issue.

That isn’t a dig at frameworks. It’s just the reality of things. Front-end developers gotta be aware, and that includes awareness of third-party code.

Direct Link to ArticlePermalink


The post Don’t put pointer-events: none on form labels appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[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]

Happier HTML5 form validation in Vue

It’s kind of neat that we can do input:invalid {} in CSS to style an input when it’s in an invalid state. Yet, used exactly like that, the UX is pretty bad. Say you have <input type="text" required>. That’s immediately invalid before the user has done anything. That’s such a bummer UX that you never see it used in the wild. But if we could just avoid that one thing, that :invalid selector can do a ton of work for us in form validation without needing to lean on a big fancy library.

Dave has an idea that’s a variation on the original 2017 idea.

It’s basically:

form.errors :invalid {   outline: 2px solid red; }

Now you’re only conditionally applying those native error styles when you’ve determined the form to be in an error state and added a class. And fortunately, testing that is pretty easy too. We could apply that class when the submit button is clicked:

submitButton.addEventListener("click", (e) => {   form.classList.toggle("errors", !form.checkValidity()) });

Or you could do it when an input blurs or something. You could even wrap each input set in a wrapper and toggle the class on the wrapper when appropriate. The commented out code here could get you going there…

Dave kicked this idea over to Vue:

We initialize the form component with errors: false because we don’t want the error styling until the user has submitted the form. The invalidateForm function just sets this.error = true. That’s one problem with the CSS :invalid pseudo class, it’s way too eager. Hooking into the invalid events, delays the styling until after the first form submission attempt and we know the form has errors.

Not using any library (on top of what you already use) is pretty sweet. HTML form validation is pretty much there. Here’s a fork of Dave’s where error messaging is revealed as well:

Direct Link to ArticlePermalink


The post Happier HTML5 form validation in Vue appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

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

,
[Top]