Source: delite/Invalidating.js

/** @module delite/Invalidating */
define(["dcl/dcl", "dojo/_base/lang", "./Stateful"], function (dcl, lang, Stateful) {

	/**
	 * Mixin for classes (usually widgets) that watch a set of invalidating properties.
	 * @class module:delite/Invalidating
	 * @augments {module:delite/Stateful}
	 * @mixin
	 */
	return dcl(Stateful, /** @lends module:delite/Invalidating# */{
		// summary:
		//		Mixin for classes (usually widgets) that watch a set of invalidating properties
		//		and delay to the next execution frame the refresh following the changes of
		//		the values of these properties. The receiving class must extend delite/Widget
		//		or dojo/Evented.
		// description:
		//		Once a set of properties have been declared subject to invalidation using the method
		//		addInvalidatingProperties(), changes of the values of these properties possibly
		//		end up calling refreshProperties() and in all cases refreshRendering(),
		//		thus allowing the receiving class to refresh itself based on the new values.

		_renderHandle: null,

		// _invalidatingProperties: [private] Object
		//		A hash of properties to watch in order to trigger the invalidation of these properties
		//		and/or the rendering invalidation.
		//		This list must be initialized by the time buildRendering() completes, usually in preCreate(),
		//		using addInvalidatingProperties(). Default value is null.
		_invalidatingProperties: null,
		
		// _invalidatedProperties: [private] Object
		//		A hash of invalidated properties either to refresh them or to refresh the rendering.
		_invalidatedProperties: null,
		
		// invalidProperties: Boolean
		//		Whether at least one property is invalid. This is readonly information, one must call
		//		invalidateProperties() to modify this flag.
		
		/**
		 * Whether at least one property is invalid. This is readonly information, one must call
		 * invalidateProperties() to modify this flag.
		 * @member {boolean}
		 * @default false
		 */
		invalidProperties: false,
		
		// invalidRenderering: Boolean
		//		Whether the rendering is invalid. This is readonly information, one must call
		//		invalidateRendering() to modify this flag.
		
		/**
		 * Whether the rendering is invalid. This is readonly information, one must call
		 * invalidateRendering() to modify this flag.
		 * @member {boolean}
		 * @default false
		 */
		invalidRendering: false,

		// if we are not a Widget, setup the listeners at construction time
		constructor: dcl.after(function () {
			this._initializeInvalidating();
		}),

		// if we are on a Widget, listen for any changes to properties after the widget has been rendered,
		// including when declarative properties (ex: iconClass=xyz) are applied.
		buildRendering: dcl.after(function () {
			// tags:
			//		protected
			this._initializeInvalidating();
		}),

		_initializeInvalidating: function () {
			if (this._invalidatingProperties) {
				var props = Object.keys(this._invalidatingProperties);
				for (var i = 0; i < props.length; i++) {
					this.watch(props[i], lang.hitch(this, this._invalidatingProperties[props[i]]));
				}
			}
			this._invalidatedProperties = {};
		},

		/**
		 * Adds the properties listed as arguments to the properties watched for triggering invalidation.
		 * This method must be called during the startup lifecycle before buildRendering() completes,
		 * usually in preCreate().
		 */
		addInvalidatingProperties: function () {
			// summary:
			//		Adds the properties listed as arguments to the properties watched for triggering invalidation.
			// 		This method must be called during the startup lifecycle before buildRendering() completes,
			//		usually in preCreate().
			// description:
			//		This can be used to trigger invalidation for rendering or for both property and rendering. When
			//		no invalidation mechanism is specified, only the rendering refresh will be triggered, that is only
			//		the refreshRendering() method will be called.
			//		This method can either be called with a list of properties to invalidate the rendering as follows:
			//			this.addInvalidatingProperties("foo", "bar", ...);
			//		or with an hash of keys/values, the keys being the properties to invalidate and the values
			//		being the invalidation method (either rendering or property and rendering):
			//			this.addInvalidatingProperties({
			//				"foo": "invalidateProperty",
			//				"bar": "invalidateRendering"
			//			});
			// tags:
			//		protected
			if (this._invalidatingProperties == null) {
				this._invalidatingProperties = {};
			}
			for (var i = 0; i < arguments.length; i++) {
				if (typeof arguments[i] === "string") {
					// we just want the rendering to be refreshed
					this._invalidatingProperties[arguments[i]] = "invalidateRendering";
				} else {
					// we just merge key/value objects into our list of invalidating properties
					var props = Object.keys(arguments[i]);
					for (var j = 0; j < props.length; j++) {
						this._invalidatingProperties[props[j]] = arguments[i][props[j]];
					}
				}
			}
		},
		
		/**
		 * Invalidates the property for the next execution frame.
		 */
		invalidateProperty: function (name) {
			// summary:
			//		Invalidates the property for the next execution frame.
			// name: String?
			//		The name of the property to invalidate. If absent, the revalidation
			//		is performed without a particular property being invalidated, that is
			//		the argument passed to refreshProperties() does not contain is called without any argument.
			// tags:
			//		protected
			if (name) {
				this._invalidatedProperties[name] = true;
			}
			if (!this.invalidProperties) {
				this.invalidProperties = true;
				// if we have a pending render, let's cancel it to execute it post properties refresh
				if (this._renderHandle) {
					// TODO: if we switch to defer() use remove() here
					clearTimeout(this._renderHandle);
					this.invalidRendering = false;
					this._renderHandle = null;
				}
				// TODO: should be defer but we might change this mechanism to a centralized one, so keep it like
				// that for now. If we don't come up with a centralized one, move defer to Destroyable and require
				// the Destroyable mixin.
				setTimeout(lang.hitch(this, "validateProperties"), 0);
			}
		},
		
		/**
		 * Invalidates the rendering for the next execution frame.
		 */
		invalidateRendering: function (name) {
			// summary:
			//		Invalidates the rendering for the next execution frame.
			// name: String?
			//		The name of the property to invalidate. If absent then the revalidation is asked without a
			//		particular property being invalidated, that is refreshRendering() is called without
			//		any argument.
			// tags:
			//		protected
			if (name) {
				this._invalidatedProperties[name] = true;
			}
			if (!this.invalidRendering) {
				this.invalidRendering = true;
				// TODO: should be defer but we might change this mechanism to a centralized one, so keep it like
				// that for now. If we don't come up with a centralized one, move defer to Destroyable and require
				// the Destroyable mixin.
				this._renderHandle = setTimeout(lang.hitch(this, "validateRendering"), 0);
			}
		},
		
		validateProperties: function () {
			// summary:
			//		Immediately validates the properties.
			// description:
			//		Does nothing if no invalidating property is invalid.
			//		You generally do not call that method yourself.
			// tags:
			//		protected
			if (this.invalidProperties) {
				var props = lang.clone(this._invalidatedProperties);
				this.invalidProperties = false;
				this.refreshProperties(this._invalidatedProperties);
				this.emit("refresh-properties-complete",
					{ invalidatedProperties: props, bubbles: true, cancelable: false });
				// if there are properties still marked invalid pursue further with rendering refresh
				this.invalidateRendering();
			}
		},
		
		/**
		 * Immediately validates the rendering.
		 */
		validateRendering: function () {
			// summary:
			//		Immediately validates the rendering.
			// description:
			//		Does nothing if the rendering is not invalid.
			//		You generally do not call that method yourself.
			// tags:
			//		protected
			if (this.invalidRendering) {
				var props = lang.clone(this._invalidatedProperties);
				this.invalidRendering = false;
				this.refreshRendering(this._invalidatedProperties);
				// do not fully delete invalidateProperties because someone might have set a property in
				// its refreshRendering method (not wise but who knows what people are doing) and a new cycle
				// should start with that properties listed as invalid instead of a blank set of properties
				for (var key in props) {
					delete this._invalidatedProperties[key];
				}
				this.emit("refresh-rendering-complete",
					{ invalidatedProperties: props, bubbles: true, cancelable: false });
			}
		},
		
		/**
		 * Immediately validates the properties and the rendering.
		 */
		validate: function () {
			// summary:
			//		Immediately validates the properties and the rendering.
			// description:
			//		The method calls validateProperties() then validateRendering().
			//		You generally do not call that method yourself.
			// tags:
			//		protected
			this.validateProperties();
			this.validateRendering();
		},
		
		/**
		 * Actually refreshes the properties. 
		 */
		refreshProperties: function (/*jshint unused: vars */props) {
			// summary:
			//		Actually refreshes the properties. 
			// description:
			//		The default implementation does nothing. A class using this mixin
			//		should implement this method if it needs to react to changes
			//		of the value of an invalidating property, except for modifying the
			//		DOM in which case refreshRendering() should be used instead.
			//		Typically, this method should be overriden for implementing
			//		the reconciliation of properties, for instance for adjusting 
			//		interdependent properties such as "min", "max", and "value". 
			//		The mixin calls this method before refreshRendering().
			// props: Object
			//		A hash of invalidated properties. This hash will then be passed further down to the
			//		refreshRendering() method. As such any modification to this hash will be 
			//		visible in refreshRendering().
			// tags:
			//		protected
		},
		
		/**
		 * Actually refreshes the rendering.
		 */
		refreshRendering: function (/*jshint unused: vars */props) {
			// summary:
			//		Actually refreshes the rendering.
			// description:
			//		The default implementation does nothing. A class using this mixin
			//		should implement this method if it needs to modify the DOM in reaction 
			//		to changes of the value of invalidating properties.
			//		The mixin calls this method after refreshProperties().
			// props: Object
			//		A hash of invalidated properties.
			// tags:
			//		protected
		}
	});
});