How to update an ancestral element class in response to child input change with Vue.js? - vue.js

I'm using Vue (v2.6.11) and I want to add/remove a class on an ancestor (grandparent) element of an input (checkbox) when it's value is changed. There are an unknown number of checkboxes so they are generated using v-for on the element that I want to add/remove the class. I have a function registered with my Vue instance methods when the checkbox value is changed.
In my code below, the <label> is the element to which I want to add/remove the class in response to the checkbox value changing. I'm trying to resist the urge to apply an :id attribute to the label and then do a lookup in my updateCounter method that will use the id of the checkbox to find the label and add/remove the class using pure JS. Is there a more Vue-esque way to do this?
<label class="item"
v-for="item in myItems"
v-bind:key="item.id"
:for="'item'+item.id">
<div>
<input type="checkbox"
:id="'item'+item.id"
v-model="item.isAccepted"
#change="updateCounter($event)" />
<span>{{ item.name }}</span>
<span>{{ item.status }}</span>
</div>
<div>{{ item.date }}</div>
</label>

just use a dynamic class like so: :class="item.applyClass ? 'item' : ' ' " so put a property on the item and then you can dynamically change that in your updatedCounter function

You can do
<label class="item"
v-for="item in myItems"
v-bind:key="item.id"
:for="'item'+item.id"
:class="{test: item.isAccepted === true}">
<div>
<input type="checkbox"
:id="'item'+item.id"
v-model="item.isAccepted"
#change="updateCounter($event)" />
<span>{{ item.name }}</span>
<span>{{ item.status }}</span>
</div>
<div>{{ item.date }}</div>
This would apply test class only when is Accepted is true for that particular checkbox.

Related

Vue Component that Can Override What Child Renders Dynamically

I am working on a component (InfoCard) that should be able to render any number of fields passed into it with a 'fields' prop, as an array of json objects with a name, value, and some styling options. For certain fields, I want to be able to override what component is used to render, but do it from the parent (Table) rather than inside the InfoCard component, as it should be generic. My first thought was to use a <component :is='field.component'></component>, where it will render as plaintext if field.component is not defined, but to my understand it will be difficult to pass in any potential children necessary for the <component/>. My second thought is to use named slots from within the parent, but I don't think this is possible either in a good way. I'll show my current code.
In my example, I want to be able to detect if the field being rendered is 'status', and if it is, use a different rendering mechanism than displayValue(attribute), without hardcoding it inside InfoCard; I want the parent to be able to override this rendering conditionally. Is there a way to do this in Vue? Thanks
From Table, where data.records is an array of JSON objects:
<info-card
v-for="(record,index) in data.records"
:key="index"
:fields="record"
>
<div v-for="key in Object.keys(record)" :key="key">
<template v-if="field.name=='status'" v-slot:[`${field.name}_value`]>
<p> Field is status !</p>
</template>
</div>
</info-card>
From InfoCard:
<template>
<el-col
:lg="3"
:md="3"
:sm="3"
:xs="3"
v-for="(attribute, index) in fields"
:key="index"
class="attribute"
>
<div
#click="$emit('fieldClicked', attribute)"
>
<el-row
:class="`mid-gray f6 clipped fw5-ns m-b-10 ${attribute.nameClasses}`"
:title="displayName(attribute)"
:style="attribute.nameStyle?attribute.nameStyle:''"
>
<slot
v-if="Object.keys($scopedSlots).includes(`${attribute.name}_name`)"
:name="$scopedSlots[`${attribute.name}_name`]"
>
</slot>
<div v-else>
{{ displayName(attribute) }}
</div>
</el-row>
<el-row
:class="`mid-gray f6 clipped fw5-ns m-b-10 ${attribute.valueClasses}`"
:title="displayValue(attribute)"
:style="attribute.valueStyle?attribute.valueStyle:''"
>
<slot
v-if="Object.keys($scopedSlots).includes(`${attribute.name}_value`)"
:name="$scopedSlots[`${attribute.name}_value`]"
>
</slot>
<div v-else>
{{ displayValue(attribute) }}
</div>
</el-row>
</div>
</el-col>
</template>

VueJS: read div content from its id

In my first example, I have in my component's template:
<div id="hello">hello world</div>
When console.log(this.$refs['hello']) is called in myMethod(), I get undefined in the console.
In my second example, I have:
<div v-for="item in data">
<div :id="'hello-'+item.id">hello {{ item.id }}</div>
</div>
When console.log(this.$refs['hello-1']) is called in myMethod(), I also get undefined in the console.
What's wrong in my code ?
You cannot get element with refs by giving that element id. You should bind ref to that element. Here is an example:
<div v-for="item in data" :key="item.id">
<div :id="'hello-'+item.id" :ref="'hello-'+item.id">hello {{ item.id }}</div>
</div>
And also don't forget to bind key to your iterating elements with v-for

