Ultimately, I want to select individual states from this geochart. But this question is limited to getting the observer labeled _computeData to fire in response to mutating the array of selected states.
Reproduce the problem with the following steps:
- Open this jsBin.
- Clear the console.
- Select the state of Texas.
Note the console reads:
You selected: Colorado,South Dakota,Texas
which is expected per this line:
console.log('You selected: ' + this.selected); // Logs properly
However, I expect the console to also read:
selected
per this line:
_computeData: function() {
console.log('selected'); // Does not log properly; function not called?
...
which should be called by the following set of observers.
http://jsbin.com/wuqugigeha/1/edit?html,console,output...
observers: [
'_computeData(items.*, selected.*)',
'_dataChanged(data.*)',
],
...
Question
What's going on here? Why isn't the observer calling the _computeData method? What can be done to get the method to fire after mutating the selected array?
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<base href="https://polygit.org/components/">
<script src="webcomponentsjs/webcomponents-lite.min.js"></script>
<link href="polymer/polymer.html" rel="import">
<link href="google-chart/google-chart.html" rel="import"> </head>
<body>
<dom-module id="x-element"> <template>
<style>
google-chart {
width: 100%;
}
</style>
<br><br><br><br>
<button on-tap="_show">Show Values</button>
<button on-tap="clearAll">Clear All</button>
<button on-tap="selectAll">Select All</button>
<div>[[selected]]</div>
<google-chart
id="geochart"
type="geo"
options="[[options]]"
data="[[data]]"
xon-google-chart-select="_onGoogleChartSelect">
</google-chart>
</template>
<script>
(function() {
Polymer({
is: 'x-element',
properties: {
items: {
type: Array,
value: function() {
return [ 'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming', ].sort();
},
},
color: {
type: String, // '#455A64'
value: function() {
return 'blue';
}
},
options: {
type: Object,
notify: true,
reflectToAttribute: true,
computed: '_computeOptions(color)',
},
selected: {
type: Array,
notify: true,
reflectToAttribute: true,
value: function() {
return [];
},
//observer: '_computeData', // Unsuccessfully tried this
},
data: {
type: Array,
notify: true,
reflectToAttribute: true,
//computed: '_computeData(items.*, selected.*)', // Unsuccessfully tried this
},
},
observers: [
'_computeData(items.*, selected.*)',
'_dataChanged(data.*)',
],
// Bind select event listener to chart
ready: function() {
var _this = this;
this.$.geochart.addEventListener('google-chart-select', function(e) {
this._onGoogleChartSelect(e);
}.bind(_this));
},
_computeOptions: function() {
return {
region: 'US',
displayMode: 'regions',
resolution: 'provinces',
legend: 'none',
defaultColor: 'white',
colorAxis: {
colors: ['#E0E0E0', this.color],
minValue: 0,
maxValue: 1,
}
}
},
// On select event, compute 'selected'
_onGoogleChartSelect: function(e) {
var string = e.path[0].textContent.split('Select')[0].trim(), // e.g. 'Ohio'
selected = this.selected, // Array of selected items
index = selected.indexOf(string);
// If 'string' is not in 'selected' array, add it; else delete it
if (index === -1) {
selected.push(string);
selected.sort();
} else {
selected.splice(index, 1);
}
this.set('selected', selected);
console.log('You selected: ' + this.selected); // Logs properly
// Next step should be '_computeData' per observers
},
// After 'items' populates or 'selected' changes, compute 'data'
_computeData: function() {
console.log('selected'); // Does not log properly; function not called?
var data = [],
items = this.items,
selected = this.selected,
i = items.length;
while (i--) {
data.unshift([items[i], selected.indexOf(items[i]) > -1 ? 1 : 0]);
}
data.unshift(['State', 'Select']);
this.set('data', data);
},
// After 'data' changes, redraw chart
// Add delay to avoid 'google not defined' error
_dataChanged: function() {
var _this = this;
setTimeout(function() {
_this._drawChart();
}.bind(_this), 100)
},
// After delay, draw chart
_drawChart: function() {
var data = this.data,
dataTable = this.$.geochart._createDataTable(data);
console.log(dataTable);
this.$.geochart._chartObject.draw(dataTable, this.options);
},
clearAll: function() {
this.set('selected', []);
},
selectAll: function() {
this.set('selected', this.items);
},
_show: function() {
console.log('items: ' + this.items);
console.log('selected: ' + this.selected);
console.log('data: ' + this.data);
},
});
})();
</script>
</dom-module>
<x-element color="red" selected='["Colorado", "South Dakota"]'></x-element>
</body>
</html>
Your problem is here:
if (index === -1) {
selected.push(string);
selected.sort();
} else {
selected.splice(index, 1);
}
this.set('selected', selected);
Polymer's data-handling methods like set allow you to give Polymer specific information about how your data is changing, allowing Polymer to make very fast DOM updates.
In this case, you are doing work where Polymer cannot see it (i.e. the array manipulations), and then asking set to figure out what happened. However, when you call this.set('selected', selected);, Polymer sees that the identity of selected hasn't changed (that is, it's the same Array object as before) and it simply stops processing. (Fwiw, this is a common problem, so we are considering a modification that will go ahead and examine the array anyway.)
The solution is two-fold:
1) In the case where you are sorting the array, create a fresh array reference to for set via slice() :
if (index === -1) {
selected.push(string);
selected.sort();
this.set('selected', selected.slice());
2) In the case where you are simply splicing, use the splice helper function :
} else {
this.splice('selected', index, 1);
}
Ideally you avoid sorting your array, then you can use this.push directly.
Note: with these changes _computeData is being called, but now it's being called way too many times. Partly this is due to observing selected.* which will fire for selected, selected.length, and selected.splices. Observing selected.length instead of selected.* might help.
UPDATE
There were three other major problems with your example:
datais bound to to thegoogle-chart(i.e.data="[[data]]") so the chart will redraw itself whendatachanges and we can remove_drawChartcompletely._computeData(items.*, selected.*)is too aggressive, asselected.*will fire for changes in 'selected.length', 'selected.splices', andselected. Instead use_computeData(items, selected.length).google-chartitself appears to be buggy. In particular, it's owndrawChartis not set up to be properly re-entrant. The most obvious problem is that every time the chart draws, it adds an additional selection listener (which causes multiplyingchart-selectevents to fire on your application). I would appreciate it if you would file a bug ongoogle-chartand link back to this SO. :)
Here is a modified version where I've monkey patched google-chart.drawChart, fixed the other two major problems, and made a variety of smaller repairs.
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<base href="https://polygit.org/components/">
<script src="webcomponentsjs/webcomponents-lite.min.js"></script>
<link href="polymer/polymer.html" rel="import">
<link href="google-chart/google-chart.html" rel="import"> </head>
<body>
<dom-module id="x-element"> <template>
<style>
google-chart {
width: 100%;
}
</style>
<br><br><br><br>
<button on-tap="_show">Show Values</button>
<button on-tap="clearAll">Clear All</button>
<button on-tap="selectAll">Select All</button>
<div>[[selected]]</div>
<google-chart
id="geochart"
type="geo"
options="[[options]]"
data="[[data]]"
on-google-chart-select="_onGoogleChartSelect">
</google-chart>
</template>
<script>
(function() {
// monkey-patching google-chart
var gcp = Object.getPrototypeOf(document.createElement('google-chart'));
gcp.drawChart = function() {
if (this._canDraw) {
if (!this.options) {
this.options = {};
}
if (!this._chartObject) {
var chartClass = this._chartTypes[this.type];
if (chartClass) {
this._chartObject = new chartClass(this.$.chartdiv);
google.visualization.events.addOneTimeListener(this._chartObject,
'ready', function() {
this.fire('google-chart-render');
}.bind(this));
google.visualization.events.addListener(this._chartObject,
'select', function() {
this.selection = this._chartObject.getSelection();
this.fire('google-chart-select', { selection: this.selection });
}.bind(this));
if (this._chartObject.setSelection){
this._chartObject.setSelection(this.selection);
}
}
}
if (this._chartObject) {
this._chartObject.draw(this._dataTable, this.options);
} else {
this.$.chartdiv.innerHTML = 'Undefined chart type';
}
}
};
Polymer({
is: 'x-element',
properties: {
items: {
type: Array,
value: function() {
return [ 'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming', ].sort();
},
},
color: {
type: String, // '#455A64'
value: 'blue'
},
options: {
type: Object,
computed: '_computeOptions(color)',
},
selected: {
type: Array,
value: function() {
return [];
}
},
data: {
type: Array,
computed: '_computeData(items, selected.length)'
},
},
_computeOptions: function() {
return {
region: 'US',
displayMode: 'regions',
resolution: 'provinces',
legend: 'none',
defaultColor: 'white',
colorAxis: {
colors: ['#E0E0E0', this.color],
minValue: 0,
maxValue: 1,
}
}
},
// On select event, compute 'selected'
_onGoogleChartSelect: function(e) {
console.log('_onGoogleChartSelect: ', e.detail)
var string = e.path[0].textContent.split('Select')[0].trim(), // e.g. 'Ohio'
selected = this.selected, // Array of selected items
index = selected.indexOf(string);
// If 'string' is not in 'selected' array, add it; else delete it
if (index === -1) {
this.push('selected', string);
} else {
this.splice('selected', index, 1);
}
// Next step should be '_computeData' per observers
console.log('_select:', this.selected);
},
// After 'items' populates or 'selected' changes, compute 'data'
_computeData: function(items, selectedInfo) {
console.log('_computeData');
var data = [],
selected = this.selected,
i = items.length;
while (i--) {
data.unshift([items[i], selected.indexOf(items[i]) > -1 ? 1 : 0]);
}
data.unshift(['State', 'Select']);
return data;
},
clearAll: function() {
this.set('selected', []);
},
selectAll: function() {
this.set('selected', this.items);
},
_show: function() {
console.log('items: ' + this.items);
console.log('selected: ' + this.selected);
console.log('data: ' + this.data);
},
});
})();
</script>
</dom-module>
<x-element color="red" selected='["Colorado", "South Dakota"]'></x-element>
</body>
</html>
HTH
Random extra stuff:
var _this = this;
setTimeout(function() {
_this._drawChart();
}.bind(_this), 100)
You need to either capture the value of this (_this) or use bind, but it doesn't make sense to do both.
setTimeout(function() {
this._drawChart();
}.bind(this), 100)
... is enough.
Here is an example of implementation of the accepted solution.
http://jsbin.com/xonanucela/edit?html,console,output<!doctype html>
<head>
<meta charset="utf-8">
<base href="https://polygit.org/components/">
<script src="webcomponentsjs/webcomponents-lite.min.js"></script>
<link href="paper-button/paper-button.html" rel="import">
</head>
<body>
<x-element></x-element>
<dom-module id="x-element">
<template>
<br><br>
<paper-button on-tap="_addNew">Click To Add</paper-button>
<p>
<strong>Items</strong>:
<template is="dom-repeat" items="{{items}}">
<span>[[item]] </span>
</template>
</p>
</template>
<script>
Polymer({
is: 'x-element',
properties: {
items: {
type: Array,
value: function() {
return ['foo'];
}
}
},
_addNew: function() {
var a = this.items; // Clones array
a.push('bar'); // Updates "value"
console.log('a', a);
this.set('items', a.slice()); // Updates "identity"
console.log('items', this.items);
},
});
</script>
</dom-module>
</body>
来源:https://stackoverflow.com/questions/35362992/polymer-1-x-observers