Polymer repeat template bidirectional data binding - javascript

I'm trying to build a WebComponent where you can edit items in an array, with the Polymer javascript framework. Model to DOM bindings work OK, but DOM to Model doesn't - simplified example:
<polymer-element name="rep-test">
<template>
<template repeat="{{item in items}}">
<input type="text" value="{{item}}" placeholder="changes don't work!">
</template>
<button on-click="{{add}}">Add</button>
{{items}}
</template><script>
Polymer({
ready: function() { this.items = [] },
add: function() { this.items.push('') },
itemsChanged: function() { console.log(this.items) } // debug
})
</script>
</polymer-element>
<rep-test></rep-test>
The items are correctly displayed in the input elements, but when I change the value inside an input, changes are not reflected to the model (items). The binding works only in one direction.
Is there any way to make the binding bidirectional, so that when a change occur in the DOM, it is copied in the model ?
I've seen this todo demo which achieves this effect, but it does so with custom events associated with items changes. This obviously works but I'm looking for a more declarative way of doing this with bindings.

Since changes in array’s elements are not reflected to itemsChanged, I would suggest you to listen on the input changes:
<!-- ⇓⇓⇓⇓⇓⇓⇓⇓⇓ -->
<input type="text" on-change="{{ itemChanged }}"
value="{{item}}" placeholder="changes don't work!">
[...]
<!-- inside script -->
itemChanged: function(e) {
console.log(e.path[0].value)
}
Below is the link to the working example: http://plnkr.co/edit/sZYHeMuAVB0G1muHhFNK?p=preview

Here is an example of bidirectional binding: as you change the values in the input fields model is updated:
Plunk
Follow data changes:
<br>
{{testData.employees[0].firstName}}
<br>
{{testData.employees[3].firstName}}
<br><br>
<template repeat="{{person in testData.employees}}">
{{person.firstName}}
<input type="text" value="{{person.firstName}}">
<br>
</template>
I'll reference this post because it explains how this works better then I can:
"...if you change the data values, the new values are NOT available to all other instances - because the instance variables are just copies of the referenced strings. By using an object with data properties, as in the edited version above, and only ever reading from and assigning to the data properties of that object rather than overwriting the object itself, changed values are shareable between instances."

Related

About Vue two-way data bindings: what if form field content doesn't come from direct user input?

So I've got some code like this:
<input name="date" v-model="date" id="date" type="text" value="" />
...then somewhere else:
<li><b>Date:</b> {{ date }}</li>
...And Vue:
var vueapp = new Vue({
el: '#form'
,data:{
date:''
}
});
Normally, this code will update li's content in real time while the user types into the input field. But in my case, I've assigned a javascript date picker to it, and the li's content doesn't get updated. I guess because there are no keyup (or the like) events. How to elegantly overcome this lovely issue?
You can use components for it, which will handle such scenarios gracefully.
For example using vuejs-datepicker:
const app = new Vue({
el: '#app',
components: {
vuejsDatepicker
},
data: {
date: ''
}
})
.as-console-wrapper{max-height:0!important;bottom:0;}
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuejs-datepicker"></script>
<div id="app">
<vuejs-datepicker v-model="date"></vuejs-datepicker>
<br/>
<p><b>Date:</b> {{ date }}</p>
</div>
You can also create your own component for it and use it anywhere in your app.
Ok my own answer comes from #numbers1311407's suggestion. Essentially, I hooked into the plugins events from the mounted Vue's function, and then used the $set instance method to change the property I needed. So, to recap, here's my html:
<input name="project[deadline]" v-model="form.project.deadline" data-toggle="datepicker" id="project-deadline" type="text" value="" />
And here's my js:
var vueapp = new Vue({
el: '#form'
,data:{
form:{ // for tech reasons I'm using several nested empty objects here
contact:{}
project:{}
component:{}
// [...] other several objects ...
}
}
,mounted: function(){
$('[data-toggle="datepicker"]').on('hide.datepicker', function (e) {
_.delay(function(){ // from lodash.js
this.$set(this.form.project, 'deadline', $(e.target).val());
}.bind(this), 150); // because the plugin's event fires before the field gets updated
}.bind(this));
}
});
Note that I'm slightly delaying the assignment, but this is not a Vue thing. It's just because the plugin's event I'm using fires just before changing the value, and the plugin itself doesn't expose any event that fires after. So I have to wait a bit for the field to be updated and then I can pick up its value and assign it to Vue's model.
For reference, the plugin currently in use is this one.

Can I change element tag name with Polymer data binding?