vuejs component that inherits parent's context

Let's start from this
<div class="form-group" :class="{'has-error':determineError('content')}">
<label>Content Label</label>
<div class="mandat">*</div>
<input v-model="form.content" name="content" v-validate="'required|min:5|max:100'" class="form-control">
</div>
The first thing I would like to obtain is to put this piece of code somehow inside a component, something like this:
Vue.component('form-group', {
...
template: `<div class="form-group" :class="{'has-error':determineError('content')}">
<label>Content Label</label>
<div class="mandat">*</div>
<input v-model="form.content" name="content" v-validate="'required|min:5|max:100'" class="form-control">
</div>`
});
As you can see I still have the input field right there. What I would like to do is pass any piece of code instead and the current component must inherit parent's context.
Something like
<form-group>
<template>
<input v-model="form.content" name="content" v-validate="'required|min:5|max:100'" class="form-control">
</template>
</form-group>
How can this be achieved? Notice that I still use parent's context. If using parent's context is not possible, then how can I achieve this in the simplest way?
You have to use slots, which are expanded in the component template with the contents passed by the parent.
In the form-group component:
<template>
<div class="form-group" :class="{'has-error':determineError('content')}">
<label>Content Label</label>
<div class="mandat">*</div>
<slot v-bind:form="form"></slot>
</div>
</template>
You can also add a fallback content inside the <slot> (a default input maybe). Note we are passing the context for the slot contents (see Scoped Slots).

Suffix vue data to class with v-bind

I have the following template within a Vue Multiselect component.
<template slot="option" scope="props">
<div class="option__desc">
<span class="flag-icon flag-icon-{{ props.option.code }}">{{ props.option.code }}</span>
<span class="option__small">{{ props.option.name }}</span>
</div>
</template>
I need to suffix props.option.code to the flag-icon- class but Vue does not allow me to add to the class this way anymore. It requires me to do it with v-bind however I've tried to make it work with v-bind and I cannot.
Is there a way to make this work with v-bind?
try this
<span :class="'flag-icon flag-icon-' + props.option.code">

Vuejs - Event delegation and v-for context reference

I'm using the following snippet to render a list:
<div #click.prevent="myhandler($event)"><!-- Delegated event handler-->
<div class="tasks" v-for="(task, subName) in step.tasks">
<button type="button">
{{ task.caption }}
</button>
<span> {{ task.callableName }} </span>
</div>
</div>
methods: {
myhandler(e){
// Event target may be a button element.
let target = e.target;
// …
// Let's assume we know 'target' is a button element instance.
// I wish I could have access to the v-for item ("task") which is associated in some ways to the button that was clicked.
// let the_task_reference = ?;
}…
Is there a clean way so that I could reach the v-for scope specific task related to that button?
Thank you.
An alternative would be to store the index of the task on the button.
<div class="tasks" v-for="(task, index) in step.tasks">
<button type="button" :data-index="index">
{{ task.caption }}
</button>
<span> {{ task.callableName }} </span>
</div>
And in your handler get the task using the index.
myhandler(evt){
const task = this.step.tasks[evt.target.dataset.index]
...
}
Example.
If you had something stronger like an id, that would be even better.
Not Recommended
There is a hidden property, __vue__ that is added to Vue and Component root elements. Were you to iterate over a component, you would be able to do something like in this example.
I wouldn't recommend that approach because it relies heavily on Vue internals that could change from version to version, but it's there today.
The most straight-forward solution would be to put the event handler on the same div as the v-for directive and just pass in the task variable:
<div
class="tasks"
v-for="(task, subName) in step.tasks"
#click.prevent="myhandler(task, $event)"
>
<button type="button">{{ task.caption }}</button>
<span>{{ task.callableName }}</span>
</div>
If you really need to use an event handler on a parent element, you could keep track of the clicked task via a component property and add an extra click handler to the div with the v-for directive:
<div #click.prevent="myhandler($event)"
<div
class="tasks"
v-for="(task, subName) in step.tasks"
#click="clickedTask = task"
>
<button type="button">{{ task.caption }}</button>
<span>{{ task.callableName }}</span>
</div>
</div>
You would need to add a clickedTask property:
data() {
return {
clickedTask: null,
}
}
And then in the handler you could refer to the task via this.clickedTask.