Polymer dynamic element name in dom-repeat - html

how can I use dom-repeat to create different elements for iron-pages? something like this:
<template is="dom-repeat" items="{{pages}}">
<[[item.name]]></[[item.name]]>
</template>
...
pages: {
type: Array,
value() {
return [
{
"name": "element-a",
},
{
"name": "element-b",
},
{
"name": "element-c",
},
},
I'm using polymer 2.0.

since my comment had some interest, I put it as an answer.
The code examples will be in polymer 1.9 for now, I'll update my answer when I'll do the switch to 2.0 but the idea should be the same anyway
First you need a wrapper element, wich will be capable of creating another element dynamically from a property, and adding it to itself.
In my example the name of the element to create will be a property named type of a JSON object Data wich came from a database in XHR.
With a dynamically created element the binding won't work, so you have to do it by hand. That's what the _updateStatefunction is for, here it only update on one property but the idea is the same is there is more.
wrapper :
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../styles/shared-styles.html">
<dom-module id="element-wrapper">
<template></template>
<script>
Polymer({
is: 'element-wrapper',
properties: {
elementData: {
type: Object
},
saveFbPath: {
type: String
},
element: {
type: Object
},
formSubmitPressed: {
type: Boolean,
value: false
}
},
observers: [
'_updateState(elementData.*)'
],
attached: function () {
console.log("creating element : ", this.elementData);
this.async(function () {
this.element = this.create(this.elementData.type, {
"data": this.elementData
});
this.appendChild(this.element);
}.bind(this));
},
_updateState: function (elementData) {
if (typeof this.element !== "undefined") {
this.element.data = elementData.value;
console.log('Change captured', elementData);
}
}
});
</script>
</dom-module>
The this.element = this.create(this.elementData.type, {"data":this.elementData}); line is the one creating the element, first argument is the dom-modulename, and the second a JSON object wich will be binded to the properties of the element.
this.appendChild(this.element);will then add it to the dom
All this is in a this.async call for a smoother display
You then need a dom-repeat which will call this element, and give it the datas it need to create the dynamic ones.
Here is an example of an element-list but you don't necessary need a specific element for that, this logic can be in a bigger one.
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../styles/shared-styles.html">
<link rel="import" href="element-wrapper">
<dom-module id="element-list">
<template>
<style is="custom-style" include="shared-styles"></style>
<template is="dom-repeat" items="[[ datas ]]" initial-count="10" as="data">
<element-wrapper element-data="[[data]]"></element-wrapper>
</template>
</template>
<script>
Polymer({
is: 'element-list',
properties: {
datas: {
type: Array,
value: function () {
return [];
}
}
}
});
</script>
</dom-module>
This should do the trick :)

Related

Polymer - How to bind a dynamic path?

How to bind a dynamic path in Polymer?
For instance:
Lets say our component has 2 properties:
list: an array of objects.
map : a javascript object which map sub-objects.
Each item in the list has a property key which is the key to get the value from the map property.
I would like to "dynamic" bind a path like map[item.key]. The only way to do something like this is to make a function, but it will not be triggered on changes of properties and sub-properties of map. =/
In the following snippet, you can see, by clicking on the button, it will dynamicly place an object in the map.key2 property, using the Polymer.Element.set method. But this doesn't trigger any changes because Polymer doesn't bind a path. It only execute the display function once.
So this Stackoverflow answer doesn't help (even though it's the same question).
<script src="https://polygit.org/components/webcomponentsjs/webcomponents-loader.js"></script>
<link rel="import" href="employee-list.html">
<link rel="import" href="https://polygit.org/components/polymer/polymer-element.html">
<link rel="import" href="https://polygit.org/components/paper-button/paper-button.html">
<link rel="import" href="https://polygit.org/components/polymer/lib/elements/dom-repeat.html">
<dom-module id="my-element">
<template>
<ul>
<template is="dom-repeat" items="[[list]]">
<!--Bind something like this-->
<li> [[ _getAt(item.key) ]] </li>
</template>
</ul>
<!--This button will add the 2nd object-->
<paper-button on-tap="_onButtonClicked" raised>Add key 2</paper-button>
</template>
<script>
class MyElement extends Polymer.Element {
static get is(){
return "my-element";
}
static get properties(){
return {
list : {
type : Array,
value : function () {
return [
{
key : "key1",
// ...
},
{
key : "key2",
// ...
},
// ...
]
}
},
map : {
type : Object,
value : function () {
return {
key1 : {
message : "Hello",
// ...
},
// ...
}
}
}
};
}
_onButtonClicked(){
// Add the 2nd object
this.set("map.key2", {
message : "World",
});
console.log("key 2 added");
}
_getAt(key){
if (this.map[key])
return this.map[key].message;
}
}
customElements.define(MyElement.is, MyElement);
</script>
</dom-module>
<my-element></my-element>
The Polymer documentation says that it's possible to build a path in a array. But I didn't find a way to bind an array of string as a path.
"The only way to do something like this is to make a function, but it
will not be triggered on changes of properties and sub-properties of
map"
You can make this work by passing the objects/properties that are changing.
Basic example
this will NOT update:
<template is="dom-repeat" items="[[_getItems()]]"></template>
this WILL update:
<template is="dom-repeat" items="[[_getItems(list)]]"></template>
Now the dom-repeat will fire again once the property 'list' changes.
Let's say you have 2 properties, and you want to re-run dom-repeat when one of them changes:
<template is="dom-repeat" items="[[_getItems(list, somethingElse)]]"></template>
You might also want to take a look at https://www.polymer-project.org/1.0/docs/devguide/model-data#override-dirty-check
EDIT:
Where are you updating the property LIST? dom-repeat wont run again until that property is changed
DOUBLE EDIT:
try this (polygit is currently having server issues):
<script src="https://polygit.org/components/webcomponentsjs/webcomponents-loader.js"></script>
<link rel="import" href="employee-list.html">
<link rel="import" href="https://polygit.org/components/polymer/polymer-element.html">
<link rel="import" href="https://polygit.org/components/paper-button/paper-button.html">
<link rel="import" href="https://polygit.org/components/polymer/lib/elements/dom-repeat.html">
<dom-module id="my-element">
<template>
<ul>
<template is="dom-repeat" items="[[list]]">
<!--Bind something like this-->
<li> [[ _getAt(item.key, map, list) ]] </li>
</template>
</ul>
<!--This button will add the 2nd object-->
<paper-button on-tap="_onButtonClicked" raised>Add key 2</paper-button>
</template>
<script>
class MyElement extends Polymer.Element {
static get is() {
return "my-element";
}
static get properties() {
return {
list: {
type: Array,
value: function() {
return [{
key: "key1",
// ...
},
{
key: "key2",
// ...
},
// ...
]
}
},
map: {
type: Object,
value: function() {
return {
key1: {
message: "Hello",
// ...
},
// ...
}
}
}
};
}
_onButtonClicked() {
var foo = this.list;
this.set("map.key2", {
message: "World",
});
this.set("list", {});
this.set("list", foo);
console.log("key 2 added");
}
_getAt(key) {
if (this.map[key])
return this.map[key].message;
}
}
customElements.define(MyElement.is, MyElement);
</script>
</dom-module>
<my-element></my-element>

