Skip to content

Commit

Permalink
Fixed tests
Browse files Browse the repository at this point in the history
  • Loading branch information
smikhalevski committed Nov 10, 2023
1 parent a3fdb5f commit cfa2bbe
Show file tree
Hide file tree
Showing 39 changed files with 1,121 additions and 1,194 deletions.
48 changes: 24 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const planetsField = universeField.at('planets');
// ⮕ Field<Planet[] | undefined>
```

`planetsField` is a derived field, and it is linked to its parent `universeField`.
`planetsField` is a child field, and it is linked to its parent `universeField`.

```ts
planetsField.key;
Expand All @@ -126,18 +126,18 @@ universeField.at('planets');
// ⮕ planetsField
```

So most of the time you don't need to store a derived field in a variable if you already have a reference to a parent
So most of the time you don't need to store a child field in a variable if you already have a reference to a parent
field.

The derived field has all the same functionality as its parent, so you can derive a new field from it as well:
The child field has all the same functionality as its parent, so you can derive a new field from it as well:

```ts
planetsField.at(0).at('name');
// ⮕ Field<string>
```

When a value is set to a derived field, a parent field value is also updated. If parent field doesn't have a value yet,
Roqueform would infer its type from on the derived field key.
When a value is set to a child field, a parent field value is also updated. If parent field doesn't have a value yet,
Roqueform would infer its type from on the child field key.

```ts
universeField.value;
Expand All @@ -152,7 +152,7 @@ universeField.value;
By default, for a string key a parent object is created, and for a number key a parent array is created. You can change
this behaviour with [custom accessors](#accessors).

When a value is set to a parent field, derived fields are also updated:
When a value is set to a parent field, child fields are also updated:

```ts
const nameField = universeField.at('planets').at(0).at('name');
Expand All @@ -171,19 +171,19 @@ nameField.value;
You can subscribe a subscriber to a field updates. The returned callback would unsubscribe the subscriber.

```ts
const unsubscribe = planetsField.subscribe((updatedField, currentField) => {
const unsubscribe = planetsField.on('*', (updatedField, currentField) => {
// Handle the update
});
// ⮕ () => void
```

Listeners are called with two arguments:
Subscribers are called with two arguments:

<dl>
<dt><code>updatedField</code></dt>
<dd>

The field that initiated the update. In this example it can be `planetsField` itself, any of its derived fields, or any
The field that initiated the update. In this example it can be `planetsField` itself, any of its child fields, or any
of its ancestor fields.

</dd>
Expand All @@ -195,15 +195,15 @@ The field to which the subscriber is subscribed. In this example it is `planetsF
</dd>
</dl>

Listeners are called when a field value is changed or [when a plugin mutates the field object](#authoring-a-plugin).
The root field and all derived fields are updated before subscribers are called, so it's safe to read field values in a
Subscribers are called when a field value is changed or [when a plugin mutates the field object](#authoring-a-plugin).
The root field and all child fields are updated before subscribers are called, so it's safe to read field values in a
subscriber.

Fields use [SameValueZero](https://262.ecma-international.org/7.0/#sec-samevaluezero) comparison to detect that the
value has changed.

```ts
planetsField.at(0).at('name').subscribe(subscriber);
planetsField.at(0).at('name').on('*', subscriber);

// ✅ The subscriber is called
planetsField.at(0).at('name').setValue('Mercury');
Expand All @@ -215,10 +215,10 @@ planetsField.at(0).setValue({ name: 'Mercury' });
# Transient updates

When you call [`setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#setValue) on a field
then subscribers of its ancestors and its updated derived fields are triggered. To manually control the update propagation
then subscribers of its ancestors and its updated child fields are triggered. To manually control the update propagation
to fields ancestors, you can use transient updates.

When a value of a derived field is set transiently, values of its ancestors _aren't_ immediately updated.
When a value of a child field is set transiently, values of its ancestors _aren't_ immediately updated.

```ts
const avatarField = createField();
Expand All @@ -240,7 +240,7 @@ avatarField.at('eyeColor').isTransient;
// ⮕ true
```

To propagate the transient value contained by the derived field to its parent, use
To propagate the transient value contained by the child field to its parent, use
the [`dispatch`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#dispatch) method:

```ts
Expand All @@ -253,7 +253,7 @@ avatarField.value;
`setTransientValue` can be called multiple times, but only the most recent update is propagated to the parent field
after the `dispatch` call.

When a derived field is in a transient state, its value as observed from the parent may differ from the actual value:
When a child field is in a transient state, its value as observed from the parent may differ from the actual value:

```ts
const planetsField = createField(['Mars', 'Pluto']);
Expand Down Expand Up @@ -282,20 +282,20 @@ planetsField.at(1).value;

# Accessors

[`Accessor`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html) creates, reads and updates
[`ValueAccessor`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html) creates, reads and updates
field values.

- When the new field is derived via
- When the new field is child via
[`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#at) method, the field value is
read from the value of the parent field using the
[`Accessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method.
[`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method.

- When a field value is updated via
[`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#setValue), then the parent
field value is updated with the value returned from the
[`Accessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the
updated field has derived fields, their values are updated with values returned from the
[`Accessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method.
[`ValueAccessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the
updated field has child fields, their values are updated with values returned from the
[`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method.

You can explicitly provide a custom accessor along with the initial value. Be default, Roqueform uses
[`naturalAccessor`](https://smikhalevski.github.io/roqueform/variables/roqueform.naturalAccessor.html):
Expand Down Expand Up @@ -375,7 +375,7 @@ planetField.element;
// ⮕ null
```

The plugin would be called for each derived field when it is first accessed:
The plugin would be called for each child field when it is first accessed:

```ts
planetField.at('name').element
Expand Down Expand Up @@ -413,7 +413,7 @@ Now when `setElement` is called on a field, its subscribers would be invoked.
```ts
const planetField = createField({ name: 'Mars' }, elementPlugin);

planetField.at('name').subscribe((updatedField, currentField) => {
planetField.at('name').on('*', (updatedField, currentField) => {
// Handle the update
currentField.element;
// ⮕ document.body
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { dispatchEvents, Event as Event_, Field, PluginInjector, PluginOf, Subscriber, Unsubscribe } from 'roqueform';
import { dispatchEvents, Event, Field, PluginInjector, PluginOf, Subscriber, Unsubscribe } from 'roqueform';

const EVENT_CHANGE_ERROR = 'change:error';

Expand All @@ -7,7 +7,7 @@ const EVENT_CHANGE_ERROR = 'change:error';
*/
export interface ConstraintValidationPlugin {
/**
* An error associated with the field, or `null` if there's no error.
* A non-empty error message associated with the field, or `null` if there's no error.
*/
error: string | null;

Expand All @@ -17,7 +17,7 @@ export interface ConstraintValidationPlugin {
element: Element | null;

/**
* `true` if the field or any of its derived fields have {@link error an associated error}, or `false` otherwise.
* `true` if the field or any of its child fields have {@link error an associated error}, or `false` otherwise.
*/
readonly isInvalid: boolean;

Expand All @@ -37,7 +37,7 @@ export interface ConstraintValidationPlugin {
/**
* The origin of the associated error:
* - 0 if there's no associated error.
* - 1 if an error was set using Constraint Validation API;
* - 1 if an error was set by Constraint Validation API;
* - 2 if an error was set using {@link ValidationPlugin.setError};
*
* @protected
Expand All @@ -52,24 +52,24 @@ export interface ConstraintValidationPlugin {
/**
* Associates an error with the field.
*
* @param error The error to set. If `null` or `undefined` then an error is deleted.
* @param error The error to set. If `null`, `undefined`, or an empty string then an error is deleted.
*/
setError(error: string | null | undefined): void;

/**
* Deletes an error associated with this field. If this field has {@link element an associated element} that supports
* [Constraint Validation API](https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation) and the
* element has non-empty `validationMessage` then this message would be immediately set as an error for this field.
* element has a non-empty `validationMessage` then this message would be immediately set as an error for this field.
*/
deleteError(): void;

/**
* Recursively deletes errors associated with this field and all of its derived fields.
* Recursively deletes errors associated with this field and all of its child fields.
*/
clearErrors(): void;

/**
* Shows error message balloon for the first element that is associated with this field or any of its derived fields,
* Shows error message balloon for the first element that is associated with this field or any of its child fields,
* that has an associated error via calling
* {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidity reportValidity}.
*
Expand Down Expand Up @@ -104,14 +104,17 @@ export interface ConstraintValidationPlugin {
*/
export function constraintValidationPlugin(): PluginInjector<ConstraintValidationPlugin> {
return field => {
field.error = field.element = field.validity = null;
field.error = null;
field.element = null;
field.validity = null;
field.errorCount = 0;
field.errorOrigin = 0;

Object.defineProperties(field, {
isInvalid: { get: () => field.errorCount !== 0 },
isInvalid: { configurable: true, get: () => field.errorCount !== 0 },
});

const changeListener = (event: Event): void => {
const changeListener: EventListener = event => {
if (field.element === event.target && isValidatable(field.element)) {
dispatchEvents(setError(field, field.element.validationMessage, 1, []));
}
Expand All @@ -132,30 +135,30 @@ export function constraintValidationPlugin(): PluginInjector<ConstraintValidatio
field.element.removeEventListener('change', changeListener);
field.element.removeEventListener('invalid', changeListener);

field.element = field.error = null;
field.element = null;
}

field.element = element;
field.validity = null;

const events: Event_[] = [];
const events: Event[] = [];

if (isValidatable(element)) {
// Connect new element
element.addEventListener('input', changeListener);
element.addEventListener('change', changeListener);
element.addEventListener('invalid', changeListener);

setError(field, element.validationMessage, 1, events);

field.validity = element.validity;
setError(field, element.validationMessage, 1, events);
} else {
// Delete the associated constraint error
deleteError(field, 1, events);
}

try {
ref?.(element);
} finally {
dispatchEvents(events);
}
dispatchEvents(events);

ref?.(element);
};

field.setError = error => {
Expand All @@ -172,7 +175,7 @@ export function constraintValidationPlugin(): PluginInjector<ConstraintValidatio

field.reportValidity = () => reportValidity(field);

field.getErrors = () => getErrors(getInvalidFields(field, []));
field.getErrors = () => getInvalidFields(field, []).map(field => field.error!);

field.getInvalidFields = () => getInvalidFields(field, []);
};
Expand All @@ -192,8 +195,8 @@ function setError(
field: Field<ConstraintValidationPlugin>,
error: string | null | undefined,
errorOrigin: 1 | 2,
events: Event_[]
): Event_[] {
events: Event[]
): Event[] {
if (error === null || error === undefined || error.length === 0) {
return deleteError(field, errorOrigin, events);
}
Expand Down Expand Up @@ -228,7 +231,7 @@ function setError(
return events;
}

function deleteError(field: Field<ConstraintValidationPlugin>, errorOrigin: 1 | 2, events: Event_[]): Event_[] {
function deleteError(field: Field<ConstraintValidationPlugin>, errorOrigin: 1 | 2, events: Event[]): Event[] {
const originalError = field.error;

if (field.errorOrigin > errorOrigin || originalError === null) {
Expand Down Expand Up @@ -263,7 +266,7 @@ function deleteError(field: Field<ConstraintValidationPlugin>, errorOrigin: 1 |
return events;
}

function clearErrors(field: Field<ConstraintValidationPlugin>, events: Event_[]): Event_[] {
function clearErrors(field: Field<ConstraintValidationPlugin>, events: Event[]): Event[] {
deleteError(field, 2, events);

if (field.children !== null) {
Expand Down Expand Up @@ -300,17 +303,6 @@ function getInvalidFields(
return batch;
}

function getErrors(batch: Field<ConstraintValidationPlugin>[]): string[] {
const errors = [];

for (const field of batch) {
if (field.error !== null) {
errors.push(field.error);
}
}
return errors;
}

function isValidatable(element: Element | null): element is ValidatableElement {
return element instanceof Element && 'validity' in element && element.validity instanceof ValidityState;
}
Loading

0 comments on commit cfa2bbe

Please sign in to comment.