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

Commit 8563479

Browse files
committed
fix(renderer): native elements could not be moved (fixes TodoMVC sample)
1 parent f147a60 commit 8563479

File tree

6 files changed

+81
-45
lines changed

6 files changed

+81
-45
lines changed

src/renderer/native_command.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,18 @@ export class NativeCommandAttachAfter extends NativeCommand {
135135
private _attach(wrapper: ReactNativeWrapper, node: Node, ancestor: Node, counters: Map<Node, number>, baseNativeIndex: number): void {
136136
var shift = counters.get(this.anchor) || 0;
137137
var nativeIndex = baseNativeIndex + shift;
138-
wrapper.manageChildren(ancestor.nativeTag, null, null, [node.nativeTag], [nativeIndex], null);
139-
ancestor.nativeChildren.splice(nativeIndex, 0, node.nativeTag);
138+
if (!node.toBeMoved) {
139+
wrapper.manageChildren(ancestor.nativeTag, null, null, [node.nativeTag], [nativeIndex], null);
140+
ancestor.nativeChildren.splice(nativeIndex, 0, node.nativeTag);
141+
} else {
142+
var currentIndex = ancestor.nativeChildren.indexOf(node.nativeTag);
143+
if (currentIndex != nativeIndex) {
144+
wrapper.manageChildren(ancestor.nativeTag, [currentIndex], [nativeIndex], null, null, null);
145+
ancestor.nativeChildren.splice(currentIndex, 1);
146+
ancestor.nativeChildren.splice(nativeIndex, 0, node.nativeTag);
147+
}
148+
node.toBeMoved = false;
149+
}
140150
counters.set(this.anchor, shift + 1);
141151
}
142152
}
@@ -148,14 +158,18 @@ export class NativeCommandDetach extends NativeCommand {
148158

149159
execute(wrapper: ReactNativeWrapper) {
150160
var parent = this.target.parent;
151-
var index = parent.children.indexOf(this.target);
152-
parent.children.splice(index, 1);
153161
var toDetach = this.target.nativeTag > -1 ? this.target : (this.target.isVirtual ? this.target.getFirstCreatedChild() : null);
154162
if (toDetach) {
155-
var nativeIndex = parent.nativeChildren.indexOf(toDetach.nativeTag);
156-
parent.nativeChildren.splice(nativeIndex, 1);
157-
wrapper.manageChildren(parent.nativeTag, null, null, null, null, [nativeIndex]);
158-
toDetach.destroyNative();
163+
if (toDetach.toBeDestroyed) {
164+
var nativeIndex = parent.nativeChildren.indexOf(toDetach.nativeTag);
165+
parent.nativeChildren.splice(nativeIndex, 1);
166+
wrapper.manageChildren(parent.nativeTag, null, null, null, null, [nativeIndex]);
167+
toDetach.destroyNative();
168+
}
169+
else {
170+
//Node is being moved
171+
this.target.toBeMoved = true;
172+
}
159173
}
160174
}
161175
}

