Enhancing Arrays in HTML forms using AngularJS

This was inspired by Adam Wathan's blog post here: https://adamwathan.me/2016/04/06/cleaning-up-form-input-with-transpose/

Check out his book here, he's brilliant!

The purpose of Adam's blog post was to show the utility of Collections in Laravel. I recently ran into the same problem and solved it on the front-end using AngularJS instead.

Dealing with arrays in HTML form submissions tends to be overly complicated, however with a little javascript (AngularJS), it can actually be bearable. I recently solved this in a really fun way and I thought I'd tell you guys about it.


So let's set the stage.

We want people to be able to go to our website and fill out a form with contact information for everyone they know. Not only do they need to be able to lots of contacts, but also should be able to remove mistakes as well before submitting the form.

In the best of situations, you would like to send something like this to the server as a form submission:

[
    'contacts' => [
        [
            'name' => 'Jim Awesomesos',
            'email' => 'jim@example.com',
            'relation' => 'My best friend',
        ],
        [
            'name' => 'Fred William Decker III',
            'email' => 'fwdiii@example.com',
            'relation' => 'My landlord',
        ],
        [
            'name' => 'Juanita Garcia',
            'email' => 'juanita@example.com',
            'relation' => 'My co-worker',
        ],

        ...

    ],
];

And making a simple HTML form to accomplish this is not as simple as you might think. Let's not worry about the multiple contacts right yet. We can get started by simply trying to get our data in the correct format.

HTML forms can use array format to return multiple instances of a contact so, same as in PHP, you might think that this could work:

<form method="POST" action="/contacts">
    <div>
        <label>
            Name
            <input name="contacts[][name]">
        </label>
        <label>
            Email
            <input name="contacts[][email]">
        </label>
        <label>
            Relation
            <input name="contacts[][relation]">
        </label>
    </div>

    <div>
        <label>
            Name
            <input name="contacts[][name]">
        </label>
        <label>
            Email
            <input name="contacts[][email]">
        </label>
        <label>
            Relation
            <input name="contacts[][relation]">
        </label>
    </div>

    <button type="button">Add another contact</button>

    <button type="submit">Save contacts</button>
</form>

but that actually returns an array that looks like this:

Broken Input

HTML has no real understanding what you meant, so it did not only add a second instance of the array like we intended. Instead it saw [] as meaning "add a new array", so each field is within it's own array. :( This will not do.

To fix this, we need to explicitly set the index that HTML will use to build out the request. Something like this should do the trick:

<form method="POST" action="/contacts">
    <div>
        <label>
            Name
            <input name="contacts[0][name]">
        </label>
        <label>
            Email
            <input name="contacts[0][email]">
        </label>
        <label>
            Relation
            <input name="contacts[0][relation]">
        </label>
    </div>
    <div>
        <label>
            Name
            <input name="contacts[1][name]">
        </label>
        <label>
            Email
            <input name="contacts[1][email]">
        </label>
        <label>
            Relation
            <input name="contacts[1][relation]">
        </label>
    </div>

    <button type="button">Add another contact</button>

    <button type="submit">Save contacts</button>
</form>

So we will now have to keep track of a running number of how many contacts are added at any given time. Since we set both of these explicitly the HTML grouped the fields together instead of separating them, just like we wanted:

Correct Input


Now we can go about adding Javascript into the application. Personally, I like to use AngularJS (1.5.3 at the time of writing) for my form handling. It provides some simple solutions for some of the common problems with vanilla Javascript/jQuery approaches.

Let's add AngularJS to our form.

  • Include AngularJS and setup the controller:
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js"></script>
<script type="text/javascript">

angular.module('contacts_form', [])
.controller('ContactsFormCtrl', function()
{
    //
});

</script>
  • Instantiate AngularJS on the page:
<body ng-app="contacts_form">
  • Connect the controller:
<form method="POST" action="/contact" ng-controller="ContactsFormCtrl as contactsCtrl">

Now, we can set up an array which will hold all of the contacts that are added in the form. This will serve as our "source of truth" for the number of contacts that are added/removed.

.controller('OwnershipFormCtrl', function()
{
    this.contacts = [{}];
});

In order to get the multiple contact instances, we will use the ng-repeat directive on the div surrounding the form inputs.

<div ng-repeat="contact in contactsCtrl.contacts">
    <label>
        Name
        <input name="contacts[0][name]">
    </label>
    <label>
        Email
        <input name="contacts[0][email]">
    </label>
    <label>
        Relation
        <input name="contacts[0][relation]">
    </label>
</div>

Now let's add functionality to our button to add new objects to the array. We can use the ng-click directive and a method on our controller to handle this rather simply.

  • Add the ng-click to your html view
<button type="button" ng-click="contactsCtrl.addContact()">Add another contact</button>
  • Add the method to the controller:
this.addContact = function()
{
    this.contacts.push({});
};

That works great, now we can dynamically add tons and tons of contacts in the form. The next problem is that now, if we submit it, our submission now looks like this:

Only One Contact

Because every repeated row had an index of 0, only the last row is processed. We need a way to set indexes for each array of contacts dynamically. Luckily, the ng-repeat directive has a special variable called $index that automatically iterates over each element as it builds out the form.

Now we can simply use $index as our array index instead of 0:

<div ng-repeat="contact in contactsCtrl.contacts">
    <label>
        Name
        <input name="contacts[{{ $index }}][name]">
    </label>
    <label>
        Email
        <input name="contacts[{{ $index }}][email]">
    </label>
    <label>
        Relation
        <input name="contacts[{{ $index }}][relation]">
    </label>
</div>

And it works!

Correct Input

But before we go, there is one more thing left to consider. People should be able to delete a contact after they have added it. While normally, we would need to be concerned about indexes getting out of place if they deleted one, then added one, etc. like this:

var array = [
    0: [],
    1: [],
    2: [],
    3: [],
    4: [],
    5: [],
]

// delete indexes 3 and 4
// and now the indexes are out of sync
// this could cause problems on the backend
var array = [
    0: [],
    1: [],
    2: [],
    5: [],
]

However, since we are using Angular's $index this shouldn't be a problem. Angular's ng-repeat is handling the $index, and the array we are building vm.contacts is only a placeholder without any real information except for the amount of objects to repeat.

(Side note: ng-repeat creates a unique identifier hash that has nothing to do with the actual indexes in the array. This can be changed using track by)

Now we can add a simple delete function using ng-click again, but this time within the ng-repeat.

  • Add the ng-click
<div ng-repeat="contact in contactsCtrl.contacts">
    ...

    <button type="button" ng-click="contactsCtrl.removeContact($index)"> Remove</button>
</div>
  • Add the removeContact method to the controller
this.removeContact = function(index)
{
    this.contacts.splice(index, 1);
};

And there you have it, no matter how many contacts we add or delete, it will always return a perfectly indexed array. I have uploaded a working version (aside from the server script) onto codepen.io if you're interested.

Handling arrays in HTML forms doesn't need to be a complex endeavor. AngularJS has some really simple solutions to problems that are usually very complicated. I hope this has been useful, and I'd love to hear your suggestions.

-- jwk --

Josh Friend

Full-Stack Software Developer at Ramsey Solutions. I build mostly using Ruby on Rails, React, Laravel, and Magnolia CMS. I always wanted to be an inventor when I grew up, so now I enjoy providing simple, intuitive solutions to complex problems.

Nashville, Tennessee

Subscribe to joshwhatk

Get the latest posts delivered right to your inbox.