Skip to content

Commit

Permalink
Fix #94
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-kinokon committed Nov 14, 2023
1 parent 03d3458 commit 3c0e7b5
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 8 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/triple-slash-reference": "off",
"@typescript-eslint/no-empty-interface": "off",
"arrow-body-style": ["error", "as-needed"],
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ node_modules
index.js
build/*
esm
cjs
cjs
drafts
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 8.1.0
- Fixes #97: support `disabled` on `<link>` element.
- Fixes #94: supports `forwardRef` and `useImperativeHandle`.
- Bumped TypeScript definition sync with `@types/react` at #e05c7e9.

# 8.0.5
- Added support for using `DOMTokenList` (e.g. `element.classList`) for `className`.
- Renamed `ClassList` type declaration to `BasicClassList` to not confuse with the browser’s class list type.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,11 @@ The following functions are included for compatibility with React API:

```ts
function createFactory(component: string): (props: object) => JSX.Element
function useImperativeHandle<T>(ref: Ref<T>, init: () => T, deps?: DependencyList): void
function useRef<T>(initialValue?: T): RefObject<T>
function forwardRef<T = Node, P = {}>(
render: (props: P, ref: Ref<T>) => ReactNode
): FunctionComponent<P & { ref?: Ref<T> }>
```

The following functions do **not** have memoization or optimization, and are only useful if you are
Expand Down
10 changes: 9 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
export { createRef, isRef } from "./ref"
export { useClassList, useText } from "./hooks"
export { memo, StrictMode, useCallback, useMemo, useRef } from "./react-compat-api"
export {
forwardRef,
memo,
StrictMode,
useCallback,
useImperativeHandle,
useMemo,
useRef,
} from "./react-compat-api"

import { Component, Fragment, createElement } from "./jsx-dom"
import { ShadowRoot } from "./shadow"
Expand Down
6 changes: 3 additions & 3 deletions src/jsx-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ export function createElement(tag: any, attr: any, ...children: any[]) {
return jsx(tag, { ...attr, children }, attr.key)
}

function attachRef(ref: any | undefined, node: Node) {
if (isRef<Node>(ref)) {
export function attachRef<T = Node>(ref: any | undefined, node: T) {
if (isRef<T>(ref)) {
ref.current = node
} else if (isFunction(ref)) {
ref(node)
Expand Down Expand Up @@ -251,7 +251,7 @@ function style(node: Element & HTMLOrSVGElement, value?: any) {
node.setAttribute("style", value)
} else if (isObject(value)) {
forEach(value, (val, key) => {
if (key.indexOf('-') === 0) {
if (key.indexOf("-") === 0) {
// CSS custom properties (variables) start with `-` (e.g. `--my-variable`)
// and must be assigned via `setProperty`.
cast<HTMLElement>(node).style.setProperty(key, val)
Expand Down
14 changes: 14 additions & 0 deletions src/react-compat-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import type { FunctionComponent, ReactElement, Ref } from "../types"
import { attachRef } from "./jsx-dom"
import { createRef } from "./ref"

export { createRef as useRef } from "./ref"
export { identity as useCallback, identity as memo } from "./util"
export { Fragment as StrictMode } from "./jsx-dom"

export function useMemo<T>(factory: () => T): T {
return factory()
}

export function forwardRef<T extends Node, P = {}>(
render: (props: P, ref: Ref<T>) => ReactElement
): FunctionComponent<P & { ref?: Ref<T> }> {
return ({ ref, ...props }) => render(props as P, ref ?? createRef<T>())
}

export function useImperativeHandle<T>(ref: Ref<T>, init: () => T, _deps: unknown) {
attachRef(ref, init())
}
4 changes: 2 additions & 2 deletions src/ref.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { isObject } from "./util"

interface Ref<T extends Node> {
interface Ref<T = Node> {
current: null | T
}

export function createRef<T extends Node = Node>(): Ref<T> {
return Object.seal({ current: null })
}

export function isRef<T extends Node>(maybeRef: any): maybeRef is Ref<T> {
export function isRef<T = Node>(maybeRef: any): maybeRef is Ref<T> {
return isObject(maybeRef) && "current" in maybeRef
}
56 changes: 56 additions & 0 deletions test/test-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,62 @@ describe("jsx-dom", () => {
expect(button).to.be.instanceOf(HTMLButtonElement)
})

describe("supports forwardRef", () => {
it("element", () => {
const Container = React.forwardRef<HTMLButtonElement, any>((props, ref) => (
<div {...props}>
<button ref={ref}>Button</button>
</div>
))

const ref = lib.createRef()
const node = (
<Container className="container" ref={ref}>
Click me!
</Container>
)
expect(node.className).to.equal("container")
expect(ref.current).to.be.instanceOf(HTMLButtonElement)
})

it("component", () => {
const Button = props => <button {...props} />
const Container = React.forwardRef<HTMLButtonElement, any>((props, ref) => (
<div {...props}>
<Button ref={ref}>Button</Button>
</div>
))

const ref = lib.createRef()
const node = (
<Container className="container" ref={ref}>
Click me!
</Container>
)
expect(node.className).to.equal("container")
expect(ref.current).to.be.instanceOf(HTMLButtonElement)
})
})

it("supports useImperativeHandle", () => {
const Button = React.forwardRef<{ focus: () => string }, any>((props, ref) => {
React.useImperativeHandle(ref, () => ({
focus: () => "ping",
}))
return <button {...props} />
})

const ref = lib.createRef()
const node = (
<Button className="container" ref={ref}>
Click me!
</Button>
)
expect(node.className).to.equal("container")
expect(ref.current).to.have.property("focus")
expect(ref.current.focus()).to.equal("ping")
})

it("supports defaultProps in functional components", () => {
const Button = (props: any) => <div className={props.className} />
Button.defaultProps = { className: "defaultClass" }
Expand Down
11 changes: 10 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ interface AttrWithRef<T> extends Attributes {
ref?: Ref<T> | undefined
}

type ReactElement = HTMLElement | SVGElement
export type ReactElement = HTMLElement | SVGElement

type DOMFactory<P extends DOMAttributes<T>, T extends Element> = (
props?: (AttrWithRef<T> & P) | null,
Expand Down Expand Up @@ -255,6 +255,13 @@ export interface MutableRefObject<T> {

export function createRef<T = any>(): RefObject<T>

/**
* React compatibility-only API.
*/
export function forwardRef<T = Node, P = {}>(
render: (props: P, ref: Ref<T>) => ReactNode
): FunctionComponent<P & { ref?: Ref<T> }>

/**
* `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
* (`initialValue`). The returned object will persist for the full lifetime of the component.
Expand Down Expand Up @@ -297,6 +304,8 @@ export function useRef<T>(initialValue: T | null): RefObject<T>
*/
export function useRef<T = unknown>(): MutableRefObject<T | undefined>

export function useImperativeHandle<T>(ref: Ref<T>, init: () => T, deps?: DependencyList): void

// I made 'inputs' required here and in useMemo as there's no point to memoizing without the memoization key
// useCallback(X) is identical to just using X, useMemo(() => Y) is identical to just using Y.
/**
Expand Down

0 comments on commit 3c0e7b5

Please sign in to comment.