I have an object as below. Now when I change a property and call requestRender it won't trigger. As far as I see it, the property change is not detected. How can I achieve this?
static get properties() {
return {
items: Object
}
}
constructor() {
super();
this.items = {
list: [{ a: 5, b: 6 },{ a: 5, b: 6 }]
}
}
onClick() {
this.items.list[1].a = 10; //change property
requestRender(); //doesn't pick up the change...
}
lit-element requires you to manage your properties immutably, so to update that data point, something like the following will be required:
onClick() {
let list = this.items.list.slice();
let item = Object.assign({}, list[1]);
item.a = 10;
list[1] = item;
this.items = {
list: list
};
// will update on it's own with out a render request
}
The above is a very complete, but ES5(ish) answer to this problem. It's also possible to do this with more modern JS that would make this much more manageable.
onClick() {
this.items.list[1].a = 10;
this.items = {
...this.items
};
// will update on it's own with out a render request
}
Hope that helps.
Related
Is there any way to get a key-value from an object's parent object? In the example below, I want to combine urlParent with section:
const linkItems = [
{
id: 1,
name: 'Home Page',
urlParent: '/home',
subItems: [
{
subId: 1,
name: 'Project 1',
section: '#project1',
get url() {
//output /home#project1
}
}
]
}
];
console.log(linkItems[0].subItems[0].url) // /home#project1;
You cannot reference an Object parent that way (Object(value) ← Array ← Object), and not even from an Object's parent Object.
What you can do instead is:
Create two Classes, one for the parent and one for the child.
When adding a child to the parent, just make a "linked list", by referencing the parent's this to the created child item parent property
class Child {
constructor(data) {
Object.assign(this, data);
}
get url() {
return this.parent.urlParent + this.section
}
}
class Parent {
constructor(data) {
Object.assign(this, data);
this.subItems = [];
}
addChild(item) {
this.subItems.push(new Child({...item, parent: this}));
}
}
// Example:
const parent = new Parent({id:1, name:"Home", urlParent:"/home"});
parent.addChild({subId:1, name:"Project 1", section:"#project1"});
console.log(parent.subItems[0].url) // /home#project1;
But hey! Nodes and trees
Your original idea and the above use too much complexity.
What I'd suggest is to treat all parent, child, whatever, as Page Nodes.
class Page {
constructor(data) {
Object.assign(this, data);
this.children = {};
}
addChild(page) {
page.parent = this; // Linked to parent!
this.children[page.id] = page;
}
get url() {
// Generate full URI by recursing the parents tree
return this.parent ? `${this.parent.url}/${this.slug}` : this.slug;
}
}
// Example:
// 1. Create pages:
const pageRoot = new Page({id:1, name:"Home page", slug:""});
const pageProj = new Page({id:3, name:"All projects", slug:"projects"});
const pageWebs = new Page({id:4, name:"Websites", slug:"websites"});
const pageStOv = new Page({id:6, name:"Stack Overflow", slug:"so"});
const pageSpec = new Page({id:9, name:"Stack Overflow Specs", slug:"specs"});
// 2. Create Tree:
pageRoot.addChild(pageProj);
pageProj.addChild(pageWebs);
pageWebs.addChild(pageStOv);
pageStOv.addChild(pageSpec);
// 3. Test
console.log(pageRoot.url); // "";
console.log(pageProj.url); // "/projects";
console.log(pageSpec.url); // "/projects/websites/so/specs";
console.log(pageRoot);
const linkItems = [
{
id: 1,
name: 'Home Page',
urlParent: '/home',
get subItems(){
console.log(this.name);
return ([
(() => {
console.log(this);
return {
subId: 1,
name: 'Project 1',
section: '#project1',
urlParentFromOuterScope: () => {
return this.urlParent;
},
sectionString(){
return this.section;
},
url(){
console.log('url', this);
return this.urlParentFromOuterScope() + this.sectionString();
}
}
})()
])
}
}
];
const subItems = linkItems[0].subitems;
console.log(linkItems[0].subItems[0].url());
Please feel free to remove the unnecessary 'console.log's after you understand the approach.
I took the liberty of adding a few methods.
This is a tricky one and has to do with the scope of this in array functions.
P.S.: I guess this can be simplified.
I have the following state
const state = {
courses: [],
series: [],
course: {
title: 'testing',
course_notes: [
{
id: 1,
note: "one" // want to edit this
},
{
id: 2,
note: "two"
}
]
}
}
I want to change state.course.course_notesp[0].name
I've never fully understood how this works, read a lot of tutorials, I feel I know how it works but it always trips me up. This is what I am trying
const m = {
...state,
course: {
course_notes:[
...state.course.course_notes,
state.course.course_notes.find(n => n.id === 1).note = "edited"
]
}
}
That seems to add edited as an extra node. state.course.course_notes.length ends up being 3.
You are using the spread operator for arrays like you would for objects.
Assume you have an object
const obj = { a: 1, b: 2 }
If you say:
{...obj, a: 2}
What you are saying is:
{ a: 1, b: 2, a: 2 }
The property a is defined twice, but the second one overrrides the first one.
If you do something similar for an array, however, the result would be different:
const arr = [1, 2];
const newArr = [...arr, arr[0]];
// here the result would be [1, 2, 1]
This is why when you are saying:
course_notes:[
...state.course.course_notes,
state.course.course_notes.find(n => n.id === 1).note = "edited"
]
what it does is add an extra element to the array.
What you should do is instead create a modified version of the array, for example using map
course_notes: state.course.course_notes.map(el => {
if (el.id === 1) {
el.note = 'edited';
}
return el;
});
There are lots of ways you could modify the state of your store to update one element of course_notes.
If we assumed the ids to be unique, I would map the previous array modifying the element with id 1.
....
course_notes: state.course.course_notes.map(x => x === 1
? { ...x, note: 'edited' }
: x
)
...
I have two objects containing the same data, I want to delete an item from one of the objects (and keep the other one with the original data for other things), however, when I use the splice method on one of the objects, both are affected, so I lose my item in both objects.
<button #click="deleteData(0)" />
export default {
data() {
return {
arrayA: [],
arrayB: []
}
},
methods: {
async initData() {
const { data: response } = await this.$store.dispatch("getData", { id: this.$route.params.id })
this.arrayA = response
this.arrayB = response
},
deleteData(indexOfItem) {
console.log("arrayA & arrayB before splice: ", this.arrayA, this.arrayB);
// arrayA & arrayB before splice : [{...}, {...}], [{...}, {...}]
this.arrayA.splice(indexOfItem, 1);
console.log("arrayA & arrayB after splice :", this.arrayA, this.arrayB);
// arrayA & arrayB after splice : [{...}], [{...}]
}
}
}
Due to the reactivity in vue.js, changes on one object causing changes on everything else, which was declared with a reference.
Example: If you have objectA with your data and declare objectB = objectA, all changes on objectA would affect objectB.
Solution: If you really need objectB with the same data, but without reference to objectA, you can do it like this:
objectB = JSON.parse(JSON.stringify(objectA));
Note that objectB is unable to react to changes of objectA with this solution.
EDIT: Explanation to your code
As you provided the following code:
data() {
return {
a: [],
b: []
}
},
methods: {
getData() {
const response = (request to retrieve the data...);
this.a = response.data;
this.b = response.data;
}
}
Both, a and b are reactive. Both have the same source for their data. So changing the content of a or b means to change the source of data and with this, both have the same changed content.
According to my approach you would do it like this:
data() {
return {
a: [],
b: []
}
},
methods: {
getData() {
const response = (request to retrieve the data...);
this.a = response.data;
this.b = JSON.parse(JSON.stringify(this.a));
}
}
I'm running in a loop which should update some values inside an object on vue environment.
I tried map, foreach & for loops but the specific property (e.g Id, or show) getting always the same values.
I took the same logic to jsfiddle (i put part of the same data there for playing) and it's worked there.
Here is the code:
var data = t.data.map((item) => {
item.Status = Status.Draft;
if (item.IsSent) {
item.Status = Status.Sent;
}
return item;
});
data.forEach((item) => {
item.Buttons.forEach((b, index) => {
const id = { Id: "button_" + item.ID + "_" + index };
let show = { show: false };
switch (b.action) {
case ButtonActions.Duplicate:
case ButtonActions.Delete:
case ButtonActions.Preview: {
show = { show: true };
break;
}
case ButtonActions.Reports:
case ButtonActions.Groups: {
if (item.IsSent) {
show = { show: true };
} else {
show = { show: false };
}
break;
}
case ButtonActions.Send:
case ButtonActions.Schedule:
case ButtonActions.Edit: {
if (item.IsSent) {
show = { show: false };
} else {
show = { show: true };
}
break;
}
}
Object.assign(b, id);
Object.assign(b, show);
});
//console.log(item.Buttons);
});
button.id getting always the same id for all the buttons.
button.show can be true or false but always the same at all buttons no matter what loop i'm using,
the console show the expected value,
then after the loop, it is strange, the property is added but with the same values all over.
It looks like I found a Vue bug! :(
Can someone help with it?
In the vue documentation regarding Reactivity caveats for objects,
It states:
Sometimes you may want to assign a number of properties to an existing object, for example using Object.assign() or _.extend(). However, new properties added to the object will not trigger changes. In such cases, create a fresh object with properties from both the original object and the mixin object:
// instead of `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
So, in that vein, this should help
b = Object.assign({}, b, { id, show })
Alternatively, you can use
this.$set(b, 'show', show);
this.$set(b, 'id', id);
I'm trying to populate a drop down box rendered by Mithril's view from methods being called outside of its module (not sure if this terminology is correct, but outside of the property which contains the view, model and controller).
This Chrome extension adds a new field to an existing page and depending on what the user select, the drop down box should refresh to items pertaining to the selected item. I can get up to the stage of getting the new list of items, but i cannot get the drop down list to redraw with the new objects.
The following shows the module which gets inserted inside an existing page:
var ItemsList = {
model: function () {
this.list = function (id) {
var d = m.deferred()
// Calls Chrome extension bg page for retrieval of items.
chromeExt.getItems(pId, function (items) {
// Set default values initially when the controller is called.
if (items.length === 0) {
items = [
{name: 'None', value: 'none'}
]
}
d.resolve(items || [])
})
return d.promise
}
},
controller: function () {
this.model = new ItemsList.model()
this.index = m.prop(0)
this.onchange = function (e) {
console.info('ctrl:onchange', e.target)
}
// Initialise the drop down list array list.
this.dropDownItemsList = m.prop([]);
// This sets the default value of the drop down list to nothing by calling the function in the model,
// until the user selects an item which should populate the drop down list with some values.
this.getItems = function(pId) {
this.model.list(pId).then(function (data) {
this.dropDownItemsList(data)
m.redraw()
}.bind(this))
}
this.getItems(0);
},
view: function (ctrl) {
var SELECT_ID = 'record_select'
return vm.Type() ? m('div', [
m('.form__item', [
m('.label', [
m('label', {
htmlFor: SELECT_ID
}, 'ID')
]),
m('.field', [
m('select#' + SELECT_ID, {
onchange: ctrl.onchange.bind(ctrl)
},
ctrl.dropDownItemsList().map(function (it, i) {
return m('option', {
value: it.value,
checked: ctrl.model.index === i
}, it.name)
})
),
])
]),
]) : null
}
}
And it is mounted using
m.mount("element name here", ItemsList);
The code which checks to see if the item has changed is using a mutation observer, and whenever it detects changes to a certain field, it will call a method to get the new values. I can see that the return value has my new items.
I have tried various different methods on trying to update the drop down list, first by trying to set the "this.list" with the new items list i've got, or trying to create a returnable method on the controller which i can call when the mutation observer fires.
After getting the new items, how can i make the drop down list show the new items which has been retrieved?
I have read guides which shows functions in the controller or model being run - but only if they've been defined to use them already in the view (i.e. have an onclick method on the view which calls the method) but so far i cannot figure out how to update or call methods from outside of the module.
Is there a way to achieve the above or a different method i should approach this?
After some more research into how Mithril works, seems like that it's not possible to call any functions defined within the component.
Due to this, i have moved the model outside of the component (so now it only has the controller and the view defined) and bound the view to use the model outside of the component.
Now calling a function which updates the model (which is now accessible from elsewhere in the code) and redrawing shows the correct values that i need.
If I understand correctly, you need to have two variables to store your lists, one to store the old list and one to store the updated list so you can always map the updated one and go to your old one if you need.
Here is a simple implementation of a drop down list with some methods to update and search. You can update the list on the fly using the methods.
mithDropDown
jsFiddle
var MythDropDown = function(list) {
if (Array.isArray(list))
this.list = list;
else
list = [];
if (!(this instanceof MythDropDown))
return new MythDropDown(list);
var self = this;
this.selected = {
name: list[0],
index: 0
};
this.list = list;
};
MythDropDown.prototype.view = function(ctrl) {
var self = this;
return m('select', {
config: function(selectElement, isinit) {
if (isinit)
return;
self.selectElement = selectElement;
self.update(self.list);
},
onchange: function(e) {
self.selected.name = e.target.value;
self.selected.index = e.target.selectedIndex;
}
},
this.list.map(function(name, i) {
return m('option', name);
}));
};
MythDropDown.prototype.getSelected = function() {
return (this.selected);
};
MythDropDown.prototype.update = function(newList) {
this.list = newList;
this.selectElement.selectedIndex = 0;
this.selected.name = newList[0];
this.selected.index = 0;
};
MythDropDown.prototype.sort = function() {
this.list.sort();
this.update(this.list);
};
MythDropDown.prototype.delete = function() {
this.list.splice(this.selected.index, 1);
this.update(this.list);
};
var list = ['test option 1', 'test option 2'];
var myList = new MythDropDown(list);
var main = {
view: function() {
return m('.content',
m('button', {
onclick: function() {
var L1 = ['Banana', 'Apple', 'Orange', 'Kiwi'];
myList.update(L1);
}
},
'Fruits'),
m('button', {
onclick: function() {
var L1 = ['Yellow', 'Black', 'Orange', 'Brown', 'Red'];
myList.update(L1);
}
},
'Colors'),
m('button', {
onclick: function() {
myList.sort();
}
},
'Sort'),
m('button', {
onclick: function() {
myList.delete();
}
},
'Remove Selected'),
m('', m.component(myList),
m('', 'Selected Item: ' + myList.selected.name, 'Selected Index: ' + myList.selected.index)
)
);
}
};
m.mount(document.body, main);
<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/0.2.3/mithril.min.js"></script>