start to simplify Component (breaks)

This commit is contained in:
David Greenspan
2013-08-23 19:12:46 -07:00
parent bfb2df6226
commit 02a067d5c2
2 changed files with 96 additions and 273 deletions

View File

@@ -8,10 +8,35 @@ _extend = function (tgt, src) {
return tgt;
};
// Defines a single non-enumerable, read-only property
// on `tgt`.
// It won't be non-enumerable in IE 8, so its
// non-enumerability can't be relied on for logic
// purposes, it just makes things prettier in
// the dev console.
var _defineNonEnum = function (tgt, name, value) {
try {
Object.defineProperty(tgt, name, {value: value});
} catch (e) {
// IE < 9
tgt[name] = value;
}
return tgt;
};
// Make `typeName` a non-empty string starting with an ASCII
// letter or underscore and containing only letters, underscores,
// and numbers. This makes it safe to insert into evaled JS
// code.
var sanitizeTypeName = function (typeName) {
return String(typeName).replace(/^[^a-zA-Z_]|[^a-zA-Z_0-9]+/g,
'') || 'Component';
};
UI = {
nextGuid: 2, // Component is 1!
// Components and Component "classes" are the same thing, just
// Components and Component kinds are the same thing, just
// objects; there are no constructor functions, no `new`,
// and no `instanceof`. A Component object is like a class,
// until it is inited, at which point it becomes more like
@@ -32,52 +57,74 @@ UI = {
// are a little mysterious, but a function name in
// the source code (as in `function Component() {}`)
// seems to be reliable and high precedence.
return _extend(new constr, {_constr: constr});
return _defineNonEnum(new constr, '_constr', constr);
})(function Component() {}),
isComponent: function (obj) {
return obj && obj.isa === UI.Component.isa;
return obj && UI.isKindOf(obj, UI.Component);
},
attachRoot: function (comp, parentNode, beforeNode) {
comp._requireNotDestroyed();
if (! comp.isInited)
comp.makeRoot();
if (comp.parent)
throw new Error("Component is inited with a parent (not a root)");
comp._attach(parentNode, beforeNode);
},
// global append to body; experimental
append: function (comp) {
UI.attachRoot(comp, document.body);
// `UI.isKindOf(a, b)` where `a` and `b` are Components
// (or kinds) asks if `a` is or descends from
// (transitively extends) `b`.
isKindOf: function (a, b) {
while (a) {
if (a === b)
return true;
a = a._super;
}
return false;
}
// use these to produce error messages for developers
// (though throwing a more specific error message is
// even better)
// _requireNotDestroyed: function (c) {
// if (c.isDestroyed)
// throw new Error("Component has been destroyed; can't perform this operation");
// },
// _requireInited: function (c) {
// if (! c.isInited)
// throw new Error("Component must be inited to perform this operation");
// },
// _requireDom: function (c) {
// if (! c.dom)
// throw new Error("Component must be built into DOM to perform this operation");
// }
};
Component = UI.Component;
_extend(UI.Component, {
// If a Component has a `typeName` property set via `extend`,
// If a Component has a `kind` property set via `extend`,
// we make it use that name when printed in Chrome Dev Tools.
// If you then extend this Component and don't supply any
// new typeName, it should use the same typeName (or the
// new `kind`, it should use the same value of kind (or the
// most specific one in the case of an `extend` chain with
// `typeName` set at multiple points).
// `kind` set at multiple points).
//
// To accomplish this, keeping performance in mind,
// any Component where `typeName` is explicitly set
// any Component where `kind` is explicitly set
// also has a function property `_constr` whose source-code
// name is `typeName`. `extend` creates this `_constr`
// name is `kind`. `extend` creates this `_constr`
// function, which can then be used internally as a
// constructor to quickly create new instances that
// pretty-print correctly.
typeName: "Component",
_constr: function Component() {},
_super: null,
kind: "Component",
guid: "1",
data: null,
dom: null,
// Has this Component ever been inited?
isInited: false,
// Has this Component been destroyed? Only inited Components
// can be destroyed.
isDestroyed: false,
// Component that created this component (typically also
// the DOM containment parent).
// No child pointers (except in `dom`).
parent: null,
// create a new subkind or instance whose proto pointer
// points to this, with additional props set.
extend: function (props) {
// this function should never cause `props` to be
// mutated in case people want to reuse `props` objects
@@ -92,11 +139,11 @@ _extend(UI.Component, {
var constr;
var constrMade = false;
// Any Component with a typeName of "Foo" (say) is given
// Any Component with a kind of "Foo" (say) is given
// a `._constr` of the form `function Foo() {}`.
if (props && props.typeName) {
if (props && props.kind) {
constr = Function("return function " +
sanitizeTypeName(props.typeName) +
sanitizeTypeName(props.kind) +
"() {};")();
constrMade = true;
} else {
@@ -118,116 +165,33 @@ _extend(UI.Component, {
// for efficient Component instantiations, we assign
// as few things as possible here.
c._super = this;
_defineNonEnum(c, '_super', this);
c.guid = String(UI.nextGuid++);
return c;
},
// `x.isa(Foo)` where `x` is a Component returns `true`
// if `x` is `Foo` or a Component that descends from
// (transitively extends) `Foo`.
isa: function (obj) {
var x = this;
while (x) {
if (x === obj)
return true;
x = x._super;
}
return false;
}
});
Empty = Component.extend({
render: function (buf) {}
});
_defineNonEnum(UI.Component, '_constr',
function Component() {});
_defineNonEnum(UI.Component, '_super', null);
callChainedCallback = function (comp, propName, orig) {
//callChainedCallback = function (comp, propName, orig) {
// Call `comp.foo`, `comp._super.foo`,
// `comp._super._super.foo`, and so on, but in reverse
// order, and only if `foo` is an "own property" in each
// case. Furthermore, the passed value of `this` should
// remain `comp` for all calls (which is achieved by
// filling in `orig` when recursing).
if (comp._super)
callChainedCallback(comp._super, propName, orig || comp);
// if (comp._super)
// callChainedCallback(comp._super, propName, orig || comp);
//
// if (comp.hasOwnProperty(propName))
// comp[propName].call(orig || comp);
//};
if (comp.hasOwnProperty(propName))
comp[propName].call(orig || comp);
};
// Make `typeName` a non-empty string starting with an ASCII
// letter or underscore and containing only letters, underscores,
// and numbers. This makes it safe to insert into evaled JS
// code.
var sanitizeTypeName = function (typeName) {
return String(typeName).replace(/^[^a-zA-Z_]|[^a-zA-Z_0-9]+/g,
'') || 'Component';
};
var SEALED_EMPTY_OBJECT = {};
if (Object.seal)
// IE 9+, FF, Chrome, Safari
Object.seal(SEALED_EMPTY_OBJECT);
_extend(UI.Component, {
// Has this Component ever been inited?
isInited: false,
// Has this Component ever been built into DOM nodes?
// Implies isInited.
isBuilt: false,
// Has this Component been destroyed? Only inited Components
// can be destroyed, but built and unbuilt Components
// can both be destroyed (and their value of isBuilt
// stays the same when they are).
isDestroyed: false,
destroy: function () {
if (! this.isInited)
throw new Error("Can't destroy an uninited Component");
if (this.isDestroyed)
return;
this.isDestroyed = true;
// recursively destroy children as well
for (var k in this.children)
this.children[k].destroy();
// clean up any data associated with offscreen nodes
if (this._offscreen)
$.cleanData(this._offscreen.childNodes);
// stop all computations (rebuilding and comp.autorun)
var comps = this._computations;
if (comps)
for (var i = 0; i < comps.length; i++)
comps[i].stop();
callChainedCallback(this, 'destroyed');
},
// use this to produce error messages for developers
// (though throwing a more specific error message is
// even better)
_requireNotDestroyed: function () {
if (this.isDestroyed)
throw new Error("Component has been destroyed; can't perform this operation");
},
_requireInited: function () {
if (! this.isInited)
throw new Error("Component must be inited to perform this operation");
},
_requireBuilt: function () {
if (! this.isBuilt)
throw new Error("Component must be built into DOM to perform this operation");
}
});
/*
_extend(UI.Component, {
// Parent Component in the composition hierarchy.
// An inited
@@ -1107,7 +1071,7 @@ var compareElementIndex = function (a, b) {
// Returns true if element a contains node b and is not node b.
var elementContains = function (a, b) {
if (a.nodeType !== 1) /* ELEMENT */
if (a.nodeType !== 1) // ELEMENT
return false;
if (a === b)
return false;
@@ -1120,7 +1084,7 @@ var elementContains = function (a, b) {
// IE can't handle b being a text node. We work around this
// by doing a direct parent test now.
b = b.parentNode;
if (! (b && b.nodeType === 1)) /* ELEMENT */
if (! (b && b.nodeType === 1)) // ELEMENT
return false;
if (a === b)
return true;
@@ -1145,9 +1109,10 @@ var makeSafeDiv = function () {
var frag = $.buildFragment([div], document);
return div;
};
*/
UI.body = UI.Component.extend({
typeName: 'body',
kind: 'body',
contentParts: [],
render: function (buf) {
for (var i = 0; i < this.contentParts.length; i++)

View File

@@ -1,8 +1,6 @@
UI.Empty = Empty;
UI.Text = Component.extend({
typeName: 'Text',
kind: 'Text',
_encodeEntities: UI.encodeSpecialEntities,
_stringify: function (x) {
return String(x == null ? '' : x);
@@ -14,7 +12,7 @@ UI.Text = Component.extend({
});
UI.HTML = Component.extend({
typeName: 'HTML',
kind: 'HTML',
_stringify: function (x) {
return String(x == null ? '' : x);
},
@@ -25,7 +23,7 @@ UI.HTML = Component.extend({
});
UI.If = Component.extend({
typeName: 'If',
kind: 'If',
init: function () {
// XXX this probably deserves a better explanation if this code is
// going to stay with us.
@@ -49,7 +47,7 @@ UI.If = Component.extend({
});
UI.Unless = Component.extend({
typeName: 'Unless',
kind: 'Unless',
init: function () {
this.condition = this.data;
delete this.data;
@@ -64,143 +62,3 @@ UI.Unless = Component.extend({
buf.write(condition ? self.elseContent : self.content);
}
});
// for the demo.....
FadeyIf = Component.extend({
typeName: 'FadeyIf',
animationDuration: 1000,
init: function () {
this.condition = this.data;
// content doesn't see the condition as `data`
delete this.data;
// XXX I guess this means it's kosher to mutate properties
// of a Component during init (but presumably not before
// or after)?
},
render: function (buf) {
var self = this;
var curCondition;
// XXXX this exact pattern appears a couple times (the whole
// autorun). Should probably be abstracted / is a sign we
// don't quite have the right abstraction for reactively
// rearranging components.
Deps.autorun(function (c) {
// capture dependencies of this line:
var condition = Deps.isolateValue(function () {
return !! self.get('condition');
});
if (c.firstRun) {
// right away
curCondition = condition;
} else {
// later (on subsequent runs)...
if (! self.isBuilt ||
self.isDestroyed) {
c.stop();
} else {
curCondition = condition;
var oldChild = self.curComp;
var newChild = self.curComp = constructify(
curCondition ? self.content : self.elseContent);
var newDiv = $('<div style="display:none"></div>');
self.append(newDiv);
self.insertBefore(newChild, null, newDiv.get(0));
newDiv.animate({height: 'show',
width: 'show',
opacity: 1},
{queue: false,
duration: self.animationDuration});
if (self.hasChild(oldChild)) {
$(oldChild.parentNode()).animate(
{height: 0, width: 0, opacity: 0},
{queue: false, duration: self.animationDuration,
complete:
(function (oldChild) {
return function () {
if (self.hasChild(oldChild)) {
var div = oldChild.parentNode();
oldChild.remove();
// XXX need a good way to remove DOM nodes from a
// Component.
// Assume here there is more than one div because
// we just added one.
if (div === self.start)
self.start = div.nextSibling;
else if (div === self.end)
self.end = div.previousSibling;
$(div).remove();
}
};
})(oldChild)});
}
}
}
});
buf.write("<div>");
self.curComp = buf.write(
curCondition ? self.content : self.elseContent);
buf.write("</div>");
}
});
Checkbox = UI.makeTemplate(Component.extend({
typeName: 'Checkbox',
init: function () {
var self = this;
if (typeof self.data === 'string') {
var field = self.data;
self.set('checked', self.get(field));
self.autorun(function (c) {
var checked = self.get('checked');
if (! c.firstRun)
self.set(field, checked);
});
}
},
render: function (buf) {
var self = this;
buf.write('<input type="checkbox"',
{attrs: function () {
return self.get('checked') ?
{'checked':''} : {};
}},'>');
}
}))({
fields: {checked: false},
'change input': function (evt) {
var comp = UI.Component.current;
var newChecked = !! evt.target.checked;
if (newChecked !== comp.get('checked'))
comp.set('checked', newChecked);
}
});
/*
UI.Counter = Component.extend({
typeName: "Counter",
fields: {
count: 0
},
increment: function () {
this.set('count', this.count() + 1);
},
render: function (buf) {
var self = this;
buf("<div style='background:yellow'>",
new UI.Text(function () {
return self.count();
}),
"</div>");
},
built: function () {
var self = this;
self.$("div").on('click', function (evt) {
self.increment();
});
}
});
*/