Polymer data-binding not updated on array mutation

I noticed that template/data-binding in Polymer doesn't seem to reflect when a array property is mutated (i.e. push()). Sample code below:
<body>
<dom-module id="my-element">
<template>
<pre>
[my-element]
myArray: [[jsonStringify(myArray)]]
</pre>
</template>
</dom-module>
<my-element id="elm"></my-element>
<button onclick="pushArray()">pushArray</button>
<button onclick="setArray()">setArray</button>
<script>
(function registerElements() {
Polymer({
is: 'my-element',
properties: {
myArray: {
type: Array,
value: function () {
return [];
}
}
},
pushArray: function(value) {
this.push('myArray', {id: value});
},
setArray: function(value) {
this.set('myArray', [{id: value}]);
},
jsonStringify: function(obj) {
return JSON.stringify(obj);
}
});
})();
function pushArray () {
var elm = document.querySelector('#elm');
elm.pushArray('Push');
}
function setArray () {
var elm = document.querySelector('#elm');
elm.setArray('Set');
}
</script>
</body>
Whenever I click the pushArray button, an item "Push" should be added in myArray, but it wasn't reflected in the template [[jsonStringify(myArray)]]. is this an expected behavior? Anyway to work around this?
The Array change observer is a bit tricky. In your code, by using myArray, you implicitly observe (only) the reference for the (whole) array, which only changes when you run setArray.
In order to overcome this, you must also use a deep observer, namely myArray.*. The correct code for your dom-module is therefore the following:
<dom-module id="my-element">
<template>
<pre>
[my-element]
myArray: [[jsonStringify(myArray, myArray.*)]]
</pre>
</template>
</dom-module>
Live demo: http://jsbin.com/yulivuwufu/1/edit?html,css,output
No other code changes are necessary.

Multiple polymer 2 elements with independent properties?

