I have a Polymer app with many templates, in which I would like to manage localization. I use app-localize-behavior for that. I found a nice way of managing localization from a single file. The solution proposed there uses behaviors, and requires little additions in each template, with a unique json data file.
My problem is that I would like to be able to change the language dynamically, and probably store it in a app-localstorage-document element, which I manage from another file. How can I set this language property from outside this file?
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../bower_components/app-localize-behavior/app-localize-behavior.html">
<script>
MyLocalizeImpl = {
properties: {
language: {
value: 'fr'
}
},
attached: function() {
this.loadResources(this.resolveUrl('../locales.json'));
},
};
MyLocalize = [MyLocalizeImpl, Polymer.AppLocalizeBehavior];
</script>
OK I adapted the app-localize-behavior demo code. The template which changes the language has to override the language property, including “notify: true”:
properties: {
language: {
value: 'en',
type: String,
notify: true
},
},
Then I bound all other elements' language property with this:
<element language="{{language}}"></element>
Templates which use localize must of course link the localize script:
<link rel="import" href="localize.html">
… and add the behavior in the script part:
Polymer({
is: "my-page",
behaviors: [
MyLocalize
]
Bonus: how to use a different json file for every language: https://abendigo.github.io/2016/08/03/lazyloading_localization.html
This meteor code has menuItems to populate a li in the template. When an item is clicked, I need to get the value of the "login" key from the object that was clicked.
I could not use menuItems.find(); because it is not a mongo collection.
I could not create a json object JSON.parse(menuItem) because menuItem is undefined in the console.
I could not think of what to google for either.
Thanks
Template.mainMenu.helpers({
menuItems: [
{menuItem: "task1", login: false},
{menuItem: "task2", login: true},
{menuItem: "task3", login: true},
{menuItem: "task4", login: true},
{menuItem: "task5", login: true},
{menuItem: "task6", login: true},
{menuItem: "task7", login: false},
{menuItem: "task8", login: false},
{menuItem: "task9", login: false}
]
});
Template.mainMenu.events({
'click .menuItem': function (event) {
var item = $(event.currentTarget).data('value');
var isLogin = what is the value of login for this "item"
}
});
This is so basic, you should have seen the answer in first 100 lines of docs. Nevertheless, let me explain it a bit.
You're using a helper to put the data into li elements. This helper returns an array of objects, so you can use it as an iterable in your template, i.e. use each block:
{{#each menuItems}}
<li class="menuItem">{{menuItem}} {{login}}</li>
{{/each}}
Something like that.
Then, you have an event handler which listens to every click event on every element that has menuItem class. Think of it as of jQuery's event handler, so, in jQuery, you would write it in a way like that:
$('.menuItem').each(function () {...});
Within the function you pass into each method, you can access the iterable's element through this keyword. So, for example, you want to access login property of any of iterable's items. You could do that like
$('.menuItem').each(function () {
console.log(this.login);
});
Let's go back to Meteor. In event handler, you can use the same approach to access any property of the object. this will refer to the element as user clicked. So, the answer is
Template.mainMenu.events({
'click .menuItem': function () {
const item = this;
// do whatever you need with this object
}
});
Template event handler function has two arguments, one for event and one for the template. You may need them, but in this particular case, both are unnecessary, hence no arguments supplied to the function assigned as a handler to the click event.
Just keep in mind that each block in the template creates a new nested context, and in this context, this refers to the item of whatever each block iterates over. This is why you can refer to properties of each object in your JSON via helpers:
{{#each menuItems}}
<li class="menuItem">{{menuItem}} {{login}}</li>
{{/each}}
You can see that you don't have menuItem or login helpers but they work anyway. In fact, this is a short form of
{{#each menuItems}}
<li class="menuItem">{{this.menuItem}} {{this.login}}</li>
{{/each}}
I have setup two way binding with a normal textarea in Polymer using:
<textarea id="textbox" value="{{editText::input}}" autofocus></textarea>
I've also tried a two way binding iron-autogrow-textarea using the bindValue attribute:
<iron-autogrow-textarea bindValue="{{editText}}" class="fit" autofocus></iron-autogrow-textarea>
The property editText is assigned as follows:
Polymer({
is: "page-editor",
properties: {
editText: {
type: String,
value: ""
}
},
But when change the editText in code below it won't update the respective textarea values...
this.editText = "new message";
Interestingly a console.log(this.editText) says its 'undefined'
The correct attribute to use is bind-value="{{editText}}". CamelCase properties are translated to attributes with dashes (source).
I'm still ramping up on Polymer, but I think you need to set notify to true.
Polymer({
is: "page-editor",
properties: {
editText: {
type: String,
value: "",
+ notify: true
}
},
...
If that doesn't work, post a full sample and I'll be happy to debug with you.
You can add an event listener with Polymer's on-* syntax, where * is the event to listen for.
<iron-autogrow-textarea bindValue="{{editText}}"
class="fit"
on-click="f'
autofocus></iron-autogrow-textarea>
Define the event listener:
Polymer({
is: "page-editor",
properties: {
editText: {
type: String,
value: "",
notify: true
}
},
/* the function signature below may be wrong...
* don't know how many arguments it takes... */
f: function(e, detail, sender) {
this.editText = 'yay';
}
...
I have a couple of conditionally stamped elements inside a template repeater. Now when I update the data, the if-conditions don't seem to take effect, which results in undefined getting passed into the functions which handle the data for those elements.
Using the restamp property didn't seem to help (https://www.polymer-project.org/1.0/docs/devguide/templates.html#dom-if). So far I was only able to solve this by emptying the items property (this.items = [];) inside my change handler which initiates a new request.
This works, but results in the template being empty for a short amount of time before the new data gets displayed. Not necessarily a problem, but I wonder if I'm doing something wrong.
Here are the corresponding parts of the code:
<template>
...
<iron-ajax
id="ironajax"
url="https://www.url.com/api"
params="{{ajaxParams}}"
handle-as="json"
on-response="handleResponse"
on-error="handleError">
</iron-ajax>
...
<template is="dom-repeat" items="{{items}}" id="items">
...
<template is="dom-if" if="{{item.info.subtitle}}">:
<span>{{truncateSubtitle(item.info.subtitle)}}</span
</template>
...
Polymer({
is: 'my-element',
properties: {
query: {
type: String,
value: ''
}
...
items: {
type: Array,
value: []
}
}
...
handleChange: function() {
if (this.value !== '') {
this.items = [];
this.query = this.value;
this.$.ironajax.generateRequest();
}
}
...
Given "{{item.info.subtitle}}", if item.info is null or undefined or if item.info.subtitle is undefined, the binding will not refresh and may have a stale value (in the context of a repeat where nodes are re-used).
Polymer doesn't perform calculations on undefined values because it's a performance enhancement in a large number of cases, however it can be tricky in this scenario.
You should use a function to resolve the correct state. Something like
if="{{hasSubtitle(item)}}"
and
hasSubtitle function(item) {
return item.info && item.info.subtitle;
}
The "if" property of "dom-if" requires a Boolean.
Try creating a polymer property that will be set to true or false if "item.info.subtitle" is not empty or null.
Then use the created property with dom-if:
<template is="dom-if" if="{{hasSubtitle}}" >
reference: http://polymer.github.io/polymer/ --> dom-if (dropdown)
Additional info: Added on June 10th
Try using a property instead of a method.
Declare your property like this:
Polymer({
is: "my-element",
properties: {
hasSubtitle: {
type: Boolean,
value: 'false', //false or true, the more common case.
}
},
...
});
The Polymer 1.0 documentation states:
The path syntax doesn’t support array-style accessors (such as
users[0].name). However, you can include indexes directly in the path
(users.0.name).
How would one get around this in setting the path dynamically, and obtain the same behavior as the following example using Polymer 0.5? This is specifically in the context of generating forms for a model defined by an Object.
<template repeat="{{row in fieldset.rows}}">
<div layout horizontal flex>
<template repeat="{{field in row}}" flex>
<paper-field field="{{model.fields[field]}}" value="{{obj[field]}}">
</paper-field>
</template>
</div>
</template>
edit:
Per https://github.com/Polymer/polymer/issues/1504:
No near-term plans to support this. Polymer 0.5 had a complex expression parser used for bindings that we have eliminated for simplicity and performance. There are alternate patterns you can use today to achieve similar results that just require you to be more explicit.
What the alternate pattern would be to achieve two way data binding remains unclear.
Yes, it is true that Polymer 1.0 no longer supports myObject[key] in binding expressions. However, in your particular use-case, there are ways to sidestep this problem.
One-way data-binding
It is fairly simple to overcome this limitation when it comes to one-way data-binding. Simply use a computed property that accepts both the object and the key in question:
<my-element value="[[getValue(obj, key)]]"></my-element>
getValue: function(obj, key) {
return obj[key];
}
Two-way data-binding
In the case of two-way data-binding, it is still possible to create a functional alternative to the binding expression {{obj[key]}} in Polymer 1.0. However, it will require taking into consideration the particular use-case in which you are hoping to implement the binding.
Taking into account the example in your question, it seems that you are doing work with some sort of table or fieldset. For the purposes of the example here, I will use a slightly different, yet very similar structure.
Let's assume that we have a fieldset object, and that this object is structured as such:
{
"fields": [
{ "name": "Name", "prop": "name" },
{ "name": "E-mail", "prop": "email" },
{ "name": "Phone #", "prop": "phone" }
],
"rows": [
{
"name": "John Doe",
"email": "jdoe#example.com",
"phone": "(555) 555-1032"
},
{
"name": "Allison Dougherty",
"email": "polymer.rox.1337#example.com",
"phone": "(555) 555-2983"
},
{
"name": "Mike \"the\" Pike",
"email": "verypunny#example.com",
"phone": "(555) 555-7148"
}
]
}
If we want to create some sort of table-like output which represents this object, we can use two nested repeating templates: the first to iterate through the different rows, and the second to iterate through the different fields. This would be simple using the one-way data-binding alternative above.
However, achieving two-way data-binding is very different in this case. How is it do-able?
In order to understand how to come up with a solution, it is important to break down this structure in a way that will help us figure out what observation flow we should implement.
It becomes simple when you visualize the fieldset object like this:
fieldset
--> rows (0, 1, ...)
--> row
--> fields (name, email, phone)
--> value
And becomes even simpler when you factor in the workflow of your element/application.
In this case, we will be building a simple grid editor:
+---+------+--------+-------+
| | Name | E-mail | Phone |
+---+------+--------+-------+
| 0 | xxxx | xxxxxx | xxxxx |
+---+------+--------+-------+
| 1 | xxxx | xxxxxx | xxxxx |
+---+------+--------+-------+
| 2 | xxxx | xxxxxx | xxxxx |
+---+------+--------+-------+
There are two basic ways that data will flow in this app, triggered by different interactions.
Pre-populating the field value: This is easy; we can easily accomplish this with the nested templates mentioned earlier.
User changes a field's value: If we look at the application design, we can infer that, in order to handle this interaction, whatever element we use as an input control, it must have some sort of way of knowing:
The row it will affect
The field it represents
Therefore, first let's create a repeating template which will use a new custom element that we will call basic-field:
<div class="layout horizontal flex">
<div>-</div>
<template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
<div class="flex">[[item.name]]</div>
</template>
</div>
<template is="dom-repeat" items="{{fieldset.rows}}" as="row" index-as="rowIndex">
<div class="layout horizontal flex">
<div>[[rowIndex]]</div>
<template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
<basic-field class="flex" field="[[item]]" row="{{row}}"></basic-field>
</template>
</div>
</template>
(In the snippet above, you'll notice that I've also added a separate repeating template to generate the column headers, and added a column for the row index - this has no influence on our binding mechanism.)
Now that we've done this, let's create the basic-field element itself:
<dom-module>
<template>
<input value="{{value}}">
</template>
</dom-module>
<script>
Polymer({
is: 'basic-field',
properties: {
row: {
type: Object,
notify: true
},
field: {
type: Object
},
value: {
type: String
}
}
});
</script>
We have no need to modify the element's field from within the element itself, so the field property does not have notify: true. However, we will be modifying the contents of the row, so we do have notify: true on the row property.
However, this element is not yet complete: In its current state, it doesn't do any of the work of setting or getting its value. As discussed before, the value is dependent on the row and the field. Let's add a multi-property observer to the element's prototype that will watch for when both of these requirements have been met, and populate the value property from the dataset:
observers: [
'_dataChanged(row.*, field)'
],
_dataChanged: function(rowData, field) {
if (rowData && field) {
var value = rowData.base[field.prop];
if (this.value !== value) {
this.value = value;
}
}
}
There are several parts to this observer that make it work:
row.* - If we had just specified row, the observer would only be triggered if we set row to an entirely different row, thus updating the reference. row.* instead means that we are watching over the contents of row, as well as row itself.
rowData.base[field.prop] - When using obj.* syntax in an observer, this tells Polymer that we are using path observation. Thus, instead of returning just the object itself, this means that rowData will return an object with three properties:
path - The path to the change, in our case this could be row.name, row.phone, etc.
value - The value that was set at the given path
base - The base object, which in this case would be the row itself.
This code, however, only takes care of the first flow - populating the elements with data from the fieldset. To handle the other flow, user input, we need a way to catch when the user has inputted data, and to then update the data in the row.
First, let's modify the binding on the <input> element to {{value::input}}:
<input value="{{value::input}}">
Polymer does not fully automate binding to native elements as it doesn't add its own abstractions to them. With just {{value}}, Polymer doesn't know when to update the binding. {{value::input}} tells Polymer that it should update the binding on the native element's input event, which is fired whenever the value attribute of the <input> element is changed.
Now let's add an observer for the value property:
value: {
type: String,
observer: '_valueChanged'
}
...
_valueChanged: function(value) {
if (this.row && this.field && this.row[this.field] !== value) {
this.set('row.' + this.field.prop, value);
}
}
Notice that we aren't using this.row[this.field.prop] = value;. If we did, Polymer would not be aware of our change, as it does not do path observation automatically (for the reasons described earlier). The change would still be made, but any element outside of basic-field that may be observing the fieldset object would not be notified. Using this.set gives Polymer a tap on the shoulder that we are changing a property inside of row, which allows it to then trigger all the proper observers down the chain.
Altogether, the basic-field element should now look something like this (I've added some basic styling to fit the <input> element to the basic-field element):
<link rel="import" href="components/polymer/polymer.html">
<dom-module id="basic-field">
<style>
input {
width: 100%;
}
</style>
<template>
<input value="{{value::input}}">
</template>
</dom-module>
<script>
Polymer({
is: 'basic-field',
properties: {
row: {
type: Object,
notify: true
},
field: {
type: Object
},
value: {
type: String,
observer: '_valueChanged'
}
},
observers: [
'_dataChanged(row.*, field)'
],
_dataChanged: function(rowData, field) {
if (rowData && field) {
var value = rowData.base[field.prop];
if (this.value !== value) {
this.value = value;
}
}
},
_valueChanged: function(value) {
if (this.row && this.field && this.row[this.field] !== value) {
this.set('row.' + this.field.prop, value);
}
}
});
</script>
Let's also go ahead and take the templates we've made before and throw them into a custom element as well. We'll call it the fieldset-editor:
<link rel="import" href="components/polymer/polymer.html">
<link rel="import" href="components/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="basic-field.html">
<dom-module id="fieldset-editor">
<style>
div, basic-field {
padding: 4px;
}
</style>
<template>
<div class="layout horizontal flex">
<div>-</div>
<template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
<div class="flex">[[item.name]]</div>
</template>
</div>
<template is="dom-repeat" items="{{fieldset.rows}}" as="row" index-as="rowIndex">
<div class="layout horizontal flex">
<div>[[rowIndex]]</div>
<template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
<basic-field class="flex" field="[[item]]" row="{{row}}"></basic-field>
</template>
</div>
</template>
<pre>[[_previewFieldset(fieldset.*)]]</pre>
</template>
</dom-module>
<script>
Polymer({
is: 'fieldset-editor',
properties: {
fieldset: {
type: Object,
notify: true
}
},
_previewFieldset: function(fieldsetData) {
if (fieldsetData) {
return JSON.stringify(fieldsetData.base, null, 2);
}
return '';
}
});
</script>
You'll notice we're using the same path observation syntax as we did inside of basic-field to observe changes to fieldset. Using the _previewFieldset computed property, we will generate a JSON preview of the fieldset whenever any change is made to it, and display it below the data entry grid.
And, using the editor is now very simple - we can instantiate it just by using:
<fieldset-editor fieldset="{{fieldset}}"></fieldset-editor>
And there you have it! We have accomplished the equivalent of a two-way binding using bracket-notation accessors using Polymer 1.0.
If you'd like to play with this setup in real-time, I have it uploaded on Plunker.
You can make a computed binding. https://www.polymer-project.org/1.0/docs/migration.html#computed-bindings
<paper-field field="{{_computeArrayValue(model.fields, field)}}" value="{{_computeArrayValue(obj, field}}"></paper-field>
<script>
Polymer({
...
_computeArrayValue: function(array, index) {
return array[index];
},
...
});
</script>
As an aside, you also need to update your repeat to dom-repeat https://www.polymer-project.org/1.0/docs/devguide/templates.html#dom-repeat
Edit: Here is my ugly solution to the 2-way binding. The idea is that you have a calculated variable get the initial value and then update this variable upon updates with an observer.
<!-- Create a Polymer module that takes the index and wraps the paper field-->
<paper-field field="{{fieldArrayValue}}" value="{{objArrayValue}}"></paper-field>
<script>
Polymer({
...
properties: {
fields: { //model.fields
type: Array,
notify: true
},
obj: {
type: Array,
notify: true
},
arrayIndex: {
type: Number,
notify: true
},
fieldArrayValue: {
type: String,
computed: '_computeInitialValue(fields, number)'
},
objArrayValue: {
type: String,
computed: '_computeInitialValue(obj, number)'
}
},
_computeInitialValue: function(array, index) {
return array[index];
},
observers: [
'fieldsChanged(fields.*, arrayIndex)',
'objChanged(fields.*, arrayIndex)'
],
fieldsChanged: function (valueData, key) {
this.set('fieldArrayValue', this.fields[this.arrayIndex]);
},
objChanged: function (valueData, key) {
this.set('objArrayValue', this.obj[this.arrayIndex]);
},
...
});
</script>
Edit 2: Updated the code in Edit 1 to reflect the obeserver changes pointed out by Vartan Simonian