src/renderer/node.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export abstract class Node {
1010
public tagName: string = "";
1111
public properties: {[s: string]: any } = {};
1212
public isVirtual: boolean = false;
13+
public toBeDestroyed: boolean = false;
14+
public toBeMoved: boolean = false;
1315

1416
public nativeTag: number = -1;
1517
public isCreated: boolean = false;
@@ -121,6 +123,7 @@ export abstract class Node {
121123
nodeMap.delete(this.nativeTag);
122124
this.nativeTag = -1;
123125
this.nativeChildren = [];
126+
this.toBeDestroyed = this.toBeMoved = false;
124127
for (var i = 0; i < this.children.length; i++) {
125128
this.children[i].destroyNative();
126129
}

src/renderer/renderer.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,18 @@ export class ReactNativeRenderer implements Renderer {
192192
detachView(viewRootNodes: Node[]): void {
193193
for (var i = 0; i < viewRootNodes.length; i++) {
194194
var node = viewRootNodes[i];
195+
var parent = node.parent;
196+
var index = parent.children.indexOf(node);
197+
parent.children.splice(index, 1);
195198
this._rootRenderer.addDetachCommand(node);
196199
}
197200
}
198201

199-
destroyView(hostElement:any, viewAllNodes:any[]):any {
200-
//TODO: Nothing to do, detachView took care of it. Can it be improved to avoid destruction and creation?
202+
destroyView(hostElement: any, viewAllNodes: Node[]): void {
203+
for (var i = 0; i < viewAllNodes.length; i++) {
204+
var node = viewAllNodes[i];
205+
node.toBeDestroyed = true;
206+
}
201207
}
202208

203209
listen(renderElement: Node, name: string, callback: Function): Function {

src/wrapper/wrapper_mock.ts

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import {ReactNativeWrapper, overridePlatform} from "./wrapper";
22

3+
class Command {
4+
constructor(public name: string, public target: number, public details: string) { }
5+
toString(): string {
6+
return `${this.name}+${this.target}+${this.details}`;
7+
}
8+
}
9+
310
export class MockReactNativeWrapper extends ReactNativeWrapper {
411
commandLogs: Array<Command>;
512
root: NativeElement;
@@ -46,34 +53,44 @@ export class MockReactNativeWrapper extends ReactNativeWrapper {
4653
// moveTo and addAt are both relative to the final state of the View's children.
4754
manageChildren(parentTag: number, moveFrom: Array<number>, moveTo: Array<number>, addTags: Array<number>, addAt: Array<number>, removeFrom: Array<number>) {
4855
var parentElement = this.nativeElementMap.get(parentTag);
49-
var toBeDeleted = removeFrom || [];
56+
var toBeDeleted = [];
5057
var toBeAdded: Array<any> = [];
51-
var toBeMoved: Array<any> = [];
52-
if (moveFrom && moveTo && moveFrom.length == moveTo.length) {
53-
toBeDeleted = toBeDeleted.concat(moveFrom).sort();
58+
if (moveFrom && moveTo && moveFrom.length != moveTo.length) {
59+
throw new Error(`manageChildren - MOVE - Invalid lengths: ${moveFrom.length} vs ${moveTo.length}`);
60+
}
61+
if (addTags && addAt && addTags.length != addAt.length) {
62+
throw new Error(`manageChildren - MOVE - Invalid lengths: ${addTags.length} vs ${addAt.length}`);
63+
}
64+
//Detach commands
65+
if (removeFrom) {
66+
for (var i = 0; i < removeFrom.length; i++) {
67+
var index = removeFrom[removeFrom.length - i - 1];
68+
toBeDeleted.push(index);
69+
this.commandLogs.push(new Command('DETACH', parentTag, '' + index));
70+
}
71+
}
72+
//Move commands
73+
if (moveFrom) {
5474
for (var i = 0; i < moveFrom.length; i++) {
5575
var tag = parentElement.children[moveFrom[i]].tag;
76+
toBeDeleted.push(moveFrom[i]);
5677
toBeAdded.push({index: moveTo[i], tag: tag});
57-
toBeMoved.push(tag);
5878
}
79+
this.commandLogs.push(new Command('MOVE', parentTag, '' + moveFrom.join(',') + '+' + moveTo.join(',')));
5980
}
60-
if (addTags && addAt && addTags.length == addAt.length) {
81+
//Attach commands
82+
if (addTags) {
6183
for (var i = 0; i < addTags.length; i++) {
6284
toBeAdded.push({index: addAt[i], tag: addTags[i]});
85+
this.commandLogs.push(new Command('ATTACH', parentTag, addTags[i] + '+' + addAt[i]));
6386
}
6487
}
65-
//Detach
88+
//Update data structure
89+
toBeDeleted.sort();
90+
toBeAdded.sort((a, b) => { return a.index - b.index});
6691
for (var i = 0; i < toBeDeleted.length; i++) {
67-
var index = toBeDeleted[toBeDeleted.length - i - 1];
68-
var tag = parentElement.children[index].tag;
69-
parentElement.children.splice(index, 1);
70-
if (toBeMoved.indexOf(tag) == -1) {
71-
this.nativeElementMap.delete(tag);
72-
}
73-
this.commandLogs.push(new Command('DETACH', parentTag, '' + index));
92+
parentElement.children.splice(toBeDeleted[toBeDeleted.length - i - 1], 1);
7493
}
75-
//Attach
76-
toBeAdded.sort((a, b) => { return a.index - b.index});
7794
for (var i = 0; i < toBeAdded.length; i++) {
7895
var item = toBeAdded[i];
7996
var element = this.nativeElementMap.get(item.tag);
@@ -83,8 +100,6 @@ export class MockReactNativeWrapper extends ReactNativeWrapper {
83100
} else {
84101
throw new Error(`manageChildren - ATTACH - Invalid index ${item.index}, size is ${parentElement.children.length}`);
85102
}
86-
87-
this.commandLogs.push(new Command('ATTACH', parentTag, item.tag + '+' + item.index));
88103
}
89104
}
90105

@@ -158,13 +173,6 @@ export class MockReactNativeWrapper extends ReactNativeWrapper {
158173
}
159174
}
160175

161-
class Command {
162-
constructor(public name: string, public target: number, public details: string) { }
163-
toString(): string {
164-
return `${this.name}+${this.target}+${this.details}`;
165-
}
166-
}
167-
168176
export class NativeElement {
169177
name: string;
170178
tag: number;

test/renderer/element_spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,17 @@ describe('Element', () => {
179179
rootRenderer.executeCommands();
180180
expect(mock.nativeElementMap.get(1).children[0].children.map((a: NativeElement) => a.children[0].properties['text']).join(',')).toEqual('1,2,3');
181181

182+
mock.clearLogs();
183+
fixture.debugElement.componentInstance.a.splice(1, 1);
184+
fixture.detectChanges();
185+
rootRenderer.executeCommands();
186+
expect(mock.commandLogs.toString()).toEqual('DETACH+2+1'); //MOVE+2+1+1 is removed for optimization
187+
expect(mock.nativeElementMap.get(1).children[0].children.map((a: NativeElement) => a.children[0].properties['text']).join(',')).toEqual('1,3');
188+
182189
fixture.debugElement.componentInstance.a.pop();
183190
fixture.detectChanges();
184191
rootRenderer.executeCommands();
185-
expect(mock.nativeElementMap.get(1).children[0].children.map((a: NativeElement) => a.children[0].properties['text']).join(',')).toEqual('1,2');
192+
expect(mock.nativeElementMap.get(1).children[0].children.map((a: NativeElement) => a.children[0].properties['text']).join(',')).toEqual('1');
186193

187194
fixture.debugElement.componentInstance.a = [];
188195
fixture.detectChanges();

test/wrapper/wrapper_mock_spec.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,9 @@ describe('MockReactNativeWrapper', () => {
7070
mock.manageChildren(1, null, null, null, null, [0]);
7171
expect(mock.commandLogs.length).toEqual(1);
7272
expect(mock.commandLogs.toString()).toEqual('DETACH+1+0');
73-
var element1 = mock.nativeElementMap.get(2);
74-
var element2 = mock.nativeElementMap.get(3);
75-
expect(element1).toEqual(undefined);
76-
expect(element2.parent).toEqual(mock.root);
77-
expect(mock.root.children).toEqual([element2]);
73+
var element = mock.nativeElementMap.get(3);
74+
expect(element.parent).toEqual(mock.root);
75+
expect(mock.root.children).toEqual([element]);
7876
});
7977

8078
it('should move a native element', () => {
@@ -85,8 +83,8 @@ describe('MockReactNativeWrapper', () => {
8583
mock.manageChildren(1, null, null, [2,3,4], [0,1,2], null);
8684
mock.clearLogs();
8785
mock.manageChildren(1, [1], [2], null, null, null);
88-
expect(mock.commandLogs.length).toEqual(2);
89-
expect(mock.commandLogs.toString()).toEqual('DETACH+1+1,ATTACH+1+3+2');
86+
expect(mock.commandLogs.length).toEqual(1);
87+
expect(mock.commandLogs.toString()).toEqual('MOVE+1+1+2');
9088
var element = mock.nativeElementMap.get(3);
9189
expect(element.parent).toEqual(mock.root);
9290
expect(mock.root.children[2]).toEqual(element);
@@ -102,8 +100,8 @@ describe('MockReactNativeWrapper', () => {
102100
mock.createView('RCTView', 1, {});
103101
mock.clearLogs();
104102
mock.manageChildren(1, [0,1], [1,2], [5,6], [0,3], [2]);
105-
expect(mock.commandLogs.length).toEqual(7);
106-
expect(mock.commandLogs.toString()).toEqual('DETACH+1+2,DETACH+1+1,DETACH+1+0,ATTACH+1+5+0,ATTACH+1+2+1,ATTACH+1+3+2,ATTACH+1+6+3');
103+
expect(mock.commandLogs.length).toEqual(4);
104+
expect(mock.commandLogs.toString()).toEqual('DETACH+1+2,MOVE+1+0,1+1,2,ATTACH+1+5+0,ATTACH+1+6+3');
107105
expect(mock.nativeElementMap.get(1).children.map((a) => a.tag).join(',')).toEqual('5,2,3,6');
108106
});
109107

0 commit comments

Comments
 (0)