nesting elements and dynamic content inside dom-repeat - Polymer 1.0 - polymer

I have a <parent> element, a <tabs> element inside it with an arbitrary number of tabs (purely for hiding/showing logic in the UI), and a <child> element inside each <tab>. Right now I have the following working:
<!-- inside parent.html: -->
<tabs></tabs>
<!-- inside tabs.html: -->
<template is="dom-repeat" items="{{tabs}}" as="tab" index-as="item_no">
<section>
<child id="element-{{tab.index}}"></child>
</section>
</template>
Only <parent> knows how many instances of <child> there needs to be (<tabs> merely receives an array from <parent> and iterates over it.
Is there a way to not hard-code <child> inside the local DOM of <tabs>? Thinking of using <content> and light DOM but no idea where to even start. Would it be a promising route to take?
Desired state:
<!-- inside parent.html: -->
<tabs>
// somehow specify base content to be rendered in each tab
</tabs>
<!-- inside tabs.html: -->
<template is="dom-repeat" items="{{tabs}}" as="tab" index-as="item_no">
<section>
// somehow inject stuff from parent element, perhaps via <content>?
</section>
</template>

This is my interpretation of your question, so I am not really sure if it will be OK with you. If I misunderstood you, please drop a comment and I will gladly update my answer.
I have come up with a simple element composition, requiring no dom-repeat or manual template stamping. The solution consists of two custom elements, namely my-parent and my-child.
The definitions for both custom elements are the following:
<dom-module id="my-parent">
<template>
<tabs>
<content></content>
</tabs>
</template>
<script>
Polymer({
is: 'my-parent',
});
</script>
</dom-module>
<dom-module id="my-child">
<template>
<section>
<content></content>
</section>
</template>
<script>
Polymer({
is: 'my-child',
});
</script>
</dom-module>
And the proposed usage of them is the following:
<my-parent>
<my-child>First tab</my-child>
<my-child>Second tab</my-child>
<my-child>Third tab</my-child>
</my-parent>
Online demo: http://jsbin.com/hibuzafapu/1/edit?html,output
The resulting computed HTML code looks something like this:
<my-parent>
<tabs>
<my-child>
<section>
First tab
</section>
</my-child>
<my-child>
<section>
Second tab
</section>
</my-child>
<my-child>
<section>
Third tab
</section>
</my-child>
</tabs>
</my-parent>
If I understood you correctly, then only the <my-child> tag wrapping the <section> tag is redundant. Currently the aforementioned tag does nothing and is just a block-level element that wraps everything (just like a div). If this bothers you, then you can actually omit the <section> tag and put all the styling directly on the <my-child> tag.
In this case, the resulting computed HTML would look something like this:
<my-parent>
<tabs>
<my-child>
First tab
</my-child>
<my-child>
Second tab
</my-child>
<my-child>
Third tab
</my-child>
</tabs>
</my-parent>
UPDATE
In order to add some dynamics to the solution (adding/removing tabs), you have two options: use dom-repeat and stamp the items in light DOM, or push the items array into the my-parent element and use dom-repeat there. Both options are very similar to implement and don't have much difference in the way they work.
Option A: stamping in light DOM:
Definitions for both custom elements remain unchanged, the only difference is how you use them. Instead of hardcoding the light DOM, you make it more dynamic.
<dom-module is="tmp-element">
<template>
<my-parent>
<template is="dom-repeat" items="[[myItems]]">
<my-child>[[item.content]]</my-child>
</template>
</my-parent>
</template>
<script>
Polymer({
is: 'tmp-element',
ready: function() {
this.myItems = [
{ content: "First tab" },
{ content: "Second tab" },
{ content: "Third tab" },
],
};
});
</script>
</dom-module>
<tmp-element></tmp-element>
The tmp-element is used purely to create a binding scope and to feed the data into the dom-repeat.
Live demo: http://jsbin.com/gafisuwege/1/edit?html,console,outputenter link description here
Option B: stamping inside parent:
In this option, the parent needs to have an additional property, in which will we will supply the array of items.
The new version of the my-parent element is the following:
<dom-module id="my-parent">
<template>
<tabs>
<template is="dom-repeat" items="[[items]]">
<my-child>[[item.content]]</my-child>
</template>
</tabs>
</template>
<script>
Polymer({
is: 'my-parent',
properties: {
items: Array,
},
});
</script>
</dom-module>
And the usage is:
<dom-module is="tmp-element">
<template>
<my-parent items="[[myItems]]"></my-parent>
</template>
<script>
Polymer({
is: 'tmp-element',
ready: function() {
this.myItems = [
{ content: "First tab" },
{ content: "Second tab" },
{ content: "Third tab" },
];
},
});
</script>
</dom-module>
<tmp-element></tmp-element>
Here, I have also used a tmp-element (a different one than before) to feed the my-parent its' data.
Live demo: http://jsbin.com/kiwidaqeki/1/edit?html,console,output

Related

how to dynamically append an element to dom-if in Polymer?

My goal is to append an element to existing dom-if dynamically. Problem is that after appending I can see appended element in the DOM three but it never reacts on condition and stays always hidden.
<template>
<template id="domif" is="dom-if" if="[[condition]]" restamp></template>
</template>
ready() {
var el = document.createElement("input");
Polymer.dom(this.$.domif).appendChild(el);
Polymer.dom.flush();
}
Exploring DOM with hardcoded dom-if and input shows that <input /> element is actually not a child of dom-if but lives next to it..
<template>
<template is="dom-if" if="[[condition]]" restamp>
<input />
</template>
</template>
That gave me a clue that I probably should append my element next to dom-if... But now the biggest question is how to say to dom-if that appended element should be rendered if condition is satisfied. Any ideas?
How about adding a span in your dom-if and appending it to that span?
Update after some comments : We need to use this.async for the item to be found. Using the ready-event only works when the condition is true initially. So you could append the element in a conditionChanged-observer - this is a working example :
<dom-module id='my-element1'>
<template>
<template is="dom-if" if="[[condition]]" restamp>
<span id="appendHere"></span>
</template>
</template>
</dom-module>
<script>
Polymer({
is: 'my-element1',
properties: {
condition: {
type: Boolean,
observer: "_conditionChanged"
}
},
_conditionChanged: function(newVal) {
if (newVal) {
this.async(function() {
var el = document.createElement("input");
Polymer.dom(this.$$("#appendHere")).appendChild(el);
Polymer.dom.flush();
});
}
}
});
</script>
Try it here : http://plnkr.co/edit/1IIeM3gSjHIIZ5xpZKa1?p=preview .
A side-effect of using dom-if in this case is that after setting the condition to false, the element disappears completely and gets added on the next condition-change again. So every change before setting the condition to false gets lost. You could work around it by putting the added element somewhere hidden when the condition changes and getting it back later, but I don't think this is a good idea, if the following is an alternative :
The Polymer-team recommends using dom-if only if there is no other way, like hiding the element. So, if it is possible you also could do something like this (condition has to be true to hide the element) :
<dom-module id='my-element1'>
<template>
<span id="appendHere" hidden$="[[condition]]"></span>
</template>
</dom-module>
<script>
Polymer({
is: 'my-element1',
properties: {
condition: Boolean
},
ready: function() {
var el = document.createElement("input");
Polymer.dom(this.$.appendHere).appendChild(el);
Polymer.dom.flush();
}
});
</script>
Try it here :
http://plnkr.co/edit/mCtwqmqtCPaLOUveOqWS?p=preview
The template element itself will not be added to the DOM, this is the reason you can't access it using querySelector or getElementXxx

Polymer 1.0: Clickable item in dom-repeat to e.g. iron-page which contains further info (contact list)

I'm setting up a contact list in Polymer 1.0. When the user clicks on a name, there should be a (animated) page opened for further details. All of these data elements are pulled from an external .json file.
Two questions for this approach..:
1) where to begin? How do I wrap, for example, an iron-page or neon-animated-page around my current setup (which is searchable, which is also the -temporary- reason it's a dom-repeat instead of an iron-list):
<template id="resultlist" is="dom-repeat" items="{{data}}" filter="contactFilter">
<paper-item>
<paper-item-body two-line>
<div>{{item.name}}</div>
<div secondary>{{item.number}}</div>
</paper-item-body>
</paper-item>
</template>
2) For quick try-out with binding options I've created an paper-dialog (instead of an page behaviour) which displays further data for the chosen person... On top of that paper-dialog should the chosen name being displayed. But I only get the first name of the array in my .json file. How can I setup the code to display the {{item.name}} of the chosen item?
Ps. I'm aware of the contacts-app from Rob Dodson (https://github.com/robdodson/contacts-app), but I can't figure out how it should be done in Polymer 1.0.
Update 27.10.2015
After Hugo's answer I'm not able to get the solution to work in an dom-module structure.
Sorry for misunderstanding, but I can't figure out where I'm wrong.
Having to following:
phonebook.html, which acts like an index
...
<body unresolved>
<template is="dom-bind" id="application">
<neon-animated-pages selected="[[selected]]" entry-animation="fade-in-animation" exit-animation="fade-out-animation">
<contact-list></contact-list>
<contact-details></contact-details>
</neon-animated-pages>
</template>
<script>
var application = document.querySelector('#application');
application.selected = 0;
document.addEventListener('show-details', function() {
application.selected = 1;
});
document.addEventListener('show-list', function() {
application.selected = 0;
});
</script>
</body>
DOM-module contact-list.html, the list it self.
<dom-module id="contact-list">
<template>
<style include="phonebook-styles"></style>
<iron-ajax url="../data/data.json" handle-as="json" last-response="{{data}}" auto></iron-ajax>
<div class="container">
<h3>Contactlist:</h3>
<div class="template-container">
<template is="dom-repeat" id="templateUsers" items="{{data}}">
<paper-item on-tap="showDetails">
<paper-item-body two-line>
<div>{{item.name}}</div>
<div secondary>{{item.phonenumber}}</div>
</paper-item-body>
<div class="item-details-link">
<iron-icon icon="account-circle"></iron-icon>
</div>
</paper-item>
</template>
</div>
</div>
</template>
<script>
Polymer({
is: 'contact-list',
properties: {
selectedContact:{
type:Object,
value:function(){
return null;
}
}
},
showDetails: function(ev) {
var data = this.$.templateUsers.itemForElement(ev.target);
//alert(JSON.stringify(data)) // works with data chosen data selection...
this.selectedContact = data;
this.fire('show-details', this.selectedContact);
}
});
</script>
</dom-module>
DOM-module contact-details.html, the details-list.
<dom-module id="contact-details">
<template>
<!-- Do I need to declare the .json in my details module? -->
<iron-ajax url="../data/data.json" handle-as="json" last-response="{{data}}" auto></iron-ajax>
<paper-icon-button icon="arrow-back" on-tap="showList"></paper-icon-button>
<h3>Contact details</h3>
<template is="dom-repeat" items="{{data}}">
<div>{{selectedContact.name}}</div>
</template>
</template>
<script>
Polymer({
is: 'contact-details',
showList: function() {
this.fire('show-list');
}
});
</script>
</dom-module>
Everything, like the transitions, work. The chosen contact is also displayed in an alertbox (commented out in contact-list.html), but isn't forwarded to the contact-details.html page.
There are multiple steps to implement the solution:
Setup the neon animated pages ( one page would be the contact list, the other page would be the details )
Display the list of contacts ( you already have this one )
Add a "selectedContact" property to your element
Add a tap/click handler to the list items element and inside the handler set the selectedContact. You need to get the contact item from the DOM element clicked. ( Check an example here : http://jsbin.com/lofarabare/6/edit )
You can bind the contact details page elements to the selectedContact properties, e.g {{selectedContact.name}}
Inside the handler also Change the neon animated pages selected property to have it display the animation to the other page.
-- Extra feedback
I checked the way you handle events, feedback below:
Give the elements some id so you can add the event listener directly to them (e.g application.$.myContactList.addEventListener('show-detail',function(ev){...})
The way you fire the event from the contact-list is correct, however you are not reading the event data inside the event listener for the 'show-detail' event. The event listener receives the event as argument "ev". You can get the event data using ev.detail
With the event data (the selected contact) you can update your contact details component. Give it some id like 'details' and just update the 'selectedContact' property. **You need to declare the selectedContact in the details component, right now you don't have it there **

Tap listener for polymer iron-list item?

I have a custom element that utilizes iron-list to display an array of objects. Each item is generated via a template as follows:
<iron-list id="projectList" items="[[projects]]" indexAs="_id" as="projLI" class="layout flex">
<template>
<div>
<paper-material id="itemShadow" animated elevation="1">
<div class="item layout horizontal" onmouseover="hoverOver(this)" onmouseout="hoverOut(this)">
<!-- I use a paper-menu-button to display a list of available actions here -->
<!-- list item object content here such as: [[projLI.desc]] etc. -->
</div>
</paper-material>
</div>
</template>
</iron-list>
What is the best polymer-friendly approach to detect both a tap event on the iron-list item itself (ideally knowing which item was actually tapped via projLI._id), yet also be able to handle the internal paper-menu-button tap events in a different way?
I've eye-balled polymer 1.0's new event listeners (https://www.polymer-project.org/1.0/docs/devguide/events.html), as a possible approach, attempting to listen for different element tap events (as shown in example 1 on that page), but I'm not sure if that will work here. I've also considered possibly using iron-selector somehow around iron-list? Is that doable? I'm not sure that will work either, given that iron-selector would only have one child (i.e. the iron-list element and not it's templated children).
I feel like I'm missing a really easy way to accomplish this. Can someone please show me the light?
Follow the model outlined on lines 154 and 184 of this demo.
https://github.com/PolymerElements/iron-list/blob/master/demo/collapse.html
my-element.html
<iron-list items="[[items]]">
<template>
<my-list-item on-tap="_toggleMe"></my-list-item>
</template>
</iron-list>
...
_toggleMe: function(e) {
console.log(e.model.index);
}
The key is to place the event and listener method (toggleMe() in this case) inside the <template> of the iron-list. This allows the iron-list to register the array index.
I do this by encoding an array index in a list element id, then pulling the id out of a list item event target. Here is an example Polymer element that does this.
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../bower_components/iron-list/iron-list.html">
<dom-module id="list-example">
<style>
:host {
display: block;
}
#list-example {
height: 100px;
}
</style>
<template>
<paper-material animated elevation="1">
<iron-list id="list-example" items="[[data]]">
<template>
<div id="{{index2id(item.index)}}" on-mouseover="onMouseOverItem">{{item.name}}</div>
</template>
</iron-list>
</paper-material>
</template>
</dom-module>
<script>
(function () {
Polymer({
is: 'list-example',
ready: function() {
for(var i = 0; i < this.data.length; i++) {
this.data[i].index = i;
}
},
index2id: function(index) {
return "_" + index;
},
id2index: function(id) {
return Number(id.substr(1));
},
onMouseOverItem: function(e) {
console.log('on-mouseover list item:', this.data[this.id2index(e.target.getAttribute('id'))]);
},
properties: {
data: {
type: Array,
value: [{name: 'A'}, {name: 'B'}, {name: 'C'},
{name: 'D'}, {name: 'E'}, {name: 'F'},
{name: 'G'}, {name: 'H'}, {name: 'I'}]
}
}
});
})();
</script>
I was having a similar issue and solved my problem using <array-selector> as follows:
<iron-list items="{{myarray}}" as="ref">
<template>
<div>
<paper-checkbox on-tap="toggleSelection"></paper-checkbox>
<span>{{ref.name}}</span>
</div>
</template>
</iron-list>
<array-selector id="arrsel" items="{{myarray}}"
selected="{{selectedName}}" toggle></array-selector>
and myarray is an array of objects:
var myarray = [{name: "Alice"}, {name: "Ben"}, ...]
and the function toggleSelection is defined as follows:
toggleSelection: function(e) {
console.log ("Selected index is " + e.model.index);
console.log ("Selected name is " + e.model.ref);
this.$.arrsel.select (e.model.ref);
console.log ("Current selection: ", this.selectedName);
}
The field name ref after e.model.__ is the value of the as attribute of iron-list.
WARNING: The variable e.model is not officially documented on the Polymer 1.0 iron-list doc (https://elements.polymer-project.org/elements/iron-list), however I discovered it during my debugging session. I am assuming that e.model is a public property (the coding style of Polymer uses underscore prefix for private property such as: _scroll_Position) and it is not a candidate for deprecation.
I just solved my issue of here https://groups.google.com/forum/#!topic/polymer-dev/r9IsUKVnLVM. Reading this documentation https://www.polymer-project.org/1.0/docs/devguide/events.html.
I hope it helps you!
Wrap your iron-list with an iron-selector - this allows you get to get the row selected / tapped.
(NB: you might need to remove your custom indexAs="_id" attribute to get the right row index)
<iron-selector attr-for-selected="index" on-tap="_itemSelected">
<iron-list id="projectList" items="[[projects]]" as="projLI" class="fit">
<template>
<div class="layout horizontal center" id="{{index}}">
<!-- your row content here -->
</div>
</template>
</iron-list>
</iron-selector>
Polymer method for row item selected:
_itemSelected: function (e) {
console.log(e.target.id); // selected iron-list row index
}

How do I put the selected core-menu item into a custom polymer element?

I'm trying to encapsulate a paper-dropdown in a paper-button. To do this, I made a custom element, paper-dropdown-holder:
<polymer-element name="paper-dropdown-holder" extends="paper-button" relative on-tap="{{toggle}}">
<template>
<shadow></shadow>
<content></content>
</template>
<script>
Polymer({
toggle: function() {
if (!this.dropdown) {
this.dropdown = this.querySelector('paper-dropdown');
}
this.dropdown && this.dropdown.toggle();
}
});
</script>
</polymer-element>
and I'm using it in the page like:
<paper-dropdown-holder raised tabindex="0" class="unpadded">
<paper-dropdown class="dropdown" flex>
<core-menu class="menu" selected="0">
<paper-item>Writing</paper-item>
<paper-item>Blog</paper-item>
<paper-item>Art</paper-item>
</core-menu>
</paper-dropdown>
</paper-dropdown-holder>
My problem is deciphering The documentation to figure out how to automatically put the text of the currently-selected menu item into the paper-dropdown-holder.
My first attempt was to just use a standard paper-dropdown-menu, but I couldn't as easily style that like a paper-button. Is there any way to do this that's not (for lack of a better term) hacky? I'd love if the answer would keep to the Polymer philosophies.
Bonus challenge: How do I set default text like "choose section"?
One of awesome things of Polymer is it's open source... that said you could learn how to implement new element based on already existing elements....
If you have a look at paper-dropdown-menu source you could easily make something like it but with paper-button as a "control".
So
The new element should extend core-dropdown-base not
paper-button.
To make that element logically working you could do that with
some help of paper-dropdown-menu by binding (core-overlay-open,
core-activate, core-select) events to the according handlers.
(the actual binding happens in core-dropdown-base in dropdown
getter which called inside attached event listener.
To put them together:
<polymer-element name="paper-dropdown-holder" extends="core-dropdown-base" relative>
<template>
<div>
<paper-button raised on-tap="{{toggle}}">{{selectedItemLabel || label}}</paper-button>
<content></content>
</div>
</template>
<script>
Polymer('paper-dropdown-holder', {
publish: {
label: 'Select an item',
},
selectedItemLabel: '',
overlayListeners: {
'core-overlay-open': 'openAction',
'core-activate': 'activateAction',
'core-select': 'selectAction'
},
activateAction: function(e) {
this.opened = false;
},
selectAction: function(e) {
var detail = e.detail;
if (detail.isSelected) {
this.selectedItemLabel = detail.item.label || detail.item.textContent;
} else {
this.selectedItemLabel = '';
}
}
});
</script>
</polymer-element>
Demo.

Recommendations for managing multiple instances of the same polymer element in a page?

I have a general question. One of the major benefits of building a new polymer element is that it can be used like a native HTML element in a page. So, depending on the element that you build, it's logical that you would be able to add multiple instances of that element in a page.
Say I build a simple task list polymer element that has multiple views. A simple view that just lists the task names in a list and a detailed view that list the tasks and many other properties of the task in a list.
Then I add the element to my page multiple times. Maybe I want one instance of the element to list tasks related to Home and another to list tasks related to Work. But I want to send a link to someone with the Home task list opened in the simple view and the Work task list opened in detailed view. Or maybe I want the Home task list opened in edit mode and the Work task list opened in view mode.
How would you build the element so that you can change attributes/settings to more then one of these elements on a page?
The beauty of polymer is that you can change your component view by just adding / changing attributes to it.
Create custom tags and provide specific attributes depending on your requirement (HOME / WORK profile), and change your view accordingly.
Example:
Step 1: Create task container
<polymer-element name="task-list" noscript>
<template>
<h3>Tasklist</h3>
<core-menu id="tasks">
<content></content>
</core-menu>
</template>
</polymer-element>
Step2: Create task component
<polymer-element name="add-task" attributes="label detail">
<template>
<div id="task">
<input type="checkbox" id="tick" on-click="{{lineThrough}}" /> {{label}}
<div style="color:#999;margin: 5px 25px;">
{{detail}}
</div>
</div>
</template>
<script>
Polymer('add-task', {
lineThrough: function() {
this.$.task.style.textDecoration = this.$.tick.checked ? 'line-through': 'initial';
}
});
</script>
</polymer-element>
And now using above components, you can create your basic task list:
<task-list>
<add-task label="Learn Polymer" detail="http://www.polymer-project.org/"></add-task>
<add-task label="Build something great" detail="create polymer element"></add-task>
</task-list>
Screenshot
Now, To have control over changing task view (list / detailed / editable). Just add 2 attributes to task-list component. To control child view add-task from parent task-list element, you need to publish properties of your child element.
Your child component should be:
<polymer-element name="add-task" attributes="label detail">
<template>
<div id="task">
<template if="{{isEditable}}">
<input value="{{label}}" />
</template>
<template if="{{!isEditable}}">
<input type="checkbox" id="tick" on-click="{{lineThrough}}" /> {{label}}
</template>
<template if="{{isDetailed}}">
<div style="color:#999;margin: 5px 25px;">
{{detail}}
</div>
</template>
</div>
</template>
<script>
Polymer('add-task', {
publish: {
isDetailed: false,
isEditable: false
},
lineThrough: function() {
this.$.task.style.textDecoration = this.$.tick.checked ? 'line-through': 'initial';
}
});
</script>
</polymer-element>
Parent component with required attributes
<polymer-element name="task-list" attributes="editable detailed">
<template>
<h3>Tasklist</h3>
<core-menu flex id="tasks">
<content></content>
</core-menu>
</template>
<script>
Polymer('task-list', {
editable: false,
detailed: false,
domReady: function() {
var items = this.$.tasks.items;
for(var i = 0; i < items.length; i++) {
items[i].isDetailed = this.detailed;
items[i].isEditable = this.editable;
}
}
});
</script>
</polymer-element>
That's it, now you can control your task view by specifying required attributes to your parent component.
<task-list detailed editable>
<add-task label="Learn Polymer" detail="http://www.polymer-project.org/"></add-task>
<add-task label="Build something great" detail="create polymer element"></add-task>
</task-list>
Screenshots
With detailed and editable attributes
Without detailed and editable attributes