Skip to content

Commit

Permalink
fixes problem with nested block statements
Browse files Browse the repository at this point in the history
  • Loading branch information
justinbmeyer committed Jun 1, 2012
1 parent 1acf841 commit 4d4d31f
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 36 deletions.
2 changes: 1 addition & 1 deletion util/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ can.dispatch = function(event){
}

var eventName = event.type.split(".")[0],
handlers = this.__bindEvents[eventName] || [],
handlers = (this.__bindEvents[eventName] || []).slice(0),
self= this,
args = [event].concat(event.data || []);

Expand Down
45 changes: 40 additions & 5 deletions view/ejs/ejs.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,48 @@

</head>
<body>
<div id="dummy"></div>

<!-- PUT ANY TEMPLATES YOU NEED HERE -->
<script id="listEJS" type="text/ejs">
<form>
<h1>Done items</h1>
<input type="button" value="Add item" class="add-item" />
<% items.each(function(item){%>
<% if(item.attr('is_done') === false){ %>
<div <%= (el)-> el.data('item', item) %> class="item"><%= item.title %></div>
<% } %>
<%})%>
</form>
</script>
<script type='text/javascript' src='../../../steal/steal.js'></script>
<script type='text/javascript'>
steal('jquery/view/ejs/ejs2.js', function(){
$.get('test_template.ejs',{},function(text){
console.log(EJS(text,{}))
})
})
steal('can/view/ejs', 'can/control','can/model',function(){
Dummy = can.Control({
init: function(element, options) {
can.append(this.element, can.view('listEJS', {
items: this.options.list
}));
},
"div click" : function(el, ev){
el.data('item').attr('is_done', true);
},
".add-item click" : function(el, ev){
var item = new Item({title: "Title " + (this.options.list.length + 1), is_done: false})
this.options.list.push(item)
}
});

can.Model('Item', {}, {});
var list = new Item.List;
for(var i = 0; i < 2; i++){
var item = new Item({title: "Title " + i, is_done: (i % 2 === 0)})
list.push(item)
}


new Dummy('#dummy', {list: list});
})
</script>
</body>

Expand Down
108 changes: 82 additions & 26 deletions view/ejs/ejs.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,42 @@ steal('can/view', 'can/util/string').then(function( $ ) {
el.removeAttribute(attrName)
}
},
// a helper to get the parentNode for a given element el
// if el is in a documentFragment, it will return defaultParentNode
getParentNode = function(el, defaultParentNode){
return defaultParentNode && el.parentNode.nodeType === 11 ? defaultParentNode : el.parentNode;
},
// helper to know if property is not an expando on oldObserved's list of observes
// this should probably be removed and oldObserved should just have a
// property with observes
observeProp = function(name){
return name.indexOf("|") >= 0;
},
// This is used to setup live binding on a list of observe/attribute
// pairs for a given element.
// - observed - an array of observe/attribute
// - el - the parent element, if removed, unbinds all observes
// - cb - a callback function that gets called if any observe/attribute changes
// - oldObserve - a mapping of observe/attributes already bound

