Skip to content
Open
33 changes: 33 additions & 0 deletions layout-test.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
{{> yield region="footer"}}
</template>

<template name="LayoutThatSetsData">
{{#with childData}}
{{> yield}}
{{/with}}
{{#with childData}}
{{> yield region="footer"}}
{{/with}}
</template>

<template name="ChildWithData">
child {{title}}
</template>
Expand Down Expand Up @@ -54,3 +63,27 @@
footer
{{/contentFor}}
</template>

<template name="RegionArgumentsTestStatic">
{{#Layout template='LayoutWithTwoYields' footer='One'}}
inside
{{/Layout}}
</template>

<template name="RegionArgumentsTestDynamic">
{{#Layout template='LayoutWithTwoYields' footer=footerHelper}}
inside
{{/Layout}}
</template>

<template name="TemplateWithCreatedCallback">
callback
</template>

<template name="DefaultDataForLayout">
{{#with title="ok"}}
{{#Layout template="LayoutWithOneYield"}}
inner{{title}}
{{/Layout}}
{{/with}}
</template>
69 changes: 69 additions & 0 deletions layout-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ Tinytest.add('layout - default main region using Layout template', function (tes
});
});

Tinytest.add('layout - default data context using Layout template', function (test) {
withRenderedComponent(Template.DefaultDataForLayout, function (cmp, screen) {
test.equal(screen.innerHTML.compact(), 'layoutinnerok', 'default data context should be outer data context');
});
});

Tinytest.add('layout - dynamic yield regions', function (test) {
withRenderedLayout({template: 'LayoutWithTwoYields'}, function (layout, screen) {
var renderedCount = 1;
Expand Down Expand Up @@ -222,3 +228,66 @@ Tinytest.add('layout - region templates not found in lookup', function (test) {
document.body.removeChild(div);
}
});


Tinytest.add('layout - set regions via arguments - static', function (test) {
withRenderedLayout({template: 'RegionArgumentsTestStatic'}, function (layout, screen) {
test.equal(screen.innerHTML.compact(), 'insideone', 'One template should render into footer region');
});
});

Tinytest.add('layout - set regions via arguments - dynamic', function (test) {
var footerTemplate = new ReactiveVar('One');
Template.RegionArgumentsTestDynamic.footerHelper = function() { return footerTemplate.get(); }

withRenderedLayout({template: 'RegionArgumentsTestDynamic'}, function (layout, screen) {
test.equal(screen.innerHTML.compact(), 'insideone', 'One template should render into footer region');

footerTemplate.set('Two');
Deps.flush()
test.equal(screen.innerHTML.compact(), 'insidetwo', 'Two template should render into footer region');
});
});

// SEE IR#276 for detailed discussion
Tinytest.add('layout - Templates render with correct data even if setData is called after setRegion', function (test) {
withRenderedLayout({template: 'LayoutWithOneYield'}, function (layout, screen) {
Template.TemplateWithHelper = function() {};
layout.setData(false);
layout.setRegion('One');
Deps.flush();
test.equal(screen.innerHTML.compact(), 'layoutone');

Template.TemplateWithCreatedCallback.created = function() {
test.equal(this.data, true);
}

layout.setRegion('TemplateWithCreatedCallback');
layout.setData(true);
Deps.flush();
test.equal(screen.innerHTML.compact(), 'layoutcallback');
});
});

// XXX: This test doesn't work.
// To be totally honest, I'm not sure how it *should* work -- should
// the yield be getting the data context of the with block? Maybe..
// perhaps yield.data() should look at parent's data (modolo __isTemplateWith)
// just like layout does.
//
// Tinytest.add('layout - set data via with', function (test) {
// withRenderedLayout({template: 'LayoutThatSetsData'}, function (layout, screen) {
// layout.setRegion('main', 'ChildWithData');
// layout.setRegion('footer', 'FooterWithData');
//
// layout.setData({
// title: 'parentTitle',
// childData: {
// title: 'childTitle'
// }
// });
//
// Deps.flush();
// test.equal(screen.innerHTML.compact(), 'childchildTitlefooterchildTitle');
// });
// });
123 changes: 81 additions & 42 deletions layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,21 @@ Layout = UI.Component.extend({
var tmplDep = new Deps.Dependency;

// get the initial data value
var data = Deps.nonreactive(function () { return self.get(); });
var data, dataSet = false;
var dataDep = new Deps.Dependency;
var regions = this._regions = new ReactiveDict;
var content = this.__content;


// look first in regions that have been explicitly set, then data
var regionCaches = {};
var getRegion = function(region) {
regionCaches[region] = regionCaches[region] || Deps.cache(function () {
return self._regions.get(region) || self.get(region);
});

return regionCaches[region].get()
}

// a place to put content defined like this:
// {{#contentFor region="footer"}}content{{/contentFor}}
// this will be searched in the lookup chain.
Expand All @@ -134,13 +144,28 @@ Layout = UI.Component.extend({
};

var cachedData = Deps.cache(function () {
log('return data()');
dataDep.depend();
return data;
if (dataSet) {
return data;
} else {
// find the closest parent with a data context.
// If it's the direct parent, and it has `__isTemplateWith` set,
// then it's because we have `{{#Layout foo=bar}}` and we should ignore
var parent = self.parent;
if (parent) {
if (parent.__isTemplateWith)
parent = parent.parent;
return getComponentData(parent);
} else {
// the only time we don't have a parent is when we are in tests really
return null
}
}
});

this.setData = function (value) {
log('setData', value);
dataSet = true;
log('setData', EJSON.stringify(value, 2));
if (!EJSON.equals(value, data)) {
data = value;
dataDep.changed();
Expand All @@ -149,13 +174,10 @@ Layout = UI.Component.extend({

this.getData = function () {
var val = cachedData.get();
log('return data()', EJSON.stringify(val, 2));
return val;
};

this.data = function () {
return self.getData();
};

/**
* Set a region template.
*
Expand All @@ -164,6 +186,7 @@ Layout = UI.Component.extend({
*
*/
this.setRegion = function (key, value) {
log('setRegion', key, value);
if (arguments.length < 2) {
value = key;
key = 'main';
Expand Down Expand Up @@ -200,10 +223,17 @@ Layout = UI.Component.extend({
region = 'main';

self.region = region;
self.text = !! (data && data.text);

// reset the data function to use the layout's
// data
this.data = function () {
// XXX: should we be instead trying to sensibly get parent's
// data -- much like layout.getData() does?
// then we'd expect to inherit layout.getData() (via layout.render)
// unless we wrapped our {{> yield}} in a with.
// is this what users would expect?
// see 'layout - set data via with' test
return layout.getData();
};
},
Expand All @@ -217,9 +247,12 @@ Layout = UI.Component.extend({
// changes, this comp will be rerun and the new template
// will get put on the screen.
return function () {
var regions = layout._regions;
// create a reactive dep
var tmpl = regions.get(region);
var tmpl = getRegion(region);
log('rendering yield', region, tmpl)

if (self.text)
return tmpl;

if (tmpl)
return lookupTemplate.call(layout, tmpl);
Expand All @@ -231,6 +264,10 @@ Layout = UI.Component.extend({
};
}
});

this.hasYield = function(region) {
return !! getRegion(region);
};

// render content into a yield region using markup. when you call setRegion
// manually, you specify a string, not a content block. And the
Expand Down Expand Up @@ -289,38 +326,40 @@ Layout = UI.Component.extend({

render: function () {
var self = this;
// return a function to create a reactive
// computation. so if the template changes
// the layout is re-endered.
return function () {
// reactive
var tmplName = self.template();

//XXX hack to make work with null/false values.
//see this.template = in ctor function.
if (tmplName === '_defaultLayout')
return self._defaultLayout;
else if (tmplName) {
var tmpl = lookupTemplate.call(self, tmplName);
// it's a component
if (typeof tmpl.instantiate === 'function')
// See how __pasthrough is used in overrides.js
// findComponentWithHelper. If __passthrough is true
// then we'll continue past this component in looking
// up a helper method. This allows this use case:
// <template name="SomeParent">
// {{#Layout template="SomeLayout"}}
// I want a helper method on SomeParent
// called {{someHelperMethod}}
// {{/Layout}}
// </template>
tmpl.__passthrough = true;
return tmpl;
}
else {
return self['yield'];
return UI.With(_.bind(self.getData, self), UI.block(function () {
// return a function to create a reactive
// computation. so if the template changes
// the layout is re-endered.
return function() {
// reactive
var tmplName = self.template();

//XXX hack to make work with null/false values.
//see this.template = in ctor function.
if (tmplName === '_defaultLayout')
return self._defaultLayout;
else if (tmplName) {
var tmpl = lookupTemplate.call(self, tmplName);
// it's a component
if (typeof tmpl.instantiate === 'function')
// See how __pasthrough is used in overrides.js
// findComponentWithHelper. If __passthrough is true
// then we'll continue past this component in looking
// up a helper method. This allows this use case:
// <template name="SomeParent">
// {{#Layout template="SomeLayout"}}
// I want a helper method on SomeParent
// called {{someHelperMethod}}
// {{/Layout}}
// </template>
tmpl.__passthrough = true;
return tmpl;
}
else {
return self['yield'];
}
}
};
}));
}
});

Expand Down
2 changes: 1 addition & 1 deletion overrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ UI.Component.lookup = function (id, opts) {

if (id === 'yield') {
throw new Error("Sorry, would you mind using {{> yield}} instead of {{yield}}? It helps the Blaze engine.");
} else if (id === 'contentFor') {
} else if (id === 'contentFor' || id === 'hasYield') {
var layout = findComponentOfKind('Layout', this);
if (!layout)
throw new Error("Couldn't find a Layout component in the rendered component tree");
Expand Down