Link

Building a custom form

Estimated time to implement: 1 - 2 hours, difficulty: 6/10

Before getting started

If you’re looking to build a custom form because of specific design requirements, it’s heavily recommended to try building a form using our app and writing style rules around our markup structure instead. Our form builder supports custom HTML too! Doing this instead of building an entirely custom form from scratch can save you hours of time, and we all know that time is money 💸

There are several challenges to be aware of when building a custom form:

  • Properly formatting phone numbers

Phone numbers need to be a specific format, enforced by Shopify. For example, if you’re in the US, you can’t just send 253 555 1234. It needs to be in E.164 format: +12535551234. Libraries such as libphonenumber-js can help you accomplish this.

  • Ensuring addresses are valid

Shopify is picky about which values can be present for the default_address.country and default_address.province fields. For instance, if you pass United States of America instead of United States, Shopify will reject the request with errors. See the CF.customer.countries global variable for a set of valid country/province names and codes that you can use.

  • Handling server errors

Front-end validation can’t catch everything, such as emails or phone numbers already being taken by another customer account. If the customer inputs data that causes these errors, you’ll need to properly parse the errors from our servers and show them on the page. We wrote a whole page on how to do this.

There are many other use-cases that our forms cover, requiring little-to-no effort on your part. However, if we are missing functionality that your use-case requires, let us know and continue on!

Beginning markup

Begin with a simple form structure that looks something like this:

{% form 'create_customer', id: 'create-customer' %}
  <div>
    <label>First name</label>
    <input name="customer[first_name]" type="text">
  </div>

  <div>
    <label>Last name</label>
    <input name="customer[last_name]" type="text">
  </div>

  <div>
    <label>Email</label>
    <input name="customer[email]" type="email">
  </div>

  {% unless customer %}
    <div>
      <label>Password</label>
      <input name="customer[password]" type="password">
    </div>
  {% endunless %}

  <button type="submit">Create account</button>
{% endform %}

This form will only create an account in Shopify and won’t support custom fields. What we need to do is:

  1. Render the snippet in the <head>
  2. Add column keys to the input elements
  3. Hook into the form’s submit event
  4. Call event.preventDefault to prevent the form from submitting natively
  5. Merge the form data with the customer with CF.customer.set
  6. Submit the data to Customer Fields by calling CF.customer.save
  7. Login the customer automatically and send them on their way

Render the snippet

Navigate to layout/theme.liquid in your theme editor, then add the following code inside the <head> tag:

{% render 'customer-fields', customer_api: true, version: '<Version number>' %}

Add column keys to the fields

The name attributes on these fields aren’t quite right for Customer Fields. The app needs first_name, not customer[first_name]. We could write code to strip away the customer[] text, but it’s much simpler for all of us to just add an extra attribute we can read from. We’ll leave the name attributes there just in case something wrong happens, it’s a good idea to fallback on Shopify.

For each of the input elements, copy the name attribute to a new one called data-column-key then remove customer[] from its value. We’ll use this attribute later on.

Before:

<input name="customer[first_name]" type="text">

After:

<input name="customer[first_name]" data-column-key="first_name" type="text">

Hook into the form’s submit event

Our form has an id of create-customer, so let’s find it by that. We’ll wrap all of our logic in a CF.customerReady call to ensure we’re only changing the form’s behavior if Customer Fields is properly prepared.

CF.customerReady(function() {
  var $form = document.querySelector('#create-customer');
});

Now, we can addEventListener to run a function when the submit event fires:

CF.customerReady(function() {
  var $form = document.querySelector('#create-customer');

  $form.addEventListener('submit', function handleCustomFormSubmit(event) {
  
  });
});

Prevent native submission

In our event handler called “handleCustomFormSubmit”, let’s call preventDefault so that the form doesn’t go directly to Shopify:

CF.customerReady(function() {
  var $form = document.querySelector('#create-customer');

  $form.addEventListener('submit', function handleCustomFormSubmit(event) {
    event.preventDefault();
  });
});

Merge the form data with the customer

It’d take a ton of time to go through each line of code, so here’s all that’s needed with some helpful comments:

CF.customerReady(function() {
  var $form = document.querySelector('#create-customer');

  $form.addEventListener('submit', function handleCustomFormSubmit(event) {
    event.preventDefault();
    
    // Create an object that we'll add key/value pairs to
    var formData = {};

    // Find any element inside the form that has a data-column-key attribute
    var $inputs = $form.querySelectorAll('[data-column-key]');

    // Loop through each of those elements to put together our formData object
    for (var i = 0; i < $inputs.length; i++) {
      var $input = $inputs[i];

      // Extract the key and value from each element
      var key = $input.getAttribute('data-column-key');
      var value = $input.value;

      // Ignore non-checked radio fields
      if ($input.type == 'radio' && !$input.checked) continue;
      
      // Use boolean values for checkboxes
      if ($input.type == 'checkbox') value = $input.checked;

      // Add the key/value pair to formData
      formData[key] = value;
    }

    // Finally, mutate the global customer data object
    CF.customer.set(formData);
  });
});

