Managing user addresses in Shopify

In Shopify, you can allow your customers to create logins and see stuff like past purchases and edit their details. There's a handful of templates expressly for this purpose inside the /templates/customers directory. But it's likely that these files are blank by default.

Shopify have some documentation on what's supposed to be in the addresses.liquid file, but it's ... lacking in detail. So let's fill that in, shall we?

Minimum viable product

Here's what I'd want to be able to do, as a customer:

  • View a list of my addresses
  • Edit old addresses
  • Add a new address
  • Delete even older addresses

View a list of my addresses

Here's how you generate a list of all the addresses the customer currently has in their account:

{% for address in customer.addresses %}
<p>
    {{ address.first_name }} {{ address.last_name }}
    {% if address.first_name != blank or address.last_name != blank %}<br>{% endif %}
    {% if address.company != blank %}
        {{ address.company }}<br>
    {% endif %}
    {% if address.address1 != blank %}
        {{ address.address1 }}<br>
    {% endif %}
    {% if address.address2 != blank %}
        {{ address.address2 }}<br>
    {% endif %}
    {% if address.city != blank %}
        {{ address.city }}<br>
    {% endif %}
    {% if address.country != blank %}
        {{ address.country }}<br>
    {% endif %}
    {% if address.province != blank %}
        {{ address.province }}<br>
    {% endif %}
    {% if address.zip != blank %}
        {{ address.zip }}<br>
    {% endif %}
    {{ address.phone }}
</p>
{% endfor %}

Edit old addresses

When it comes to editing current addresses, Shopify even have this nice table for you to refer to:

Input Type Required name attribute
First name text address[first_name]
Last name text address[last_name]
Company text address[company]
Address 1 text address[address1]
Address 2 text address[address2]
City text address[city]
Country select address[country]
Province select address[province]
ZIP/Postal Code text address[zip]
Phone Number tel address[phone]

Using this, we can use the following Liquid markup to generate all of the forms required for users to edit their addresses:

{% for address in customer.addresses %}
    {% form 'customer_address', address %}
        <p>
            <input type="text" name="address[first_name]" id="forname{{ forloop.index }}" value="{{ address.first_name }}">
            <label for="forname{{ forloop.index }}">First name</label>
        </p>
        <p>
            <input type="text" name="address[last_name]" id="surname{{ forloop.index }}" value="{{ address.last_name }}">
            <label for="surname{{ forloop.index }}">Last name</label>
        </p>
        <p>
            <input type="text" name="address[company]" id="company{{ forloop.index }}" value="{{ address.company }}">
            <label for="company{{ forloop.index }}">Company</label>
        </p>
        <p>
            <input type="text" name="address[address1]" id="address1-{{ forloop.index }}" value="{{ address.address1 }}">
            <label for="address1-{{ forloop.index }}">Address 1</label>
        </p>
        <p>
            <input type="text" name="address[address2]" id="address1-{{ forloop.index }}" value="{{ address.address2 }}">
            <label for="address2-{{ forloop.index }}">Address 2</label>
        </p>
        <p>
            <input type="text" name="address[city]" id="city{{ forloop.index }}" value="{{ address.city }}">
            <label for="city{{ forloop.index }}">City</label>
        </p>
        <p>
            <select name="address[country]" id="country{{ forloop.index }}" data-provinceid="province{{ forloop.index }}">
                {{ country_option_tags }}
            </select>
            <label for="country{{ forloop.index }}">Country</label>
        </p>
        <p>
            <select name="address[province]" id="provinceid{{ forloop.index }}" disabled>
                <option value="">Please select a country first</option>
            </select>
            <label for="province{{ forloop.index }}">Province</label>
        </p>
        <p>
            <input type="text" name="address[zip]" id="postcode{{ forloop.index }}" value="{{ address.zip }}">
            <label for="postcode{{ forloop.index }}">ZIP/Postal Code</label>
        </p>
        <p>
            <input type="tel" name="address[phone]" id="phone{{ forloop.index }}" value="{{ address.phone }}">
            <label for="phone{{ forloop.index }}">Phone Number</label>
        </p>
        <p>
            <input type="submit" value="Update address">
        </p>
    {% endform %}
{% endfor %}

