Skip to content

Commit

Permalink
Implement basic version of NodeList
Browse files Browse the repository at this point in the history
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
  • Loading branch information
rubennorte authored and facebook-github-bot committed Mar 20, 2023
1 parent e4d83a1 commit c4b84ba
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,19 @@ export function* createValueIterator<T>(arrayLike: ArrayLike<T>): Iterator<T> {
yield arrayLike[i];
}
}

export function* createKeyIterator<T>(
arrayLike: ArrayLike<T>,
): Iterator<number> {
for (let i = 0; i < arrayLike.length; i++) {
yield i;
}
}

export function* createEntriesIterator<T>(
arrayLike: ArrayLike<T>,
): Iterator<[number, T]> {
for (let i = 0; i < arrayLike.length; i++) {
yield [i, arrayLike[i]];
}
}
104 changes: 104 additions & 0 deletions packages/react-native/Libraries/DOM/OldStyleCollections/NodeList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict
*/

// flowlint unsafe-getters-setters:off

import type {ArrayLike} from './ArrayLikeUtils';

import {
createEntriesIterator,
createKeyIterator,
createValueIterator,
} from './ArrayLikeUtils';

// IMPORTANT: The Flow type definition for this module is defined in `NodeList.js.flow`
// because Flow only supports indexers in classes in declaration files.

// $FlowIssue[prop-missing] Flow doesn't understand [Symbol.iterator]() {} and thinks this class doesn't implement the Iterable<T> interface.
export default class NodeList<T> implements Iterable<T>, ArrayLike<T> {
_length: number;

/**
* Use `createNodeList` to create instances of this class.
*
* @private This is not defined in the declaration file, so users will not see
* the signature of the constructor.
*/
constructor(elements: $ReadOnlyArray<T>) {
for (let i = 0; i < elements.length; i++) {
Object.defineProperty(this, i, {
value: elements[i],
writable: false,
});
}
this._length = elements.length;
}

get length(): number {
return this._length;
}

item(index: number): T | null {
if (index < 0 || index >= this._length) {
return null;
}

// assigning to the interface allows us to access the indexer property in a
// type-safe way.
// eslint-disable-next-line consistent-this
const arrayLike: ArrayLike<T> = this;
return arrayLike[index];
}

entries(): Iterator<[number, T]> {
return createEntriesIterator(this);
}

forEach<ThisType>(
callbackFn: (value: T, index: number, array: NodeList<T>) => mixed,
thisArg?: ThisType,
): void {
// assigning to the interface allows us to access the indexer property in a
// type-safe way.
// eslint-disable-next-line consistent-this
const arrayLike: ArrayLike<T> = this;

for (let index = 0; index < this._length; index++) {
if (thisArg == null) {
callbackFn(arrayLike[index], index, this);
} else {
callbackFn.call(thisArg, arrayLike[index], index, this);
}
}
}

keys(): Iterator<number> {
return createKeyIterator(this);
}

values(): Iterator<T> {
return createValueIterator(this);
}

// $FlowIssue[unsupported-syntax] Flow does not support computed properties in classes.
[Symbol.iterator](): Iterator<T> {
return createValueIterator(this);
}
}

