Skip to content

Commit

Permalink
Merge pull request #1709 from bitovi/undefined-parent-579
Browse files Browse the repository at this point in the history
Makes scope.computeData listen to every observable in scope when value is not defined.
  • Loading branch information
justinbmeyer committed May 23, 2015
2 parents 62d3ac4 + bf436ec commit 83617f2
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 151 deletions.
6 changes: 4 additions & 2 deletions compute/get_value_and_bind.js
Expand Up @@ -95,7 +95,7 @@ steal("can/util", function(){
can.__clearObserved = can.__clearReading = function () {
if (observedStack.length) {
var ret = observedStack[observedStack.length-1];
observedStack[observedStack.length-1] = {observed: {}};
observedStack[observedStack.length-1] = {names: "", observed: {}};
return ret;
}
};
Expand All @@ -108,7 +108,9 @@ steal("can/util", function(){

can.__addObserved = can.__addReading = function(o){
if (observedStack.length) {
can.simpleExtend(observedStack[observedStack.length-1], o);
var last = observedStack[observedStack.length-1];
can.simpleExtend(last.observed, o.observed);
last.names += o.names;
}
};

Expand Down
25 changes: 1 addition & 24 deletions test/pluginified/2.0.5.test.js
Expand Up @@ -13618,30 +13618,7 @@ var __m78 = (function () {
ok(fooInfo.parent === top, "we pick the current if we have no leads");
})*/
test('use highest default observe in stack unless you\'ve found your way in something that does exist', function () {
var bottom = new can.Map({
name: {
first: 'Justin'
}
});
var middle = new can.Map({
name: {
first: 'Brian'
}
});
var top = new can.Map({
title: 'top'
});
var cur = new can.view.Scope(bottom)
.add(middle)
.add(top);
var lastNameInfo = cur.read('name.last', {});
ok(lastNameInfo.rootObserve === middle, 'pick the default observe with the highest depth');
deepEqual(lastNameInfo.reads, [
'name',
'last'
], 'pick the default observe with the highest depth');
});

/* test("use observe like objects, e.g. can.route, within scope properly", function() {
var expected = "video"
var cur = new can.view.Scope({}).add(can.route);
Expand Down
4 changes: 2 additions & 2 deletions view/callbacks/callbacks.js
Expand Up @@ -73,9 +73,9 @@ steal("can/util", "can/view",function(can){
res;

if(tagCallback) {
var reads = can.__clearReading();
var reads = can.__clearObserved();
res = tagCallback(el, tagData);
can.__setReading(reads);
can.__setObserved(reads);
} else {
res = scope;
}
Expand Down
11 changes: 7 additions & 4 deletions view/scope/compute_data.js
Expand Up @@ -38,12 +38,14 @@ steal("can/util","can/compute","can/compute/get_value_and_bind.js",function(can,
};
var scopeReader = function(scope, key, options, computeData, newVal){
if (arguments.length > 4) {
if(computeData.root.isComputed) {
computeData.root(newVal);
var root = computeData.root || computeData.setRoot;

if(root.isComputed) {
root(newVal);
} else if(computeData.reads.length) {
var last = computeData.reads.length - 1;
var obj = computeData.reads.length ? can.compute.read(computeData.root, computeData.reads.slice(0, last)).value
: computeData.root;
var obj = computeData.reads.length ? can.compute.read(root, computeData.reads.slice(0, last)).value
: root;
can.compute.set(obj, computeData.reads[last], newVal, options);
}
// **Compute getter**
Expand All @@ -64,6 +66,7 @@ steal("can/util","can/compute","can/compute/get_value_and_bind.js",function(can,
computeData.initialValue = data.value;
computeData.reads = data.reads;
computeData.root = data.rootObserve;
computeData.setRoot = data.setRoot;
return data.value;
}
};
Expand Down
83 changes: 32 additions & 51 deletions view/scope/scope.js
Expand Up @@ -174,32 +174,19 @@ steal(
context,
// The current scope.
scope = this,
// While we are looking for a value, we track the most likely place this value will be found.
// This is so if there is no me.name.first, we setup a listener on me.name.
// The most likely candidate is the one with the most "read matches" "lowest" in the
// context chain.
// By "read matches", we mean the most number of values along the key.
// By "lowest" in the context chain, we mean the closest to the current context.
// We track the starting position of the likely place with `defaultObserve`.
defaultObserve,
// Tracks how to read from the defaultObserve.
defaultReads = [],
// Tracks the highest found number of "read matches".
defaultPropertyDepth = -1,
// `scope.read` is designed to be called within a compute, but
// for performance reasons only listens to observables within one context.
// This is to say, if you have me.name in the current context, but me.name.first and
// we are looking for me.name.first, we don't setup bindings on me.name and me.name.first.
// To make this happen, we clear readings if they do not find a value. But,
// if that path turns out to be the default read, we need to restore them. This
// variable remembers those reads so they can be restored.
defaultComputeReadings,
// Tracks the default's scope.
defaultScope,

// If no value can be found, this is a list of of every observed
// object and property name to observe.
undefinedObserves = [],
// Tracks the first found observe.
currentObserve,
// Tracks the reads to get the value for a scope.
currentReads;
currentReads,

// Tracks the most likely observable to use as a setter.
setObserveDepth = -1,
currentSetReads,
currentSetObserve;

// Goes through each scope context provided until it finds the key (attr). Once the key is found
// then it's value is returned along with an observe, the current scope and reads.
Expand All @@ -218,16 +205,11 @@ steal(
currentObserve = observe;
currentReads = names.slice(nameIndex);
},
// Called when we were unable to find a value.
earlyExit: function (parentValue, nameIndex) {
/* If this has more matching values */
if (nameIndex > defaultPropertyDepth) {
defaultObserve = currentObserve;
defaultReads = currentReads;
defaultPropertyDepth = nameIndex;
defaultScope = scope;
/* Clear and save readings so next attempt does not use these readings */
defaultComputeReadings = can.__clearReading();
if (nameIndex > setObserveDepth) {
currentSetObserve = currentObserve;
currentSetReads = currentReads;
setObserveDepth = nameIndex;
}
},
// Execute anonymous functions found along the way
Expand All @@ -241,10 +223,12 @@ steal(
value: data.value,
reads: currentReads
};
} else {
// save all old readings before we try the next scope
undefinedObserves.push( can.__clearObserved() );
}
}
// Prevent prior readings and then move up to the next scope.
can.__clearReading();

if(!stopLookup) {
// Move up to the next scope.
scope = scope._parent;
Expand All @@ -253,24 +237,21 @@ steal(
}
}

// **Key was not found**, return undefined for the value. Unless an observable was
// found in the process of searching for the key, then return the most likely observable along with it's
// scope and reads.

if (defaultObserve) {
can.__setReading(defaultComputeReadings);
return {
scope: defaultScope,
rootObserve: defaultObserve,
reads: defaultReads,
value: undefined
};
} else {
return {
names: names,
value: undefined
};
// **Key was not found**, return undefined for the value.
// Make sure we listen to everything we checked for when the value becomes defined.
// Once it becomes defined, we won't have to listen to so many things.
var len = undefinedObserves.length;
if (len) {
for(var i = 0; i < len; i++) {
can.__addObserved(undefinedObserves[i]);
}
}
return {
setRoot: currentSetObserve,
reads: currentSetReads,
value: undefined
};

}
});

