I have an application that is using Polymer. In this application, I am binding an array of items to the UI. The user can click a button. When that button is clicked, a task associated with a third-party library is called. When that task is completed, it returns a status. I need to bind that status to a property of an item in my array. The third-party library allows me to use a callback function. For that reason, I'll demonstrate my challenge using JavaScript's baked in setTimeout function.
my-component.html
<dom-module id="view-tests">
<template>
<table>
<tbody>
<template is="dom-repeat" items="{{ items }}" as="item">
<tr>
<td>[[ item.name ]]</td>
<td><item-status status="[[ item.status ]]"></item-status></td>
</tr>
</template>
</tbody>
</table>
<button on-click="bindClick">Bind</button>
</template>
<script>
Polymer({
is: "my-component",
properties: {
items: {
type: Array,
notify: true,
value: function() {
return [
new Item({ name:'Item 1', status:'In Stock' }),
new Item({ name:'Item 2', status:'Sold Out' })
];
}
},
},
bindClick: function() {
var items = items;
setTimeout(function() {
this.set('items.1.status', 'In Stock');
}, 1000);
}
});
</script>
</dom-module>
As shown in the code snippet above, there is another component item-status.
item-status.html
<dom-module id="test-status">
<template>
<span class$="{{ statusClass }}">{{ status }}</span>
</template>
<script>
Polymer({
is: "item-status",
properties: {
status: {
type: String,
value: '',
observer: '_statusChanged'
}
},
_statusChanged: function(newValue, oldValue) {
alert(newValue);
if (newValue === 'In Stock') {
this.statusClass = 'green';
} else if (newValue === 'Sold Out') {
this.statusClass = 'red';
} else {
this.statusClass = 'black';
}
}
});
</script>
</dom-module>
When a user clicks the "Bind" button, the status does not get updated in the UI. I noticed the alert that I added for debugging purposes appears when the view initially loads. However, the alert window does not appear when the "Bind" button is clicked. This implies that the observer function is not firing. My callback actually looks something like this:
getStatus(1, function(status) {
this.set('items.1.status', status);
});
How do I set the property of an array item from a callback?
setTimeout has its own scope. '.bind(this)' can be used to bind the Polymer element scope to the callback function. Below bindClick function should work
bindClick: function() {
setTimeout(function() {
this.set('items.1.status', 'In Stock');
}.bind(this), 1000);
}
Working jsbin: http://jsbin.com/mehovu/edit?html,output
Related
I have a simple component in Vue.js which is used in a partial view - question.blade.php:
{{--HTML code--}}
<my-component type='question'>
<div class="question">[Very long text content...]</div>
</my-component>
{{--more HTML code--}}
The idea behind the component is to create a "show more - show less" logic around the question content.
The component is compiled and renders just fine on page load. However, there are cases where I need to dynamically load a question via Ajax. For this, I make a simple jQuery Ajax call, retrieve the HTML of the question.blade.php and append it in the DOM. The problem is, the component is not compiled.
How do I make sure the component is always compiled when the partial view gets rendered, independently of whether it occurs on page load or via Ajax call?
Full component code:
{% verbatim %}
<template>
<div>
<div v-bind:class="cssClasses" v-html="content"></div>
<div v-if="activateShowMore && !isShown" class="sml-button closed" v-on:click="toggleButton()">
<span class="sml-ellipsis">...</span><span class="sml-label">{{$t('show_more')}}</span>
</div>
<div v-if="activateShowMore && isShown" class="sml-button open" v-on:click="toggleButton()">
<span class="sml-label">{{$t('show_less')}}</span>
</div>
</div>
</template>
<style lang="sass" scoped>
/*styles*/
</style>
<script type="text/babel">
export default {
props: ['content', 'type'],
data() {
return {
activateShowMore: false,
isShown: false,
cssClasses: this.getCssClasses()
}
},
locales: {
en: {
'show_more': 'show more',
'show_less': 'show less'
},
de: {
'show_more': 'mehr anzeigen',
'show_less': 'weniger anzeigen'
}
},
mounted() {
this.checkShowMore();
},
watch: {
isShown: function(shouldBeShown) {
this.cssClasses = this.getCssClasses(shouldBeShown);
}
},
methods: {
checkShowMore: function() {
let $element = $(this.$el);
let visibleHeight = $element.outerHeight();
let realHeight = $element.find('.text-area-read').first().outerHeight();
let maxHeight = this.getMaxHeight();
this.activateShowMore = (visibleHeight === maxHeight) && (visibleHeight < realHeight);
},
getMaxHeight: function() {
switch (this.type) {
case 'question':
return 105;
case 'answer':
return 64;
}
},
toggleButton: function() {
this.isShown = !this.isShown;
},
getCssClasses: function(shouldBeShown) {
if (undefined === shouldBeShown || !shouldBeShown) {
return 'sml-container' + ' sml-' + this.type + ' sml-max-height';
}
return 'sml-container' + ' sml-' + this.type;
}
}
}
</script>
I don't think this is the best way but it should do the trick. But I had to deal with vue and jquery communication before.
What I did is created a hidden input and changed the value with jquery after the ajax call finished and then triggered the change event with jquery. Then you already listening to the event inside vue and you will know you need to update the content. This should get you going with some modification to your vue component and should be able to update. If you need to send the content to vue you might need to send it in the input hidden value. I did a quick demo code to explain what I mean. Here's the link.
var app = new Vue({
el: '#app',
data(){
return{
content: 'hi there',
}
},
methods: {
onChangeHandler: function(e){
this.content = e.target.value
}
},
});
$('#me').on('click',function(){
$('#update').val('Good Day!')
$('#update').trigger("click")
});
I am trying to bind a Polymer property to the disabled attribute of a CheckBox, but the CheckBox only get disabled/enabled at the beginning.
I have to mention that the property I am binding to is a sub-property of a Polymer property.
Component's code:
<link rel="import" href="../polymer/polymer.html"/>
<dom-module id="checkbox-disabled-example">
<template>
<button
on-click="onClick" >
Toggle Disabled Enabled </button>
<template is="dom-repeat" items="{{elementsObject.items}}" as="item">
<input
type="checkbox"
disabled$="{{item.disabled}}" >
{{item.name}} <br>
</template>
</template>
<script>
Polymer({
is: 'checkbox-disabled-example',
properties:{
elementsObject:{
type: Object,
value: {}
},
},
onClick: function(){
this.elementsObject.items.forEach(
function(item, index, array){
item.disabled = !item.disabled;
}
);
}
});
</script>
</dom-module>
index.html code:
<body>
<template id="just-for-demo" is="dom-bind" >
<checkbox-disabled-example elements-object={{ckData}} >
</checkbox-disabled-example>
</template>
<script>
window.addEventListener('WebComponentsReady', function() {
var template = document.querySelector('template[is=dom-bind]');
var data =
{
items:
[
{ disabled: true, name:"Element 01"},
{ disabled: false, name:"Element 02"},
{ disabled: false, name:"Element 03"},
{ disabled: false, name:"Element 04"},
{ disabled: false, name:"Element 05"},
{ disabled: true, name:"Element 06"}
]
};
template.ckData = data;
});
</script>
</body>
As you can see I create several Check Boxes, each one of them bound to the disabled property of its item.
When I click on the button I toggle the disabled value of each item, however the Check boxes states don't change.
What am I doing wrong?
Changes that imperatively mutate an object or array are not observable.
If you have a simple property like:
this.name = 'Jane';
Polymer will automatically create a setter and it will automatically pick up any changes on that property.
However, changes on an object subproperty or an array item will not work:
this.address.street = 'Elm Street';
The setter on address will not be called and the change will not be detected.
Polymer provides specific methods for making observable changes to subproperties and arrays, in the example above you would need to call:
this.set('address.street', 'Elm Street');
The address.street part is called path. It's a string that identifies a property or subproperty relative to a scope. In most cases, the scope is a host element.
If you are creating a path to an array item, you will need to pass the index:
this.set('items.5', 'Changed index 5');
In your specific example, a simple change to the click event handler would be enough:
onClick: function() {
var that = this;
this.elementsObject.items.forEach(function(item, index, array) {
that.set('elementsObject.items.'+index+'.disabled', !item.disabled);
});
}
Or you can use the cool new ES6 arrow function syntax to make it more readable and remove the ugly that = this:
onClick: function() {
this.elementsObject.items.forEach((item, index, array) => {
this.set('elementsObject.items.'+index+'.disabled', !item.disabled);
});
}
I have something like this:
<dom-module id="bar-foo">
<template>
<span class$="{{getState()}}">Bar Foo</span>
</template>
<script>
(function() {
class BarFoo {
beforeRegister() {
this.is = 'bar-foo';
this.properties = {
data: {
type: Object,
notify: true,
observer: '_updateData'
};
}
getState() {
if (this.data) {
return this.data.val > 0 ? 'positive' : 'negative';
}
}
}
Polymer(BarFoo);
})();
</script>
</dom-module>
Now, the getState function is only called once, but the data property is updated every second. Is it possible to update the class$ property when the data changes ?
If you want the getState function to be evaluated every time data.val changes, you can pass it as an argument to the function.
<span class$="{{getState(data.val)}}">Bar Foo</span>
Have a look at the docs for more information on computed bindings.
So I have this Polymer element with dom-repeat. It binds correctly. However, when the array is modified, it doesn't reflect back to DOM. Nothing changed when I click on the button.
<dom-module id="my-element">
<template>
<template is="dom-repeat" id="allRules" items="{{allRules}}">
<span class="tag" rule-enabled$={{item.enabled}}>{{item.name}}</span>
</template>
<button on-click="click">Click me</button>
</template>
</dom-module>
<script>
Polymer({
is: "my-element",
properties: {
allRules: {
type: Array,
notify: true
}
},
click: function() {
this.allRules[0].name = "three";
},
ready: function() {
this.allRules = [
{
name: "one",
enabled: true
}, {
name: "two",
enabled: false
}
]
},
setBind: function(bind) {
this.bind = bind;
}
});
</script>
Is there a method like notifyArrayUpdated to tell the DOM to update binding data?
When you change a subproperty inside an array, you need to do
this.set('allRules.0.name', 'three');
Check out array binding for details.
Here is a plunker for it.
I have these two custom Polymer Elements (Polymer 1.0.3):
Displays text to be translated.
Displays button to trigger loading of translation.
I also have a Behavior that holds the translations (json object) and contains all the functions that make translation possible.
This is what I expect to happen:
Click the button in Element 2
Translations load into the Behavior
Language selection is set in the Behavior
Text in Element 1 is updated with the translated equivalent
Steps 1 - 3 happen, but 4 doesn't. The text is never updated. I can get it to work if Elements 1 & 2 are combined as the same element, but not if they're separate (any they need to be separate).
If you're wondering about the "kick" property, it's something I learned from Polymer 0.5. It got things working when the two elements were combined, so I'm thinking it'll be be necessary when the elements are separate.
Any idea how I can make this happen? I'm open to alternative paradigms.
Code
This is roughly how my code is laid out. I also made a plunker with a single-page test case.
index.html
<!doctype html>
<html>
<head>
<script src="http://www.polymer-project.org/1.0/samples/components/webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="http://www.polymer-project.org/1.0/samples/components/polymer/polymer.html">
<link rel="import" href="behavior.html">
<link rel="import" href="element1.html">
<link rel="import" href="element2.html">
</head>
<body>
<my-element></my-element>
<another-element></another-element>
</body>
</html>
Element 1
<dom-module id="my-element">
<template>
<p>{{localize(label, kick)}}</p>
</template>
</dom-module>
<script>
Polymer({
is: 'my-element',
behaviors: [
behavior
],
properties: {
label: {
type: String,
value: 'original'
}
}
});
</script>
Element 2
<dom-module id="another-element">
<template>
<button on-click="buttonClicked">load</button>
</template>
</dom-module>
<script>
Polymer({
is: 'another-element',
behaviors: [
behavior
],
buttonClicked: function() {
this.registerTranslation('en', {
original: 'changed'
})
this.selectLanguage('en');
}
});
</script>
Behavior
<script>
var behavior = {
properties: {
kick: {
type: Number,
value: 0
},
language: {
type: String,
value: 'fun'
},
translations: {
type: Object,
value: function() {
return {};
}
}
},
localize: function(key, i) {
if (this.translations[this.language] && this.translations[this.language][key]) {
return this.translations[this.language][key];
}
return key;
},
registerTranslation: function(translationKey, translationSet) {
this.translations[translationKey] = translationSet;
},
selectLanguage: function(newLanguage) {
this.language = newLanguage;
this.set('kick', this.kick + 1);
}
};
</script>
First, although the notion is to have behavior be a conduit for shared data between instances, as written, each instance will have it's own copy of the translations object and the kick property.
Second, even if that data was privatized so it could be shared, the kick binding made via localize(label, kick) is in a different scope from the expression that modifies kick (i.e. this.set('kick', this.kick + 1); [{sic} this could simply be this.kick++;]).
To notify N instances of a change in shared data, one must keep track of those instances. A good way to do this is by attaching event listeners. Another way is simply keeping a list.
Here is an example implementation of your design:
<script>
(function() {
var instances = [];
var translationDb = {};
var language = '';
window.behavior = {
properties: {
l10n: {
value: 0
}
},
attached: function() {
instances.push(this);
},
detached: function() {
this.arrayDelete(instances, this);
},
_broadcast: function() {
instances.forEach(function(i) {
i.l10n++;
});
},
localize: function(key, i) {
if (translationDb[language] && translationDb[language][key]) {
return translationDb[language][key];
}
return key;
},
registerTranslation: function(translationKey, translationSet) {
translationDb[translationKey] = translationSet;
},
selectLanguage: function(newLanguage) {
language = newLanguage;
this._broadcast();
}
};
})();
</script>
<dom-module id="my-element">
<template>
<p>{{localize(label, l10n)}}</p>
</template>
<script>
Polymer({
behaviors: [
behavior
],
properties: {
label: {
type: String,
value: 'original'
}
}
});
</script>
</dom-module>
<dom-module id="another-element">
<template>
<button on-tap="buttonClicked">load</button>
</template>
<script>
Polymer({
behaviors: [
behavior
],
buttonClicked: function() {
this.registerTranslation('en', {
original: 'changed'
});
this.selectLanguage('en');
}
});
</script>
</dom-module>