Skip to content

Commit e7f2d29

Browse files
Update FormControlRange explainer to increase usability (#1131)
This PR updates the `FormControlRange` explainer to clarify encapsulation and improve usability: - Clarifies `FormControlRange` as an `AbstractRange` over form-control value space with live behavior - Makes previously hidden methods and properties from `FormControlRange `to be accessible for devs: - `startContainer`, `endContainer`: returns the host `<input>/<textarea>` - `startOffset`, `endOffset`: indexes into `element.value` (same units as `selectionStart/selectionEnd`), updated as the text changes. - Replaces the earlier idea of hiding endpoints or returning` null`/`-1` Minor wording cleanups were also made for clarity.
1 parent 5bb66f8 commit e7f2d29

1 file changed

Lines changed: 37 additions & 33 deletions

File tree

FormControlRange/explainer.md

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515

1616
## Introduction
1717

18-
The current `Range` interface methods do not support retrieving or creating Range objects that represent the `value` (rather than the element itself) of `<textarea>` and `<input>` elements. As a result, if web developers want to use the `getBoundingClientRect()` method in a `<textarea>` or `<input>` element to position a popup beneath the user's current caret for delivering contextual autocomplete suggestions or marking syntax errors as users type using the [Custom Highlight API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API), they must find workarounds. These workarounds often involve cloning these elements and their styles into `<div>`s, which is both difficult to maintain and may impact the web application's performance.
18+
The current `Range` interface methods do not support retrieving or creating ranges that represent the `value` (rather than the element itself) of `<textarea>` and `<input>` elements. As a result, if web developers want to use the `getBoundingClientRect()` method in a `<textarea>` or `<input>` element to position a popup beneath the user's current caret for delivering contextual autocomplete suggestions or marking syntax errors as users type using the [Custom Highlight API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API), they must find workarounds. These workarounds often involve cloning these elements and their styles into `<div>`s, which is both difficult to maintain and may impact the web application's performance.
1919

20-
This proposal aims to address these issues by introducing `FormControlRange`, a new type of `Range` object that extends `AbstractRange` and serves as a way to reference spans of text within form control elements.
20+
This proposal aims to address these issues by introducing `FormControlRange`, a new `AbstractRange` subclass that serves as a way to reference spans of text within form control elements while preserving encapsulation.
2121

2222
## User-Facing Problem
2323

@@ -29,7 +29,7 @@ This proposal aims to address these issues by introducing `FormControlRange`, a
2929

3030
![highlight-api-example](highlight-api-example.jpg)
3131

32-
Today, web developers have two options to implement these uses cases:
32+
Today, web developers have two options to implement these use cases:
3333

3434
### Option 1: Cloning the form control element and copying styles into a `<div>`
3535

@@ -109,7 +109,7 @@ function highlightSyntax(start_index, end_index) {
109109
// Copy styles
110110
copyStyles();
111111
// Set content
112-
measuringDIv.textContent = text;
112+
measuringDiv.textContent = text;
113113

114114
// Create range
115115
const range = document.createRange();
@@ -156,7 +156,7 @@ copyStyles();
156156
// Initial word setup for highlight
157157
var previousWord = '';
158158
// Dictionary for syntax check
159-
const dictionary = Set();
159+
const dictionary = new Set();
160160
// Create highlight object
161161
const highlight = new Highlight();
162162
// Handle window resize
@@ -170,7 +170,7 @@ Using a `<div contenteditable>` for direct text handling can be challenging. Web
170170
This is roughly the sample code from the example above, some functionality is omitted for brevity:
171171

172172
```html
173-
<form id="messageForm" onsubmit="returnvalidateAndSubmit(event)">
173+
<form id="messageForm" onsubmit="return validateAndSubmit(event)">
174174
<!-- Hidden input for form validation -->
175175
<input type="hidden" id="hiddenContent" name="message" required>
176176
<div contenteditable="true" id="nameField">Type your message here. Use @ to mention users.</div>
@@ -224,7 +224,7 @@ nameField.addEventListener('input', (e) => {
224224

225225
### Goal
226226

227-
Provide a way of retrieving a `FormControlRange`—a specialized type of `Range` object—that represents part or all of the `value` of `<textarea>` and `<input>` elements, enabling range-based operations such as getting bounding rects and setting custom highlights, while limiting access to the standard Range API to enforce encapsulation.
227+
Provide a way to obtain a `FormControlRange`—a specialized, live range for `<textarea>` and `<input>` values implemented as an `AbstractRange` subclass—that enables range-based operations (e.g. getting bounding rects, setting custom highlights, etc.) while restricting standard `Range` mutations to preserve encapsulation of these form controls.
228228

229229
### Non-goals
230230

@@ -234,21 +234,36 @@ Provide a way of retrieving a `FormControlRange`—a specialized type of `Range`
234234

235235
## Proposed Approach
236236

237-
The `FormControlRange` interface extends `AbstractRange` and provides a controlled way to create limited `Range` objects for the entirety or a part of the `value` of `<textarea>` and `<input>` elements. To protect the inner workings of these elements, `FormControlRange` instances have several limitations on the methods and attributes (from the Range API) that can be accessed through JavaScript.
237+
The `FormControlRange` interface extends `AbstractRange` and provides a controlled way to reference parts of the `value` of `<textarea>` and `<input>` elements. It exposes useful endpoint properties while limiting certain mutation methods to preserve encapsulation.
238238

239239
Unlike `StaticRange`, `FormControlRange` is **live** — it tracks changes to the underlying text in the `<textarea>` or `<input>` element and automatically updates its start and end positions, similar to how a regular `Range` tracks DOM mutations. This ensures that operations like `getBoundingClientRect()` or `toString()` always reflect the element’s current content, even after edits.
240240

241241
This live-update behavior also aligns conceptually with the `InputRange()` from [Keith Cirkel’s Richer Text Fields proposal](https://open-ui.org/components/richer-text-fields.explainer/), which takes a broader approach to enabling richer interactions in form controls. While that proposal covers more editing capabilities, `FormControlRange` focuses on a limited, encapsulated `AbstractRange`-based API, but both aim to support dynamic interaction with text as it changes.
242242

243-
**Initially available methods and properties:**
243+
### Properties and Methods
244+
245+
#### Properties
246+
FormControlRange exposes useful endpoint information while maintaining encapsulation:
247+
- `startContainer` and `endContainer`: Return the host form control element (`<input>` or `<textarea>`). No internal/shadow nodes are exposed.
248+
- `startOffset` and `endOffset`: Return indices into the form control's `value`. These values match those passed to `setFormControlRange()` and are automatically updated as text changes.
249+
- `collapsed`: Returns whether `startOffset` equals `endOffset`.
250+
251+
#### Available Methods
244252
- `getBoundingClientRect()`: Returns the bounding rectangle of the range
245-
- `getClientRects()`: Returns a list of rectangles for the range
253+
- `getClientRects()`: Returns a list of rectangles for the range
246254
- `toString()`: Returns the text content of the range
247-
- `collapsed`: Returns whether the range is collapsed (inherited from AbstractRange)
248-
249-
**Initially unavailable methods and properties (to prevent exposure and mutation of inner browser implementation details):**
250-
- `setStart()` and `setEnd()`
251-
- `startContainer`, `startOffset`, `endOffset`, and `endContainer`
255+
- `setFormControlRange(element, startOffset, endOffset)`: Sets endpoints directly in value space
256+
- `element`: Must be a supported text control (`<input>` or `<textarea>`)
257+
- `startOffset`: Index into element's value where range should start (`0 ≤ startOffset ≤ endOffset`)
258+
- `endOffset`: Index into element's value where range should end (`endOffset ≤ element.value.length`)
259+
- Throws `IndexSizeError` if bounds are invalid
260+
- Throws `NotSupportedError` if `element` is not a supported text control type
261+
262+
#### Unavailable Methods
263+
The following methods are not available to avoid exposing or mutating inner browser implementation details:
264+
- `setStart()`, `setEnd()` (use `setFormControlRange()` instead)
265+
- `setStartBefore()`, `setStartAfter()`, `setEndBefore()`, `setEndAfter()`
266+
- `selectNode()`, `selectNodeContents()`
252267
- `surroundContents()`
253268
- `extractContents()`
254269
- `deleteContents()`
@@ -274,13 +289,6 @@ The instance can be then set using the following method:
274289
formRange.setFormControlRange(formControl, startOffset, endOffset);
275290
```
276291

277-
The `setFormControlRange()` method accepts three parameters:
278-
- `element`: The `<textarea>` or `<input>` element
279-
- `startOffset`: The start position within the element's value
280-
- `endOffset`: The end position within the element's value
281-
282-
If either `startOffset` or `endOffset` are out of bounds, it will throw a `RangeError` exception. If the `value` of the `<textarea>` or `<input>` element is empty, the `FormControlRange` will represent an empty range.
283-
284292
The following sample code showcases how the new `FormControlRange` interface would solve the main use cases laid out in the [User-Facing Problem](#user-facing-problem) section.
285293

286294
```html
@@ -341,7 +349,7 @@ As we want the `FormControlRange` interface to be aligned with the current selec
341349
- URL
342350
- Password
343351

344-
Following this same alignment with selection APIs, the parameters `startOffset` and `endOffset` of the `setFormControlRange()` method will be based on caret position, rather than character index, just as [`selectionStart`](https://html.spec.whatwg.org/#the-textarea-element:dom-textarea/input-selectionstart) and [`selectionEnd`](https://html.spec.whatwg.org/#the-textarea-element:dom-textarea/input-selectionend) are.
352+
Following this same alignment with selection APIs, `startOffset` and `endOffset` are indices into `element.value`, matching the units used by [`selectionStart`](https://html.spec.whatwg.org/#the-textarea-element:dom-textarea/input-selectionstart) and [`selectionEnd`](https://html.spec.whatwg.org/#the-textarea-element:dom-textarea/input-selectionend).
345353

346354
Sample code for `<input type="text">`:
347355

@@ -426,15 +434,11 @@ const textarea = document.querySelector("#messageArea");
426434
const wordRange = new FormControlRange();
427435
wordRange.setFormControlRange(textarea, 0, 5);
428436

429-
// Apply highlight to that live range.
437+
// Bind a highlight to that live range.
430438
const highlight = new Highlight(wordRange);
431439
CSS.highlights.set("tracked-word", highlight);
432440

433-
// Reapply the same highlight object after each edit.
434-
// The live range automatically adjusts as text changes — no offset recalculation needed.
435-
textarea.addEventListener("input", () => {
436-
CSS.highlights.set("tracked-word", highlight);
437-
});
441+
// No input-handler needed: as the control’s value changes, the range stays in sync and the highlight repaints automatically.
438442
```
439443
If text is inserted before `"hello"`, the highlight automatically shifts so it still covers the same word.
440444

@@ -518,20 +522,20 @@ This `setFormControlRange()` method would set a new flag `IsFormControl()` in th
518522

519523
```javascript
520524
range.startContainer // Returns startNode
521-
formRange.startContainer // Flag is on: Throws exception
525+
formRange.startContainer // Returns the host <textarea>/<input> element
522526
```
523527

524-
This approach addresses encapsulation concerns for ranges in form controls. It also offers better compatibility than the `FormControlRange` interface, since it would work seamlessly with APIs that involve `Range` (not only `AbstractRange`) objects, such as the Selection API:
528+
This approach addresses encapsulation concerns for ranges in form controls. It also offers better compatibility than the `FormControlRange` interface, since it remains a `Range` and is compatible at the type level with APIs that involve `Range` (not only `AbstractRange`) objects, such as the Selection API. However, Selection behavior for value-space ranges is not defined today and would require additional spec work.
525529

526530
```javascript
527531
// Using this approach
528-
selection.addRange(formRange) // Works as intended.
532+
selection.addRange(formRange) // Type-checks; actual behavior would need explicit spec rules
529533

530534
// Using FormControlRange interface
531535
selection.addRange(formRange2) // Throws exception: "parameter 1 is not of type Range."
532536
```
533537

534-
Ultimately, the `FormControlRange` interface was chosen instead. While the `setFormControlRange()` approach offered better API compatibility, it did not provide an explicit distinction between regular and form control ranges—both would be of type `Range`, potentially confusing web authors about why some methods may or may not be available depending on how the `Range` was set (using `setStart()` and `setEnd()` or `setFormControlRange()`).
538+
Ultimately, the `FormControlRange` interface was chosen instead. While the `setFormControlRange()` approach offered better type-level compatibility, it did not provide an explicit distinction between regular and form control ranges—both would be of type `Range`, potentially confusing web authors about why some methods may or may not be available depending on how the `Range` was set (using `setStart()` and `setEnd()` or `setFormControlRange()`).
535539

536540
## Other Considerations
537541

0 commit comments

Comments
 (0)