I am trying to use Polymer and am struggling to figure out how to create my system in a Polymer-y way.
I'd like a left nav bar (a column on the left side), as shown in the example app docs. For the purposes of the explanation, the left nav bar shows a list of people and the main content window shows a list of books they own. When a person is selected, the main content window should be updated to show only that person's books.
Both lists should be dynamically populated via a network query and be able to be re-populated via a refresh button. It seems like this might complicate how I bind data, as the functions can't just live in a the shadowdom, as the nav bar and content lists need to be able to communicate.
From the day I've spent looking into Polymer, the nav bar (list of people) seems a natural candidate for <iron-selector>. The list of books sounds like <iron-list>. I'll need to have several instances of the book list, however--one for each person.
Should I create a <book-list> custom element for this, perhaps with <iron-list> in the template? Is it straightforward to create these <book-list> elements in response to selections made in <iron-selector>, as I won't know how to create them before querying for book owners? Any pointers on how to do this?
This sounds like a very basic use case, but I still don't know enough about Polymer to have any intuitions about the best way to do it. Any help would be much appreciated.
I eventually got this working. As a starting point, I leaned heavily on the example given in the docs that uses the app-drawer-template.
As suggested by a commenter above, using a router ended up being part of the answer. What was confusing me was that I wan't sure how to dynamically update both the <iron-selector> used in that example as well as the <iron-pages>. I wanted both to represent the same list of users. How would I keep these in sync, etc?
I ended up with three custom elements. Two were thin wrappers around <iron-selector> and <iron-pages>. The third was a <user-provider> element. The role of <user-provider> is to perform the request for the list of users and expose the result as a variable that the other two elements can interact with.
<user-provider> has only a refresh button. It has a property called users of type Array. In the ready() lifecycle callback and in response to clicking the refresh button, it updates the list of users. Note that reflectToAttribute and notify are both set to true, ensuring that it can indeed provide the list to the other elements. A stripped down version is shown below:
<dom-module id="user-provider">
<template>
<style>
:host {
display: block;
box-sizing: border-box;
</style>
<paper-fab icon="refresh" on-tap="refresh"></paper-fab>
</template>
<script>
Polymer({
is: 'user-provider',
properties: {
users: {
type: Array,
notify: true,
reflectToAttribute: true
}
},
refresh: function() {
this.refreshUsers();
},
refreshUsers: function() {
var _this = this;
// in reality I require a browserify module here
window.getUsersViaPromise()
.then(retrievedUsers => {
_this.users = retrievedUsers;
});
},
// Element Lifecycle
ready: function() {
this.users = [];
this.refreshUsers();
}
});
</script>
</dom-module>
My <user-selector> element takes an array as an argument. The thing that was stumping me here was that I didn't realize how simple it was to use a dom-repeat template inside of an <iron-selector>. I thought I'd have to wrap the repeats in a <div> or something, at which point the <iron-selector> would stop working. With dom-repeat I'm able to even use my special settings element and keep it always at the bottom of the list. The incoming variable corresponds to the element that is selected.
<dom-module id="user-selector">
<template>
<style>
:host {
display: block;
--app-primary-color: #4285f4;
--app-secondary-color: black;
}
</style>
<iron-selector
selected="{{incoming}}"
attr-for-selected="name"
class="drawer-list"
role="navigation">
<template is="dom-repeat" items="{{users}}">
<a name="view{{index}}">{{item.instanceName}}</a>
</template>
<a name="settings">Settings</a>
</iron-selector>
</template>
<script>
Polymer({
is: 'user-selector',
properties: {
incoming: {
type: String,
reflectToAttribute: true,
notify: true
},
users: {
type: Array,
reflectToAttribute: true
},
}
});
</script>
</dom-module>
<user-pages> is a thin wrapper around <iron-pages>. It works together with <user-selector> to update the content displayed in the main portion of the app.
<dom-module id="user-pages">
<template>
<style>
:host {
display: block;
box-sizing: border-box;
}
</style>
<iron-pages role="main" selected="{{incoming}}" attr-for-selected="name">
<template is="dom-repeat" items="{{users}}">
<user-summary
name="view{{index}}"
username="{{item.userName}}">
</user-summary>
</template>
<settings-view name="settings"></settings-view>
</iron-pages>
</template>
<script>
Polymer({
is: 'user-pages',
properties: {
users: Array,
incoming: {
type: String,
reflectToAttribute: true,
notify: true
},
}
});
</script>
</dom-module>
And, finally, my <my-app> element plumbs it all together.
<dom-module id="my-app">
<template>
<style>
:host {
display: block;
--app-primary-color: #4285f4;
--app-secondary-color: black;
}
app-header {
background-color: var(--app-primary-color);
color: #fff;
}
app-header paper-icon-button {
--paper-icon-button-ink-color: white;
}
.drawer-list {
margin: 0 20px;
}
.drawer-list a {
display: block;
padding: 0 16px;
line-height: 40px;
text-decoration: none;
color: var(--app-secondary-color);
}
.drawer-list a.iron-selected {
color: black;
font-weight: bold;
}
.drawer-list a.subroute {
padding-left: 32px;
}
</style>
<app-route
route="{{page}}"
data="{{routeData}}"
tail="{{subroute}}"></app-route>
<app-drawer-layout fullbleed>
<!-- Drawer content -->
<app-drawer>
<app-toolbar>My App</app-toolbar>
<start-app-button></start-app-button>
<user-provider users="{{retrievedUsers}}"></user-provider>
<user-selector incoming="{{page}}" users="{{retrievedUsers}}">
</user-selector>
</app-drawer>
<!-- Main content -->
<app-header-layout has-scrolling-region>
<app-header condenses reveals effects="waterfall">
<app-toolbar>
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
<div title>My App</div>
</app-toolbar>
</app-header>
<user-pages
incoming="{{page}}"
users="{{retrievedUsers}}">
</user-pages>
</app-header-layout>
</app-drawer-layout>
</template>
<script>
Polymer({
is: 'my-app',
properties: {
page: {
type: String,
reflectToAttribute: true
},
},
observers: [
'_routePageChanged(routeData.page)'
],
_routePageChanged: function(page) {
this.page = page || 'view1';
}
});
</script>
</dom-module>
Note in particular the way that <user-provider> is responsible for obtaining the list of users and exposing it just via a variable, allowing the other two elements to make obtain the data via basic databinding. That, along with the dom-repeat elements as immediate children of <iron-selector> and <iron-pages>, allowed this to be pretty straight forward once I started thinking of it in the Polymer way.
Related
I'm new to this framework and would love to see some useful and simple examples of notify and reflect to attribute properties being put to use.
Please keep the examples simple or add explanation for whatever code you provide.
Notify:
can be set to True|False. Let's say you have parent-element and child-element. Working example
parent-element.html:
<dom-module id="parent-element">
<template>
<child-element foo="{{test}}"></child-element>
</template>
<script>
Polymer({
is: "parent-element",
properties: {
test: {
value: "bar"
}
}
})
</script>
</dom-module>
As you can see, we have 1 property called test which is propagated to child element, where we can manipulate with it.
child-element.html:
<dom-module id="child-element">
<template>
<paper-input value="{{test}}"></paper-input>
</template>
<script>
Polymer({
is: 'child-element',
properties: {
test: {
}
},
});
</script>
</dom-module>
What is hapenning? In child element we defined test property and this property is binded to paper-input, which means, whenever we write something in paper-input, the property updates itself automatically . YEE that's awesome, we don't need to take care of updating property inside child-element, but HOW can parent know that property test has changed? Well, he can't.
And here comes notify: true. If you set it up, you don't have to care about notifying parent-element that somewhere inside of the child-element, test property has been changed.
Shortly, property test in parent-element and child-element will updates simultaneously
Reflect-to-attribute:
As name already says, when you set this to some property, it's value will be visible in attribute of host element. Better to show this on some example.
In Polymer we know, that setting some binding to attribute of some element needs $ sign.
<custom-elem foo-attribute$="[[someProperty]]"></custom-elem>
Well, this can't be used everywhere. Let's say, that we need foo-attribute inside custom-elem.
Becuase foo-attribute was declared as attribute and not property, it's value won't be visible inside of element. So we need something where some attribute will represent attribute and also property.
So some working example, with some real situation would be like:
<dom-module id='parent-element'>
<template>
<style>
child-elemet[foo='bar'] {background-color: green}
child-element[foo='foo'] {background-color: red}
</style>
<child-element foo="{{test}}"></child-element>
</template>
<script>
Polymer({
is: "parent-element",
properties: {
test: {
value: "bar"
}
}
})
</script>
</dom-module>
In this case, CSS won't work, because foo is property (not an attribute) and CSS is applied on attributes only.
To make it work, we have to add reflectToAttribute on foo property inside child-element.
<dom-module id='child-element'>
<template>
<div>[[foo]]</div>
</template>
<script>
Polymer({
is: "child-element",
properties: {
foo: {
reflectToAttribute: true
}
}
})
</script>
</dom-module>
After this, foo will become attribute and also property. At this moment, CSS will be applied on child-element and we will be able to work with value of foo inside child-element
From the docs: https://www.polymer-project.org/1.0/docs/devguide/data-binding
To bind to an attribute, add a dollar sign ($) after the attribute
name:
<div style$="color: {{myColor}};">
Two-way bindings ... property must be set to notify: true.
I am trying to use an iron-list (and iron-scroll-threshold) within a app-header-layout with has-scrolling-region.
I generated the basic app layout with the polymer-CLI.
If I do not use has-scrolling-region on the app-header-layout and use "document" for scroll-target on the iron-list it kinda works. But with this solution the scrollbar belongs to the window and does not slide beneath the header and I obviously cannot get the nice "waterfall" behaviour that is usually associated with these kinds of layouts.
Therefore, I use has-scrolling-region on the app-header-layout, but what is the right way to pass the corresponding scoller to the scroll-target property of the iron-list?
<!-- Main content -->
<app-header-layout has-scrolling-region id="layout">
<app-header condenses reveals effects="waterfall">
<app-toolbar>
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
<div title>Twiamo</div>
</app-toolbar>
</app-header>
<iron-pages role="main" selected="[[page]]" attr-for-selected="name" id="page">
<my-iron-list name="view1" scroll-target="[[_getScrollTarget()]]"></my-iron-list>
<my-view2 name="view2"></my-view2>
<my-view3 name="view3"></my-view3>
</iron-pages>
</app-header-layout>
I looked into the implementation of app-header-layout to find the right element. This expression actually yields me the right element and everything works fine.
_getScrollTarget: function() {
return this.$.layout.shadowRoot.querySelector("#contentContainer");
}
But there has to be a better, a right way? Grabbing into the shadow DOM of the app-header-layout is not exactly using "public interface"!
To complete the example, here my code for my-iron-list. My-iron-list wraps and iron-list, iron-scroll-theshold, and some dummy data provider stuff. The scroll-target on my-iron-list is just passed to the iron-list and iron-scroll-threshold within my-iron-list:
<dom-module id="my-iron-list">
<template>
<iron-list items="[]" as=item id="list" scroll-target="[[scrollTarget]]">
<template>
<div class="item">[[item]]</div>
</template>
</iron-list>
<iron-scroll-threshold
id="scrollTheshold"
lower-threshold="100"
on-lower-threshold="_loadMoreData"
scroll-target="[[scrollTarget]]">
</iron-scroll-threshold>
</template>
<script>
Polymer({
is: 'my-iron-list',
properties: {
page: {
type : Number,
value : 0
},
perPage: {
type : Number,
value : 100
},
scrollTarget: HTMLElement,
},
_pushPage: function() {
for (i = 0; i < this.perPage; i++) {
this.$.list.push('items', 'Entry number ' + (i+1+this.page*this.perPage));
}
},
_loadMoreData: function() {
this._pushPage();
this.page = this.page + 1;
this.$.scrollTheshold.clearTriggers();
}
});
</script>
</dom-module>
I have the same problem as you, for now the cleanest anwser I have was to use the app-header scrollTarget.
In your case move add an id to the app-header
<app-header condenses reveals effects="waterfall" id="header">
<app-toolbar>
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
<div title>Twiamo</div>
</app-toolbar>
</app-header>
and then instead of
_getScrollTarget: function() {
return this.$.layout.shadowRoot.querySelector("#contentContainer");
}
just use the scrollTarget property
_getScrollTarget: function() {
return this.$.header.scrollTarget;
}
If you found out a better way let me know.
Cheers,
I struggled with the same issue. While I was using iron-scroll-target-behavior instead of iron-scroll-threshold, I still needed to pass a scroll-target reference to an element inside a app-layout-header.
If has-scrolling-region is true, app-header-layout sets the scroll-target to be an internal div with an ID of #contentContainer. You can target this div and pass it as the scroll-target to your iron-list.
You would just need to alter the _getScrollTarget function inside your original code.
_getScrollTarget: function() {
return this.$.layout.$.contentContainer;
}
Hope it helps!
If anyone is coming here for an answer in 2017, I'm just letting you know that the same issue persists in Polymer 2.0.
I was able to overcome the issue by having the following code in my app shell (eg. PSK's my-app.html):
First, put an id attribute of 'layout' on your app-header-layout element.
Next, add this to your Polymer class (in your my-app.html equivalent):
static get properties() {
return {
scrollTarget: HTMLElement,
}
}
ready() {
super.ready();
this.scrollTarget = this.$.layout.shadowRoot.querySelector("#contentContainer");
}
Then, pass in the property to a scroll-target attribute on your lazy-loaded pages:
<my-page scroll-target="[[scrollTarget]]"></my-page>
Finally, in your lazy-loaded pages (eg. my-page):
<iron-list scroll-target="[[scrollTarget]]"></iron-list>
...
static get properties() {
return {
scrollTarget: HTMLElement,
}
}
This isn't an ideal solution, but it works.
I am struggling to understand the subtlety of reflectToAttribute on a Polymer elements property.
I have an pair of elements for transmitting values around the dom tree like iron-meta which I have called akc-meta and akc-meta-query. In my test fixture I am doing this
<test-fixture id="basic-test">
<template>
<template is="dom-bind" id=app>
<akc-meta key="[[key1]]" value="{{value1}}" id="meta1"></akc-meta>
<akc-meta-query key="[[key2]]" value="{{value2}}" id="meta2"></akc-meta-query>
<akc-meta-query key="[[key3]]" value="{{value3}}" id="meta3"></akc-meta-query>
<akc-meta key="[[key4]]" value="{{value4}}" id="meta4"></akc-meta>
</template>
</template>
</test-fixture>
and in my test suite I can set values like this
app.key1 = 'keya';
app.key2 = 'keya';
app.key3 = 'keya';
app.value1 = 'This is a multiple query test';
expect(app.value2).to.equal('This is a multiple query test');
expect(app.value3).to.equal('This is a multiple query test');
app.value1 = 'New Value';
expect(app.value2).to.equal('New Value');
expect(app.value3).to.equal('New Value');
where these elements transmit values under the hood between the elements when the keys are the same.
Neither of the elements use reflectToAttribute on any of the properties, although the value property of akc-meta-query does use notify:true
So what does reflectToAttribute actually do and why do you need it?
I have created a small example in this jsbin.
<style>
x-test {
display: block;
}
x-test[prop1="initialvalue1"] {
border: 5px solid yellow;
}
x-test[prop2="initialvalue2"] {
background: green;
}
x-test[prop1="newvalue1"] {
border: 5px solid black;
}
x-test[prop2="newvalue2"] {
background: red;
}
</style>
<dom-module id="x-test">
<template>
<div>{{prop1}}</div>
<div>{{prop2}}</div>
<button on-click="update1">Update Prop1</button>
<button on-click="update2">Update Prop2</button>
</template>
</dom-module>
<script>
HTMLImports.whenReady(function() {
Polymer({
is: 'x-test',
properties:{
prop1: {
type:String
},
prop2: {
type:String,
reflectToAttribute: true
},
},
update1: function(){
this.prop1 = "newvalue1";
},
update2: function(){
this.prop2 = "newvalue2";
}
});
});
</script>
<x-test prop1="initialvalue1" prop2="initialvalue2"></x-test>
The element here has two properties. prop1 is not reflected to the attribute, but prop2 is. Both are set to an initial value. If you open the developer tools, you will see something like this:
There is an update button to change either prop1 or prop2. If you click each button and update both properties, you will get this picture
As you can see, prop1 still has its old state. The state change has not been reflected back to the attribute. In contrast, prop2 has been reflected and has changed.
Now if you access either property through JavaScript, you will get the updated value. However, when you access the HTML attribute, in case of prop1 you will not.
A use case for this could be when you have CSS rules with attribute selectors as in my example. The selector x-test[prop1="newvalue1"] does not apply when prop1 is updated. On the other hand x-test[prop2="newvalue2"] does apply after the update of prop2, because it is configured to reflect to attribute.
I have iron add which i use for an accordian style page. Upon toggle of accordion i want to make the icon
icon="add" is a + sign and icon="remove" is a minus sign. The icon will signify if it is expanded or not.
<accordion-question>
<iron-icon icon="add"></iron-icon>
This is the question
</accordion-question>
<accordion-answer>
This is the answer which appears if you toggle
</accordion-answer>
I think that it'd be easier to just to set the attribute in js using Polymer(dom).setAttribute(attr, value) rather than mess around with computed bindings.
<dom-module is="test-element">
<template>
<iron-icon icon="add" on-click="_onClick"></iron-icon>
<iron-collapse>
<p>Collapse Content</p>
</iron-collapse>
</template>
</dom-module>
<script>
Polymer({
is: "test-element",
_onClick: function() {
var button= this.querySelector('iron-icon'),
collapse = this.querySelector('iron-collapse')
collapse.toggle()
if (collapse.opened) {
Polymer.dom(button).setAttribute('icon', 'remove')
} else if (!collapse.opened) {
Polymer.dom(button).setAttribute('icon', 'add')
}
}
})
</script>
The CSS solution is very appealing too though cause it simplifies the js logic.
Something like
<iron-icon icon="{{_getIcon(expanded)}}"></iron-icon>
and then
onToggle: function () {
//Maybe this is your click event
this.expanded = !this.expanded
},
_getIcon: function () {
if (this.expanded) {
return 'add';
}
return 'remove';
}
Edit: Improving the _getIcon function, as suggested by #ScottMiles
_getIcon: function(expanded) {
return expanded ? 'add' : 'remove;
}
I'm assuming accordion-question will have some sort of open/close class or attribute, which should make it as easy as:
<style>
accordion-question:not(.open) iron-icon[icon="add"],
accordion-question.open iron-icon[icon="remove"] {
display: inline-flex;
}
accordion-question:not(.open) iron-icon[icon="remove"],
accordion-question.open iron-icon[icon="add"] {
display: none;
}
</style>
<!-- Shows "add" icon -->
<accordion-question>
<iron-icon icon="add"></iron-icon>
<iron-icon icon="remove"></iron-icon>
This is the question.
</accordion-question>
<!-- Shows "remove" icon -->
<accordion-question class="open">
<iron-icon icon="add"></iron-icon>
<iron-icon icon="remove"></iron-icon>
This is the question.
</accordion-question>
I am trying to get my head around Polymer. Right now I have successfully created a custom news element (my-newsbite) and can populate and display attribute data from the page calling the custom element.
The next step is to create a second custom element (my-newsbite-list), that takes multiple news items in the form of a json object from attributes in the calling page and displays a list of the my-newsbite custom elements.
For some reason I am unable to get it to work.
My test index.php page calls the my-newsbite-list like this...
<div style="width:200px;">
<my-newsbite-list news="[{title: 'title here',date:'12 July 2014',content:'content here',url:'http://www.frfrf.com'}]">
</my-newsbite-list>
</div>
The my-newsbite-list custom element looks like this...
<link rel="import" href="../components/bower_components/polymer/polymer.html">
<polymer-element name="my-newsbite-list" attributes="news" >
<template >
<template repeat="{{item in news}}">
<my-newsbite title="item.title" date="item.date" content="item.content" url="item.url">
</my-newsbite>
</template>
</template>
<script>
Polymer('my-newsbite-list', {
created: function() {
this.news = []; // Initialize and hint type to an object.
}
});
</script>
</polymer-element>
And finally my custom element my-newsbite looks like this...
<link rel="import" href="../components/bower_components/polymer/polymer.html">
<polymer-element name="my-newsbite" attributes="title date content url width">
<template>
<div class="my-newsbite-title">
{{title}}
<div class="my-newsbite-date">{{date}}</div>
</div>
<div class="my-newsbite-content">{{content}}...</div>
<div class="my-newsbite-url">more...</div>
<style>
:host {
/* Note: by default elements are always display:inline. */
display: block;
background-color:#FFFFFF;
font-fam
}
.my-newsbite-title {
/* Note: by default elements are always display:inline. */
display: block;
background-color:#06F;
color:white;
padding:3px;
}
.my-newsbite-content{
padding:3px;
}
.my-newsbite-date{
padding:3px;
font-size:8pt;
text-align:right;
float:right;
}
.my-newsbite-url{
padding:3px;
}
</style>
</template>
<script>
Polymer('my-newsbite', {
title: "****",
date: "****",
content: "****...",
url: "http://****.com",
});
</script>
</polymer-element>
The end result of all this is a blank rendered index page.
Can anyone see why the json object is not rendered with in the my-newsbite-list custom element ?
Thanks in advance.
I had already tried ebidel's suggestion but that didn't solve it.
Scott Miles is correct...
Polymer is very fussy about the JSON format passed in the attribute. (I don't know how to give you credit for the answer from a comment.)
So, in the index.php page the code looks like this (Note the double quotes around both the key names and the values)...
<my-newsbite-list news='[{"title":"sdfg","date":"12 July 2014","content":"sdfg sdfg sdfg sdfg sdfg sdfg sdf","url":"http://www.frfrf.com"}]'>
</my-newsbite-list>
The custom element Polymer javascript looks like this...
Polymer('my-newsbite-list', {
news: [],
ready: function(){
}
});
Thanks for all the suggestions.
The problem seems to lie in the way the json object in the calling attribute is received by the custom element. Adding the following code to the Polymer ready function fixes the issue.
Polymer('my-newsbite-list', {
ready: function(){
this.news = eval(this.news) || [];
}
});
This solution does work, but the correct method is shown in the answer above.