Our first form needs an uncertain amount of fields.


With active model serializer I was able to post back to the server with data. I wanted to take this a step further and create my first form. If I’m going to make a recipe site where a user can add recipes I really need to have a new recipe form. This form will have various fields for the recipe such as Title, Description, and Ingredients.

But wait, ingredients are not just a simple one entry field. An ingredient has a name, an amount, and it might have a short description on its kind, minced or whole garlic for example. Ingredients really need their own model and a recipe will have many ingredients. So the form has the recipe fields and then some separate fields for the ingredients.

But wait, how many ingredient fields should I have? Short answer: I don’t know. I need to have a way for the user to add and remove ingredient fields from the page. As a basis I will display three ingredient fields. I like the number three plus it doesn’t overwhelm the user with too many fields. Most recipes have more than three ingredients so I need a button that adds more fields. I also need a delete button which will delete individual ingredients.

First pass: (Note syntax is Emblem)

//templates/recipes/new.emblem
Recipe fields up here

div.text-center
  .container-fluid
    .form
      .form-group
        .row
          .col-md-1
          .col-md-2
            label Ingredient Amount
          .col-md-2
            label Ingredient Name
          .col-md-4
            label Ingredient Type(minced, crushed, etc.)
          .col-md-2
            | Delete
      .form-group
        .row
          .col-md-1
          .col-md-2
            = input value=amount class="form-control"
          .col-md-2
            = input value=name class="form-control"
          .col-md-4
            = input value=type class="form-control"
          .col-md-2{action "remove"}
            i.delete-icon
      // more ingredient fields
    button.btn.btn-small{action "addField"} Add Another Ingredient

First those ingredient fields are going to be reproduced A LOT so lets throw it into a component. I’ll get to the actions later.

ingredient-field name=(mut (get ingredient 'name')) amount=(mut (get ingredient 'amount')) type=(mut (get ingredient 'type'))

Woh, that is a lot of stuff! I’ve named my component ingredient-field. Components must have a - in their name to avoid conflicts with built-in controls that wrap HTML elements. This is also a requirement in web components so Ember is following the standard here. Next I am passing in data to the component through name, amount, and type. Components are usually data down, meaning that they receive data to display but do not change it. This is because components do not have the controllers scope that they are called from. They are unaware of their surroundings and can only access what is passed to them.

But the user will be entering data into these fields. That data has to go somewhere to get saved! It sure does and we use Ember’s mut and get to obtain a two-way binding. Taking the first property passed into the component, the name, we can run through a quick explanation. First I get an object, the ingredient, and I want the name property. I want this property to be mutable so I use mut. The same goes for the amount and type.

Ok, I’ve got this figured out for one set of ingredient fields now how do I make it dynamic and how do I keep the ingredients separate but still have access to them on the recipe new controller? ENTER (handlebars syntax). Each as allows iteration over a list of items. In this case I’m going to build up an array of ingredient objects which I’ll call rangeOfInputs and then iterate over each object.

each rangeOfInputs as |ingredient index|
  ingredient-field name=(mut (get ingredient 'name')) amount=(mut (get ingredient 'amount')) type=(mut (get ingredient 'type'))
button.btn.btn-small{action "addField"} Add Another Ingredient

Each as accepts a block. I’m passing the ingredient object and the index. I can then mutate the data on each individual ingredient. In the recipe/new controller I just need to iterate over this array.

//controllers/recipes/new.coffee
RecipesNewController = Ember.Controller.extend
  initialInputs: 3,
  formFields: [],

  _newIngredient: ->
    @store.createRecord 'ingredient'

  rangeOfInputs: Ember.computed 'initialInputs', ->
    initialInputs = @get 'initialInputs'
    Array.apply(null, Array(initialInputs)).map( (_, i) => @_newIngredient())

  actions:
    addField: ->
      @get('rangeOfInputs').pushObject(@_newIngredient())

rangeOfInputs first grabs the initalInput number. I build an array with this number of elements. I then fill each element of array with an ingredient object. When I add another field I push a new ingredient object into the rangeOfInputs array. Removal of fields is done in the ingredient-field component.

//controller/components/ingredient-field.coffee
IngredientFieldComponent = Ember.Component.extend

  actions:
    remove: ->
      @remove()

This is not a complete solution yet. We still need to save the recipe fields and then save the ingredients and build the relationship to the recipe. This code also has a flaw. The remove action does not remove the element of the rangeOfInputs array. I have not figured out the solution to this but I have two basic ideas.

  1. Pass the index along so when I remove the component I also remove it from the correct position in the array.

  2. Don’t worry about removing it from the array. Have the remove action also wipe all the data out of the ingredient fields. Then when I save the record I can ignore all ingredient objects that have no properties.

Comments