From 02a067d5c2b96254beb99d47292bee0eb558f19c Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Fri, 23 Aug 2013 19:12:46 -0700 Subject: [PATCH] start to simplify Component (breaks) --- packages/ui/base.js | 219 ++++++++++++++++---------------------- packages/ui/components.js | 150 +------------------------- 2 files changed, 96 insertions(+), 273 deletions(-) diff --git a/packages/ui/base.js b/packages/ui/base.js index d5d9093fbd..4ff30f4329 100644 --- a/packages/ui/base.js +++ b/packages/ui/base.js @@ -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++) diff --git a/packages/ui/components.js b/packages/ui/components.js index 99f8eadf7e..6b72c2b704 100644 --- a/packages/ui/components.js +++ b/packages/ui/components.js @@ -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 = $('
'); - 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("
"); - self.curComp = buf.write( - curCondition ? self.content : self.elseContent); - buf.write("
"); - } -}); - -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(''); - } -}))({ - 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("
", - new UI.Text(function () { - return self.count(); - }), - "
"); - }, - built: function () { - var self = this; - self.$("div").on('click', function (evt) { - self.increment(); - }); - } -}); - */