-
Notifications
You must be signed in to change notification settings - Fork 3k
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 ([email protected])
*/
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'
});
});