Skip to content

Commit

Permalink
feat(input): elementinternals implementation for input component (#220)
Browse files Browse the repository at this point in the history
* feat(input): input as native form elements

* chore(input): add element-internals-polyfill to support safari

* refactor(input): validation state with elementinternals

* refactor(input): elementinternals for input

* feat(input): elementinternals implementation for input

Co-authored-by: Levent Anil Ozen <[email protected]>
  • Loading branch information
muratcorlu and Levent Anil Ozen authored Sep 28, 2022
1 parent e65cb90 commit 8d3d693
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 89 deletions.
96 changes: 47 additions & 49 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"@floating-ui/dom": "^0.5.4",
"@fontsource/rubik": "^4.5.9",
"@lit-labs/react": "^1.0.7",
"@open-wc/form-control": "^0.4.1",
"@open-wc/form-helpers": "^0.1.2",
"element-internals-polyfill": "^1.1.11",
"lit": "^2.2.3"
},
"engines": {
Expand Down
6 changes: 6 additions & 0 deletions src/components/button/bl-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ export default class BlButton extends LitElement {
@property({ type: String })
target?: TargetType = '_self';

/**
* Sets the type of the button. Set `submit` to use button as the submitter of parent form.
*/
@property({ type: String })
type: 'submit' | null;

/**
* Fires when button clicked
*/
Expand Down
8 changes: 4 additions & 4 deletions src/components/input/bl-input.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ input:focus {
--bl-input-border-color: var(--bl-color-primary);
}

input:focus ~ bl-icon {
--bl-input-icon-color: var(--bl-color-primary);
}

:host([label-fixed]) bl-icon {
top: calc(var(--bl-input-padding-vertical) + var(--bl-size-m));
}

input:focus ~ bl-icon {
--bl-input-icon-color: var(--bl-color-primary);
}

:host ::placeholder {
color: var(--bl-color-content-tertiary);
}
Expand Down
33 changes: 32 additions & 1 deletion src/components/input/bl-input.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const LabelStylesTemplate = args => html`

Input component is the component for taking text input from user.

<bl-alert variant="warning" icon caption="Note">Inline styles in examples are only for **demo purposes**. Use regular CSS classes or tag selectors to set styles.</bl-alert>
<bl-alert variant="warning" icon>Inline styles in examples are only for **demo purposes**. Use regular CSS classes or tag selectors to set styles.</bl-alert>

## Basic Usage

Expand Down Expand Up @@ -135,6 +135,9 @@ If you want to use always it on top of the input, then you can use `label-fixed`
<Story name="Input Without Label" args={{ placeholder: 'Enter Your Name' }}>
{SingleInputTemplate.bind({})}
</Story>
<Story name="Input with value" args={{ label: 'Your name', placeholder: 'Name Surname', value: 'Random User' }}>
{SingleInputTemplate.bind({})}
</Story>
</Canvas>

## Input Help Text
Expand Down Expand Up @@ -200,6 +203,34 @@ Inputs have 2 size options: `medium` and `large`. `medium` size is default and i
</Story>
</Canvas>

## Using within a form

Input component uses [ElementInternals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) to associate with it's parent form automatically. When you use `bl-input` within a form with a `name` attribute, input's value will be automatically set parent form's FormData. Check the example below:

```html
<form novalidate>
<bl-input name="name" label="Your Name"></bl-input>
<bl-input name="age" type="number" required min="18" label="Age"></bl-input>

<button type="submit">Submit</button>
</form>

<script>
document.querySelector('form').addEventListener('submit', (event) => {
event.preventDefault();
const formData = new FormData(event.target);
for (const [key, value] of formData.entries()) {
console.log(key, value);
}
});
</script>
```

When you run this example and submit the form, you'll see key/value pairs of the inputs in the console.

<bl-alert icon>If user presses `Enter` key in an input inside a form, this will trigger submit of the form. This behaviour mimics the native input behaviour.</bl-alert>

## Reference

<ArgsTable of="bl-input" />
65 changes: 64 additions & 1 deletion src/components/input/bl-input.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assert, expect, fixture, oneEvent, html } from '@open-wc/testing';
import { assert, expect, fixture, oneEvent, html, elementUpdated } from '@open-wc/testing';
import BlInput from './bl-input';

describe('bl-input', () => {
Expand Down Expand Up @@ -73,6 +73,11 @@ describe('bl-input', () => {
const el = await fixture<BlInput>(
html`<bl-input required invalid-text="${errorMessage}"></bl-input>`
);

el.reportValidity();

await elementUpdated(el);

const errorMessageElement = <HTMLParagraphElement>(
el.shadowRoot?.querySelector('.invalid-text')
);
Expand All @@ -86,6 +91,9 @@ describe('bl-input', () => {
it('should show error when reportValidity method called', async () => {
const el = await fixture<BlInput>(html`<bl-input required></bl-input>`);
el.reportValidity();

await elementUpdated(el);

expect(el.validity.valid).to.be.false;
const errorMessageElement = <HTMLParagraphElement>(
el.shadowRoot?.querySelector('.invalid-text')
Expand Down Expand Up @@ -125,4 +133,59 @@ describe('bl-input', () => {
expect(ev.detail).to.be.equal('some value');
});
});

describe('form integration', () => {
it('should show errors when parent form is submitted', async () => {
const form = await fixture<HTMLFormElement>(html`<form novalidate>
<bl-input required></bl-input>
</form>`);

const blInput = form.querySelector<BlInput>('bl-input');

form.addEventListener('submit', e => e.preventDefault());

form.dispatchEvent(new SubmitEvent('submit', {cancelable: true}));

await elementUpdated(form);

const errorMessageElement = <HTMLParagraphElement>(
blInput?.shadowRoot?.querySelector('.invalid-text')
);

expect(blInput?.validity.valid).to.be.false;

expect(errorMessageElement).to.exist;

});

it('should submit parent form when pressed Enter key', async () => {
const form = await fixture<HTMLFormElement>(html`<form novalidate>
<bl-input name="user" value="name"></bl-input>
<button type="submit">Submit</button>
</form>`);

const blInput = form.querySelector<BlInput>('bl-input');

await elementUpdated(form);

const submitEvent = new Promise(resolve => {
function listener(ev: SubmitEvent) {
ev.preventDefault();
resolve(ev);
form.removeEventListener('submit', listener);
}
form.addEventListener('submit', listener);
});

const enterEvent = new KeyboardEvent('keydown', {
code: 'Enter',
cancelable: true
});

blInput?.dispatchEvent(enterEvent);

const ev = await submitEvent;
expect(ev).to.exist;
});
});
});
Loading

0 comments on commit 8d3d693

Please sign in to comment.