I have this application which contains many types of Polymer elements that can be added to a main Polymer app element. This main element manages instances of these elements and shows them in a UI.
eg.
item1.html
item2.html
item3.html
my-app.html
As I add new types of items (eg. item4.html), I need to make several changes to the main UI to handle creating, managing, and showing them. Each type is unique enough that I do not want to merge them into a single item type.
What I'd like to do is have each Polymer element 'register' itself into my-app by calling a function which can add a new object to an array.
To do this, my-app will have a property called itemMap which is an array of objects. One property in this object is the type of item.
itemMap: [
{
type: 'item-1',
instances: []
}, {
type: 'item-2',
instances: []
}
...
]
This implementation works in code. When adding a new instance, I can add a new object to the instances array for that type. However, I do not know how to show the items in the UI. As each type is a different Polymer element, I cannot use a simple dom-repeat template. At the same time, I do not want to hardcode each type in the main UI to improve modularity.
Right now I have:
<iron-list id="item-1-list" items="[[item1_array]]" as="item" grid>
<template>
<div class="item">
<item-1 properties=[[item]]></item-1>
</div>
</template>
</iron-list>
<iron-list id="item-2-list" items="[[item2_array]]" as="item" grid>
<template>
<div class="item">
<item-2 properties=[[item]]></item-2>
</div>
</template>
</iron-list>
What I want to do is something like the snippet below, which would work for any type I create.
<template is="dom-repeat" items="{{itemMap}}" as="itemType" id="item-grid">
<iron-list id="[[itemType.type]]-list" as="item" grid items="[[itemType.instances]]">
<template>
<div class="item">
<[[itemType.type]] properties=[[item]]></[[itemType.type]]>
</div>
</template>
</iron-list>
</template>
However, this does not work.
Is this possible, or something equivalent, or am I going down the wrong path completely?
Polymer data binding does not work for tag names. What you might do to implement this kind of behavior is to create another custom element that accepts the item type as a property and dynamically creates an element of that type:
<template is="dom-repeat" items="{{itemMap}}" as="itemType" id="item-grid">
<iron-list id="[[itemType.type]]-list" as="item" grid items="[[itemType.instances]]">
<template>
<div class="item">
<x-item-selector type=[[itemType.type]] properties=[[item]]></x-item-selector>
</div>
</template>
</iron-list>
</template>
There could be several ways to implement the x-item-selector element: declarative and imperative:
Declarative: Create a set of <dom-if>s--one per type
If there are only a few element types and you can list them all, you could create a template for the x-item-selector like below:
<template>
<template is="dom-if" if="[[_isEqual(type, 'item-1')]]" restamp>
<item-1 properties="[[properties]]"></item-1>
</template>
...
</template>
Imperative: Observe the type property and update the child element manually
If you are going to support many types of elements, you might want to avoid a large set of <dom-if>s, and update the children of the x-item-selector element imperatively whenever the type property changes. As a downside, the mapping for the properties property you'll also have to establish manually.
_onTypeChanged(newType, oldType) {
if (newType !== oldType) {
for (let child of this.children) {
this.removeChild(child);
}
const newChild = document.createElement(newType);
newChild.properties = this.properties;
// also need to add some code to update newChild.properties
// when this.properties change
this.appendChild(newChild);
}
}

Polymer 1.0+, can't define template in light dom for use by component

