Link Search Menu Expand Document

Building a custom form

Implementation methods

There are multiple methods to build a custom form with Customer Fields. You should consider the following methods that best matches your use case:

  1. Customize a form with CSS
    In many cases, you can use our form builder and add your own CSS to the page to change the look and feel of the form. If you need to add special interaction, take a look at the form API to see if it fits your use case. This is generally the simplest solution for a custom form experience. Learn more

  2. Use data binding with your own markup
    If you need your own HTML form markup, you can build a custom form without needing to write any JavaScript using data binding. We automatically integrate the data into your fields using the name attribute. This is the method covered in this guide.

  3. Use the front-end Customer API
    For any other custom interactions, you can use our Customer API, which doesn’t interact directly with the document. Instead, you manage the values from the user and use CF.customer.set to save data directly to the customer.

Custom form challenges

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!

Step 1: Add the JS Customer API to your theme

Ensure you have installed a form on the theme you’re working with. We recommend creating a form called something like: “Custom development”, then installing it with only the “Other location” box ticked so our snippet and assets get added.

Take note of the form ID in the URL, for example “https://app.customerfields.com/forms/gBXtJo/editor” has a form ID of "gBXtJo".

Add this to the <head> inside your layout/theme.liquid:

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

Step 2: Create new data columns

For this example, let’s collect a few bits of custom data: shirt and shoe sizes. Go to the “Data columns” page in the app, and create two data columns:

Label Key Data type
Shirt size shirt_size text
Shoe size shoe_size text

Tip
You’ll need to be on at least the Pro plan to create custom data columns.

Step 3: Hook up Customer Fields to your registration form

We expose a library called Rivets, which serves to interpolate customer data with the DOM. In this case, we’re going to use a rivets binder that we built called rv-cf-custom-form that hooks up the form to Customer Fields.

Let’s start off with a basic registration form that most (if not all) themes are very similar to:

{% form 'create_customer' %}
  {% if form.errors %}
    {{ form.errors | default_errors }}
  {% endif %}

  <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>

  <div>
    <label>Password</label>
    <input name="customer[password]" type="password" />
  </div>

  <button>Create account</button>
{% endform %}

Add three attributes data-cf-view, rv-cf-custom-form and data-cf-form-id to the {% form %} tag:

{% form 'create_customer', data-cf-view: '', rv-cf-custom-form: '', data-cf-form-id: 'gBXtJo' %}
  <!-- ... form content ... -->
{% endform %}

Make sure to enter your development form’s ID over our example one.

It doesn’t matter what type of form is specified, it could be create_customer or new_comment. It doesn’t even need to be a liquid {% form %}, it just needs to be an HTML <form>.

Here’s the function of each attribute:

data-cf-view

This attribute binds Rivets to the element and all of its children. This allows you do to things like:

<div data-cf-view>
  <h1>Hello, {customer.first_name}!</h1>
</div>

Note: that syntax isn’t Liquid! It’s rivets templating.

This is super helpful in the case where the data collected during the session hasn’t been sent to Shopify just yet, as that data isn’t available in Liquid (read more about guest data.)

rv-cf-custom-form

This is a Rivets binder that we built which submits the form’s data to our servers on submit, instead of sending it straight to Shopify. This will allow custom fields to be saved.

data-cf-form-id

This allows Customer Fields to track form submissions from your custom form, which become visible on the Forms page. This also connects whitelisted tags set up in the form builder to your custom form, which enables customer tagging.

Step 4: Add custom fields

{% form 'create_customer', data-cf-view: '', rv-cf-custom-form: '', data-cf-form-id: 'gBXtJo' %}
  <!-- other fields -->

  <div>
    <label>Shirt size</label>

    <select name="customer[shirt_size]">
      <option>Extra small</option>
      <option>Small</option>
      <option>Medium</option>
      <option>Large</option>
      <option>Extra large</option>
    </select>
  </div>

  <div>
    <label>Shoe size</label>
    <input name="customer[shoe_size]" type="text" />
  </div>

  <button>Create account</button>
{% endform %}

