I'm dealing with a problem I can't figure out where I have an array of items that should be rendered in a component but inside that component they can be manipulated into a new array, so whenever a change is made into one of the items it should be pushed into the itemsToEdit array, instead of modifying the original item because later I need to send that new array to the server with only items modified and only the fields modified...
My child component has a simple checkbox (that is working the way it should) with a checked property which shows the default value if given, and a v-model with all the logic that actually works.
If I set up the v-model to v-model="item.show" it changes the original item, so there's nothing to change in there, but I can't send from parent to children the itemsToEdit array because it is empty and v-model="items.id.show" won't work.
I've worked with multiple checkboxes and an array v-model but it is a different workflow because I actually edit the original array of items, so it will push/remove items as I check the checkboxes but that's not what I want, the original array should stay as it is all the time.
Here's my simplified code, the children actually has a lot of checkboxes but I'll show just one because simplicity.
Parent component
<template>
<div>
<TestComponent v-for="i in items" :key="i.id" :item="i" :items-to-edit="itemsToEdit"/>
</div>
</template>
<script>
import TestComponent from '#/TestComponent'
export default {
name: 'MyParent',
components: { TestComponent },
data () {
return {
items: [
{ id: 1, name: 'test', show: false },
{ id: 2, name: 'test 2', show: false },
{ id: 3, name: 'test 3', show: true },
{ id: 4, name: 'test 4', show: false }
],
itemsToEdit: []
}
}
}
</script>
Child component
<template>
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>
<MyCheckbox :checked="item.show"/>
</td>
</tr>
</template>
<script>
export default {
name: 'TestComponent',
components: { MyCheckbox },
props: ['item', 'itemsToEdit']
}
</script>
EDIT: One thing I forgot, I obviously can use $emit and listen on parent then push into the array, but that's not what I want, I am looking for a better way to implement this, if I have no other option, I will go with the events.
EDIT2: I can't 'clone' original array into the itemsToEdit because I want that array to be only filled up whenever a real change comes in, because later, the request send to server will only contain real changes, if I send the whole array of id's it will try to modify them even if they have no changes so it will be a waste of performance checking everything serverside.
Regardless of your selected approach, my recommendation is to keep your list logic in one place (if you can) for the sake of easier maintenance. Side effects are necessary at times, but they can be very difficult to work with as they spread out.
Also, I'm not sure if this is the problem you are running into, but I can see why $emit might be a problem if you made a different event for each of your checkboxes/changed values. I think you can consolidate this into a single event like item-updated to prevent things from getting too unwieldy. For example, in the snippet below, I've used a shared method named updateItem in the input event listeners on each checkbox like this: #change="updateItem({ show: $event.target.checked })" and #change="updateItem({ active: $event.target.checked })". This way, there is just one $emit call (inside the updateItem method). Try running the snippet below - I think it should give you the results you were looking for:
Vue.config.devtools=false
Vue.config.productionTip = false
// Parent element
Vue.component('v-items', {
template: `
<div class="items">
<div class="items__list">
<v-item v-for="i in items" :key="i.id" :item="i" #item-updated="itemUpdated"/>
</div>
</div>
`,
data () {
const origItems = [
{ id: 1, name: 'test', show: false, active: true },
{ id: 2, name: 'test 2', show: false, active: true },
{ id: 3, name: 'test 3', show: true, active: true },
{ id: 4, name: 'test 4', show: false, active: true },
]
return {
origItems,
items: origItems.map(item => Object.assign({}, item)),
editedItems: [],
}
},
methods: {
itemUpdated(item) {
const origItem = this.origItems.find(o => o.id === item.id)
const indexInEdited = this.editedItems.findIndex(o => o.id === item.id)
const objectChanged = JSON.stringify(item) !== JSON.stringify(origItem)
if (indexInEdited !== -1) {
this.editedItems.splice(indexInEdited, 1)
}
if (objectChanged) {
this.editedItems.push(item)
}
// Show the editedItems list
console.clear()
console.log(this.editedItems)
}
},
})
// Child elements
Vue.component('v-item', {
props: ['item'],
template: `
<div class="item">
<div>{{ item.id }}</div>
<div>{{ item.name }}</div>
<input type="checkbox"
:checked="item.show"
#change="updateItem({ show: $event.target.checked })"
/>
<input type="checkbox"
:checked="item.active"
#change="updateItem({ active: $event.target.checked })"
/>
</div>
`,
methods: {
updateItem(update) {
// Avoid directly modifying this.item by creating a cloned object
this.$emit('item-updated', Object.assign({}, this.item, update))
}
},
})
new Vue({ el: "#app" })
console.clear()
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
min-height: 300px;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
}
.items {
display: grid;
grid-auto-flow: row;
gap: 10px;
}
.items__list {
display: grid;
grid-template-columns: auto 1fr auto auto;
gap: 5px;
}
.item {
display: contents;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<v-items></v-items>
</div>
Related
I am new to Svelte and I am struggling with the stores. What I am trying is to individually toggle the background color of an element in an each loop. I figured it out by simply giving each div block its own html id and then addressing it via getElementById().
My question is, is there a more elegant way to do it?
Like binding the individual css classes of each div-block to the store values? I tried thing like class:backgound={todo.done} but that only worked on the initial render. Once the value was updated, it didn't work.
import { writable } from 'svelte/store'
export const list = writable([
{
text: 'test1',
done: false,
id:1,
},
{
text: 'test2',
done: true,
id:2,
},
{
text: 'test3',
done: false,
id:3,
},
]);
There seems to be something I am missing when it comes to how and when the store is accessed. What I want to avoid is to store the props in a variable because I never know how many elements there will be in the store.
<script>
import {list} from './stores.js'
const toggleDone = (todo) => {
todo.done = !todo.done
let elem = document.getElementById(todo.id);
elem.style.backgroundColor = todo.done ? 'red' : '';
console.log($list)
}
let completedTodos = $list.filter((todo) => todo.completed).length
let totalTodos = $list.length
</script>
<style>
.list {
display: flex;
justify-content: center;
padding-right: 10px;
margin: 10px;
flex-wrap: wrap;
}
.active {
background-color: red;
}
</style>
<h2 id="">{completedTodos} out of {totalTodos} items completed</h2>
{#each $list as todo, index (todo.id)}
<div class="list" id="{todo.id}" >
<form action="">
<input type="checkbox" checked={todo.done} on:click={toggleDone(todo)}>
</form>
{todo.text} id: {todo.id}
</div>
{/each}
The problem is that you are using a function to toggle the item, or rather that the function is fully disconnected from the store: Svelte does not know that the item in the function is a part of the store, the function also could be called with other values.
The easiest fix is to toggle the item inline:
<div class="list" class:active={todo.done}>
<label>
<input type="checkbox" checked={todo.done}
on:click={() => todo.done = !todo.done}>
{todo.text} id: {todo.id}
</label>
</div>
Or rather, you could just use bind:
<input type="checkbox" bind:checked={todo.done}>
(Also, please use label elements like this, so it is more accessible. Clicking the label will also toggle the item.)
REPL
If the function is implemented in a way that it writes to the store, state will update correctly as well. For list this usually can be done most conveniently via the index, e.g.
function toggle(index) {
$list[index].done = !$list[index].done
}
<input type="checkbox" checked={todo.done}
on:click={() => toggle(index)}>
So I am trying to filter my table (element plus table) based on the tags that user clicks. (I am working in a Vue project)
I have written a method that outputs all the rows that contains the tag clicked by the user.
<el-table-column prop="Keyword" label="Keyword" width="200" ">
<template #default="scope" >
<el-tag v-for="(k,index) in (scope.row.Keyword.split(','))" :key="index" #click="handlekey(k)">{{k}} </el-tag>
</template>
</el-table-column>
handlekey(val){
return this.data.filter((item)=>{return item.Keyword.split(',').includes(val)} )
}
When user clicks on a tag the console output is
Now I am trying to filter the table, so when a user clicks on a tag the table only shows the rows which contain that tag.
I have tried to write this function is computed but that gives an error sating that $options.handlekey is not a function
I have also tried to change the change the original data by doing
this.data = this.data.filter((item)=>{return item.Keyword.split(',').includes(val)} ) at the end of handlekey method but that doesn't work either.
I'll be grateful if anyone could give me suggestions on how I can achieve this.
You need a computed (derived state) which returns all rows containing one of the currently active keywords (if any), or all rows if there are no active keywords.
Instead of feeding the rows into the table, you feed this computed.
What's special about computed is they get recalculated every time the state involved in computing them changes.
Computed never mutate state. Picture a computed as a lens through which you see the state, but you can't actually touch it. If you mutate the source data inside a computed, you will trigger another computing of itself, ending up in an endless loop.
In the example below, the computed renderedRows will recalculate when:
rows change
the (selected) keywords change
Here's the example:
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: () => ({
rows: [
{
name: '1-5',
keywords: 'one,two,three,four,five'
},
{
name: '6-10',
keywords: 'six,seven,eight,nine,ten'
},
{
name: '2n + 1',
keywords: 'one,three,five,seven,nine'
},
{
name: '2n',
keywords: 'two,four,six,eight,ten'
},
{
name: '3n + 1',
keywords: 'one,four,seven,ten'
},
{
name: '5n + 2',
keywords: 'two,seven'
}
],
keywords: []
}),
computed: {
renderedRows() {
return this.keywords.length
? this.rows.filter((row) =>
this.keywords.some((kw) => row.keywords.split(',').includes(kw))
)
: this.rows
},
allKeywords() {
return [
...new Set(this.rows.map((row) => row.keywords.split(',')).flat())
]
}
},
methods: {
toggleKeyword(k) {
this.keywords = this.keywords.includes(k)
? this.keywords.filter((kw) => kw !== k)
: [...this.keywords, k]
}
}
})
td {
padding: 3px 7px;
}
button {
cursor: pointer;
margin: 0 2px;
}
button.active {
background-color: #666;
color: white;
border-radius: 3px;
}
button.active:hover {
background-color: #444;
}
<script src="https://unpkg.com/vue#2"></script>
<div id="app">
<h4>Keywords:</h4>
<div>
<button
v-for="key in allKeywords"
:key="key"
v-text="key"
:class="{ active: keywords.includes(key) }"
#click="toggleKeyword(key)"
/>
</div>
<h4>Rows:</h4>
<table>
<thead>
<th>Name</th>
<th>Keywords</th>
</thead>
<tbody>
<tr v-for="row in renderedRows" :key="row.name">
<td v-text="row.name" />
<td>
<button
v-for="key in row.keywords.split(',')"
:key="key"
v-text="key"
:class="{ active: keywords.includes(key) }"
#click="toggleKeyword(key)"
/>
</td>
</tr>
</tbody>
</table>
</div>
Note: provide what you have in a runnable minimal reproducible example on codesandbox.io or similar and I'll modify it to include the principle outlined above.
Question
I want to toggle an event if the 'active' class gets added to an element. How can I achieve this?
In my opinion it could be somehow achieved with a watcher method but I don't know how to watch if a classname applies on an element.
I'm using vue3.
Edit
I have a carousel, where you can slide through some divs and the visible gets the class 'active'. I want to watch all divs, and if they get active call a function.
Here's an example of achieving this in a declarative way.
const { watch, ref } = Vue;
const CarouselItem = {
props: ['item'],
template: `<h1 :class="{ ...item }">{{ item.name }}</h1>`,
setup(props) {
watch(
props.item,
(item, prevItem) => item.active && console.log(`${item.name} made active!`),
);
}
};
Vue.createApp({
components: { CarouselItem },
template: '<CarouselItem v-for="item in items" :item="item" />',
setup() {
const items = ref([
{ name: 'Doril', active: false },
{ name: 'Daneo', active: false },
{ name: 'Mosan', active: false },
]);
// simulate a carousel item being made active
setTimeout(() => items.value[1].active = true, 1000);
return { items };
},
}).mount('#app');
.active {
color: red;
}
<script src="https://unpkg.com/vue#3.0.7/dist/vue.global.js"></script>
<div id="app"></div>
I have a data structure with nested objects that I want to bind to sub-components, and I'd like these components to edit the data structure directly so that I can save it all from one place. The structure is something like
job = {
id: 1,
uuid: 'a-unique-value',
content_blocks: [
{
id: 5,
uuid: 'some-unique-value',
block_type: 'text',
body: { en: { content: 'Hello' }, fr: { content: 'Bonjour' } }
},
{
id: 9,
uuid: 'some-other-unique-value',
block_type: 'text',
body: { en: { content: 'How are you?' }, fr: { content: 'Comment ça va?' } }
},
]
}
So, I instantiate my sub-components like this
<div v-for="block in job.content_blocks" :key="block.uuid">
<component :data="block" :is="contentTypeToComponentName(block.block_type)" />
</div>
(contentTypeToComponentName goes from text to TextContentBlock, which is the name of the component)
The TextContentBlock goes like this
export default {
props: {
data: {
type: Object,
required: true
}
},
created: function() {
if (!this.data.body) {
this.data.body = {
it: { content: "" },
en: { content: "" }
}
}
}
}
The created() function takes care of adding missing, block-specific data that are unknown to the component adding new content_blocks, for when I want to dynamically add blocks via a special button, which goes like this
addBlock: function(block_type) {
this.job.content_blocks = [...this.job.content_blocks, {
block_type: block_type,
uuid: magic_uuidv4_generator(),
order: this.job.content_blocks.length === 0 ? 1 : _.last(this.job.content_blocks).order + 1
}]
}
The template for TextContentBlock is
<b-tab v-for="l in ['fr', 'en']" :key="`${data.uuid}-${l}`">
<template slot="title">
{{ l.toUpperCase() }} <span class="missing" v-show="!data.body[l] || data.body[l] == ''">(missing)</span>
</template>
<b-form-textarea v-model="data.body[l].content" rows="6" />
<div class="small mt-3">
<code>{{ { block_type: data.block_type, uuid: data.uuid, order: data.order } }}</code>
</div>
</b-tab>
Now, when I load data from the API, I can correctly edit and save the content of these blocks -- which is weird considering that props are supposed to be immutable.
However, when I add new blocks, the textarea above wouldn't let me edit anything. I type stuff into it, and it just deletes it (or, I think, it replaces it with the "previous", or "initial" value). This does not happen when pulling content from the API (say, on page load).
Anyway, this led me to the discovery of immutability, I then created a local copy of the data prop like this
data: function() {
return {
block_data: this.data
}
}
and adjusted every data to be block_data but I get the same behaviour as before.
What exactly am I missing?
As the OP's comments, the root cause should be how to sync textarea value between child and parent component.
The issue the OP met should be caused by parent component always pass same value to the textarea inside the child component, that causes even type in something in the textarea, it still bind the same value which passed from parent component)
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So adjust your codes to like below demo:
for input, textarea HTMLElement, uses v-bind instead of v-model
then uses $emit to popup input event to parent component
In parent component, uses v-model to sync the latest value.
Vue.config.productionTip = false
Vue.component('child', {
template: `<div class="child">
<label>{{value.name}}</label><button #click="changeLabel()">Label +1</button>
<textarea :value="value.body" #input="changeInput($event)"></textarea>
</div>`,
props: ['value'],
methods: {
changeInput: function (ev) {
let newData = Object.assign({}, this.value)
newData.body = ev.target.value
this.$emit('input', newData) //emit whole prop=value object, you can only emit value.body or else based on your design.
// you can comment out `newData.body = ev.target.value`, then you will see the result will be same as the issue you met.
},
changeLabel: function () {
let newData = Object.assign({}, this.value)
newData.name += ' 1'
this.$emit('input', newData)
}
}
});
var vm = new Vue({
el: '#app',
data: () => ({
options: [
{id: 0, name: 'Apple', body: 'Puss in Boots'},
{id: 1, name: 'Banana', body: ''}
]
}),
})
.child {
border: 1px solid green;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<span> Current: {{options}}</span>
<hr>
<div v-for="(item, index) in options" :key="index">
<child v-model="options[index]"></child>
</div>
</div>
Well I hope to explain, I'm generating this data from a component, when I click the checkbox changes in the generated data are not reflected, but when clicking the button with a data already in the instance changes are made, I appreciate if you explain Why or do they have a solution?
this my code
js
Vue.component('fighters', {
template: '#fighters-template',
data() {
return {
items: [
{ name: 'Ryu' },
{ name: 'Ken' },
{ name: 'Akuma' }
],
msg: 'hi'
}
},
methods: {
newData() {
this.items.forEach(item => {
item.id = Math.random().toString(36).substr(2, 9);
item.game = 'Street Figther';
item.show = false;
});
this.items.push()
},
greeting() {
this.msg = 'hola'
}
}
});
new Vue({
el: '#app'
})
html
<main id="app">
<fighters></fighters>
</main>
<template id="fighters-template">
<div class="o-container--sm u-my1">
<ul>
<li v-for="item in items">
<input type="checkbox" v-model="item.show">
{{ item.name }}</li>
</ul>
<button class="c-button c-button--primary" #click="newData()">New Data</button>
<h2>{{ msg }}</h2>
<button class="c-button c-button--primary" #click="greeting()">Greeting</button>
<hr>
<pre>{{ items }}</pre>
</div>
</template>
this live code
https://jsfiddle.net/cqx12a00/1/
Thanks for you help
You don't declare the show variables that your checkboxes are bound to, so they are not reactive – Vue is not aware when one is updated.
It should be initialized like so:
items: [
{ name: 'Ryu', show: false },
{ name: 'Ken', show: false },
{ name: 'Akuma', show: false }
]
You might think that newData would fix it, since it assigns a show member to each item, but Vue cannot detect added properties so they're still not reactive. If you initialized the data as I show above, then the assignment would be reactive.
If you want to add a new reactive property to an object, you should use Vue.set.