The history property defined in my sma-canvas element:
static get properties() {
return {
history: {
type: Array,
value: () => {
return [];
},
notify: true
}
};
}
static get observers() {
return [
'_historyChanged(history.*)'
];
}
_historyChanged() {
console.log("History Changed!");
this._redrawCanvas(this.$.canvasEl, this.history);
}
I'm trying to set the history property from another element:
<dom-module id="sma-create-studio">
<template>
<sma-canvas id="shapesCanvasEdit" history={{shapesOnCanvasEdit}}></sma-canvas>
</template>
<dom-module>
...
static get properties() {
return {
shapesOnCanvasEdit: {
type: Array,
value: () => {
return [];
},
notify: true
}
};
}
_selectFrame(e) {
// this.shapesOnCanvasEdit = e.detail.frame.shapes;
// this.shapesOnCanvasEdit.splice(0, this.shapesOnCanvasEdit.length);
// this.shapesOnCanvasEdit.push(e.detail.frame.shapes);
this.set('shapesOnCanvasEdit', e.detail.frame.shapes);
// this.splice('shapesOnCanvasEdit', 0, this.shapesOnCanvasEdit.length, e.detail.frame.shapes);
// this.splice('shapesOnCanvasEdit', 0, this.shapesOnCanvasEdit.length);
// this.push('shapesOnCanvasEdit', e.detail.frame.shapes);
// this.notifyPath('shapesOnCanvasEdit');
// this.notifySpice('shapesOnCanvasEdit');
}
I need to call _historyChanged (the observer in sma-canvas), whenever I change the value of shapesOnCanvasEdit. As you can see from the last code snippet, I tried multiple ways to do that, but all in-vain.
Can anybody please help me in this regard? I have no clue, what I'm doing wrong.
SOLUTION 1
static get properties() {
return {
active: {
type: Boolean,
// Observer method identified by name
observer: '_activeChanged'
}
}
}
// Observer method defined as a class method
_activeChanged(newValue, oldValue) {
this.toggleClass('highlight', newValue);
}
https://www.polymer-project.org/2.0/docs/devguide/observers#change-callbacks
From my experience in 1.93, adding a simple observer for an array updates the array if the length changes. (Change Boolean to Array in the example above.)
SOLUTION 2
You can observe splices:
static get observers() {
return [
'usersAddedOrRemoved(myArray.splices)'
]
https://www.polymer-project.org/2.0/docs/devguide/observers#array-observation
}
It only fires if the array length changes. To force update, reset it array myArray with this.set('myArray', []), if you're desperate.
SOLUTION 3
The truly desperate solution. Add a secondary int variable that you update every time there is a change in the array. So _selectFrame() should first update your array history and then update the integer, and both should be properties in sma-canvas. Set an observer on that integer variable in your sma-canvas element, as shown in solution 1.
Related
I have a LitElement that represents a file upload for multiple files.
This uses a sub-component that represents each file.
I'm struggling to find examples of the best practice for propagating changes into the sub component using LitElements as it appears to be very different from Polymer 3
Here's a cut down example of what I'm trying:
import './uploadFile.js';
class Upload extends LitElement {
...
static get properties() { return { files: Object } }
_render({files}) {
return html`
<input type="file" multiple onchange="...">
${this.renderFiles(files)}`
}
renderFiles(files) {
const filesTemplate = [];
for (var i = 0; i < files.length; i++) {
filesTemplate.push(html`
<upload-file file="${files[i]}"></upload-file>
`);
}
return filesTemplate;
}
}
When I update the status of a file the upload component re-renders but the upload-file component does not.
What am I doing wrong here? There aren't may examples of LitElement usage out there.
TIA
Best practice is "properties down, events up"; meaning that parent elements should share data with children by binding properties to them, and child elements should share data with parents by raising an event with relevant data in the detail of the event.
I can't comment on what you're doing wrong as I can't see how you're updating the status of the files, or your implementation of the child element.
One thing to be aware of is that because of dirty checking, lit-element can only observe changes to the top-level properties that you've listed in the properties getter, and not their sub-properties.
Something like
this.myObj = Object.assign({}, this.myObj, {thing: 'stuff'});
will trigger changes to an object and its sub-properties to render, while
this.myObj.thing='stuff';
will not.
To get sub-property changes to trigger a re-render, you would need to either request one with requestRender() or clone the whole object.
Here is some sample code showing a basic "properties down, events up" model:
Warning: lit-element is still pre-release and syntax will change.
parent-element.js
import { LitElement, html} from '#polymer/lit-element';
import './child-element.js';
class ParentElement extends LitElement {
static get properties(){
return {
myArray: Array
};
}
constructor(){
super();
this.myArray = [
{ val: 0, done: false },
{ val: 1, done: false },
{ val: 2, done: false },
{ val: 3, done: false }
];
}
_render({myArray}){
return html`
${myArray.map((i, index) => {
return html`
<child-element
on-did-thing="${(e) => this.childDidThing(index, i.val)}"
val="${i.val}"
done="${i.done}">
</child-element>
`})}
`;
}
childDidThing(index, val){
this.myArray[index].done=true;
/**
* Mutating a complex property (i.e changing one of its items or
* sub-properties) does not trigger a re-render, so we must
* request one:
*/
this.requestRender();
/**
* Alternative way to update a complex property and make
* sure lit-element observes the change is to make sure you
* never mutate (change sub-properties of) arrays and objects.
* Instead, rewrite the whole property using Object.assign.
*
* For an array, this could be (using ES6 object syntax):
*
* this.myArray =
* Object.assign([], [...this.myArray], {
* [index]: { val: val, done: true }
* });
*
*/
}
}
customElements.define('parent-element', ParentElement);
child-element.js
import { LitElement, html} from '#polymer/lit-element';
class ChildElement extends LitElement {
static get properties(){
return {
val: Number,
done: Boolean
};
}
_render({val, done}){
return html`
<div>
Value: ${val} Done: ${done}
<button on-click="${(e) => this.didThing(e)}">do thing</button>
</div>
`;
}
didThing(e){
var event = new CustomEvent('did-thing', { detail: { stuff: 'stuff'} });
this.dispatchEvent(event);
}
}
customElements.define('child-element', ChildElement);
Hope that helps.
I am using Redux spread operator to hopefully mantain the state as immutable objects.
However, i am managing to make the most simple unit test fail.
I assume the error probably has to do with immutables, but am i not using the spread operator correctly?
Here is my unit test:
describe('app logic', () => {
it('initialises app', () => {
const newState = reducer(INITIAL_STATE, {type: "NEXT"})
const expectState = {
start: true,
render_component: null,
requests: {},
results: {},
}
console.log('newState', newState)
console.log('expected state', expectState)
expect(newState).to.equal(expectState)
})
})
and here is my reducer
export const INITIAL_STATE = {
start: false,
render_component: null,
requests: {},
results: {}
}
export const next = (state) => {
if (state === INITIAL_STATE) {
return {
...state,
start: true,
}
}
return state
}
export function reducer(state = INITIAL_STATE, action) {
switch (action.type) {
case 'NEXT':
return next(state)
default:
return state
}
}
I print the two objects, and they look the same.
i get the error :
1) app logic initialises app:
AssertionError: expected { Object (start, render_component, ...) } to equal { Object (start, render_component, ...) }
Not sure exactly which testing library you are using, but usually a name like .equal is used to test strict equality ( === ), which means (at least in the case of objects) that the two things being compared must actually reference the exact same object. So, for example,
const original = { a: 1 }; // creates a new object, assign it
const testMe = { a: 1 }; // creates another new object, assign it
console.log( original === testMe ) // false
evaluates to false, because while the objects have the same content, they do not reference the exact same object. They are separate, independently created, objects that happen to have the same content. Compare that to
const original = {a: 1}; // create a new object
const testMe = original; // create another reference to the same object
console.log( original === testMe ); // true
So when you return
return {
...state,
start: true,
}
you are creating and returning a new object, so it naturally can not reference the same object that you created and assigned to the variable name expectedState.
If what you are interested in is not strict equality, but rather just that the content in the two objects are the same, there exists other methods than .equal, usually named something with deep (since they go deep into the objects/arrays/whatever to check if the values are the same).
Chai.js has examples of both expect(x).to.equal(y) and expect(x).to.deep.equal(y) in their docs: http://chaijs.com/api/bdd/#method_equal
Your testing library probably has very similar, if not identical, syntax.
I am working with a dataset that cannot be modified on the server side. So I am trying to setup the local data model on the client in a way that I can easily traverse through the model when updating parts of the data.
Therefore I am trying to create a multi-leveled Map from multi-leveled Maps including Lists, that themselves include Maps, etc. (see schematics at the end of this post).
What I am trying to get is a Map containing other Maps, with the key of the included Map being the value of the object (again please see schematics at the end of this post).
I got it to work on the first level:
const firstLevel = data.toMap().mapKeys((key, value) => value.get('value'));
See it in action here: https://jsfiddle.net/9f0djcb0/4/
But there is a maximum of 3 levels of nested data and I can't get my head around how to get the transformation done. Any help appreciated!
The schematic datasets:
// This is what I got
const dataset = [
{
field: 'lorem',
value: 'ipsum',
more: [
{
field: 'lorem_lvl1',
value: 'ispum_lvl1',
more: [
{
field: 'lorem_lvl2',
value: 'ispum_lvl2',
more: [
{
field: 'lorem_lvl3',
value: 'ispum_lvl3',
}
]
}
]
}
]
},
{
field: 'glorem',
value: 'blipsum'
},
{
field: 'halorem',
value: 'halipsum'
}
];
This is where I want to go:
// This is what I want
const dataset_wanted = {
ipsum: {
field: 'lorem',
value: 'ipsum',
more: {
lorem_lvl1: {
field: 'lorem_lvl1',
value: 'ispum_lvl1',
more: {
lorem_lvl2: {
field: 'lorem_lvl2',
value: 'ispum_lvl2',
more: {
lorem_lvl3: {
field: 'lorem_lvl3',
value: 'ispum_lvl3',
}
}
}
}
}
}
},
glorem: {
field: 'glorem',
value: 'blipsum'
},
halorem: {
field: 'halorem',
value: 'halipsum'
}
};
Retrieve nested structures using "getIn" is beter.
const data = Immutable.fromJS(dataset[0]);
const firstLevel = data.getIn(['more']);
const twoLevel = firstLevel.getIn([0,'more']);
const threeLevel = twoLevel.getIn([0,'more']);
console.log(firstLevel.toJS(),twoLevel.toJS(),threeLevel.toJS());
As for a more generative solution, I re-wrote the answer before to a recursive approach:
function mapDeep(firstLevel) {
return firstLevel.map((obj) => {
if (obj.has('more')) {
const sec = obj.get('more').toMap().mapKeys((key, value) => value.get('value'));
const objNext = mapDeep(sec);
obj = obj.set('more', objNext);
}
return obj;
});
}
The first level still needs to be mapped manually before.
const firstLevel = data.toMap().mapKeys((key, value) => value.get('value'));
const secondLevel = mapDeep(firstLevel);
Again, see it in action: https://jsfiddle.net/9f0djcb0/12/
This is good enough for me for now. Still feels like this can be solved smarter (and more performant).. Cheers :)
So after some time passed I came up with a solution that works for me:
let sec, third, objThird;
// 1st level: simple mapping
const firstLevel = data.toMap().mapKeys((key, value) => value.get('value'));
// 2nd level: walk through updated firstLevel's subobjects and do the mapping again:
const secondLevel = firstLevel.map((obj) => {
if (obj.has('more')) {
sec = obj.get('more').toMap().mapKeys((key, value) => value.get('value'));
// 3nd level: walk through updated secondLevel's subobjects and do the mapping again:
objThird = sec.map((o) => {
if (o.has('more')) {
third = o.get('more').toMap().mapKeys((key, value) => value.get('value'));
o = o.set('more', third);
}
return o;
});
obj = obj.set('more', objThird);
}
return obj;
});
See it in action here: https://jsfiddle.net/9f0djcb0/7/
This has been working nicely so far, thur pretty hard-coded. If anyone has a more elegant solution to this, I am happy to learn about it!
Is it possible to reflect changes to polymer component property without update the value via .set(path, value).
For example I have object with overrided setter which based on this value apply new value to other field.
Polymer({
is: 'x-element',
properties: {
form: {type: Object,
value: {
set name(v) {
this._name = v;
this.additional = v; // change the different property
},
get name() {
return this.name;
},
set additional(v) {
// process v
this._additional = v; // field does not reflect
},
get additional() {
return this._additional;
}
},
reflectToAttribute: true
}
})
I have element and I put in it object with data for it.
<my-element data="{{item.content}}"></my-element>
For now I have not done this so I have all in data property declaration.
properties: {
data: {
type: Object,
value: {
active: false,
name: "Some name."
...
}
}
}
I also have to click handler on child element which should toggle the boolean value.
_handleClickBlock: function () {
this.data.active = !this.data.active;
}
I need to set [active] attribute on my-element to data.active value and make changes every time, because I use it in my CSS styles like :host[active] ....
How can I achieve it? I tried this.setAttribute('active', this.data.active) in event handler, but it change the value only once; console log was toggling.
Edit:
I have tried use this code:
observers: [
'_activeChange(data.active)'
],
_activeChange: function(newValue) {
this.active = newValue;
console.log("change");
},
_handleClickBlock: function () {
console.log("------ click");
this.data.active = !this.data.active;
console.log(this.data.active);
}
But in my console output is only this, so observer is not called.
------ click
false
------ click
true
Try including 'active' as a property in you my-element and set reflectToAttribute to true.
properties: {
data:{...}
active: {
type: Boolean,
reflectToAttribute: true
}
}
observers: [
'_activeChange(data.active)'
],
_activeChange: function(newValue) {
this.active = newValue;
}
Edit: Please go through how to change the path such that observers are notified. You need to update data.active as below
this.set('data.active', !this.data.active);
Below jsbin:
http://jsbin.com/qupoja/4/edit?html,output