0

Notice: This question is not a duplicate of this SO question because the first question was solved with a syntax correction in the markup. This question involves the JavaScript and has a different error message.


My Goal

I am trying to preset a custom element property named selected to a value of ["Colorado", "South Dakota"] via a declarative attribute.

Expected vs Actual Behavior

I expect to see the page load a map with an outline of all the U.S. states. With no errors in the console. And the states of Colorado and South Dakota pre-selected and colored blue.

Instead, I see a blank page with no map at all. The page prints the correct selected variable: Colorado,South Dakota. A set of errors in the console begins with the following:

console.error

TypeError: Cannot read property 'length' of undefined

Possible problems and solutions

The problem is that the items property has not yet been populated by the ready function before _doAll tries to access it to set the selected states.

I tracked the issue to the following spot in the code:

http://jsbin.com/qezogimude/1/edit?html,console,output
_doAll: function(verb) {
  ...
  var items = this.items, // undefined
      i = items.length; // error, crashes
  ...

I also (unsuccessfully) tried the following (separately):

  • async
  • while(this.items === undefined){...

Recreating the problem

The following steps will recreate the problem:

  1. Open this jsBin.
  2. Notice the map loads properly in the output pane. (And works properly when states are selected and de-selected.)
  3. Scroll down to the bottom of the page to the <x-element> tag.
  4. Delete the x character in front of the selected attribute so the complete attribute reads: selected='["Colorado", "South Dakota"]'.
  5. Notice the map disappeared, no longer loads properly and the errors described above appear in the console.
  6. Replace the x you just deleted and notice the map loads and works properly again.

Question

How do I get _doAll to wait for ready to populate the items property? Please include a working jsBin example of your solution if you can.

http://jsbin.com/qezogimude/1/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="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%;
          max-height: 300px;
        }
      </style>
      <button on-tap="_show">Show</button>
      <div>[[selected]]</div>
      <google-chart
        id="geochart"
        type="geo"
        options="{{options}}"
        data="{{items}}"
        xon-google-chart-select="_onGoogleChartSelect"></google-chart>
    </template>

    <script>
      (function(){
        Polymer({
          is: 'x-element',
          /** /
           * Fired when user selects chart item.
           *
           * @event us-map-select
           * @param {object} detail Alpabetized array of selected state names.
          /**/
          properties: {
            selected: {
              type: Array,
              notify: true,
              //reflectToAttribute: true,
            },
            items: {
              type: Array,
              notify: true,
              reflectToAttribute: true,
            },
            color: {
              type: String, // '#455A64'
              value: function() {
                return 'blue';
              }
            },
            options: {
              type: Object,
              notify: true,
              reflectToAttribute: true,
              computed: '_computeOptions(color)'
            },
            itemIndices: {
              type: Object,
              computed: '_computeItemIndices(items)',
            },
          },

          observers: [
            '_selectedChanged(selected.*)'
          ],

          ready: function() {
            var a = [['State', 'Select'], ['Alabama', 0], ['Alaska', 0], ['Arizona', 0], ['Arkansas', 0], ['California', 0], ['Colorado', 0], ['Connecticut', 0], ['Delaware', 0], ['Florida', 0], ['Georgia', 0], ['Hawaii', 0], ['Idaho', 0],  ['Illinois', 0], ['Indiana', 0], ['Iowa', 0], ['Kansas', 0], ['Kentucky', 0], ['Louisiana', 0], ['Maine', 0], ['Maryland', 0], ['Massachusetts', 0], ['Michigan', 0], ['Minnesota', 0], ['Mississippi', 0], ['Missouri', 0], ['Montana', 0], ['Nebraska', 0], ['Nevada', 0], ['New Hampshire', 0], ['New Jersey', 0], ['New Mexico', 0], ['New York', 0], ['North Carolina', 0], ['North Dakota', 0], ['Ohio', 0], ['Oklahoma', 0], ['Oregon', 0], ['Pennsylvania', 0], ['Rhode Island', 0], ['South Carolina', 0], ['South Dakota', 0], ['Tennessee', 0], ['Texas', 0], ['Utah', 0], ['Vermont', 0], ['Virginia', 0], ['Washington', 0], ['West Virginia', 0], ['Wisconsin', 0], ['Wyoming', 0]];
            this.set('items', a);
            var _this = this; 
            this.$.geochart.addEventListener('google-chart-select', function(e){this._onGoogleChartSelect(e)}.bind(_this));
          },

          _computeItemIndices: function(a) {
            var out = {},
                i = a.length;
            while(i--){
              out[a[i][0]] = i;
            }
            return out;
          },

          _onGoogleChartSelect: function(e) {
            var s = e.path[0].textContent.split('Select')[0].trim(), // e.g. 'Ohio'
                temp = [],
                a = this.items,
                index = this.itemIndices[s], // e.g. 35
                i = a.length;
            this.set('items.' + index + '.1', a[index][1] ? 0 : 1);
            while(i---1){
              /** /
              if(s === a[i][0]){
                this.set('items.' + i + '.1', a[i][1] ? 0 : 1);
                //this.items[i][1] = a[i][1] ? 0 : 1;
              }
              /**/
              if(a[i][1]){
                temp.push(a[i][0]);
              }
            }
            temp.sort();
            this.set('selected', temp);
            this._drawChart();
            //console.log(this.selected);
          },

          _drawChart: function() {
            this.$.geochart._dataTable=this.$.geochart._createDataTable(this.items);
            this.$.geochart._chartObject.draw(this.$.geochart._dataTable,
                                              this.$.geochart.options);
          },

          doAll: function(verb) {
            verb = verb || 'clear'; // verb: 'clear'(default)|'select'
            verb = (verb === 'select') ? 'select' : 'clear';
            this._doAll(verb);
            this._drawChart();
          },

          _doAll: function(verb) {
            var resetSelect = (verb && verb === 'some') ? false : true;
            verb = verb || 'clear'; // verb: 'clear'(default)|'select'|'some'
            verb = (verb === 'select') ? 'select' : 'clear';
            var temp = [];
            var items = this.items, // undefined
                i = items.length; // error, crashes
            switch(verb) {
              case 'select':
                while(i---1){
                  items[i][1] = 1;
                  temp.push(items[i][0]);
                }
                break;
              case 'clear':
                while(i---1){
                  items[i][1] = 0;
                }
                break;
              default:
                break;
            }
            this.set('items', items);
            if(resetSelect){
              temp.sort();
              this.set('selected', temp);
            }
          },

          _selectedChanged: function(e) {
            var a = e.base,
                i = a.length;
            this._doAll('some');
            while(i--){
              var index = this.itemIndices[a[i]];
              this.set('items.' + index + '.1', 1);
            }
            this._drawChart();
            this.fire('us-map-select', this.selected)
            console.log(this.selected);
          },

          _computeOptions: function(str) {
            return {
              region: 'US',
              displayMode: 'regions',
              resolution: 'provinces',
              legend: 'none',
              defaultColor: '#F5F5F5',
              colorAxis: {
                colors: ['#F5F5F5', str],
                minValue: 0,  
                maxValue: 1,
              }
            }
          },

          _show: function(){
            //this.set('selected', ['Ohio', 'New Mexico']);
            this.doAll();
            //console.log(this.itemIndices);
          },
        });
      })();
    </script>

  </dom-module>

  <x-element xcolor="#455A64"
             xselected='["Colorado", "South Dakota"]'></x-element>

</body>
</html>
Community
  • 1
  • 1
Let Me Tink About It
  • 15,156
  • 21
  • 98
  • 207

1 Answers1

2

Looking at your jsbin, the problem is that your items property doesn't have a default value. I tried setting it to [], but then your _doAll function tried to access the 1st element of it.

You haven't made items observable - so how does it change and how do you recalculate when it does?

I am not sure what doAll is trying to do, but you might want to put some checks in that items is valid as its called somehow as items is created.

I had something similar, and found the best approach was to derive properties from it as computed properties which I then used to drive my elements through data binding. That way all the edge cases that cropped up as the item was being created and had bad values in it went away.

akc42
  • 4,893
  • 5
  • 41
  • 60
  • In response to your answer and questions, I added the following to the question for clarity... The problem is that the `items` property has not yet been populated by the `ready` function before `_doAll` tries to access it (to set the selected states). (The `items` property is initialized by the `ready` method.) How do I get `_doAll` to wait for `ready` to populate the `items` property? – Let Me Tink About It Feb 09 '16 at 12:33
  • I think `ready` is already too late. As I said in my reply I had a similar problem when I had a property `user`, set by databinding and then a function `isManager` which tried to look at the properties of `user` . `isManager` was called via databinding for another object (hidden attribute in my case). It was getting various versions of undefined and blank objects . I solved that problem by making `isManager` a computed property based in `user` and databinding to my `isManager` property. I am suggesting you can do the same. Make `items` a computed property of `selected` – akc42 Feb 09 '16 at 14:56