-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from open-wc/documentation
docs(all) add initial readmes for components and root
- Loading branch information
Showing
5 changed files
with
349 additions
and
9 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,9 @@ | ||
# Form Participation | ||
The ability to create custom form elements that extend the behavior and UI/UX of native form elements is an area that has a huge need for standardization. The packages in this monorepo enable web component authors seeking to create custom element inputs a standardized approach to do so. | ||
|
||
|
||
### Packages | ||
|
||
- [`@open-wc/form-control`](./packages/form-control) : A `FormControlMixin` that enables creating a web component that functions like a native form element in a standardized way | ||
- [`@open-wc/form-helpers`](./packages/form-helpers) Form control related utilities such as implicit submit and form value parsing. | ||
|
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,176 @@ | ||
# @open-wc/form-control | ||
A standardized mixin for creating form-associated custom elements using a standardized validation function pattern. | ||
|
||
## Install | ||
|
||
```sh | ||
# npm | ||
npm install @open-wc/form-control | ||
|
||
# yarn | ||
yarn add @open-wc/form-control | ||
``` | ||
|
||
## Usage | ||
|
||
After importing, create a web component class that extends the mixin, and provide your desired base class as the input to `FormControlMixin`. | ||
|
||
> The `FormControlMixin` has been tested with both [LitElement](https://lit.dev/) and `HTMLElement`, so `LitElement` is not required, but all examples in this documentation will show `LitElement` web component syntax and decorators. | ||
|
||
```typescript | ||
// custom web component class that extends FormControlMixin | ||
|
||
import { LitElement, html } from 'lit'; | ||
import { customElement, query, property } from 'lit/decorators.js' | ||
import { live } from 'lit/directives/live.js'; | ||
|
||
import { FormControlMixin } from '@open-wc/form-control'; | ||
|
||
@customElement('demo-form-control') | ||
class DemoFormControl extends FormControlMixin(LitElement) { | ||
@property({ type: String }) | ||
value = ''; | ||
|
||
render() { | ||
return html` | ||
<label for="input"><slot></slot></label> | ||
<input | ||
id="input" | ||
.value="${live(this.value)}" | ||
@input="${this.#onInput}" | ||
> | ||
`; | ||
} | ||
|
||
#onInput({ target }: { target: HTMLInputElement }): void { | ||
this.value = target.value; | ||
} | ||
} | ||
``` | ||
|
||
Now, the `demo-form-control` custom element will participate as if it was a native element in an HTML form. | ||
|
||
```html | ||
<form> | ||
<demo-form-control | ||
name="demo" | ||
value="Hello world" | ||
>Demo form element</demo-form-control> | ||
|
||
<button type="submit">Submit</button> | ||
</form> | ||
|
||
<script> | ||
const form = document.querySelector('form'); | ||
form.addEventListener('submit', event => { | ||
/** Prevent the page from reloading */ | ||
event.preventDefault(); | ||
/** Get form data object via built-in API */ | ||
const data = new FormData(event.target); | ||
console.log('demo-form-control value:', data.get('demo')); | ||
}); | ||
</script> | ||
``` | ||
|
||
### ElementInternals | ||
|
||
This library makes use of [ElementInternals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) features. As of the time of writing `ElementInternals` features are fully supported in Chrome, partially supported in Firefox and being strongly considered by Webkit. | ||
|
||
In order to make these features work in all browsers you will need to include the [element-internals-polyfill](https://www.npmjs.com/package/element-internals-polyfill). Refer to the `element-internals-polyfill` documentation for installation and usage instructions. | ||
|
||
### `Value` & `checked` | ||
Any component that uses the `FormControlMixin` will have a `value` property that the element will apply to the parent form. If the element also has a `checked` property on the prototype (think checkbox or radio button) the element's value will only be applied to the parent form when the `checked` property is truthy (like native checkboxes and radio buttons behave). | ||
|
||
## Validation | ||
|
||
The `FormControlMixin` includes an API for constraint validations and a set of common validators for validity states like `required`, `minlength`, `maxlength` and `pattern`. | ||
|
||
```typescript | ||
import { LitElement, html } from 'lit'; | ||
import { customElement, query, property } from 'lit/decorators.js' | ||
import { live } from 'lit/directives/live.js'; | ||
|
||
import { FormControlMixin, requiredValidator } from '@open-wc/form-control'; | ||
|
||
@customElement('demo-form-control') | ||
class DemoFormControl extends FormControlMixin(LitElement) { | ||
static formControlValidators = [requiredValidator]; | ||
|
||
@property({ type: Boolean, reflect: true }) | ||
required = false; | ||
|
||
@property({ type: String }) | ||
value = ''; | ||
|
||
render() { | ||
return html` | ||
<label for="input"><slot></slot></label> | ||
<input | ||
id="input" | ||
.value="${live(this.value)}" | ||
@input="${this.#onInput}" | ||
> | ||
`; | ||
} | ||
|
||
#onInput({ target }: { target: HTMLInputElement }): void { | ||
this.value = target.value; | ||
} | ||
} | ||
``` | ||
|
||
Including the `requiredValidator` adds a validation function attached to the `valueMissing` validity state to the component instance. | ||
|
||
> Note, this does require the element's prototype to actually have a `required` property defined. | ||
### Validation Target | ||
Every `FormControlMixin` element will need a public `validationTarget` which must be a focusable DOM element in the custom element's `shadowRoot`. In the event a control becomes invalid, this item will be focused on form submit for accessibility purposes. Failure to do so will cause an error to throw. | ||
|
||
### Validators | ||
This package contains a few standardized validators, though more could be added for various unconsidered use cases. So far, there are validators for: | ||
|
||
- **required** (valueMissing) : fails when the element's `value` is falsy while the element's `required` property equals `true` | ||
- **minlength** (rangeUnderflow) : fails if the length of the element's value is less than the defined `minLength` | ||
- **maxlength** (rangeOverflow) : fails if the length of the element's value is greater than the defined `maxLength` | ||
- **programmatic** (customError) : Allows setting a completely custom error state and message as a string. | ||
|
||
If you have an idea for another standardized validator, please [Submit an issue](/../../issues) (preferred so that we can discuss) or [Send a PR](/../../pulls) with your ideas. | ||
|
||
### Creating a custom validator | ||
|
||
It is possible to create a custom validator object using the `Validator` interface: | ||
|
||
```typescript | ||
export interface Validator { | ||
attribute?: string; | ||
key?: string; | ||
message: string | ((instance: any, value: any) => string); | ||
callback(instance: HTMLElement, value: any): boolean; | ||
} | ||
``` | ||
|
||
| Property | Type | Required | Description | | ||
| ---- | ---- | ---- | ---- | | ||
| attribute | `string`| true | If defined, adds the specified attribute to the element's `observedAttributes` and the validator will run when the provided attribute changed | | ||
| key| `string` | - | String name of one of the fields in the `ValidityState` object to override on validator change. If `key` is not set, it is assumed to be `customError`. | | ||
| message | `string \| ((instance: any, value: any) => string)` | true | When set to a string, the `message` will equal the string passed in. If set to a function, the validation message will be the returned value from the callback. The message callback takes two arguments, the element instance and the control's form value (not the element's value property) | | ||
| callback | `(instance: any, value: any) => boolean`| true | When `callback` returns `true`, the validator is considered to be in a valid state. When the callback returns `false` the validator is considered to be in an invalid state. | | ||
|
||
|
||
#### Example custom validator | ||
|
||
So, a validator that would key off an `error` attribute to attach a programatic validation to an input might look like this: | ||
|
||
```typescript | ||
export const programaticValidator: Validator = { | ||
attribute: 'error', | ||
message(instance: HTMLElement & { error: string }): string { | ||
return instance.error; | ||
}, | ||
callback(instance: HTMLElement & { error: string }): boolean { | ||
return !instance.error; | ||
} | ||
}; | ||
``` |
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,122 @@ | ||
# @open-wc/form-utils | ||
A collection of form control related utilities for working with forms. | ||
|
||
|
||
## Install | ||
|
||
```sh | ||
# npm | ||
npm install @open-wc/form-helpers | ||
|
||
# yarn | ||
yarn add @open-wc/form-helpers | ||
``` | ||
|
||
### Implicit form submit | ||
The `submit` helper is a useful helper for firing the forms `submit` event – as a preventable event – only when the form's validity reports back as truthy (meaning the form has all valid values in its inputs) and calling the provided form's `submit()` method if the submit event is not `defaultPrevented`. | ||
|
||
It is perhaps best used to add implicit form submission to inputs in a form when the `Enter` key is pressed so that any input can submit a form. Such a feature can be useful for search inputs with no submit button. | ||
|
||
> This helper is somewhat of a stop gap method until Safari implements [HTMLFormElement.requestSubmit()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit) | ||
```html | ||
<form id="someForm" @submit="${submitHandler}"> | ||
<input required> | ||
</form> | ||
``` | ||
```js | ||
import { submit } from '@open-wc/form-helpers'; | ||
|
||
let submitted = false; | ||
const form = document.querySelector('#someForm'); | ||
const input = document.querySelector('input'); | ||
|
||
input.addEventListener( 'keypress', ($event) => { | ||
if($event.keyCode === 13) { // Enter key | ||
submit(form); // submit event is emitted, and form's submit() method is called if the `submit` event is not `defaultPrevented` | ||
console.log(submitted) // submitted will be false if the input doesn't have a value AND is required | ||
} | ||
}); | ||
|
||
function submitHandler(event) { | ||
// the event is not prevented, so the form will be submitted | ||
submitted = true; | ||
}; | ||
``` | ||
|
||
### Parse form values | ||
The `formValues` helper is a useful function for parsing out the values of a form's inputs in an object format. | ||
|
||
```js | ||
import { formValues } from '@open-wc/form-helpers'; | ||
``` | ||
|
||
Given a form like: | ||
```html | ||
<form> | ||
<input name="foo" value="one"> | ||
<input name="bar" value="two"> | ||
<input name="baz" value="1"> | ||
<input name="baz" value="2"> | ||
</form> | ||
``` | ||
|
||
parsing the form's values can be performed as such: | ||
|
||
```js | ||
import { formValues } from '@open-wc/form-helpers'; | ||
|
||
const form = document.querySelector('form'); | ||
|
||
console.log(formValues(form)) | ||
|
||
// Output: | ||
// { | ||
// foo: 'one', | ||
// bar: 'two', | ||
// baz: ['1', '2'] | ||
// } | ||
``` | ||
|
||
### Parse form object | ||
The `parseFormObject` helper enables deeper nesting and organization of inputs in a form by inspecting the `name` attribute on each input element and analyzing according to dot notation. | ||
|
||
```js | ||
import { parseFormAsObject } from '@open-wc/form-helpers'; | ||
``` | ||
|
||
Given a form like | ||
```html | ||
<form> | ||
<input name="one.a" value="a"> | ||
<input name="one.b" value="b"> | ||
<input name="two" value="2"> | ||
<input name="foo.bar.baz" value="baz"> | ||
<input name="foo.bar.qux" value="qux"> | ||
<input name="three" value="three"> | ||
<input name="three" value="tres"> | ||
</form> | ||
``` | ||
parsing the form values as a deeply nested object can be perfomed as such: | ||
```js | ||
import { parseFormAsObject } from '@open-wc/form-helpers'; | ||
|
||
const form = document.querySelector('form'); | ||
|
||
console.log(parseFormAsObject(form)) | ||
|
||
// Output: | ||
// { | ||
// one: { | ||
// a: 'a', | ||
// b: 'b', | ||
// }, | ||
// two: '2', | ||
// foo: { | ||
// bar: { | ||
// baz: 'baz', | ||
// qux: 'qux' | ||
// } | ||
// }, | ||
// three: ['three', 'tres'] | ||
// } | ||
``` |
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 |
---|---|---|
@@ -1,34 +1,67 @@ | ||
import { aTimeout, expect, fixture, fixtureCleanup, html } from '@open-wc/testing'; | ||
import * as sinon from 'sinon'; | ||
import { submit } from '../src'; | ||
|
||
let submitted = false; | ||
const submitCallback = (event: Event) => { | ||
const submitCallbackPrevented = (event: Event) => { | ||
event.preventDefault(); | ||
submitted = true; | ||
}; | ||
const submitCallback = (event: Event) => { | ||
submitted = true; | ||
}; | ||
|
||
describe('The submit form helper', () => { | ||
let form: HTMLFormElement; | ||
let formSubmitStub: sinon.SinonSpy; | ||
|
||
beforeEach(async () => { | ||
form = await fixture<HTMLFormElement>(html`<form @submit="${submitCallback}"> | ||
<input> | ||
</form>`); | ||
formSubmitStub = sinon.stub(form, 'submit').callsFake(() => {}); | ||
submitted = false; | ||
}); | ||
|
||
afterEach(fixtureCleanup); | ||
afterEach(() => { | ||
sinon.restore(); | ||
fixtureCleanup(); | ||
}); | ||
|
||
it('will submit a form that is valid', async () => { | ||
submit(form); | ||
await aTimeout(0); | ||
expect(submitted).to.be.true; | ||
// form.submit() is called | ||
expect(formSubmitStub.callCount).to.equal(1); | ||
}); | ||
|
||
it('will not submit a form that is invalid', async () => { | ||
it('will not fire the submit event for a form that is invalid', async () => { | ||
const input = form.querySelector<HTMLInputElement>('input')!; | ||
input.required = true; | ||
submit(form); | ||
expect(submitted).to.be.false; | ||
}); | ||
|
||
it('will not submit a form that is invalid', async () => { | ||
const input = form.querySelector<HTMLInputElement>('input')!; | ||
input.required = true; | ||
submit(form); | ||
|
||
expect(formSubmitStub.callCount).to.equal(0); | ||
}); | ||
|
||
it('will not submit a form when the submit event is `defaultPrevented`', async () => { | ||
form = await fixture<HTMLFormElement>(html`<form @submit="${submitCallbackPrevented}"> | ||
<input> | ||
</form>`); | ||
|
||
formSubmitStub = sinon.stub(form, 'submit').callsFake(() => {}); | ||
|
||
submit(form); | ||
|
||
expect(formSubmitStub.callCount).to.equal(0); | ||
}); | ||
|
||
|
||
}); |
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 |
---|---|---|
@@ -1,7 +1,7 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"rootDir": "src", | ||
"outDir": "lib" | ||
} | ||
} | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"rootDir": "src", | ||
"outDir": "lib" | ||
} | ||
} |