Skip to content

Commit d8a3e5b

Browse files
committed
Add index watchers
1 parent a9990cf commit d8a3e5b

3 files changed

Lines changed: 100 additions & 11 deletions

File tree

src/core/scope/scope.spec.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3297,6 +3297,22 @@ describe("Scope", () => {
32973297
expect(scope._children.includes(child2)).toBeFalse();
32983298
});
32993299

3300+
it("should destroy a displaced direct child scope when replacing an array item by index", () => {
3301+
const scope = createScope();
3302+
const child = scope.$new();
3303+
const onDestroy = jasmine.createSpy("child destroy on index replace");
3304+
3305+
child.$on("$destroy", onDestroy);
3306+
scope.items = [child];
3307+
3308+
scope.items[0] = { replacement: true };
3309+
3310+
expect(onDestroy).toHaveBeenCalled();
3311+
expect(child.$handler._destroyed).toBeTrue();
3312+
expect(scope._children.includes(child)).toBeFalse();
3313+
expect(scope.items[0].replacement).toBeTrue();
3314+
});
3315+
33003316
it("should destroy direct child scopes contained in a displaced collection when overwritten", () => {
33013317
const scope = createScope();
33023318
const child1 = scope.$new();

src/core/scope/scope.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,10 @@ export class Scope {
751751
if (_foreignListeners) {
752752
this._scheduleListener(_foreignListeners);
753753
}
754+
755+
if (!isProxy(value)) {
756+
this._scheduleArrayOwnerListeners(target, proxy, property);
757+
}
754758
}
755759

756760
if (this._objectListeners.get(target[property])) {
@@ -789,6 +793,10 @@ export class Scope {
789793
}
790794

791795
this._checkListenersForAllKeys(value);
796+
797+
if (!isProxy(value)) {
798+
this._scheduleArrayOwnerListeners(target, proxy, property);
799+
}
792800
}
793801
target[property] = createScope(value, this);
794802

@@ -847,17 +855,7 @@ export class Scope {
847855
}
848856

849857
if (isArray(target)) {
850-
if (this._objectListeners.has(proxy) && property !== "length") {
851-
const keyList = this._objectListeners.get(proxy);
852-
853-
if (keyList) {
854-
for (let i = 0, l = keyList.length; i < l; i++) {
855-
const currentListeners = this._watchers.get(keyList[i]);
856-
857-
if (currentListeners) this._scheduleListener(currentListeners);
858-
}
859-
}
860-
}
858+
this._scheduleArrayOwnerListeners(target, proxy, property);
861859
}
862860

863861
return true;
@@ -1798,6 +1796,35 @@ export class Scope {
17981796
return false;
17991797
}
18001798

1799+
/** @internal Reschedules watchers that observe this array through its owning scope property. */
1800+
_scheduleArrayOwnerListeners(
1801+
target: ScopeTarget,
1802+
proxy: ScopeProxy,
1803+
property: string,
1804+
): void {
1805+
if (!isArray(target) || property === "length") {
1806+
return;
1807+
}
1808+
1809+
if (!this._objectListeners.has(proxy)) {
1810+
return;
1811+
}
1812+
1813+
const keyList = this._objectListeners.get(proxy);
1814+
1815+
if (!keyList) {
1816+
return;
1817+
}
1818+
1819+
for (let i = 0, l = keyList.length; i < l; i++) {
1820+
const currentListeners = this._watchers.get(keyList[i]);
1821+
1822+
if (currentListeners) {
1823+
this._scheduleListener(currentListeners);
1824+
}
1825+
}
1826+
}
1827+
18011828
/** Evaluates an Angular expression in the context of this scope. */
18021829
$eval(expr: ng.Expression, locals?: Record<string, any>) {
18031830
const fn = $parse(expr);

src/directive/repeat/repeat.spec.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,34 @@ describe("ngRepeat", () => {
649649
expect(lis[1].getAttribute("mark")).toEqual("a");
650650
});
651651

652+
it("should preserve repeated row DOM when the collection is shallow-cloned with the same items", async () => {
653+
element = $compile(
654+
'<ul><li ng-repeat="item in items">{{item.name}}|</li></ul>',
655+
)(scope);
656+
const first = { name: "a" };
657+
const second = { name: "b" };
658+
659+
scope.items = [first, second];
660+
await wait();
661+
662+
let lis = element.querySelectorAll("li");
663+
const firstRow = lis[0];
664+
const secondRow = lis[1];
665+
666+
firstRow.setAttribute("mark", "first");
667+
secondRow.setAttribute("mark", "second");
668+
669+
scope.items = [...scope.items];
670+
await wait();
671+
672+
lis = element.querySelectorAll("li");
673+
expect(lis[0]).toBe(firstRow);
674+
expect(lis[1]).toBe(secondRow);
675+
expect(lis[0].getAttribute("mark")).toBe("first");
676+
expect(lis[1].getAttribute("mark")).toBe("second");
677+
expect(element.textContent).toBe("a|b|");
678+
});
679+
652680
it("keeps grouped interpolation bindings when archived items are removed", async () => {
653681
element = $compile(
654682
'<ul><li ng-repeat="todo in tasks">{{todo.task}} {{todo.done}}|</li></ul>',
@@ -667,6 +695,24 @@ describe("ngRepeat", () => {
667695
await wait();
668696
expect(element.textContent).toBe("Build an AngularTS app false|");
669697
});
698+
699+
it("updates repeated rows when an array item object is replaced by index", async () => {
700+
element = $compile(
701+
'<ul><li ng-repeat="todo in tasks">{{todo.task}} {{todo.done}}|</li></ul>',
702+
)(scope);
703+
scope.tasks = [
704+
{ task: "First Task", done: false },
705+
{ task: "Second Task", done: true },
706+
];
707+
708+
await wait();
709+
expect(element.textContent).toBe("First Task false|Second Task true|");
710+
711+
scope.tasks[0] = { task: "New Task", done: false };
712+
await wait();
713+
714+
expect(element.textContent).toBe("New Task false|Second Task true|");
715+
});
670716
});
671717

672718
describe("nesting in replaced directive templates", () => {

0 commit comments

Comments
 (0)