Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

feat($compile): add partialDigest property in directive definition #8055

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,13 @@
* parameter of directive controllers.
* `function([scope], cloneLinkingFn)`.
*
* #### `partialDigest`
* This property is used only if `scope` is an object, implying the directive will have an isolated scope.
*
* You can specify `partialDigest` as a boolean or as a function which takes two
* arguments `tElement` and `tAttrs` (described in the `compile` function api above) and returns
* a boolean. See {@link api/ng.$rootScope.Scope#$new $new()} for more information about partialDigest.
*
*
* #### Pre-linking function
*
Expand Down Expand Up @@ -1462,7 +1469,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (newIsolateScopeDirective) {
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;

isolateScope = scope.$new(true);
isolateScope = scope.$new({isolate: true, partialDigest: isFunction(newIsolateScopeDirective.partialDigest)
? newIsolateScopeDirective.partialDigest($compileNode, templateAttrs)
: newIsolateScopeDirective.partialDigest});

if (templateDirective && (templateDirective === newIsolateScopeDirective ||
templateDirective === newIsolateScopeDirective.$$originalDirective)) {
Expand Down
12 changes: 9 additions & 3 deletions src/ng/interval.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,13 @@ function $IntervalProvider() {
* @param {number} delay Number of milliseconds between each function call.
* @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
* indefinitely.
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
* @param {(boolean|Scope)=} [invokeApply=true] By default, {@link ng.$rootScope.Scope#$apply $apply}
* is called on the {@link ng.$rootScope $rootScope} after each tick of the interval.
* The default behavior can be changed by setting the parameter to one of:
* - `true` (default) calls {@link ng.$rootScope.Scope#$apply $apply} on the {@link ng.$rootScope $rootScope}
* - `false` skips dirty checking completely. Can be useful for performance.
* - `Scope` calls {@link ng.$rootScope.Scope#$apply $apply} on the given {@link ng.$rootScope.Scope Scope}.
* Useful if the scope was created as partially digestible {@link ng.$rootScope.Scope#$new partialDigest}.
* @returns {promise} A promise which will be notified on each iteration.
*
* @example
Expand Down Expand Up @@ -136,6 +141,7 @@ function $IntervalProvider() {
clearInterval = $window.clearInterval,
iteration = 0,
skipApply = (isDefined(invokeApply) && !invokeApply),
scope = isScope(invokeApply) ? invokeApply : $rootScope,
deferred = (skipApply ? $$q : $q).defer(),
promise = deferred.promise;

Expand All @@ -152,7 +158,7 @@ function $IntervalProvider() {
delete intervals[promise.$$intervalId];
}

if (!skipApply) $rootScope.$apply();
if (!skipApply) scope.$apply();

}, delay);

Expand Down
24 changes: 17 additions & 7 deletions src/ng/rootScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ function $RootScopeProvider(){
this.$$listeners = {};
this.$$listenerCount = {};
this.$$isolateBindings = {};
this.$$digestTarget = true;
}

/**
Expand Down Expand Up @@ -162,19 +163,25 @@ function $RootScopeProvider(){
* desired for the scope and its child scopes to be permanently detached from the parent and
* thus stop participating in model change detection and listener notification by invoking.
*
* @param {boolean} isolate If true, then the scope does not prototypically inherit from the
* parent scope. The scope is isolated, as it can not see parent scope properties.
* When creating widgets, it is useful for the widget to not accidentally read parent
* state.
* @param {Object=} options An object that can contain two optional boolean flags:
*
* - `isolate` If true, then the scope does not prototypically inherit from the
* parent scope. The scope is isolated, as it can not see parent scope properties.
* When creating widgets, it is useful for the widget to not accidentally read parent
* state.
* - `partialDigest` If true, then calling {@link ng.$rootScope.Scope#$apply $apply()} on the scope
* will result in {@link ng.$rootScope.Scope#$digest $digest()} only happening on the scope itself
* and all its descendants. Useful as a performance improvement for structurally isolated components.
*
*
* @returns {Object} The newly created child scope.
*
*/
$new: function(isolate) {
$new: function(options) {
var ChildScope,
child;

if (isolate) {
if (options && options.isolate) {
child = new Scope();
child.$root = this.$root;
// ensure that there is just one async queue per $rootScope and its children
Expand Down Expand Up @@ -205,6 +212,7 @@ function $RootScopeProvider(){
} else {
this.$$childHead = this.$$childTail = child;
}
child.$$digestTarget = !!(options && options.partialDigest);
return child;
},

Expand Down Expand Up @@ -978,7 +986,9 @@ function $RootScopeProvider(){
} finally {
clearPhase();
try {
$rootScope.$digest();
var scope = this;
while(!scope.$$digestTarget) scope = scope.$parent;
scope.$digest();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my proposal: This is the place where we should evaluate the $$digestTarget expression:

```if (scope.$$digestTarget && !scope.$eval(scope.$$digestTarget)) scope = scope.$parent; ...`

} catch (e) {
$exceptionHandler(e);
throw e;
Expand Down
12 changes: 9 additions & 3 deletions src/ng/timeout.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,20 @@ function $TimeoutProvider() {
*
* @param {function()} fn A function, whose execution should be delayed.
* @param {number=} [delay=0] Delay in milliseconds.
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
* @param {(boolean|Scope)=} [invokeApply=true] By default, {@link ng.$rootScope.Scope#$apply $apply}
* is called on the {@link ng.$rootScope $rootScope} when the timeout is reached.
* The default behavior can be changed by setting the parameter to one of:
* - `true` (default) calls {@link ng.$rootScope.Scope#$apply $apply} on the {@link ng.$rootScope $rootScope}
* - `false` skips dirty checking completely. Can be useful for performance.
* - `Scope` calls {@link ng.$rootScope.Scope#$apply $apply} on the given {@link ng.$rootScope.Scope Scope}.
* Useful if the scope was created as partially digestible {@link ng.$rootScope.Scope#$new partialDigest}.
* @returns {Promise} Promise that will be resolved when the timeout is reached. The value this
* promise will be resolved with is the return value of the `fn` function.
*
*/
function timeout(fn, delay, invokeApply) {
var skipApply = (isDefined(invokeApply) && !invokeApply),
scope = isScope(invokeApply) ? invokeApply : $rootScope,
deferred = (skipApply ? $$q : $q).defer(),
promise = deferred.promise,
timeoutId;
Expand All @@ -49,7 +55,7 @@ function $TimeoutProvider() {
delete deferreds[promise.$$timeoutId];
}

if (!skipApply) $rootScope.$apply();
if (!skipApply) scope.$apply();
}, delay);

promise.$$timeoutId = timeoutId;
Expand Down
132 changes: 129 additions & 3 deletions test/ng/compileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2190,6 +2190,132 @@ describe('$compile', function() {
});
});
});

describe('partialDigest', function() {
var childScope;
beforeEach(module(function() {
directive('isolatePartialDigest', valueFn({
scope: {value: '='},
partialDigest: true,
template: '{{value}}',
link: function(scope) {childScope = scope;}
}));

directive('isolateTransPartialDigest', valueFn({
scope: {value: '='},
partialDigest: true,
transclude: true,
template: '{{value}}<span ng-transclude></span>',
link: function(scope) {childScope = scope;}
}));

}));

afterEach(function() {
childScope = null;
});


it('should allow you to mark an isolated scope with partialDigest', inject(
function($rootScope, $compile) {
element = $compile('<div>{{value}}<div isolate-partial-digest value="value"></div></div>')($rootScope);

$rootScope.$apply('value=1');
expect(element.text()).toBe('11');

$rootScope.value = 2;
childScope.$apply();
expect(element.text()).toBe('11');

$rootScope.$apply('value=3');
expect(element.text()).toBe('33');

childScope.$apply('value=4');
expect(element.text()).toBe('34');

$rootScope.$apply();
expect(element.text()).toBe('44');
}));


it('should digest the correct scopes with an isolate scope and transclusion', inject(
function($rootScope, $compile) {
element = $compile('<div>{{value}}<div isolate-trans-partial-digest value="value">{{value}}</div></div>')($rootScope);

$rootScope.$apply('value=1');
expect(element.text()).toBe('111');

$rootScope.value = 2;
childScope.$apply();
expect(element.text()).toBe('111');

$rootScope.$apply('value=3');
expect(element.text()).toBe('333');

childScope.$apply('value=4');
expect(element.text()).toBe('343');

$rootScope.$apply();
expect(element.text()).toBe('444');
}));

});

describe('partialDigest as function', function() {
var childScope;
beforeEach(module(function() {
directive('isolatePartialDigest', valueFn({
scope: {value: '='},
partialDigest: function ($element, $attrs) {
expect(nodeName_($element[0])).toBe('div');
expect($attrs.partialDigest).toBeDefined();
return $attrs.partialDigest === "true";
},
template: '{{value}}',
link: function(scope) {childScope = scope;}
}));
}));

afterEach(function() {
childScope = null;
});


it('should allow you to mark an isolated scope with partialDigest', inject(
function($rootScope, $compile) {
element = $compile('<div>{{value}}<div isolate-partial-digest value="value" partial-digest="true">content</div></div>')($rootScope);

$rootScope.$apply('value=1');
expect(element.text()).toBe('11');

$rootScope.value = 2;
childScope.$apply();
expect(element.text()).toBe('11');

$rootScope.$apply('value=3');
expect(element.text()).toBe('33');

childScope.$apply('value=4');
expect(element.text()).toBe('34');

$rootScope.$apply();
expect(element.text()).toBe('44');
}));


it('should allow you to not mark an isolated scope with partialDigest', inject(
function($rootScope, $compile) {
element = $compile('<div>{{value}}<div isolate-partial-digest value="value" partial-digest="false">content</div></div>')($rootScope);

$rootScope.$apply('value=1');
expect(element.text()).toBe('11');

$rootScope.value = 2;
childScope.$apply();
expect(element.text()).toBe('22');
}));

});
});


Expand Down Expand Up @@ -3111,7 +3237,7 @@ describe('$compile', function() {
expect(componentScope.ref).toBe('hello world');

componentScope.ref = 'ignore me';
expect($rootScope.$apply).
expect(function() {$rootScope.$apply(); }).
toThrowMinErr("$compile", "nonassign", "Expression ''hello ' + name' used with directive 'myComponent' is non-assignable!");
expect(componentScope.ref).toBe('hello world');
// reset since the exception was rethrown which prevented phase clearing
Expand Down Expand Up @@ -5277,7 +5403,7 @@ describe('$compile', function() {
/* jshint scripturl:true */
element = $compile('<iframe src="{{testUrl}}"></iframe>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl("javascript:doTrustedStuff()");
expect($rootScope.$apply).toThrowMinErr(
expect(function() {$rootScope.$apply(); }).toThrowMinErr(
"$interpolate", "interr", "Can't interpolate: {{testUrl}}\nError: [$sce:insecurl] Blocked " +
"loading resource from url not allowed by $sceDelegate policy. URL: javascript:doTrustedStuff()");
}));
Expand Down Expand Up @@ -5323,7 +5449,7 @@ describe('$compile', function() {
/* jshint scripturl:true */
element = $compile('<form action="{{testUrl}}"></form>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl("javascript:doTrustedStuff()");
expect($rootScope.$apply).toThrowMinErr(
expect(function() {$rootScope.$apply(); }).toThrowMinErr(
"$interpolate", "interr", "Can't interpolate: {{testUrl}}\nError: [$sce:insecurl] Blocked " +
"loading resource from url not allowed by $sceDelegate policy. URL: javascript:doTrustedStuff()");
}));
Expand Down
2 changes: 1 addition & 1 deletion test/ng/directive/ngSrcSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('ngSrc', function() {
it('should error on non-resource_url src attributes', inject(function($compile, $rootScope, $sce) {
element = $compile('<iframe ng-src="{{testUrl}}"></iframe>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl("javascript:doTrustedStuff()");
expect($rootScope.$apply).toThrowMinErr(
expect(function() {$rootScope.$apply(); }).toThrowMinErr(
"$interpolate", "interr", "Can't interpolate: {{testUrl}}\nError: [$sce:insecurl] Blocked " +
"loading resource from url not allowed by $sceDelegate policy. URL: " +
"javascript:doTrustedStuff()");
Expand Down
22 changes: 22 additions & 0 deletions test/ng/intervalSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,28 @@ describe('$interval', function() {
}));


it('should call $evalAsync or $digest on the scope that\'s passed as invokeApply',
inject(function($interval, $rootScope, $window, $timeout) {
var scope = $rootScope.$new({partialDigest: true});

var rootScopeDigest = jasmine.createSpy('rootScopeDigest');
var childScopeDigest = jasmine.createSpy('childScopeDigest');
$rootScope.$watch(rootScopeDigest);
scope.$watch(childScopeDigest);

var notifySpy = jasmine.createSpy('notify');

$interval(notifySpy, 1000, 1, scope);

$window.flush(2000);
$timeout.flush(); // flush $browser.defer() timeout

expect(notifySpy).toHaveBeenCalledOnce();
expect(rootScopeDigest).not.toHaveBeenCalled();
expect(childScopeDigest).toHaveBeenCalled();
}));


it('should allow you to specify the delay time', inject(function($interval, $window) {
var counter = 0;
$interval(function() { counter++; }, 123);
Expand Down
40 changes: 39 additions & 1 deletion test/ng/rootScopeSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,51 @@ describe('Scope', function() {
}));

it('should create a non prototypically inherited child scope', inject(function($rootScope) {
var child = $rootScope.$new(true);
var child = $rootScope.$new({isolate: true});
$rootScope.a = 123;
expect(child.a).toBeUndefined();
expect(child.$parent).toEqual($rootScope);
expect(child.$new).toBe($rootScope.$new);
expect(child.$root).toBe($rootScope);
}));

it('should create a child scope with partialDigest', inject(function($rootScope) {
var child = $rootScope.$new({isolate: false, partialDigest: true});
var spyRoot = jasmine.createSpy('spyRoot');
var spyChild = jasmine.createSpy('spyChild');
$rootScope.$watch(spyRoot);
child.$watch(spyChild);

child.$apply();
expect(spyRoot).not.toHaveBeenCalled();
expect(spyChild).toHaveBeenCalled();

spyRoot.reset();
spyChild.reset();

$rootScope.$apply();
expect(spyRoot).toHaveBeenCalled();
expect(spyChild).toHaveBeenCalled();
}));

it('should create an isolated child scope with partialDigest', inject(function($rootScope) {
var child = $rootScope.$new({isolate: true, partialDigest: true});
var spyRoot = jasmine.createSpy('spyRoot');
var spyChild = jasmine.createSpy('spyChild');
$rootScope.$watch(spyRoot);
child.$watch(spyChild);

child.$apply();
expect(spyRoot).not.toHaveBeenCalled();
expect(spyChild).toHaveBeenCalled();

spyRoot.reset();
spyChild.reset();

$rootScope.$apply();
expect(spyRoot).toHaveBeenCalled();
expect(spyChild).toHaveBeenCalled();
}));
});


Expand Down
Loading