Skip to content

Commit

Permalink
docs(contributing/forms): init
Browse files Browse the repository at this point in the history
  • Loading branch information
florian-sanders-cc committed Feb 19, 2025
1 parent 8f8332c commit e6111c1
Showing 1 changed file with 389 additions and 0 deletions.
389 changes: 389 additions & 0 deletions docs/contributing/forms.md
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

0 comments on commit e6111c1

Please sign in to comment.