From e4d83a14f5f0e2878b899c8ae62f88396af4c2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 20 Mar 2023 06:01:25 -0700 Subject: [PATCH] Implement basic version of HTMLCollection Summary: This implements a basic version of HTMLCollection 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 `ReadOnlyElement.children` (behind a flag). See: https://github.com/react-native-community/discussions-and-proposals/pull/607 Changelog: [internal] Reviewed By: yungsters Differential Revision: D44055912 fbshipit-source-id: 37bcd7c12916b95a258e6b2e5717a642f478abdf --- .../DOM/OldStyleCollections/ArrayLikeUtils.js | 30 +++++++ .../DOM/OldStyleCollections/HTMLCollection.js | 82 +++++++++++++++++++ .../HTMLCollection.js.flow | 27 ++++++ .../__tests__/HTMLCollection-test.js | 80 ++++++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 packages/react-native/Libraries/DOM/OldStyleCollections/ArrayLikeUtils.js create mode 100644 packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js create mode 100644 packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js.flow create mode 100644 packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/HTMLCollection-test.js diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/ArrayLikeUtils.js b/packages/react-native/Libraries/DOM/OldStyleCollections/ArrayLikeUtils.js new file mode 100644 index 00000000000..a52bb77003d --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/ArrayLikeUtils.js @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/** + * This definition is different from the current built-in type `$ArrayLike` + * provided by Flow, in that this is an interface and that one is an object. + * + * The difference is important because, when using objects, Flow thinks + * a `length` property would be copied over when using the spread operator, + * which is incorrect. + */ +export interface ArrayLike extends Iterable { + // This property should've been read-only as well, but Flow doesn't handle + // read-only indexers correctly (thinks reads are writes and fails). + [indexer: number]: T; + +length: number; +} + +export function* createValueIterator(arrayLike: ArrayLike): Iterator { + for (let i = 0; i < arrayLike.length; i++) { + yield arrayLike[i]; + } +} diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js b/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js new file mode 100644 index 00000000000..18cbd0af9e5 --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js @@ -0,0 +1,82 @@ +/** + * 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 {createValueIterator} from './ArrayLikeUtils'; + +// IMPORTANT: The type definition for this module is defined in `HTMLCollection.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 interface. +export default class HTMLCollection implements Iterable, ArrayLike { + _length: number; + + /** + * Use `createHTMLCollection` 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) { + for (let i = 0; i < elements.length; i++) { + Object.defineProperty(this, i, { + value: elements[i], + enumerable: true, + configurable: false, + 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 = this; + return arrayLike[index]; + } + + /** + * @deprecated Unused in React Native. + */ + namedItem(name: string): T | null { + return null; + } + + // $FlowIssue[unsupported-syntax] Flow does not support computed properties in classes. + [Symbol.iterator](): Iterator { + return createValueIterator(this); + } +} + +/** + * This is an internal method to create instances of `HTMLCollection`, + * which avoids leaking its constructor to end users. + * We can do that because the external definition of `HTMLCollection` lives in + * `HTMLCollection.js.flow`, not here. + */ +export function createHTMLCollection( + elements: $ReadOnlyArray, +): HTMLCollection { + return new HTMLCollection(elements); +} diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js.flow b/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js.flow new file mode 100644 index 00000000000..8f14d1d098c --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/HTMLCollection.js.flow @@ -0,0 +1,27 @@ +/** + * 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 HTMLCollection<+T> + implements Iterable, ArrayLike +{ + // 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; + namedItem(name: string): T | null; + @@iterator(): Iterator; +} + +declare export function createHTMLCollection( + elements: $ReadOnlyArray, +): HTMLCollection; diff --git a/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/HTMLCollection-test.js b/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/HTMLCollection-test.js new file mode 100644 index 00000000000..8000e1551bf --- /dev/null +++ b/packages/react-native/Libraries/DOM/OldStyleCollections/__tests__/HTMLCollection-test.js @@ -0,0 +1,80 @@ +/** + * 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 {createHTMLCollection} from '../HTMLCollection'; + +describe('HTMLCollection', () => { + it('provides an array-like interface', () => { + const collection = createHTMLCollection(['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('is immutable (loose mode)', () => { + const collection = createHTMLCollection(['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 = createHTMLCollection(['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 = createHTMLCollection(['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 = createHTMLCollection(['a', 'b', 'c']); + + let i = 0; + for (const value of collection) { + expect(value).toBe(collection[i]); + i++; + } + }); + + describe('item()', () => { + it('returns elements at the specified position, or null', () => { + const collection = createHTMLCollection(['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); + }); + }); +});