Skip to content

Component pattern

addyosmani edited this page Dec 4, 2014 · 2 revisions

The following pattern will be used for new components moving forward:

/**
 * A possible component handler interface using the revealing module design
 * pattern.
 * @author Jason Mayes (jmayes@google.com)
 */
var componentHandler = function() {
  'use strict';

  var registeredComponents_ = [];
  var createdComponents_ = [];


  /**
   * Searches registered components for a class we are interested in using.
   * Optionally replaces a match with passed object if specified.
   * @param {string} name The name of a class we want to use.
   * @param {object} opt_replace Optional object to replace match with.
   * @return {Object | false}
   * @private
   */
  function findRegisteredClass_(name, opt_replace) {
    for (var i = 0; i < registeredComponents_.length; i++) {
      if (registeredComponents_[i].className === name) {
        if (opt_replace !== undefined) {
          registeredComponents_[i] = opt_replace;
        }
        return registeredComponents_[i];
      }
    }
    return false;
  }


  /**
   * Searches existing DOM for elements of our component type and upgrades them
   * if they have not already been upgraded.
   * @param {string} jsClass the programatic name of the element class we need
   * to create a new instance of.
   * @param {string} cssClass the name of the CSS class elements of this type
   * will have.
   */
  function upgradeDomInternal(jsClass, cssClass) {
    if (cssClass === undefined) {
      var registeredClass = findRegisteredClass_(jsClass);
      if (registeredClass) {
        cssClass = registeredClass.cssClass;
      }
    }

    var elements = document.querySelectorAll('.' + cssClass);
    for (var n = 0; n < elements.length; n++) {
      upgradeElementInternal(elements[n], jsClass);
    }
  }


  /**
   * Upgrades a specific element rather than all in the DOM.
   * @param {HTMLElement} element The element we wish to upgrade.
   * @param {string} jsClass The name of the class we want to upgrade
   * the element to.
   */
  function upgradeElementInternal(element, jsClass) {
    // Only upgrade elements that have not already been upgraded.
    if (element.getAttribute('data-upgraded') === null) {
      // Upgrade element.
      element.setAttribute('data-upgraded', '');
      var registeredClass = findRegisteredClass_(jsClass);
      if (registeredClass) {
        createdComponents_.push(new registeredClass.classConstructor(element));
      } else {
        // If component creator forgot to register, try and see if
        // it is in global scope.
        createdComponents_.push(new window[jsClass](element));
      }
    }
  }


  /**
   * Registers a class for future use and attempts to upgrade existing DOM.
   * @param {object} config An object containting:
   * {constructor: Constructor, classAsString: string, cssClass: string}
   */
  function registerInternal(config) {
    var newConfig = {
      'classConstructor': config.constructor,
      'className': config.classAsString,
      'cssClass': config.cssClass
    };

    var found = findRegisteredClass_(config.classAsString, newConfig);

    if (!found) {
      registeredComponents_.push(newConfig);
    }
    
    upgradeDomInternal(config.classAsString);
  }


  // Now return the functions that should be made public with their publicly
  // facing names...
  return {
    upgradeDom: upgradeDomInternal,
    upgradeElement: upgradeElementInternal,
    register: registerInternal
  };
}();








/**
 * An example Class constructor for a WSK component. Note capital camel case.
 * @param {HTMLElement} element The element that will be upgraded.
 */
function MaterialComponentClassname(element) {
  'use strict';

  // Example private variable. Uses underscore notation to denote private var.
  this.element_ = element;

  // Other private vars can go here as needed...

  // Initialize instance.
  this.init();
}

/**
 * Store constants in one place so they can be updated easily.
 * @enum {string}
 * @private
 */
MaterialComponentClassname.prototype.Constant_ = {
  /**
   * Name should be descriptive so no comment needed.
   */
  MEANING_OF_LIFE: '42',
  SPECIAL_WORD: 'HTML5'
};

/**
 * Store strings for class names defined by this component that are used in
 * JavaScript. This allows us to simply change it in one place should we
 * decide to modify at a later date.
 * @enum {string}
 * @private
 */
MaterialComponentClassname.prototype.CssClasses_ = {
  /**
   * Class names should use camelCase and be prefixed with the word "material"
   * to minimize conflict with 3rd party systems.
   */
  SHOW: 'materialShow',
  /**
   * Explain what the class is for.
   */
  HIDE: 'materialHidden'
};


/**
 * Example of a private function, note the underscore and 2 blank lines
 * between function defintion and previous lines of code.
 * @private
 */
MaterialComponentClassname.prototype.privateFunction_ = function() {
  'use strict';
  // Your code here...
  console.log(this.Constant_.SPECIAL_WORD + ' is cool!');
};


// Other private functions could be defined here. 2 lines space between each.
// Public functions can also be defined here, simply without underscores at
// end of funciton name.
MaterialComponentClassname.prototype.init = function() {
  // In this example we will add an event listener to the element.
  if (this.element_) {
    this.element_.addEventListener('click', this.privateFunction_.bind(this));
  }
};


window.addEventListener('load', function() {
  // On document ready, the component registers itself. It can assume
  // componentHandler is available in the global scope.
  componentHandler.register({
    constructor: MaterialComponentClassname,
    classAsString: 'MaterialComponentClassname',
    cssClass: 'MaterialComponentClassname'
  });
});
Clone this wiki locally