liveBind = function( observed, el, cb, oldObserved ) {
// record if this is the first liveBind call for this magic tag
var first = oldObserved.matched === undefined;

// If there is no element, teardown.
// This case happens when a parent block, like an `if(X){}`, replaces
// the content of children bindings like `<%= you.attr('name') %>` and
// the same property change that cause the parent block change changes
// the child bindings.
// If the parent block change did not change the child bindings, liveBind would
// not be called and the bindings would still be present until the
// parentElement `el` is removed from the page.
if(el == null){
oldObserved.teardown();
can.unbind.call(oldObserved.el,'destroyed', oldObserved.teardown)
return;

}
// toggle the 'matched' indicator
oldObserved.matched = !oldObserved.matched;

Expand All @@ -89,7 +116,7 @@ steal('can/view', 'can/util/string').then(function( $ ) {
// that are no longer being bound and unbind them
for ( var name in oldObserved ) {
var ob = oldObserved[name];
if(name !== "matched" && ob.matched !== oldObserved.matched){
if(observeProp(name) && ob.matched !== oldObserved.matched){
ob.obj.unbind(ob.attr);
delete oldObserved[name];
}
Expand All @@ -98,13 +125,15 @@ steal('can/view', 'can/util/string').then(function( $ ) {
// If this is the first time binding, listen
// for the element to be destroyed and unbind
// all event handlers for garbage collection.
can.bind.call(el,'destroyed', function(){
can.each(oldObserved, function(ob){
if(typeof ob !== 'boolean'){
oldObserved.el = el;
oldObserved.teardown = function(){
can.each(oldObserved, function(ob, name){
if(observeProp(name)){
ob.obj.unbind(ob.attr, cb)
}
})
})
});
};
can.bind.call(el,'destroyed', oldObserved.teardown)
}

},
Expand Down Expand Up @@ -153,9 +182,15 @@ steal('can/view', 'can/util/string').then(function( $ ) {
// a magic tag. For example, `<%= task.attr() %>` becomes
// `function(){ return task.attr() }`.
getValueAndObserved = function(func, self){
// Set a callback on can.Observe to know
// when an attr is read.

var oldReading;
if (can.Observe) {
// Set a callback on can.Observe to know
// when an attr is read.
// Keep a reference to the old reader
// if there is one. This is used
// for nested live binding.
oldReading = can.Observe.__reading;
can.Observe.__reading = function(obj, attr){
// Add the observe and attr that was read
// to `observed`
Expand All @@ -173,7 +208,7 @@ steal('can/view', 'can/util/string').then(function( $ ) {

// Set back so we are no longer reading.
if(can.Observe){
delete can.Observe.__reading;
can.Observe.__reading = oldReading;
}
return {
value : value,
Expand Down Expand Up @@ -249,6 +284,7 @@ steal('can/view', 'can/util/string').then(function( $ ) {
* @param {Object} func
*/
txt : function(escape, tagName, status, self, func){

// Get teh value returned by the wrapping function and any observe/attributes read.
var res = getValueAndObserved(func, self),
observed = res.observed,
Expand All @@ -258,8 +294,6 @@ steal('can/view', 'can/util/string').then(function( $ ) {
oldObserved = {},
// The tag type to create within the parent tagName
tag = (tagMap[tagName] || "span");



// If we had no observes just return the value returned by func.
if(!observed.length){
Expand All @@ -272,13 +306,13 @@ steal('can/view', 'can/util/string').then(function( $ ) {
escape ?
// If we are escaping, replace the parentNode with
// a text node who's value is `func`'s return value.
function(el){
var parent = el.parentNode,
function(el, parentNode){
var parent = getParentNode(el, parentNode),
node = document.createTextNode(value),
binder = function(){
var res = getValueAndObserved(func, self);
node.nodeValue = ""+res.value;
liveBind(res.observed, parent, binder,oldObserved);
liveBind(res.observed, node.parentNode, binder,oldObserved);
};

parent.insertBefore(node, el);
Expand All @@ -288,36 +322,51 @@ steal('can/view', 'can/util/string').then(function( $ ) {
:
// If we are not escaping, replace the parentNode with a
// documentFragment created as with `func`'s return value.
function(span){
function(span, parentNode){
parentNode = getParentNode(span, parentNode)
// A helper function to manage inserting the contents
// and removing the old contents
var makeAndPut = function(val, remove){

var frag = can.view.frag(val),
// create the fragment, but don't hook it up
// we need to insert it into the document first

var frag = can.view.frag(val, parentNode),
// keep a reference to each node
nodes = can.map(frag.childNodes,function(node){
return node;
}),
last = remove[remove.length - 1];

// Insert it in the `document`.
// Insert it in the `document` or `documentFragment`
if( last.nextSibling ){
last.parentNode.insertBefore(frag, last.nextSibling)
} else {
last.parentNode.appendChild(frag)
}

// Remove the old content.
can.remove( can.$(remove) );

return nodes;
},
// nodes are the nodes that any updates will replace
// at this point, these nodes could be part of a documentFragment
nodes = makeAndPut(value, [span]);

// Anytime a live-bound attribute changes this method gets called
var binder = function(){
var res = getValueAndObserved(func, self);
nodes = makeAndPut(res.value, nodes);
liveBind(res.observed, span.parentNode, binder ,oldObserved);

// is this still part of the DOM?
var attached = nodes[0].parentNode,
// get the new value
res = getValueAndObserved(func, self);
// update the nodes in the DOM with the new rendered value
if( attached ) {
nodes = makeAndPut(res.value, nodes);
}
// updating the bindings (some observes may have changed)
liveBind(res.observed, nodes[0].parentNode, binder ,oldObserved);
}
liveBind(observed, span.parentNode, binder ,oldObserved);
// setup initial live-binding
liveBind(observed, parentNode, binder ,oldObserved);
}) + "></" +tag+">";
// In a tag, but not in an attribute
} else if(status === 1){
Expand Down Expand Up @@ -530,7 +579,13 @@ steal('can/view', 'can/util/string').then(function( $ ) {
} else {
content += token;
}

// if it's a tag like <input/>
if(lastToken.substr(-1) == "/"){
// remove the current tag in the stack
tagNames.pop();
// set the current tag to the previous parent
tagName = tagNames[tagNames.length-1];
}
break;
case "'":
case '"':
Expand Down Expand Up @@ -582,7 +637,7 @@ steal('can/view', 'can/util/string').then(function( $ ) {

endStack.push({
before: "",
after: finishTxt+"}));"
after: finishTxt+"}));\n"
})
}
else {
Expand Down Expand Up @@ -657,6 +712,7 @@ steal('can/view', 'can/util/string').then(function( $ ) {
put(content)
}
buff.push(";")

var template = buff.join(''),
out = {
out: 'with(_VIEW) { with (_CONTEXT) {' + template + " "+finishTxt+"}}"
Expand Down
48 changes: 48 additions & 0 deletions view/ejs/ejs_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,55 @@ test("nested properties", function(){

equals(div.innerHTML, "Brian")

});

test("tags without chidren or ending with /> do not change the state", function(){
var ta = can.$('#qunit-test-area')[0]
ta.innerHTML = ""

var hookup = can.view.hookup;
can.view.hookup = function(frag){
// check that there are no spans in this frag
can.append( can.$('#qunit-test-area'), frag );
equal( ta.getElementsByTagName('span').length, 0, "there are no spans");
equal( ta.getElementsByTagName('td').length, 2, "there are 2 td");
}
var text = "<table><tr><td/><%== obs.attr('content') %></tr></div>"
var obs = new can.Observe({
content: "<td>Justin</td>"
})
var compiled = new can.EJS({text: text}).render({obs: obs});

var div = document.createElement('div');

can.view.frag(compiled);
can.view.hookup = hookup;
})



test("nested live bindings", function(){
var items = new can.Observe.List([
{title: 0, is_done: false, id: 0}
]);

var div = document.createElement('div');
div.appendChild(can.view("//can/view/ejs/nested_live_bindings.ejs",{items: items}))
items.push({title: 0, is_done: false, id: 1});
// this will throw an error unless EJS protects against
// nested objects
items[0].attr('is_done',true)
});

// Similar to the nested live bindings test, this makes sure templates with control blocks
// will eventually remove themselves if at least one change happens
// before things are removed.
// It is currently commented out because
//
/*test("memory safe without parentElement of blocks", function(){
})*/



})()
7 changes: 7 additions & 0 deletions view/ejs/nested_live_bindings.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<form>
<% items.each(function(item){%>
<% if(item.attr('is_done') === false){ %>
<div>item</div>
<% } %>
<%})%>
</form>
16 changes: 12 additions & 4 deletions view/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,21 @@ steal("can/util")
};

can.extend( $view, {
frag: function(result){
// creates a frag and hooks it up all at once
frag: function(result, parentNode ){
return $view.hookup( $view.fragment(result), parentNode );
},
// simply creates a frag
// this is used internally to create a frag
// insert it
// then hook it up
fragment: function(result){
var frag = can.buildFragment(result,document.body);
// If we have an empty frag...
if(!frag.childNodes.length) {
frag.appendChild(document.createTextNode(''))
}
return $view.hookup(frag);
return frag;
},
// Convert a path like string into something that's ok for an `element` ID.
toId : function( src ) {
Expand All @@ -43,7 +51,7 @@ steal("can/util")
}
}).join("_");
},
hookup: function(fragment){
hookup: function(fragment, parentNode ){
var hookupEls = [],
id,
func,
Expand All @@ -61,7 +69,7 @@ steal("can/util")
for (; el = hookupEls[i++]; ) {

if ( el.getAttribute && (id = el.getAttribute('data-view-id')) && (func = $view.hookups[id]) ) {
func(el, id);
func(el, parentNode, id);
delete $view.hookups[id];
el.removeAttribute('data-view-id');
}
Expand Down

0 comments on commit 4d4d31f

Please sign in to comment.