In Polymer 1.0 and Vaadin-grid v1 I was using a cell renderer along the following lines to add an icon based on the value of the data:
grid.columns[6].renderer = function(cell) {
if (cell.data < 0){
cell.element.innerHTML = Math.abs(cell.data) + '<iron-icon icon="arrow-downward" style="color: red"/>';
}
else if (cell.data > 0) {
cell.element.innerHTML = cell.data + '<iron-icon icon="arrow-upward" style="color: green"/>';
}
else {cell.element.innerHTML = '<iron-icon icon="settings-ethernet" style="color: #ffcc00"/>';}
};
Of course the migration to vaadin-grid 2 means no more renderer function and recommendation to use templates instead.What would be the most efficient way to achieve this?
Any help appreciated - I'm kind of learning this stuff as I go along and the examples on the vaadin site assume a bit more expertise than I have !
Tomi Virkki over on the Vaadin forum was kind enough to post me a simple example using computed bindings.Seems obvious now it has been explained .. gotta learn somehow I guess! Thanks Tomi
<vaadin-grid items="[[users.result]]">
<vaadin-grid-column width="50px" flex-grow="0">
<template class="header">#</template>
<template>
<iron-icon icon="[[_itemIcon(item)]]" style$="[[_itemIconStyles(item)]]"/>
</template>
</vaadin-grid-column>
<vaadin-grid-column>
<template class="header">First Name</template>
<template>[[item.firstName]]</template>
</vaadin-grid-column>
<vaadin-grid-column>
<template class="header">Last Name</template>
<template>[[item.lastName]]</template>
</vaadin-grid-column>
</vaadin-grid>
</template>
<script>
window.addEventListener('WebComponentsReady', () => {
class XGrid extends Polymer.Element {
static get is() {
return 'x-grid';
}
_itemIcon(item) {
return item.firstName > 'N' ? 'arrow-upward' : 'arrow-downward';
}
_itemIconStyles(item) {
return `color: ${item.firstName > 'N' ? 'green' : 'red'}`;
}
}
customElements.define('x-grid', XGrid);
Related
I'm looking for a solution to manage a HTML tag type with a Reactive Var. I looked all the Blaze documentation but found nothing..
Simple example
I want to change a tag from div to form when a boolean ReactiveVar is updated.
Template.MyExample.onCreated(function() {
this.is_form = new ReactiveVar(false)
})
Template.MyExample.helpers({
getTag() {
return Template.instance().is_form.get() ? 'form' : 'div'
}
})
This obviously didn't work:
<Template name="MyExample">
<{{getTag}}>
</{{getTag}}>
</Template>
Nicer solution ?
The "best" way I found to get it was to create a tag template and list everycase a single time, but I didn't like that solution.
Template.MyExample.onCreated(function() {
this.is_form = new ReactiveVar(false)
})
Template.MyExample.helpers({
getTag() {
return Template.instance().is_form.get() ? 'form' : 'div'
}
})
Template.MyExample.events({
'click .switch'(e, instance) {
e.preventDefault()
instance.is_form.set(!instance.is_form.get())
}
})
Blaze Templates:
<Template name="MyExample">
<div>
Switch type
{{#MyTag tag=getTag}}
Parent tag is {{getTag}}
{{/MyTag}}
{{#MyTag tag="a" attributes=(object href="#" target="_blank")}}
Link
{{/MyTag}}
</div>
</Template>
<Template name="MyTag">
{{#if equals tag 'form'}}
<form {{attributes}}>
{{> Template.contentBlock }}
</form>
{{else if equals tag 'a'}}
<a {{attributes}}>
{{> Template.contentBlock }}
</a>
<!-- and more and more.... -->
{{else}}
<div {{attributes}}>
{{> Template.contentBlock }}
</div>
{{/if}}
</Template>
Helpers required:
Template.registerHelper('object', function({hash}) {
return hash;
})
Template.registerHelper('equals', function (a, b) {
return a === b
})
This is working but i'm wondering if it's to much for Meteor (and DOM updates). Does this solution works like an simple {{#if}}...{{/if}} or it's way heavier ?
The feature you request is basically not supported by Blaze. While static code generators can easily include dynamic tags, this is a very hard one at runtime where you have to deal with the DOM tree, whose element's tag-types are immutable by design.
I first thought of a workaround, that uses child swapping using jQuery in the onRendered of MyTag:
Template.MyTag.onRendered(function () {
const instance = this
instance.autorun(() => {
const data = Template.currentData()
const attributes = data.attributes || {}
const elementName = data.tag
const refTag = instance.$('.my-tag-ref')
const newTag = $(`<${elementName}>${refTag.html()}</${elementName}>`)
Object.keys(attributes).forEach(attKey => newTag.attr(attKey, attributes[ attKey ]))
newTag.addClass('my-tag-ref')
refTag.replaceWith(newTag)
})
})
But this is unfortunately not working, because the content bock looses it's reactivity and the jQuery instance of the current Template looses it's scope to the root element. I just add it here in case someone catches up on this and finds a solution that works.
Now there is still a solution that works using dynamic Templates:
<Template name="MyTag">
{{#Template.dynamic template=getTemplate data=getData}}
{{> Template.contentBlock }}
{{/Template.dynamic}}
</Template>
<template name="mytaga">
<a {{attributes}}>
{{> Template.contentBlock }}
</a>
</template>
<template name="mytagform">
<form {{attributes}}>
{{> Template.contentBlock }}
</form>
</template>
<template name="mytagdiv">
<div {{attributes}}>
{{> Template.contentBlock }}
</div>
</template>
As you can see the disadvantage is clearly that you have to define lots of new Templates. The advantage is, that you don't have to use so many if/else anymore and it pays out the more often you will have to include MyTag in your code.
The respective helpers look like the following:
Template.MyTag.helpers({
getTemplate() {
const instance = Template.instance()
console.log(instance.data)
return `mytag${instance.data.tag}`
},
getData () {
return Template.instance().data
}
})
This is working but i'm wondering if it's to much for Meteor (and DOM updates). Does this solution works like an simple {{#if}}...{{/if}} or it's way heavier ?
Blaze is overall slower than for example React or Vue. However, the rendering only updates if the reactive data updates, thus it is just as heavy as the amount of updates to be triggered.
Can I use the polymer dom-repeat template without an array?
For example I want to render some code 20 times.
<template is="dom-repeat" items="[[itemCounter]]">
<div>Whatever</div>
</template>
This would render <div>Whatever</div> 20 times, but to achieve this I have to create an array "itemCounter" in the components properties with length 20 with the sole purpose of looping over it.
I was wondering if something like this is possible, so I don't need to create the Array.
<template is="dom-repeat" times="20">
<div>Whatever</div>
</template>
Nope, you can't do this with the normal dom-repeat but I wrote a component which does exactly the trick: https://github.com/MeTaNoV/dom-repeat-n
There is also a discussion about this feature on the Polymer github repository that you can find here: https://github.com/Polymer/polymer/issues/3313
You can do a messy hack like this
Properties:
totalNo: {
type: Number,
value: 20
},
_arrayContainer: {
type: Array ,
value: [],
computed: '_numberToArray(totalNo)'
}
Method:
_numberToArray: function(totalNo) {
var array = [], i;
for (i = 0; i < totalNo; ++i) {
array.push(i);
};
return array;
},
HTML:
<template is="dom-repeat" items="[[_arrayContainer]]">
<div>Whatever</div>
</template>
But I'm not sure you'd every really want to.
I am writing a simple widget that will create an output based on fetched data (taken from an AJAX request).
This version of the my-element is the non-configurable, standard one:
http://jsbin.com/rivala/edit?html,output#H:L56
Thing is, I want the user to be able to decide what the output will look like. Since Polymer doesn't allow us to extend existing elements, I went the other way around: I create a behaviour (err... excuse me, a behavior, it's so hard not to type that "u" every time) that does most of the work. Here is my result:
http://jsbin.com/yuxecu/edit?html,output
So, in order to create create an element, all the user needs to do is:
<dom-module id="my-element">
<template>
<!-- THE FOLLOWING PART IS THE ONLY THING THE USER WILL CHANGE -->
<paper-dropdown-menu label="Your favourite category">
<paper-menu class="dropdown-content">
<template is="dom-repeat" items="{{_data}}">
<paper-item>{{item.name}}</paper-item>
</template>
</paper-dropdown-menu>
</template>
<script>
Polymer({
is: "my-element",
behaviors: [ MyBehaviour],
})
</script>
</dom-module>
And then use it:
I would have much much preferred something a little easier. For example, it would have been much nicer to allow something like this:
<my-element url="http://output.jsbin.com/zonona/3.js">
<template id="bindme">
<!-- THE FOLLOWING PART IS THE ONLY THING THE USER WILL CHANGE -->
<paper-dropdown-menu label="Your favourite category">
<paper-menu class="dropdown-content">
<template is="dom-repeat" items="{{_data}}">
<paper-item>{{item.name}}</paper-item>
</template>
</paper-dropdown-menu>
</template>
</my-element>
But I tried and tried and then tried some more, and it doesn't seem to be possible unless you really want to get your hands dirty.
Once extending non-native elements is possible, I assume I can just create an element declaratively that extends my-element and defines a new template. Till then...
Questions:
Does my code seem to be following at least roughly Polymer's best practices?
Is there a much easier way to do this, that I didn't think of?
Any more comments?
Thank you as ever...
I don't know what I am doing is quite the same thing, but you might be able to draw inspiration from it. I have created a generic dialog box that will provide the results from a database query in it, with the headings data driven and the row size and content also data driven. I actually create this element dynamically in a "manager" element.
Something like this is how the manager retrieves the data and creates the dialog (I call it a report-grid)...
newGrid: function(name, useId, useDates, parent) {
var self = this;
var body;
// jshint unused: false
var dataPromise = new Promise(function(accept, reject) {
var sendOptions = {
url: '/api/queries',
method: 'POST',
handleAs: 'json',
headers: {'content-type': 'application/json'}
};
body = {};
body.name = name;
if (useId) {
body.id = parent.id;
}
if (useDates) {
body.startdate = parent.startdate;
body.enddate = parent.enddate;
}
sendOptions.body = body;
var request = document.createElement('iron-request');
request.send(sendOptions).then(function() {
accept(request.response);
});
});
// jshint unused: true
var x;
var y;
var grid = document.createElement('pas-report-grid');
Polymer.dom(self).appendChild(grid);
if (this.grids.length === 0) {
x = 0;
y = 0;
} else {
x = this.grids[this.grids.length - 1].x + this.deltaX;
y = this.grids[this.grids.length - 1].y + this.deltaY;
}
this.grids.push(grid);
grid.open(dataPromise,body,x,y);
And then the element itself has a load of stuff (not shown) to provide drag and resize handles, but the core of the grid is the following templated stuff
<div class="layout horizontal">
<template is="dom-repeat" items="[[heading]]">
<span class="flex">[[item]]</span>
</template>
</div>
<iron-list id="grid" class="flex" items="[[data]]" as="row">
<template>
<div class="layout horizontal row" tabindex$="[[tabIndex]]" index="[[index]]">
<template is="dom-repeat" items="[[row]]" as="field">
<div class="flex field">[[field]]</div>
</template>
</div>
</template>
</iron-list>
The open function of the grid does this with the data
open: function(dataPromise, params, x, y) {
var self = this;
this.x = x;
this.y = y;
dataPromise.then(function(data) {
self.title = data.name;
self.heading = data.heading;
self.data = data.data;
self.$.griddialog.open();
});
this.params = params;
So what is happening here is the manager is making an iron request (also created dynamically) for a generic query that might or might not need an id and start and end dates, the server responds with a json object which contains a heading array, with a list of heading names, and a data array which is the rows, each row also being an array with the values from the query. I pass that info to the grid element as a promise - so it can get started, attach and so on, and then when the data arrives its loaded into a heading div and an iron list.
The grid element knows nothing about the actual query, how many fields each row will have, or indeed how many rows.
I am using Polymer paper-dropdown-menu.
I need to show drop down for numbers 1 to 5. The crude way to do it is
<paper-dropdown-menu label="Numbers" >
<paper-dropdown class="dropdown">
<core-menu class="menu">
<paper-item>1</paper-item>
<paper-item>2</paper-item>
<paper-item>3</paper-item>
<paper-item>4</paper-item>
<paper-item>5</paper-item>
</core-menu>
</paper-dropdown>
Is there a way to avoid repeating <paper-item> code by using <template>
Something like:
<template repeat="{{ i in [0:25] }}">
<paper-item>i</paper-item>
</template>
you could do a "range" function to produce the array then use the array in the method already posted.
that would look something like
<paper-dropdown-menu label="Numbers" >
<paper-dropdown class="dropdown">
<core-menu class="menu">
<template repeat="{{range}}">
<paper-item>{{}}</paper-item>
</template>
</core-menu>
</paper-dropdown>
</paper-dropdown-menu>
then in js you create the range function
var range = function(begin, end) {
if (typeof end === "undefined") {
end = begin; begin = 0;
}
var result = [], modifier = end > begin ? 1 : -1;
for ( var i = 0; i <= Math.abs(end - begin); i++ ) {
result.push(begin + i * modifier);
}
return result;
}
this range function came from this post which also has several diff methods for doing this. Does JavaScript have a method like "range()" to generate an array based on supplied bounds?
then you assign the range to the polymer variable the repeat template is using
this.range = range(1,25);
hope this helps. sorry i couldn't answer yesterday was leaving for work when i sent last response.
edit: a example on plunker http://plnkr.co/edit/4TkQdR2B5vakbwOSAulK?p=preview
As mentioned in the comments there is an example in the demo provided by polymer.
https://github.com/Polymer/paper-dropdown/blob/master/demo.html
<x-trigger icon="menu">
<paper-dropdown class="with-margin">
with margin<br>
<br>
<template repeat="{{countries}}">
{{name}}<br>
</template>
</paper-dropdown>
</x-trigger>
scope.countries = [
{name: 'Afghanistan', code: 'AF'},
{name: 'Åland Islands', code: 'AX'}
];
<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!