Commits

Tim Molendijk  committed 8bcf02c

Improvements in flirt. Progress on implementation and tests of dataview. Fixed fundamental bug in implementation of $.al.Field et al..

  • Participants
  • Parent commits 92d0b8c

Comments (0)

Files changed (6)

File src/jquery.al.core.js

 		return new $.al.MolendijkClass();
 	}
 };
+
+// Problem: because we are trying to emulate inheritance by merely *wrapping*
+// an instance of another class, there is some unexpected behavior. If an
+// instance of the extended class overrides a method, one would expect other
+// methods from the superclass that use that method to start using that new
+// method instead. That's how classical OOP works. But that's not the case
+// here. Also; if the instance of the superclass has methods that return
+// 'this', one would expect these to return these to be of the type of the
+// subclass. Yet, that's not the case.
 $.al.MolendijkClass.extend = function(wrap) {
 	var Class = this;
 	var Subclass = function() {
 			// TODO: don't copy properties that are only present
 			// on the parent's prototype (and not on its instance) -- one of
 			// them: constructor
-			for (p in parent) {
-				this[p] = parent[p];
-			}
+			var self = this;
+			$.each(parent, function(key, value) {
+				self[key] = value;
+				// if ($.isFunction(value)) {
+				// 	self[key] = function() {
+				// 		console.log(key + ': pass to parent');
+				// 		console.log(this);
+				// 		console.log(value);
+				// 		console.log('with arguments:');
+				// 		console.log(arguments);
+				// 		var t = value.apply(this, arguments);
+				// 		console.log('returned by parent (' + key + ')');
+				// 		console.log(t);
+				// 		return t;
+				// 	};
+				// } else {
+				// 	self[key] = value;
+				// }
+			});
+			// for (p in parent) {
+			// 	// this[p] = parent[p];
+			// 	if ($.isFunction(parent[p])) {
+			// 		this[p] = function() {
+			// 			console.log('pass to parent');
+			// 			console.log(this);
+			// 			console.log(parent[p]);
+			// 			var t = parent[p].apply(this, arguments);
+			// 			console.log(t);
+			// 			return t;
+			// 		};
+			// 	} else {
+			// 		this[p] = parent[p];
+			// 	}
+			// }
 			wrap.apply(this, args);
 		}
 	};
 
 // TODO: Support for supplying an object instance as value for methods.
 $.al.Meta = $.al.MolendijkClass.extend(function(type, instance, playback) {
-	var self = this;
-	
 	if (typeof instance !== 'string') {
 		instance = 'instance';
 	}
 		playback = 'playback';
 	}
 	
-	self[instance] = function() {
+	this[instance] = function() {
 		return type.apply(this, arguments);
 	};
 	
-	self[playback] = function(on) {
+	this[playback] = function(on) {
 		if (!(on instanceof type)) {
-			on = self[instance].apply(self, arguments);
+			on = this[instance].apply(this, arguments);
 		}
 		for (var i = 0, l = record.length; i < l; i++) {
-			on[record[i].method].apply(record[i].context, record[i].arguments);
+			on[record[i].method].apply(on, record[i].arguments);
 		}
 		return on;
 	};
 	
-	var methods = _.keys(self[instance]()),
+	var self = this,
+		methods = _.keys(this[instance]()),
 		record = [];
 	$.each(methods, function(i, method) {
 		self[method] = function() {
 			record.push({
 				method: method,
-				context: this,
+				// context: this,
 				arguments: arguments
 			});
-			return self;
+			return this;
 		};
 	});
 	
 });
 
 $.al.Field = $.al.MolendijkClass.extend(function(base, context) {
-	var self = this,
-		$registry = $('<div />');
+	var $registry = $('<div />');
 	
 	var lastSignal = {};
 	var signal = function(type) {
 		var change = {
-			from: type in lastSignal ? lastSignal[type] : self.base(),
-			to: self.val()
+			from: type in lastSignal ? lastSignal[type] : this.base(),
+			to: this.val()
 		};
 		if (change.from !== change.to) {
 			lastSignal[type] = change.to;
 		}
 	};
 	var notify = function() {
-		if (self.sleep()) {
+		if (this.sleep()) {
 			return;
 		}
-		signal('silent');
-		if (self.notifies()) {
-			signal('notify');
+		signal.call(this, 'silent');
+		if (this.notifies()) {
+			signal.call(this, 'notify');
 		}
 	};
 	
 	// value against which is decided if a value change has occurred or not.
 	// Setting the base value will never cause notifications.
 	// FIXME: Get rid of this concept.
-	self.base = function(i) {
+	this.base = function(i) {
 		if (arguments.length === 0) {
 			return base;
 		}
 		base = i;
-		return self;
+		return this;
 	};
 	
 	var val;
-	self.val = function(v) {
+	this.val = function(v) {
 		if (arguments.length === 0) {
-			return val === undefined ? self.base() : val;
+			return val === undefined ? this.base() : val;
 		}
 		val = v;
-		notify();
-		return self;
+		notify.call(this);
+		return this;
 	};
 	
-	self.context = function(c) {
+	this.context = function(c) {
 		if (arguments.length === 0) {
-			return context === undefined ? self : context;
+			return context === undefined ? this : context;
 		}
 		context = c;
-		return self;
+		return this;
 	};
 	
 	// TODO: We can move this one to prototype (but do we want to?)
-	self.bind = function() {
+	this.bind = function() {
 		var args = arguments,
 			binding;
 		
 		}
 		
 		if ($.isFunction(binding)) {
-			binding.call(self.context(), self.val);
+			binding.call(this.context(), $.proxy(this, 'val'));
 		}
 		
-		return self;
+		return this;
 	};
 	
-	self.observe = function(observer, silent) {
+	this.observe = function(observer, silent) {
+		var self = this;
 		$registry.bind('fieldchange' + (silent ? 'silent' : 'notify'), function(e, data) {
 			// TODO: Supply of data.from is not documented/tested.
 			if (observer.call(self.context(), data.to, data.from) === false) {
 				$registry.unbind(e);
 			}
 		});
-		return self;
+		return this;
 	};
 	
 	var triggersOn;
-	self.triggersOn = function(on) {
+	this.triggersOn = function(on) {
 		if (arguments.length === 0) {
-			return triggersOn === undefined ? self.context() : triggersOn;
+			return triggersOn === undefined ? this.context() : triggersOn;
 		}
 		triggersOn = on;
-		return self;
+		return this;
 	};
 	
 	var triggers = {};
 	var always = function() {
 		return true;
 	};
-	self.triggers = function(events) {
+	this.triggers = function(events) {
 		if (arguments.length === 0) {
 			return triggers;
 		}
 			triggers = {};
 		}
 		
-		return self;
+		return this;
 	};
 	
 	// We do not create a field instance before notifies is explicitly set
 	// because otherwise we would end up in a field instantiation loop.
 	var notifies;
-	self.notifies = function(condition) {
+	this.notifies = function(condition) {
 		if (arguments.length === 0) {
 			return notifies === undefined ? true : !!notifies.val();
 		}
 		// behavior by adding this feature to the general field
 		// implementation.
 		if ($.isFunction(condition)) {
-			condition = condition.call(self.context());
+			condition = condition.call(this.context());
 		}
 		// String values are interpreted as properties of our context.
 		else if (typeof condition === 'string') {
-			condition = self.context()[condition];
+			condition = this.context()[condition];
 		}
 		if (!(condition instanceof $.al.Field)) {
 			condition = $.al.Field(condition);
 		}
 		// notifies is now guaranteed to be a field.
+		var self = this;
 		notifies = condition.observe(function() {
 			// notify can be safely called at any time, regardless of the
 			// value of notifies and the value that was last notified, as it
 			// will check for itself if it needs to do actual signaling.
-			notify();
+			notify.call(self);
 		});
 		// Observer is not called for current value of notifies, so call
 		// notify right now as well.
-		notify();
-		return self;
+		notify.call(this);
+		return this;
 	};
 	// FIXME: temporary
-	self.notifiesField = function() {
+	this.notifiesField = function() {
 		return notifies instanceof $.al.Field ? notifies : undefined;
 	};
 	
 	var sleep = false;
-	self.sleep = function(s) {
+	this.sleep = function(s) {
 		if (arguments.length < 1) {
 			return sleep;
 		}
 		sleep = !!s;
-		notify();
-		return self;
+		notify.call(this);
+		return this;
 	};
 	
 	// Triggering events is always done along with notification, so we can
 	// define one in terms of the other.
-	self.observe(function(v) {
+	var self = this;
+	this.observe(function(v) {
 		var eventData = {
-			to: v
-		};
+				to: v
+			};
 		$.each(self.triggers(), function(type, condition) {
 			if (condition.call(self.context(), v) === true) {
 				var on = self.triggersOn();
 });
 
 $.al.ConjunctionField = $.al.Field.extend(function() {
-	var self = this,
-		operands = [];
+	var operands = [];
 	
-	delete self.bind;
+	delete this.bind;
 	
 	// TODO: use 'bind' instead of 'operand'.
-	self.operand = function() {
-		operands.push($.al.Field(false, self.context()).
+	this.operand = function() {
+		var self = this,
+			operand = $.al.Field(false, this.context());
+		operand.
 			observe(function(v) {
 				var conjunction = true;
 				for (var i = 0, l = operands.length; i < l; i++) {
 					conjunction = conjunction && !!operands[i].val();
 				}
+				// TODO: self is here *always* an instance of
+				// $.al.ConjunctionField, even if this constructor is being
+				// called as the result of the instantiation of a subclass.
+				// Is this safe??
 				self.val(conjunction);
 			}).
-			bind.apply(undefined, arguments));
-		return self;
+			bind.apply(operand, arguments);
+		operands.push(operand);
+		return this;
 	};
 	
 });
 
 $.al.List = $.al.Field.extend(function() {
-	var self = this;
-	
 	var fetcher;
-	self.fetcher = function(f) {
+	this.fetcher = function(f) {
 		if (arguments.length < 1) {
 			return fetcher;
 		}
 		fetcher = f;
-		return self;
+		return this;
 	};
 	
-	self.fetch = function() {
-		if ($.isFunction(self.fetcher())) {
-			self.fetcher().call(self.context(), self.val);
+	this.fetch = function() {
+		if ($.isFunction(this.fetcher())) {
+			this.fetcher().call(this.context(), $.proxy(this, 'val'));
 		}
-		return self;
+		return this;
 	};
 	
 });

File src/jquery.al.dataview.js

 		};
 	};
 
-$.fn.dataview = function(action, data, templateName) {
+var setCallback = function(data) {
+	var $nodes = this;
+	if (data instanceof $.al.Field) {
+		data.observe(function() {
+			// TODO: Simply call invalidate on $nodes (instead
+			// of $nodes.eq(0)) as soon as invalidate is smart
+			// enough to recognize that the corresponding
+			// template part only needs to be invalidated
+			// once.
+			$nodes.eq(0).dataview('invalidate');
+			return false;
+		});
+	}
+	$nodes.store('dataview', 'data', new Record(data));
+};
+
+$.fn.dataview = function(action) {
+	
+	switch (action) {
+		
+		case 'set':
+			var data = arguments[1],
+				templateName = arguments[2];
+			
+			return this.each(function() {
+				var $this = $(this);
+				
+				// Do not support set on a part of the view because that would
+				// lead to the data in the view running out of sync with the
+				// data in the list on the template node.
+				if ($this.flirt('closest').length > 0) {
+					return true;
+				}
+				
+				$this.flirt('templateNode', templateName).store('dataview', 'data', new Record(data));
+				
+				if (data instanceof $.al.List) {
+					data.observe(function() {
+						// TODO: Note that templateName can be undefined, in
+						// which case all views contained by $this will be
+						// invalidated if we implement the idea that is
+						// described in the first TODO under case
+						// 'invalidate'.
+						$this.dataview('invalidate', templateName);
+					});
+				}
+				
+				$this.flirt('set', data instanceof $.al.List ? data.val() : data, templateName, setCallback);
+			});
+		
+		case 'get':
+			var $this = this.eq(0),
+				$closest = $this.flirt('closest');
+			
+			if ($closest.length > 0) {
+				return $closest.fetch('dataview', 'data').gettt();
+			}
+			
+			var templateName = arguments[1],
+				data = $this.flirt('templateNode', templateName).fetch('dataview', 'data');
+			
+			return data ? data.gettt() : [];
+		
+		case 'invalidate':
+			var templateName = arguments[1];
+			
+			// TODO: Allow invalidate to be called on multiple rendered nodes
+			// from the same template part without that part being invalidated
+			// more than once.
+			return this.each(function() {
+				var $this = $(this),
+					$closest = $this.flirt('closest');
+				
+				if ($closest.length > 0) {
+					// Do not use dataview's set here because it does not
+					// support (re)setting part of the view.
+					$closest.flirt('set', $closest.dataview('get'), setCallback);
+					return true;
+				}
+				
+				// TODO: In case of no templateName provided, I think what we
+				// would want there to happen is that the views of all
+				// contained templates would invalidate. The problem is that
+				// in order to implement this we would need a means of getting
+				// all template nodes and looping them. This functionality is
+				// part of flirt and as of now not yet exposed via a public
+				// method. The current 'templateNode' method returns the one
+				// template node based on a supplied name, and the first in
+				// case of no name provided.
+				$this.dataview('set', $this.dataview('get', templateName), templateName);
+			});
+			
+	}
+	
+};
+
+$.fn.dataview_old = function(action, data, templateName) {
 	var self = this;
 	
 	switch (action) {

File src/jquery.al.flirt.js

 		var body = $template[0].data,
 			nameMatch = /^\S+\s/.exec(body);
 		if (nameMatch !== null) {
-			body = body.substr(nameMatch.length + 1);
+			body = body.substr(nameMatch[0].length);
 		}
 		$template.store('flirt', {
 			name: name,
 	
 	switch (action) {
 		
+		case 'templateNode':
+			var templateName = arguments[1];
+			return $(templateNode(this[0], templateName));
+		
 		case 'closest':
-			var $closest = this.eq(0);
+			var $this = this.eq(0),
+				$closest = $this;
 			
+			// First check for a renderer manually (without using the :tree
+			// selector) because we want to support text nodes as well, on
+			// which selectors do not work.
 			if ($closest.fetch('flirt', 'renderer') === undefined) {
 				$closest = $closest.closest(':data(flirt.renderer)');
 			}
+			
 			// We found a piece of the edge of a template part, now get the
 			// entire edge.
 			if ($closest.length > 0) {
 					return $(this).fetch('flirt', 'renderer') === identity;
 				});
 			}
+			
 			return $closest;
 		
+		// TODO: Not sure if this is necessary/desired
+		case 'get':
+			var $this = this.eq(0),
+				$closest = $this.flirt('closest');
+			
+			if ($closest.length > 0) {
+				return $closest;
+			} else {
+				var templateName = arguments[1],
+					templateNodes = $this.chain($findTemplates, function() {
+						var flirt = $(this).fetch('flirt');
+						return flirt && (templateName === undefined || flirt.name === templateName);
+					}).get(),
+					i = templateNodes.length,
+					nodes = [],
+					node;
+				while (i--) {
+					node = templateNodes[i].previousSibling;
+					while (node && $(node).fetch('flirt', 'renderer') !== undefined) {
+						nodes.unshift(node);
+						node = node.previousSibling;
+					}
+				}
+				return $(nodes);
+			}
+		
 		case 'add':
 			var data = arguments[1],
 				templateName = arguments[2],
 			});
 		
 	}
+	return this;
 	
 };
 

File test/dataview.js

 	
 	module('dataview');
 	
-	test("set", 3, function() {
+	test("$.fn.dataview: set", 3, function() {
 		var $dataview = $('#flirt');
 		
-		equals($dataview.dataview('set', 'some data'), $dataview, "Dataview returns the jQuery object upon set");
-		equals($dataview.find('li').eq(1).text(), "some data", "If no template name is supplied the first (breadth-first) template is used");
+		equals($dataview.dataview('set', 'some data'), $dataview, "Returns object on which set is initiated");
+		equals($dataview.find('li').eq(1).text(), "some data", "If no template name is supplied the (breadth-)first template is used");
 		
 		$dataview.dataview('set', ['more data', 'and even more']);
-		equals($dataview.find('li').length, 3, "Set overwrites existing views of the same template");
+		equals($dataview.find('li').length, 5, "Set overwrites existing views of the same template");
+		
+	});
+	
+	test("$.fn.dataview: get from containing element", 3, function() {
+		var $dataview = $('#flirt').dataview('set', data, 'complex');
+		
+		$dataview.dataview('set', ["tim", "art"], 'simple');
+		equals($dataview.dataview('get').length, 2, "Getting data without specifying template name returns data from (breadth-)first template");
+		equals($dataview.dataview('get', 'complex'), data, "Getting data from container returns the exact data object that was used to create the dataview using the same template name");
+		equals($('#main').dataview('get', 'complex'), data, "Data can be retrieved from a containing element at any level, as long as the template name will end up at the same template node");
+		
+	});
+	
+	test("$.fn.dataview: get from rendered element", 3, function() {
+		var $dataview = $('#flirt').dataview('set', data, 'complex');
+		
+		equals($dataview.find('li').eq(2).dataview('get'), group, "Getting data from a node that is part of a view returns the smallest (closest) piece of data that is responsible for the node");
+		equals($dataview.find('li').eq(2).contents().dataview('get'), group, "Getting from a child node that is not part of a smaller data piece returns the same data");
+		
+		equals($dataview.find('li').eq(2).find('a:first').dataview('get'), member, "Closest piece of data may well be a nested item");
+		
+	});
+	
+	test("$.fn.dataview: invalidate", 2, function() {
+		var $dataview = $('#flirt'),
+			d = $.merge([], data);
+		
+		$dataview.dataview('set', d, 'complex');
+		d[0].group = 'A"';
+		$dataview.dataview('invalidate', 'complex');
+		ok($dataview.text().indexOf('A"') !== -1, "Changed data item is reflected in the view");
+		
+		var list = ["tim", "art", "manja"];
+		$dataview.dataview('set', list, 'simple');
+		list.push("molendijk");
+		$dataview.dataview('invalidate', 'simple');
+		equals($dataview.find('li.simple').length, 4, "Changed list definition is reflected in the view");
+		
+		// TODO: Do we really want this behavior? See first TODO under case
+		// 'invalidate'.
+		// d.push(d[0]);
+		// list.pop(0);
+		// $dataview.dataview('invalidate');
+		// equals($dataview.find('li').length, 14, "Invalidate without template name invalidates all contained data");
+		
+	});
+	
+	test("$.fn.dataview: auto-invalidation for data items of type $.al.Field", 3, function() {
+		var $dataview = $('#flirt');
+		
+		var list = [$.al.Field().val("tim"), $.al.Field().val("art")];
+		$dataview.dataview('set', list, 'fields');
+		equals($dataview.find('li.field').length, 2, "View based on fields is rendered correctly");
+		
+		list[0].val("molendijk");
+		equals($dataview.find('li.field:first').text(), "molendijk", "View is updated automagically upon field change");
+		
+		list[0].val("wizard");
+		equals($dataview.find('li.field:first').text(), "wizard", "This capability is maintained after first change");
+		
+	});
+	
+	test("$.fn.dataview: auto-invalidate for data lists of type $.al.List", 3, function() {
+		var $dataview = $('#flirt');
+		
+		var list = $.al.List().val(["tim", "art"]);
+		
+		$dataview.dataview('set', list);
+		equals($dataview.find('li.simple').length, 2, "View based on list is rendered correctly");
+		
+		list.val(["tim", "art", "manja"]);
+		equals($dataview.find('li.simple').length, 3, "View is updated automagically upon list change");
+		
+		list.val(["tim"]);
+		equals($dataview.find('li.simple').length, 1, "This capability is maintained after first change");
 		
 	});
 	

File test/flirt.js

 	
 });
 
+test("$.fn.flirt: get from containing element", 3, function() {
+	var $flirt = $('#flirt');
+	
+	$flirt.
+		flirt('set', ["tim", "art", "manja"]).
+		flirt('set', data, 'complex');
+	
+	equals($flirt.flirt('get', 'complex').filter('li').length, 6, "Returns DOM nodes that were rendered from the specified template");
+	equals($flirt.flirt('get').filter('li').length, 9, "If no template is specified, return all rendered items from any contained template");
+	equals($flirt.flirt('get', 'doesnotexist').length, 0, "In case of no items rendered or specified template not found, and empty selection is returned");
+	
+});
+
+test("$.fn.flirt: get from rendered element", 1, function() {
+	var $flirt = $('#flirt').flirt('set', ["tim", "art"]);
+	
+	equals($flirt.find('li.simple:first').contents().flirt('get')[0], $flirt.find('li.simple:first')[0], "Returns closest (and smallest) rendered item that contains the (first) selected node");
+	
+});
+
 test("$.fn.flirt: clear from containing element", 2, function() {
 	var $flirt = $('#flirt').flirt('add', ["tim", "art", "manja"]);
 	
 	
 	$flirt.flirt('set', data);
 	equals($flirt.find('ul > li').length, 7, "If no template name is specified the breadth-first template (comment node) is used");
-	ok($flirt.text().indexOf('complex') === -1, "Template name is never part of the template, regardless of whether it was explicitly selected or not");
 	
 	$flirt.flirt('set', ["tim", "art", "manja"], 'simple');
 	equals($flirt.find('ul > li:first strong').length, 3, "If template name is specified the entire DOM tree is searched for the particular template");
 	
+	equals(flatten($('#flirt-name').flirt('set', ["tim", "art"]).text()), flatten("timart"), "Template name is never part of the template, regardless of whether it was explicitly selected or not");
+	
 });
 
 test("$.fn.flirt: working with multiple templates in the same container", 5, function() {

File test/index.html

 			</ul>
 			<ul id="flirt">
 				<li>Header</li>
-				<!--simple <li><%= data %></li> -->
+				<!--simple <li class="simple"><%= data %></li> -->
 				<li>In between</li>
 				<!--complex
 					<li>
 					->
 				-->
 				<li>Footer</li>
+				<!--fields
+					<li class="field"><%= val() %></li>
+				-->
 			</ul>
 			<ul id="flirt-deep">
 				<li>
 				</li>
 				<li>Another item</li>
 			</ul>
+			<div id="flirt-name">
+				<!--templatename <%= data %> -->
+			</div>
 			<ul id="dataview">
 				<!-- <li>whatever</li> -->
 			</ul>