Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/content/docs/api-surface.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,35 @@ let element = document->WebAPI.Document.createElement("div")
let node = element->WebAPI.Element.asNode
```

### Classifying DOM values

Event libraries and browser APIs sometimes return a type that is broader than the value present at
runtime. A React form event target, for example, does not tell ReScript whether the target is an
input, select, or another element. Calling an input-specific API then requires narrowing that value.

Use `classify` instead of repeating an `instanceof` binding and an unchecked `Obj.magic` cast. The
classifier performs the runtime check and returns the correctly typed value as an `option`.

```ReScript
let readInputValue = target =>
switch target->WebAPI.HTMLInputElement.classify {
| Some(input) => Some(input.value)
| None => None
}
```

The classifier parameter is polymorphic, so a library-owned target can be passed directly without an
identity cast to a WebAPI type first. `Element.classify`, `Document.classify`,
`HTMLElement.classify`, `HTMLInputElement.classify`, `HTMLImageElement.classify`, and
`HTMLCanvasElement.classify` follow the same contract. Their matching `isInstanceOf` functions are
available when only a boolean check is needed.

Classification uses the corresponding constructor from `globalThis`. It returns `None` rather than
raising when that constructor is unavailable in a server or worker environment. Like JavaScript
`instanceof`, it only recognizes values created in the current realm; values from another window or
iframe may not match. Enable the `WebAPI.HTML` feature for HTML-specific classifiers and
`WebAPI.Canvas` for `HTMLCanvasElement.classify`.

## Visual Viewport

`Window.visualViewport` returns a nullable `WebAPI.VisualViewport.t`.
Expand Down
17 changes: 16 additions & 1 deletion docs/content/docs/philosophy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,19 @@ let element: WebAPI.Element.t = document->WebAPI.Document.createElement("div")
let node: WebAPI.Node.t = element->WebAPI.Element.asNode
```

Any other conversions should be treated as unsafe casts and used with caution, because the type system cannot guarantee they are valid at runtime.
Converting in the other direction requires proving the runtime type. Broad DOM values commonly come
from event libraries, selector APIs, or JavaScript bindings that cannot describe the concrete
interface statically. Use the destination module's `classify` function for that downcast instead of
calling `Obj.magic` directly.

```ReScript
switch target->WebAPI.HTMLInputElement.classify {
| Some(input) => Some(input.value)
| None => None
}
```

