I am trying my hands at Nuxt and having a problem that I can't seem to figure out.
The concept:
I am having two arrays, one called items and one called selectedItems. I loop through the array of items to showcase them, if I click them, I simply add them to the array of selectItems and if I click them again, then they are removed from the array. Now I also added a click variable which helps enable coloring in the text to showcase which are selected. I tried simplifying the example with array of [A,B,C] in the code snippet down below!
The problem:
Now in the page, you will already have existing items in the selectedItems like in my example code, whereas [B] already exists in the array. Now how would I on a pageload, set the objects in the array SelectItems, in this case B to have already been clicked? I tried doing a comparing of the items in array Item and array selectedItem via a computed and then a if statement on the component for a class. This worked sorta EXCEPT the element wasn't actually clicked, so you would have to click TWICE in order for the object to be removed in the array. So how do I make the objects from selectedItem be click=true?
Vue.component('Item', {
template: `
<div>
<div :class="{'clicked': clicked,}" #click="selectItem()">
{{item.name}}
</div>
</div>
`,
props: {
item: {
type: Object,
default: null,
},
},
data() {
return {
clicked: false,
}
},
methods: {
selectItem() {
this.clicked = !this.clicked
const clickedItem = this.item
const id = clickedItem.id
if (this.clicked === true) {
this.$emit('add-item', clickedItem)
} else {
this.$emit('remove-item', id)
}
},
},
})
new Vue({
el: '#demo',
data() {
return {
items: [{id: 1, name: 'a'}, {id: 2,name: 'b'}, {id: 3, name: 'c'}],
selectedItems: [{id: 2, name: 'b'}],
}
},
methods: {
addItemToArray(clickedItem){
this.selectedItems = [...this.selectedItems, clickedItem]
},
removeItemFromArray(id){
this.selectedItems = this.selectedItems.filter(
(clickedItem) => clickedItem.id !== id
)
},
}
})
.clicked {background-color: coral;}</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<template>
<div v-for="item in items" :key="item.id">
<Item :item="item" #add-item="addItemToArray"
#remove-item="removeItemFromArray"> </Item>
</div>
</template>
</div>
The code:
https://stackblitz.com/edit/nuxt-starter-cxakbv?file=pages/index.vue
You can try this way:
In index you pass selectedItems to Item component:
<Item :item="item" :selectedItems="selectedItems" #add-item="addItemToArray"
#remove-item="removeItemFromArray"> </Item>
In Item component add new property selectedItem:
selectedItems: {
type: Array,
default: null,
},
And in mounted hook you check is item in selectItems array:
mounted() {
if (this.selectedItems.some(item => item.id === this.item.id)) this.clicked = true
}
Vue.component('Item', {
template: `
<div>
<div :class="{'clicked': clicked,}" #click="selectItem()">
{{item.name}}
</div>
</div>
`,
props: {
item: {
type: Object,
default: null,
},
selecteditems: {
type: Array,
default: null,
},
},
data() {
return {
clicked: false,
}
},
methods: {
selectItem() {
this.clicked = !this.clicked
const clickedItem = this.item
const id = clickedItem.id
if (this.clicked === true) {
this.$emit('add-item', clickedItem)
} else {
this.$emit('remove-item', id)
}
},
},
mounted() {
if (this.selecteditems.some(item => item.id === this.item.id)) this.clicked = true
}
})
new Vue({
el: '#demo',
data() {
return {
items: [{id: 1, name: 'a'}, {id: 2,name: 'b'}, {id: 3, name: 'c'}],
selectedItems: [{id: 2, name: 'b'}],
}
},
methods: {
addItemToArray(clickedItem){
this.selectedItems = [...this.selectedItems, clickedItem]
},
removeItemFromArray(id){
this.selectedItems = this.selectedItems.filter(
(clickedItem) => clickedItem.id !== id
)
},
}
})
.clicked {background-color: coral;}</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<template>
<div v-for="item in items" :key="item.id">
<Item :item="item" :selectedItems="selectedItems" #add-item="addItemToArray"
#remove-item="removeItemFromArray"> </Item>
</div>
</template>
</div>
Related
I am new to Vue.js and would like to understand how v-model works with a checkbox.
I am working with vuetify checkboxes.
My components gets an object as value prop and I would like to display checkboxes according to each key value pair that would look like that
this.value = {property1: true, property2: false}
So here i want to display a checkbox with lable property1 and the checkbox should be checked as the value is true. The second checkbox would be unchecked.
When checking a checkbox I want to send an object with the key and the value in order to save it. I am only able to get the value for now, but what can I do with it ?
If I try to get the key and the value, the issue is that when i uncheck it sends null instead of the key, value pair, how should i manage this ?
<template>
<div class="d-flex flex-wrap justify-space-between">
<div class="d-flex flex-wrap flex-column">
<v-checkbox
class="add-checkbox"
ref="additionalDetails"
v-for="(value, key) in additionalDetails"
type="checkbox"
:key="key"
:value="{key, value}"
v-model="additionalDetails"
:label="key"
><template v-slot:label
><span class="body-2"
>{{
key
}}
</span>
</template></v-checkbox
>
</div>
</div>
</template>
<script>
export default {
name: "additional-details",
props: {
value: Object,
},
components: {},
data: function () {
return {
newAdditionalDetails: [],
};
},
computed: {
additionalDetails: {
get: function () {
return this.value;
},
set: function ({ key, value}) {
let newObject = { ...this.value };
newObject[key] = value;
this.newAdditionalDetails = newObject
},
},
},
methods: {},
beforeMount() {},
};
</script>
v-model should be same type of value you want to track, in this case an object, but actually because of the v-for an array of objects, initialized with the values you expect the checkboxes to hold/emit. The v-checkbox component will also need to be explicitely told what object key-value equals 'truthy' vs 'falsy'. Another tip: since you're also looping over object properties, a third alias in the v-for will give you the index. Something like this will work:
<template>
<div>
<v-checkbox
v-for="(value, key, index) in initValues" :key=index
:label=key
v-model=checkboxes[index]
:value="{ key: key, value: value }"
:false-value="{ key: key, value: false }"
:true-value="{ key: key, value: true }"
#change="log(checkboxes[index])"
></v-checkbox>
</div>
</template>
<script>
export default {
data: () => ({
initValues: {
'hi': true,
'bye': false,
'ok': true
},
checkboxes: []
}),
methods: {
log(newValue) {
console.log(newValue)
}
},
created() {
// initialize array of objects where each object is a key-value pair from initValues
this.checkboxes = Object.entries(this.initValues).map(( [k, v] ) => ({ [k]: v }))
}
};
</script>
It's a bit tricky, and is honestly not how I would've done it. A simpler solution would be to track just the value and create/send the key-value pair as the checkbox input changes, e.g.
<template>
<div>
<v-checkbox
v-for="(value, key, index) in initValues" :key=index
:label="key"
v-model=checkboxes[index]
#change="log(key, checkboxes[index])"
></v-checkbox>
</div>
</template>
<script>
export default {
data: () => ({
initValues: {
'hi': true,
'bye': false,
'ok': true
},
checkboxes: []
}),
methods: {
log(key, newValue) {
console.log({ [key]: newValue })
}
},
created() {
this.checkboxes = Object.values(this.initValues)
}
};
</script>
Instead of writing this much complex logic, You can simply achieve that by binding object property in v-model as per the additionalDetails.
Live Demo :
new Vue({
el: '#app',
vuetify: new Vuetify(),
data () {
return {
additionalDetails: [{
property1: true
}, {
property2: false
}]
}
},
methods: {
getChecboxValues() {
console.log('additionalDetails', JSON.stringify(this.additionalDetails))
}
}
})
<script src="https://unpkg.com/vue#2.x/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify#2.6.8/dist/vuetify.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/vuetify#2.6.8/dist/vuetify.min.css"/>
<link rel="stylesheet" href="https://unpkg.com/#mdi/font#6.x/css/materialdesignicons.min.css"/>
<div id="app">
<v-app id="inspire">
<v-container
class="px-0"
fluid
>
<v-checkbox
v-for="(value, key) in additionalDetails"
:key="key"
:label="Object.keys(value)[0]"
v-model="value[Object.keys(value)[0]]"
></v-checkbox>
<v-btn depressed color="primary" #click="getChecboxValues">
Submit
</v-btn>
</v-container>
</v-app>
</div>
Finally what I did I converted to an array the initail object (that comes in stringified by the way here) and added the checked prop to update in the v-model. I also had to add a method to set the new object (The example code below is a bit different from the original one sorry for that) Somehow the setter of the computed additionalDetails never triggers I have no clue why, if someone has, thanks in advance for sharing toughts.
<template>
<div class="d-flex flex-wrap justify-space-between">
<div class="d-flex flex-wrap flex-column">
<v-checkbox class="add-checkbox" ref="additionalDetails" v-for="(prop, index) in additionalDetails" type="checkbox" :label="prop.label" :key="index" :dataIndex="index" v-model="prop.checked" #change="setAdditionalDetails(prop)"><template v-slot:label><span class="body-2">{{ prop.label }} </span>
</template></v-checkbox>
</div>
</div>
</template>
<script>
export default {
name: "additional-details",
props: {
value: Object,
},
computed: {
additionalDetails: {
get: function() {
if (this.value.JsonAdditionalFields !== null) {
let res = [];
let parsedJsonAdditionalFields = JSON.parse(
this.value.JsonAdditionalFields
);
for (const [key, value] of Object.entries(
parsedJsonAdditionalFields
)) {
res = [
...res,
{
[key]: value,
checked: value,
label: "label"
},
];
}
return res;
} else {
return [];
}
},
},
},
methods: {
setAdditionalDetails(val) {
let newObject = { ...this.value
};
newObject.JsonAdditionalFields = JSON.parse(
newObject.JsonAdditionalFields
);
newObject.JsonAdditionalFields[Object.keys(val)[0]] = val.checked;
this.value.JsonAdditionalFields = JSON.stringify(
newObject.JsonAdditionalFields
);
},
},
watch: {
additionalDetails: {
handler(newVal) {
console.log("additionalDetails new Value:", newVal);
},
deep: true,
},
},
};
</script>
I have a v-for to display all my categories. I would like to exclude a category, this category has an id = 1. So basically how can I remove by Id ? or maybe how can I exclude the first element of the array ? what do I do ?
<div class="list" v-for="(category,index ) in Categories" :key="category.id">
Use computed property component option.
computed: {
filteredCategories: function(){
//example ids to ignore
var ignore = [1, 10];
return this.categories.filter(cat => ignore.indexOf(cat.id) === -1);
}
}
<div class="list" v-for="(category,index ) in filteredCategories" :key="category.id">
You can use v-if.
<div class="list" v-for="(category,index ) in Categories" :key="category.id">
<template v-if="category.id === 1">
...
To get all categories with an id greater than 1, the cleanest way would be to have a filteredCategories computed:
<div class="list" v-for="category in filteredCategories" :key="category.id" />
computed: {
filteredCategories: {
return this.Categories.filter(cat => cat.id > 1);
}
}
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: () => ({
Categories: [
{id: 1, name: 'I won\'t get rendered!'},
{id: 2, name: 'Second category'},
{id: 3, name: 'Third category'},
{id: 4, name: 'Fourth category'},
{id: 0, name: 'I won\'t get rendered, either. My id is `0`'}
]
}),
computed: {
filteredCategories() {
return this.Categories.filter(cat => cat.id > 1);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<div id="app">
<div class="list" v-for="category in filteredCategories" :key="category.id">
{{category.id}} - {{ category.name }}
</div>
</div>
The advantages are:
the initial Categories array is not mutated, so if you use it anywhere else in the component it will still contain all categories (which is not true if you filter using .splice)
the filtering is done based on the value of id, not on the category position in the array.
Here's how to get the Categories from the api:
data: () => ({
Categories: []
}),
computed: {
filteredCategories() {
return this.Categories.filter(cat => cat.id > 1);
}
},
mounted() {
axios.get("path/to/your/api")
.then(response => this.Categories = response.data);
}
Currently I use Vuetify for base components and would like to create reusable extensions. For example a list containing checkboxes, a datatable column with some functionality etc.
For this question I will take the list containing checkboxes example. I created the following component called CheckboxGroup.vue
<template>
<v-container>
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item.state"
:label="item.title"
></v-checkbox>
</v-container>
</template>
<script>
export default {
props: {
items: Array,
required: true
}
};
</script>
This component takes an array of objects as a property and creates a checkbox for each entry.
Important parts are v-model="item.state" and :label="item.title". Most of the time the state attribute will have a different name, same for the title attribute.
For testing purposes I created a view file called Home.vue holding an array of documents.
<template>
<v-container>
<CheckboxGroup :items="documents"/>
<v-btn #click="saveSettings">Save</v-btn>
</v-container>
</template>
<script>
import CheckboxGroup from "../components/CheckboxGroup";
export default {
components: {
CheckboxGroup
},
data: function() {
return {
documents: [
{
id: 1,
name: "Doc 1",
deleted: false
},
{
id: 2,
name: "Doc 2",
deleted: false
},
{
id: 3,
name: "Doc 3",
deleted: true
}
]
};
},
methods: {
saveSettings: function() {
console.log(this.documents);
}
}
};
</script>
This time title is called name and state is called deleted. Obviously CheckboxGroup is not able to manage the documents because the attribute names are wrong.
How would you solve this problem? Would you create a computed property and rename these attributes? Would be a bad idea I think...
And by the way, is using v-model a good idea? A different solution would be to listen to the changed event of a checkbox and emit an event with the item index. Then you would have to listen for the change in the parent component.
I don't think there is a way to create something like
<CheckboxGroup :items="documents" titleAttribute="name" stateAttribute="deleted"/>
because it would be bad design anyway. I hope that this is a very trivial problem and every Vue developer has been confronted with it, since the primary goal should always be to develop abstract components that can be reused multiple times.
Please keep in mind that this checkbox problem is just an example. A solution for this problem would also solve same or similar problems :)
If I understood what you wanted, it`s not so trivial. Using props is a good idea. You dont need to manage the documents attribute names, just set the attribute name to your component.
Note
Renaming the attributes or using proxies is more resource-intensive like this solution, because you need to run loop to rename the attribute names or apply aliases to data array objects.
Example
CheckboxGroup.vue
<template>
<v-container fluid>
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item[itemModel]"
:label="item[itemValue]"
></v-checkbox>
<hr>
{{items}}
</v-container>
</template>
<script>
export default {
name: "CheckboxGroup",
props: {
items: {
type: Array,
required:true
},
itemValue:{
type:String,
default: 'title',
// validate props if you need
//validator: function (value) {
// return ['title', 'name'].indexOf(value) !== -1
// }
// or make required
},
itemModel:{
type:String,
default: 'state',
// validate props if you need
//validator: function (value) {
// validate props if you need
// return ['state', 'deleted'].indexOf(value) !== -1
// }
// or make required
}
}
};
</script>
Home.vue
<template>
<div id="app">
<checkbox-group :items="documents"
item-value="name"
item-model="deleted"
>
</checkbox-group>
</div>
</template>
<script>
import CheckboxGroup from "./CheckboxGroup.vue";
export default {
name: "App",
components: {
// HelloWorld,
CheckboxGroup
},
data: function() {
return {
documents: [
{
id: 1,
name: "Doc 1",
deleted: false
},
{
id: 2,
name: "Doc 2",
deleted: false
},
{
id: 3,
name: "Doc 3",
deleted: true
}
]
}
}
};
</script>
Based on your example I`v tried to show how to create component to managing object attributes in child component. If you need more information, please let me know.
Some good answers here that definitely solve your issue - you are essentially wanting to pass data down to a child (which isn't bad design - you were on the right track!)..
I am kind of shocked that slots or scoped-slots haven't been mentioned yet... so I figured I would chime in..
Scoped-slots allow you to take advantage of data you are passing to a child - but within the parent. The child essentially "reflects" data back to the parent, which allows you to style the child component/slot however you wish, from the parent.
This is different than just passing data via a prop attribute, because you would have to rely on styling within the child - you couldn't change the styles on a 'per-use' basis. The styles you set in the child would be "hard coded"..
In this example I am riding on top of the already provided label slot that Vuetify provides - just passing my own custom scoped-slot to it.. How to find documentation on v-checkbox slots
I made some minor changes to help spice some things up, and to show how you have greater control over styles this way (and you can use any object prop for the label you want .name, .whatever, .label, etc..)
Lastly, it is important to note that Vuetify already provides a "grouped checkbox" component - v-radio-group - I know it's called "radio-group" but it supports checkboxes...
Edit: fixed the state "issue"...
Scoped Slots With Render Function - Original Answer Moved To Bottom
Thanks to #Estradiaz for collaborating on this with me!
Vue.component('checkboxgroup', {
props: {
items: { type: Array, required: true }
},
render (h) {
return h('v-container', this.items.map((item) => {
return this.$scopedSlots.checkbox({ item });
}));
},
})
new Vue({
el: "#app",
data: {
documents: [{
id: 1,
name: "Doc 1 - delete",
deleted: false,
icon: "anchor",
},
{
id: 12,
title: "Doc 1 - state",
state: false,
icon: "anchor",
},
{
id: 2,
name: "Doc 2 - delete",
deleted: false,
icon: "mouse"
},
{
id: 3,
name: "Doc 3 - delete",
deleted: true,
icon: "watch"
}
]
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script src="https://unpkg.com/vuetify/dist/vuetify.min.js"></script>
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet" type="text/css">
<link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet" type="text/css"></link>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://use.fontawesome.com/releases/v5.0.8/css/all.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/2.1.99/css/materialdesignicons.min.css" rel="stylesheet" />
<div id="app">
<v-app>
<v-container>
<CheckboxGroup :items="documents">
<template #checkbox={item}>
<v-checkbox
v-model="item[(item.name && 'deleted') || (item.title && 'state') ]" color="red">
<template #label>
<v-icon>mdi-{{item.icon}}</v-icon>
{{ item.name || item.title }}
{{ item }}
</template>
</v-checkbox>
</template>
</CheckboxGroup>
</v-container>
</v-app>
</div>
My attempt of a json to component parser
usefull names are welcome
so basically you can target element tagnames as slot #[slotname] or put slot names and target entries to overwrite the default component.
omiting tag property in the component will append children to the parent vnode
Consider:
[
{
ElementTag: 'Liste',
id: 1,
tag: 'p',
items: [
{
ElementTag: 'input',
id: 11,
type: 'checkbox',
title: "Sub Doc 1 - state",
state: true,
slotName: "slotvariant"
},
{
ElementTag: 'input',
id: 12,
type: 'date',
title: "Sub Doc 2 - Date",
date: "",
}
]
},
{
ElementTag: 'input',
id: 2,
type: 'checkbox',
title: "Doc 2 - deleted",
deleted: true,
slotName: 'deleted'
}
]
Example :
Vue.component('Liste', {
props:["tag", "items"],
render(h){
console.log(this.items)
let tag = this.tag || (this.$parent.$vnode && this.$parent.$vnode.tag)
if(tag === undefined) throw Error(`tag property ${tag} is invalid. Scope within valid vnode tag or pass valid component/ html tag as property`)
return h(tag, this.items.map(item => {
const {ElementTag, slotName, ...attrs} = item;
return (
this.$scopedSlots[slotName || ElementTag]
&& this.$scopedSlots[slotName || ElementTag]({item})
)
|| h(ElementTag, {
attrs: attrs,
scopedSlots: this.$scopedSlots
})
}))
}
})
new Vue({
data(){
return {
items: [
{
ElementTag: 'Liste',
id: 1,
tag: 'p',
items: [
{
ElementTag: 'input',
id: 11,
type: 'checkbox',
text: "Sub Doc 1 - state",
state: true,
slotName: "slotvariant"
},
{
ElementTag: 'input',
id: 12,
type: 'date',
title: "Sub Doc 2 - Date",
date: "",
}
]
},
{
ElementTag: 'input',
id: 2,
type: 'checkbox',
title: "Doc 2 - deleted",
deleted: true,
slotName: 'deleted'
}
]}
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<Liste tag="p" :items="items">
<template #input="{item}">
<label :for="item.id">
{{ item.title }}
</label>
<input :type="item.type" :id="item.id" v-model="item.date"/>
</template>
<template #slotvariant="{item}">
slotvariant - {{item.text}}<br>
</template>
<template #deleted="{item}">
<label :for="item.id">
{{ item.title }}
</label>
<input :type="item.type" :id="item.id" v-model="item.deleted"/>
</template>
</Liste>
</div>
Typescript:
import {Vue, Component, Prop} from 'vue-property-decorator'
export type AbstractElement = {
[key: string]: any // passed as $attrs | useable for assigned $props
ElementTag: string
slotName?: string
}
#Component<List>({
render(h){
let tag = this.tag
|| (this.$parent.$vnode && this.$parent.$vnode.tag)
|| (this.$parent.$el && this.$parent.$el.tagName)
if(tag === undefined) throw Error(`tag prperty: ${tag} is invalid. Scope within valid vnode tag or pass valid component/ html tag as property`)
return h(tag, this.items.map(item => {
const {ElementTag, slotName, ...attrs} = item;
console.log("slotName", slotName)
return (this.$scopedSlots[slotName || ElementTag]
&& this.$scopedSlots[slotName || ElementTag]({item}))
|| h(ElementTag, {
attrs: attrs,
slot: slotName || ElementTag,
scopedSlots: this.$scopedSlots
})
}))
}
})
export default class List extends Vue{
#Prop(String) readonly tag?: string
#Prop(Array) readonly items!: Array<AbstractElement>
}
will raise this here
You can use a Proxy to map the document property names during access.
Note
In my original answer, I used Proxy handlers for get and set, which is sufficient for plain javascript objects, but fails when used with Vue data properties because of the observer wrappers that Vue applies.
By also trapping has in the Proxy, this can be overcome. I left the original answer below for anyone interested in this problem.
Here is a demo of how to use Proxy to 'alias' Vue reactive properties to different names
without affecting the original data structure
without having to copy the data
console.clear()
Vue.config.productionTip = false
Vue.config.devtools = false
Vue.component('checkboxgroup', {
template: '#checkboxGroup',
props: { items: Array, required: true },
});
const aliasProps = (obj, aliasMap) => {
const handler = {
has(target, key) {
if (key in aliasMap) {
return true; // prevent Vue adding aliased props
}
return key in target;
},
get(target, prop, receiver) {
const propToGet = aliasMap[prop] || prop;
return Reflect.get(target, propToGet);
},
set(target, prop, value, receiver) {
const propToSet = aliasMap[prop] || prop;
return Reflect.set(target, propToSet, value)
}
};
return new Proxy(obj, handler);
}
new Vue({
el: '#app',
data: {
documents: [
{ id: 1, name: "Doc 1", deleted: false },
{ id: 2, name: "Doc 2", deleted: false },
{ id: 3, name: "Doc 3", deleted: true },
]
},
computed: {
checkBoxItems() {
const aliases = {
title: 'name',
state: 'deleted'
}
return this.documents.map(doc => aliasProps(doc, aliases));
}
},
methods: {
saveSettings: function() {
console.log(this.documents);
}
},
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify/dist/vuetify.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons" rel="stylesheet"/>
<link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet"/>
<div id="app">
<v-app id="theapp">
<v-container>
<checkboxgroup :items="checkBoxItems"></checkboxgroup>
<v-btn color="info"
#click="saveSettings">Save</v-btn>
</v-container>
</v-app>
</div>
<template id="checkboxGroup">
<v-container style="display: flex">
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item.state"
:label="item.title"
></v-checkbox>
</v-container>
</template>
Original answer
You can use a Proxy to map the document property names during access.
<template>
...
<CheckboxGroup :items="checkBoxItems"/>
...
</template>
<script>
export default {
...
computed: {
checkBoxItems() {
const handler = {
get: function(target, prop) {
return prop === 'title' ? target.name :
prop === 'state' ? target.deleted :
target[prop];
},
set(obj, prop, value) {
const propToSet =
prop === 'title' ? 'name' :
prop === 'state' ? 'deleted' :
prop;
obj[propToSet] = value;
}
};
return documents.map(doc => new Proxy(doc, handler))
},
},
...
}
</script>
Demo
const documents = [
{ id: 1, name: "Doc 1", deleted: false },
{ id: 2, name: "Doc 2", deleted: false },
{ id: 3, name: "Doc 3", deleted: true },
]
const handler = {
get: function(target, prop) {
return prop === 'title' ? target.name :
prop === 'state' ? target.deleted :
target[prop];
},
set(obj, prop, value) {
const propToSet =
prop === 'title' ? 'name' :
prop === 'state' ? 'deleted' :
prop;
obj[propToSet] = value;
}
};
const checkItems = documents.map(doc => new Proxy(doc, handler))
console.log('Accessing new property names via checkItems')
checkItems.forEach(ci => console.log(ci.id, ci.title, ci.state))
console.log('After update, values of documents')
checkItems.forEach(ci => ci.state = !ci.state )
documents.forEach(doc => console.log(doc.id, doc.name, doc.deleted))
I am working on an click and drop feature --- where on the page the module is used in a recursive way so it has a parent and children.
I have hit an issue where if the user started to select the children - and then selects the parent - I want to deselect the children. Although I am unsure how to store or monitor a change in the parent/child items selected to make global deselection.
So the user has selected the child of bacon3.. if they select the parent - it would need to deselect the children -- but I feel I am currently locked in the scope of the module
I think this example will help you https://canary.ember-twiddle.com/468a737efbbf447966dd83ac734f62ad?openFiles=utils.tree-helpers.js%2C
So, this was an interesting problem. It turned out to be more of a recursion problem than anything having to do with ember, javascript, or checkbox behavior.
Here is what I have (using the updated syntax and such (if you have the option to upgrade to 3.4, you most definitely should -- it's a dream))
// wrapping-component.js
import Component from '#ember/component';
import { action, computed } from '#ember-decorators/object';
import { check } from 'twiddle/utils/tree-helpers';
export default class extends Component {
options = [{
id: 1,
label: 'burger',
checked: false,
children: [{
id: 3,
label: 'tomato',
checked: false
}, {
id: 4,
label: 'lettus',
checked: false
}, {
id: 5,
label: 'pickle',
checked: false
}]
}, {
id: 2,
label: 'kebab',
checked: false,
children: [{
id: 6,
label: 'ketchup',
checked: false
}, {
id: 7,
label: 'chilli',
checked: false
}]
}];
#action
toggleChecked(id) {
const newTree = check(this.options, id);
this.set('options', newTree);
}
}
template:
{{yield this.options (action this.toggleChecked)}}
and the usage:
// application.hbs
<WrappingComponent as |options toggle|>
{{#each options as |item|}}
<CheckboxGroup #item={{item}} #onClick={{toggle}} />
{{/each}}
</WrappingComponent>
CheckboxGroup is a template-only component:
// checkbox-group.hbs
<div class="checkboxhandler">
<input
type="checkbox"
checked={{#item.checked}}
onclick={{action #onClick #item.id}}
>
<label>{{#item.label}}</label>
{{#if #item.children}}
{{#each #item.children as |child|}}
<CheckboxGroup #item={{child}} #onClick={{#onClick}} />
{{/each}}
{{/if}}
</div>
and the recursive helpers (this is a mess, but I've just been prototyping):
// utils/tree-helpers.js
const toggle = value => !value;
const disable = () => false;
// the roots / siblings are contained by arrays
export function check(tree, id, transform = toggle) {
if (tree === undefined) return undefined;
if (Array.isArray(tree)) {
return selectOnlySubtree(tree, id, transform);
}
if (tree.id === id || id === 'all') {
return checkNode(tree, id, transform);
}
if (tree.children) {
return checkChildren(tree, id, transform);
}
return tree;
}
function selectOnlySubtree(tree, id, transform) {
return tree.map(subTree => {
const newTree = check(subTree, id, transform);
if (!newTree.children || (transform !== disable && didChange(newTree, subTree))) {
return newTree;
}
return disableTree(subTree);
});
}
function isTargetAtThisLevel(tree, id) {
return tree.map(t => t.id).includes(id);
}
function checkNode(tree, id, transform) {
return {
...tree,
checked: transform(tree.checked),
children: disableTree(tree.children)
};
}
function disableTree(tree) {
return check(tree, 'all', disable);
}
function checkChildren(tree, id, transform) {
return {
...tree,
checked: id === 'all' ? transform(tree.checked) : tree.checked,
children: check(tree.children, id, transform)
};
}
export function didChange(treeA, treeB) {
const rootsChanged = treeA.checked !== treeB.checked;
if (rootsChanged) return true;
if (treeA.children && treeB.children) {
const compares = treeA.children.map((childA, index) => {
return didChange(childA, treeB.children[index]);
});
const nothingChanged = compares.every(v => v === false);
return !nothingChanged;
}
return false;
}
hope this helps.
I am creating a Vue component, which should refresh restaurants depending on user dynamically selected filters.
Therefor I have to update the filteredRestaurants in the data() function of my Vue component.
However, at first, when the Vue component is rendered, it takes the restaurant information from the "restaurants" prop.
I have tried to insert the "restaurants" into the filteredRestaurants data attribute to set it as a default value. Unfortunatelly then the stores wouldnt show at tall, as if the "restaurants" prop is inserted after the filteredRestaurants is assigned its value.
My question is, how can i get the "restaurants" prop into filteredRestaurants so that I can later on, re-render the Vue component when the user changes the filters.
<template lang="html">
<div class="test">
<Filters></Filters>
<div>
<ul class="o-list c-stores">
<Result v-bind:total="restaurants.length" v-bind:open="isOpen" v-on:toggle="toggleRestaurantList"></Result>
<li v-for="(restaurant, index) in restaurants" class="c-stores__location" :class="{'first': isFirst(index), 'last': isLast(index, restaurants)}">
<Location :index="index" :store="restaurant" :link="() => setCurrentRestaurant(restaurant)"></Location>
</li>
</ul>
</div>
</div>
</template>
<script>
import eventHub from './../../event-hubs/storefinder'
import Location from './Location'
import Filters from './Filters'
import Result from './Result'
export default {
props: ["restaurants", "isOpen", "currentSearch"],
data() {
return {
attributes : [],
// Here I am assigning the prop
filteredRestaurants : this.restaurants
}
},
head: {
title: function () {
return {
inner: this.$t('storefinder.overview')
}
},
meta: function functionName() {
return [{
name: 'og:title',
content: this.$t('storefinder.overview') + ' - ' + this.$t('storefinder.name'),
id: "og-title"
},
{
name: 'description',
content: this.$t('storefinder.description'),
id: "meta-description"
},
{
name: 'og:description',
content: this.$t('storefinder.description'),
id: "og-description"
},
]
}
},
components: {
Location,
Filters,
Result
},
methods: {
toggleRestaurantList() {
eventHub.$emit('showRestaurantList');
},
setCurrentRestaurant(restaurant) {
this.trackRestaurantSelect(restaurant.publicNameSlug);
this.$router.push({
name: "store",
params: {
restaurant: restaurant.publicNameSlug
}
});
},
trackRestaurantSelect(restaurantName) {
dataLayer.push({
'event': 'GAEvent',
'eventCategory': 'restaurants',
'eventAction': 'clickResult',
'eventLabel': restaurantName,
'eventValue': undefined,
'searchTerm': this.currentSearch && this.currentSearch.toLowerCase(),
'amountSearchResults': 1
});
},
created() {
eventHub.$on('addFilterTheRestaurants', (attribute) => this.attributes.push(attribute));
eventHub.$on('removeFilterTheRestaurants', (attribute) => this.attributes = this.attributes.filter(item => item !== attribute));
},
isLast: function (idx, list) {
return idx === list.length - 1;
},
isFirst: function (idx) {
return idx === 0;
},
}
}
</script>
The only way this worked, was when I had the filteredRestaurants as a function which returned "restaurants", and I called it inside the Vue template:
filteredRestaurants(){
return this.restaurants
}
Any help appreciated.
If I understand your question correctly, you are looking for a computed property:
computed: {
filteredRestaurants() {
return this.restaurants;
}
}
This will update whenever the value of this.restaurants changes.