Expand Down
100 changes: 34 additions & 66 deletions view/scope/scope_test.js
@@ -1,26 +1,21 @@
steal("can/view/scope", "can/route", "can/test", "steal-qunit", function () {
QUnit.module('can/view/scope');
/* test("basics",function(){
var items = { people: [{name: "Justin"},[{name: "Brian"}]], count: 1000 };
var itemsScope = new can.view.Scope(items),
arrayScope = new can.view.Scope(itemsScope.attr('people'), itemsScope),
firstItem = new can.view.Scope( arrayScope.attr('0'), arrayScope );
var nameInfo = firstItem.get('name');
equal(nameInfo.name, "name");
equal(nameInfo.scope, firstItem);
equal(nameInfo.value,"Justin");
equal(nameInfo.parent, items.people[0]);

test("basics",function(){

var countInfo = firstItem.get('count');
equal( countInfo.name, "count" );
equal( countInfo.scope, itemsScope );
equal(countInfo.value,1000);
equal(countInfo.parent, items);
var items = new can.Map({ people: [{name: "Justin"},[{name: "Brian"}]], count: 1000 });

var itemsScope = new can.view.Scope(items),
arrayScope = new can.view.Scope(itemsScope.attr('people'), itemsScope),
firstItem = new can.view.Scope( arrayScope.attr('0'), arrayScope );

var nameInfo = firstItem.read('name');
deepEqual(nameInfo.reads, ["name"]);
equal(nameInfo.scope, firstItem);
equal(nameInfo.value,"Justin");
equal(nameInfo.rootObserve, items.people[0]);

});*/
});
/*
* REMOVE
test("adding items",function(){
Expand Down Expand Up @@ -111,54 +106,7 @@ steal("can/view/scope", "can/route", "can/test", "steal-qunit", function () {
equal(cur.attr('..'), row, 'got row');
equal(cur.attr('../first'), 'Justin', 'got row');
});
/* test("use highest default observe in stack", function(){
var bottom = new can.Map({
name: "bottom"
});
var top = new can.Map({
name: "top"
});
var base = new can.view.Scope( bottom ),
cur = base.add(top);
var fooInfo = cur.get("foo");
ok(fooInfo.parent === top, "we pick the current if we have no leads");

})*/
test('use highest default observe in stack unless you\'ve found your way in something that does exist', function () {
var bottom = new can.Map({
name: {
first: 'Justin'
}
});
var middle = new can.Map({
name: {
first: 'Brian'
}
});
var top = new can.Map({
title: 'top'
});
var cur = new can.view.Scope(bottom)
.add(middle)
.add(top);
var lastNameInfo = cur.read('name.last', {});
ok(lastNameInfo.rootObserve === middle, 'pick the default observe with the highest depth');
deepEqual(lastNameInfo.reads, [
'name',
'last'
], 'pick the default observe with the highest depth');
});
/* test("use observe like objects, e.g. can.route, within scope properly", function() {
var expected = "video"
var cur = new can.view.Scope({}).add(can.route);
can.route.attr('type', expected);
var type = cur.get('type');
equal(type.value, expected);
equal(type.parent, can.route);
})*/
test('nested properties with compute', function () {
var me = new can.Map({
name: {
Expand Down Expand Up @@ -425,5 +373,25 @@ steal("can/view/scope", "can/route", "can/test", "steal-qunit", function () {
equal(compute(), 5, "updated compute value");
equal( computeData.compute(), 5, "the compute has the right value");
});

test("computesData can find update when initially undefined parent scope becomes defined (#579)", function(){
expect(2);

var map = new can.Map();
var scope = new can.view.Scope(map);
var top = scope.add(new can.Map());

var computeData = top.computeData("value",{});

equal( computeData.compute(), undefined, "initially undefined");

computeData.compute.bind("change", function(ev, newVal){
equal(newVal, "first");
});

map.attr("value","first");


});

});
4 changes: 2 additions & 2 deletions view/stache/mustache_core.js
Expand Up @@ -498,9 +498,9 @@ steal("can/util",
// we hide any observables read in the function by saving any observables that
// have been read and then setting them back which overwrites any `can.__observe` calls
// performed in value.
var old = can.__clearReading();
var old = can.__clearObserved();
value(this);
can.__setReading(old);
can.__setObserved(old);

}
// If the compute has observable dependencies, setup live binding.
Expand Down

0 comments on commit 83617f2

Please sign in to comment.