Skip to content

Commit c4b84ba

Browse files
rubennortefacebook-github-bot
authored andcommitted
Implement basic version of NodeList
Summary: This implements a basic version of NodeList that's close to the spec but diverges in some things (e.g.: methods could be called with an instance created through `Object.create`, etc.). This will be used soon to implement `ReadOnlyNode.childNodes` (behind a flag). See: react-native-community/discussions-and-proposals#607 Changelog: [internal] Reviewed By: yungsters Differential Revision: D44055911 fbshipit-source-id: 10b435b06ea6f75eaead85f01c2703e05bb3bd37
1 parent e4d83a1 commit c4b84ba

File tree

4 files changed

+312
-0
lines changed

4 files changed

+312
-0
lines changed

packages/react-native/Libraries/DOM/OldStyleCollections/ArrayLikeUtils.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,19 @@ export function* createValueIterator<T>(arrayLike: ArrayLike<T>): Iterator<T> {
2828
yield arrayLike[i];
2929
}
3030
}
31+
32+
export function* createKeyIterator<T>(
33+
arrayLike: ArrayLike<T>,
34+
): Iterator<number> {
35+
for (let i = 0; i < arrayLike.length; i++) {
36+
yield i;
37+
}
38+
}
39+
40+
export function* createEntriesIterator<T>(
41+
arrayLike: ArrayLike<T>,
42+
): Iterator<[number, T]> {
43+
for (let i = 0; i < arrayLike.length; i++) {
44+
yield [i, arrayLike[i]];
45+
}
46+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict
9+
*/
10+
11+
// flowlint unsafe-getters-setters:off
12+
13+
import type {ArrayLike} from './ArrayLikeUtils';
14+
15+
import {
16+
createEntriesIterator,
17+
createKeyIterator,
18+
createValueIterator,
19+
} from './ArrayLikeUtils';
20+
21+
// IMPORTANT: The Flow type definition for this module is defined in `NodeList.js.flow`
22+
// because Flow only supports indexers in classes in declaration files.
23+
24+
// $FlowIssue[prop-missing] Flow doesn't understand [Symbol.iterator]() {} and thinks this class doesn't implement the Iterable<T> interface.
25+
export default class NodeList<T> implements Iterable<T>, ArrayLike<T> {
26+
_length: number;
27+
28+
/**
29+
* Use `createNodeList` to create instances of this class.
30+
*
31+
* @private This is not defined in the declaration file, so users will not see
32+
* the signature of the constructor.
33+
*/
34+
constructor(elements: $ReadOnlyArray<T>) {
35+
for (let i = 0; i < elements.length; i++) {
36+
Object.defineProperty(this, i, {
37+
value: elements[i],
38+
writable: false,
39+
});
40+
}
41+
this._length = elements.length;
42+
}
43+
44+
get length(): number {
45+
return this._length;
46+
}
47+
48+
item(index: number): T | null {
49+
if (index < 0 || index >= this._length) {
50+
return null;
51+
}
52+
53+
// assigning to the interface allows us to access the indexer property in a
54+
// type-safe way.
55+
// eslint-disable-next-line consistent-this
56+
const arrayLike: ArrayLike<T> = this;
57+
return arrayLike[index];
58+
}
59+
60+
entries(): Iterator<[number, T]> {
61+
return createEntriesIterator(this);
62+
}
63+
64+
forEach<ThisType>(
65+
callbackFn: (value: T, index: number, array: NodeList<T>) => mixed,
66+
thisArg?: ThisType,
67+
): void {
68+
// assigning to the interface allows us to access the indexer property in a
69+
// type-safe way.
70+
// eslint-disable-next-line consistent-this
71+
const arrayLike: ArrayLike<T> = this;
72+
73+
for (let index = 0; index < this._length; index++) {
74+
if (thisArg == null) {
75+
callbackFn(arrayLike[index], index, this);
76+
} else {
77+
callbackFn.call(thisArg, arrayLike[index], index, this);
78+
}
79+
}
80+
}
81+
82+
keys(): Iterator<number> {
83+
return createKeyIterator(this);
84+
}
85+
86+
values(): Iterator<T> {
87+
return createValueIterator(this);
88+
}
89+
90+
// $FlowIssue[unsupported-syntax] Flow does not support computed properties in classes.
91+
[Symbol.iterator](): Iterator<T> {
92+
return createValueIterator(this);
93+
}
94+
}
95+
96+
/**
97+
* This is an internal method to create instances of `NodeList`,
98+
* which avoids leaking its constructor to end users.
99+
* We can do that because the external definition of `NodeList` lives in
100+
* `NodeList.js.flow`, not here.
101+
*/
102+
export function createNodeList<T>(elements: $ReadOnlyArray<T>): NodeList<T> {
103+
return new NodeList(elements);
104+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict
9+
*/
10+
11+
import type {ArrayLike} from './ArrayLikeUtils';
12+
13+
declare export default class NodeList<+T> implements Iterable<T>, ArrayLike<T> {
14+
// This property should've been read-only as well, but Flow doesn't handle
15+
// read-only indexers correctly (thinks reads are writes and fails).
16+
[index: number]: T;
17+
+length: number;
18+
item(index: number): T | null;
19+
entries(): Iterator<[number, T]>;
20+
forEach<ThisType>(
21+
callbackFn: (value: T, index: number, array: NodeList<T>) => mixed,
22+
thisArg?: ThisType,
23+
): void;
24+
keys(): Iterator<number>;
25+
values(): Iterator<T>;
26+
@@iterator(): Iterator<T>;
27+
}
28+
29+
declare export function createNodeList<T>(
30+
elements: $ReadOnlyArray<T>,
31+
): NodeList<T>;
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
import {createNodeList} from '../NodeList';
13+
14+
describe('NodeList', () => {
15+
it('provides an array-like interface', () => {
16+
const collection = createNodeList(['a', 'b', 'c']);
17+
18+
expect(collection[0]).toBe('a');
19+
expect(collection[1]).toBe('b');
20+
expect(collection[2]).toBe('c');
21+
expect(collection[3]).toBe(undefined);
22+
expect(collection.length).toBe(3);
23+
});
24+
25+
it('provides indexed access through the item method', () => {
26+
const collection = createNodeList(['a', 'b', 'c']);
27+
28+
expect(collection.item(0)).toBe('a');
29+
expect(collection.item(1)).toBe('b');
30+
expect(collection.item(2)).toBe('c');
31+
expect(collection.item(3)).toBe(null);
32+
});
33+
34+
it('is immutable (loose mode)', () => {
35+
const collection = createNodeList(['a', 'b', 'c']);
36+
37+
collection[0] = 'replacement';
38+
expect(collection[0]).toBe('a');
39+
40+
// $FlowExpectedError[cannot-write]
41+
collection.length = 100;
42+
expect(collection.length).toBe(3);
43+
});
44+
45+
it('is immutable (strict mode)', () => {
46+
'use strict';
47+
48+
const collection = createNodeList(['a', 'b', 'c']);
49+
50+
expect(() => {
51+
collection[0] = 'replacement';
52+
}).toThrow(TypeError);
53+
expect(collection[0]).toBe('a');
54+
55+
expect(() => {
56+
// $FlowExpectedError[cannot-write]
57+
collection.length = 100;
58+
}).toThrow(TypeError);
59+
expect(collection.length).toBe(3);
60+
});
61+
62+
it('can be converted to an array through common methods', () => {
63+
const collection = createNodeList(['a', 'b', 'c']);
64+
65+
expect(Array.from(collection)).toEqual(['a', 'b', 'c']);
66+
expect([...collection]).toEqual(['a', 'b', 'c']);
67+
});
68+
69+
it('can be traversed with for-of', () => {
70+
const collection = createNodeList(['a', 'b', 'c']);
71+
72+
let i = 0;
73+
for (const value of collection) {
74+
expect(value).toBe(collection[i]);
75+
i++;
76+
}
77+
});
78+
79+
describe('keys()', () => {
80+
it('returns an iterator for keys', () => {
81+
const collection = createNodeList(['a', 'b', 'c']);
82+
83+
const keys = collection.keys();
84+
expect(keys.next()).toEqual({value: 0, done: false});
85+
expect(keys.next()).toEqual({value: 1, done: false});
86+
expect(keys.next()).toEqual({value: 2, done: false});
87+
expect(keys.next()).toEqual({done: true});
88+
89+
let i = 0;
90+
for (const key of collection.keys()) {
91+
expect(key).toBe(i);
92+
i++;
93+
}
94+
});
95+
});
96+
97+
describe('values()', () => {
98+
it('returns an iterator for values', () => {
99+
const collection = createNodeList(['a', 'b', 'c']);
100+
101+
const values = collection.values();
102+
expect(values.next()).toEqual({value: 'a', done: false});
103+
expect(values.next()).toEqual({value: 'b', done: false});
104+
expect(values.next()).toEqual({value: 'c', done: false});
105+
expect(values.next()).toEqual({done: true});
106+
107+
let i = 0;
108+
for (const value of collection.values()) {
109+
expect(value).toBe(collection[i]);
110+
i++;
111+
}
112+
});
113+
});
114+
115+
describe('entries()', () => {
116+
it('returns an iterator for entries', () => {
117+
const collection = createNodeList(['a', 'b', 'c']);
118+
119+
const entries = collection.entries();
120+
expect(entries.next()).toEqual({value: [0, 'a'], done: false});
121+
expect(entries.next()).toEqual({value: [1, 'b'], done: false});
122+
expect(entries.next()).toEqual({value: [2, 'c'], done: false});
123+
expect(entries.next()).toEqual({done: true});
124+
125+
let i = 0;
126+
for (const entry of collection.entries()) {
127+
expect(entry).toEqual([i, collection[i]]);
128+
i++;
129+
}
130+
});
131+
});
132+
133+
describe('forEach()', () => {
134+
it('iterates over the elements like array.forEach (implicit `this`)', () => {
135+
const collection = createNodeList(['a', 'b', 'c']);
136+
137+
let i = 0;
138+
collection.forEach(function (this: mixed, value, index, list) {
139+
expect(value).toBe(collection[i]);
140+
expect(index).toBe(i);
141+
expect(list).toBe(collection);
142+
expect(this).toBe(window);
143+
i++;
144+
});
145+
});
146+
147+
it('iterates over the elements like array.forEach (explicit `this`)', () => {
148+
const collection = createNodeList(['a', 'b', 'c']);
149+
150+
let i = 0;
151+
const explicitThis = {id: 'foo'};
152+
collection.forEach(function (this: mixed, value, index, list) {
153+
expect(value).toBe(collection[i]);
154+
expect(index).toBe(i);
155+
expect(list).toBe(collection);
156+
expect(this).toBe(explicitThis);
157+
i++;
158+
}, explicitThis);
159+
});
160+
});
161+
});

0 commit comments

Comments
 (0)