This all seems pretty straightforward, until you get to Country and Province. How do you populate them with the right values?

The country select box uses {{ country_option_tags }} to generate a generic list of countries. This list does not automagically select the correct country for the current address. What about province? Surely that's tied to the country, right? What if the user updates their country and the province needs to reflect this?

Shopify suggests installing a whole JavaScript library to handle this, which seems to do a whole lot more than just populating a select box. Hmm.

There is, however, an easier way.

I couldn't be bothered reading the documentation, so I re-wrote the code from scratch

Inspect the output of those {{ country_option_tags }} and you'll see it comes with a whole heap of extras. Each option tag has a data-provinces attribute, which mostly holds an empty object ([]), but sometimes holds a lot more.

We need the JavaScript to do the following:

  1. Loop through each of the user's addresses, then build an array of values for Country and Province
  2. Loop through each option tag within each country select box and add a selected="selected to the option which matches the current address (or remove the attribute, if it doesn't)
  3. Check each country select box, to see if it has any province data hidden on the currently selected option
  4. Update the corresponding province select box with this data
  5. Loop through each province select box and ensure that the correct province is selected by default (or disable the select box, if there are no provinces)

Building relationships

Shopify have already established that the countries select box must have a name attribute of address[country] and the provinces select box should have a name attribute of address[province]. But there might be potentially multiple addresses on the page. How do we establish a relationship between these two select boxes?

Notice that the country select box has a data-provinceid="province{{ forloop.index }}" attribute. This will generate a string which matches the id of the following select box, establishing a means for the JavaScript to jump from one select box to the other (also, this imposes no other markup dependencies).

Here's the JavaScript setup function:

function selectorSetup() {
    // Adds an event listener to the document, rather than the select box
    document.addEventListener('change', function(e) {
        var countrySelect = e.target;
        if (countrySelect.getAttribute('data-provinceid')) {
            updateProvince(countrySelect);
        }
    }, false);

    var countrySelecters = document.querySelectorAll('[name="address[country]"]');
    {% comment %} Builds up an Array of the values for country and province {% endcomment %}
    var selectedCountry = [{% for address in customer.addresses %}'{{ address.country }}'{% if forloop.last != true %},{% endif %}{% endfor %}];
    var selectedProvince = [{% for address in customer.addresses %}'{{ address.province }}'{% if forloop.last != true %},{% endif %}{% endfor %}];
    // Loops through every address on screen (there can be up to 100!)
    for (var i = 0; i < countrySelecters.length; i++) {
        // Finds the elements we need to tinker with
        var countrySelect = countrySelecters[i];
        var provinceSelect = document.getElementById(countrySelect.getAttribute('data-provinceid'));
        if (provinceSelect) {
            // Makes sure that the current selected value matches the data for the current address
            preSelect(countrySelect,selectedCountry[i]);
            // Ensures that the province select box is right for the current country
            updateProvince(countrySelect);
            preSelect(provinceSelect,selectedProvince[i]);
        }
    }
}
selectorSetup();

Note that a Liquid for loop is used here, to generate two Arrays containing the current values of Country and Province for each address in our list. The index of these values will match the for loop which takes up most of the bottom of the function.

Because of this Liquid dependency (and that this JavaScript is only really any use within the addresses.liquid template), this JavaScript should be included in a script tag towards the bottom of addresses.liquid.

Next, is the updateProvince() function:

