Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

vincej's avatar
Level 15

Vue js: How to add a class to the closest td on click of Button

I am new to Vue coming off of JS/jQuery. I have a table, and each row has 2 possible buttons and two inputs, all wrapped in <td>. When I click a button I want the nearest <td> to have a class added offering a coloured border. In JQuery I would have used the closest method in selecting the neighbouring <td> Here is my Vue template syntax.

Many thanks!

<tbody>
     <tr v-for="child in registeredChildren">
         <td class="col-2"><a :href="'/getchild/'+ child.child_id">{{child.childFirstName}}</a>&nbsp &nbsp {{child.childLastName}}</td>

                        <!--========TIME IN===========-->
       <td class="col-3 pb-2"}"><input style="text-align:center;" class="form-control" type="text" v-model="child.timeIn" ></td>
       <td><button v-on:click="updateTimeIn(child)" class="btn btn-outline-success">Reset</button></td>

                        <!-- //========TIME Out ===========//-->
       <td class="col-3 pb-2" ><input style="text-align:center;" class="form-control " type="text" v-model="child.timeOut" ></td>
        <td><button v-on:click="updateTimeOut(child)" class="btn btn-outline-success">Reset</button></td>
 </tr>
 </tbody>
         

Methods: I was thinking if I could add some code to the UpdateTimeIn and updateTimeOut methods, this could be an approach?

