polymer class binding inside dom-repeat not working

847 views Asked by At

I'm trying to bind a computed value to an element inside a dom-repeat template.

<iron-selector class="dropdown-content" selected="0">
   <template is="dom-repeat" items="[[items]]">
      <div class$="item {{_itemClass(selectedItems)}}" data-value="[[item]]" on-click="_onItemClick">[[item]]</div>
   </template>
</iron-selector>

My goal here is to update the dropdown items styling when the user selects one of them. When this happens, I update the selectedItems array by pushing the item value to it. This will trigger the rendering of the selection box but will fail to call the _itemClass(selectedItems) expression.

What am I missing here? I read in the documentation that you must specify the properties that will trigger the binding and I'm doing that by adding the selectedItems to the binding expression with no luck at all.

I attached a snippet with the current behaviour.

Any help will be welcome.

<!doctype html>
<head>
  
  <meta charset="utf-8">
  <!-- polygit -->
  <base href="https://polygit.org/components/"> <!-- saves typing! -->
  <script src="webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="iron-flex-layout/iron-flex-layout-classes.html">
  <link rel="import" href="iron-icons/iron-icons.html">
  <link rel="import" href="iron-dropdown/iron-dropdown.html">
  <link rel="import" href="iron-selector/iron-selector.html">
  <!-- style -->
  <style>
    x-example {
      width: 250px;
    }
  </style>
</head>
<body>
  
<x-example></x-example>
  
<dom-module id="x-example">
  <style include="iron-flex iron-flex-alignment"></style>
  <style>
    :host {
      position: relative;
      border: 1px solid blue;
      display: block;
    }
    
    .body {
      @apply --layout-horizontal;
    }

    .body .selection {
      border: 1px solid green;
      margin: 2px;
      flex-grow: 1
    }

    .body .selection .item {
      display: inline-block;
      margin: 2px;
      background: lightgreen;
      padding: 2px 5px;
      border: 1px solid green;
      border-radius: 4px;
      color: darkblue;
    }

    .body .selection .item .text {
      display: inline-block;
    }

    .body .selection .item iron-icon {
      width: 18px;
      cursor: pointer;
    }

    .body .dropdown {
      border: 1px solid yellow;
      margin: 2px;
      cursor: pointer;
      min-height: 40px;
    }

    .body .dropdown iron-icon {
      margin: auto;
    }

    .options iron-dropdown {
      border: 1px solid grey;
      border-radius: 6px;
      overflow: hidden;
    }

    .options .item {
      cursor: pointer;
    }

    iron-selector>* {
      padding: 8px;
      background: aqua;
    }

    .horizontal-section {
      padding: 0;
    }

    .iron-selected {
      background-color: var(--google-blue-500);
      color: white;
    }
    
  </style>
  <template>
 
        <div class="body">
            <div class="flex selection">
                <template is="dom-repeat" items="[[selectedItems]]">
                    <div class="item">
                        <div class="text">[[item]]</div>
                        <iron-icon class="dropdown-trigger" icon="icons:clear" on-click="_onRemoveItem"></iron-icon>
                    </div>
                </template>
            </div>
            <div class="dropdown" on-click="_toggleDropdown">
                <iron-icon icon="icons:arrow-drop-down"></iron-icon>
            </div>
        </div>
        <div class="options">
            <iron-dropdown auto-fit-on-attach="true" id="dropdown" horizontal-align="left" vertical-align="top">
                <iron-selector class="dropdown-content" selected="0">
                    <template is="dom-repeat" items="[[items]]">
                        <div class$="item {{_itemClass(selectedItems)}}" data-value="[[item]]" on-click="_onItemClick">[[item]]</div>
                    </template>
                </iron-selector>
            </iron-dropdown>
        </div>
    
  </template>
  <script>
    
    // only need this when both (1) in the main document and (2) on non-Chrome browsers
    addEventListener('WebComponentsReady', function() {
      
      Polymer({
        is: "x-example",
        properties: {
          items: {
            type: Array,
            value: ["One", "Two", "Three"]
          },
          selectedItems: {
            type: Array,
            value: []
          }
        },
        _toggleDropdown: function() {
          this.$.dropdown.toggle();
        },
        _onItemClick: function(e) {
          var value = e.srcElement.dataValue;
          if (this.selectedItems.indexOf(value) >= 0) {
            return;
          }
          this.push("selectedItems", value)
          this._refreshDropdown();
        },
        _refreshDropdown: function() {
          this.async(function() {
            this.$.dropdown.position();
          })
        },
        _onRemoveItem(e) {
          let index = this.selectedItems.indexOf(e.srcElement.dataValue);
          this.splice("selectedItems", index, 1);
          this._refreshDropdown();
        },
        _itemClass: function() {
          console.log("item class called")
        }
        
      });
      
    });
    
  </script>
</dom-module>
  
</body>

2

There are 2 answers

0
Daniel Radliński On BEST ANSWER

Your binding is _itemClass(selectedItems) which only triggers when the variable selectedItems changes, not when the array mutates.

You've got two options:

1) Either change your binding to _itemClass(selectedItems.*), which will get triggered by this.push and other array mutating methods

2) Change the variable each time:

this.set('selectedItems', [].concat(this.selectedItems).concat(value));

A working example with the first solution (and 2nd solution commented out) is available here

3
Kristfal On

I haven't tested your issue, but I'm using a similar pattern myself. My guess is that Polymer isn't supporting declaration of a class and a bound class within the same tag.

I use attributes and attribute selectors instead, which works just as expected.

Can you try to change the class to an attribute and style based on attribute selectors? Here is the relevant code:

<style>

.item[selected] {
  font-weight: bold;
}

</style>

...

<template is="dom-repeat" items="[[items]]">
  <div class="item" selected$="[[_computeSelected]]" on-tap="_onItemTap">[[item]]</div>
</template>


_onItemTap: function(event) {
  const item = event.model.item;
},

_computeSelected: function(item) (
  return yourSelectionLogic ? true : false;
}

Two other notes: Use on-tap instead of on-click. It makes mobile touch events normalized with desktop events. Also, you don't need to bind the data value for each item as it is included through the tap-event when it is triggered within a dom-repeat.