I have a question to my custom polymer element. First, I made an element with a simple paper-input. My problem is, that I don't know, how to use this element as an "independent" element. My example is here in this jsfiddle. Type in the first input "asd" and hit Enter and then in the second input "asd" and hit Enter. You can see, that both elements are sharing the properties (console log "-1" not found in array and second log will be "1")
<!doctype html>
<html>
<head>
<title>2.0 preview elements</title>
<base href="http://polygit.org/polymer+v2.0.0-rc.4/webcomponentsjs+webcomponents+v1.0.0-rc.6/shadycss+webcomponents+1.0.0-rc.2/paper*+polymerelements+:2.0-preview/iron*+polymerelements+:2.0-preview/app*+polymerelements+:2.0-preview/neon*+polymerelements+:2.0-preview/components/">
<script src="webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="polymer/polymer.html">
<link rel="import" href="paper-input/paper-input.html">
</head>
<body>
<input-test></input-test>
<input-test></input-test>
<dom-module id="input-test">
<template>
<paper-input value="{{aValue}}" on-keydown="_keydown"></paper-input>
</template>
<script>
window.addEventListener('WebComponentsReady', function() {
class InputTest extends Polymer.Element {
static get is() {
return 'input-test';
}
static get properties() {
return {
aValue: {
type: String,
value: ''
},
_inputStore: {
type: Array,
value: []
}
};
}
_keydown(event) {
const keyCode_enter = '13';
if (event.keyCode == keyCode_enter) {
console.log(this._inputStore.indexOf(this.aValue))
this.push('_inputStore', this.aValue);
}
}
}
customElements.define(InputTest.is, InputTest);
})
</script>
</dom-module>
</body>
</html>
What can I do, to have independent properties?
Thanks!
I found the answer.
The problem is the default value declaration of the array.
_inputStore: {
type: Array,
value: function () {
return [];
}
}
This code solves the problem.

How to combine polymer-redux and <app-route>?

We are currently using <app-route> for routing, and are now implementing Redux using polymer-redux. It's unclear what the best way is to combine the two, however. Since <app-route> maintains its own state, we can't really store it in our Redux store. However, for some actions the user can perform, we also want to update the URL.
My current line of thinking is doing something with middleware, but it's not quite clear to me how best to access/modify the routes in <app-route> from within that middleware. How can we best approach this?
I've ran into the same problem myself. The clash is that the main principle of redux is that it advocates for a unique centralized state, while polymer advocates for multiple decentralized states. Reconciling both obviously requires some hack from time to time. Syncing the browser URL in the redux state through polymer (to use with app-route) is a good example. Using app-location, what I've done is this:
<!-- my-app.html -->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="/bower_components/app-route/app-location.html">
<link rel="import" href="/src/redux/redux-behavior.html">
<link rel="import" href="/src/redux/route-action-creator.html">
<dom-module id="my-app">
<template>
<app-location route="{{_browserRoute}}"></app-location>
</template>
<script>
Polymer({
is: 'my-app',
behaviors: [ReduxBehavior, RouteActionCreator],
properties: {
_browserRoute: {
type: Object
}
},
observers: [
'_browserRouteChanged(_browserRoute.*)'
],
_browserRouteChanged: function(browserRoute) {
this.dispatch('browsed', browserRoute.base)
}
})
</script>
</dom-module>
We use a two-way binding to keep my-app's _browserRoute in sync with the browser url. Upon change, it dispatches a 'browsed' action, which is going to reduce the current route in the state to the new value of _browserRoute.
<!-- route-action-creator.html -->
<script>
const ROUTE_ACTIONS = {}
const routeActions = {}
ROUTE_ACTIONS.BROWSED = 'ROUTE_BROWSED'
routeActions.browsed = function(route) {
return {
type: ROUTE_ACTIONS.BROWSED,
route: route
}
}
ROUTE_ACTIONS.REDIRECTED = 'ROUTE_REDIRECTED'
routeActions.redirected = function(path) {
window.history.pushState({}, null, path)
window.dispatchEvent(new CustomEvent('location-changed'))
return {
type: ROUTE_ACTIONS.REDIRECTED,
path: path
}
}
/**
* Route action creator behavior
* #polymerBehavior
*/
RouteActionCreator = {
actions: routeActions
}
</script>
<!-- route-reducer.html -->
<link rel="import" href="/src/redux/route-action-creator.html">
<script>
const reducers = {}
reducers.routeReducer = function(state, action) {
if (!state) {
return {
current: null
}
}
switch (action.type) {
case ROUTE_ACTIONS.BROWSED:
return Object.assign({}, state, {
current: action.route
})
case ROUTE_ACTIONS.REDIRECTED:
// Browser state is reduced in action creator because it uses an event
return state
default:
return state
}
}
</script>
And so now I can bind
statePath: 'route.current'
to any app-route component in my app. I can also use
this.dispatch('redirected', '/some-path')
for redirection and standard anchor tags as the browser will sync with the state.
I would create a non-visual polymer element that wraps an app-route and also implements the polymer-redux behavior and handles the interaction between app-route and the redux store.
Here is an example of a such an element for a wizard (with steps and analysis to be loaded):
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/app-route/app-location.html">
<link rel="import" href="../bower_components/app-route/app-route.html">
<dom-module id="my-router">
<style>
</style>
<template>
<app-location route="{{route}}" id="location" use-hash-as-path></app-location>
<app-route
active="{{active}}"
route="{{route}}"
pattern="/:step/:id"
data="{{page}}"
tail="{{tail}}">
</app-route>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'my-router',
behaviors: [ ReduxBehavior ],
properties: {
page: {
type:Object,
value: function(){ return {};},
},
currentAnalysisId: {
type:String,
statePath: 'id',
},
currentStep: {
type:String,
statePath:'currentStep',
},
active:{
type: Boolean,
observer:'_onInValidRoute'
}
},
observers: [
'_changeRoute(page)',
'_changeStepOrAnalysisId(currentStep,currentAnalysisId)',
],
actions: {
changeStep: function(step) {
return {
type: 'CHANGE_CURRENT_STEP',
step,
}
},
loadAnalysis: function(id,step) {
return {
type: 'LOAD_ANALYSIS',
id,
step,
}
},
},
_initialized : false,
ready: function() {
// required otherwise if navigating to a sub-route for the first time will be reset, by the localstorage initial redux state update
this._initialized = true;
},
_changeRoute: function(page) {
// required because otherwise
this.debounce('route_update',()=>{
let step = page.step;
let id = page.id;
if (id && this.getState().id !== id) {
ga('send', 'event', 'Analysis', 'load');
this.dispatch('loadAnalysis',id,step)
}
else if (this.getState().currentStep !== step) {
this.dispatch('changeStep',step);
}
ga('send', 'pageview');
},1);
},
_changeStepOrAnalysisId: function(step, id) {
if (!this._initialized) {
return;
}
this.debounce('state_changed',()=>{
if (this.page.step !== step || (this.page.id !== id && this.page.id !== '' && id !== null)) {
this.page = {step,id};
}
})
},
_onInValidRoute: function(valid) {
if (!valid) {
this.async(()=> {
this.$.location.set('route.path','/start/');
})
}
},
});
})();
</script>
</dom-module>