methods:{
        updateTimeIn(child){
         this.updatedChild = child;
        console.log(child.child_id,child.timeIn)


            axios.post('http://kidsclub.test/upDateChildTimeIn', {
                child_id: child.child_id,
                timeIn: child.timeIn,
            })
                .then(function (response) {
                    console.log(response);
                })
                .catch(function (error) {
                    console.log(error);
                });
        },

**NB** I have the same for updateTimeOut

0 likes
15 replies
tykus's avatar

Generally, Vue's approach is data-driven meaning some piece of state determines how your template is rendered.

You can then conditionally apply a CSS rule depending on some truth test based on that state (using v-bind:class attribute):

<td>
  <button v-on:click="updateTimeIn(child)"
      class="btn btn-outline-success"
      v-bind:class="{'css-rule-name': child.something == 'foo'}"
  >Reset</button>
</td>
1 like
vincej's avatar
Level 15

@tykus HI, thanks for that. Yes, I have been looking at the Vue docs on the topic, ( https://vuejs.org/v2/guide/class-and-style.html )

and SO all afternoon, and I understand the fundamentals however, I have not yet managed to apply it such that when I click the button it applies the class to that neighbouring <td> and not all of them, as they are all generated with a v-for

tykus's avatar

@vincej What happens to the state of the child whenever you click the button; what changes?

vincej's avatar
Level 15

@tykus HI MANY Thanks for coming back! Ok, a nice person on SO has made a suggestion which has gotten my code half the way there. When you click the button the background colour changes, bravo. However, it changes the background for ALL the<td>, and not the input standing next to it's associated button. NB: every input has it's own button which updates the updateTimeIn(Child). Child refers to an id which comes off of the <tr v-for="child in registeredChildren">. Somehow, I need to apply the css to ONLY that input neighbouring button so that the user knows that they have altered that specific input. For the sake of brevity I offer you here only that code which has been altered:

Template

<td class="col-3 pb-2 upDated"  ><input style="text-align:center;" class="form-control"  :class="{'dynamic-class': isUpDated }"   type="text" v-model="child.timeIn" ></td>
<td><button @click="updateTimeIn(child)" class="btn btn-outline-success">Reset</button></td>

Data

 data: function () {
        return {
            registeredChildren: [],
            updatedChild:{},
            childTimeOut:{},
            isUpDated:false
        }
    },

Script

  methods:{
        updateTimeIn(child){
         this.updatedChild = child;
          this.isUpDated = !this.isUpDated;
}

Styles

 <style>
    .upDated {
      background-color: white;
    }

    .dynamic-class {
      background-color: red;   // red is only for testing! 
    }
  </style>

vincej's avatar
Level 15

@jlrdw Thanks for that. I have thousands of lines of regular JS, and that is my go to answer for this kind of problem. I would have had this answered days ago with JS / JQuery. However, I am trying to get off of JS, as it is difficult to maintain and it is very verbose. I tried using livewire with the same problem, and for me that was a total road accident. I find it bizaar that this is so hard with such a great framework as Vue .... I am also looking at Svelte, which is fascinating and not such a monster as Vue. Cheers!

vincej's avatar
Level 15

@jlrdw Well, I thought I would update you. I spent like forever trying to use LiveWire to solve this problem. Then I spent what felt like a gazillion years with Vue trying to solve the same problem. And in the end I came to the conclusion that JS frameworks are very poor at identifying a single and specific input wrapped in a <td></td> in a sea of rows in an ocean of <td>'s. So, yesterday I gave up, and resorted to JS/JQuery:

        let timeIn = $(this).closest("tr").find("td.timeIn>input").val();
        let id = $(this).closest("tr").find("td.timeIn>input").attr("id");
        $(this).closest("tr").find("td.timeIn>input").css({"color": "red", "border": "2px solid red"});
});

Needless to say, it works and is fine. So, I am going to try one more JS Framework, "Svelte" BUT, in a test system - not my live system. I will give it this very task to see if it can handle it. I am hopeful, as Svelte is marketed as a JS compiler with bolt on Framework features. Anyway - I thought of you, when I gave up with Vue, and thought this might bring a smile to you. Cheers! Vince

vincej's avatar
Level 15

@tykus Ok, I have done a little more reading, and I have added a :key="child.child_id " . I have tried applying it 5 different ways, and I still do not get the neighbouring input to exclusively take the class. All the inputs have the class applied. I am perplexed, it is supposed to work.

<tr v-for="child in registeredChildren"  :item="child" :key="child.child_id">
  <td class="col-2"><a :href="'/getchild/'+ child.child_id">{{child.childFirstName}}</a>&nbsp &nbsp {{child.childLastName}}</td>

                        <!--========TIME IN===========-->
 <td class="col-3 pb-2 upDated"    ><input  :key="child.child_id"  style="text-align:center;" class="form-control"  :class="{'dynamic-class': isUpDated }"   type="text" v-model="child.timeIn" ></td>
 <td><button @click="updateTimeIn(child)" class="btn btn-outline-success">Reset</button></td>

I have added an image to help you understand.. https://imgur.com/a/rzCVccD

MANY THANKS!

martinbean's avatar

@vincej As a couple have said, Vue is based on state, and your template’s appearance should be a function of that view. If a CSS class should be applied to a particular table row, then it should be because the state of the object that row represents has changed and the condition evaluates to a truth-y value.

I’m unsure what class you’re trying to apply when from your example, so I’ll try to create a simplified example to hopefully demonstrate what we mean by using state to drive your UI.

A Vue component has data. So if you have a table, I imagine you have an array of objects representing some form of entity that you want to display as rows in a table. If you specify this array in the data key of your Vue component, then it becomes “reactive”. That means, any modifications to the array or objects inside it, will cause your template to re-rendered with the new state of that data:

data() {
  return {
    items: [
      { id: 1, name: 'Foo', isComplete: false },
      { id: 2, name: 'Bar', isComplete: false },
    ];
  };
},

So we have an array with two objects, and each object has three keys: id, name, and isComplete. We can render that in a simple table in the template:

<table>
  <thead>
    <tr>
      <th scope="col">ID</th>
      <th scope="col">Name</th>
      <th scope="col">Actions</th>
    </tr>
  </thead>
  <tbody>
    <tr v-for="item in items" v-bind:key="item.id">
      <td>{{ item.id }}</td>
      <td>{{ item.name }}</td>
      <td>
        <template v-if="item.isComplete">
          <button type="button" v-on:click="markAsIncomplete(item)">Mark as incomplete</button>
        </template>
        <template v-else>
          <button type="button" v-on:click="markAsComplete(item)">Mark as complete</button>
        </template>
      </td>
    </tr>
  </tbody>
</table>

So we have a simple table. There’s only one <tr> element inside the <tbody>, but that iterates for every element in the items data array (so in the browser they’ll actually be as many table rows as there are items in the array). If an element is added, the table will be updated to include a row for that element. If an element is removed, the corresponding row will be removed from the table. If an item’s name is updated, it will also be updated in the corresponding row. So we never manipulate the template directly; it updates in response to changes in the data (the state).

So, we can conditionally add a CSS class to each row based on an expression:

<tr v-for="item in items" v-bind:key="item.id" v-bind:class="{ 'is-complete': item.isComplete }">

So now, if the item’s isComplete attribute is true, a CSS class named is-complete will be added to this row. We can see this in action by implementing handlers for the buttons I defined in the template above:

markAsComplete(item) {
    this.$set(item, 'isComplete', true);
},
markAsIncomplete(item) {
    this.$set(item, 'isComplete', false);
},

The current item in the iteration was passed in as a parameter to this methods, so we can manipulate it and Vue’s reactivity will keep an eye on it. So if you change one of these object’s properties, then again the template will re-render to show the state of those latest changes.

So yeah, I think the takeaway is, don’t think in terms of manipulating the actual DOM and template itself. Instead, build your template that responds shows/hides elements, or adds/removes classes based on state. Instead of thinking, “when X happens, add this class to this element”, instead think, “this element has a class, but it should only be shown when X is true”.

1 like
vincej's avatar
Level 15

@martinbean Martin, Many thanks for this outstanding response. It should be pinned. I have spent years writing raw JS/JQuery and my brain is deeply scripted in it's patterns. Too often Vue is described as an adjunct to JS. This is wholly misleading. Using Vue requires a wholesale reset of one's thinking. I am 7 hours behind the UK and I am going to have a go now at apply state to my table. Cross fingers I hope I get it right first time, and don't hit a wall. Many thanks is not enough!! Cheers Vince

vincej's avatar
Level 15

@martinbean Ok, So, I have been studying your outstanding response together with the Vue docs and online resources. Nearly all the examples I have looked at require a true / false test as you also pointed out. However, my data set does not contain a such attributes off the DB. My table columns are "timeIn" and "timeOut". Both contain either a time eg 15:30 or "absent". Vue does not like either of those and they break the site. Any effort to adapt "absent" to say, "null" also breaks the site. I have not been able to find how to adapt these values to "true" and "false". So, that has been issue One.

HOWEVER, In my specific case, this above issue might be irrelevant. My rows of inputs have a unique Child_id contained within them verifiable through Chrome console. When I click the "Reset" button neighbouring an input, , Vue calls a method called "UpdateTimeIn". This method knows that what the unique Child_id is for that specific input and updates the "timeIn" / "timeOut" in the DB for that specific child. It then adds the time into for that correct input. BUT! It will also apply the class to ALL the inputs, and not just the unique child input just updated with a "timeIn". Note: there is no true/false test here at all. This is a mystery for me. Here is my code so far:

Relevant Template Section

<tr v-for="child in registeredChildren" v-bind:key="child.child_id">
      <td class="col-2"><a :href="'/getchild/'+ child.child_id">{{child.childFirstName}}</a>&nbsp &nbsp {{child.childLastName}}</td>

                        <!--========TIME IN===========-->
                       <td class="col-3 pb-2" >
                         <input
                           :id=child.child_id
                           style="text-align:center;"
                           class="form-control upDated"
                           :class="{'dynamic-class': isUpDated }"
                           type="text" v-model="child.timeIn"
                           v-bind:class = "(child.child_id = id)?'upDated':'dynamic-class'">  // This does not apply the css
                       </td>

                       <td><button @click="updateTimeIn(child)" class="btn btn-outline-success" >Reset</button></td>
                    </tr>

It seems obvious to me that I should be able to make use of this Vue ability to identify the correct input by the child.child_id, and in addition to adding the time to the correct input, then also add a css class to the same input driven by the same click event which updated the timeIn. However, despite all my searching over the weekend, I can not find the solution.

Relevant Script sections

export default {
    data: function () {
        return {
            registeredChildren: [],
            updatedChild:{},
            childTimeOut:{},
            isUpDated:false
        }
    },
    mounted() {
        this.loadRegisteredChildren();   // Axios Loads all the children
    },

    methods:{
        updateTimeIn(child){
         this.updatedChild = child;
          this.isUpDated = !this.isUpDated;
        console.log(child.child_id,child.timeIn)

//AXIOS UPDATE THE DB here, with the correct timeIn for the child 

 <style>
    .upDated {
      background-color: lightblue;
    }
    .dynamic-class {
      background-color: lightpink;
    }
  </style>

I do not want to wear out my welcome with you, however I do not want to give up! The docs are good, but not specific to my question. If you can point me in the right direction I would be very grateful!! Many Thanks Vince !

apex1's avatar

@vincej Haven't read your entire post, but this:

v-bind:class = "(child.child_id = id)?'upDated':'dynamic-class'">

You should be using a comparison operator, e.g.,: == or ===

vincej's avatar
Level 15

@apex1 Thanks Apex1, you are a star for always helping out! I tried your suggestion and it did not work. Essentially my problem is that Vue is applying the class to ALL the inputs and not just the selected input. I am going to look at "refs" in the hope that might find the answer. Cheers.

apex1's avatar

@vincej I don't know exactly what you want to accomplish, but one thing that you might want to try is in your loadRegisteredChildren() method, when you set your data to this.registeredChildren, you can map over the array and create a temporary property. Something like:

   const response = await axios.get(...etc...);
   this.registeredChildren = response.map((child) => ({
        ...child,
        isUpdated: false,
      }));

Then in your updateTimeIn() method, you can set that property (isUpdated or whatever you want to call it) to true:

updateTimeIn(id) {
      this.registeredChildren = this.registeredChildren.map((child) =>
        id === child.id ? { ...child, isUpdated: true } : child
      );
    },

or if you want to toggle:

this.registeredChildren = this.registeredChildren.map((child) =>
        id === child.id ? { ...child, isUpdated: !child.isUpdated } : child
      );

And then in your input:

      <input
		...other attributes...
        :class="child.isUpdated ? 'some class' : 'or default class''"
 
      />

There is probably a cleaner way, I saw martinbean use Vue.set() (this.$set(...)); so you may also want to look into that.

Please or to participate in this conversation.