Easily implement final form in your Riot components.
- Usage - Example:
- withFinalForm(component)
- onFormChange :
function
- onFormMutated :
function
- onFieldChange :
function
npm i -S riot-final-form
Your riot component:
<some-form>
<form>
<div class="field">
<label for="name">Name</label>
<input type="text" id="name" name="name" />
<div class="error"></div>
</div>
<div class="field">
<label for="age">Age</label>
<input type="text" id="age" name="age" />
<div class="error"></div>
</div>
<div class="field">
<label for="address">Address</label>
<input type="text" id="address" name="address" />
<div class="error"></div>
</div>
<div class="field">
<label for="password">This element will be ignored</label>
<input type="password" id="password" name="password" ignore />
<div class="error"></div>
</div>
<div>
<div class="col w-50">
<button type='reset'>Reset</button>
</div>
<div class="col w-50 text-right">
<button type="submit">Submit</button>
</div>
</div>
</form>
<script>
import withFinalForm from '..';
export default withFinalForm({
onUpdated() {
// this.finalForm becomes available after
// component has mounted. This gives direct
// access to the instantiated final form object
const form = this.finalForm();
if (this.state.likesCheese) {
form.batch(() => {
form.change('name', 'pepe');
form.change('age', 66);
})
}
},
formElement() {
return this.$('form');
},
// https://final-form.org/docs/final-form/types/Config#onsubmit
onSubmit(values) {
$api.post('/stuff', values);
},
// https://final-form.org/docs/final-form/types/Config#initialvalues
initialValues: {
name: '',
age: null,
address: ''
},
// https://final-form.org/docs/final-form/types/Config#validate
validate(values) {
const errors = {};
if (!values.name) errors.name = 'name is required';
if (!values.age) errors.age = 'age is required';
if (!/^\d+$/.test(values.age)) errors.age = 'age must be a number';
return errors;
},
// https://final-form.org/docs/final-form/types/FormApi#subscribe
// https://final-form.org/docs/final-form/types/FormState
onFormChange(formState) {
const submit = this.formElement().querySelector('[type=submit]');
submit.disabled = !formState.valid;
},
// https://final-form.org/docs/final-form/types/FormApi#registerfield
// `subscriber: FieldState => void`
// Omits `blur, change, focus` keys
onFieldChange(field, { touched, error, valid, visited, dirty }) {
const errorEl = field.parentElement.querySelector('.error');
if (touched && error) {
if (errorEl) errorEl.innerHTML = error;
field.parentElement.classList.add('error');
} else {
if (errorEl) errorEl.innerHTML = '';
field.parentElement.classList.remove('error');
}
},
// https://final-form.org/docs/final-form/types/Config
// validate, initialValues, onSubmit, and destroyOnUnregister cannot be overwritten. `destroyOnUnregister` is always true.
formConfig: {
debug: true
},
// can be one of: active, dirty, dirtyFields, dirtySinceLastSubmit, error, errors, hasSubmitErrors, hasValidationErrors, initialValues, invalid, modified, pristine, submitting, submitError, submitErrors, submitFailed, submitSucceeded, touched, valid, validating, values, visited
formSubscriptions: {
visited: true,
dirty: true
},
// https://final-form.org/docs/final-form/types/FormApi#registerfield
// `subscription: { [string]: boolean }`
fieldSubscriptions: {
name: {
pristine: true,
valid: true
},
age: {
submitFailed: true,
valid: true
}
},
// https://final-form.org/docs/final-form/types/FieldConfig
// Based on a name basis. If your field name is `nested.stuff[0]`, then your config is `{ 'nested.stuff[0]': { ... } }`
fieldConfigs: {
address: {
afterSubmit: () => console.log('afterSubmit yay!!')
}
}
});
</script>
</some-form>
Manually initialize final form:
There may be cases where you want to manually initialize FF, such as when you depend on an XHR request to load initial values. For these scenarios, you can use the manuallyInitializeFinalForm
flag on your component, and manually trigger component.initializeFinalForm();
inside of a lifecycle hook. Be cautious not to lose the lexical this
of the initializeFinalForm()
function. If you need to deeply nest or pass a callback that later calls this, you can do so by extracting into self const self = this;
and binding it initializeFinalForm.bind(self)
.
<some-form>
...
<script>
export default withFinalForm({
manuallyInitializeFinalForm: true,
// Simple async or sync usage
async onMounted() {
this.initialValues = await getStateFromSomewhere();
if (specificCondition) {
this.validate = otherValidationFunction
}
this.initializeFinalForm();
}
// Nested lexical this
onMounted() {
// Reference component
const self = this;
getData().then((data) => {
getMoreData().then(data2 => {
self.initialValues = someData;
// Must pass component for cases where you cannot
// depend on lexical this
self.initializeFinalForm.apply(self);
})
});
}
// External configuration
onMounted() {
const self = this;
const callback = () => self.initializeFinalForm.apply(self);
// Dynamically configure any final form configs before applyings
dynamicallyConfigure(self, callback);
}
})
</script>
</some-form>
Notes:
e.preventDefault()
is called on submit unless explicitly specified otherwise. See this
Input fields can have an
ignore
attribute attached to them which will flag them to be skipped for registration by final form. For example:<input type='hidden' name='csrf_token' value='abc123' ignore /> <input type='hidden' name='post_id' value='1' ignore />
Creates a final form wrapper for a component. Automatically unsubscribes and removes form when component unmounts. Configuration callbacks are all called bound to the riot component, so the lexical this
will be the same as onMounted
. The following configuration options are available:
Param | Type | Description |
---|---|---|
component.formElement | function |
Required function that returns the form element to bind to |
component.onSubmit | function |
Final Form submit function. Prevents defeault behavior. If undefined, default DOM behavior will occur. |
component.initialValues | object |
Final Form initialValues |
component.validate | function |
Form validate function |
component.onFormChange | onFormChange |
Final Form listener that passes form state |
component.onFormMutated | onFormMutated |
MutationObserver that listens for changes in the HTML form |
component.formSubscriptions | object |
Final Form subscriptions |
component.formConfig | object |
Final Form configs |
component.onFieldChange | onFieldChange |
Callback ran when a field changes |
component.fieldSubscriptions | object |
Final Form field subscriptions |
component.fieldConfigs | object |
Final Form field configs |
component.manuallyInitializeFinalForm | boolean |
In case you want to manually initialize final form after some async event. Read more about this flag. |
Form change callback
Param | Type | Description |
---|---|---|
formState | object |
final form state |
Mutation observer callback
Param | Type | Description |
---|---|---|
formMutationOptions | object |
Options to perform changes to final form state |
<script>
withFinalForm({
// ...
onFormMutated(formMutationOptions) {
const {
// Mutation observer callback
mutationsList,
observer,
// Map of registrations (Map<HTMLElement, deregisterFunction()>)
registrations,
// Final form API
form,
// registerField(field: HTMLFormInputElement)
registerField
} = formMutationOptions;
for (const mutations of mutationsList) {
const {
addedNodes,
removedNodes
} = mutation;
for (const el of [...addedNodes]) {
if (/^(?:input|select|textarea)$/i.test(el.nodeName)) {
registerField(el);
}
}
for (const el of [...removedNodes]) {
if (registrations.has(el)) {
const unregister = registrations.get(el);
unregister();
}
}
}
}
// ...
});
</script>
Field change callback
Param | Type | Description |
---|---|---|
field | HTMLElement |
form field |
fieldState | object |
final form field state |