From 320fc842e8a1f8aa6b1fb4ad7ffec4f96ab7de6e Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Wed, 24 Jul 2013 23:58:19 -0700 Subject: [PATCH] port "if"; fix chaining; buf.write(func); more get() --- packages/deps/deps.js | 2 +- packages/ui/components.js | 4 +- packages/ui2/base.js | 14 ++++-- packages/ui2/components.js | 35 +++++++------- packages/ui2/fields.js | 35 ++++++++++++-- packages/ui2/render.js | 14 ++++-- packages/ui2/render_tests.js | 94 +++++++++++++++++++++++++++++++++++- 7 files changed, 165 insertions(+), 33 deletions(-) diff --git a/packages/deps/deps.js b/packages/deps/deps.js index 8b57311037..4387c80815 100644 --- a/packages/deps/deps.js +++ b/packages/deps/deps.js @@ -307,7 +307,7 @@ _.extend(Deps, { }, // two values are equal if `equals(x, y)`, which defaults to `===` - isolate: function (f, equals) { + isolateValue: function (f, equals) { if (! Deps.active) return f(); diff --git a/packages/ui/components.js b/packages/ui/components.js index 8bfaa68cf4..5981f4bf22 100644 --- a/packages/ui/components.js +++ b/packages/ui/components.js @@ -43,7 +43,7 @@ UI.If = Component.extend({ }, render: function (buf) { var self = this; - var condition = Deps.isolate(function () { + var condition = Deps.isolateValue(function () { return !! self.condition(); }); buf(condition ? self.content() : self.elseContent()); @@ -59,7 +59,7 @@ UI.Unless = Component.extend({ }, render: function (buf) { var self = this; - var condition = Deps.isolate(function () { + var condition = Deps.isolateValue(function () { return ! self.condition(); }); buf(condition ? self.content() : self.elseContent()); diff --git a/packages/ui2/base.js b/packages/ui2/base.js index ca3c209b7b..4516cc102f 100644 --- a/packages/ui2/base.js +++ b/packages/ui2/base.js @@ -133,12 +133,18 @@ _extend(UI.Component, { } }); -callChainedCallback = function (comp, propName) { +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); + callChainedCallback(comp._super, propName, orig || comp); if (comp.hasOwnProperty(propName)) - comp[propName].call(comp); + comp[propName].call(orig || comp); }; // Make `typeName` a non-empty string starting with an ASCII @@ -410,7 +416,7 @@ _extend(UI.Component, { // building on the client, and it can also be used on the // client or server to generate initial HTML. render: function (buf) { - buf.write(this.content.extend()); + buf.write(this.content); }, _populate: function (div) { diff --git a/packages/ui2/components.js b/packages/ui2/components.js index 75be5b3202..0f82d6819e 100644 --- a/packages/ui2/components.js +++ b/packages/ui2/components.js @@ -23,46 +23,45 @@ UI.HTML = Component.extend({ buf.write(this._stringify(data)); } }); -/* + UI.If = Component.extend({ typeName: 'If', init: function () { - // here we implement the idea that the one positional arg to - // a component becomes its data by default, but components - // like `#if` don't want it to be the data context - // seen by the content so they can change it. - // the implementation will change (but not the idea) - // if Geoff's proposal for extend and args is implemented. - // It's also possible the right thing to do is - // to have `arg` and `data` be separate. this.condition = this.data; - this.data = this.parent.data; + // content doesn't see the condition as `data` + this.data = null; + // 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 condition = Deps.isolate(function () { - return !! self.condition(); + // re-render if and only if condition changes + var condition = Deps.isolateValue(function () { + return !! self.get('condition'); }); - buf(condition ? self.content() : self.elseContent()); + buf.write(condition ? self.content : self.elseContent); } }); UI.Unless = Component.extend({ typeName: 'Unless', init: function () { - // see comment in `If` this.condition = this.data; - this.data = this.parent.data; + this.data = null; }, render: function (buf) { var self = this; - var condition = Deps.isolate(function () { - return ! self.condition(); + // re-render if and only if condition changes + var condition = Deps.isolateValue(function () { + return !! self.get('condition'); }); - buf(condition ? self.content() : self.elseContent()); + buf.write(condition ? self.elseContent : self.content); } }); + +/* UI.Counter = Component.extend({ typeName: "Counter", fields: { diff --git a/packages/ui2/fields.js b/packages/ui2/fields.js index 52811afe39..59e6bbfd1a 100644 --- a/packages/ui2/fields.js +++ b/packages/ui2/fields.js @@ -1,11 +1,38 @@ var UI = UI2; +var findComponentWithProp = function (id, comp) { + while (comp) { + if (id in comp) + return comp; + comp = comp.parent; + } + return null; +}; + +var getData = function (comp) { + comp = findComponentWithProp('data', comp); + return (comp ? + (typeof comp.data === 'function' ? + comp.data() : comp.data) : + null); +}; + _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); + if (! id) { + // `id` is `""` or absent/undefined + return getData(this); + } else { + var comp = findComponentWithProp(id, this); + if (comp) { + // found a method + return (typeof comp[id] === 'function' ? + comp[id]() : comp[id]); + } else { + var data = getData(this); + return data[id]; + } + } }, // convenient syntax withData: function (data) { diff --git a/packages/ui2/render.js b/packages/ui2/render.js index 72ba231750..8fa58f9118 100644 --- a/packages/ui2/render.js +++ b/packages/ui2/render.js @@ -114,7 +114,8 @@ makeRenderBuffer = function (component, options) { arg = arg.extend(); handleComponent(arg); - } else if (arg.child) { + } else if ((typeof arg === 'function') || arg.child) { + // `componentFunction`, or // `{child: componentOrFunction, props: object}` // In `{child: comp}` with no `props`, it's ok @@ -130,8 +131,15 @@ makeRenderBuffer = function (component, options) { // is uninited and we instantiate a copy, `curComp` // is not that copy, it's the original component used // as a prototype. - var curComp = arg.child; - var props = arg.props; + var curComp, props; + if (typeof arg === 'function') { + curComp = arg; + props = null; + } else { + curComp = arg.child; + props = arg.props; + } + if (typeof curComp === 'function') { var compFunc = curComp; // use `Deps.autorun`, not `component.autorun`, diff --git a/packages/ui2/render_tests.js b/packages/ui2/render_tests.js index 4b768089bd..4e2500f168 100644 --- a/packages/ui2/render_tests.js +++ b/packages/ui2/render_tests.js @@ -150,6 +150,7 @@ Tinytest.add("ui - render", function (test) { test.equal(which.numListeners(), 0); })(); + (function () { var R = ReactiveVar(1); var which = ReactiveVar("H"); @@ -170,7 +171,7 @@ Tinytest.add("ui - render", function (test) { var c = UI.Component.extend({ render: function (buf) { var self = this; - // also, choose which one to use reactively + // choose which one to use reactively buf.write({child: function () { return which.get() === "H" ? Hello : World; }, props: { @@ -203,4 +204,95 @@ Tinytest.add("ui - render", function (test) { test.equal(name.numListeners(), 0); })(); + (function () { + var which = ReactiveVar("H"); + + // two factory (uninited) Components + var Hello = UI.Text.withData("hello"); + var World = UI.Text.withData("world"); + + var c = UI.Component.extend({ + render: function (buf) { + var self = this; + // choose which one to use reactively + buf.write(function () { + return which.get() === "H" ? Hello : World; + }); + } + }); + + c.build(); + test.equal($(c._offscreen).html(), "hello"); + which.set("W"); + Deps.flush(); + test.equal($(c._offscreen).html(), "world"); + test.equal(_.keys(c.children).length, 1); + which.set("H"); + Deps.flush(); + test.equal($(c._offscreen).html(), "hello"); + test.equal(_.keys(c.children).length, 1); + c.destroy(); + test.equal(which.numListeners(), 0); + })(); + +}); + +Tinytest.add("ui - if/unless", function (test) { + var hello = UI.Text.withData("hello"); + var world = UI.Text.withData("world"); + var R = ReactiveVar('true'); + var renderedCounts = [0, 0, 0]; + var c = UI.Component.extend({ + rendered: function () { renderedCounts[0]++; }, + render: function (buf) { + buf.write( + UI.If.extend({ + data: function () { + return R.get().charAt(0) === 't'; + }, + content: hello, + elseContent: world, + rendered: function () { + renderedCounts[1]++; + } + }), + UI.Unless.extend({ + data: function () { + return R.get().charAt(0) === 't'; + }, + content: hello, + elseContent: world, + rendered: function () { + renderedCounts[2]++; + } + })); + } + }); + + c.build(); + test.equal($(c._offscreen).html(), "helloworld"); + test.equal(renderedCounts, [1,1,1]); + R.set('false'); + Deps.flush(); + test.equal($(c._offscreen).html(), "worldhello"); + test.equal(renderedCounts, [1,2,2]); + R.set('true'); + Deps.flush(); + test.equal($(c._offscreen).html(), "helloworld"); + test.equal(renderedCounts, [1,3,3]); + R.set('torrid'); + Deps.flush(); + test.equal($(c._offscreen).html(), "helloworld"); + test.equal(renderedCounts, [1,3,3]); + R.set('flagrant'); + Deps.flush(); + test.equal($(c._offscreen).html(), "worldhello"); + test.equal(renderedCounts, [1,4,4]); + R.set('fromage'); + Deps.flush(); + test.equal($(c._offscreen).html(), "worldhello"); + test.equal(renderedCounts, [1,4,4]); + + c.destroy(); + test.equal(R.numListeners(), 0); }); \ No newline at end of file