Classifiers return an `option` because a runtime value may not implement the requested interface.
They also return `None` when the browser constructor is unavailable. The checked cast remains inside
the binding, so application code handles absence explicitly and receives the concrete public
interface type only after a successful check.
4 changes: 2 additions & 2 deletions docs/pages/examples.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const headings = testFiles.map(({ name }) => ({
The best example of usage of these bindings are the <a href="./06-testing"
>tests</a
>. <br />
These typically contain tweaks made to the generated bindings to make them more
ergonomic.
These exercise the public bindings directly. Reusable Web API bindings and conversion
helpers belong in the library rather than in individual examples.
</p>
{
testFiles.map(({ source, output, name }) => {
Expand Down
25 changes: 25 additions & 0 deletions src/canvas/CanvasRenderingContext2D.res
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
/**
[Read more on MDN](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/fillStyle)
*/
@get
external getFillStyle: DOM.canvasRenderingContext2D => CanvasTypes.fillStyle = "fillStyle"

/**
[Read more on MDN](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/fillStyle)
*/
@set
external setFillStyle: (DOM.canvasRenderingContext2D, CanvasTypes.fillStyle) => unit = "fillStyle"

/**
[Read more on MDN](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/font)
*/
@set
external setFont: (DOM.canvasRenderingContext2D, string) => unit = "font"

/**
[Read more on MDN](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/textBaseline)
*/
@set
external setTextBaseline: (DOM.canvasRenderingContext2D, CanvasTypes.canvasTextBaseline) => unit =
"textBaseline"

/**
[Read more on MDN](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/save)
*/
Expand Down
34 changes: 34 additions & 0 deletions src/canvas/HTMLCanvasElement.res
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,40 @@ type t = {

include HTMLElement.Impl({type t = t})

/**
`isInstanceOf(value)`

Returns whether `value` is an `HTMLCanvasElement` created in the current JavaScript realm.

This is a runtime check. It returns `false` when `globalThis.HTMLCanvasElement` is unavailable, such
as in some server or worker environments.
*/
let isInstanceOf = (_: 'value): bool =>
%raw(`typeof globalThis.HTMLCanvasElement === "function" && param instanceof globalThis.HTMLCanvasElement`)

/**
`classify(value)`

Safely narrows a value from a broad DOM element type to `HTMLCanvasElement.t`.

Use this for values returned by APIs such as `Document.getElementById`. Returns `Some(canvas)` for
an `HTMLCanvasElement` in the current realm, and `None` when the value is not a canvas or when the
`HTMLCanvasElement` constructor is unavailable.

```res
switch element->HTMLCanvasElement.classify {
| Some(canvas) => canvas->HTMLCanvasElement.getContext2D
| None => Null
}
```
*/
let classify = (value: 'value): option<t> =>
if value->isInstanceOf {
Some(Obj.magic(value))
} else {
None
}

/**
Returns an object that provides methods and properties for drawing and manipulating images and graphics on a canvas element in a document. A context object includes information about colors, line widths, fonts, and other graphic parameters that can be drawn on a canvas.
Creates a CanvasRenderingContext2D object representing a two-dimensional rendering context.
Expand Down
34 changes: 33 additions & 1 deletion src/dom-nodes/Element.res
Original file line number Diff line number Diff line change
Expand Up @@ -506,4 +506,36 @@ Returns true if qualifiedName is now present, and false otherwise.

include Impl({type t = t})

let isInstanceOf = (_: 't): bool => %raw(`param instanceof Element`)
/**
`isInstanceOf(value)`

Returns whether `value` is an `Element` created in the current JavaScript realm.

This is a runtime check. It returns `false` when `globalThis.Element` is unavailable, such as in
some server or worker environments.
*/
let isInstanceOf = (_: 'value): bool =>
%raw(`typeof globalThis.Element === "function" && param instanceof globalThis.Element`)

/**
`classify(value)`

Safely narrows a value from a broad DOM or library type to `Element.t`.

Use this when an event target, JavaScript binding, or union-like API cannot express its concrete DOM
type statically. Returns `Some(element)` when the value is an `Element` in the current realm, and
`None` when it is not or when the `Element` constructor is unavailable.

```res
switch value->Element.classify {
| Some(element) => element->Element.hasAttribute("data-ready")
| None => false
}
```
*/
let classify = (value: 'value): option<t> =>
if value->isInstanceOf {
Some(Obj.magic(value))
} else {
None
}
34 changes: 34 additions & 0 deletions src/html/HTMLElement.res
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,37 @@ rather than relying on coercion.
}

include Impl({type t = t})

/**
`isInstanceOf(value)`

Returns whether `value` is an `HTMLElement` created in the current JavaScript realm.

This is a runtime check. It returns `false` when `globalThis.HTMLElement` is unavailable, such as in
some server or worker environments.
*/
let isInstanceOf = (_: 'value): bool =>
%raw(`typeof globalThis.HTMLElement === "function" && param instanceof globalThis.HTMLElement`)

/**
`classify(value)`

Safely narrows a value from a broad DOM or library type to `HTMLElement.t`.

Use this when an event target or element-returning API does not distinguish HTML elements from SVG
or other element kinds. Returns `Some(element)` for an `HTMLElement` in the current realm, and
`None` when the value is not an HTML element or when the `HTMLElement` constructor is unavailable.

```res
switch value->HTMLElement.classify {
| Some(element) => element->HTMLElement.focus
| None => ()
}
```
*/
let classify = (value: 'value): option<t> =>
if value->isInstanceOf {
Some(Obj.magic(value))
} else {
None
}
34 changes: 34 additions & 0 deletions src/html/HTMLImageElement.res
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,40 @@ type t = {

include HTMLElement.Impl({type t = t})

/**
`isInstanceOf(value)`

Returns whether `value` is an `HTMLImageElement` created in the current JavaScript realm.

This is a runtime check. It returns `false` when `globalThis.HTMLImageElement` is unavailable, such
as in some server or worker environments.
*/
let isInstanceOf = (_: 'value): bool =>
%raw(`typeof globalThis.HTMLImageElement === "function" && param instanceof globalThis.HTMLImageElement`)

/**
`classify(value)`

Safely narrows a value from a broad DOM element type to `HTMLImageElement.t`.

Use this for values returned by element-creating or selector APIs. Returns `Some(image)` for an
`HTMLImageElement` in the current realm, and `None` when the value is not an image or when the
`HTMLImageElement` constructor is unavailable.

```res
switch element->HTMLImageElement.classify {
| Some(image) => Some(image.src)
| None => None
}
```
*/
let classify = (value: 'value): option<t> =>
if value->isInstanceOf {
Some(Obj.magic(value))
} else {
None
}

/**
[Read more on MDN](https://developer.mozilla.org/docs/Web/API/HTMLImageElement/decode)
*/
Expand Down
34 changes: 34 additions & 0 deletions src/html/HTMLInputElement.res
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,40 @@ type t = {

include HTMLElement.Impl({type t = t})

/**
`isInstanceOf(value)`

Returns whether `value` is an `HTMLInputElement` created in the current JavaScript realm.

This is a runtime check. It returns `false` when `globalThis.HTMLInputElement` is unavailable, such
as in some server or worker environments.
*/
let isInstanceOf = (_: 'value): bool =>
%raw(`typeof globalThis.HTMLInputElement === "function" && param instanceof globalThis.HTMLInputElement`)

/**
`classify(value)`

Safely narrows a value from a broad event target or element type to `HTMLInputElement.t`.

Use this for values such as React form event targets, which are typed more broadly than the input
element that emitted the event. Returns `Some(input)` for an `HTMLInputElement` in the current realm,
and `None` when the value is not an input or when the `HTMLInputElement` constructor is unavailable.

```res
switch target->HTMLInputElement.classify {
| Some(input) => Some(input.value)
| None => None
}
```
*/
let classify = (value: 'value): option<t> =>
if value->isInstanceOf {
Some(Obj.magic(value))
} else {
None
}

/**
Increments a range input control's value by the value given by the Step attribute. If the optional parameter is used, will increment the input control's value by that value.
@param n Value to increment the value by.
Expand Down
31 changes: 30 additions & 1 deletion src/window/Document.res
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,36 @@ external hasStorageAccess: DOM.document => promise<bool> = "hasStorageAccess"
@send
external requestStorageAccess: DOM.document => promise<unit> = "requestStorageAccess"

let isInstanceOf = (_: 't): bool => %raw(`param instanceof Document`)
/**
`isInstanceOf(value)`

Returns whether `value` is a `Document` created in the current JavaScript realm.

This is a runtime check. It returns `false` when `globalThis.Document` is unavailable, such as in
some server or worker environments.
*/
let isInstanceOf = (_: 'value): bool =>
%raw(`typeof globalThis.Document === "function" && param instanceof globalThis.Document`)

/**
`classify(value)`

Safely narrows a value from a broad DOM or library type to `DOM.document`.

Use this when a JavaScript binding or union-like API can contain a document but cannot express that
concrete type statically. Returns `Some(document)` when the value is a `Document` in the current
realm, and `None` when it is not or when the `Document` constructor is unavailable.

```res
let document = value->Document.classify
```
*/
let classify = (value: 'value): option<DOM.document> =>
if value->isInstanceOf {
Some(Obj.magic(value))
} else {
None
}

/**
Returns the Location associated with this document, which provides information about the current URL and methods for navigating to another URL.
Expand Down
46 changes: 46 additions & 0 deletions tests/DOMAPI/Classify__test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
%%raw(`
globalThis.Element = class Element {}
globalThis.Document = class Document {}
globalThis.HTMLElement = class HTMLElement extends globalThis.Element {}
globalThis.HTMLCanvasElement = class HTMLCanvasElement extends globalThis.HTMLElement {}
globalThis.HTMLImageElement = class HTMLImageElement extends globalThis.HTMLElement {}
globalThis.HTMLInputElement = class HTMLInputElement extends globalThis.HTMLElement {
checkValidity() {
return true
}
}
`)

let element: unknown = %raw(`new globalThis.Element()`)
let document: unknown = %raw(`new globalThis.Document()`)
let htmlElement: unknown = %raw(`new globalThis.HTMLElement()`)
let canvas: unknown = %raw(`new globalThis.HTMLCanvasElement()`)
let image: unknown = %raw(`new globalThis.HTMLImageElement()`)
let input: unknown = %raw(`new globalThis.HTMLInputElement()`)

assert(element->Element.classify->Option.isSome)
assert(document->Document.classify->Option.isSome)
assert(htmlElement->HTMLElement.classify->Option.isSome)
assert(canvas->HTMLCanvasElement.classify->Option.isSome)
assert(image->HTMLImageElement.classify->Option.isSome)
assert(input->HTMLInputElement.classify->Option.isSome)

assert(input->Element.classify->Option.isSome)
assert(input->HTMLElement.classify->Option.isSome)
assert(document->Element.classify->Option.isNone)
assert(image->HTMLCanvasElement.classify->Option.isNone)
assert(htmlElement->HTMLInputElement.classify->Option.isNone)

input
->HTMLInputElement.classify
->Option.forEach(input => assert(input->HTMLInputElement.checkValidity))

%%raw(`
delete globalThis.HTMLCanvasElement
delete globalThis.HTMLImageElement
delete globalThis.HTMLInputElement
`)

assert(canvas->HTMLCanvasElement.classify->Option.isNone)
assert(image->HTMLImageElement.classify->Option.isNone)
assert(input->HTMLInputElement.classify->Option.isNone)
Loading
Loading