Material-design-lite: it is difficult to call componentHandler.upgradeElement when use material-design-lite in two-way databinding js framework, like Aurelia or Angular

Created on 9 Jul 2015  路  11Comments  路  Source: google/material-design-lite

Could material-design-lite add support automatically register and render after page loaded, by some config?

As description in GETTING STARTED/Use MDL on dynamic websites
Material Design Lite will automatically register and render all elements marked with MDL classes upon page load. However in the case where you are creating DOM elements dynamically you need to register new elements using the upgradeElement function.

when I use material-design-lite in Aurelia or Angular, which support two-way databinding, and web component concept, it is difficult to call componentHandler.upgradeElement or componentHandler.upgradeDom();

code like below, mdl-js-ripple-effect in

  •     <button id='Add' type='button' click.delegate='nextButtonEffective()' class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" >Button ${iButtonEffective} Got Effect, Click to Next Button</button>
        <ul>
          <li repeat.for='i of 10' >
            <button href='' class="mdl-button mdl-js-button mdl-button--raised ${i==$parent.iButtonEffective?'mdl-button--colored mdl-js-ripple-effect':''}">${i}</button>
            <br/>
          </li>
        </ul>
    
    export class Welcome{
      iButtonEffective = 1;
    
      nextButtonEffective()
      {
        this.iButtonEffective ++;
        if(this.iButtonEffective >= 10)
        {
        this.iButtonEffective %= 10;
        }
      }
    
  • Most helpful comment

    @liuyuanhuo
    I had a similar problem and came up with following solution taking advantage of Mutation Observers.
    It is still not perfect but it is certainly less memory consuming than calling a function which check all the DOM every 200ms.

    var observer = new MutationObserver(function(mutations) {
            var upgrade = false;
    
            for (var i = 0; i < mutations.length; i++) {
                if (mutations[i].addedNodes.length > 0) {
                    upgrade = true;
                    break;
                } 
            }
            if (upgrade) {
                // If there is at least a new element, upgrade the DOM.
                // Note: upgrading elements one by one seems to insert bugs in MDL 
                window.componentHandler.upgradeDom();
            }
        });
    observer.observe(document, {
        childList : true,
        subtree : true
    });
    

    In my use-case, the classes don't change, so I check only for new elements.
    But the Mutation Observer API is able to also detect attribute changes. Just adapt the callback to your needs.

    Again, it is only a workaround, until MDL supports "automatically dynamic" websites.

    All 11 comments

    You can call componentHandler.upgradeAllRegistered() when bootstrapping your angular app, or wrap inside a $timeout call when $viewContentLoaded is fired for a view. This should handle registering your elements.

    I add code below to upgrade, and click on buttons got some error like image at the end

        <script src="https://storage.googleapis.com/code.getmdl.io/1.0.0/material.js"></script>
    
        <script>
          setInterval("upgradeMDL();", 100);
          function upgradeMDL() {
            componentHandler.upgradeDom();
            //componentHandler.upgradeDom();
            //componentHandler.upgradeAllRegistered();
          }
        </script>
    

    material.js:3646 Uncaught TypeError: Cannot read property 'classList' of nullMaterialRipple.upHandler_

    qq 20150709182744

    You need to call it when the DOM is ready.

    Sorry, but this falls too far into the scope of general support vs a problem with MDL directly. It would be best for you to ask for help on StackOverflow with the appropriate tags (material-design-lite and angular has some as well). This way the wider community looking for help with these kinds of setups can find it more easily.

    material.js:3646 Uncaught TypeError: Cannot read property 'classList' of nullMaterialRipple.upHandler_

    the error was cause by mdl-js-ripple-effect and mdl-js-button class not add to dom at the same time. I think it is necessary to support separate the mdl-js-ripple-effect and mdl-js-button.
    code in material.js cause this issue

    /**
     * Initialize element.
     */
    MaterialButton.prototype.init = function() {
      'use strict';
    
      if (this.element_) {
        if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) {    // this line would return false first time, and later this will not excute because it had upgraded
    
          var rippleContainer = document.createElement('span');
    

    and also, It need downgrade an element when set class with mdl-js-ripple-effect mdl-js-button to non mdl-js-ripple-effect mdl-js-button

    @kelsmj
    I solve this problem by setInterval, thank you.
    and could you consider add a method like downgradeAllComponentThatNotInClassInternal to material.js?

    at main index.html

      <body aurelia-app="animation-main" onload='setIntervalSyncMDL()'>
    
        <script>
          function setIntervalSyncMDL()
          {
            setInterval("syncMDL();", 200);
          }
          function syncMDL() {
            componentHandler.downgradeAllComponentThatNotInClass();
            componentHandler.upgradeDom();
          }
        </script>
    

    and add code below to material.js

    // add this function 
        /**
         * Downgrade all component that cssClass Not In element's class 
         *
         * @param {*} nodes
         */
      function downgradeAllComponentThatNotInClassInternal() {
    
          for (var n = 0; n < createdComponents_.length; n++) {
              var component = createdComponents_[n];
              if (!component.element_.classList.contains(component[componentConfigProperty_].cssClass))
              {
                  deconstructComponentInternal(component);
              }
          }
      }
    
      // Now return the functions that should be made public with their publicly
      // facing names...
      return {
        upgradeDom: upgradeDomInternal,
        upgradeElement: upgradeElementInternal,
        upgradeAllRegistered: upgradeAllRegisteredInternal,
        registerUpgradedCallback: registerUpgradedCallbackInternal,
        register: registerInternal,
        downgradeElements: downgradeNodesInternal
    // add blow line
          ,downgradeAllComponentThatNotInClass : downgradeAllComponentThatNotInClassInternal
      };
    

    @nicolasgarnier @Garbee
    could you have a look at this issue, and enhance the mdl to support angular?

    @nicolasgarnier @Garbee
    I have do some modify and create a pull request #949, could you have a look , do some modify and add it.

    then I can use it like below:

     <body aurelia-app="animation-main" onload='setIntervalSyncMDL()'>
    
        <script>
          function setIntervalSyncMDL()
          {
            setInterval("syncMDL();", 200);
          }
          function syncMDL() {
            componentHandler.syncElementsThatCssClassChanged();
          }
        </script>
    

    @liuyuanhuo
    I had a similar problem and came up with following solution taking advantage of Mutation Observers.
    It is still not perfect but it is certainly less memory consuming than calling a function which check all the DOM every 200ms.

    var observer = new MutationObserver(function(mutations) {
            var upgrade = false;
    
            for (var i = 0; i < mutations.length; i++) {
                if (mutations[i].addedNodes.length > 0) {
                    upgrade = true;
                    break;
                } 
            }
            if (upgrade) {
                // If there is at least a new element, upgrade the DOM.
                // Note: upgrading elements one by one seems to insert bugs in MDL 
                window.componentHandler.upgradeDom();
            }
        });
    observer.observe(document, {
        childList : true,
        subtree : true
    });
    

    In my use-case, the classes don't change, so I check only for new elements.
    But the Mutation Observer API is able to also detect attribute changes. Just adapt the callback to your needs.

    Again, it is only a workaround, until MDL supports "automatically dynamic" websites.

    for Aurelia you can take a look at https://github.com/redpelicans/aurelia-material

    @Garbee Please take a look for the fun of Polymer DOM dynamic loading & MDL elements
    @Drestin Look, this is yet another MDL loader using Polymer 1.0

    I use Polymer.importHref for dynamic MDL, so i prefer to use updateElement function, to update all child MDL nodes

    The key elements of successful dynamic MDL element loading in Polymer are calls to

    Polymer.dom(root).appendChild(newElement) for element root and window.componentHandler.upgradeElement(newElement), which is called for each child node for loaded Polymer dom module.

    Compete Polymer importer for MDL classes:

      function Loader(url) {
        this._pageLoaded = false;
        this._options = {};
        this._sync = function () {};
      }
            app.updateElement = function(html) {
              [].forEach.call(html.children, function (el) {
                app.updateElement(el);
              });
              window.componentHandler.upgradeElement(html);
            };
    
            Loader.prototype.bootstrap = function (container, tag, url) {
              var loader = this;
              if (window.Polymer === undefined) {
                return;
              }
              var importer = document.createElement('x-import');
              importer.elementUrl = url;
              importer.elementContainer = container;
              importer.elementTag = tag;
              importer.import(function (newElement) {
                app.updateElement(newElement);
                loader._options[tag] = true;
                loader._sync(loader._options);
                console.log(tag);
              }, function () {
                loader._options[tag] = false;
                loader._sync(loader._options);
                console.error(tag);
              });
            };
    
            Loader.prototype.sync = function (sync) {
              this._sync = sync;
            };
    
            app.loader = new Loader();
    

    I use it as follows:

                  app.loader.sync(function (options) {
                    if (options['x-load'] && options['x-login']) {
                      // do some fun staff
                    }
                  });
                  app.loader.bootstrap('load', 'x-load', 'elements/components/load/load.html');
                  app.loader.bootstrap('login', 'x-login', 'elements/components/login/login.html');
    

    Files:

    <!-- elements/components/load/load.html -->
    <dom-module id='x-load'>
      <template>
        <style>
          :host {
            transform: translate(-50%, -50%);
            position: absolute;
            left: 50%;
            top: 50%;
          }
        </style>
        <div class="mdl-spinner mdl-js-spinner is-active is-upgraded"></div>
      </template>
      <script>
        Polymer({
          is: 'x-load'
        });
      </script>
    </dom-module>
    
    <!-- elements/components/login/login.html -->
    <dom-module id='x-login'>
      <template>
        <style>
        </style>
        <div class="mdl-layout__container">
          <div class="layout-center mdl-layout mdl-js-layout mdl-color--grey-100">
            <main id="mdl-layout__content" class="layout__content mdl-layout__content">
              <div class="mdl-card mdl-shadow--2dp">
                <div class="mdl-card__title mdl-color--primary mdl-color-text--white">
                  <a class="mdl-color--primary mdl-color-text--white" href="http://www.groupe-auchan.com/">
                    <button class="mdl-button mdl-js-button mdl-button--icon">
                      <i class="material-icons">home</i>
                    </button>
                  </a>
                  <div class="mdl-card__title-text">Auchan</div>
                </div>
                <div class="mdl-card__media">
                  <div class="i"></div>
                </div>
                <div class="mdl-card__supporting-text">
                  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" id="username" type="text" value='{{username::input}}'>
                    <label class="mdl-textfield__label" for="username">袙胁械写懈褌械 懈屑褟 锌芯谢褜蟹芯胁邪褌械谢褟...</label>
                  </div>
                  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" id="password" type="password" value='{{password::input}}'>
                    <label class="mdl-textfield__label" for="userpass">袙胁械写懈褌械 锌邪褉芯谢褜...</label>
                  </div>
                </div>
                <div class="mdl-card__actions mdl-card--border">
                  <button class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" on-click="login">袙褏芯写</button>
                </div>
              </div>
            </main>
          </div>
        </div>
        <div aria-live="assertive" aria-atomic="true" aria-relevant="text" id='toast' class="mdl-js-snackbar mdl-snackbar">
          <div class="mdl-snackbar__text"></div>
          <button class="mdl-snackbar__action" type="button"></button>
        </div>
      </template>
      <script>
        Polymer({
          is: 'x-login',
          properties: {
            username: String,
            password: String
          },
          login: function(e) {
            var username = this.username;
            var password = this.password;
    
            if (typeof username === 'undefined' || username.length === 0) {
              this.$.username.focus();
              return;
            }
    
            if (typeof password === 'undefined' || password.length === 0) {
              this.$.password.focus();
              return;
            }
    
            var consoleError = app.consoleError;
            app.error = [];
            app.consoleError = function(message) {
              var data = {
                message: '#' + (++app.counter) + ' ' + message
              };
              app.error.push(data);
            };
            app.user.authenticate(username, password, function(success) {
              app.loader.bootstrap('page', 'x-application', 'elements/components/application/application.html');
              app.onReady();
              if (app.error.length > 0) {
                app.consoleError(app.error[app.error.length - 1]);
              }
              app.error = [];
              app.consoleError = consoleError;
            }, function(error) {
              app.onReady();
              if (app.error.length > 0) {
                app.consoleError(app.error[app.error.length - 1]);
              }
              app.error = [];
              app.consoleError = consoleError;
            });
          }
        });
      </script>
    </dom-module>
    
    
    <!-- app.html -->
    <link rel="import" href="bower_components/polymer/polymer.html">
    <dom-module id='x-import'>
      <script>
        // element registration
        Polymer({
          is: 'x-import',
    
          properties: {
            elementUrl: String,
            elementTag: String,
            elementContainer: String,
            loading: {
              type: Boolean,
              value: false,
              nolify: true,
              readonly: true
            },
            loaded: {
              type: Boolean,
              value: false,
              notify: true,
              readonly: true
            }
          },
    
          import: function(successCallback, errorCallback) {
            if (!this.loaded && !this.loading) {
              this.loading = true;
              this.importHref(this.elementUrl, function() {
                  try {
                    var newElement = document.createElement(this.elementTag);
                    Polymer.dom(document.getElementById(this.elementContainer)).appendChild(newElement);
                    this.loaded = true;
                    this.loading = false;
                    successCallback(newElement);
                  } catch (e) {
                    console.error(e.message);
                    // loading error
                    this.loaded = false;
                    this.loading = false;
                    errorCallback();
                  } finally {
                    //
                  }
                },
                function() {
                  // loading error
                  this.loaded = false;
                  this.loading = false;
                  errorCallback();
                });
            }
          }
        });
      </script>
    </dom-module>
    
    Was this page helpful?
    0 / 5 - 0 ratings