AngularJS Infinite List – How to create a list that automatically adds a blank textarea as the user adds new data

This tutorial is about a neat trick you can use with ng-repeat and inputs using AngularJS. This is just one tiny part of a larger AngularJS project of mine you can explore here: Chicken Breast Meals on GitHub.

Let’s say you are building a user input form that lets the user input series of items in a list, such as ingredients in a recipe. You could have the user click a link to add a new input field before typing in each ingredient, but that’s an extra (and annoying) step nowadays for users.

What you really want is a list of inputs that grows itself, offering a new blank input in response to each addition the user makes:

dynamic_list_animation
Infinitely-expanding list grows as the user adds to it

 

PLUNKER DEMO

The rest of this tutorial uses the Chicken Breast Meals project code to explain how this feature was made.

Part 1: In the view (.html file)

The html code (and Angular directives) that create the ingredients list above is in app/views/admin/admin-edit-meal-view.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<h2>Ingredients</h2>
  <ol class="ingredients-list">
  <!-- loop through and display existing ingredients -->
    <li data-ng-repeat="ingredient in formMeal.ingredients track by $index">
    <textarea name="ingredientLines" 
              type="text"
              data-ng-model="formMeal.ingredients[$index].name"
              placeholder="Add ingredient"
              data-ng-change="changeIngredient($index)">
    </textarea>
 <!-- trash can button -->
 <a href="" data-ng-show="ingredient" 
            data-ng-click="formMeal.ingredients.splice($index,1)">
 <img src="/assets/delete.png"/></a>
 </li>
 </ol>

When the user selects a recipe to edit in the admin page, that selected recipe is represented by an object called formMeal. Inside formMeal are properties like:

  • name (which is saved as a String)
  • yield (saved as a Number)
  • cookTime (another Number)
  • ingredients (an Array of Objects)

On the <li>

The ng-repeat directive builds the list of ingredients by creating a <li> and a <textarea> for each ingredient already found in the saved recipe data.  Each ingredient has an index in the ingredients array, so we grab its name out of the array of ingredient objects like so:

formMeal.ingredients[$index].name

Immediately following the ng-repeat directive is $track by index. This bit of code is easy to overlook but it’s very important: it’s what keeps the user’s current textarea in focus while the user edits it. Without $track by index, the app kicks the user out of that text box after the first typed letter. (Ask me how much fun I had debugging this lose-focus problem…)

In the <textarea>

Each ingredient is represented by a <textarea>, and each one has its own ng-model directive pairing it with that particular index in the array.

data-ng-model="formMeal.ingredients[$index].name"

This lets us edit an existing ingredient anywhere in the list by that ingredient’s index. Since ingredients is an array, we need to pass it the index of the ingredient we’re editing via the <textarea>. (You can read more about ng-repeat and $index here in the Angular documentation.) This placeholder part is straightforward:

placeholder="Add ingredient"

This is what puts the default text into each <textarea> when the user hasn’t entered anything yet. It’s just a nice UX touch.

Finally, we have an ng-change directive. You can read more about ng-change here, basically all it does is call the method (or do the thing) you tell it to do any time there’s a change in the <textarea> it’s associated with.

data-ng-change="changeIngredient($index)"

A change to the <textarea> (ie: user typing) causes the method changeIngredient() to run with each change.

Wait, where’s changeIngredient()? It’s over in app/js/controllers/cbm-admin-controller.js, which we will look at next.

Part 2: In the controller (.js file)

Now we’re inside app/js/controllers/cbm-admin-controller.js looking at the changeIngredient() method.

We already saw that whenever the user updates text inside one of those <textarea> regions, this method gets called. (If you were to put a console log inside changeIngredient(), you would see it called every time you typed a letter into the textarea.)

1
2
3
4
5
$scope.changeIngredient = function(index) {
   if (index == $scope.formMeal.ingredients.length -1){
     $scope.formMeal.ingredients.push('');
   }
};

changeIngredient(index) checks the index that’s been passed in:

  • if that index is at the end of the array (ie: its index number is one less than the array’s length), then we are editing the last ingredient in the list and we need to push an empty ingredient (”) to the ingredients array to make the empty box appear at the end
  • if that index is not at the end of the array, we just update whatever’s at this index since it’s an ingredient that already exists. This is why you don’t see an empty box get added to the end of the list if you’re editing a field that’s not at the end.

It’s important to observe that this method works by checking that the user is editing the last index (which is always the empty <textarea>). This is how we  don’t spawn new, empty textareas for editing earlier ingredients in the list.

What updates the ingredients list automatically? That’s Angular’s two-way data binding at work. Any time you update a model the change happens in real time.  If you’re new to Angular, here’s a Plunker demonstrating a very simple implementation of Angular’s two-way data binding.

Part 3: Offering an empty field by default

When you initialize your data or your app, you’ll need to include something like:

$scope.formMeal.ingredients = [''];

or

$scope.ingredients.push('');

so that the ingredients list has an empty one in it by default. Your implementation needs will vary, of course, but hopefully this little guide gave you enough of a start to build this “infinity list” into your own AngularJS form!

Don’t miss the Plunker demo of a simplified version of this feature that you can play with and adapt to your own project.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.