Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extensible validation support in Volto forms #6181

Merged
merged 55 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
d02a64b
Add foundations for extensible validation in forms
sneridagh Jul 10, 2024
d4a6d70
Fix tests
sneridagh Jul 10, 2024
bc7b779
fix build-deps precedence
sneridagh Jul 10, 2024
ad3a0d6
Fix more
sneridagh Jul 10, 2024
1fa71ed
Fix generator tests
sneridagh Jul 10, 2024
f6bba96
Fix react-share package
sneridagh Jul 10, 2024
3a4b000
fix test for social sharing
sneridagh Jul 10, 2024
709ed59
Add custom validation field property
sneridagh Jul 10, 2024
84bc5bf
Merge branch 'main' into extensible-validation
sneridagh Jul 11, 2024
26c82f9
Fix uniqueValidator
sneridagh Jul 12, 2024
3378ade
Add documentation
sneridagh Jul 12, 2024
0101fca
Changelog
sneridagh Jul 12, 2024
7f2cfaa
Changelog
sneridagh Jul 12, 2024
7963197
Checkpoint
sneridagh Jul 12, 2024
b135545
Validation for blocks too :)
sneridagh Jul 12, 2024
3d6ef22
locales
sneridagh Jul 15, 2024
80d7462
Refactor validation in Form
sneridagh Jul 15, 2024
9ad0eea
Changelog
sneridagh Jul 15, 2024
6234d48
Separate type-widget validators. Add behavior-fieldId validator types.
sneridagh Jul 15, 2024
30db8cd
Block-fieldId validators
sneridagh Jul 15, 2024
bef6a77
Changelog
sneridagh Jul 16, 2024
41da3fb
Typos correction for validation.md
ichim-david Jul 16, 2024
2a32edc
Complete default explanation
sneridagh Jul 16, 2024
34490f9
Add more down to earth examples
sneridagh Jul 16, 2024
3152854
Apply suggestions from code review
sneridagh Jul 18, 2024
b89d0ce
Refactor and re-document for clarity and simplicity
sneridagh Jul 22, 2024
11c24c7
locales
sneridagh Jul 22, 2024
e11e9d5
Remove cruft
sneridagh Jul 22, 2024
8094fa4
Bring back correct typing
sneridagh Jul 22, 2024
0b4f253
carry over suggestions from #6161 to index.md
stevepiercy Jul 22, 2024
ca2bdd2
Steve's review and edits of validation.md
stevepiercy Jul 22, 2024
641ce69
Apply suggestions from code review
sneridagh Jul 23, 2024
2a61300
Unify validator names, ending in `Validator`
sneridagh Jul 23, 2024
091711a
Fix start-end date validator i18n msg, add tests
sneridagh Jul 23, 2024
79b9a0f
Add pattern validator
sneridagh Jul 23, 2024
6cb3f2b
Implemented default `maxItems`/`minItems`
sneridagh Jul 23, 2024
0999019
Update docs/source/configuration/validation.md
sneridagh Jul 24, 2024
e787a8a
Apply suggestions from code review
sneridagh Jul 24, 2024
44001cb
Added missing docs for utilities in types
sneridagh Jul 24, 2024
aff0010
Add initial sort for keys when hashing them into the depsString
sneridagh Jul 24, 2024
443f9c7
Fix out of date registry changelog
sneridagh Jul 24, 2024
01a14cf
Include in the documentation all the default validators in Volto
sneridagh Jul 24, 2024
f8b7287
Change the name of the dependency key from `widgetName` to `widget`
sneridagh Jul 24, 2024
f7dee7e
Remove granularity from the paragraph
sneridagh Jul 24, 2024
2578cf5
locales
sneridagh Jul 24, 2024
a2afa51
Add an example of how to build an invariant
sneridagh Jul 24, 2024
0b2c26e
Move the default validators section to the top
sneridagh Jul 24, 2024
3140a42
Merge branch 'main' into extensible-validation-format
sneridagh Jul 24, 2024
2e2df93
Merge branch 'main' into extensible-validation-format
sneridagh Jul 24, 2024
c01bb39
Apply suggestions from code review
sneridagh Jul 26, 2024
f619cd8
Pass `blocksErrors` as a separate prop, aside from the whole errors o…
sneridagh Jul 29, 2024
461b188
Add new `blocksErrors` prop to all stock blocks
sneridagh Jul 29, 2024
5f32593
Remove the optional from Validator type, since the engine always push…
sneridagh Jul 29, 2024
0eaf1ee
Merge branch 'main' into extensible-validation-format
sneridagh Jul 30, 2024
d50cda0
Cypress Test for Validation of Field Types in CoreSandBox (#6217)
Tishasoumya-02 Jul 30, 2024
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
2 changes: 1 addition & 1 deletion .github/actions/node_env_setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ runs:

- name: Install Volto dependencies
shell: bash
run: pnpm i
run: make install

- name: Install Cypress if not in cache
if: steps.cache-cypress-binary.outputs.cache-hit != 'true'
Expand Down
38 changes: 2 additions & 36 deletions .github/workflows/acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -497,45 +497,11 @@ jobs:
steps:
- uses: actions/checkout@v4

# node setup
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Set up Node.js environment
uses: ./.github/actions/node_env_setup
with:
node-version: ${{ matrix.node-version }}

- name: Enable corepack
run: corepack enable

- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Cache Cypress Binary
id: cache-cypress-binary
uses: actions/cache@v4
with:
path: ~/.cache/Cypress
key: binary-${{ matrix.node-version }}-${{ hashFiles('pnpm-lock.yaml') }}

- run: pnpm i

- name: Build dependencies
run: pnpm build:deps

- name: Install Cypress if not in cache
if: steps.cache-cypress-binary.outputs.cache-hit != 'true'
working-directory: packages/volto
run: make cypress-install

# Generator own tests
- name: Generator tests
run: pnpm test
Expand Down
23 changes: 2 additions & 21 deletions .github/workflows/unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,11 @@ jobs:
steps:
- uses: actions/checkout@v4

# node setup
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Set up Node.js environment
uses: ./.github/actions/node_env_setup
with:
node-version: ${{ matrix.node-version }}

- name: Enable corepack
run: corepack enable

- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- run: pnpm i

# Locales in place are needed for the tests to pass
- run: pnpm --filter @plone/volto i18n

Expand Down
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ clean: ## Clean development environment
find ./packages -name node_modules -exec rm -rf {} \;

.PHONY: install
install: build-deps ## Set up development environment
install: ## Set up development environment
# Setup ESlint for VSCode
node packages/scripts/vscodesettings.js
pnpm i
node packages/scripts/vscodesettings.js
make build-deps

##### Documentation

Expand Down Expand Up @@ -137,10 +138,10 @@ docs-test: docs-clean docs-linkcheckbroken docs-vale ## Clean docs build, then
cypress-install: ## Install Cypress for acceptance tests
$(NODEBIN)/cypress install

packages/registry/dist: packages/registry/src
packages/registry/dist: $(shell find packages/registry/src -type f)
pnpm build:registry

packages/components/dist: packages/components/src
packages/components/dist: $(shell find packages/components/src -type f)
pnpm build:components

.PHONY: build-deps
Expand Down
1 change: 1 addition & 0 deletions docs/source/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ environmentvariables
expanders
locking
slots
validation
```
273 changes: 273 additions & 0 deletions docs/source/configuration/validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
---
myst:
html_meta:
"description": "Client side form field validation"
"property=og:description": "Client side form field validation"
"property=og:title": "Form fields validation"
sneridagh marked this conversation as resolved.
Show resolved Hide resolved
"keywords": "Volto, Plone, frontend, React, configuration, form, fields, validation"
---

# Client side form field validation

Volto provides a mechanism for delivering form field validation in an extensible way.
sneridagh marked this conversation as resolved.
Show resolved Hide resolved
This extensibility is based on the Volto registry.
It applies to content types, custom programatically generated forms and blocks schema settings.
All of them are serialized using JSON schema standard and then Volto generates the resultant form out of it.

## Registering a validator

You can register a validator using the registry API from your add-on configuration.
The validators are registered using the `registerUtility` API method.

### Registering and declaring a simple validator

The most common thing is to have a field that you want to validate with a specific validator.
Volto provide some default validators, see at the end of this chapter for more information.

#### For Volto custom forms and block schema forms

When you define them programatically, in your core, using JSON schema, you can register a custom validator using the `format` property.
This is the case of creating the schema for a block:

```ts
let blockSchema = {
// ... fieldset definition in here
properties: {
...schema.properties,
customField: {
title: 'My custom URL field',
description: '',
format: 'url'
},
},
required: [],
};
```

`url` named validator should be registered by this name as a Volto validator utility:

```ts
config.registerUtility({
type: 'validator',
name: 'url',
method: urlValidator,
})
```

In this case, the `urlValidator` method validator will be applied for the block field `customField`.

#### For content types

Content types also can specify the `format` using the schema hints in the backend in the `frontendOptions`:

```python
from plone.supermodel import model
from zope import schema

class IMyContent(model.Schema):
directives.widget(
"customField",
frontendOptions={
"format": "url",
},
)
customField = schema.TextLine(
title="Custom URL field",
required=False,
)
# Rest of your content type definition
```

For the record, the resultant `plone.restapi` response will be something like the following, a bit different than in blocks JSON schema.
But the validation engine will take care too:

```json
{
"properties": {
"customField": {
"title": "Custom URL field",
"widgetOptions": {
"frontendOptions": {
"format": "url"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change the api call so it will be "flattened" to the root of the field so we only have 1 way specifying the format?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had this wish back in the day too, but it never happened, since we already had this "scape hatch" for all customizable things in Python schema hints. Maybe we can push again in the backend side for this to happen.

@davisagli do you think we could try to unify this also in the backend? Or do you remember the reason behind we never did it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on the property. For things that are valid JSON Schema, it would make sense to have plone.restapi serialize them at the top level. For other properties that are not part of JSON Schema, it makes sense to keep them inside the widgetOptions.

}
}
}
}
}
```

and the `urlValidator` method validator will be applied for the content type field `customField`.

### Advanced scenarios

In case you need more granularity and you don't have access to modify the existing implementation of the JSON schema definitions for existing content types, blocks or forms (might be in third party add-ons), you can use the following advanced validator registrations, using `field`, `widget`, `behaviorName` or `blockType` validator registrations.

#### Per field `type` validators

These validators are applied depending on the specified `type` of the field in the JSON schema from content types, forms or blocks.
The next example is for the use case of JSON schema defined in a block:

```ts
let blockSchema = {
// ... fieldset definition in here
properties: {
...schema.properties,
customField: {
title: 'My custom field',
description: '',
type: 'integer',
maximum: 30
},
},
required: [],
};
```

```ts
config.registerUtility({
type: 'validator',
name: 'maximum',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is indeed part of the validation spec of json schema (same for maxLength, minLength, maxItems, pattern etc). Should we make these validators customisable? I think it would make more sense to have these "build-in" as validators since the name already implies what it should do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we have today as built in:

  • minLength
  • maxLenght
  • minimum
  • maximum
  • uniqueItem

We could implement the rest, for sure.

dependencies: {
fieldType: 'integer',
},
method: maximumValidator,
})
```

You should specify the `type` in the JSON schema of the block (in a content type, it is included in the default serialization of the field).
If a field does not specify type, it assumes a `string` type as validator.

#### Per field `widget` validators

These validators are applied depending on the specified `widget` of the field.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the above examples you have to specify in the json schema if the validator is enabled or not. In the registerUtility method you only specify if the validator is available or not. It seems in this example registering the validator will have it enabled by default. I think for consistency we should also specify it in the schema and not enable it by default.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will clarify which validators are included and enabled by default, and put it on the top, instead than at the bottom.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, having that at the end of the docs, with a seealso where it was first mentioned, didn't feel right. This would be a good change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevepiercy moved to the top.


```ts
let blockSchema = {
// ... fieldset definition in here
properties: {
...schema.properties,
customField: {
title: 'My custom field',
description: '',
widget: 'phoneNumber',
},
},
required: [],
};
```

```ts
config.registerUtility({
type: 'validator',
name: 'phoneNumber',
dependencies: {
widgetName: 'phoneNumber',
sneridagh marked this conversation as resolved.
Show resolved Hide resolved
},
method: phoneValidator,
})
```


You should specify the `widget` in the JSON schema of the block, or as additional data in the content type definition.
Content types also can specify the `widget` to be used using the schema hints in the backend in the `frontendOptions`:


```python
from plone.supermodel import model
from zope import schema

class IMyContent(model.Schema):
directives.widget(
"customField",
frontendOptions={
"widget": "url",
},
)
customField = schema.TextLine(
title="Custom URL field",
required=False,
)
# Rest of your content type definition
```

the validation engine will take care too, and the `urlValidator` method validator will be applied for the content type field `customField`.

#### Per behavior and field name validator

These validators are applied depending on the behavior (usually coming from a content type definition) in combination with the name of the field.

```ts
config.registerUtility({
type: 'validator',
name: 'dateRange',
dependencies: {
behaviorName: 'plone.eventbasic',
davisagli marked this conversation as resolved.
Show resolved Hide resolved
fieldName: 'start'
},
method: startEventDateRangeValidator,
})
```

It takes the `behaviorName` and the `fieldName` as dependencies.
This type of validator only applies to content type validators.

#### Per block type and field name validator

These validators are applied depending on the block type in combination with the name of the field in the block settings JSON schema.

```ts
config.registerUtility({
type: 'validator',
name: 'url',
dependencies: {
blockType: 'slider',
fieldName: 'url'
},
method: urlValidator,
})
```

It takes the `blockType` and the `fieldName` as dependencies.
This type of validator only applies to blocks.

## Volto's default validators

Volto provides a set of validators by default, you can find them in this module: `packages/volto/src/config/validators.ts`

### How to override a validator

You can override them in your add-on as any other component defined in the registry, by redefining them using the same `dependencies`, and providing your own.

## Signature of a validator

A validator has the following signature:

```ts
type Validator = {
// The field value
value: string;
// The field schema definition object
field: Record<string, any>;
// The form data
formData?: any;
// The intl formatMessage function
formatMessage: Function;
};
```

This is an example of an `isNumber` validator:

```ts
export const isNumber = ({ value, formatMessage }: Validator) => {
const floatRegex = /^[+-]?\d+(\.\d+)?$/;
const isValid =
typeof value === 'number' && !isNaN(value) && floatRegex.test(value);
return !isValid ? formatMessage(messages.isNumber) : null;
};
```

## Invariants

Using the `formData` you can perform validation checks using other field data as source.
This is interesting in the case that two fields are related, like `start` and `end` dates.
You can create invariant type of validators thanks to this.
Loading
Loading