// Updates the province select box, if required
function updateProvince(countrySelect) {
    var provinceObj = JSON.parse(countrySelect.options[countrySelect.selectedIndex].dataset.provinces);
    var provinceSelect = document.getElementById(countrySelect.getAttribute('data-provinceid'));
    // Does the chosen country have provinces?
    if (provinceSelect && provinceObj.length > 0) {
        var options = '';
        for (var i = 0; i < provinceObj.length; i++) {
            options = options + '<option value="' + provinceObj[i][0] +'">' + provinceObj[i][1] + '</option>\n';
        }
        provinceSelect.removeAttribute('disabled');
        provinceSelect.innerHTML = options;
    } else {
        provinceSelect.setAttribute('disabled','disabled');
        provinceSelect.innerHTML = '<option value="">Please select a country first</option>';
    }
}

Shopify's province data takes the form of two nested Array-like structures. The inner of these only has two values, which match the value of the option tag and the string of text which appears between the opening and closing option tags.

These two strings are mostly identical, but not always. The script pulls them out by their index position.

Finally, we need a function which loops through the select boxes and ensures that the right data is selected by default (this is not done automatically by {{ country_option_tags }}):

// This is passed a select box and a value. It attempts to find the value within the select box, then change that option tag to "selected".
function preSelect(obj,value) {
    if (obj && value !== '') {
        for (var i = 0; i < obj.length; i++) {
            var option = obj[i];
            if (option.text === value) {
                option.setAttribute('selected','selected');
            } else {
                option.removeAttribute('selected');
            }
        }
    }
}

Nothing particularly smart here. The function either strips out any selected attributes, or adds in one which matches a string which has been passed.

Adding a new address

This one's pretty straightforward:

{% comment %}
Note that you can add a class to this kind of tag like this:
{% form 'customer_address', customer.new_address, class: 'myClass' %}
{% endcomment %}
{% form 'customer_address', customer.new_address %}
    <p>
        <input type="text" name="address[first_name]" id="forname-new">
        <label for="forname-new">First name</label>
    </p>
    <p>
        <input type="text" name="address[last_name]" id="surname-new">
        <label for="surname-new">Last name</label>
    </p>
    <p>
        <input type="text" name="address[company]" id="company-new">
        <label for="company-new">Company</label>
    </p>
    <p>
        <input type="text" name="address[address1]" id="address1-new">
        <label for="address1-new">Address 1</label>
    </p>
    <p>
        <input type="text" name="address[address2]" id="address1-new">
        <label for="address2-new">Address 2</label>
    </p>
    <p>
        <input type="text" name="address[city]" id="city-new">
        <label for="city-new">City</label>
    </p>
    <p>
        <select name="address[country]" id="country-new" data-province="province-new">
            {{ country_option_tags }}
        </select>
        <label for="country-new">Country</label>
    </p>
    <p>
        <select name="address[province]" id="province-new" disabled>
            <option value="">Please select a country first</option>
        </select>
        <label for="province-new">Province</label>
    </p>
    <p>
        <input type="text" name="address[zip]" id="postcode-new">
        <label for="postcode-new">ZIP/Postal Code</label>
    </p>
    <p>
        <input type="tel" name="address[phone]" id="phone-new">
        <label for="phone-new">Phone Number</label>
    </p>
    <p>
        <input type="submit" value="Update details">
    </p>
{% endform %}

The same JavaScript which links together the country and province select boxes further up the page will work here.

Delete an address

{% for address in customer.addresses %}
    <form method="post" action="/account/addresses/{{ address.id }}">
        <p>
            <input type="hidden" name="_method" value="delete">
            <button>Delete {{ address.address1 }} {{ address.address2 }} {{ address.city }} address</button>
        </p>
    </form>
{% endfor %}

Note that in Shopify's documentation of this they add a JavaScript confirm box, before allowing the form to submit.

There's nothing to stop you from including this markup within any of the other places in the liquid file where the same for loop takes place. As long as you don't nest your form tags within each other, it should be possible (for example) to have a Delete button appearing under the form which allows users to edit an address.

Get in touch to find out more     We turn casual browsers into committed buyers