Getting Started
If you don't know what Final Form is, you are either probably a masochist, or have never dealt with forms on web pages. If you come from the world of React, you might be familiar with this tool already. It is similar to Formik and React Hook Form.
To sum it up, it is a framework-agnostic, minimalistic form validation library. For us it was a no-brainer to implement this for Riot. You should understand Final Form first before you attempt to use this tool with Riot so you are not confused as to the implementation.
#
Usagenpm i -S @riot-tools/final-form
<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 '@riot-tools/final-form';
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', 'pepelepew'); form.change('age', 77); }) } },
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 formThere 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)
.
#
Example<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 />