Polymer. Update view after model changed

This is a common question in Polymer about updating view when model was updated (answer is to use this.set or this.push Polymer methods).
But when I have two elements:
First element:
properties:{
items: {
type: Array
}
},
someFunction: {
this.push('items', {'Name': 'something'});
}
Second Element has property which is bound to 'items' from first element
ready: function(){
this.items = firstElement.items;
}
I would like second element 'items' to be updated when 'items' updated on firstElement. I can't manually notify secondElement, because of app restrictions.
So for now I see (in devTools) that model of second element is correct ('items' contains objects), but view isn't updated.
How to update it?
You need to set notity on the property of the first element to receive changes outside of the element itself
properties:{
items: {
type: Array,
notify: true
}
},
then, in secondElement you can
<firstElement items="{{items}}" />
Let Polymer's two-way binding handle the notifications for you.
Consider two elements, x-foo and x-bar, in a container element, x-app.
In x-foo, declare items with notify:true so that its changes would be propagated upward.
Polymer({
is: 'x-foo',
properties: {
items: {
type: Array,
value: function() { return []; },
notify: true
}
}
});
In x-app, bind x-foo's items to x-bar's.
<dom-module id="x-app">
<template>
<x-foo items="{{items}}"></x-foo>
<x-bar items="[[items]]"></x-bar>
</template>
<script>
Polymer({ is: 'x-app' });
</script>
</dom-module>
Now, x-bar would be notified of all changes to the items array (when an item is added/removed).
HTMLImports.whenReady(() => {
"use strict";
Polymer({
is: 'x-app'
});
Polymer({
is: 'x-foo',
properties: {
items: {
type: Array,
value: function() { return []; },
notify: true
}
},
_addItem: function() {
this.push('items', {name: 'item' + this.items.length});
}
});
Polymer({
is: 'x-bar',
properties: {
items: {
type: Array
}
}
});
});
<head>
<base href="https://polygit.org/polymer+1.6.0/components/">
<script src="webcomponentsjs/webcomponents-lite.min.js"></script>
<link rel="import" href="polymer/polymer.html">
</head>
<body>
<x-app></x-app>
<dom-module id="x-app">
<template>
<x-foo items="{{items}}"></x-foo>
<x-bar items="[[items]]"></x-bar>
</template>
</dom-module>
<dom-module id="x-foo">
<template>
<h2><code>x-foo</code></h2>
<button on-tap="_addItem">Add item</button>
</template>
</dom-module>
<dom-module id="x-bar">
<template>
<h2><code>x-bar</code></h2>
<ul>
<template is="dom-repeat" items="[[items]]">
<li>[[item.name]]</li>
</template>
</ul>
</template>
</dom-module>
</body>
codepen