Submit the data

The data extraction code has been omitted for simplicity:

CF.customerReady(function() {
  var $form = document.querySelector('#create-customer');

  $form.addEventListener('submit', function handleCustomFormSubmit(event) {
    // event.preventDefault(), get the form data, ... 

    CF.customer.set(formData);

    var customerSaveOptions = { // Always login and redirect to the account page after saving
      redirect: true,
      login: true,
      redirectUrl: '/account',
    }

    CF.customer.save(customerSaveOptions)
      .then(function(errorObj) {
        if (errorObj) {
          // You've got data problems. Look through errorObj and let the customer know.
        } else {
          // Customer has been created! They will automatically be logged in and redirected to /account
        }
      })
      .catch(function(error) {
        alert('Failed to save customer! :(');
        console.error(error);
      });
  });
});

It’s crucial to handle server errors properly, or customers using the form will become very frustrated that nothing’s happening if they submit bad data.

Fill forms with existing customer data

You’ve built a custom form that allows customers to create accounts. However, what if they come back to fix a typo? The form will be empty for them, even though they’ve already submitted it. All you need to do is fill in the value of each input with the data stored in Shopify’s Customer object.

Here’s an example of how to do it for a first_name field.

Before:

<input
  type="text"
  name="customer[first_name]"
  data-column-key="first_name"
>

After:

<input
  type="text"
  name="customer[first_name]"
  data-column-key="first_name"
  value="{{ customer.first_name }}"
>

Now, for a custom field. Let’s say you have a field for the customer’s favorite_color. Since this data isn’t part of Shopify’s Customer object, you’ll need to read from customer.metafields.

Before:

<input
  type="text"
  name="customer[favorite_color]"
  data-column-key="favorite_color"
>

After:

<input
  type="text"
  name="customer[favorite_color]"
  data-column-key="favorite_color"
  value="{{ customer.metafields.customer_fields.data.favorite_color }}"
>

No JavaScript needed here! Make sure you do this for every field in your custom form.

End result

Here’s a full snippet that includes all of the markup and scripting for a custom form:

It is assumed that you have {% render 'customer-fields', customer_api: true, version: '<Version number>' %} in your theme’s layout/theme.liquid

{% form 'create_customer', id: 'create-customer' %}
  <div>
    <label>First name</label>
    <input
      type="text"
      name="customer[first_name]"
      data-column-key="first_name"
      value="{{ customer.first_name }}"
    >
  </div>

  <div>
    <label>Last name</label>
    <input
      type="text"
      name="customer[last_name]"
      data-column-key="last_name"
      value="{{ customer.last_name }}"
    >
  </div>

  <div>
    <label>Email</label>
    <input
      type="text"
      name="customer[email]"
      data-column-key="email"
      value="{{ customer.email }}"
    >
  </div>

  {% unless customer %}
    <div>
      <label>Password</label>
      <input
        type="text"
        name="customer[password]"
        data-column-key="password"
        value="{{ customer.password }}"
      >
    </div>
  {% endunless %}

  <div>
    <label>Favorite color</label>
    <input
      type="text"
      name="customer[favorite_color]"
      data-column-key="favorite_color"
      value="{{ customer.metafields.customer_fields.data.favorite_color }}"
    >
  </div>

  <button type="submit">Create account</button>
{% endform %}

<script>
  CF.customerReady(function() {
    var $form = document.querySelector('#create-customer');

    $form.addEventListener('submit', function handleCustomFormSubmit(event) {
      event.preventDefault();
      
      // Create an object that we'll add key/value pairs to
      var formData = {};

      // Find any element inside the form that has a data-column-key attribute
      var $inputs = $form.querySelectorAll('[data-column-key]');

      // Loop through each of those elements to put together our formData object
      for (var i = 0; i < $inputs.length; i++) {
        var $input = $inputs[i];

        // Extract the key and value from each element
        var key = $input.getAttribute('data-column-key');
        var value = $input.value;

        // Ignore non-checked radio fields
        if ($input.type == 'radio' && !$input.checked) continue;
        
        // Use boolean values for checkboxes
        if ($input.type == 'checkbox') value = $input.checked;

        // Add the key/value pair to formData
        formData[key] = value;
      }

      // Mutate the global customer data object
      CF.customer.set(formData);

      var customerSaveOptions = { // Always login and redirect to the account page after saving
        redirect: true,
        login: true,
        redirectUrl: '/account',
      }

      CF.customer.save(customerSaveOptions)
        .then(function() {
          if (errorObj) {
            // You've got data problems. Look through errorObj and let the customer know.
          } else {
            // Customer has been created! They will automatically be logged in and redirected to /account
          }
        })
        .catch(function(error) {
          // Failed to send request, or failed to handle the server response.
          console.error(error);
        });
    });
  });
</script>