The value for the name attribute needs to be in one of the following formats:

  • name="customer[column_key]"
  • name="customer[parent_column.column_key]"
  • name="column_key"
  • name="parent_column.column_key"

Step 5: Handle server errors

Start off by removing any usage of the {{ form }} object in liquid, as it will not do anything anymore. If it does, then something is wrong, and the form is most likely not submitting to our servers properly.

It’s up to you to inform the customer on how to fix issues when they arise. Luckily, it’s easy to display form errors using rivets:

<ul class="errors" rv-if="has_error">
  <h5>Please fix errors:</h5>

  <li rv-each-error="errors.email">
    <span rv-if="error | eq 'verify'">
      Email must be verified.
    </span>
    
    <span rv-if="error | eq 'taken'">
      Email has already been taken.
    </span>
    
    <span rv-if="error | eq 'invalid'">
      Email is invalid.
    </span>
  </li>

  <li rv-each-error="errors.phone">
    <span rv-if="error | eq 'taken'">
      Phone number has already been taken.
    </span>
    
    <span rv-if="error | eq 'invalid'">
      Phone number is invalid.
    </span>
  </li>

  <li rv-each-error="errors.default_address.country">
    <span rv-if="error | eq 'invalid'">
      Country is invalid.
    </span>
  </li>

  <li rv-each-error="errors.default_address.province">
    <span rv-if="error | eq 'invalid'">
      Province is invalid.
    </span>
  </li>

  <li rv-each-error="errors.addresses.country">
    <span rv-if="error | eq 'invalid'">
      Country is invalid.
    </span>
  </li>

  <li rv-each-error="errors.addresses.province">
    <span rv-if="error | eq 'invalid'">
      Province is invalid.
    </span>
  </li>

  <li rv-each-error="errors.password">
    <span rv-if="error | eq 'too_short'">
      Password is too short (minimum is 5 characters)
    </span>
  </li>

  <li rv-each-error="errors.password_confirmation">
    <span rv-if="error | eq 'must_match_password'">
      Password confirmation must match the provided password.
    </span>
  </li>
</ul>

There are two rivets state variables in play here: has_error, and errors.

has_error is simply a boolean flag that is true whenever our servers reject a submission from a rivets form.

The errors object is only present if a rivets form has been submitted and our servers have returned errors. Otherwise, it is null. When it is defined, it looks something like this:

{
  email: ['invalid', 'taken'],
  phone: ['invalid'],
}

The above markup demonstrates how to properly access and display those errors. Copy and paste the example anywhere inside whatever element has the data-cf-view attribute (in this case, our {% form %} tag.)

Note

Some errors may not be applicable, so for example if you don’t have a phone field, you don’t need to output errors from errors.phone.

For this guide, we only need to handle errors from email, password and password_confirmation. We can strip out the others.

Step 6: Submitting/done states

You can tell when the form is (or is done) submitting by reading these variables from the rivets state: customer_saving, and customer_saved. They work exactly like you think they would, except for one thing: after the customer is done being saved, customer_saved will revert back to false after 3 seconds. Lastly, if your form is set to redirect after submitting, the redirecting variable will be set to true after a successful submission.

Here’s an example of how to handle these different states:

{% form 'create_customer', data-cf-view: '', rv-cf-custom-form: '', data-cf-form-id: 'gBXtJo' %}
  <!-- fields, errors, etc... -->

  <button>
    <span rv-unless="customer_saving | or redirecting">
      <span>Create account</span>
    </span>

    <span rv-if="customer_saving">Submitting...</span>
    <span rv-if="redirecting">Redirecting...</span>
  </button>
{% endform %}

Step 7 (optional): Change markup for logged-in customers

Some fields aren’t applicable for customers who are logged in, such as password and password confirmation. Another variable we attach to the rivets state, customer_logged_in, helps you accomplish this. Use the rv-unless binder to render the password field unless the customer is logged in:

