Ember - nested recursive modules - javascript

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.

Related

Vue: existing array, add click effect and set true?

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>

LitElement maintain internal state

I am trying to build a chart with LitElement. The chart takes a data property from the user, and displays this data (the chart plot). It also gets series names from the data, in order to display a legend with a checkbox for each series that can be used to show or hide the data for that series on the chart plot.
The below is a very minimal example where the chart plot is simply divs containing the data points (3, 5, 4, 7), and the legend is just checkboxes. The expected behaviour is that when a checkbox is selected/deselected, the corresponding data in the chart plot (data divs) is shown/hidden. For example, initially both checkboxes are selected by default, and the data for both series is correctly display. However, if I deselect the first checkbox, I expect the data for "series1" to be hidden, so only 5 and 7 are displayed.
It is this checkbox behaviour that I cannot get working. When I select or deselect a checkbox, I log this.series which seems to be correctly updated reflect which checkboxes are selected, however the chart plot (data divs) is not updated.
import { LitElement, css, html } from "lit-element";
import { render } from "lit-html";
class TestElement extends LitElement {
static get properties() {
return {
data: { type: Array },
series: { type: Array },
};
}
constructor() {
super();
this.data = [];
this.series = [];
}
checkboxChange(e) {
const inputs = Array.from(this.shadowRoot.querySelectorAll("input")).map(n => n.checked);
this.series = this.series.map((s, i) => ({ ...s, checked: inputs[i] }));
console.log("this.series", this.series);
}
render() {
this.series = Object.keys(this.data[0]).map(key => ({ key, checked: true }));
const data = this.data.map(d => this.series.map(s => (s.checked ? html`<div>${d[s.key]}</div>` : "")));
const series = this.series.map(
s => html`<input type="checkbox" ?checked=${s.checked} #change=${this.checkboxChange} />`
);
return html`${data}${series}`;
}
}
customElements.define("test-element", TestElement);
render(
html`<test-element
.data=${[
{ series1: "3", series2: "5" },
{ series1: "4", series2: "7" },
]}
></test-element>`,
window.document.body
);
Try the following:
import { LitElement, html } from 'lit-element';
class TestElement extends LitElement {
static get properties() {
return {
data: { attribute: false, accessors: false },
series: { attribute: false, accessors: false },
checked: { attribute: false, accessors: false },
};
}
constructor() {
super();
this.data = [];
this.series = new Map();
this.checked = new Map();
}
get data() {
return this.__data || [];
}
set data(v) {
const oldValue = this.__data;
this.__data = Array.isArray(v) ? v : [];
this.series = new Map();
for (const row of this.data) {
for (const [series, value] of Object.entries(row)) {
this.series.set(series, [...this.series.get(series) || [], value])
}
}
for (const series of this.series.keys()) {
this.checked.set(series, this.checked.get(series) ?? true);
}
this.requestUpdate('data', oldValue);
this.requestUpdate('series', null);
this.requestUpdate('checked', null);
}
checkboxChange(e) {
this.checked.set(e.target.dataset.series, e.target.checked);
this.requestUpdate('checked', null);
}
render() {
return [
[...this.series.entries()].map(([series, values]) => values.map(value => html`
<div ?hidden="${!this.checked.get(series)}">${value}</div>
`)),
[...this.checked.entries()].map(([series, checked]) => html`
<input type="checkbox" ?checked=${checked} data-series="${series}" #change=${this.checkboxChange} />
`)
];
}
}
customElements.define("test-element", TestElement);
Live Example: https://webcomponents.dev/edit/FEbG9UA3nBMqtk9fwQrD/src/index.js
This solution presents a few improvements:
cache the series and checked state when data updates, instead of on each render
use hidden attr to hide unchecked series
use data-attributes to pass serializable data on collection items to event listeners.
use attribute: false instead of type: Array (assuming you don't need to deserialize data from attributes.

creating abstract components that can manage external 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))

Cloning a element and setting state

I'm trying to make a clone button in React, I have a state with an array that has 2 items in it. The button will send the index of the element selected, in this case let's say index 0. :) I can't get the following code to work
elements = [
{ item: 'something1', another: 'something2' },
{ item: 'something1', another: 'something2' }
];
setState( {
elements: [
...elements.slice( 0, index ),
{
...elements[ index ],
item: 'something'
},
...elements.slice( index + 1 )
]
} )
I know I'm doing something wrong, but...
Use index + 1 in the 1st slice call because you want to get all items up to and including the item you clone (slice stops before the end index), insert the clone, and add all other items after it:
const elements = [
{ item: 'something1', another: 'something1' },
{ item: 'something2', another: 'something2' },
{ item: 'something3', another: 'something3' }
];
const index = 1;
const newElements = [
...elements.slice(0, index + 1),
{
...elements[index],
item: 'new something !!!'
},
...elements.slice(index + 1)
];
console.log(newElements);
Ori Drori's quite correct about why the cloning isn't working. But there are two other issues with the code you'll want to address:
When setting state based on existing state, you must use the callback version of setState since state updates can be stacked and are asynchronous. So not:
this.setState({
elements: [/*...*/]
});
but instead:
this.setState(prevState => {
return {
elements: [/*...*/]
};
});
Partially because of #1 above, using the index is unreliable; other changes may have altered the array (your element may not even be in it anymore). Use the element itself. For instance, if your click handler is cloneClick, you'd use onClick={this.cloneClick.bind(this, el)} on the button to pass the element to the handler.
Here's a complete example:
class Example extends React.Component {
constructor(...args) {
super(...args);
this.state = {
elements: [
{ item: 'item1', another: 'something1' },
{ item: 'item2', another: 'something2' }
]
};
}
cloneClick(el) {
this.setState(prevState => {
const index = prevState.elements.indexOf(el);
if (index === -1) {
return null;
}
return {
elements: [
...prevState.elements.slice(0, index + 1),
{
...el,
item: el.item + " (copy)"
},
...prevState.elements.slice(index + 1)
]
};
// Alternately, this is more efficient:
/*
let clone = null;
const elements = [];
prevState.elements.forEach(prevEl => {
elements[elements.length] = prevEl;
if (prevEl === el) {
elements[elements.length] = clone = {
...el,
item: el.item + " (copy)"
};
}
});
return clone ? {elements} : null; // No update if the el wasn't found
*/
});
}
render() {
return <div>
{this.state.elements.map((el, i) =>
<div key={i}>
{el.item}
-
{el.another}
<input type="button" value="Clone" onClick={this.cloneClick.bind(this, el)} />
</div>
)}
</div>;
}
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

Props not being assigned to data() attribute in Vue

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.

Categories

Resources