Form HOC for Web Components. It's also works for LitElement.
Lite Form implements a Formik-like API, so just try using it. See API Reference for more information.
import { LitElement, html } from 'lit-element'
import { withForm } from 'lite-form'
class MyForm extends LitElement {
static get properties() {
return {
values: { type: Object },
errors: { type: Object },
touched: { type: Object }
}
}
render() {
return html`
<form method="POST" @submit=${this.handleSubmit}>
<input
id="login"
@input=${this.handleChange}
@change=${this.handleChange}
@blur=${this.handleBlur}
.value=${this.values.login}
/>
${this.errors.login && this.touched.login
? this.errors.login
: ''}
<input
id="password"
type="password"
@input=${this.handleChange}
@change=${this.handleChange}
@blur=${this.handleBlur}
.value=${this.values.password}
/>
${this.errors.password && this.touched.password
? this.errors.password
: ''}
<button type="submit">Submit</button>
</form>
`
}
}
const enhance = withForm({
initialValues: { login: '', password: '' },
onSubmit: values => console.log(values),
validationSchema: {
login: value => {
if (!value) return 'Required'
},
password: value => {
if (value.length < 5) return 'Must be more than 5 letters'
}
}
})
customElements.define('native-inputs-form', enhance(MyForm))
It will become more concise if you create your own <custom-input>
using withField
and <error-message>
using withError
:
import { LitElement, html } from 'lit-element'
import { withForm } from 'lite-form'
class MyForm extends LitElement {
render() {
return html`
<form method="POST" @submit=${this.handleSubmit}>
<custom-input name="login"></custom-input>
<error-message name="login"></error-message>
<custom-input name="password" type="password"></custom-input>
<error-message name="password"></error-message>
<button type="submit">Submit</button>
</form>
`
}
}
const enhance = withForm({
initialValues: { login: '', password: '' },
onSubmit: values => console.log(values),
validationSchema: {
login: value => {
if (!value) return 'Required'
},
password: value => {
if (value.length < 5) return 'Must be more than 5 letters'
}
}
})
customElements.define('wrapped-inputs-form', enhance(MyForm))
Here are the components:
// custom-input
import { LitElement, html } from 'lit-element'
import { withField } from 'lite-form'
export default class ExwcInput extends LitElement {
static get properties() {
return {
type: { type: String },
name: { type: String },
value: { type: String }
}
}
constructor() {
super()
this.type = 'text'
this.value = ''
}
render() {
return html`
<input
autocomplete="off"
type=${this.type}
name=${this.name}
.value=${this.value}
@input=${this.handleChange}
/>
`
}
}
customElements.define('custom-input', withField(ExwcInput))
// error-message
import { LitElement, html } from 'lit-element'
import { withError } from 'lite-form'
export default class ExwcInput extends LitElement {
static get properties() {
return {
name: { type: String },
error: { type: String },
touched: { type: Boolean }
}
}
render() {
return html`
${this.error && this.touched
? this.error
: ''}
`
}
}
customElements.define('error-message', withError(ExwcInput))
You can also create your own base form class. For example:
// Form Base Class
import { LitElement, html } from 'lit-element'
import { withForm } from 'lite-form'
class LiteForm extends LitElement {
render() {
return html`<form @submit=${this.handleSubmit} method=${this.method}>
${this.formRender(this)}
</form>`
}
}
customElements.define('lite-form', withForm(LiteForm))
And use it in HTML:
// Using Base Form Class
import { html, render } from 'lit-html'
const formRender = ({ values, handleBlur, handleChange }) =>
html`
<custom-input name="login"></custom-input>
<error-message name="login"></error-message>
<custom-input name="password" type="password"></custom-input>
<error-message name="password"></error-message>
<button type="submit">Submit</button>
`
const MyForm = html`
<lite-form
method="post"
.formRender=${formRender}
.onSubmit=${values => console.log(values)}
.initialValues=${{
login: '',
password: ''
}}
.validationSchema=${{
login: value => {
if (!value) return 'Required'
},
password: value => {
if (value.length < 5) return 'Must be more than 5 letters'
}
}}
></lite-form>
`
render(html`${MyForm}`, document.getElementById('root'))
Don't use a slot instead of providing a form template if you need from events - the form will not trigger its events on elements inside the slot.
You can also extends builtin form element using Lite Form:
import { withForm } from 'lite-form'
class LiteForm extends HTMLFormElement {
connectedCallback() {
this.addEventListener('submit', this.handleSubmit)
this.addEventListener('reset', this.handleReset)
}
disconnectedCallback() {
this.removeEventListener('submit', this.handleSubmit)
this.removeEventListener('reset', this.handleReset)
}
}
customElements.define('native-form', withForm(LiteForm), { extends: 'form' })
And use it in HTML:
// Using form element
import { html } from 'lit-element'
const MyForm = html`
<form
is="native-form"
.onSubmit=${values => console.log(values)}
.initialValues=${{
login: '',
password: ''
}}
.validationSchema=${{
login: value => {
if (!value) return 'Required'
},
password: value => {
if (value.length < 5) return 'Must be more than 5 letters'
}
}}
>
<custom-input name="login"></custom-input>
<error-message name="login"></error-message>
<custom-input name="password" type="password"></custom-input>
<error-message name="password"></error-message>
<button type="submit">Submit</button>
</form>
`
render(html`${MyForm}`, document.getElementById('root'))
Customising builtin elements does not work in Safari and iOS (13) without polyfill.
withForm(config)(Component)
or withForm(ComponentWithConfig)
Instead of using config in the HOC, you can put it in the Component class. This will allow you to create your own base form class.
- onSubmit:
(values, props)=>{}
- initialValues:
{ propName: value } || props => ({ propName: value })
- validationSchema:
{ propName: (value, props) => 'error'||{} }
- validateOnBlur:
boolean
. Default:true
- validateOnChang:
boolean
. Default:true
- values:
object
- errors:
object
- touched:
object
- isValid:
boolean
- handleSubmit:
function
- handleChange:
(path || event, value) => {}
. event:Event: {target: {name || id, value}} || CustomEvent: {detail: {name || id, value}}
- handleBlur:
(path || event) => {}
. event:Event: {target: {name || id}} || CustomEvent: {detail: {name || id}}
- handleValidate:
function
- handleReset:
function
- values_change:
e=>console.log(e.detail.values)
- errors_touched_change:
e=>console.log(e.detail.errors, e.detail.touched)
withField(Component)
or withField(config)(Component)
Component must have name
(means path) or id
attribute.
- captureBlur
boolean
. Default:true
- listenChange
boolean
. Default:false
captureBlur
is usefull if you don't use Shadow DOM in your Component or if you use it with slots. If you use Shadow DOM without slots the event will be bubbling (composed) by default and you don't need to use capture for it.
listenChange
is usefull if you don't using Shadow DOM in your Component or if your Component dispatch @change event (with options {bubbles: true, composed: true}) instead of using handleChange method. It is useless if you use Shadow DOM in your Component and not dispatch @change event, because js will replace event.target
from your internal input to your Component, and it will lose target.value
- value:
any
- handleChange:
(value || event) => {}
. event:Event: {target: {value}} || CustomEvent: {detail: {value}}
- handleBlur:
function
- blur:
Event or CustomEvent
. IfcaptureBlur==true
it will be listening on capturing phase. - change:
Event: {target: {value}} || CustomEvent: {details: {value}}
. Listening only iflistenChange==true
withError(Component)
Component must have name
(means path) or id
attribute.
- error:
object
- touched:
object
withValue(Component)
Component must have name
(means path) or id
attribute.
- value:
any
withFormClass(Component)
- _formClass: your withForm(Component)
class
. It's usefull to build your own HOCs.
- Don't use slots with input elements inside
<form>
tag if you need to catch form's events from them! - Don't use Shadow DOM if you need autofill to your form (e.g. login & password)!
- Customising builtin elements does not work in Safari and iOS (13) without polyfill.