diff --git a/packages/ui2/attrs.js b/packages/ui2/attrs.js index 9b1df25584..ac5d77889f 100644 --- a/packages/ui2/attrs.js +++ b/packages/ui2/attrs.js @@ -1,3 +1,4 @@ +var UI = UI2; var ATTRIBUTE_NAME_REGEX = /^[^\s"'>/=/]+$/; @@ -76,7 +77,8 @@ _extend(AttributeManager.prototype, { var handlers = self.handlers; component.autorun(function (c) { - if (component.stage !== Component.BUILT || + if ((! component.isBuilt) || + component.isDestroyed || ! component.containsElement(element)) { c.stop(); return; diff --git a/packages/ui2/base.js b/packages/ui2/base.js index a05717b31e..78759707fe 100644 --- a/packages/ui2/base.js +++ b/packages/ui2/base.js @@ -1,3 +1,13 @@ +// A very basic operation like Underscore's `_.extend` that +// copies `src`'s own, enumerable properties onto `tgt` and +// returns `tgt`. +_extend = function (tgt, src) { + for (var k in src) + if (src.hasOwnProperty(k)) + tgt[k] = src[k]; + return tgt; +}; + // @export UI2 var UI = UI2 = { nextGuid: 2, // Component is 1! @@ -14,78 +24,18 @@ var UI = UI2 = { // create instances (and the hope is we can gloss over the // difference in the docs). - Component: { - // If a Component has a `typeName` 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 - // most specific one in the case of an `extend` chain with - // `typeName` set at multiple points). - // - // To accomplish this, keeping performance in mind, - // any Component where `typeName` is explicitly set - // also has a function property `_constr` whose source-code - // name is `typeName`. `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() {}, + Component: (function (constr) { + // Make sure the "class name" that Chrome infers for + // UI.Component is "Component", and that + // `new UI.Component._constr` (which is what `extend` + // does) also produces objects whose inferred class + // name is "Component". Chrome's name inference rules + // 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}); + })(function Component() {}), - _super: null, - guid: 1, - - extend: function (props) { - // this function should never cause `props` to be - // mutated in case people want to reuse `props` objects - // in a mixin-like way. - - if (this.isInited) - // Disallow extending inited Components so that - // inited Components don't inherit instance-specific - // properties from other inited Components, just - // default values. - throw new Error("Can't extend an inited Component"); - - // Any Component with a typeName of "Foo" (say) is given - // a `._constr` of the form `function Foo() {}`. - if (props && props.typeName) - this._constr = - Function("return function " + - sanitizeTypeName(props.typeName) + - "() {};")(); - - // We don't know where we're getting `_constr` from -- - // it might be from some supertype -- just that it has - // the right function name. So set the `prototype` - // property each time we use it as a constructor. - this._constr.prototype = this; - - var c = new this._constr; - if (props) - _extend(c, props); - - // for efficient Component instantiations, we assign - // as few things as possible here. - c._super = this; - c.guid = 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; - } - }, isComponent: function (obj) { return obj && obj.isa === UI.Component.isa; }, @@ -102,15 +52,86 @@ var UI = UI2 = { } }; -// A very basic operation like Underscore's `_.extend` that -// copies `src`'s own, enumerable properties onto `tgt` and -// returns `tgt`. -_extend = function (tgt, src) { - for (var k in src) - if (src.hasOwnProperty(k)) - tgt[k] = src[k]; - return tgt; -}; +_extend(UI.Component, { + // If a Component has a `typeName` 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 + // most specific one in the case of an `extend` chain with + // `typeName` set at multiple points). + // + // To accomplish this, keeping performance in mind, + // any Component where `typeName` is explicitly set + // also has a function property `_constr` whose source-code + // name is `typeName`. `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, + guid: 1, + + extend: function (props) { + // this function should never cause `props` to be + // mutated in case people want to reuse `props` objects + // in a mixin-like way. + + if (this.isInited) + // Disallow extending inited Components so that + // inited Components don't inherit instance-specific + // properties from other inited Components, just + // default values. + throw new Error("Can't extend an inited Component"); + + var constr; + var constrMade = false; + // Any Component with a typeName of "Foo" (say) is given + // a `._constr` of the form `function Foo() {}`. + if (props && props.typeName) { + constr = Function("return function " + + sanitizeTypeName(props.typeName) + + "() {};")(); + constrMade = true; + } else { + constr = this._constr; + } + + // We don't know where we're getting `constr` from -- + // it might be from some supertype -- just that it has + // the right function name. So set the `prototype` + // property each time we use it as a constructor. + constr.prototype = this; + + var c = new constr; + if (constrMade) + c._constr = constr; + + if (props) + _extend(c, props); + + // for efficient Component instantiations, we assign + // as few things as possible here. + c._super = this; + c.guid = 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; + } +}); callChainedCallback = function (comp, propName) { if (comp._super) @@ -532,7 +553,6 @@ _extend(UI.Component, { _attach: function (parentNode, beforeNode) { var self = this; - this._requireBuilt(); this._requireNotDestroyed(); if (! self.isInited) diff --git a/packages/ui2/components.js b/packages/ui2/components.js index 8bfaa68cf4..75be5b3202 100644 --- a/packages/ui2/components.js +++ b/packages/ui2/components.js @@ -1,8 +1,5 @@ - -// All `` tags in HTML files are compiled to extend -// Body. If you put helpers and events on Body, they all -// inherit them. -UI.Body = Component.extend({isRoot: true}); +var UI = UI2; +var Component = UI.Component; UI.Text = Component.extend({ typeName: 'Text', @@ -11,8 +8,8 @@ UI.Text = Component.extend({ return String(x == null ? '' : x); }, render: function (buf) { - var data = this.data(); - buf(this._encodeEntities(this._stringify(data))); + var data = this.get(); + buf.write(this._encodeEntities(this._stringify(data))); } }); @@ -22,11 +19,11 @@ UI.HTML = Component.extend({ return String(x == null ? '' : x); }, render: function (buf) { - var data = this.data(); - buf(this._stringify(data)); + var data = this.get(); + buf.write(this._stringify(data)); } }); - +/* UI.If = Component.extend({ typeName: 'If', init: function () { @@ -89,4 +86,5 @@ UI.Counter = Component.extend({ self.increment(); }); } -}); \ No newline at end of file +}); + */ \ No newline at end of file diff --git a/packages/ui2/fields.js b/packages/ui2/fields.js new file mode 100644 index 0000000000..52811afe39 --- /dev/null +++ b/packages/ui2/fields.js @@ -0,0 +1,14 @@ +var UI = UI2; + +_extend(UI.Component, { + get: function (id) { + // this is the (! id) case where `id` is `""` or absent. + // actually it should probably search up the parent tree too. + return (typeof this.data === 'function' ? + this.data() : this.data); + }, + // convenient syntax + withData: function (data) { + return this.extend({data: data}); + } +}); diff --git a/packages/ui2/package.js b/packages/ui2/package.js index 0b1bde845e..57af12f042 100644 --- a/packages/ui2/package.js +++ b/packages/ui2/package.js @@ -8,7 +8,11 @@ Package.on_use(function (api) { api.use('ejson'); api.use('ordered-dict'); - api.add_files(['base.js']); + api.add_files(['base.js', + 'attrs.js', + 'render.js', + 'fields.js', + 'components.js']); }); Package.on_test(function (api) { @@ -17,6 +21,7 @@ Package.on_test(function (api) { api.use(['test-helpers', 'underscore'], 'client'); api.add_files([ - 'base_tests.js' + 'base_tests.js', + 'render_tests.js' ], 'client'); }); diff --git a/packages/ui2/render.js b/packages/ui2/render.js index 342e44f4d5..4a21bafa44 100644 --- a/packages/ui2/render.js +++ b/packages/ui2/render.js @@ -97,7 +97,7 @@ makeRenderBuffer = function (component, options) { componentsToAttach[commentString] = arg; } else if (arg.extend) { // `{extend: componentOrFunction, props: object}` - if (UI.isComponent(arg.extend)) { + /* if (UI.isComponent(arg.extend)) { // In `{extend: comp}` with no `props`, it's ok // for `comp` to be already inited. This lets // you write `{{> foo}}` to insert already-inited @@ -133,8 +133,8 @@ makeRenderBuffer = function (component, options) { var curChild = curType.create(arg.args); handle(curChild); } else { - throw new Error("Expected 'type' to be Component or function"); - } + throw new Error("Expected 'extend' to be Component or function"); + }*/ } else if (arg.attrs) { // `{attrs: functionOrDictionary }` // attrs object inserts zero or more `name="value"` items @@ -175,7 +175,8 @@ makeRenderBuffer = function (component, options) { } }; - var buf = function (/*args*/) { + var buf = {}; + buf.write = function (/*args*/) { for (var i = 0; i < arguments.length; i++) handle(arguments[i]); }; @@ -204,13 +205,13 @@ makeRenderBuffer = function (component, options) { if (n === root.lastChild) end = comp; } - if (comp.stage === Component.INITIAL) { + if (! comp.isInited) { component.add(comp); } else if (comp.parent !== component) { throw new Error("Component used in render must be a child " + "(or addable as one)"); } - comp.attach(parent, n); + comp._attach(parent, n); parent.removeChild(n); delete componentsToAttach[n.nodeValue]; } diff --git a/packages/ui2/render_tests.js b/packages/ui2/render_tests.js new file mode 100644 index 0000000000..d665baef83 --- /dev/null +++ b/packages/ui2/render_tests.js @@ -0,0 +1,66 @@ +var UI = UI2; + +Tinytest.add("ui - render", function (test) { + var c, R; + + c = UI.Component.extend({ + render: function (buf) { + buf.write("asdf"); + } + }); + + c.build(); + test.equal($(c._offscreen).html(), "asdf"); + c.destroy(); + + + + c = UI.Component.extend({ + render: function (buf) { + buf.write("
asdf
"); + } + }); + + c.build(); + test.equal($(c._offscreen).html(), "
asdf
"); + c.destroy(); + + + + R = ReactiveVar("blam"); + c = UI.Component.extend({ + render: function (buf) { + buf.write( + "foo", + UI.Text.withData(function () { return R.get(); }), + "bar"); + } + }); + + c.build(); + test.equal($(c._offscreen).html(), "fooblambar"); + R.set("ki"); + Deps.flush(); + test.equal($(c._offscreen).html(), "fookibar"); + c.destroy(); + + + + R = ReactiveVar("
"); + c = UI.Component.extend({ + render: function (buf) { + buf.write( + "foo", + UI.HTML.withData(function () { return R.get(); }), + "bar"); + } + }); + + c.build(); + test.equal($(c._offscreen).html(), "foo
bar"); + R.set("
hi
"); + Deps.flush(); + test.equal($(c._offscreen).html(), "foo
hi
bar"); + c.destroy(); + +}); \ No newline at end of file