-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8f8332c
commit e6111c1
Showing
1 changed file
with
389 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,389 @@ | ||
--- | ||
kind: '👋 Contributing' | ||
title: 'Forms guidelines' | ||
--- | ||
# Forms guidelines | ||
|
||
In this guide, we detail how to handle forms and validation with Clever Components. | ||
|
||
## Form Handling and Data | ||
|
||
Forms in Clever Components rely on the `formSubmit` directive, which: | ||
|
||
* Handles form submission | ||
* Calls `validate()` on each form control to check validity | ||
* Calls `reportInlineValidity()` to display any errors | ||
* Manages focus on invalid fields | ||
* Calls appropriate callbacks | ||
|
||
The directive takes 2 optional callbacks: | ||
|
||
* `onValid(formData, formElement)`: Called when all fields are valid. | ||
The formData object contains all form values with the form control's name attribute as key. | ||
Typically used to dispatch data to smart components. | ||
|
||
* `onInvalid(formValidity, formElement)`: Only needed when using native form controls that require custom inline error messages. | ||
Called when some fields are invalid. | ||
|
||
Example usage: | ||
|
||
```js | ||
class MyForm extends LitElement { | ||
_onValid(formData) { | ||
// formData = { | ||
// email: '[email protected]', | ||
// age: '25' | ||
// } | ||
dispatchCustomEvent(this, 'change', formData); | ||
} | ||
|
||
render() { | ||
return html` | ||
<!-- bind 'this' so it refers to the component instance in the callback instead of the form submit inner code --> | ||
<form ${formSubmit(this._onValid.bind(this))}> | ||
<cc-input-text name="email" required></cc-input-text> | ||
<cc-input-number name="age"></cc-input-number> | ||
<cc-button type="submit">Submit</cc-button> | ||
</form> | ||
`; | ||
} | ||
} | ||
``` | ||
|
||
### Handling Multiple Values | ||
|
||
There are two ways to handle multiple values in forms: | ||
|
||
#### Grouping form controls with identical names | ||
|
||
Multiple form controls with the same name have their values automatically aggregated into an array: | ||
|
||
```js | ||
<form ${formSubmit(this._onValid.bind(this))}> | ||
<cc-input-text name="tags" value="tag1"></cc-input-text> | ||
<cc-input-text name="tags" value="tag2"></cc-input-text> | ||
</form> | ||
// formData.tags = ['tag1', 'tag2'] | ||
``` | ||
|
||
#### Components with internal array handling | ||
|
||
Form controls can manage array values internally. | ||
The array values are processed correctly by the `formSubmit` directive: | ||
|
||
```js | ||
<form ${formSubmit(this._onValid.bind(this))}> | ||
<cc-input-text name="tags" .tags=${[]}></cc-input-text> | ||
<cc-tcp-redirection-form name="redirections"></cc-tcp-redirection-form> | ||
</form> | ||
// formData.tags and formData.redirections will be arrays | ||
``` | ||
|
||
## Form Controls and Validation | ||
|
||
### Available Controls | ||
|
||
We provide several form control components: | ||
|
||
* `<cc-input-text>`: For text input (with support for email/password/etc.) | ||
* `<cc-input-number>`: For numeric input with min/max constraints | ||
* `<cc-input-date>`: For date selection | ||
* `<cc-select>`: For option selection | ||
|
||
Each control requires a `name` attribute and you must use `<cc-button type="submit">` for submission. | ||
|
||
All Clever Components form controls extend `CcFormControlElement` which provides built-in validation: | ||
|
||
* Required fields with `required` attribute | ||
* Email format with `type="email"` | ||
* Number ranges with `min`/`max` | ||
* Date formats for date inputs | ||
|
||
Validation is a two step process: | ||
|
||
1. Values are validated against built-in and custom validators on input, | ||
2. Error messages are displayed on form submission or by calling `reportInlineValidity()` on a form control that implements `CcFormControlElement`. | ||
|
||
Example with built-in validation: | ||
|
||
```js | ||
<form ${formSubmit(this._onValid.bind(this))}> | ||
<cc-input-text name="email" type="email" required></cc-input-text> | ||
<cc-input-number name="age" min="0" max="120"></cc-input-number> | ||
<cc-input-date name="startDate" required></cc-input-date> | ||
</form> | ||
``` | ||
|
||
### Custom Validation | ||
|
||
Add custom validation with the `customValidator` property. | ||
A custom validator is an object with a `validate` method that returns a `Validation` object. | ||
|
||
```js | ||
import { Validation } from '@clevercloud/components/lib/form/validation.js'; | ||
|
||
const LOWERCASE_VALIDATOR = { | ||
validate: (value) => { | ||
return value.toLowerCase() !== value | ||
? Validation.invalid('notLowerCase') | ||
: Validation.VALID; | ||
}, | ||
}; | ||
|
||
render () { | ||
return html` | ||
<cc-input-text | ||
name="username" | ||
.customValidator=${LOWERCASE_VALIDATOR} | ||
.customErrorMessages=${{ | ||
notLowerCase: 'Username must be lowercase' | ||
}} | ||
></cc-input-text> | ||
`; | ||
} | ||
``` | ||
|
||
### Custom Error Messages | ||
|
||
Override default messages or provide messages for custom validators by providing a `customErrorMessages` object. | ||
This object uses validation error codes as keys (like `empty`, `badEmail`) and maps them to error message strings or functions that return messages. | ||
|
||
```js | ||
const ERROR_MESSAGES = { | ||
empty: 'This field is required!', | ||
badEmail: 'Please enter a valid email', | ||
notLowerCase: () => sanitize`Must be <strong>lowercase</strong>` | ||
}; | ||
|
||
render() { | ||
return html` | ||
<cc-input-text | ||
name="field" | ||
.customErrorMessages=${ERROR_MESSAGES} | ||
></cc-input-text> | ||
`; | ||
} | ||
``` | ||
|
||
### Native Form Controls | ||
|
||
Native form controls are validated but don't show inline errors by default. Use `onInvalid` for custom error handling: | ||
|
||
```js | ||
class MyForm extends LitElement { | ||
_onInvalid(formValidity) { | ||
const nameError = formValidity.find(v => v.name === 'name'); | ||
if (nameError?.validity.valid === false) { | ||
this._nameErrorMessage = nameError.validity.code; | ||
} | ||
} | ||
|
||
render() { | ||
return html` | ||
<form ${formSubmit(null, this._onInvalid.bind(this))}> | ||
<input name="name" required> | ||
${this._nameErrorMessage | ||
? html`<div class="error">${this._nameErrorMessage}</div>` | ||
: '' | ||
} | ||
<cc-button type="submit">Submit</cc-button> | ||
</form> | ||
`; | ||
} | ||
} | ||
``` | ||
For custom validation on native inputs, you'll need to validate onInput and update the input's validity state using `setCustomValidity()`: | ||
```js | ||
class MyForm extends LitElement { | ||
_onNameInput(e) { | ||
const input = e.target; | ||
// Custom validation logic | ||
if (input.value.toLowerCase() !== input.value) { | ||
input.setCustomValidity('Value must be lowercase'); | ||
} | ||
else { | ||
input.setCustomValidity(''); // Clear error | ||
} | ||
} | ||
|
||
_onValid(formData) { | ||
// Clear error message when form is valid | ||
this._nameErrorMessage = null; | ||
// Dispatch data for smart component | ||
dispatchCustomEvent(this, 'change', formData); | ||
} | ||
|
||
_onInvalid(formValidity) { | ||
const nameError = formValidity.find(v => v.name === 'name'); | ||
if (nameError?.validity.valid === false) { | ||
this._nameErrorMessage = nameError.validity.code; | ||
} | ||
} | ||
|
||
render() { | ||
return html` | ||
<form ${formSubmit(this._onValid.bind(this), this._onInvalid.bind(this))}> | ||
<input | ||
name="name" | ||
required | ||
@input=${this._onNameInput} | ||
> | ||
${this._nameErrorMessage | ||
? html`<div class="error">${this._nameErrorMessage}</div>` | ||
: '' | ||
} | ||
<cc-button type="submit">Submit</cc-button> | ||
</form> | ||
`; | ||
} | ||
} | ||
``` | ||
## Smart Component Interactions | ||
Please refer to the [smart component documentation](https://www.clever-cloud.com/doc/clever-components/?path=/docs/%F0%9F%91%8B-contributing-smart-component-guidelines--docs) for details about state management with smart components. | ||
Here is an example of a form component working with its smart parent: | ||
```js | ||
// Form component | ||
class FormComponent extends LitElement { | ||
static get properties() { | ||
return { | ||
formState: { type: Object, attribute: false }, | ||
}; | ||
} | ||
|
||
constructor() { | ||
super(); | ||
|
||
this.formState = { type: 'idle' }; | ||
|
||
this._formRef = createRef(); | ||
|
||
// The FormErrorFocusController automatically handles focusing the first invalid field when form errors occur. | ||
// It takes the component instance, form reference, and a function that returns the current form errors. | ||
new FormErrorFocusController(this, this._formRef, () => this.formState.errors); | ||
} | ||
|
||
// resetForm() clears all form fields back to their default state. | ||
// This is used after successful submission to start fresh. | ||
resetForm() { | ||
this._formRef.value.reset(); | ||
} | ||
|
||
// Helper to map error codes to user-friendly messages | ||
_getErrorMessage(code) { | ||
if (code === 'email-used') { | ||
return 'This email is already taken'; | ||
} | ||
return null; | ||
} | ||
|
||
// _onValidSubmit is called when the form passes validation. | ||
_onValidSubmit(formData) { | ||
// We reset the formState with new values so that the component state is synced with what is displayed | ||
this.formState = { | ||
type: 'idle', | ||
values: { | ||
name: formData.name, | ||
email: formData.email, | ||
}, | ||
}; | ||
dispatchCustomEvent(this, 'submit-form', formData); | ||
} | ||
|
||
render() { | ||
const isFormSubmitting = this.formState.type === 'submitting'; | ||
|
||
return html` | ||
<form ${ref(this._formRef)} ${formSubmit(this._onValidSubmit.bind(this))}> | ||
<cc-input-text | ||
label="Name" | ||
name="name" | ||
required | ||
?readonly=${isFormSubmitting} | ||
value="${this.formState.values?.name}" | ||
></cc-input-text> | ||
<cc-input-text | ||
label="Email" | ||
name="email" | ||
type="email" | ||
required | ||
?readonly=${isFormSubmitting} | ||
value="${this.formState.values?.email}" | ||
.errorMessage=${this._getErrorMessage(this.formState.errors?.email)} | ||
></cc-input-text> | ||
<cc-button primary type="submit" ?waiting=${isFormSubmitting}>Submit</cc-button> | ||
</form> | ||
`; | ||
} | ||
} | ||
|
||
// Smart component configuration | ||
defineSmartComponent({ | ||
selector: 'form-component', | ||
params: {}, | ||
async onContextUpdate({ component, updateComponent, onEvent }) { | ||
// Reset form and set loading state | ||
component.resetForm(); | ||
updateComponent('formState', { type: 'loading' }); | ||
|
||
// Fetch initial form data | ||
getFormData({ apiConfig, signal }) | ||
.then((formData) => { | ||
updateComponent('formState', { | ||
type: 'idle', | ||
values: formData | ||
}); | ||
}) | ||
.catch((error) => { | ||
console.error(error); | ||
updateComponent('formState', { type: 'error' }); | ||
}); | ||
|
||
// Handle form submission | ||
onEvent('form-component:submit-form', (data) => { | ||
updateComponent('formState', (formState) => { | ||
formState.type = 'submitting'; | ||
}); | ||
|
||
updateDataThroughAPI(data) | ||
.then(() => { | ||
updateComponent('formState', (formState) => { | ||
formState.type = 'idle'; | ||
}); | ||
|
||
component.resetForm(); | ||
notifySuccess('Done successfully 🎉'); | ||
}) | ||
.catch((error) => { | ||
updateComponent('formState', (formState) => { | ||
formState.type = 'idle'; | ||
|
||
if (error.message === 'email-used') { | ||
formState.errors = { | ||
email: 'email-used', | ||
}; | ||
} | ||
}); | ||
}); | ||
}); | ||
}, | ||
}); | ||
``` | ||
The form component manages its state through the `formState` property which tracks form values and validation errors. | ||
The smart component configuration handles initializing the form with default values and processes form submissions with proper error handling and success notifications. | ||
This creates a clean separation between form UI logic and data/state management. | ||
To reset the form after successful submission: | ||
1. Call `component.resetForm()` to clear form fields | ||
2. Reset form state to `idle` using `updateComponent()` | ||
3. The new state will trigger a rerender with cleared fields and errors | ||
To display error messages coming from the API: | ||
1. Define error codes in the smart component's error handling using `updateComponent` to trigger a rerender (`formState.errors = { email: 'email-used' }`). Using `updateComponent` ensures property changes trigger a component update. | ||
2. Map error codes to messages in the form component's `_getErrorMessage()` method | ||
3. Pass the mapped message to form controls via the `errorMessage` property |