Iterating through polymer content insertion points - polymer

What I'd like is for the following html:
<father name = "John">
<son name = "Bob">likes lollipops</son>
<son name = "David">likes computers</son>
</father>
to generate:
John has two sons, Bob and David:
Bob: likes lollipops
David: likes computers
Now, assume I've correctly written the "son" polymer tag, and it renders a block div with "Bob: likes lolipops" as the contents. this I can get working easily, so that I have something like:
John
Bob: likes lolipops
David: likes computers
However, to get that top line, I'm really not sure what the correct approach is, basically I'd like something like this:
<polymer-element name = "father" attributes = "name">
<template>
<style>...
</style>
<div layout vertical>
{{name}} has ???{count}??? sons, ???{{ iterate son nodes, print out name attribute for each }}???<br/>
<!-- I dont know how ot access JS here, so not sure how to turn all child nodes into a children array -->
<content select = "son"></content>
</div>
</template>
<script>
Polymer({});
</script>
</polymer-element>

There are probably a few ways of doing this, but here's one approach. (Full version on JSBin).
<polymer-element name="son-element" attributes="name" noscript>
<template>
<p>{{name}}: <content></content></p>
</template>
</polymer-element>
<polymer-element name="father-element" attributes="name">
<template>
<p>{{name}} has {{count}} sons, {{sons}}:</p>
<p><content id="content"></content></p>
</template>
<script>
Polymer({
count: 0,
sons: '',
domReady: function() {
var names = [].map.call(this.children, function(sonElement) {
return sonElement.name;
});
this.count = names.length;
this.sons = names.join(' and ');
}
});
</script>
</polymer-element>
<father-element name="John">
<son-element name="Bob">likes lollipops</son-element>
<son-element name="David">likes computers</son-element>
</father-element>
The key piece is hooking into the domReady lifecycle method of <father-element>, and inspecting its, erm, children. Since we know they'll all be <son-element>s, we just blindly look for their name attributes and map them to an array, but if you wanted you could take some steps to only query for the <son-element>s.
(Oh, and Polymer elements all need to have a - in their names, so you'd need to go with <father-element> (or the like) instead of <father>.)

This is my solution, it is based on Jeff Posnick's solution but with Scott Miles suggestion to listen for add- remove- events.
<polymer-element name="baby-element" attributes="name">
<template>
<p>{{name}}: <content></content> <input type="button" value="Kill Baby" on-click="{{killBaby}}"/></p>
</template>
<script>
Polymer("baby-element", {
attached: function(){
this.fire('baby-added');
},
killBaby: function(){
this.fire('baby-removed');
// this.remove();
},
detached: function() {
this.fire('baby-removed');
}
});
</script>
</polymer-element>
<polymer-element name="parent-element" attributes="name">
<template>
<p>{{name}} has {{count}} babys, {{babys}}:</p>
<p><content id="content"></content></p>
</template>
<script>
Polymer({
count: 0,
babys: '',
domReady: function() {
this.recalcBabys();
this.addEventListener('baby-added', this.babyAdded);
this.addEventListener('baby-removed', this.babyRemoved);
},
babyRemoved: function(e) {
e.target.remove();
this.recalcBabys();
},
babyAdded: function(e) {
this.recalcBabys();
},
recalcBabys: function(){
var names = [].map.call(this.children, function(babyElement) {
return babyElement.name;
});
this.count = names.length;
this.babys = names.join(' and ');
}
});
</script>
</polymer-element>
<h1>It's aparent baby thing:</h1>
<input type=button value="make baby" id="makeBaby"></input>
<input type=input placeholder="baby name" id="babyName"></input>
<input type=input placeholder="baby info" id="babyInfo"></input>
<parent-element id="parent" name="Slim Sim">
<baby-element name="Curie">has X</baby-element>
<baby-element name="Korbut">volts like no other!</baby-element>
<baby-element name="Pavlov">has dogs!</baby-element>
<baby-element name="Schrodinger">... maybe has a cat?</baby-element>
</parent-element>
<script>
document.getElementById('makeBaby').addEventListener('click', function(){
var name = document.getElementById('babyName');
var info = document.getElementById('babyInfo');
var parent = document.getElementById('parent');
var baby = document.createElement("baby-element");
var inner = document.createTextNode(info.value);
baby.appendChild(inner);
baby.setAttribute("name",name.value);
parent.appendChild(baby);
});
</script>
I did not manage to get the parent-element to recalculateBabys if the baby is removed from outside, with for instance document.getElementsByTagName('baby-element')[0].remove().
I would have liked to (on the baby-element) only have the attached and detached methods fire the baby-added and baby-removed events, and the killBaby-funktion to only do 'this.remove()'.
But I could never get the Parent to hear all those events...
Please feel free to scrutinize this code and tell me any errors, I am kind of new to Polymer, but I still hop I can contribute!

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

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.

How to bind {{items | sum }} outside a template repeat?

<div id="order">
<template repeat="{{items as item}}">
<p data-id="{{item.id}}" data-qty="{{item.qty}}" data-price="{{item.price}}">
<span>- {{item.id}}</span>
<span> x{{item.qty}}</span>
</p>
</template>
<p>- Total {{total}}€</p>
</div>
Tried itemsChanged first to update the total but that did not work because the observer does not look at the property items[id].qty
The documentation does mention a more specific observer but can not use it when items is a array.
{{items | sum }} fails too because it only updates one time at start up.
Last option is
var order = this.$.order
order.onMutation(order, this.sum)
But then polymer crashes without a error message. I just see a blank screen when I put it in ready:function(){...}
I would say it's not doable right now. or maybe with some crazy hack.
let me suggest that solution:
I have created simple polymer and use power of Computed properties
<polymer-element name="x-repeat">
<template>
<template repeat="{{items as item}}">
<div>{{item.q}}</div>
</template>
<div>Total is {{total}}</div>
</template>
<script>
Polymer({
items : [],
created : function () {
this.items = [{q:1}, {q:2}];
},
computed: { // NOTE: computed set
total: 'items | sum'
},
sum : function (items) { // NOTE : defined 'pipeline' function
var total = 0;
items.forEach(function (i) {
total += i.q;
});
return total;
}
});
</script>
</polymer-element>
Hope that helps!

How to create dynamically polymer element\simple tag

I have a polymer-element like this:
<polymer-element name="x-block" attributes="data">
<template>
<div class="block-wrapper">
<div class="plus-button"on-click="{{showMdl}}">+</div>
<div hidden?="{{!showModal}}" id="modal">
Modal
</div>
<content select="header"></content>
</div>
</template>
/*Polymer */
<script>
Polymer({
ready: function(){
this.showModal = false;
},
showMdl: function(e,detail,sender){
this.showModal = true;
this.$.modal.style.top = e.layerY+'px';
this.$.modal.style.left = e.layerX+'px';
var newElement = document.createElement('div')
newElement.innerHTML = 'dynamicllyElement';
newElement.setAttribute('on-click','{{clickOnDynamicllyElement}}');
this.$.modal.appendChild(newElement);
},
clickOnDynamicllyElement:function(){
console.log('1111')
}
});
</script>
</polymer-element>
clickOnDynamicllyElement does not work.
You can use the undoc'd injectBoundHTML()
Example:
this.injectBoundHTML('<div on-click="{{clickOnDynamicllyElement}}">dynamicllyElement</div>', this.$.modal);
Disclaimer - this has been answered by the Polymer team elsewhere on SO
From the Polymer docs
https://www.polymer-project.org/docs/polymer/polymer.html#imperativeregister
Registering imperatively
Elements can be registered in pure JavaScript like this:
<script>
Polymer('name-tag', {nameColor: 'red'});
var el = document.createElement('div');
el.innerHTML = '\
<polymer-element name="name-tag" attributes="name">\
<template>\
Hello <span style="color:{{nameColor}}">{{name}}</span>\
</template>\
</polymer-element>';
// The custom elements polyfill can't see the <polymer-element>
// unless you put it in the DOM.
document.body.appendChild(el);
</script>
You need to add the to the document so that the Custom Elements polyfill picks it up.
Important: Since the Polymer call here is outside the , it must include the tag name argument.

Polymer: reverting/ordering items in repeat without touching array order

This seems a trivial thing but I'm unable to find it:
What if I want to reverse the order of my items in a repeat, without actually touching the order of the array, like in:
<template repeat="{{layer in layers}}">
<div>{{layer.name}}</div>
</template>
where layers is an array of objects.
I've tried applying a filter and then working with a copy of the array, like in:
<template repeat="{{layer in layers | reverse}}">
<div>{{layer.name}}</div>
</template>
...
reverse: function(arr){
return _(arr).reverse();
}
but that results in some observers failing since they're looking at the copy instead of the original objects. I don't want to apply a sort to my original array since other parts of the code depend on that order.
Anyone knows of an option where just the order of display in the DOM is affected?
I think you need to do something like this
<template repeat="{{layer in temp_array}}">
<div>{{layer.name}}</div>
</template>
<script>
Polymer('el-name',{
ready: function(){
this.temp_array =[];
this.temp_array = layers.reverse();
}
}
);
</script>
if your layers is empty when ready called, use change listener
<script>
Polymer('el-name',{
ready: function(){
this.temp_array =[];
},
layersChanged: function(oldValue, newValue){
if(newValue.length != 0)
this.temp_array = newValue.reverse();
}
}
);
</script>
Hope it help for you
If it is possible to put the repeated elements in a vertical/horizontal layout, then reverse might do the trick (see layout documentation):
<div vertical layout reverse?="{{ isReversed }}">
<template repeat="{{ layer in layers }}">
<div>{{ layer.name }}</div>
</template>
</div>
I would like to offer a safier and more clear way to revert an array for repeat binding:
<polymer-element name="my-element" attributes="layers layersReversed">
<template>
<template repeat="{{layer in layers}}">
<div>{{layer.name}}</div>
</template>
</template>
<script>
Polymer({
layersReversedChanged: function() {
var layers = this.layersReversed.slice();
layers.reverse();
this.layers = layers;
}
});
</script>
</polymer-element>
<my-element layers="{{layers}}"><!-- direct order --></my-element>
<my-element layersReversed="{{layers}}"><!-- reverse order --></my-element>
Direct or reverse order is defined by used attribute: layers or layersReversed.
There are no value changing in corresponding -Changed event by itself (which may cause falling to endless loop).
The .reverse() method changes the original array, so it should be applied on its copy.
There is another funny and extravagant way to do the same via an intermediate web-component:
<polymer-element name="reverse-order" attributes="in out">
<template></template>
<script>
Polymer({
inChanged: function() {
var out = this.in.slice();
out.reverse();
this.out = out;
}
});
</script>
</polymer-element>
It can be used to bind some elements with different order. I.e., array is populated by .push() method, while preferred array presentation is in reverse order:
<my-element layers="{{layersReversed}}"></my-element>
<reverse-order in="{{layers}}" out="{{layersReversed}}"></reverse-order>
<core-localstorage name="layers" value="{{layers}}"></core-localstorage>