Source: delite/Stateful.js

/** @module delite/Stateful */
define(["dcl/dcl"], function (dcl) {

	// module:
	//		delite/Stateful

	var apn = {};

	function propNames(name) {
		// summary:
		//		Helper function to map "foo" --> "_setFooAttr" with caching to avoid recomputing strings.
		//		Note: in dojo/Stateful they are _fooGetter and _fooSetter.

		if (apn[name]) {
			return apn[name];
		}
		var uc = name.replace(/^[a-z]|-[a-zA-Z]/g, function (c) {
			return c.charAt(c.length - 1).toUpperCase();
		});
		var ret = apn[name] = {
			p: "_" + name + "Attr",		// shadow property, since real property hidden by setter/getter
			s: "_set" + uc + "Attr",	// converts dashes to camel case, ex: accept-charset --> _setAcceptCharsetAttr
			g: "_get" + uc + "Attr"
		};
		return ret;
	}

	/**
	 * @summary 
	 * Base class for objects that provide named properties with optional getter/setter
	 * control and the ability to watch for property changes.
	 * @description
	 * The class also provides the functionality to auto-magically manage getters
	 * and setters for class attributes/properties.  Note though that expando properties
	 * (i.e. properties added to an instance but not in the prototype) are not supported.
	 * <p>
	 * Getters and Setters should follow the format of _setXxxAttr or _getXxxAttr where
	 * the xxx is a name of the attribute to handle.  So an attribute of "foo"
	 * would have a custom getter of _getFooAttr and a custom setter of _setFooAttr.
	 * Setters must save and announce the new property value by calling this._set("foo", val),
	 * and getters should access the property value as this._fooAttr.
	 * </p>
	 * @example <caption>Example 1</caption>
	 * var MyClass = dcl(Stateful, { foo: "initial" });
	 * var obj = new MyClass();
	 * obj.watch("foo", function(){
	 * 	console.log("foo changed to " + this.foo);
	 * });
	 * obj.foo = bar;
	 * // Stateful by default interprets the first parameter passed to
	 * // the constructor as a set of properties to set on the widget 
	 * // immediately after it is created.
	 * 
	 * @example <caption>Example 2</caption>
	 * var MyClass = dcl(Stateful, { foo: "initial" });
	 * var obj = new MyClass({ foo: "special"});
	 * 
	 * @class module:delite/Stateful
	 */
	var Stateful = dcl(null, /** @lends module:delite/Stateful# */{
		// summary:
		//		Base class for objects that provide named properties with optional getter/setter
		//		control and the ability to watch for property changes.
		//
		//		The class also provides the functionality to auto-magically manage getters
		//		and setters for class attributes/properties.  Note though that expando properties
		//		(i.e. properties added to an instance but not in the prototype) are not supported.
		//
		//		Getters and Setters should follow the format of _setXxxAttr or _getXxxAttr where
		//		the xxx is a name of the attribute to handle.  So an attribute of "foo"
		//		would have a custom getter of _getFooAttr and a custom setter of _setFooAttr.
		//		Setters must save and announce the new property value by calling this._set("foo", val),
		//		and getters should access the property value as this._fooAttr.
		//
		// example:
		//	|	var MyClass = dcl(Stateful, { foo: "initial" });
		//	|	var obj = new MyClass();
		//	|	obj.watch("foo", function(){
		//	|		console.log("foo changed to " + this.foo);
		//	|	});
		//	|	obj.foo = bar;
		//
		//		Stateful by default interprets the first parameter passed to the constructor as
		//		a set of properties to set on the widget immediately after it is created.
		//
		// example:
		//	|	var MyClass = dcl(Stateful, { foo: "initial" });
		//	|	var obj = new MyClass({ foo: "special"});

		_getProps: function () {
			// summary:
			//		Return the list of properties that should be watchable

			var list = [];
			for (var prop in this) {
				if (typeof this[prop] !== "function" && !/^_/.test(prop)) {
					list.push(prop);
				}
			}
			return list;
		},

		_introspect: function (/*String[]*/ props) {
			// summary:
			//		Sets up ES5 getters/setters for each class property.
			//		Inside _introspect(), "this" is a reference to the prototype rather than any individual instance.

			props.forEach(function (prop) {
				var names = propNames(prop),
					shadowProp = names.p,
					getter = names.g,
					setter = names.s;

				// Setup ES5 getter and setter for this property, if not already setup.
				// For a property named foo, saves raw value in _fooAttr.
				// ES5 setter intentionally does late checking for this[names.s] in case a subclass sets up a
				// _setFooAttr method.
				if (!(shadowProp in this)) {
					this[shadowProp] = this[prop];
					delete this[prop]; // make sure custom setters fire
					Object.defineProperty(this, prop, {
						enumerable: true,
						set: function (x) {
							setter in this ? this[setter](x) : this._set(prop, x);
						},
						get: function () {
							return getter in this ? this[getter]() : this[shadowProp];
						}
					});
				}
			}, this);
		},

		constructor: dcl.advise({
			before: function () {
				// First time this class is instantiated, introspect it.
				// Use _introspected flag on constructor, rather than prototype, to avoid hits when superclass
				// was already inspected but this class wasn't.
				var ctor = this.constructor;
				if (!ctor._introspected) {
					// note: inside _introspect() this refs prototype
					ctor.prototype._introspect(ctor.prototype._getProps());
					ctor._introspected = true;
				}
			},

			after: function (args) {
				// Automatic setting of params during construction.
				// In after() advice so that it runs after all the subclass constructor methods.
				this.processConstructorParameters(args);
			}
		}),

		/**
		 * Called after Object is created to process parameters passed to constructor.
		 */
		processConstructorParameters: function (args) {
			// summary:
			//		Called after Object is created to process parameters passed to constructor
			if (args.length) {
				this.mix(args[0]);
			}
		},

		/**
		 * Set a hash of properties on a Stateful instance
		 * @example
		 * myObj.mix({
		 * 	foo: "Howdy",
		 *	bar: 3
		 * })
		 */
		mix: function (/*Object*/ hash) {
			// summary:
			//		Set a hash of properties on a Stateful instance
			//	|	myObj.mix({
			//	|		foo: "Howdy",
			//	|		bar: 3
			//	|	})

			for (var x in hash) {
				if (hash.hasOwnProperty(x) && x !== "_watchCallbacks") {
					this[x] = hash[x];
				}
			}
		},

		_set: function (name, value) {		// note: called _changeAttrValue() in dojo/Stateful
			// summary:
			//		Internal helper for directly changing an attribute value.
			// name: String
			//		The property to set.
			// value: Mixed
			//		The value to set in the property.
			// description:
			//		Directly change the value of an attribute on an object, bypassing any
			//		accessor setter.  Also notifies callbacks registered via watch().
			//		It is designed to be used by descendant class when there are two values
			//		of attributes that are linked, but calling .set() is not appropriate.

			var shadowPropName = propNames(name).p;
			var oldValue = this[shadowPropName];
			this[shadowPropName] = value;
			if (this._watchCallbacks) {
				this._watchCallbacks(name, oldValue, value);
			}
		},

		_get: function (name) {
			// summary:
			//		Internal helper for directly accessing an attribute value.
			// description:
			//		Directly get the value of an attribute on an object, bypassing any accessor getter.
			//		It is designed to be used by descendant class if they want
			//		to access the value in their custom getter before returning it.
			// name: String
			//		The property to get.

			return this[propNames(name).p];
		},

		/**
		 * Watches a property for changes.
		 */
		watch: function (/*String?*/ name, /*Function*/ callback) {
			// summary:
			//		Watches a property for changes
			// name:
			//		Indicates the property to watch. This is optional (the callback may be the
			//		only parameter), and if omitted, all the properties will be watched
			// returns:
			//		An object handle for the watch. The unwatch method of this object
			//		can be used to discontinue watching this property:
			//		|	var watchHandle = obj.watch("foo", callback);
			//		|	watchHandle.unwatch(); // callback won't be called now
			// callback:
			//		The function to execute when the property changes. This will be called after
			//		the property has been changed. The callback will be called with the |this|
			//		set to the instance, the first argument as the name of the property, the
			//		second argument as the old value and the third argument as the new value.

			var callbacks = this._watchCallbacks;
			if (!callbacks) {
				var self = this;
				callbacks = this._watchCallbacks = function (name, oldValue, value, ignoreCatchall) {
					var notify = function (propertyCallbacks) {
						if (propertyCallbacks) {
							propertyCallbacks = propertyCallbacks.slice();
							for (var i = 0, l = propertyCallbacks.length; i < l; i++) {
								propertyCallbacks[i].call(self, name, oldValue, value);
							}
						}
					};
					notify(callbacks["_" + name]);
					if (!ignoreCatchall) {
						notify(callbacks["*"]); // the catch-all
					}
				}; // we use a function instead of an object so it will be ignored by JSON conversion
			}
			if (!callback && typeof name === "function") {
				callback = name;
				name = "*";
			} else {
				// prepend with dash to prevent name conflicts with function (like "name" property)
				name = "_" + name;
			}
			var propertyCallbacks = callbacks[name];
			if (typeof propertyCallbacks !== "object") {
				propertyCallbacks = callbacks[name] = [];
			}
			propertyCallbacks.push(callback);

			return {
				remove: function () {
					var index = propertyCallbacks.indexOf(callback);
					if (index > -1) {
						propertyCallbacks.splice(index, 1);
					}
				}
			}; //Object
		}
	});

	dcl.chainAfter(Stateful, "_introspect");

	return Stateful;
});