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>
Related
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>
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 :)
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.
//index.html
<html>
<head>
<link rel="import" href="test-table.html">
</head>
<body>
<template is="dom-bind" id="index">
<test-table data="{{data}}" ></test-table>
</template>
</body>
</html>
Polymer({
is: "test-table",
properties : {
data : {type : Array},
}
/*I dont know where should I put this stuff
"queryForIds:"
"getByIds :"
"set:"
"length:0"
*/
});
<dom-module id="test-table">
<template>
<paper-datatable-card id="datatableCard" header="Users" page-size="10" data-source="{{data}}" id-property="_id" selected-ids="{{selectedIds}}">
<paper-datatable id="datatable" data='{{data}}' selectable multi-selection selected-items="{{selectedItems}}">
<paper-datatable-column header="Id" property="_id" sortable>
<template>
<span>{{value}}</span>
</template>
</paper-datatable-column>
</paper-datatable>
</paper-datatable-card>
</template>
</dom-module>
as part of single page application I am using “paper-datatable-card” in my own custom-tag. I able to display the records but I’m not getting where I have to put the code for pagination. And I don’t want to put all records into dataSource at a time.
Any help is appreciated,
Thank you,
Venkat.
From within your Polymer component, you can set data in the ready method:
ready: function() {
this.data = {
queryForIds: function(sort, page, pageSize){
// implement me
},
getByIds: function(ids){
// implement me
},
set: function(item, property, value){
// implement me
},
length:0
};
}
Your comment question:
So I am unable to put the code from lines 117 to 133 in my custom elements as it doesnt support dom-bind
Answer (in Polymer 2.0) you can do it in your component constructor all the methods and all the data variables:
class YourComponent extends Polymer.Element {
contructor() {
super();
//in case if you use paper-datatable-card
this.data = {
get: function (sort, page, pageSize) {
return new Promise((resolve, reject) => {
const exampleData = [
{ 'name': 'someName1' },
{ 'name': 'someName2' }
];
resolve(exampleData);
});
},
set: function(item, property, value){...},
length: 1
};
//in case if you use paper-datatable only then without get or set properties
this.data = [ { name: 'someName' } ];
}
}
i have a polymer custom element like this
<script src="http://www.polymer-project.org/platform.js"></script>
<script src="http://www.polymer-project.org/polymer.js"></script>
<polymer-element name="my-foo" constructor="MyFoo" attributes="controller" noscript>
<template>
hello from {{options.name}}
</template>
<script>
Polymer({
get options() {
return this.controller.options;
}
});
</script>
</polymer>
<div id="container"></div>
<script>
document.addEventListener('polymer-ready',function() {
var foo = new MyFoo();
foo.controller = {
options : {
name : "here"
}
};
document.body.appendChild(foo);
});
</script>
attribute "controller" is expected to be a object having a property "options" of type object.
I can create a Instance of the custom element using
var foo = new Foo();
but i am unable to set the attribute "controller" which results in an error:
Uncaught TypeError: Cannot read property 'options' of undefined.
Sure - i could modify the options getter in the polymer component to be fail-safe like this :
...
get options() {
return this.controller ? this.controller.options : null;
}
...
But also in this case the custom element doesnt recognize the applied data.
Final question : How do I pass required attributes to an custom element constructor ?
One solution is to use a computed property:
<script src="http://www.polymer-project.org/components/platform/platform.js"></script>
<link rel='import' href='http://www.polymer-project.org/components/polymer/polymer.html'>
<polymer-element name="my-foo" constructor="MyFoo" attributes="controller" noscript>
<template>
hello from {{options.name}}
</template>
<script>
Polymer({
computed: {
options: 'controller.options'
}
});
</script>
</polymer-element>
<script>
document.addEventListener('polymer-ready',function() {
var foo = new MyFoo();
document.body.appendChild(foo);
foo.controller = {
options : {
name : "here"
}
}
});
</script>
You can even make the computed property just a call to a method. The key thing is that by doing it this way, Polymer can tell when to check if options has changed, because it knows the inputs necessary to compute options.
Polymer({
computed: {
options: 'getOptions(controller)'
},
getOptions: function(controller) {
return controller ? controller.options : null;
}
});