Skip to content

Commit

Permalink
Merge pull request #2 from open-wc/documentation
Browse files Browse the repository at this point in the history
docs(all) add initial readmes for components and root
  • Loading branch information
calebdwilliams authored Jan 6, 2022
2 parents 3a6b75b + f6d7da2 commit d77c19c
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 9 deletions.
9 changes: 9 additions & 0 deletions README.md
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.

176 changes: 176 additions & 0 deletions packages/form-control/README.md
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;
}
};
```
122 changes: 122 additions & 0 deletions packages/form-helpers/README.md
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']
// }
```
39 changes: 36 additions & 3 deletions packages/form-helpers/tests/submit.test.ts
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);
});


});
12 changes: 6 additions & 6 deletions packages/form-helpers/tsconfig.json
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"
}
}

0 comments on commit d77c19c

Please sign in to comment.