{% form 'create_customer', data-cf-view: '', rv-cf-custom-form: '', data-cf-form-id: 'gBXtJo' %}
  <!-- other fields -->

  <div rv-unless="customer_logged_in">
    <label>Password</label>
    <input name="customer[password]" type="password" />
  </div>

  <!-- other fields, errors, submit button -->
{% endform %}

Lastly, you might want to change the submit button text from “Create account” to “Update account”. To do this, we’ll conditionally render <span> tags inside of our <span rv-unless="customer_saved"> element:

{% form 'create_customer', data-cf-view: '', rv-cf-custom-form: '', data-cf-form-id: 'gBXtJo' %}
  <!-- other fields, errors -->

  <button>
    <span rv-unless="customer_saving | or redirecting">
      <span rv-unless="customer_logged_in">Create account</span>
      <span rv-if="customer_logged_in">Update account</span>
    </span>

    <span rv-if="customer_saving">Submitting...</span>
    <span rv-if="redirecting">Redirecting...</span>
  </button>
{% endform %}

Step 8: Test your form

Try submitting the form a few times, and try to cause errors to show (improper/not unique email, short password, etc.) By default, the form will attempt to redirect the customer to the account page. If something doesn’t behave correctly, check the console for warnings and errors.

Final result

{% form 'create_customer', data-cf-view: '', rv-cf-custom-form: '', data-cf-form-id: 'gBXtJo' %}
  <ul class="errors" rv-if="has_error">
    <h5>Please fix errors:</h5>

    <li rv-each-error="errors.email">
      <span rv-if="error | eq 'verify'">
        Email must be verified.
      </span>
      
      <span rv-if="error | eq 'taken'">
        Email has already been taken.
      </span>
      
      <span rv-if="error | eq 'invalid'">
        Email is invalid.
      </span>
    </li>

    <li rv-each-error="errors.password">
      <span rv-if="error | eq 'too_short'">
        Password is too short (minimum is 5 characters)
      </span>
    </li>

    <li rv-each-error="errors.password_confirmation">
      <span rv-if="error | eq 'must_match_password'">
        Password confirmation must match the provided password.
      </span>
    </li>
  </ul>

  <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>

  <div rv-unless="customer_logged_in">
    <label>Password</label>
    <input name="customer[password]" type="password" />
  </div>
  
  <div>
    <label>Shirt size</label>

    <select name="customer[shirt_size]">
      <option>Extra small</option>
      <option>Small</option>
      <option>Medium</option>
      <option>Large</option>
      <option>Extra large</option>
    </select>
  </div>

  <div>
    <label>Shoe size</label>
    <input name="customer[shoe_size]" type="text" />
  </div>

  <button>
    <span rv-unless="customer_saving | or redirecting">
      <span rv-unless="customer_logged_in">Create account</span>
      <span rv-if="customer_logged_in">Update account</span>
    </span>

    <span rv-if="customer_saving">Submitting...</span>
    <span rv-if="redirecting">Redirecting...</span>
  </button>
{% endform %}

Other form options

You can control the redirect_url, whether or not the form should login or redirect at all by specifying the following hidden <input> elements inside your form:

<!--
  control where the form redirects to after a successful submission.
  the name can be either return_to or return_url too!
--> 
<input type="hidden" name="redirect_url" value="/account" />

<!-- disable redirecting altogether -->
<input type="hidden" name="redirect" value="false" />

<!-- disable auto-login for new customers -->
<input type="hidden" name="login" value="false" />

<!-- change the submission method to guest -->
<input type="hidden" name="guest" value="true" />

All done!

Nice work! You’ve built yourself a custom form that you can expand on to collect all kinds of information, all without writing any JavaScript. If you have any questions or feedback, feel free to get in touch!


Have any questions or comments about this post? Let us know! Your feedback is greatly appreciated.

Customer Fields is a Shopify app made by Helium.

Copyright © 2020 Helium Development, LLC