In Polymer 1.0+, how do you pass in a template from the light dom to be used in the dom-module? I'd like the template in the light dom to have bind variables, then the custom element use that template to display its data.
Here is an example of how it would be used by the component user:
<weather-forecast days="5" as="day">
<template id="forecast-template">
<img src="{{day.weatherpic}}">
<div>{{day.dayofweek}}</div>
<div>{{day.wordy}}</div>
<div>{{day.highlowtemp}}</div>
</template>
</weather-forecast>
This weather-forecast component would contain an iron-list in the dom-module and ideally, the component would reference the "forecast-template" inside the iron-list that would bind the variables to the element. Ultimately, it would generate a 5-day forecast using that template. My issue is that I haven't seen an example of bind-passing variable based templates into a Polymer 1.0 custom element. I would think this or something similar would be fairly commonplace.
The following is an example of how I would like to see it work on the client side, but no luck yet. Although the template is successfully references, it only displays once, while other fields actually do show in the loop.
<dom-module id="weather-forecast">
<template is="dom-bind">
<iron-list items="[[days]]" as="day">
<content selector='#forecast-template'"></content>
</iron-list>
</template>
</dom-module>
Any input is appreciated. Thanks!
You cannot use dom-bind inside another polymer element.
Your first element should just be dom-bind
<template is="dom-bind">
<iron-list items="[[days]]" as="day">
<weather-forecast day=[[day]]></weather-forecast>
</iron-list>
</template>
Your second element should be weather-forecast
<dom-module id="weather-forecast">
<template>
<style></style>
<img src="{{day.weatherpic}}">
<div>{{day.dayofweek}}</div>
<div>{{day.wordy}}</div>
<div>{{day.highlowtemp}}</div>
</template>
<script>
Polymer({
is: "weather-forecast",
properties: {
day: {
type: Object
}
}
});
</script>
</dom-module>
If this does not work, try wrapping the weather-forecast tag inside a template tag inside iron-list.
You can use the Templatizer. You can look at my blog for some example (there's also a Plunker link).
Unfortunately there seems to be some bug or limitation, which breaks two way binding:
Add a "dynamic" element with data-binding to my polymer-element
Polymer: Updating Templatized template when parent changes
Two way data binding with Polymer.Templatizer

Polymer, core-ajax shared response

Using Polymer, I am attempting to instantiate several ajax-service elements using template binding, <template repeat=...>.
Code is as follows:
<template repeat="{{viewName, i in views}}">
<section hash={{viewName}} layout vertical center-center on-tap={{closeOpenDrawer}}>
<core-ajax id="ajaxService" auto response={{list}}" url="../componentsItems/demo-components.json"></core-ajax>
<template repeat="{{element, j in list}}">
<workspace-elem class="dropped" name="{{element.name}}"></workspace-elem>
</template>
</section>
</template>
The problem is, each ajax response is concatenated to a shared list variable, rather then instantiating its own local list variable per repeated template, so when the sub template is triggered, it generates <workspace-elem>s in each section for the sum of data from all ajax calls.
Is there an easy way to solve this? Is there something I am over looking?
EDIT:
Same sort of problem occurs with the inner template. Each instantiated inner template shares the list variable, if anything is pushed to template.model.list, all instantiated template models are updated.
When you use response={{list}} in the template, you are not creating that variable, but you are binding the value of the response attribute to an existing variable called ‘list’.
The question you need to ask yourself is: ‘Where does the “list” variable that I'm binding to come from?’ It is not obvious from the snippet you mention, but it's very likely that it's coming from some enclosing custom element, so it's only natural that it will be shared between the iterations of the template (though I'm surprised that it get concatenated instead of overwritten…). I think a good solution would be to encapsulate the code you have in the outer template in a custom element to hold the variable:
<polymer-element name="my-element" attributes="viewName">
<template>
<!--Your original code starts here-->
<section hash={{viewName}} layout vertical center-center>
<core-ajax id="ajaxService" auto response={{list}}" url="../componentsItems/demo-components.json"></core-ajax>
<template repeat="{{element, j in list}}">
<workspace-elem class="dropped" name="{{element.name}}"></workspace-elem>
</template>
</section>
<!--Your original code ends here-->
</template>
<script>
Polymer({
publish: {
list: null
},
created: function() {
// Create your array on instantiation of an element
this.list = [];
}
}
</script>
</polymer-element>
<template repeat="{{viewName, i in views}}">
<my-element on-tap={{closeOpenDrawer}}></my-element>
</template>
I think that should solve your problem. Alternatively, I think it might help to make the outer template an auto-binding template(‘is="auto-binding"’). Then the model of the template would be the template itself, but since I have not used this facility very often, I'm not quite sure (it might be that you're then loosing the ability to bind to ‘views’).

AngularJS Nested Table

I have a table that I'm including on different pages, this works great except I can't get to the values in the included table. If I use this on a page:
<div data-ng-include="'/app/views/tasks/tasksTable.html'" />
the table shows up but I can't display the value in the file tasksTable.html, this shows undefinded:
<td>
<i class="icon-ok-sign" ng-click="addTask()"></i>
</td>
<td>
<input ng-model="task" />
</td>
From the controller:
$scope.addTask = function (data) {
console.log($scope.task);
};
If I put the table in my file instead of using ng-include to display the table I can display whatever I type into the input tied to ng-model="task".
The reason why you can't access the task is due to two reasons. Firstly, is because ng-include creates a new scope for the template that is a child of the parent controller scope. The second reason is that you are attaching your string model directly to the scope and not creating an object that contains your model. I created a working CodePen example to demonstrate how to solve your problem.
You should read up on prototypical inheritance and how it affects on scopes.
I hope this helps.

Categories