/**
* This is an internal method to create instances of `NodeList`,
* which avoids leaking its constructor to end users.
* We can do that because the external definition of `NodeList` lives in
* `NodeList.js.flow`, not here.
*/
export function createNodeList<T>(elements: $ReadOnlyArray<T>): NodeList<T> {
return new NodeList(elements);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict
*/

import type {ArrayLike} from './ArrayLikeUtils';

declare export default class NodeList<+T> implements Iterable<T>, ArrayLike<T> {
// This property should've been read-only as well, but Flow doesn't handle
// read-only indexers correctly (thinks reads are writes and fails).
[index: number]: T;
+length: number;
item(index: number): T | null;
entries(): Iterator<[number, T]>;
forEach<ThisType>(
callbackFn: (value: T, index: number, array: NodeList<T>) => mixed,
thisArg?: ThisType,
): void;
keys(): Iterator<number>;
values(): Iterator<T>;
@@iterator(): Iterator<T>;
}

declare export function createNodeList<T>(
elements: $ReadOnlyArray<T>,
): NodeList<T>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/

import {createNodeList} from '../NodeList';

describe('NodeList', () => {
it('provides an array-like interface', () => {
const collection = createNodeList(['a', 'b', 'c']);

expect(collection[0]).toBe('a');
expect(collection[1]).toBe('b');
expect(collection[2]).toBe('c');
expect(collection[3]).toBe(undefined);
expect(collection.length).toBe(3);
});

it('provides indexed access through the item method', () => {
const collection = createNodeList(['a', 'b', 'c']);

expect(collection.item(0)).toBe('a');
expect(collection.item(1)).toBe('b');
expect(collection.item(2)).toBe('c');
expect(collection.item(3)).toBe(null);
});

it('is immutable (loose mode)', () => {
const collection = createNodeList(['a', 'b', 'c']);

collection[0] = 'replacement';
expect(collection[0]).toBe('a');

// $FlowExpectedError[cannot-write]
collection.length = 100;
expect(collection.length).toBe(3);
});

it('is immutable (strict mode)', () => {
'use strict';

const collection = createNodeList(['a', 'b', 'c']);

expect(() => {
collection[0] = 'replacement';
}).toThrow(TypeError);
expect(collection[0]).toBe('a');

expect(() => {
// $FlowExpectedError[cannot-write]
collection.length = 100;
}).toThrow(TypeError);
expect(collection.length).toBe(3);
});

it('can be converted to an array through common methods', () => {
const collection = createNodeList(['a', 'b', 'c']);

expect(Array.from(collection)).toEqual(['a', 'b', 'c']);
expect([...collection]).toEqual(['a', 'b', 'c']);
});

it('can be traversed with for-of', () => {
const collection = createNodeList(['a', 'b', 'c']);

let i = 0;
for (const value of collection) {
expect(value).toBe(collection[i]);
i++;
}
});

describe('keys()', () => {
it('returns an iterator for keys', () => {
const collection = createNodeList(['a', 'b', 'c']);

const keys = collection.keys();
expect(keys.next()).toEqual({value: 0, done: false});
expect(keys.next()).toEqual({value: 1, done: false});
expect(keys.next()).toEqual({value: 2, done: false});
expect(keys.next()).toEqual({done: true});

let i = 0;
for (const key of collection.keys()) {
expect(key).toBe(i);
i++;
}
});
});

describe('values()', () => {
it('returns an iterator for values', () => {
const collection = createNodeList(['a', 'b', 'c']);

const values = collection.values();
expect(values.next()).toEqual({value: 'a', done: false});
expect(values.next()).toEqual({value: 'b', done: false});
expect(values.next()).toEqual({value: 'c', done: false});
expect(values.next()).toEqual({done: true});

let i = 0;
for (const value of collection.values()) {
expect(value).toBe(collection[i]);
i++;
}
});
});

describe('entries()', () => {
it('returns an iterator for entries', () => {
const collection = createNodeList(['a', 'b', 'c']);

const entries = collection.entries();
expect(entries.next()).toEqual({value: [0, 'a'], done: false});
expect(entries.next()).toEqual({value: [1, 'b'], done: false});
expect(entries.next()).toEqual({value: [2, 'c'], done: false});
expect(entries.next()).toEqual({done: true});

let i = 0;
for (const entry of collection.entries()) {
expect(entry).toEqual([i, collection[i]]);
i++;
}
});
});

describe('forEach()', () => {
it('iterates over the elements like array.forEach (implicit `this`)', () => {
const collection = createNodeList(['a', 'b', 'c']);

let i = 0;
collection.forEach(function (this: mixed, value, index, list) {
expect(value).toBe(collection[i]);
expect(index).toBe(i);
expect(list).toBe(collection);
expect(this).toBe(window);
i++;
});
});

it('iterates over the elements like array.forEach (explicit `this`)', () => {
const collection = createNodeList(['a', 'b', 'c']);

let i = 0;
const explicitThis = {id: 'foo'};
collection.forEach(function (this: mixed, value, index, list) {
expect(value).toBe(collection[i]);
expect(index).toBe(i);
expect(list).toBe(collection);
expect(this).toBe(explicitThis);
i++;
}, explicitThis);
});
});
});

0 comments on commit c4b84ba

Please sign in to comment.