skip to Main Content

I have a page that uses an Alpine.js template to add and remove sets of form fields for entering people’s details and that is also supposed to update a price based on how many people there are which is passed as a hidden input.

It works fine when you add people, and also when you remove them again. But if you then try adding after removing, it gets the total incorrect.

The code is

<div x-data="add()" x-init="addNew">
    <div>
      <label for="price">Price:</label>
      <input type="text" name="price" id="price" value="100">
    </div>
    <template x-for="(field, index) in fields" :key="index">
      <div>
        <p>Person: <span x-text="`${index + 1}`"></span></p>
        <div>
          <label :for="`first_name_${index + 1}`">First name:</label>
          <input
            x-model="field.first_name"
            type="text"
            :name="`first_name_${index + 1}`"
            :id="`first_name_${index + 1}`"
          >
        </div>

        <div>
          <label :for="`last_name_${index + 1}`">Last name:</label>
          <input
            x-model="field.last_name"
            type="text"
            :name="`last_name_${index + 1}`"
            :id="`last_name_${index + 1}`"
          >      
        </div>

        <button type="button" @click="remove(index)">Remove</button>
      </div>

    </template>
    <button type="button" @click="addNew">Add</button>
</div>

<script>
  let priceInput = document.getElementById('price'),
      count = 1;

  function calcTotal(count) {
    if(count==1) priceInput.value = 200;
    if(count==2) priceInput.value = 375;
    if(count>2)  priceInput.value = 550;
  }

  function add() {
    return {
      fields: [],
      addNew() {
        this.fields.push({
          first_name: '',
          last_name: '',
        });
        calcTotal(count);
        count++;
      },
      remove(index) {
        this.fields.splice(index, 1);
        count = index;
        calcTotal(count);
      }
    }
  }
</script>

and I’ve created a CodePen.

Prices start at 200 for 1 person, then 375 for 2 and then are capped at 550 for 3 or more. If you test with the CodePen, you can see that the totals go correctly if you add up to 3 people and then remove back down to 1.

But when you get back down to 1 and add another again, the total doesn’t update until you’ve added a 3rd (i.e. 375 instead of 550).

What do I need to do to fix it?

And also, unrelated to this specific issue, when I was creating the CodePen and chose Alpine as a js dependency, it automatically added Alpine 3.1.4.0 but my project was created originally with 2.x.x. With 3.x in the CodePen, nothing in the <template> would render. What’s different between 2 and 3 that causes that?

2

Answers


  1. You’re setting and updating your count variable all wrong. I’ve fixed the script for you:

    <script>
      let priceInput = document.getElementById('price'),
          count = 0;
    
      function calcTotal(total) {
        console.log(total);
        if(total==1) priceInput.value = 200;
        if(total==2) priceInput.value = 375;
        if(total>2)  priceInput.value = 550;
      }
    
      function add() {
        return {
          fields: [],
          addNew() {
            this.fields.push({
              first_name: '',
              last_name: '',
            });
            ++count;
            calcTotal(count);
          },
          remove(index) {
            this.fields.splice(index, 1);
            --count;
            calcTotal(count);
          }
        }
      }
    </script>
    

    As you can see, I’ve set the count to 0 by default, since you use x-init to add a new field, which increments the count to 1 (in your case to 2, even though there was only one field).

    I changed the variable name in the calcTotal function to prevent variable conflict.

    I changed addNew to ensure it first increments and then calculates, since you first add new fields.
    I changed remove to ensure it first decrements and then calculates, since you first remove the fields. I don’t use the index to set the count since that makes no sense whatsoever (Image you have 5 people, you delete number 2, so now you only have 2 left??).

    TIP: You could even make it simpler by just using this.fields.length and removing the count variable altogether (calcTotal(this.fields.length)).

    Login or Signup to reply.
  2. The variable count is 1 ahead of index (since index starts at 0) but when you remove a row you set them equal. Moreover if you remove a row in the middle the count variable results inconsistent.

    This is full-AlpineJs working version:

    <div x-data="add()">
    
        <div>
            <label for="price">Price:</label>
            <input type="text" name="price" x-model="priceInput"">
        </div>
    
        <template x-for="(field, index) in fields" :key="field.id">
    
            <div>
    
                <p>Person: <span x-text="`${index + 1}`"></span></p>
    
                <div>
    
                    <label>
    
                        First name:
    
                        <input
                                x-model="field.first_name"
                                type="text"
                                :name="`first_name_${index + 1}`"
                        >
    
                    </label>
    
                </div>
    
                <div>
    
                    <label>
    
                        Last name:
    
                        <input
                                x-model="field.last_name"
                                type="text"
                                :name="`last_name_${index + 1}`"
                        >
    
                        </label>
    
                </div>
    
                <button type="button" @click="remove(index)">Remove</button>
    
            </div>
    
        </template>
    
        <button type="button" @click="addNew">Add</button>
    
    </div>
    
    <script>
    
        function add() {
    
            return {
    
                fields: [],
                priceInput: 100,
    
                init() {
                    
                    this.$watch('fields', () => this.calcTotal());
                    this.addNew();
                },
    
                calcTotal() {
    
                    const count = this.fields.length;
    
                    if (count === 1) {
                        this.priceInput = 200;
                    }
                    else if (count === 2) {
                        this.priceInput = 375;
                    }
                    else if (count > 2) {
                        this.priceInput = 550;
                    }
                },
    
                addNew() {
    
                    this.fields.push ({
    
                        id: Date.now(),
                        first_name: '',
                        last_name: '',
                    });
    
                    // this.calcTotal();  //-- enable this if $watch is removed
                },
    
                remove(index) {
    
                    this.fields.splice(index, 1);
    
                    // this.calcTotal();  // -- enable this if $watch is removed
                }
            }
        }
    
    </script>
    

    I moved all the Javascript into the Alpine object.

    The price field now is binded with the priceInput variable using x-model so it’s automatically updated without listeners.

    Since the array fields[] already has its own length I have used it in place of the count variable.

    I have added an unique id to the array row because when the array is compacted using splice() also the indexes are compacted and Alpine may lose the references.

    I’ve also added a $watch, so when the array is modified the calcTotal() function is executed automatically. If you prefer, since the calcTotal() function is called also when you edit first_name and last_name, you can remove the $watch and enable the explicit call of calcTotal() in the addNew() and remove() methods

    The initialization has been moved in the init() function of the Alpine object

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search