feat: use markdown javascript (mdjs) for documentation
Co-authored-by: CubLion <alex.ghiu@ing.com>
This commit is contained in:
parent
f15de48fbf
commit
bcd074d1fb
196 changed files with 7740 additions and 11774 deletions
|
|
@ -8,6 +8,7 @@ module.exports = {
|
|||
'**/test-node/**/*.js',
|
||||
'**/demo/**/*.js',
|
||||
'**/stories/**/*.js',
|
||||
'**/docs/**/*.js',
|
||||
'**/*.config.js',
|
||||
],
|
||||
rules: {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"line_length": -1
|
||||
},
|
||||
"no-trailing-punctuation": {
|
||||
"punctuation": ".,;:!。,;:!"
|
||||
"punctuation": ".,;。,;:!"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,26 @@ const path = require('path');
|
|||
|
||||
module.exports = {
|
||||
stories: [
|
||||
'../packages/*/stories/*.stories.{js,mdx}',
|
||||
'../packages/helpers/*/stories/*.stories.{js,mdx}',
|
||||
'../demo/stories/*.stories.{js,mdx}',
|
||||
'../packages/*/README.md',
|
||||
'../packages/*/docs/*.md',
|
||||
'../docs/README.md',
|
||||
'../docs/**/*.md',
|
||||
'../README.md',
|
||||
'../demo/README.md',
|
||||
'../demo/docs/*.md',
|
||||
'../packages/helpers/README.md',
|
||||
'../packages/helpers/*/README.md',
|
||||
],
|
||||
addons: [
|
||||
// order of tabs in addons panel
|
||||
'storybook-prebuilt/addon-actions/register.js',
|
||||
'storybook-prebuilt/addon-knobs/register.js',
|
||||
'storybook-prebuilt/addon-a11y/register.js',
|
||||
'storybook-prebuilt/addon-docs/register.js',
|
||||
// no tab in addons panel (e.g. load order does not matter here)
|
||||
'storybook-prebuilt/addon-backgrounds/register.js',
|
||||
'storybook-prebuilt/addon-links/register.js',
|
||||
'storybook-prebuilt/addon-viewport/register.js',
|
||||
],
|
||||
addons: [
|
||||
'storybook-prebuilt/addon-docs/register.js',
|
||||
|
|
|
|||
193
README.md
193
README.md
|
|
@ -1,5 +1,11 @@
|
|||
# Lion Web Components
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Intro/Lion Web Components',
|
||||
};
|
||||
```
|
||||
|
||||
Lion web components is a set of highly performant, accessible and flexible Web Components.
|
||||
They provide an unopinionated, white label layer that can be extended to your own layer of components.
|
||||
|
||||
|
|
@ -13,6 +19,32 @@ We do have a [live Storybook](http://lion-web-components.netlify.com) which show
|
|||
|
||||
**Please note:** This project uses Yarn [Workspaces](https://classic.yarnpkg.com/en/docs/workspaces). If you want to run all demos locally you need to get [Yarn](https://classic.yarnpkg.com/en/docs/install) and install all depencies by executing `yarn install`.
|
||||
|
||||
## Features
|
||||
|
||||
- pure es modules
|
||||
- exposes functions/classes and web components
|
||||
- provides pure functionality
|
||||
- fully accessible
|
||||
- built to be extended
|
||||
|
||||
> Note: These demos may look a little bland but that is on purpose. They only come with functional stylings.
|
||||
> This makes sense as the main use case is to extend those components and if you do you do not want to override existing stylings.
|
||||
|
||||
## Systems
|
||||
|
||||
Lion web components is logically organized in groups of systems.
|
||||
|
||||
| System | Description |
|
||||
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Forms](?path=/docs/forms-intro--page) | A system that lets you make complex forms with ease. Add validation, translations and use a wide range of pre made form controls. |
|
||||
| [Buttons](?path=/docs/buttons-intro--page) | Every application needs buttons to allow interactions. These web components bring common UX patterns while still full integrated with (native) forms. |
|
||||
| [Overlays](?path=/docs/overlays-intro--page) | If something needs to overlay content this is your place. Dialogs, Tooltips and a full overlay system to build more if needed |
|
||||
| [Navigation](?path=/docs/navigation-intro--page) | Components which are used to guide users |
|
||||
| [Localize](?path=/docs/localize-intro--page) | Localize text, numbers, dates and a way to store/fetch these data within web components |
|
||||
| [Icons](?path=/docs/icons-intro--page) | Loading and displaying icons |
|
||||
| [Others](?path=/docs/others-intro--page) | Features not fitting any other category |
|
||||
| [Helpers](?path=/docs/helpers-intro--page) | Helpers to make your and your life easier |
|
||||
|
||||
## How to install
|
||||
|
||||
```bash
|
||||
|
|
@ -24,42 +56,42 @@ npm i @lion/<package-name>
|
|||
The accessibility column indicates whether the functionality is accessible in its core. Aspects like styling and content determine actual accessibility in usage.
|
||||
|
||||
| Package | Version | Description | Accessibility |
|
||||
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | -------------------------- |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | -------------------------- |
|
||||
| **-- Forms --** | | | ✔️ |
|
||||
| [form](?path=/docs/forms-form-overview--page) | [](https://www.npmjs.com/package/@lion/form) | Wrapper for multiple form elements | ✔️ |
|
||||
| [field](?path=/docs/forms-system-overview--page) | [](https://www.npmjs.com/package/@lion/field) | Base Class for all inputs | ✔️ |
|
||||
| [fieldset](?path=/docs/forms-fieldset-overview--page) | [](https://www.npmjs.com/package/@lion/fieldset) | Group for form inputs | ✔️ |
|
||||
| [validate](?path=/docs/form-system-validate-system--page) | [](https://www.npmjs.com/package/@lion/validate) | Validation for form components | n/a |
|
||||
| [checkbox](?path=/docs/forms-checkbox--default-story) | [](https://www.npmjs.com/package/@lion/checkbox) | Checkbox form element | ✔️ |
|
||||
| [checkbox-group](?path=/docs/forms-checkbox-group--default-story) | [](https://www.npmjs.com/package/@lion/checkbox-group) | Group of checkboxes | ✔️ |
|
||||
| [input](?path=/docs/forms-input--default-story) | [](https://www.npmjs.com/package/@lion/input) | Input element for strings | ✔️ |
|
||||
| [input-amount](?path=/docs/forms-input-amount--default-story) | [](https://www.npmjs.com/package/@lion/input-amount) | Input element for amounts | ✔️ |
|
||||
| [input-date](?path=/docs/forms-input-date--default-story) | [](https://www.npmjs.com/package/@lion/input-date) | Input element for dates | ✔️ |
|
||||
| [input-datepicker](?path=/docs/forms-input-datepicker--default-story) | [](https://www.npmjs.com/package/@lion/input-datepicker) | Input element for dates with a datepicker | ✔️ |
|
||||
| [input-email](?path=/docs/forms-input-email--default-story) | [](https://www.npmjs.com/package/@lion/input-email) | Input element for e-mails | ✔️ |
|
||||
| [input-iban](?path=/docs/forms-input-iban--default-story) | [](https://www.npmjs.com/package/@lion/input-iban) | Input element for IBANs | ✔️ |
|
||||
| [input-range](?path=/docs/forms-input-range--default-story) | [](https://www.npmjs.com/package/@lion/input-range) | Input element for a range of values | ✔️ |
|
||||
| [radio](?path=/docs/forms-radio--default-story) | [](https://www.npmjs.com/package/@lion/radio) | Radio from element | ✔️ |
|
||||
| [radio-group](?path=/docs/forms-radio-group--default-story) | [](https://www.npmjs.com/package/@lion/radio-group) | Group of radios | ✔️ |
|
||||
| [select](?path=/docs/forms-select--default-story) | [](https://www.npmjs.com/package/@lion/select) | Simple native dropdown element | ✔️ |
|
||||
| [textarea](?path=/docs/forms-textarea--default-story) | [](https://www.npmjs.com/package/@lion/textarea) | Multiline text input | ✔️ |
|
||||
| **-- Buttons --** | | | |
|
||||
| [button](./packages/button) | [](https://www.npmjs.com/package/@lion/button) | Button | ✔️ |
|
||||
| [switch](./packages/switch) | [](https://www.npmjs.com/package/@lion/switch) | Switch | ✔️ |
|
||||
| **-- Forms --** | | | |
|
||||
| [form](./packages/form) | [](https://www.npmjs.com/package/@lion/form) | Wrapper for multiple form elements | ✔️ |
|
||||
| [field](./packages/field) | [](https://www.npmjs.com/package/@lion/field) | Base Class for all inputs | ✔️ |
|
||||
| [fieldset](./packages/fieldset) | [](https://www.npmjs.com/package/@lion/fieldset) | Group for form inputs | ✔️ |
|
||||
| [validate](./packages/validate) | [](https://www.npmjs.com/package/@lion/validate) | Validation for form components | n/a |
|
||||
| [checkbox](./packages/checkbox) | [](https://www.npmjs.com/package/@lion/checkbox) | Checkbox form element | ✔️ |
|
||||
| [checkbox-group](./packages/checkbox-group) | [](https://www.npmjs.com/package/@lion/checkbox-group) | Group of checkboxes | ✔️ |
|
||||
| [input](./packages/input) | [](https://www.npmjs.com/package/@lion/input) | Input element for strings | ✔️ |
|
||||
| [input-amount](./packages/input-amount) | [](https://www.npmjs.com/package/@lion/input-amount) | Input element for amounts | ✔️ |
|
||||
| [input-date](./packages/input-date) | [](https://www.npmjs.com/package/@lion/input-date) | Input element for dates | ✔️ |
|
||||
| [input-datepicker](./packages/input-datepicker) | [](https://www.npmjs.com/package/@lion/input-datepicker) | Input element for dates with a datepicker | ✔️ |
|
||||
| [input-email](./packages/input-email) | [](https://www.npmjs.com/package/@lion/input-email) | Input element for e-mails | ✔️ |
|
||||
| [input-iban](./packages/input-iban) | [](https://www.npmjs.com/package/@lion/input-iban) | Input element for IBANs | ✔️ |
|
||||
| [input-range](./packages/input-range) | [](https://www.npmjs.com/package/@lion/input-range) | Input element for a range of values | ✔️ |
|
||||
| [radio](./packages/radio) | [](https://www.npmjs.com/package/@lion/radio) | Radio from element | ✔️ |
|
||||
| [radio-group](./packages/radio-group) | [](https://www.npmjs.com/package/@lion/radio-group) | Group of radios | ✔️ |
|
||||
| [select](./packages/select) | [](https://www.npmjs.com/package/@lion/select) | Simple native dropdown element | ✔️ |
|
||||
| [textarea](./packages/textarea) | [](https://www.npmjs.com/package/@lion/textarea) | Multiline text input | ✔️ |
|
||||
| [button](?path=/docs/buttons-button--default-story) | [](https://www.npmjs.com/package/@lion/button) | Button | ✔️ |
|
||||
| [switch](?path=/docs/buttons-switch--default-off) | [](https://www.npmjs.com/package/@lion/switch) | Switch | ✔️ |
|
||||
| **-- Overlays --** | | | |
|
||||
| [overlays](./packages/overlays) | [](https://www.npmjs.com/package/@lion/overlays) | Overlay System | ✔️ |
|
||||
| [dialog](./packages/dialog) | [](https://www.npmjs.com/package/@lion/dialog) | Dialog element | ✔️ |
|
||||
| [tooltip](./packages/tooltip) | [](https://www.npmjs.com/package/@lion/tooltip) | Tooltip element | [#175][i175] |
|
||||
| [overlays](?path=/docs/overlays-system-overview--default-story) | [](https://www.npmjs.com/package/@lion/overlays) | Overlay System | ✔️ |
|
||||
| [dialog](?path=/docs/overlays-dialog--default-story) | [](https://www.npmjs.com/package/@lion/dialog) | Dialog element | ✔️ |
|
||||
| [tooltip](?path=/docs/overlays-tooltip--default-story) | [](https://www.npmjs.com/package/@lion/tooltip) | Tooltip element | [#175][i175] |
|
||||
| **-- Icons --** | | | |
|
||||
| [icon](./packages/icon) | [](https://www.npmjs.com/package/@lion/icon) | Display our svg icons | [#173][i173], [#172][i172] |
|
||||
| [icon](?path=/docs/icons-icon--default-story) | [](https://www.npmjs.com/package/@lion/icon) | Display our svg icons | [#173][i173], [#172][i172] |
|
||||
| **-- Navigation --** | | | |
|
||||
| [steps](./packages/steps) | [](https://www.npmjs.com/package/@lion/steps) | Multi Step System | n/a |
|
||||
| [tabs](./packages/tabs) | [](https://www.npmjs.com/package/@lion/tabs) | Move between a small number of equally important views | n/a |
|
||||
| [steps](?path=/docs/navigation-steps--default-story) | [](https://www.npmjs.com/package/@lion/steps) | Multi Step System | n/a |
|
||||
| [tabs](?path=/docs/navigation-tabs--default-story) | [](https://www.npmjs.com/package/@lion/tabs) | Move between a small number of equally important views | n/a |
|
||||
| **-- Others --** | | | |
|
||||
| [core](./packages/core) | [](https://www.npmjs.com/package/@lion/core) | Core System (exports LitElement, lit-html) | n/a |
|
||||
| [calendar](./packages/calendar) | [](https://www.npmjs.com/package/@lion/calendar) | Standalone calendar | [#195][i195], [#194][i194] |
|
||||
| [localize](./packages/localize) | [](https://www.npmjs.com/package/@lion/localize) | Localize and translate your application/components | n/a |
|
||||
| [ajax](./packages/ajax) | [](https://www.npmjs.com/package/@lion/ajax) | Fetching data via ajax request | n/a |
|
||||
| [core](?path=/docs/deep-dives-core--page) | [](https://www.npmjs.com/package/@lion/core) | Core System (exports LitElement, lit-html) | n/a |
|
||||
| [localize](?path=/docs/localize-localize--default-story) | [](https://www.npmjs.com/package/@lion/localize) | Localize and translate your application/components | n/a |
|
||||
| [ajax](?path=/docs/others-ajax--default-story) | [](https://www.npmjs.com/package/@lion/ajax) | Fetching data via ajax request | n/a |
|
||||
| [calendar](?path=/docs/calendar--default-story) | [](https://www.npmjs.com/package/@lion/calendar) | Standalone calendar | [#195][i195], [#194][i194] |
|
||||
|
||||
## How to use
|
||||
|
||||
|
|
@ -135,100 +167,6 @@ Check out our [coding guidelines](./docs/README.md) for more detailed informatio
|
|||
Lion Web Components are only as good as its contributions.
|
||||
Read our [contribution guide](./CONTRIBUTING.md) and feel free to enhance/improve Lion. We keep feature requests closed while we're not working on them.
|
||||
|
||||
## Scoped elements
|
||||
|
||||
The [CustomElementRegistry](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry) provides methods for registering custom elements. One of the limitations of working with this global registry is that multiple versions of the same element cannot co-exist. This causes bottlenecks in software delivery that should be managed by the teams and complex build systems. [Scoped Custom Element Registries](https://github.com/w3c/webcomponents/issues/716) is a proposal that will solve the problem. Since this functionality won't be available (especially not cross browser) anytime soon, we've adopted [OpenWC's Scoped Elements](https://open-wc.org/scoped-element).
|
||||
|
||||
Whenever a lion component uses composition (meaning it uses another lion component inside), we
|
||||
apply ScopedElementsMixin to make sure it uses the right version of this internal component.
|
||||
|
||||
```js
|
||||
import { ScopedElementsMixin, LitElement, html } from '@lion/core';
|
||||
|
||||
import { LionInput } from '@lion/input';
|
||||
import { LionButton } from '@lion/button';
|
||||
|
||||
class MyElement extends ScopedElementsMixin(LitElement) {
|
||||
static get scopedElements() {
|
||||
return {
|
||||
'lion-input': LionInput,
|
||||
'lion-button': LionButton,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<lion-input label="Greeting" name="greeting" .modelValue=${'Hello world'}></lion-input>
|
||||
<lion-button>Save</lion-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Query selectors
|
||||
|
||||
Since Scoped Elements changes tagnames under the hood, a tagname querySelector should be written
|
||||
like this:
|
||||
|
||||
```js
|
||||
this.querySelector(this.constructor.getScopedTagName('lion-input'));
|
||||
```
|
||||
|
||||
### CSS selectors
|
||||
|
||||
Avoid tagname css selectors (we already avoid query selectors internally in lion, but just be aware
|
||||
that a selector like `lion-input {...}` will stop working ).
|
||||
|
||||
### Edge cases
|
||||
|
||||
Sometimes we need to render parts of a template to light dom for [accessibility](https://wicg.github.io/aom/explainer.html). For instance we render a node via lit-html that we append to the host element, so
|
||||
it gets slotted in the right position.
|
||||
In this case, we should also make sure that we also scope the rendered element.
|
||||
|
||||
We can do this as follows:
|
||||
|
||||
```js
|
||||
_myLightTemplate() {
|
||||
return html`
|
||||
This template may be overridden by a Subclasser.
|
||||
Even I don't end up in shadow root, I need to be scoped to constructor.scopedElements as well.
|
||||
<div>
|
||||
<lion-button>True</lion-button>
|
||||
<lion-input label="xyz"></lion-input>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
__getLightDomNode() {
|
||||
const renderParent = document.createElement('div');
|
||||
this.constructor.render(this._myLightTemplate(), renderParent, {
|
||||
scopeName: this.localName,
|
||||
eventContext: this,
|
||||
});
|
||||
// this node will be appended to the host
|
||||
return renderParent.firstElementChild;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.appendChild(this.__getLightDomNode());
|
||||
}
|
||||
```
|
||||
|
||||
In a less complex case, we might just want to add a child node to the dom.
|
||||
|
||||
```js
|
||||
import { ScopedElementsMixin, LitElement } from '@lion/core';
|
||||
|
||||
...
|
||||
|
||||
__getLightDomNode() {
|
||||
return document.createElement(this.constructor.getScopedTagName('lion-input'));
|
||||
}
|
||||
```
|
||||
|
||||
We encourage you to have a look at [OpenWC's Scoped elements](https://open-wc.org/scoped-elements).
|
||||
|
||||
## Contact
|
||||
|
||||
Feel free to create a github issue for any feedback or questions you might have.
|
||||
|
|
@ -236,11 +174,6 @@ You can also find us on the Polymer slack in the [#lion](https://polymer.slack.c
|
|||
|
||||
You can join the Polymer slack by visiting [https://www.polymer-project.org/slack-invite](https://www.polymer-project.org/slack-invite).
|
||||
|
||||
## Support and issues
|
||||
|
||||
As stated above "support and issues time" is currently rather limited: feel free to open a discussion.
|
||||
However, we can not guarantee any response times.
|
||||
|
||||
[i172]: https://github.com/ing-bank/lion/issues/172
|
||||
[i173]: https://github.com/ing-bank/lion/issues/173
|
||||
[i175]: https://github.com/ing-bank/lion/issues/175
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Story, Meta, html, Preview } from '@open-wc/demoing-storybook';
|
||||
|
||||
<Meta title="Intro/Announcement" />
|
||||
|
||||
# ING open sources Lion: A library of performant, accessible & flexible Web Components
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Intro/Announcement',
|
||||
};
|
||||
```
|
||||
|
||||
**TL;DR:** Web development can be hard, whether you're making your own components, implementing Design Systems, support many different browsers, incorporating accessibility, or adding third party dependencies. Lion aims to make your life a little bit easier, by taking the groundwork of feature complete, accessible, performant, and framework agnostic components out of your hands! Check out the repo at [ing-bank/lion](https://github.com/ing-bank/lion).
|
||||
|
||||
As some of you may already know, ING has a long and rich history of building Web Components, starting out with the Polymer library, all the way up to the recently released LitElement.
|
||||
|
|
@ -51,6 +53,7 @@ Or, alternatively
|
|||
Does Lea's story feel relatable?
|
||||
|
||||
It highlights multiple common issues in modern web development:
|
||||
|
||||
- Finding and adding a dependency is a big deal
|
||||
- Picking something because of looks alone might not always be the best choice
|
||||
- Adjusting behavior or styling can be tough to maintain
|
||||
|
|
@ -71,15 +74,17 @@ Now stop imagining, and look at Lion 🦁
|
|||
Lion is a white-label, open-source, framework-agnostic component library, and can be the foundation for your codified in-house Design System. Aligning on design and usability is already a tough challenge, with Lion, we hope to take some of the work out of your hands.
|
||||
|
||||
### White Label
|
||||
|
||||
Lion is a core package of white label Web Components. What this means is that the core components only consist of very minimal styling, yet has all the fundamental functionalities. White label products are often created so others can easily rebrand them with their own visual identity. This is great because it means that everyone can use the core functionalities of our components while bringing their own branding or Design System because surprisingly, not everyone loves orange.
|
||||
|
||||
And this is exactly what we at ING do as well. Our very own ing-web components extend the Lion components and apply our own ING visual identity which mostly is a thin layer on top of Lion.
|
||||
|
||||
Check out the [Lion demo's](http://lion-web-components.netlify.com/). Looks plain, doesn't it? And now compare Lion to ing-web:
|
||||
|
||||

|
||||

|
||||
|
||||
### 🎯 Focus
|
||||
|
||||
Lion was designed with a focus on global usage and reusability in mind. And as such, the following features were incorporated right from the start:
|
||||
|
||||
♻️ Reusability - Our components are meant to be distributed and used on a global scale
|
||||
|
|
@ -102,7 +107,7 @@ Lion is built from the ground up to allow for accessibility and extendability as
|
|||
|
||||
> Everyone should use web components but not everyone should write them
|
||||
|
||||
*- [Erik Kroes](https://twitter.com/erikkroes)*
|
||||
_- [Erik Kroes](https://twitter.com/erikkroes)_
|
||||
|
||||
## 🙋 Join us!
|
||||
|
||||
|
|
@ -153,7 +158,7 @@ export class LeaTabs extends LionTabs {
|
|||
super.styles,
|
||||
css`
|
||||
/* my stylings */
|
||||
`
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -177,7 +182,9 @@ import { LitElement, html, css } from 'lit-element';
|
|||
|
||||
export class LeaTab extends LitElement {
|
||||
static get styles() {
|
||||
return css`/* my stylings */`;
|
||||
return css`
|
||||
/* my stylings */
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
253
demo/docs/20-lea-tabs.md
Normal file
253
demo/docs/20-lea-tabs.md
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
# lea Tabs
|
||||
|
||||
> This is not a real implementation!
|
||||
>
|
||||
> It is an example of how you can reuse the functionality of Lion to create your own Style System
|
||||
|
||||
`lea-tabs` implements tabs view to allow users to quickly move between a small number of equally important views.
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import { LitElement } from '@lion/core';
|
||||
|
||||
import '../lea-tabs.js';
|
||||
import '../lea-tab.js';
|
||||
import '../lea-tab-panel.js';
|
||||
|
||||
export default {
|
||||
title: 'Intro/Tabs Example',
|
||||
};
|
||||
```
|
||||
|
||||
```js preview-story
|
||||
export const main = () => html`
|
||||
<lea-tabs>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
</lea-tabs>
|
||||
`;
|
||||
```
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
npm i --save @lion/tabs;
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```js
|
||||
import '@lion/tabs/lea-tabs.js';
|
||||
```
|
||||
|
||||
```html
|
||||
<lea-tabs>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
</lea-tabs>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Selected Index
|
||||
|
||||
You can set the `selectedIndex` to select a certain tab.
|
||||
|
||||
```js preview-story
|
||||
export const selectedIndex = () => html`
|
||||
<lea-tabs .selectedIndex=${1}>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
</lea-tabs>
|
||||
`;
|
||||
```
|
||||
|
||||
### Slots Order
|
||||
|
||||
The tab and panel slots are ordered by DOM order.
|
||||
|
||||
This means you can switch the grouping in your `lea-tabs` from tab + panel to all tabs first or all panels first.
|
||||
|
||||
```js preview-story
|
||||
export const slotsOrder = () => html`
|
||||
<lea-tabs>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
</lea-tabs>
|
||||
`;
|
||||
```
|
||||
|
||||
### Distribute New Elements
|
||||
|
||||
Below, we demonstrate on how you could dynamically add new tab + panels.
|
||||
|
||||
```js preview-story
|
||||
export const distributeNewElements = () => {
|
||||
const tagName = 'lea-tabs-experimental';
|
||||
if (!customElements.get(tagName)) {
|
||||
customElements.define(
|
||||
tagName,
|
||||
class extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
__collection: { type: Array },
|
||||
};
|
||||
}
|
||||
render() {
|
||||
return html`
|
||||
<h3>Append</h3>
|
||||
<button @click="${this.__handleAppendClick}">
|
||||
Append
|
||||
</button>
|
||||
<lea-tabs id="appendTabs">
|
||||
<lea-tab slot="tab">tab 1</lea-tab>
|
||||
<lea-tab-panel slot="panel">panel 1</lea-tab-panel>
|
||||
<lea-tab slot="tab">tab 2</lea-tab>
|
||||
<lea-tab-panel slot="panel">panel 2</lea-tab-panel>
|
||||
</lea-tabs>
|
||||
<hr />
|
||||
<h3>Push</h3>
|
||||
<button @click="${this.__handlePushClick}">
|
||||
Push
|
||||
</button>
|
||||
<lea-tabs id="pushTabs">
|
||||
<lea-tab slot="tab">tab 1</lea-tab>
|
||||
<lea-tab-panel slot="panel">panel 1</lea-tab-panel>
|
||||
<lea-tab slot="tab">tab 2</lea-tab>
|
||||
<lea-tab-panel slot="panel">panel 2</lea-tab-panel>
|
||||
${this.__collection.map(
|
||||
item => html`
|
||||
<lea-tab slot="tab">${item.button}</lea-tab>
|
||||
<lea-tab-panel slot="panel">${item.panel}</lea-tab-panel>
|
||||
`,
|
||||
)}
|
||||
</lea-tabs>
|
||||
`;
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.__collection = [];
|
||||
}
|
||||
__handleAppendClick() {
|
||||
const tabsElement = this.shadowRoot.querySelector('#appendTabs');
|
||||
const c = 2;
|
||||
const n = Math.floor(tabsElement.children.length / 2);
|
||||
for (let i = n + 1; i < n + c; i += 1) {
|
||||
const tab = document.createElement('lea-tab');
|
||||
tab.setAttribute('slot', 'tab');
|
||||
tab.innerText = `tab ${i}`;
|
||||
const panel = document.createElement('lea-tab-panel');
|
||||
panel.setAttribute('slot', 'panel');
|
||||
panel.innerText = `panel ${i}`;
|
||||
tabsElement.append(tab);
|
||||
tabsElement.append(panel);
|
||||
}
|
||||
}
|
||||
__handlePushClick() {
|
||||
const tabsElement = this.shadowRoot.querySelector('#pushTabs');
|
||||
const i = Math.floor(tabsElement.children.length / 2) + 1;
|
||||
this.__collection = [
|
||||
...this.__collection,
|
||||
{
|
||||
button: `tab ${i}`,
|
||||
panel: `panel ${i}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
return html`
|
||||
<lea-tabs-experimental></lea-tabs-experimental>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
One way is by creating the DOM elements and appending them as needed.
|
||||
|
||||
Inside your `lea-tabs` extension, an example for appending nodes on a certain button click:
|
||||
|
||||
```js
|
||||
__handleAppendClick() {
|
||||
const tabsAmount = this.children.length / 2;
|
||||
const tab = document.createElement('lea-tab');
|
||||
tab.setAttribute('slot', 'tab');
|
||||
tab.innerText = `tab ${tabsAmount + 1}`;
|
||||
const panel = document.createElement('lea-tab-panel');
|
||||
panel.setAttribute('slot', 'panel');
|
||||
panel.innerText = `panel ${tabsAmount + 1}`;
|
||||
this.append(tab);
|
||||
this.append(panel);
|
||||
}
|
||||
```
|
||||
|
||||
The other way is by adding data to a Lit property where you loop over this property in your template.
|
||||
You then need to ensure this causes a re-render.
|
||||
|
||||
```js
|
||||
__handlePushClick() {
|
||||
const tabsAmount = this.children.length;
|
||||
myCollection = [
|
||||
...myCollection,
|
||||
{
|
||||
button: `tab ${tabsAmount + 1}`,
|
||||
panel: `panel ${tabsAmount + 1}`,
|
||||
},
|
||||
];
|
||||
renderMyCollection();
|
||||
}
|
||||
```
|
||||
|
||||
Make sure your template re-renders when myCollection is updated.
|
||||
|
||||
```html
|
||||
<lea-tabs id="pushTabs">
|
||||
${myCollection.map(item => html`
|
||||
<lea-tab slot="tab">${item.button}</lea-tab>
|
||||
<lea-tab-panel slot="panel">${item.panel}</lea-tab-panel>
|
||||
`)}
|
||||
</lea-tabs>
|
||||
```
|
||||
|
||||
## Rationale
|
||||
|
||||
### No separate active/focus state when using keyboard
|
||||
|
||||
We will immediately switch content as all our content comes from light dom (e.g. no latency)
|
||||
|
||||
See Note at <https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-19>
|
||||
|
||||
> It is recommended that tabs activate automatically when they receive focus as long as their
|
||||
> associated tab panels are displayed without noticeable latency. This typically requires tab
|
||||
> panel content to be preloaded.
|
||||
|
||||
### Panels are not focusable
|
||||
|
||||
Focusable elements should have a means to interact with them. Tab panels themselves do not offer any interactiveness.
|
||||
If there is a button or a form inside the tab panel then these elements get focused directly.
|
||||
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 247 KiB |
|
|
@ -1,255 +0,0 @@
|
|||
import { Story, Meta, html, Preview } from '@open-wc/demoing-storybook';
|
||||
import { LitElement } from '@lion/core';
|
||||
|
||||
import '../lea-tabs.js';
|
||||
import '../lea-tab.js';
|
||||
import '../lea-tab-panel.js';
|
||||
|
||||
<Meta title="Intro/Tabs Example" parameters={{ component: 'lea-tabs' }} />
|
||||
|
||||
# lea Tabs
|
||||
|
||||
> This is not a real implementation!
|
||||
>
|
||||
> It is an example of how you can reuse the functionality of Lion to create your own Style System
|
||||
|
||||
`lea-tabs` implements tabs view to allow users to quickly move between a small number of equally important views.
|
||||
|
||||
<Preview>
|
||||
<Story name="default">
|
||||
{html`
|
||||
<lea-tabs>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
</lea-tabs>
|
||||
`}
|
||||
</Story>
|
||||
</Preview>
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
npm i --save @lion/tabs;
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```js
|
||||
import '@lion/tabs/lea-tabs.js';
|
||||
```
|
||||
|
||||
```html
|
||||
<lea-tabs>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
</lea-tabs>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Selected Index
|
||||
|
||||
You can set the `selectedIndex` to select a certain tab.
|
||||
|
||||
<Preview>
|
||||
<Story name="Selected index">
|
||||
{html`
|
||||
<lea-tabs .selectedIndex=${1}>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
</lea-tabs>
|
||||
`}
|
||||
</Story>
|
||||
</Preview>
|
||||
|
||||
### Slots Order
|
||||
|
||||
The tab and panel slots are ordered by DOM order.
|
||||
|
||||
This means you can switch the grouping in your `lea-tabs` from tab + panel to all tabs first or all panels first.
|
||||
|
||||
<Preview>
|
||||
<Story name="Slots order">
|
||||
{html`
|
||||
<lea-tabs>
|
||||
<lea-tab slot="tab">Info</lea-tab>
|
||||
<lea-tab slot="tab">Work</lea-tab>
|
||||
<lea-tab-panel slot="panel">
|
||||
Info page with lots of information about us.
|
||||
</lea-tab-panel>
|
||||
<lea-tab-panel slot="panel">
|
||||
Work page that showcases our work.
|
||||
</lea-tab-panel>
|
||||
</lea-tabs>
|
||||
`}
|
||||
</Story>
|
||||
</Preview>
|
||||
|
||||
### Distribute New Elements
|
||||
|
||||
Below, we demonstrate on how you could dynamically add new tab + panels.
|
||||
|
||||
<Story name="Distribute new elements">
|
||||
{() => {
|
||||
const tagName = 'lea-tabs-experimental';
|
||||
if (!customElements.get(tagName)) {
|
||||
customElements.define(
|
||||
tagName,
|
||||
class extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
__collection: { type: Array },
|
||||
};
|
||||
}
|
||||
render() {
|
||||
return html`
|
||||
<h3>Append</h3>
|
||||
<button @click="${this.__handleAppendClick}">
|
||||
Append
|
||||
</button>
|
||||
<lea-tabs id="appendTabs">
|
||||
<lea-tab slot="tab">tab 1</lea-tab>
|
||||
<lea-tab-panel slot="panel">panel 1</lea-tab-panel>
|
||||
<lea-tab slot="tab">tab 2</lea-tab>
|
||||
<lea-tab-panel slot="panel">panel 2</lea-tab-panel>
|
||||
</lea-tabs>
|
||||
<hr />
|
||||
<h3>Push</h3>
|
||||
<button @click="${this.__handlePushClick}">
|
||||
Push
|
||||
</button>
|
||||
<lea-tabs id="pushTabs">
|
||||
<lea-tab slot="tab">tab 1</lea-tab>
|
||||
<lea-tab-panel slot="panel">panel 1</lea-tab-panel>
|
||||
<lea-tab slot="tab">tab 2</lea-tab>
|
||||
<lea-tab-panel slot="panel">panel 2</lea-tab-panel>
|
||||
${this.__collection.map(
|
||||
item => html`
|
||||
<lea-tab slot="tab">${item.button}</lea-tab>
|
||||
<lea-tab-panel slot="panel">${item.panel}</lea-tab-panel>
|
||||
`,
|
||||
)}
|
||||
</lea-tabs>
|
||||
`;
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.__collection = [];
|
||||
}
|
||||
__handleAppendClick() {
|
||||
const tabsElement = this.shadowRoot.querySelector('#appendTabs');
|
||||
const c = 2;
|
||||
const n = Math.floor(tabsElement.children.length / 2);
|
||||
for (let i = n + 1; i < n + c; i += 1) {
|
||||
const tab = document.createElement('lea-tab');
|
||||
tab.setAttribute('slot', 'tab');
|
||||
tab.innerText = `tab ${i}`;
|
||||
const panel = document.createElement('lea-tab-panel');
|
||||
panel.setAttribute('slot', 'panel');
|
||||
panel.innerText = `panel ${i}`;
|
||||
tabsElement.append(tab);
|
||||
tabsElement.append(panel);
|
||||
}
|
||||
}
|
||||
__handlePushClick() {
|
||||
const tabsElement = this.shadowRoot.querySelector('#pushTabs');
|
||||
const i = Math.floor(tabsElement.children.length / 2) + 1;
|
||||
this.__collection = [
|
||||
...this.__collection,
|
||||
{
|
||||
button: `tab ${i}`,
|
||||
panel: `panel ${i}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
return html`
|
||||
<lea-tabs-experimental></lea-tabs-experimental>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
One way is by creating the DOM elements and appending them as needed.
|
||||
|
||||
Inside your `lea-tabs` extension, an example for appending nodes on a certain button click:
|
||||
|
||||
```js
|
||||
__handleAppendClick() {
|
||||
const tabsAmount = this.children.length / 2;
|
||||
const tab = document.createElement('lea-tab');
|
||||
tab.setAttribute('slot', 'tab');
|
||||
tab.innerText = `tab ${tabsAmount + 1}`;
|
||||
const panel = document.createElement('lea-tab-panel');
|
||||
panel.setAttribute('slot', 'panel');
|
||||
panel.innerText = `panel ${tabsAmount + 1}`;
|
||||
this.append(tab);
|
||||
this.append(panel);
|
||||
}
|
||||
```
|
||||
|
||||
The other way is by adding data to a Lit property where you loop over this property in your template.
|
||||
You then need to ensure this causes a re-render.
|
||||
|
||||
```js
|
||||
__handlePushClick() {
|
||||
const tabsAmount = this.children.length;
|
||||
myCollection = [
|
||||
...myCollection,
|
||||
{
|
||||
button: `tab ${tabsAmount + 1}`,
|
||||
panel: `panel ${tabsAmount + 1}`,
|
||||
},
|
||||
];
|
||||
renderMyCollection();
|
||||
}
|
||||
```
|
||||
|
||||
Make sure your template re-renders when myCollection is updated.
|
||||
|
||||
```html
|
||||
<lea-tabs id="pushTabs">
|
||||
${myCollection.map(item => html`
|
||||
<lea-tab slot="tab">${item.button}</lea-tab>
|
||||
<lea-tab-panel slot="panel">${item.panel}</lea-tab-panel>
|
||||
`)}
|
||||
</lea-tabs>
|
||||
```
|
||||
|
||||
## Rationale
|
||||
|
||||
### No separate active/focus state when using keyboard
|
||||
|
||||
We will immediately switch content as all our content comes from light dom (e.g. no latency)
|
||||
|
||||
See Note at <https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-19>
|
||||
|
||||
> It is recommended that tabs activate automatically when they receive focus as long as their
|
||||
> associated tab panels are displayed without noticeable latency. This typically requires tab
|
||||
> panel content to be preloaded.
|
||||
|
||||
### Panels are not focusable
|
||||
|
||||
Focusable elements should have a means to interact with them. Tab panels themselves do not offer any interactiveness.
|
||||
If there is a button or a form inside the tab panel then these elements get focused directly.
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
# Coding Guidelines
|
||||
|
||||
First be sure to understand our [definitions](./definitions.md).
|
||||
```js script
|
||||
export default {
|
||||
title: 'Guidelines/Intro',
|
||||
};
|
||||
```
|
||||
|
||||
- [Guidelines for Styling](./guidelines-styling.md)
|
||||
First be sure to understand our [definitions](?path=/docs/guidelines-definitions--page).
|
||||
|
||||
- [Guidelines for Styling](?path=/docs/guidelines-styling--page)
|
||||
- [Guidelines for Scoped Elements](?path=/docs/guidelines-scoped-elements--page)
|
||||
- [Guidelines for SubClasser APIs](?path=/docs/guidelines-subclasser-apis--page)
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
# Definitions and terms
|
||||
|
||||
Below you will find a list of definitions and terms that will be used throughout our code
|
||||
documentation.
|
||||
|
||||
- `Application Developer`:
|
||||
Developers consuming our webcomponents inside an application (not extending them).
|
||||
Application Developers are only allowed to interact with `public` properties and methods.
|
||||
Can be abbreviated as `AD`. Sometimes also called `Consuming Developer`.
|
||||
|
||||
- `Subclasser`:
|
||||
Developers extending our webcomponents. For instance: `MaterialInput extends LionInput`.
|
||||
Subclassers have access to protected methods (prefixed with an underscore or marked as protected),
|
||||
but not to private methods.
|
||||
|
||||
- `public`:
|
||||
Methods and properties are public when they are not prefixed by an underscore.
|
||||
They can be used by Application Developers.
|
||||
|
||||
```js
|
||||
class SoccerPlayer {
|
||||
kickBall() {
|
||||
// Soccer player can kick a ball
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `protected`:
|
||||
Methods and properties are protected when they contain one underscore or are explicitly marked as
|
||||
protected in the code.
|
||||
They can be used by Subclassers.
|
||||
|
||||
```js
|
||||
class SoccerPlayer {
|
||||
_catchBall() {
|
||||
// Soccer player usually do not need to catch a ball (with it's hands)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `private`:
|
||||
Methods and properties are protected when they contain two underscores or are explicitly marked as
|
||||
private in the code.
|
||||
They can be used within the class where they are defined (developers of Lion components).
|
||||
|
||||
```js
|
||||
class SoccerPlayer {
|
||||
__score() {
|
||||
// internally save how many goals have been made
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
# Guidlines for Styling
|
||||
|
||||
## Markup and styling
|
||||
|
||||
All Lion webcomponents have white label styling: this means theming is not applied,
|
||||
but functional styling is.
|
||||
|
||||
### Functional styling
|
||||
|
||||
Functional styling can be divided into defaults for 'basic layout' and 'accessibility'.
|
||||
Examples for both categories can be found below:
|
||||
|
||||
- Basic layout examples:
|
||||
|
||||
- A dropdown menu has 'position: absolute'
|
||||
- A lion-button might behave as an 'display: inline-block' element
|
||||
- A suffix attached to an input is horizontally positioned (instead of vertically stacked)
|
||||
|
||||
- Accessibility examples:
|
||||
- content (for instance a caption in a table) can be 'visually hidden': it will be
|
||||
readable by screen readers, but invisible for end users
|
||||
- when an html table is used, we allow [subclassers](./definitions.md) to override the display
|
||||
property ('table-cell') to 'flex'. By putting the proper accessible roles ('role="cell"') in the
|
||||
markup, we guaruantee our markup stays accessible
|
||||
|
||||
Although Lion components try to stay as unbiased as possible with regard to styling, defaults will
|
||||
be needed. In these cases we try to follow the platform as much as possible. If the platform
|
||||
doesn't provide defaults, the largest common denominator across exisiting UI frameworks is taken as
|
||||
a lead.
|
||||
|
||||
## Style components
|
||||
|
||||
A style component consists of a set of css classes mapping to a certain html structure. Although css
|
||||
components are often implemented by webcomponents, they should be considered modules on their own,
|
||||
reusable in different contexts: they should be considered the lowest abstraction layer of a
|
||||
webcomponent.
|
||||
|
||||
A webcomponent usually implements a css module (mapped at host level), but the opposite is not
|
||||
necessarily true: a css module doesn't have to be used inside a webcomponent or at the root level
|
||||
of a webcomponent.
|
||||
|
||||
Advantages of developing style components isolated from webcomponents are:
|
||||
|
||||
- They can be reused in non shadow dom contexts.
|
||||
- Not everything has to be a webcomponent: anchor or card styling inside a webcomponent would be a
|
||||
matter of importing a style component into a shadowroot.
|
||||
|
||||
### Requirements
|
||||
|
||||
Style components are written with the following assumptions in mind:
|
||||
|
||||
- **Environment agnostic**: Lion webcomponents are unopinionated about their environment: they
|
||||
should be usable in every context/framework/technology thinkable, regardless of whether shadow dom
|
||||
is used.
|
||||
- **Customisable**: They should provide an api for [subclassers](./definitions.md):
|
||||
- **white label**: components (just like webcomponents) are white label components. They provide
|
||||
a flexible html structure which should be mappable to any Design System.
|
||||
This approach is inspired by [Inuitcss](https://github.com/inuitcss/inuitcss) (an architectural
|
||||
foundation for white label style components) and [Bootstrap Material](https://fezvrasta.github.io/bootstrap-material-design/)
|
||||
(a Material Design system based on existing Bootstrap html/class names).
|
||||
This is also [a great example of a Design System](https://www.carbondesignsystem.com/components/checkbox/code)
|
||||
taking this(style components as lowest abstraction layer) approach.
|
||||
- **well structured**: for clear, readable and maintainable code we use [BEM](http://getbem.com/):
|
||||
it forces the
|
||||
developer to think about component structure in terms of 'block', 'element' and 'modifier'
|
||||
parts/roles. The classes defined in a BEM component form the api for a
|
||||
[subclasser](./definitions.md): he can 'fill in' the BEM selectors by overriding them in the
|
||||
`static get styles()` configuration property on the constructor of a webcomponent.
|
||||
- **flexible markup**: By making the HTML structure purposely 'loose', multiple Design Systems can
|
||||
be made compatible with the style component. When the default html/css structure is not flexible
|
||||
enough, it can be extended by creating new BEM element selectors.
|
||||
|
||||
## Why BEM?
|
||||
|
||||
For css class naming, we use [BEM](http://getbem.com/) naming conventions. This helps you identify
|
||||
semantics of your markup at a quick glance, resulting in more readable and maintainable code.
|
||||
BEM provides us the following advantages:
|
||||
|
||||
- **Clear namings** that convey meaning about semantics and structure at a quick glance
|
||||
- **Prevent collisions** within shadow roots. Larger shadow roots using multiple style components
|
||||
would easily collide when css selectors would be scoped to the shadow dom in which they were
|
||||
created. For instance, calling the header of a card '.header' instead of '.c-card\_\_header'
|
||||
would make the card component not reusable in different contexts.
|
||||
- **Maximum flexibility** we can write css regardless of context. When we create a style component
|
||||
for a card, we can offer it as a style component only and later decide to offer it as a webcomponent.
|
||||
Or offer the style component
|
||||
as a more lower level api for advanced use cases and the webcomponent (that has limiting markup and
|
||||
styling options) for the majority of cases.
|
||||
Also, when we already have a webcomponent for the card and people want to create a custom card
|
||||
component, it might make more sense to write a new webcomponent instead of extending the existing
|
||||
card webcomponent.
|
||||
Having style components as described above, allows for maximum flexibility in these scenarios.
|
||||
Future CSS updates can then be done from a central place, without having to rewrite forked webcomponents.
|
||||
- **Performance** we aim for applications having a limited number of shadow roots and we consider
|
||||
shadow root creation a performance concern. Style components reduce the need for shadow roots.
|
||||
<!-- TODO: we definitely suffered here in the Polymer era. We might want to create some new perf
|
||||
tests for our lit-element based apps to verify this claim again. -->
|
||||
|
||||
### Challenges with BEM in shadow dom
|
||||
|
||||
Mapping BEM components to shadow root is not really straightforward:
|
||||
|
||||
1. host styles should be rewritten to `:host {}` instead of `.my-block {}`
|
||||
2. slot styles be rewritten to `::slotted(.my-block__element)` instead of `.my-block__element`
|
||||
3. we should not [self-apply classes](https://developers.google.com/web/fundamentals/web-components/best-practices)
|
||||
on host level. Apart from this being a bad practice, it will trigger linting errors in our setup.
|
||||
|
||||
### Mapping host and slot styles
|
||||
|
||||
The [CSS Module](https://github.com/w3c/webcomponents/issues/759) and [CSS Selector](https://github.com/w3c/csswg-drafts/issues/3714) proposals would allow us to reuse BEM components more easily within webcomponents.
|
||||
We might also think about other ways of creating build steps that would allow us to map BEM
|
||||
components to a shadow root, making it possible to map hosts and slots in an eay way.
|
||||
|
||||
<!-- TODO: quite important that we find a solution to this problem -->
|
||||
|
||||
### No self-application of classes
|
||||
|
||||
In order to adhere to this rule, notation of our BEM modifiers that live on the host (which are
|
||||
ususally written like `.my-block--my-modifier`) will be rewritten to `.my-block[my-modifier]`
|
||||
Although this would only be needed for the third challenge as described above, for consistency,
|
||||
we apply it to all our modifiers within our style component.
|
||||
So for instance `.my-block__element--my-other-modifier` becomes
|
||||
`.my-block__element[my-other-modifier]`.
|
||||
|
||||
### Why mapping style components to the host matters
|
||||
|
||||
- **flexible host overrides**:
|
||||
Imagine we have a style component `.c-alert` and we want to map this to a webcomponent `<my-alert>`.
|
||||
In our Design system it might be a good practice to give all our block components a default bottom
|
||||
margin of 16px. An [Application Developer](./definitions.md) might need an instance of `<my-alert>` where he wants to
|
||||
set this bottom margin to 0.
|
||||
If the margin-bottom of 16px would not be defined on the host level but one level deeper (the first
|
||||
div element within the shadow root of `<my-alert>`), the Application Developer needs to apply a
|
||||
negative margin of -16px. The latter would be a bad practice.
|
||||
|
||||
- **easily map modifiers**:
|
||||
If we continue with our alert example and we have a flag 'is-closeable', we might need to adjust
|
||||
styling based on that. In this case we assume the attribute 'is-closeable' serves as a styling
|
||||
hook for the `.c-alert` component.
|
||||
|
||||
## Css variables
|
||||
|
||||
Css variables will not be added in our white label style components, but adding them in your own
|
||||
extension layer would be a perfect fit.
|
||||
|
||||
## Parts and themes
|
||||
|
||||
The `::part` and `::theme` specs might currently not be widely adopted enough to be used inside
|
||||
style components. When they are, we could be consider to add them later to our components as a means
|
||||
to theme components (changing a whole Design System is not a good idea).
|
||||
|
||||
<!-- TODO: check if needed for story above, else delete
|
||||
It would mainly be benificial when:
|
||||
|
||||
- styles need to be reused in a context where extending is not a solution/forking is needed (which
|
||||
happened quite some times in our previous components lib, making our core styles less scalable).
|
||||
|
||||
- style components (and their corresponding html structure) are considered the core building
|
||||
blocks of a Design System. Webcomponents provide a developer friendly abstraction that mainly
|
||||
sees fit
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
For css class naming, we use BEM naming conventions. This helps you identify semantics of
|
||||
your markup at a quick glance, resulting in more readable and maintainable code.
|
||||
BEM is an abbreviation for:
|
||||
- Block
|
||||
- Element
|
||||
- Modifier
|
||||
|
||||
## webcomponents
|
||||
|
||||
### HTML Structure
|
||||
|
||||
Despite not having applied any styling by default, our components do have an html structure that
|
||||
allows [subclassers](./definitions.md) to easily extend them.
|
||||
Webcomponents can be
|
||||
|
||||
### BEM and shadow DOM
|
||||
|
||||
Although BEM naming conventions partly encapsulate
|
||||
|
||||
All CSS our components is written from a generic mindset, following BEM conventions:
|
||||
https://en.bem.info/methodology/
|
||||
|
||||
Although the CSS and HTML are implemented by the component, they should be regarded as
|
||||
totally decoupled.
|
||||
|
||||
Not only does this force us to write better structured css, it also allows for future
|
||||
reusability in many different ways like:
|
||||
- disabling shadow DOM for a component (for water proof encapsulation can be combined with
|
||||
a build step)
|
||||
- easier translation to more flexible, WebComponents agnostic solutions like JSS
|
||||
(allowing extends, mixins, reasoning, IDE integration, tree shaking etc.)
|
||||
- export to a CSS module for reuse in an outer context
|
||||
|
||||
Please note that the HTML structure is purposely 'loose', allowing multiple design systems
|
||||
to be compatible with the style component.
|
||||
Note that every occurence of '::slotted(*)' can be rewritten to '> *' for use in an other
|
||||
context
|
||||
-->
|
||||
|
||||
<!-- TODO:
|
||||
- follow https://cssguidelin.es/ and/or inuitcss: by prefixing our components with 'c-', we mark
|
||||
our components as core components, ensuring they never conflict with css created by -->
|
||||
63
docs/guidelines/10-guidelines-definitions.md
Normal file
63
docs/guidelines/10-guidelines-definitions.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Guidelines definitions and terms
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Guidelines/Definitions',
|
||||
};
|
||||
```
|
||||
|
||||
Below you will find a list of definitions and terms that will be used throughout our code
|
||||
documentation.
|
||||
|
||||
## Application Developer
|
||||
|
||||
Developers consuming our web components inside an application (not extending them).
|
||||
Application Developers are only allowed to interact with `public` properties and methods.
|
||||
Can be abbreviated as `AD`. Sometimes also called `Consuming Developer`.
|
||||
|
||||
## Subclasser
|
||||
|
||||
Developers extending our web components. For instance: `class MaterialInput extends LionInput`.
|
||||
Subclassers have access to protected methods (prefixed with an underscore or marked as protected),
|
||||
but not to private methods.
|
||||
|
||||
## public
|
||||
|
||||
Methods and properties are public when they are not prefixed by an underscore.
|
||||
They can be used by Application Developers.
|
||||
|
||||
```js
|
||||
class SoccerPlayer {
|
||||
kickBall() {
|
||||
// Soccer player can kick a ball
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## protected
|
||||
|
||||
Methods and properties are protected when they contain one underscore or are explicitly marked as
|
||||
protected in the code.
|
||||
They can be used by Subclassers.
|
||||
|
||||
```js
|
||||
class SoccerPlayer {
|
||||
_catchBall() {
|
||||
// Soccer player usually do not need to catch a ball (with it's hands)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## private
|
||||
|
||||
Methods and properties are protected when they contain two underscores or are explicitly marked as
|
||||
private in the code.
|
||||
They can be only used within the class where they are defined (developers of Lion components).
|
||||
|
||||
```js
|
||||
class SoccerPlayer {
|
||||
__score() {
|
||||
// internally save how many goals have been made
|
||||
}
|
||||
}
|
||||
```
|
||||
46
docs/guidelines/20-guidelines-styling.md
Normal file
46
docs/guidelines/20-guidelines-styling.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Guidelines for Styling
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Guidelines/Styling',
|
||||
};
|
||||
```
|
||||
|
||||
## Markup and styling
|
||||
|
||||
All Lion web components have white label styling: this means theming is not applied,
|
||||
but functional styling is.
|
||||
|
||||
### Functional styling
|
||||
|
||||
Functional styling can be divided into defaults for `basic layout` and `accessibility`.
|
||||
Examples for both categories can be found below:
|
||||
|
||||
- Basic layout examples:
|
||||
|
||||
- A dropdown menu has `position: absolute;`
|
||||
- A lion-button might behave as an `display: inline-block;` element
|
||||
- A suffix attached to an input is horizontally positioned (instead of vertically stacked)
|
||||
|
||||
- Accessibility examples:
|
||||
- content (for instance a caption in a table) can be `visually hidden`: it will be
|
||||
readable by screen readers, but invisible for end users
|
||||
- when an html table is used, we allow [subclassers](?path=/docs/guidelines-definitions--page#subclasser) to override the display
|
||||
property (`table-cell`) to `flex`. By putting the proper accessible roles (`role="cell"`) in the
|
||||
markup, we guarantee our markup stays accessible
|
||||
|
||||
Although Lion components try to stay as unbiased as possible with regard to styling, defaults will
|
||||
be needed. In these cases we try to follow the platform as much as possible. If the platform
|
||||
doesn't provide defaults, the largest common denominator across existing UI frameworks is taken as
|
||||
a lead.
|
||||
|
||||
## Css variables
|
||||
|
||||
Css variables will not be added in our white label style components, but adding them in your own
|
||||
extension layer would be a perfect fit.
|
||||
|
||||
## Parts and themes
|
||||
|
||||
The `::part` and `::theme` specs might currently not be widely adopted enough to be used inside
|
||||
style components. When they are, we could be consider to add them later to our components as a means
|
||||
to theme components (changing a whole Design System is not a good idea).
|
||||
107
docs/guidelines/30-guidelines-scoped-elements.md
Normal file
107
docs/guidelines/30-guidelines-scoped-elements.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Guidelines Scoped Elements
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Guidelines/Scoped Elements',
|
||||
};
|
||||
```
|
||||
|
||||
The [CustomElementRegistry](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry) provides methods for registering custom elements. One of the limitations of working with this global registry is that multiple versions of the same element cannot co-exist. This causes bottlenecks in software delivery that should be managed by the teams and complex build systems. [Scoped Custom Element Registries](https://github.com/w3c/webcomponents/issues/716) is a proposal that will solve the problem. Since this functionality won't be available (especially not cross browser) anytime soon, we've adopted [OpenWC's Scoped Elements](https://open-wc.org/scoped-element).
|
||||
|
||||
Whenever a lion component uses composition (meaning it uses another lion component inside), we
|
||||
apply ScopedElementsMixin to make sure it uses the right version of this internal component.
|
||||
|
||||
```js
|
||||
import { ScopedElementsMixin, LitElement, html } from '@lion/core';
|
||||
|
||||
import { LionInput } from '@lion/input';
|
||||
import { LionButton } from '@lion/button';
|
||||
|
||||
class MyElement extends ScopedElementsMixin(LitElement) {
|
||||
static get scopedElements() {
|
||||
return {
|
||||
'lion-input': LionInput,
|
||||
'lion-button': LionButton,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<lion-input label="Greeting" name="greeting" .modelValue=${'Hello world'}></lion-input>
|
||||
<lion-button>Save</lion-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Query selectors
|
||||
|
||||
Since Scoped Elements changes tagnames under the hood, a tagname querySelector should be written
|
||||
like this:
|
||||
|
||||
```js
|
||||
this.querySelector(getTagName('lion-input', this.constructor.scopedElements));
|
||||
```
|
||||
|
||||
## CSS selectors
|
||||
|
||||
Avoid tagname css selectors.
|
||||
We already avoid query selectors internally in lion, but just be aware that a selector like
|
||||
|
||||
```css
|
||||
lion-input {
|
||||
padding: 20px;
|
||||
}
|
||||
```
|
||||
|
||||
will stop working.
|
||||
|
||||
## Edge cases
|
||||
|
||||
Sometimes we need to render parts of a template to light dom for [accessibility](https://wicg.github.io/aom/explainer.html). For instance we render a node via lit-html that we append to the host element, so
|
||||
it gets slotted in the right position.
|
||||
In this case, we should also make sure that we also scope the rendered element.
|
||||
|
||||
We can do this as follows:
|
||||
|
||||
```js
|
||||
_myLightTemplate() {
|
||||
return html`
|
||||
This template may be overridden by a Subclasser.
|
||||
Even I don't end up in shadow root, I need to be scoped to constructor.scopedElements as well.
|
||||
<div>
|
||||
<lion-button>True</lion-button>
|
||||
<lion-input label="xyz"></lion-input>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
__getLightDomNode() {
|
||||
const renderParent = document.createElement('div');
|
||||
this.constructor.render(this._myLightTemplate(), renderParent, {
|
||||
scopeName: this.localName,
|
||||
eventContext: this,
|
||||
});
|
||||
// this node will be appended to the host
|
||||
return renderParent.firstElementChild;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.appendChild(this.__getLightDomNode());
|
||||
}
|
||||
```
|
||||
|
||||
In a less complex case, we might just want to add a child node to the dom.
|
||||
|
||||
```js
|
||||
import { ScopedElementsMixin, LitElement, getScopedTagNamegetScopedTagName } from '@lion/core';
|
||||
|
||||
...
|
||||
|
||||
__getLightDomNode() {
|
||||
return document.createElement(getScopedTagName('lion-input', this.constructor.scopedElements));
|
||||
}
|
||||
```
|
||||
|
||||
We encourage you to have a look at [OpenWC's Scoped elements](https://open-wc.org/scoped-elements).
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
# Subclasser apis
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Guidelines/Subclasser APIs',
|
||||
};
|
||||
```
|
||||
|
||||
In order to make it easy for Subclassers to extend our components, we follow a certain set
|
||||
of best practices naming conventions to make our code predictable.
|
||||
|
||||
|
|
@ -7,20 +13,19 @@ of best practices naming conventions to make our code predictable.
|
|||
|
||||
### Template naming conventions
|
||||
|
||||
For template, we have the naming convention that every overridable template starts with an
|
||||
underscore and ends with
|
||||
For template, that do need access to the instance of the web component there is a prefix `_render/__render`.
|
||||
Examples are `__renderHeader` and `__renderData`.
|
||||
|
||||
### Seperation of concerns
|
||||
### Separation of concerns
|
||||
|
||||
Our components should make it possible to override markup and styling, without having to redefine
|
||||
functionality.
|
||||
By using the spread directive, we can achieve this. For more info, see:
|
||||
[https://github.com/ing-bank/lion/issues/591](explanation).
|
||||
By using the spread directive, we can achieve this. For more info, see [explanation](https://github.com/ing-bank/lion/issues/591).
|
||||
|
||||
## Node references
|
||||
|
||||
As a Subclasser, you sometimes need access to a protected node inside the shadow dom.
|
||||
Most functional nodes have their own getters. A Subclasser can acces those in his extension and
|
||||
Most functional nodes have their own getters. A Subclasser can access those in his extension and
|
||||
in some cases, override these getters.
|
||||
|
||||
### Node naming conventions
|
||||
22
docs/intros/intros-buttons.md
Normal file
22
docs/intros/intros-buttons.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Buttons
|
||||
|
||||
Buttons are clickable elements.
|
||||
|
||||
## Features
|
||||
|
||||
- fully accessible
|
||||
|
||||
## Packages
|
||||
|
||||
The following buttons area published.
|
||||
|
||||
| Package | Version | Description |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------- |
|
||||
| [button](?path=/docs/buttons-button--default-story) | [](https://www.npmjs.com/package/@lion/button) | Button |
|
||||
| [switch](?path=/docs/buttons-switch--default-off) | [](https://www.npmjs.com/package/@lion/switch) | Switch |
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Buttons/Intro',
|
||||
};
|
||||
```
|
||||
|
|
@ -1,7 +1,3 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
|
||||
<Meta title="Icons/Intro" />
|
||||
|
||||
# Icons
|
||||
|
||||
Icon system for managing iconsets, taking into account performance, maintainability and scalability.
|
||||
|
|
@ -13,3 +9,9 @@ Icons are SVGs so they can be easily scaled and styled with CSS.
|
|||
| Package | Version | Description |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------- |
|
||||
| [icon](?path=/docs/icons-icon--default-story) | [](https://www.npmjs.com/package/@lion/icon) | Icon |
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Icons/Intro',
|
||||
};
|
||||
```
|
||||
20
docs/intros/intros-navigation.md
Normal file
20
docs/intros/intros-navigation.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Navigation
|
||||
|
||||
Navigational elements are used to guide users within your page.
|
||||
|
||||
## Features
|
||||
|
||||
- fully accessible
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Version | Description |
|
||||
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
||||
| [steps](?path=/docs/navigation-steps--default-story) | [](https://www.npmjs.com/package/@lion/steps) | Multi Step System |
|
||||
| [tabs](?path=/docs/navigation-tabs--default-story) | [](https://www.npmjs.com/package/@lion/tabs) | Move between a small number of equally important views |
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Navigation/Intro',
|
||||
};
|
||||
```
|
||||
17
docs/intros/intros-others.md
Normal file
17
docs/intros/intros-others.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Others
|
||||
|
||||
Features not fitting any other category.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Version | Description |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------- |
|
||||
| [ajax](?path=/docs/others-ajax--default-story) | [](https://www.npmjs.com/package/@lion/ajax) | Ajax |
|
||||
| [calendar](?path=/docs/others-calendar--default-story) | [](https://www.npmjs.com/package/@lion/calendar) | Calendar |
|
||||
| [core](?path=/docs/others-system-core--page) | [](https://www.npmjs.com/package/@lion/core) | Core |
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Others/Intro',
|
||||
};
|
||||
```
|
||||
|
|
@ -198,6 +198,12 @@ console.log(somethingElse2);
|
|||
// console.log(ctrl.manager.list);
|
||||
```
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Rationals/Side Effects',
|
||||
};
|
||||
```
|
||||
|
||||
[1]: https://rollupjs.org/repl/?version=2.10.2&shareable=JTdCJTIybW9kdWxlcyUyMiUzQSU1QiU3QiUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMmltcG9ydCUyMCU3QiUyMHNvbWV0aGluZ0Vsc2UlMkMlMjBvdmVybGF5cyUyMCU3RCUyMGZyb20lMjAnLiUyRm92ZXJsYXlzLmpzJyUzQiU1Q24lNUNuY29uc29sZS5sb2coc29tZXRoaW5nRWxzZSklM0IlMjIlMkMlMjJpc0VudHJ5JTIyJTNBdHJ1ZSU3RCUyQyU3QiUyMm5hbWUlMjIlM0ElMjJvdmVybGF5cy5qcyUyMiUyQyUyMmNvZGUlMjIlM0ElMjJjbGFzcyUyME92ZXJsYXlNYW5hZ2VyJTIwJTdCJTdEJTVDbiU1Q25leHBvcnQlMjBsZXQlMjBvdmVybGF5cyUyMCUzRCUyMG5ldyUyME92ZXJsYXlNYW5hZ2VyKCklM0IlNUNuJTVDbmV4cG9ydCUyMGZ1bmN0aW9uJTIwc2V0T3ZlcmxheXMobmV3T3ZlcmxheXMpJTIwJTdCJTVDbiU1Q3RvdmVybGF5cyUyMCUzRCUyMG5ld092ZXJsYXlzJTNCJTVDbiU3RCU1Q24lNUNuZXhwb3J0JTIwY29uc3QlMjBzb21ldGhpbmdFbHNlJTIwJTNEJTIwJ3NvbWV0aGluZyUyMGVsc2UnJTNCJTIyJTdEJTVEJTJDJTIyb3B0aW9ucyUyMiUzQSU3QiUyMmZvcm1hdCUyMiUzQSUyMmVzJTIyJTJDJTIybmFtZSUyMiUzQSUyMm15QnVuZGxlJTIyJTJDJTIyYW1kJTIyJTNBJTdCJTIyaWQlMjIlM0ElMjIlMjIlN0QlMkMlMjJnbG9iYWxzJTIyJTNBJTdCJTdEJTdEJTJDJTIyZXhhbXBsZSUyMiUzQW51bGwlN0Q=
|
||||
[2]: https://rollupjs.org/repl/?version=2.10.2&shareable=JTdCJTIybW9kdWxlcyUyMiUzQSU1QiU3QiUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMmltcG9ydCUyMCU3QiUyMHNvbWV0aGluZ0Vsc2UlMkMlMjBvdmVybGF5cyUyMCU3RCUyMGZyb20lMjAnLiUyRm92ZXJsYXlzLmpzJyUzQiU1Q25pbXBvcnQlMjAnLiUyRm92ZXJyaWRlLmpzJyUzQiU1Q24lNUNuY29uc29sZS5sb2coc29tZXRoaW5nRWxzZSklM0IlNUNuJTVDbiUyRiUyRiUyMHRoaXMlMjB3aWxsJTIwdHJpZ2dlciUyMGxvYWRpbmclMjBvZiUyMGZ1bGwlMjBvdmVybGF5cy5qcyUyMCUyNiUyMG92ZXJyaWRlLmpzJTIwY29kZSU1Q24lMkYlMkYlMjBjb25zb2xlLmxvZyhvdmVybGF5cy5saXN0KSUzQiUyMiUyQyUyMmlzRW50cnklMjIlM0F0cnVlJTdEJTJDJTdCJTIybmFtZSUyMiUzQSUyMm92ZXJsYXlzLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMmNsYXNzJTIwT3ZlcmxheU1hbmFnZXIlMjAlN0IlN0QlNUNuJTVDbmV4cG9ydCUyMGxldCUyMG92ZXJsYXlzJTIwJTNEJTIwbmV3JTIwT3ZlcmxheU1hbmFnZXIoKSUzQiU1Q24lNUNuZXhwb3J0JTIwZnVuY3Rpb24lMjBzZXRPdmVybGF5cyhuZXdPdmVybGF5cyklMjAlN0IlNUNuJTVDdG92ZXJsYXlzJTIwJTNEJTIwbmV3T3ZlcmxheXMlM0IlNUNuJTdEJTVDbiU1Q25leHBvcnQlMjBjb25zdCUyMHNvbWV0aGluZ0Vsc2UlMjAlM0QlMjAnc29tZXRoaW5nJTIwZWxzZSclM0IlMjIlN0QlMkMlN0IlMjJuYW1lJTIyJTNBJTIyb3ZlcnJpZGUuanMlMjIlMkMlMjJjb2RlJTIyJTNBJTIyaW1wb3J0JTIwJTdCJTIwc2V0T3ZlcmxheXMlMjAlN0QlMjBmcm9tJTIwJy4lMkZvdmVybGF5cy5qcyclM0IlNUNuJTVDbnNldE92ZXJsYXlzKG5ldyUyME9iamVjdCgpKSUzQiUyMiU3RCU1RCUyQyUyMm9wdGlvbnMlMjIlM0ElN0IlMjJmb3JtYXQlMjIlM0ElMjJlcyUyMiUyQyUyMm5hbWUlMjIlM0ElMjJteUJ1bmRsZSUyMiUyQyUyMmFtZCUyMiUzQSU3QiUyMmlkJTIyJTNBJTIyJTIyJTdEJTJDJTIyZ2xvYmFscyUyMiUzQSU3QiU3RCU3RCUyQyUyMmV4YW1wbGUlMjIlM0FudWxsJTdE
|
||||
[3]: https://rollupjs.org/repl/?version=2.10.2&shareable=JTdCJTIybW9kdWxlcyUyMiUzQSU1QiU3QiUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMmltcG9ydCUyMCU3QiUyMHNvbWV0aGluZ0Vsc2UlMkMlMjBzZXRPdmVybGF5cyUyQyUyMG92ZXJsYXlzJTIwJTdEJTIwZnJvbSUyMCcuJTJGb3ZlcmxheXMuanMnJTNCJTVDbmltcG9ydCUyMCU3QiUyME92ZXJsYXlDb250cm9sbGVyJTJDJTIwc29tZXRoaW5nRWxzZTIlMjAlN0QlMjBmcm9tJTIwJy4lMkZPdmVybGF5Q29udHJvbGxlci5qcyclM0IlNUNuJTVDbiUyRiUyRiUyMHRoZSUyMGZvbGxvd2luZyUyMGNvZGUlMjB3aWxsJTIwdHJlZSUyMHNoYWtlJTIwb3ZlcmxheXMlMjBhd2F5JTIwaGVuY2UlMjBvdmVybGF5cyUyMGlzJTIwc2lkZSUyMGVmZmVjdCUyMGZyZWUlMjAlNUNuJTVDbnNldE92ZXJsYXlzKG5ldyUyME9iamVjdCgpKSUzQiU1Q25jb25zb2xlLmxvZyhzb21ldGhpbmdFbHNlKSUzQiU1Q25jb25zb2xlLmxvZyhzb21ldGhpbmdFbHNlMiklM0IlNUNuJTVDbiUyRiUyRioqJTIwVGhlJTIwZm9sbG93aW5nJTIwd2lsbCUyMHRvZ2dsZSUyMGltcG9ydGluZyUyMG9mJTIwb3ZlcmxheXMlMjAqJTJGJTVDbiU1Q24lMkYlMkYlMjAxLiUyMGltcG9ydCUyMG92ZXJsYXlzJTIwZGlyZWN0bHklMjBhbmQlMjBhY2Nlc3MlMjBpdCU1Q24lMkYlMkYlMjBjb25zb2xlLmxvZyhvdmVybGF5cy5saXN0KSUzQiUyMCU1Q24lNUNuJTJGJTJGJTIwMi4lMjBjcmVhdGUlMjBhbiUyME92ZXJsYXlDb250cm9sbGVyJTIwd2hpY2glMjBpbnRlcm5hbGx5JTIwYWNjZXNzZXMlMjBvdmVybGF5cyU1Q24lMkYlMkYlMjBjb25zdCUyMGN0cmwlMjAlM0QlMjBuZXclMjBPdmVybGF5Q29udHJvbGxlcigpJTNCJTVDbiUyRiUyRiUyMGNvbnNvbGUubG9nKGN0cmwubWFuYWdlci5saXN0KSUzQiU1Q24lNUNuJTIyJTJDJTIyaXNFbnRyeSUyMiUzQXRydWUlN0QlMkMlN0IlMjJuYW1lJTIyJTNBJTIyb3ZlcmxheXMuanMlMjIlMkMlMjJjb2RlJTIyJTNBJTIyY2xhc3MlMjBPdmVybGF5TWFuYWdlciUyMCU3QiU1Q24lNUN0Y29uc3RydWN0b3IoKSUyMCU3QiU1Q24lNUN0JTVDdHRoaXMubGlzdCUyMCUzRCUyMCU1QiU1RCUzQiU1Q24lNUN0JTdEJTVDbiUyMCUyMGFkZChhKSUyMCU3QiU1Q24lMjAlMjAlMjAlMjB0aGlzLmxpc3QucHVzaChhKSUzQiU1Q24lNUN0JTdEJTVDbiU3RCU1Q24lNUNuZXhwb3J0JTIwbGV0JTIwb3ZlcmxheXMlMjAlM0QlMjBuZXclMjBPdmVybGF5TWFuYWdlcigpJTNCJTVDbiU1Q25leHBvcnQlMjBmdW5jdGlvbiUyMHNldE92ZXJsYXlzKG5ld092ZXJsYXlzKSUyMCU3QiU1Q24lNUN0b3ZlcmxheXMlMjAlM0QlMjBuZXdPdmVybGF5cyUzQiU1Q24lN0QlNUNuJTVDbmV4cG9ydCUyMGNvbnN0JTIwc29tZXRoaW5nRWxzZSUyMCUzRCUyMCdzb21ldGhpbmclMjBlbHNlJyUzQiU1Q24lNUNuJTJGJTJGJTIwdGhlJTIwZm9sbG93aW5nJTIwbGluZSUyMGlzJTIwYSUyMHNpZGUlMjBlZmZlY3QlMjBhcyUyMGl0JTIwYWx3YXlzJTIwd2lsbCUyMGJlJTIwaW5jbHVkZWQlNUNuY29uc29sZS5sb2coJ292ZXJsYXlzJTIwc2lkZSUyMGVmZmVjdCcpJTNCJTIyJTJDJTIyaXNFbnRyeSUyMiUzQWZhbHNlJTdEJTJDJTdCJTIybmFtZSUyMiUzQSUyMk92ZXJsYXlDb250cm9sbGVyLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMmltcG9ydCUyMCU3QiUyMG92ZXJsYXlzJTIwJTdEJTIwZnJvbSUyMCcuJTJGb3ZlcmxheXMuanMnJTNCJTVDbiU1Q25leHBvcnQlMjBjbGFzcyUyME92ZXJsYXlDb250cm9sbGVyJTIwJTdCJTVDbiUyMCUyMCUyMGNvbnN0cnVjdG9yKGNvbmZpZyUyMCUzRCUyMCU3QiU3RCUyQyUyMG1hbmFnZXIlMjAlM0QlMjBvdmVybGF5cyklMjAlN0IlNUNuJTIwJTIwJTIwJTIwJTIwdGhpcy5tYW5hZ2VyJTIwJTNEJTIwbWFuYWdlciUzQiU1Q24lNUN0JTVDdCUyMHRoaXMubWFuYWdlci5hZGQodGhpcyklM0IlNUNuJTVDdCUyMCU3RCU1Q24lN0QlNUNuJTVDbmV4cG9ydCUyMGNvbnN0JTIwc29tZXRoaW5nRWxzZTIlMjAlM0QlMjAnc29tZXRoaW5nJTIwZWxzZSclM0IlMjIlN0QlNUQlMkMlMjJvcHRpb25zJTIyJTNBJTdCJTIyZm9ybWF0JTIyJTNBJTIyZXMlMjIlMkMlMjJuYW1lJTIyJTNBJTIybXlCdW5kbGUlMjIlMkMlMjJhbWQlMjIlM0ElN0IlMjJpZCUyMiUzQSUyMiUyMiU3RCUyQyUyMmdsb2JhbHMlMjIlM0ElN0IlN0QlN0QlMkMlMjJleGFtcGxlJTIyJTNBbnVsbCU3RA==
|
||||
|
|
|
|||
8
es-dev-server.config.js
Normal file
8
es-dev-server.config.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
const { mdjsTransformer } = require('@mdjs/core');
|
||||
|
||||
module.exports = {
|
||||
nodeResolve: true,
|
||||
// open: 'packages/button/README.md',
|
||||
watch: true,
|
||||
responseTransformers: [mdjsTransformer],
|
||||
};
|
||||
|
|
@ -9,7 +9,8 @@
|
|||
"@open-wc/building-rollup": "^1.2.1",
|
||||
"@commitlint/cli": "^7.0.0",
|
||||
"@commitlint/config-conventional": "^7.0.0",
|
||||
"@open-wc/demoing-storybook": "^2.0.0",
|
||||
"@mdjs/core": "^0.3.1",
|
||||
"@open-wc/demoing-storybook": "^2.0.2",
|
||||
"@open-wc/eslint-config": "^1.0.0",
|
||||
"@open-wc/testing": "^2.5.0",
|
||||
"@open-wc/testing-helpers": "^1.0.0",
|
||||
|
|
@ -39,6 +40,7 @@
|
|||
"whatwg-fetch": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev-server": "es-dev-server",
|
||||
"start": "npm run storybook",
|
||||
"storybook": "start-storybook -p 9001",
|
||||
"storybook:build": "build-storybook",
|
||||
|
|
|
|||
|
|
@ -3,10 +3,26 @@
|
|||
`ajax` is the global manager for handling all ajax requests.
|
||||
It is a promise based system for fetching data, based on [axios](https://github.com/axios/axios)
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import { ajax } from './src/ajax.js';
|
||||
import { AjaxClass } from './src/AjaxClass.js';
|
||||
|
||||
export default {
|
||||
title: 'Others/Ajax',
|
||||
};
|
||||
```
|
||||
|
||||
## Live Demo/Documentation
|
||||
|
||||
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/fetch-system-ajax) for a live demo and documentation
|
||||
|
||||
## Features
|
||||
|
||||
- only JS functions, no (unnecessarily expensive) web components
|
||||
- supports GET, POST, PUT, DELETE, REQUEST, PATCH and HEAD methods
|
||||
- can be used with or without XSRF token
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
|
@ -15,6 +31,10 @@ It is a promise based system for fetching data, based on [axios](https://github.
|
|||
npm i --save @lion/ajax
|
||||
```
|
||||
|
||||
```js
|
||||
import { ajax, AjaxClass } from '@lion/ajax';
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
|
|
@ -22,3 +42,161 @@ import { ajax } from '@lion/ajax';
|
|||
|
||||
ajax.get('data.json').then(response => console.log(response));
|
||||
```
|
||||
|
||||
### Performing requests
|
||||
|
||||
Performing a `GET` request:
|
||||
|
||||
```js preview-story
|
||||
export const performingGetRequests = () => html`
|
||||
<button
|
||||
@click=${() => {
|
||||
ajax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Request to Action Logger
|
||||
</button>
|
||||
`;
|
||||
```
|
||||
|
||||
To post data to the server, pass the data as the second argument in the `POST` request:
|
||||
|
||||
```js
|
||||
const body = {
|
||||
ant: {
|
||||
type: 'insect',
|
||||
limbs: 6,
|
||||
},
|
||||
};
|
||||
ajax
|
||||
.post('zooApi/animals/addAnimal', body)
|
||||
.then(response => {
|
||||
console.log(`POST successful: ${response.status} ${response.statusText}`);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### JSON prefix
|
||||
|
||||
The called API might add a JSON prefix to the response in order to prevent hijacking.
|
||||
The prefix renders the string syntactically invalid as a script so that it cannot be hijacked.
|
||||
This prefix should be stripped before parsing the string as JSON.
|
||||
Pass the prefix with the `jsonPrefix` option.
|
||||
|
||||
```js
|
||||
const myAjax = AjaxClass.getNewInstance({ jsonPrefix: ")]}'," });
|
||||
myAjax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
```
|
||||
|
||||
### Additional headers
|
||||
|
||||
Add additional headers to the requests with the `headers` option.
|
||||
|
||||
```js preview-story
|
||||
export const additionalHeaders = () => html`
|
||||
<button
|
||||
@click=${() => {
|
||||
const myAjax = AjaxClass.getNewInstance({ headers: { 'MY-HEADER': 'SOME-HEADER-VALUE' } });
|
||||
myAjax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Request to Action Logger
|
||||
</button>
|
||||
`;
|
||||
```
|
||||
|
||||
When executing the request above, check the Network tab in the Browser's dev tools and look for the Request Header on the GET call.
|
||||
|
||||
### Cancelable Request
|
||||
|
||||
It is possible to make an Ajax request cancelable, and then call `cancel()` to make the request provide a custom error once fired.
|
||||
|
||||
```js preview-story
|
||||
export const cancelableRequests = () => html`
|
||||
<button
|
||||
@click=${() => {
|
||||
const myAjax = AjaxClass.getNewInstance({ cancelable: true });
|
||||
requestAnimationFrame(() => {
|
||||
myAjax.cancel('too slow');
|
||||
});
|
||||
myAjax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Request to Action Logger
|
||||
</button>
|
||||
`;
|
||||
```
|
||||
|
||||
### Cancel concurrent requests
|
||||
|
||||
You can cancel concurrent requests with the `cancelPreviousOnNewRequest` option.
|
||||
|
||||
```js preview-story
|
||||
export const cancelConcurrentRequests = () => html`
|
||||
<button
|
||||
@click=${() => {
|
||||
const myAjax = AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true });
|
||||
myAjax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error.message);
|
||||
});
|
||||
myAjax
|
||||
.get('./packages/ajax/docs/assets/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Both Requests to Action Logger
|
||||
</button>
|
||||
`;
|
||||
```
|
||||
|
||||
## Considerations
|
||||
|
||||
Due to a [bug in axios](https://github.com/axios/axios/issues/385) options may leak in to other instances.
|
||||
So please avoid setting global options in axios. Interceptors have no issues.
|
||||
|
||||
## Future plans
|
||||
|
||||
- Eventually we want to remove axios and replace it with [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
||||
- This wrapper exist to prevent this switch from causing breaking changes for our users
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"directory": "packages/ajax"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cd ../../ && yarn dev-server --open packages/ajax/README.md",
|
||||
"prepublishOnly": "../../scripts/npm-prepublish.js",
|
||||
"test": "cd ../../ && yarn test:browser --grep \"packages/ajax/test/**/*.test.js\"",
|
||||
"test:watch": "cd ../../ && yarn test:browser:watch --grep \"packages/ajax/test/**/*.test.js\""
|
||||
|
|
|
|||
|
|
@ -1,279 +0,0 @@
|
|||
import {
|
||||
Story,
|
||||
Meta,
|
||||
html,
|
||||
} from '@open-wc/demoing-storybook';
|
||||
|
||||
import { ajax } from '../src/ajax.js';
|
||||
import { AjaxClass } from '../src/AjaxClass.js';
|
||||
|
||||
<Meta title="Others/Ajax" parameters={{ component: 'lion-ajax' }} />
|
||||
|
||||
# Ajax
|
||||
|
||||
`lion-ajax` is the global manager for handling all ajax requests.
|
||||
It is a promise based system for fetching data, based on <a href="https://github.com/axios/axios" target="_blank">
|
||||
axios</a>
|
||||
|
||||
<Story name="Default">
|
||||
{html`
|
||||
<button
|
||||
@click=${() => {
|
||||
ajax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Request to Action Logger
|
||||
</button>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
ajax.get('./packages/ajax/stories/data.json').then(response => console.log(response.data));
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- only JS functions, no (unnecessarily expensive) web components
|
||||
- supports GET, POST, PUT, DELETE, REQUEST, PATCH and HEAD methods
|
||||
- can be used with or without XSRF token
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
npm i --save @lion/ajax
|
||||
```
|
||||
|
||||
```js
|
||||
import { ajax, AjaxClass } from '@lion/ajax';
|
||||
```
|
||||
|
||||
### Performing requests
|
||||
|
||||
Performing a `GET` request:
|
||||
|
||||
<Story name="GET">
|
||||
{html`
|
||||
<button
|
||||
@click=${() => {
|
||||
ajax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Request to Action Logger
|
||||
</button>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
ajax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
```
|
||||
|
||||
To post data to the server, pass the data as the second argument in the `POST` request:
|
||||
|
||||
```js
|
||||
const body = {
|
||||
ant: {
|
||||
type: "insect",
|
||||
limbs: 6,
|
||||
}
|
||||
};
|
||||
|
||||
ajax
|
||||
.post('zooApi/animals/addAnimal', body)
|
||||
.then(response => {
|
||||
console.log(`POST successful: ${response.status} ${response.statusText}`);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### JSON prefix
|
||||
|
||||
The called API might add a JSON prefix to the response in order to prevent hijacking.
|
||||
The prefix renders the string syntactically invalid as a script so that it cannot be hijacked.
|
||||
This prefix should be stripped before parsing the string as JSON.
|
||||
Pass the prefix with the `jsonPrefix` option.
|
||||
|
||||
```js
|
||||
const myAjax = AjaxClass.getNewInstance({ jsonPrefix: ")]}'," });
|
||||
myAjax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
```
|
||||
|
||||
### Additional headers
|
||||
|
||||
Add additional headers to the requests with the `headers` option.
|
||||
|
||||
<Story name="Headers">
|
||||
{html`
|
||||
<button
|
||||
@click=${() => {
|
||||
const myAjax = AjaxClass.getNewInstance({ headers: { 'MY-HEADER': 'SOME-HEADER-VALUE' } });
|
||||
myAjax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Request to Action Logger
|
||||
</button>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
const myAjax = AjaxClass.getNewInstance({ headers: { 'X-CUSTOM-HEADER': 'SOME-HEADER-VALUE' } });
|
||||
myAjax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
```
|
||||
|
||||
When executing the request above, check the Network tab in the Browser's dev tools and look for the Request Header on the GET call.
|
||||
|
||||
### Cancelable Request
|
||||
|
||||
It is possible to make an Ajax request cancelable, and then call `cancel()` to make the request provide a custom error once fired.
|
||||
|
||||
<Story name="Cancelable">
|
||||
{html`
|
||||
<button
|
||||
@click=${() => {
|
||||
const myAjax = AjaxClass.getNewInstance({ cancelable: true });
|
||||
requestAnimationFrame(() => {
|
||||
myAjax.cancel('too slow');
|
||||
});
|
||||
myAjax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Request to Action Logger
|
||||
</button>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
const myAjax = AjaxClass.getNewInstance({ cancelable: true });
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
myAjax.cancel('too slow');
|
||||
});
|
||||
|
||||
myAjax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
```
|
||||
|
||||
### Cancel concurrent requests
|
||||
|
||||
You can cancel concurrent requests with the `cancelPreviousOnNewRequest` option.
|
||||
|
||||
<Story name="Cancel Previous on New Request">
|
||||
{html`
|
||||
<button
|
||||
@click=${() => {
|
||||
const myAjax = AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true });
|
||||
myAjax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error.message);
|
||||
});
|
||||
myAjax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Execute Both Requests to Action Logger
|
||||
</button>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
const myAjax = AjaxClass.getNewInstance({ cancelPreviousOnNewRequest: true });
|
||||
|
||||
myAjax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error.message);
|
||||
});
|
||||
|
||||
myAjax
|
||||
.get('./packages/ajax/stories/data.json')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error.message);
|
||||
});
|
||||
```
|
||||
|
||||
## Considerations
|
||||
|
||||
Due to a <a href="https://github.com/axios/axios/issues/385" target="_blank"> bug in axios</a> options may leak in to other instances.
|
||||
So please avoid setting global options in axios. Interceptors have no issues.
|
||||
|
||||
## Future plans
|
||||
|
||||
- Eventually we want to remove axios and replace it with <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API" target="_blank">
|
||||
Fetch</a>
|
||||
- This wrapper exist to prevent this switch from causing breaking changes for our users
|
||||
|
|
@ -223,3 +223,9 @@ module.exports = {
|
|||
],
|
||||
};
|
||||
```
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Tools/BabelPluginExtendDocs',
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,11 +1,34 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Button
|
||||
|
||||
`lion-button` provides a button component that is easily stylable and is accessible.
|
||||
`lion-button` provides a button component that is easily styleable and is accessible.
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
|
||||
import './lion-button.js';
|
||||
import iconSvg from './docs/assets/icon.svg.js';
|
||||
|
||||
export default {
|
||||
title: 'Buttons/Button',
|
||||
};
|
||||
```
|
||||
|
||||
```js story
|
||||
export const main = () => html` <lion-button>Default</lion-button> `;
|
||||
```
|
||||
|
||||
## Live Demo/Documentation
|
||||
|
||||
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/buttons-button) for a live demo and API documentation
|
||||
|
||||
## Features
|
||||
|
||||
- Clickable area that is bigger than visual size
|
||||
- Works with native form / inputs
|
||||
- Has integration for implicit form submission similar to how native `<form>`, `<input>` and `<button>` work together.
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
|
@ -15,6 +38,8 @@ npm i --save @lion/button
|
|||
```
|
||||
|
||||
```js
|
||||
import { LionButton } from '@lion/button';
|
||||
// or
|
||||
import '@lion/button/lion-button.js';
|
||||
```
|
||||
|
||||
|
|
@ -23,3 +48,96 @@ import '@lion/button/lion-button.js';
|
|||
```html
|
||||
<lion-button>Button Text</lion-button>
|
||||
```
|
||||
|
||||
## Buttons
|
||||
|
||||
### With click handler
|
||||
|
||||
```js preview-story
|
||||
export const handler = () => html`
|
||||
<lion-button @click="${ev => console.log('clicked/spaced/entered', ev)}">
|
||||
Click | Space | Enter me and see log
|
||||
</lion-button>
|
||||
`;
|
||||
```
|
||||
|
||||
### Icon button
|
||||
|
||||
```js preview-story
|
||||
export const iconButton = () => html` <lion-button>${iconSvg(html)} Bug</lion-button> `;
|
||||
```
|
||||
|
||||
### Icon only button
|
||||
|
||||
```js preview-story
|
||||
export const iconOnly = () =>
|
||||
html` <lion-button aria-label="Bug"> ${iconSvg(html)} </lion-button> `;
|
||||
```
|
||||
|
||||
### Disabled button
|
||||
|
||||
```js preview-story
|
||||
export const disabled = () => html` <lion-button disabled>Disabled</lion-button> `;
|
||||
```
|
||||
|
||||
### Usage with native form
|
||||
|
||||
Supports the following use cases:
|
||||
|
||||
- Submit on button click
|
||||
- Reset native form fields when using type="reset"
|
||||
- Submit on button enter or space keypress
|
||||
- Submit on enter keypress inside an input
|
||||
|
||||
```js preview-story
|
||||
export const withinForm = () => html`
|
||||
<form
|
||||
@submit=${ev => {
|
||||
ev.preventDefault();
|
||||
console.log('submit handler');
|
||||
}}
|
||||
>
|
||||
<label for="firstNameId">First name</label>
|
||||
<input id="firstNameId" name="firstName" />
|
||||
<label for="lastNameId">Last name</label>
|
||||
<input id="lastNameId" name="lastName" />
|
||||
<lion-button @click=${() => console.log('click handler')}>Submit</lion-button>
|
||||
</form>
|
||||
`;
|
||||
```
|
||||
|
||||
Important notes:
|
||||
|
||||
- A (lion)-button of type submit is mandatory for the last use case, if you have multiple inputs. This is native behavior.
|
||||
- `@click` on `<lion-button>` and `@submit` on `<form>` are triggered by these use cases. We strongly encourage you to listen to the submit handler if your goal is to do something on form-submit.
|
||||
- To prevent form submission full page reloads, add a **submit handler on the form** `@submit` with `event.preventDefault()`. Adding it on the `<lion-button>` is not enough.
|
||||
|
||||
## Considerations
|
||||
|
||||
### Why a Web Component?
|
||||
|
||||
There are multiple reasons why we used a Web Component as opposed to a CSS component.
|
||||
|
||||
- **Target size**: The minimum target size is 40 pixels, which makes even the small buttons easy to activate. A container element was needed to make this size possible.
|
||||
- **Advanced styling**: There are advanced styling options regarding icons in buttons, where it is a lot more maintainable to handle icons in our button using slots. An example is that a sticky icon-only buttons may looks different from buttons which have both icons and text.
|
||||
- **Native form integration**: The lion button works with native `<form>` submission, and even implicit form submission on-enter. A lot of delegation logic had to be created for this to work.
|
||||
|
||||
### Event target
|
||||
|
||||
We want to ensure that the event target returned to the user is `<lion-button>`, not `button`. Therefore, simply delegating the click to the native button immediately, is not desired. Instead, we catch the click event in the `<lion-button>`, and ensure delegation inside of there.
|
||||
|
||||
### Flashing a native button click as a direct child of form
|
||||
|
||||
By delegating the `click()` to the native button, it will bubble back up to `<lion-button>` which would cause duplicate actions. We have to simulate the full `.click()` however, otherwise form submission is not triggered. So this bubbling cannot be prevented.
|
||||
Therefore, on click, we flash a `<button>` to the form as a direct child and fire the click on that button. We then immediately remove that button. This is a fully synchronous process; users or developers will not notice this, it should not cause problems.
|
||||
|
||||
### Native button & implicit form submission
|
||||
|
||||
Flashing the button in the way we do solves almost all issues except for one.
|
||||
One of the specs of W3C is that when you have a form with multiple inputs,
|
||||
pressing enter while inside one of the inputs only triggers a form submit if that form has a button of type submit.
|
||||
|
||||
To get this particular implicit form submission to work, having a native button in our `<lion-button>` is a hard requirement.
|
||||
Therefore, not only do we flash a native button on the form to delegate `<lion-button>` trigger to `<button>`
|
||||
and thereby trigger form submission, we **also** add a native `button` inside the `<lion-button>`
|
||||
whose `type` property is synchronized with the type of the `<lion-button>`.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "../../scripts/npm-prepublish.js",
|
||||
"start": "cd ../../ && yarn dev-server --open packages/button/README.md",
|
||||
"test": "cd ../../ && yarn test:browser --grep \"packages/button/test/**/*.test.js\"",
|
||||
"test:watch": "cd ../../ && yarn test:browser:watch --grep \"packages/button/test/**/*.test.js\""
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,173 +0,0 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
|
||||
import '../lion-button.js';
|
||||
import iconSvg from './icon.svg.js';
|
||||
|
||||
<Meta title="Buttons/Button" parameters={{ component: 'lion-button' }} />
|
||||
|
||||
# Button
|
||||
|
||||
`lion-button` provides a button component that is easily stylable and is accessible.
|
||||
|
||||
<Story name="Default">
|
||||
{html`
|
||||
<lion-button>Default</lion-button>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-button>Default</lion-button>
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Clickable area that is bigger than visual size
|
||||
- Works with native form / inputs
|
||||
- Has integration for implicit form submission similar to how native `<form>`, `<input>` and `<button>` work together.
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
npm i --save @lion/button
|
||||
```
|
||||
|
||||
```js
|
||||
import '@lion/button/lion-button.js';
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```html
|
||||
<lion-button>Button Text</lion-button>
|
||||
```
|
||||
|
||||
## Buttons
|
||||
|
||||
### With click handler
|
||||
|
||||
<Story name="Handler">
|
||||
{html`
|
||||
<lion-button @click="${e => console.log('clicked/spaced/entered', e)}">
|
||||
Click | Space | Enter me and see log
|
||||
</lion-button>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-button @click="${e => console.log('clicked/spaced/entered', e)}">
|
||||
Click | Space | Enter me and see log
|
||||
</lion-button>
|
||||
```
|
||||
|
||||
### Icon button
|
||||
|
||||
<Story name="Icon">
|
||||
{html`<lion-button>${iconSvg(html)} Bug</lion-button>`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-button>${iconSvg(html)} Bug</lion-button>
|
||||
```
|
||||
|
||||
### Icon only button
|
||||
|
||||
<Story name="Icon-only">{html`
|
||||
<lion-button aria-label="Bug">
|
||||
${iconSvg(html)}
|
||||
</lion-button>
|
||||
`}</Story>
|
||||
|
||||
```html
|
||||
<lion-button aria-label="Bug">
|
||||
${iconSvg(html)}
|
||||
</lion-button>
|
||||
```
|
||||
|
||||
### Disabled button
|
||||
|
||||
<Story name="Disabled">
|
||||
{html`<lion-button disabled>Disabled</lion-button>`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-button disabled>Disabled</lion-button>
|
||||
```
|
||||
|
||||
### Usage with native form
|
||||
|
||||
Supports the following use cases:
|
||||
|
||||
- Submit on button click
|
||||
- Reset native form fields when using type="reset"
|
||||
- Submit on button enter or space keypress
|
||||
- Submit on enter keypress inside an input
|
||||
|
||||
<Story name="Within native form">
|
||||
{html`
|
||||
<form
|
||||
@submit=${ev => {
|
||||
ev.preventDefault();
|
||||
console.log('submit handler');
|
||||
}}
|
||||
>
|
||||
<label for="firstNameId">First name</label>
|
||||
<input id="firstNameId" name="firstName" />
|
||||
<label for="lastNameId">Last name</label>
|
||||
<input id="lastNameId" name="lastName" />
|
||||
<lion-button @click=${() => console.log('click handler')}>Submit</lion-button>
|
||||
</form>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<form
|
||||
@submit=${ev => {
|
||||
ev.preventDefault();
|
||||
console.log('submit handler');
|
||||
}}
|
||||
>
|
||||
<label for="firstNameId">First name</label>
|
||||
<input id="firstNameId" name="firstName" />
|
||||
<label for="lastNameId">Last name</label>
|
||||
<input id="lastNameId" name="lastName" />
|
||||
<lion-button @click=${() => console.log('click handler')}>Submit</lion-button>
|
||||
</form>
|
||||
```
|
||||
|
||||
Important notes:
|
||||
|
||||
- A (lion)-button of type submit is mandatory for the last use case, if you have multiple inputs. This is native behavior.
|
||||
- `@click` on `<lion-button>` and `@submit` on `<form>` are triggered by these use cases. We strongly encourage you to listen to the submit handler if your goal is to do something on form-submit.
|
||||
- To prevent form submission full page reloads, add a **submit handler on the form** `@submit` with `event.preventDefault()`. Adding it on the `<lion-button>` is not enough.
|
||||
|
||||
## Considerations
|
||||
|
||||
### Why a Web Component?
|
||||
|
||||
There are multiple reasons why we used a Web Component as opposed to a CSS component.
|
||||
|
||||
- **Target size**: The minimum target size is 40 pixels, which makes even the small buttons easy to activate. A container element was needed to make this size possible.
|
||||
- **Advanced styling**: There are advanced styling options regarding icons in buttons, where it is a lot more maintainable to handle icons in our button using slots. An example is that a sticky icon-only buttons may looks different from buttons which have both icons and text.
|
||||
- **Native form integration**: The lion button works with native `<form>` submission, and even implicit form submission on-enter. A lot of delegation logic had to be created for this to work.
|
||||
|
||||
### Event target
|
||||
|
||||
We want to ensure that the event target returned to the user is `<lion-button>`, not `button`. Therefore, simply delegating the click to the native button immediately, is not desired. Instead, we catch the click event in the `<lion-button>`, and ensure delegation inside of there.
|
||||
|
||||
### Flashing a native button click as a direct child of form
|
||||
|
||||
By delegating the `click()` to the native button, it will bubble back up to `<lion-button>` which would cause duplicate actions. We have to simulate the full `.click()` however, otherwise form submission is not triggered. So this bubbling cannot be prevented.
|
||||
Therefore, on click, we flash a `<button>` to the form as a direct child and fire the click on that button. We then immediately remove that button. This is a fully synchronous process; users or developers will not notice this, it should not cause problems.
|
||||
|
||||
### Native button & implicit form submission
|
||||
|
||||
Flashing the button in the way we do solves almost all issues except for one.
|
||||
One of the specs of W3C is that when you have a form with multiple inputs,
|
||||
pressing enter while inside one of the inputs only triggers a form submit if that form has a button of type submit.
|
||||
|
||||
To get this particular implicit form submission to work, having a native button in our `<lion-button>` is a hard requirement.
|
||||
Therefore, not only do we flash a native button on the form to delegate `<lion-button>` trigger to `<button>`
|
||||
and thereby trigger form submission, we **also** add a native `button` inside the `<lion-button>`
|
||||
whose `type` property is synchronized with the type of the `<lion-button>`.
|
||||
|
|
@ -2,10 +2,54 @@
|
|||
|
||||
`lion-calendar` is a reusable and accessible calendar view.
|
||||
|
||||
```js script
|
||||
import { html, css } from '@lion/core';
|
||||
import './lion-calendar.js';
|
||||
|
||||
export default {
|
||||
title: 'Others/Calendar',
|
||||
};
|
||||
|
||||
const calendarDemoStyle = css`
|
||||
.demo-calendar {
|
||||
border: 1px solid #adadad;
|
||||
box-shadow: 0 0 16px #ccc;
|
||||
max-width: 500px;
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
```js story
|
||||
export const main = () => {
|
||||
return html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar class="demo-calendar"></lion-calendar>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
## Live Demo/Documentation
|
||||
|
||||
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/calendar-standalone) for a live demo and API documentation
|
||||
|
||||
## Features
|
||||
|
||||
- fully accessible keyboard navigation (Arrow Keys, PgUp, PgDn, ALT+PgUp, ALT+PgDn)
|
||||
- **minDate**: disables all dates before a given date
|
||||
- **maxDate**: disables all dates after a given date
|
||||
- **disableDates**: disables some dates within an available range
|
||||
- **selectedDate**: currently selected date
|
||||
- **centralDate**: date that determines the currently visible month and that will be focused when keyboard moves the focus to the month grid
|
||||
- **focusedDate**: (getter only) currently focused date (if there is any with real focus)
|
||||
- **focusDate(date)**: focus on a certain date
|
||||
- **focusSelectedDate()**: focus on the current selected date
|
||||
- **focusCentralDate()**: focus on the current central date
|
||||
- **firstDayOfWeek**: typically Sunday (default) or Monday
|
||||
- **weekdayHeaderNotation**: long/short/narrow for the current locale (e.g. Thursday/Thu/T)
|
||||
- **locale**: different locale for the current component only
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
|
@ -23,3 +67,158 @@ import '@lion/calendar/lion-calendar.js';
|
|||
```html
|
||||
<lion-calendar></lion-calendar>
|
||||
```
|
||||
|
||||
## Move to the selected date
|
||||
|
||||
The `selectedDate` is the date which is currently marked as selected.
|
||||
You usually select a date by clicking on it with the mouse or hitting Enter on the keyboard.
|
||||
|
||||
The `selectedDate` might not be within the dates in the current month view.
|
||||
|
||||
```js preview-story
|
||||
export const selectedDate = () => html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar class="demo-calendar" .selectedDate=${new Date(1988, 2, 5)}></lion-calendar>
|
||||
`;
|
||||
```
|
||||
|
||||
## Central Date
|
||||
|
||||
The `centralDate` defines which day will be focused when keyboard moves the focus to the current month grid.
|
||||
By default it is set to today, or the enabled day of the current month view that is closest to today's date.
|
||||
|
||||
The next and previous months' buttons work by changing the `centralDate` with plus or minus one month.
|
||||
Changing the `centralDate` may mean a different view will be displayed to your users if it is in a different month.
|
||||
Usually if you change only the day, "nothing" happens as it's already currently in view.
|
||||
|
||||
The `centralDate` can be different from `selectedDate` as you can have today as actively selected but still look at date that is years ago.
|
||||
When the `selectedDate` changes, it will sync its value to the `centralDate`.
|
||||
|
||||
```js preview-story
|
||||
export const centralDate = () => {
|
||||
const today = new Date();
|
||||
const centralDate = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
||||
return html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar class="demo-calendar" .centralDate="${centralDate}"></lion-calendar>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
## Controlling focus
|
||||
|
||||
You can control the focus by calling the following methods
|
||||
|
||||
- `focusCentralDate()`
|
||||
- `focusSelectedDate()`
|
||||
- `focusDate(dateInstanceToFocus)`
|
||||
|
||||
Be aware that the central date changes when a new date is focused.
|
||||
|
||||
```js preview-story
|
||||
export const controllingFocus = () => {
|
||||
const today = new Date();
|
||||
const selectedDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
const centralDate = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
||||
return html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar
|
||||
id="js-demo-calendar"
|
||||
class="demo-calendar"
|
||||
.selectedDate="${selectedDate}"
|
||||
.centralDate="${centralDate}"
|
||||
></lion-calendar>
|
||||
<p>
|
||||
Focus
|
||||
<button @click="${() => document.querySelector('#js-demo-calendar').focusCentralDate()}">
|
||||
Central date
|
||||
</button>
|
||||
<button @click="${() => document.querySelector('#js-demo-calendar').focusSelectedDate()}">
|
||||
Selected date
|
||||
</button>
|
||||
<button @click="${() => document.querySelector('#js-demo-calendar').focusDate(today)}">
|
||||
Today
|
||||
</button>
|
||||
</p>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
## Limiting selectable values
|
||||
|
||||
### Providing a lower limit
|
||||
|
||||
To give a lower limit you can bind a date to the `minDate` property.
|
||||
|
||||
```js preview-story
|
||||
export const providingLowerLimit = () => {
|
||||
const minDate = new Date();
|
||||
return html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar class="demo-calendar" .minDate="${minDate}"></lion-calendar>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### Provide a higher limit
|
||||
|
||||
To give a higher limit you can bind a date to the `maxDate` property. In this example, we show how to create an offset of + 2 days.
|
||||
|
||||
```js preview-story
|
||||
export const providingHigherLimit = () => {
|
||||
const today = new Date();
|
||||
const maxDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2);
|
||||
return html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar class="demo-calendar" .maxDate="${maxDate}"></lion-calendar>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### Provide a list of disabled dates
|
||||
|
||||
In some cases a specific date or day of the week needs to be disabled, supply those days to the `disableDates` property.
|
||||
|
||||
```js preview-story
|
||||
export const disabledDates = () => html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar
|
||||
class="demo-calendar"
|
||||
.disableDates=${day => day.getDay() === 6 || day.getDay() === 0}
|
||||
></lion-calendar>
|
||||
`;
|
||||
```
|
||||
|
||||
### Combined disable dates
|
||||
|
||||
To limit the scope of possible dates further, combine the methods mentioned above.
|
||||
|
||||
```js preview-story
|
||||
export const combinedDisabledDates = () => {
|
||||
const today = new Date();
|
||||
const maxDate = new Date(today.getFullYear(), today.getMonth() + 2, today.getDate());
|
||||
return html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar
|
||||
class="demo-calendar"
|
||||
.disableDates=${day => day.getDay() === 6 || day.getDay() === 0}
|
||||
.minDate="${new Date()}"
|
||||
.maxDate="${maxDate}"
|
||||
></lion-calendar>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "../../scripts/npm-prepublish.js",
|
||||
"start": "cd ../../ && yarn dev-server --open packages/calendar/README.md",
|
||||
"test": "cd ../../ && yarn test:browser --grep \"packages/calendar/test/**/*.test.js\"",
|
||||
"test:watch": "cd ../../ && yarn test:browser:watch --grep \"packages/calendar/test/**/*.test.js\""
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,257 +0,0 @@
|
|||
import {
|
||||
Story,
|
||||
Meta,
|
||||
html,
|
||||
} from '@open-wc/demoing-storybook';
|
||||
import { css } from '@lion/core';
|
||||
|
||||
import '../lion-calendar.js';
|
||||
|
||||
<Meta title="Others/Calendar" parameters={{ component: 'lion-calendar' }} />
|
||||
|
||||
# Calendar
|
||||
|
||||
`lion-calendar` is a reusable and accessible calendar view.
|
||||
|
||||
export const calendarDemoStyle = css`
|
||||
.demo-calendar {
|
||||
border: 1px solid #adadad;
|
||||
box-shadow: 0 0 16px #ccc;
|
||||
max-width: 500px;
|
||||
}
|
||||
`;
|
||||
|
||||
<Story name="Default">
|
||||
{html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar class="demo-calendar"></lion-calendar>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-calendar></lion-calendar>
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- fully accessible keyboard navigation (Arrow Keys, PgUp, PgDn, ALT+PgUp, ALT+PgDn)
|
||||
- **minDate**: disables all dates before a given date
|
||||
- **maxDate**: disables all dates after a given date
|
||||
- **disableDates**: disables some dates within an available range
|
||||
- **selectedDate**: currently selected date
|
||||
- **centralDate**: date that determines the currently visible month and that will be focused when keyboard moves the focus to the month grid
|
||||
- **focusedDate**: (getter only) currently focused date (if there is any with real focus)
|
||||
- **focusDate(date)**: focus on a certain date
|
||||
- **focusSelectedDate()**: focus on the current selected date
|
||||
- **focusCentralDate()**: focus on the current central date
|
||||
- **firstDayOfWeek**: typically Sunday (default) or Monday
|
||||
- **weekdayHeaderNotation**: long/short/narrow for the current locale (e.g. Thursday/Thu/T)
|
||||
- **locale**: different locale for the current component only
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
npm i --save @lion/calendar
|
||||
```
|
||||
|
||||
```js
|
||||
import '@lion/calendar/lion-calendar.js';
|
||||
```
|
||||
|
||||
## Move to the selected date
|
||||
|
||||
The `selectedDate` is the date which is currently marked as selected.
|
||||
You usually select a date by clicking on it with the mouse or hitting Enter on the keyboard.
|
||||
|
||||
The `selectedDate` might not be within the dates in the current month view.
|
||||
|
||||
<Story name="Selected Date">
|
||||
{html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar class="demo-calendar" .selectedDate=${new Date(1988, 2, 5)}></lion-calendar>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-calendar .selectedDate="${new Date(1988, 2, 5)}"></lion-calendar>
|
||||
```
|
||||
|
||||
## Central Date
|
||||
|
||||
The `centralDate` defines which day will be focused when keyboard moves the focus to the current month grid.
|
||||
By default it is set to today, or the enabled day of the current month view that is closest to today's date.
|
||||
|
||||
The next and previous months' buttons work by changing the `centralDate` with plus or minus one month.
|
||||
Changing the `centralDate` may mean a different view will be displayed to your users if it is in a different month.
|
||||
Usually if you change only the day, "nothing" happens as it's already currently in view.
|
||||
|
||||
The `centralDate` can be different from `selectedDate` as you can have today as actively selected but still look at date that is years ago.
|
||||
When the `selectedDate` changes, it will sync its value to the `centralDate`.
|
||||
|
||||
<Story name="Central Date">
|
||||
{() => {
|
||||
const today = new Date();
|
||||
const centralDate = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
||||
return html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar class="demo-calendar" .centralDate="${centralDate}"></lion-calendar>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-calendar .centralDate="${new Date(1988, 2, 5)}"></lion-calendar>
|
||||
```
|
||||
|
||||
## Controlling focus
|
||||
|
||||
You can control the focus by calling the following methods
|
||||
- `focusCentralDate()`
|
||||
- `focusSelectedDate()`
|
||||
- `focusDate(dateInstanceToFocus)`
|
||||
|
||||
Be aware that the central date changes when a new date is focused.
|
||||
|
||||
<Story name="Controlling focus">
|
||||
{() => {
|
||||
const today = new Date();
|
||||
const selectedDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
const centralDate = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
||||
return html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar
|
||||
id="js-demo-calendar"
|
||||
class="demo-calendar"
|
||||
.selectedDate="${selectedDate}"
|
||||
.centralDate="${centralDate}"
|
||||
></lion-calendar>
|
||||
<p>
|
||||
Focus
|
||||
<button @click="${() => document.querySelector('#js-demo-calendar').focusCentralDate()}">
|
||||
Central date
|
||||
</button>
|
||||
<button @click="${() => document.querySelector('#js-demo-calendar').focusSelectedDate()}">
|
||||
Selected date
|
||||
</button>
|
||||
<button @click="${() => document.querySelector('#js-demo-calendar').focusDate(today)}">
|
||||
Today
|
||||
</button>
|
||||
</p>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
## Limiting selectable values
|
||||
|
||||
### Providing a lower limit
|
||||
|
||||
To give a lower limit you can bind a date to the `minDate` property.
|
||||
|
||||
<Story name="minDate">
|
||||
{() => {
|
||||
const minDate = new Date();
|
||||
return html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar class="demo-calendar" .minDate="${minDate}"></lion-calendar>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-calendar .minDate="${new Date()}"></lion-calendar>
|
||||
```
|
||||
|
||||
### Provide a higher limit
|
||||
|
||||
To give a higher limit you can bind a date to the `maxDate` property. In this example, we show how to create an offset of + 2 days.
|
||||
|
||||
<Story name="maxDate">
|
||||
{() => {
|
||||
const today = new Date();
|
||||
const maxDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2);
|
||||
return html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar class="demo-calendar" .maxDate="${maxDate}"></lion-calendar>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
const today = new Date();
|
||||
const maxDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2);
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-calendar .maxDate="${maxDate}"></lion-calendar>
|
||||
```
|
||||
|
||||
### Provide a list of disabled dates
|
||||
|
||||
In some cases a specific date or day of the week needs to be disabled, supply those days to the `disableDates` property.
|
||||
|
||||
<Story name="disableDates">
|
||||
{html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar
|
||||
class="demo-calendar"
|
||||
.disableDates=${day => day.getDay() === 6 || day.getDay() === 0}
|
||||
></lion-calendar>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-calendar
|
||||
.disableDates="${day => day.getDay() === 6 || day.getDay() === 0}"
|
||||
></lion-calendar>
|
||||
```
|
||||
|
||||
### Combined disable dates
|
||||
|
||||
To limit the scope of possible dates further, combine the methods mentioned above.
|
||||
|
||||
<Story name="Combined disable dates">
|
||||
{() => {
|
||||
const today = new Date();
|
||||
const maxDate = new Date(today.getFullYear(), today.getMonth() + 2, today.getDate());
|
||||
return html`
|
||||
<style>
|
||||
${calendarDemoStyle}
|
||||
</style>
|
||||
<lion-calendar
|
||||
class="demo-calendar"
|
||||
.disableDates=${day => day.getDay() === 6 || day.getDay() === 0}
|
||||
.minDate="${new Date()}"
|
||||
.maxDate="${maxDate}"
|
||||
></lion-calendar>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
const today = new Date();
|
||||
const maxDate = new Date(today.getFullYear(), today.getMonth() + 2, today.getDate());
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-calendar
|
||||
.disableDates=${day => day.getDay() === 6 || day.getDay() === 0}
|
||||
.minDate="${new Date()}"
|
||||
.maxDate="${maxDate}"
|
||||
></lion-calendar>
|
||||
```
|
||||
|
|
@ -1,13 +1,45 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Checkbox Group
|
||||
|
||||
`lion-checkbox-group` component enhances the functionality of the native `<input type="checkbox">` element. Its purpose is to provide a way for users to check **multiple** options amongst a set of choices, or to function as a single toggle.
|
||||
`lion-checkbox-group` component enhances the functionality of the native `<input type="checkbox">` element.
|
||||
Its purpose is to provide a way for users to check **multiple** options amongst a set of choices, or to function as a single toggle.
|
||||
|
||||
You should use `lion-checkbox` inside this element.
|
||||
> You should use [lion-checkbox](https://github.com/ing-bank/lion/tree/master/packages/checkbox) elements as the children of the `<lion-checkbox-group>`.
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import { Required, Validator, loadDefaultFeedbackMessages } from '@lion/validate';
|
||||
import './lion-checkbox-group.js';
|
||||
import './lion-checkbox.js';
|
||||
|
||||
export default {
|
||||
title: 'Forms/Checkbox Group',
|
||||
};
|
||||
|
||||
loadDefaultFeedbackMessages();
|
||||
```
|
||||
|
||||
```js story
|
||||
export const main = () => html`
|
||||
<lion-checkbox-group name="scientists[]" label="Favorite scientists">
|
||||
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
|
||||
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
|
||||
<lion-checkbox label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
`;
|
||||
```
|
||||
|
||||
> Make sure that the checkbox-group also has a name attribute, this is necessary for the [lion-form](?path=/docs/forms-form-overview--page)'s serialization result.
|
||||
|
||||
## Live Demo/Documentation
|
||||
|
||||
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/forms-checkbox-group--default-story) for a live demo and API documentation
|
||||
|
||||
## Features
|
||||
|
||||
Since it extends from [lion-fieldset](?path=/docs/forms-fieldset-overview--page), it has all the features a fieldset has.
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
|
@ -21,6 +53,26 @@ import '@lion/checkbox-group/lion-checkbox-group.js';
|
|||
import '@lion/checkbox-group/lion-checkbox.js';
|
||||
```
|
||||
|
||||
### Model value
|
||||
|
||||
The `modelValue` of a `lion-checkbox-group` is an array containing the `choiceValues` of the `lion-checkbox` elements that have been checked.
|
||||
|
||||
Given the scientists example above, say that we were to select the first and last options (Archimedes & Marie Curie).
|
||||
|
||||
Then the `modelValue` of the `lion-checkbox-group` will look as follows:
|
||||
|
||||
```js
|
||||
const groupElement = [parent].querySelector('lion-checkbox-group');
|
||||
groupElement.modelValue;
|
||||
=> ["Archimedes", "Marie Curie"];
|
||||
```
|
||||
|
||||
### The `name` attribute
|
||||
|
||||
The `name` attribute of a `lion-checkbox-group` automatically gets assigned to its `lion-checkbox` children. You can also specify names for the `lion-checkbox` elements, but if this name is different from the name assigned to `lion-checkbox-group`, then an exception will be thrown.
|
||||
|
||||
Our recommendation would be to set the `name` attribute only on the `lion-checkbox-group` and not on the `lion-checkbox` elements.
|
||||
|
||||
### Example
|
||||
|
||||
```html
|
||||
|
|
@ -33,3 +85,102 @@ import '@lion/checkbox-group/lion-checkbox.js';
|
|||
<lion-checkbox label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
```
|
||||
|
||||
### Pre-select
|
||||
|
||||
You can pre-select options by targeting the `modelValue` object of the option and setting the `checked` property to `true`.
|
||||
|
||||
```js preview-story
|
||||
export const preselect = () => html`
|
||||
<lion-checkbox-group name="scientists" label="Favorite scientists">
|
||||
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
|
||||
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'} checked></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Marie Curie"
|
||||
.modelValue=${{ value: 'Marie Curie', checked: true }}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
`;
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
You can disable the entire group by setting the `disabled` attribute on the `<lion-checkbox-group>`.
|
||||
|
||||
```js preview-story
|
||||
export const disabled = () => html`
|
||||
<lion-checkbox-group name="scientists[]" label="Favorite scientists" disabled>
|
||||
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
|
||||
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Marie Curie"
|
||||
.modelValue=${{ value: 'Marie Curie', checked: true }}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
`;
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
You can apply validation to the `<lion-checkbox-group>`, similar to how you would do so in any fieldset.
|
||||
The interaction states of the `<lion-checkbox-group>` are evaluated in order to hide or show feedback messages.
|
||||
|
||||
```js preview-story
|
||||
export const validation = () => {
|
||||
const validate = () => {
|
||||
const checkboxGroup = document.querySelector('#scientists');
|
||||
checkboxGroup.submitted = !checkboxGroup.submitted;
|
||||
};
|
||||
return html`
|
||||
<lion-checkbox-group
|
||||
id="scientists"
|
||||
name="scientists[]"
|
||||
label="Favorite scientists"
|
||||
.validators=${[new Required()]}
|
||||
>
|
||||
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
|
||||
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
|
||||
<lion-checkbox label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<button @click="${() => validate()}">Validate</button>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### Validation advanced
|
||||
|
||||
Below is a more advanced validator on the group that evaluates the children checkboxes' checked states.
|
||||
|
||||
```js preview-story
|
||||
export const validationAdvanced = () => {
|
||||
class HasMinTwoChecked extends Validator {
|
||||
execute(value) {
|
||||
return value.length < 2;
|
||||
}
|
||||
static get validatorName() {
|
||||
return 'HasMinTwoChecked';
|
||||
}
|
||||
static async getMessage() {
|
||||
return 'You need to select at least 2 values.';
|
||||
}
|
||||
}
|
||||
const validate = () => {
|
||||
const checkboxGroup = document.querySelector('#scientists2');
|
||||
checkboxGroup.submitted = !checkboxGroup.submitted;
|
||||
};
|
||||
return html`
|
||||
<lion-checkbox-group
|
||||
id="scientists2"
|
||||
name="scientists[]"
|
||||
label="Favorite scientists"
|
||||
help-text="You should have at least 2 of those"
|
||||
.validators=${[new Required(), new HasMinTwoChecked()]}
|
||||
>
|
||||
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
|
||||
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
|
||||
<lion-checkbox label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<button @click="${() => validate()}">Validate</button>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "../../scripts/npm-prepublish.js",
|
||||
"start": "cd ../../ && yarn dev-server --open packages/checkbox-group/README.md",
|
||||
"test": "cd ../../ && yarn test:browser --grep \"packages/checkbox-group/test/**/*.test.js\"",
|
||||
"test:watch": "cd ../../ && yarn test:browser:watch --grep \"packages/checkbox-group/test/**/*.test.js\""
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,328 +0,0 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
import { Required, Validator, loadDefaultFeedbackMessages } from '@lion/validate';
|
||||
|
||||
import '../lion-checkbox-group.js';
|
||||
import '../lion-checkbox.js';
|
||||
|
||||
<Meta title="Forms/Checkbox Group" parameters={{ component: 'lion-checkbox-group' }} />
|
||||
|
||||
# Checkbox Group
|
||||
|
||||
`lion-checkbox-group` component enhances the functionality of the native `<input type="checkbox">` element.
|
||||
Its purpose is to provide a way for users to check **multiple** options amongst a set of choices, or to function as a single toggle.
|
||||
|
||||
> You should use <a href="https://github.com/ing-bank/lion/tree/master/packages/checkbox" target="_blank">`<lion-checkbox>`</a> elements as the children of the `<lion-checkbox-group>`.
|
||||
|
||||
<Story name="Default">
|
||||
{html`
|
||||
<lion-checkbox-group
|
||||
name="scientists[]"
|
||||
label="Favorite scientists"
|
||||
>
|
||||
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
|
||||
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
|
||||
<lion-checkbox label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-checkbox-group
|
||||
name="scientists[]"
|
||||
label="Favourite scientists"
|
||||
>
|
||||
<lion-checkbox label="Archimedes" .choiceValue=${'Archimedes'}></lion-checkbox>
|
||||
<lion-checkbox label="Francis Bacon" .choiceValue=${'Francis Bacon'}></lion-checkbox>
|
||||
<lion-checkbox label="Marie Curie" .choiceValue=${'Marie Curie'}></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
```
|
||||
|
||||
> Make sure that the checkbox-group also has a name attribute, this is necessary for the [lion-form](?path=/docs/forms-form-overview--page)'s serialization result.
|
||||
|
||||
## Features
|
||||
|
||||
Since it extends from [lion-fieldset](?path=/docs/forms-fieldset-overview--page), it has all the features a fieldset has.
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
npm i --save @lion/checkbox-group
|
||||
```
|
||||
|
||||
```js
|
||||
import '@lion/checkbox-group/lion-checkbox-group.js';
|
||||
import '@lion/checkbox-group/lion-checkbox.js';
|
||||
```
|
||||
|
||||
### Model value
|
||||
|
||||
The `modelValue` of a `lion-checkbox-group` is an array containing the `choiceValues` of the `lion-checkbox` elements that have been checked.
|
||||
|
||||
Given the scientists example above, say that we were to select the first and last options (Archimedes & Marie Curie).
|
||||
|
||||
Then the `modelValue` of the `lion-checkbox-group` will look as follows:
|
||||
|
||||
```js
|
||||
const groupElement = [parent].querySelector('lion-checkbox-group');
|
||||
groupElement.modelValue;
|
||||
=> ["Archimedes", "Marie Curie"];
|
||||
```
|
||||
|
||||
### The `name` attribute
|
||||
|
||||
The `name` attribute of a `lion-checkbox-group` automatically gets assigned to its `lion-checkbox` children. You can also specify names for the `lion-checkbox` elements, but if this name is different from the name assigned to `lion-checkbox-group`, then an exception will be thrown.
|
||||
|
||||
Our recommendation would be to set the `name` attribute only on the `lion-checkbox-group` and not on the `lion-checkbox` elements.
|
||||
|
||||
## Examples
|
||||
|
||||
### Pre-select
|
||||
|
||||
You can pre-select options by targeting the `modelValue` object of the option and setting the `checked` property to `true`.
|
||||
|
||||
<Story name="Pre-select">
|
||||
{html`
|
||||
<lion-checkbox-group name="scientists" label="Favorite scientists">
|
||||
<lion-checkbox
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Francis Bacon"
|
||||
.choiceValue=${'Francis Bacon'}
|
||||
checked
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Marie Curie"
|
||||
.modelValue=${{ value: 'Marie Curie', checked: true }}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-checkbox-group name="scientists[]" label="Favorite scientists">
|
||||
<lion-checkbox
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Francis Bacon"
|
||||
.choiceValue=${'Francis Bacon'}
|
||||
checked
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Marie Curie"
|
||||
.modelValue=${{ value: 'Marie Curie', checked: true }}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
You can disable the entire group by setting the `disabled` attribute on the `<lion-checkbox-group>`.
|
||||
|
||||
<Story name="Disabled">
|
||||
{html`
|
||||
<lion-checkbox-group name="scientists[]" label="Favorite scientists" disabled>
|
||||
<lion-checkbox
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Francis Bacon"
|
||||
.choiceValue=${'Francis Bacon'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Marie Curie"
|
||||
.modelValue=${{ value: 'Marie Curie', checked: true }}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-checkbox-group name="scientists[]" label="Favorite scientists" disabled>
|
||||
<lion-checkbox
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Francis Bacon"
|
||||
.choiceValue=${'Francis Bacon'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Marie Curie"
|
||||
.modelValue=${{ value: 'Marie Curie', checked: true }}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
You can apply validation to the `<lion-checkbox-group>`, similar to how you would do so in any fieldset.
|
||||
The interaction states of the `<lion-checkbox-group>` are evaluated in order to hide or show feedback messages.
|
||||
|
||||
<Story name="Validation">
|
||||
{() => {
|
||||
loadDefaultFeedbackMessages();
|
||||
const validate = () => {
|
||||
const checkboxGroup = document.querySelector('#scientists');
|
||||
checkboxGroup.submitted = !checkboxGroup.submitted;
|
||||
};
|
||||
return html`
|
||||
<lion-checkbox-group
|
||||
id="scientists"
|
||||
name="scientists[]"
|
||||
label="Favorite scientists"
|
||||
.validators=${[new Required()]}
|
||||
>
|
||||
<lion-checkbox
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Francis Bacon"
|
||||
.choiceValue=${'Francis Bacon'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Marie Curie"
|
||||
.choiceValue=${'Marie Curie'}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<button @click="${() => validate()}">Validate</button>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
import { Required, loadDefaultFeedbackMessages } from '@lion/validate';
|
||||
loadDefaultFeedbackMessages();
|
||||
const validate = () => {
|
||||
const checkboxGroup = document.querySelector('#scientists');
|
||||
checkboxGroup.submitted = !checkboxGroup.submitted;
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-checkbox-group
|
||||
id="scientists"
|
||||
name="scientists[]"
|
||||
label="Favorite scientists"
|
||||
.validators=${[new Required()]}
|
||||
>
|
||||
<lion-checkbox
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Francis Bacon"
|
||||
.choiceValue=${'Francis Bacon'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Marie Curie"
|
||||
.choiceValue=${'Marie Curie'}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<button @click="${() => validate()}">Validate</button>
|
||||
```
|
||||
|
||||
### Validation advanced
|
||||
|
||||
Below is a more advanced validator on the group that evaluates the children checkboxes' checked states.
|
||||
|
||||
<Story name="Validation Advanced">
|
||||
{() => {
|
||||
loadDefaultFeedbackMessages();
|
||||
class HasMinTwoChecked extends Validator {
|
||||
execute(value) {
|
||||
return value.length < 2;
|
||||
}
|
||||
static get validatorName() {
|
||||
return 'HasMinTwoChecked';
|
||||
}
|
||||
static async getMessage() {
|
||||
return 'You need to select at least 2 values.';
|
||||
}
|
||||
}
|
||||
const validate = () => {
|
||||
const checkboxGroup = document.querySelector('#scientists2');
|
||||
checkboxGroup.submitted = !checkboxGroup.submitted;
|
||||
};
|
||||
return html`
|
||||
<lion-checkbox-group
|
||||
id="scientists2"
|
||||
name="scientists[]"
|
||||
label="Favorite scientists"
|
||||
help-text="You should have at least 2 of those"
|
||||
.validators=${[new Required(), new HasMinTwoChecked()]}
|
||||
>
|
||||
<lion-checkbox
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Francis Bacon"
|
||||
.choiceValue=${'Francis Bacon'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Marie Curie"
|
||||
.choiceValue=${'Marie Curie'}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<button @click="${() => validate()}">Validate</button>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
import { Required, Validator, loadDefaultFeedbackMessages } from '@lion/validate';
|
||||
|
||||
loadDefaultFeedbackMessages();
|
||||
|
||||
class HasMinTwoChecked extends Validator {
|
||||
|
||||
execute(value) {
|
||||
return value.length < 2;
|
||||
}
|
||||
|
||||
static get validatorName() {
|
||||
return 'HasMinTwoChecked';
|
||||
}
|
||||
|
||||
static async getMessage() {
|
||||
return 'You need to select at least 2 values.';
|
||||
}
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
const checkboxGroup = document.querySelector('#scientists2');
|
||||
checkboxGroup.submitted = !checkboxGroup.submitted;
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-checkbox-group
|
||||
id="scientists2"
|
||||
name="scientists[]"
|
||||
label="Favorite scientists"
|
||||
help-text="You should have at least 2 of those"
|
||||
.validators=${[new Required(), new HasMinTwoChecked()]}
|
||||
>
|
||||
<lion-checkbox
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Francis Bacon"
|
||||
.choiceValue=${'Francis Bacon'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
label="Marie Curie"
|
||||
.choiceValue=${'Marie Curie'}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<button @click="${() => validate()}">Validate</button>
|
||||
```
|
||||
|
|
@ -1,11 +1,19 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# ChoiceInputMixin
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Forms/System/Choice',
|
||||
};
|
||||
```
|
||||
|
||||
`lion-choice-input` mixin is a fundamental building block of form controls which return a checked-state. It is used in:
|
||||
|
||||
- [lion-checkbox](../checkbox/)
|
||||
- [lion-option](../option/))
|
||||
- [lion-radio](../radio/))
|
||||
- [lion-switch](../switch/))
|
||||
- [lion-option](../option/)
|
||||
- [lion-radio](../radio/)
|
||||
- [lion-switch](../switch/)
|
||||
|
||||
## Features
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,35 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Core
|
||||
|
||||
`lion-input-amount` component is based on the generic text input field. Its purpose is to provide a way for users to fill in an amount.
|
||||
The `@lion/core` package is mostly for in depth usage.
|
||||
It handles the version of `lit-element` and `lit-html`.
|
||||
|
||||
In order to be sure a compatible version is used import it via this package.
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Others/System/Core',
|
||||
};
|
||||
```
|
||||
|
||||
```js
|
||||
import { LitElement, html, render } from '@lion/core';
|
||||
```
|
||||
|
||||
## Live Demo/Documentation
|
||||
|
||||
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/core) for a live demo and API documentation
|
||||
|
||||
## Features
|
||||
|
||||
- [function to deduplicate mixins (dedupeMixin)](#deduping-of-mixins)
|
||||
- Mixin to handle disabled (DisabledMixin)
|
||||
- Mixin to handle disabled AND tabIndex (DisabledWithTabIndexMixin)
|
||||
- Mixin to manage auto generated needed slot elements in light dom (SlotMixin)
|
||||
|
||||
> These features are not well documented - care to help out?
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
|
@ -25,3 +49,46 @@ const BaseMixin = dedupeMixin((superClass) => {
|
|||
return class extends superClass { ... };
|
||||
});
|
||||
```
|
||||
|
||||
## Deduping of mixins
|
||||
|
||||
### Why is deduping of mixins necessary?
|
||||
|
||||
Imagine you are developing web components and creating ES classes for Custom Elements. You have two generic mixins (let's say `M1` and `M2`) which require independently the same even more generic mixin (`BaseMixin`). `M1` and `M2` can be used independently, that means they have to inherit from `BaseMixin` also independently. But they can be also used in combination. Sometimes `M1` and `M2` are used in the same component and can mess up the inheritance chain if `BaseMixin` is applied twice.
|
||||
In other words, this may happen to the protoype chain `... -> M2 -> BaseMixin -> M1 -> BaseMixin -> ...`.
|
||||
|
||||
An example of this may be a `LocalizeMixin` used across different components and mixins. Some mixins may need it and many components need it too and can not rely on other mixins to have it by default, so must inherit from it independently.
|
||||
|
||||
The more generic the mixin is, the higher the chance of being appliend more than once. As a mixin author you can't control how it is used, and can't always predict it. So as a safety measure it is always recommended to create deduping mixins.
|
||||
|
||||
### Usage of dedupeMixin()
|
||||
|
||||
This is an example of how to make a conventional ES mixin deduping.
|
||||
|
||||
```js
|
||||
const BaseMixin = dedupeMixin((superClass) => {
|
||||
return class extends superClass { ... };
|
||||
});
|
||||
|
||||
// inherits from BaseMixin
|
||||
const M1 = dedupeMixin((superClass) => {
|
||||
return class extends BaseMixin(superClass) { ... };
|
||||
});
|
||||
|
||||
// inherits from BaseMixin
|
||||
const M2 = dedupeMixin((superClass) => {
|
||||
return class extends BaseMixin(superClass) { ... };
|
||||
});
|
||||
|
||||
// component inherits from M1
|
||||
// MyCustomElement -> M1 -> BaseMixin -> BaseCustomElement;
|
||||
class MyCustomElement extends M1(BaseCustomElement) { ... }
|
||||
|
||||
// component inherits from M2
|
||||
// MyCustomElement -> M2 -> BaseMixin -> BaseCustomElement;
|
||||
class MyCustomElement extends M2(BaseCustomElement) { ... }
|
||||
|
||||
// component inherits from both M1 and M2
|
||||
// MyCustomElement -> M2 -> M1 -> BaseMixin -> BaseCustomElement;
|
||||
class MyCustomElement extends M2(M1(BaseCustomElement)) { ... }
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
|
||||
<Meta title="Others/System/Core" />
|
||||
|
||||
# Core
|
||||
|
||||
The `@lion/core` package is mostly for in depth usage.
|
||||
It handles the version of `lit-element` and `lit-html`.
|
||||
|
||||
In order to be sure a compatible version is used import it via this package.
|
||||
|
||||
```js
|
||||
import { LitElement, html, render } from '@lion/core';
|
||||
```
|
||||
|
||||
It offers low level functionality for
|
||||
|
||||
- [function to deduplicate mixins (dedupeMixin)](#deduping-of-mixins)
|
||||
- Mixin to handle disabled (DisabledMixin)
|
||||
- Mixin to handle disabled AND tabIndex (DisabledWithTabIndexMixin)
|
||||
- Mixin to manage auto generated needed slot elements in light dom (SlotMixin)
|
||||
|
||||
|
||||
> These features are not well documented - care to help out?
|
||||
|
||||
## Deduping of mixins
|
||||
|
||||
### Why is deduping of mixins necessary?
|
||||
|
||||
Imagine you are developing web components and creating ES classes for Custom Elements. You have two generic mixins (let's say `M1` and `M2`) which require independently the same even more generic mixin (`BaseMixin`). `M1` and `M2` can be used independently, that means they have to inherit from `BaseMixin` also independently. But they can be also used in combination. Sometimes `M1` and `M2` are used in the same component and can mess up the inheritance chain if `BaseMixin` is applied twice.
|
||||
In other words, this may happen to the protoype chain `... -> M2 -> BaseMixin -> M1 -> BaseMixin -> ...`.
|
||||
|
||||
An example of this may be a `LocalizeMixin` used across different components and mixins. Some mixins may need it and many components need it too and can not rely on other mixins to have it by default, so must inherit from it independently.
|
||||
|
||||
The more generic the mixin is, the higher the chance of being appliend more than once. As a mixin author you can't control how it is used, and can't always predict it. So as a safety measure it is always recommended to create deduping mixins.
|
||||
|
||||
### Usage of dedupeMixin()
|
||||
|
||||
This is an example of how to make a conventional ES mixin deduping.
|
||||
|
||||
```js
|
||||
const BaseMixin = dedupeMixin((superClass) => {
|
||||
return class extends superClass { ... };
|
||||
});
|
||||
|
||||
// inherits from BaseMixin
|
||||
const M1 = dedupeMixin((superClass) => {
|
||||
return class extends BaseMixin(superClass) { ... };
|
||||
});
|
||||
|
||||
// inherits from BaseMixin
|
||||
const M2 = dedupeMixin((superClass) => {
|
||||
return class extends BaseMixin(superClass) { ... };
|
||||
});
|
||||
|
||||
// component inherits from M1
|
||||
// MyCustomElement -> M1 -> BaseMixin -> BaseCustomElement;
|
||||
class MyCustomElement extends M1(BaseCustomElement) { ... }
|
||||
|
||||
// component inherits from M2
|
||||
// MyCustomElement -> M2 -> BaseMixin -> BaseCustomElement;
|
||||
class MyCustomElement extends M2(BaseCustomElement) { ... }
|
||||
|
||||
// component inherits from both M1 and M2
|
||||
// MyCustomElement -> M2 -> M1 -> BaseMixin -> BaseCustomElement;
|
||||
class MyCustomElement extends M2(M1(BaseCustomElement)) { ... }
|
||||
```
|
||||
|
|
@ -1,12 +1,54 @@
|
|||
<<<<<<< HEAD
|
||||
=======
|
||||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
>>>>>>> feat: use markdown javascript (mdjs) for documentation
|
||||
# Dialog
|
||||
|
||||
`lion-dialog` is a component wrapping a modal dialog controller.
|
||||
Its purpose is to make it easy to use our Overlay System declaratively.
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import demoStyle from './docs/demo-dialog-style.js';
|
||||
import './docs/styled-dialog-content.js';
|
||||
import './lion-dialog.js';
|
||||
|
||||
export default {
|
||||
title: 'Overlays/Dialog',
|
||||
};
|
||||
```
|
||||
|
||||
```js story
|
||||
export const main = () => html`
|
||||
<style>
|
||||
${demoStyle}
|
||||
</style>
|
||||
<lion-dialog>
|
||||
<button slot="invoker">Click me to open dialog</button>
|
||||
<div slot="content" class="dialog">
|
||||
Hello! You can close this dialog here:
|
||||
<button
|
||||
class="close-button"
|
||||
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
|
||||
>
|
||||
⨯
|
||||
</button>
|
||||
</div>
|
||||
</lion-dialog>
|
||||
`;
|
||||
```
|
||||
|
||||
## Live Demo/Documentation
|
||||
|
||||
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/overlays-specific-wc-dialog) for a live demo and documentation
|
||||
|
||||
## Features
|
||||
|
||||
- Show content when clicking the invoker
|
||||
- Respond to close event in the slot="content" element, to close the content
|
||||
- Have a `.config` object to set or update the OverlayController's configuration
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
|
@ -16,9 +58,27 @@ npm i --save @lion/dialog
|
|||
```
|
||||
|
||||
```js
|
||||
import { LionDialog } from '@lion/dialog';
|
||||
// or
|
||||
import '@lion/dialog/lion-dialog.js';
|
||||
```
|
||||
|
||||
## Usage notes
|
||||
|
||||
- Your `slot="content"` node will be moved to the global overlay container during initialization.
|
||||
After, your content node is no longer a child of `lion-dialog`.
|
||||
If you still need to access it from the `lion-dialog` you can do so by using the `._overlayContentNode` property.
|
||||
- To close the overlay from within the content node, you need to dispatch a `close-overlay` event that bubbles.
|
||||
It has to be able to reach the content node.
|
||||
- If you need to traverse shadow boundaries, you will have to add `composed: true` as well, although this is discouraged as a practice.
|
||||
|
||||
## Changing the configuration
|
||||
|
||||
You can use the `config` property on the dialog to change the configuration.
|
||||
The documentation of the full config object can be found in the `lion/overlay` package or here in [Overlay System - Configuration](/?path=/docs/overlays-system-configuration--placement-mode-local).
|
||||
|
||||
The `config` property uses a setter to merge the passed configuration with the current, so you only **overwrite what you pass** when updating `config`.
|
||||
|
||||
### Example
|
||||
|
||||
```html
|
||||
|
|
@ -30,3 +90,118 @@ import '@lion/dialog/lion-dialog.js';
|
|||
<button slot="invoker">Click me</button>
|
||||
</lion-dialog>
|
||||
```
|
||||
|
||||
### Styling content
|
||||
|
||||
It's not possible to style content from the dialog component. This is because the content slot is moved to the global root node. This is why a custom component should be created and slotted in as the content. This ensures style encapsulation on the dialog content.
|
||||
|
||||
```js preview-story
|
||||
export const stylingContent = () => html`
|
||||
<style>
|
||||
${demoStyle}
|
||||
</style>
|
||||
<lion-dialog .config=${{ hidesOnOutsideClick: true, hidesOnEsc: true }}>
|
||||
<button slot="invoker">Styled dialog</button>
|
||||
<styled-dialog-content slot="content"></styled-dialog-content>
|
||||
</lion-dialog>
|
||||
`;
|
||||
```
|
||||
|
||||
### Close overlay from component slotted as content
|
||||
|
||||
The overlay cannot be closed by dispatching the `close-overlay` from a button in a styled component that is slotted in as content, because it will not cross the shadow boundary of the component. A method should be created that will dispatch the `close-overlay` event from the component.
|
||||
|
||||
```js preview-story
|
||||
export const closeOverlayFromComponent = () => html`
|
||||
<style>
|
||||
${demoStyle}
|
||||
</style>
|
||||
<lion-dialog .config=${{ hidesOnOutsideClick: true, hidesOnEsc: true }}>
|
||||
<button slot="invoker">Styled dialog</button>
|
||||
<styled-dialog-content slot="content"></styled-dialog-content>
|
||||
</lion-dialog>
|
||||
`;
|
||||
```
|
||||
|
||||
### Placement overrides
|
||||
|
||||
```js preview-story
|
||||
export const placementOverrides = () => {
|
||||
const dialog = placement => html`
|
||||
<lion-dialog .config=${{ viewportConfig: { placement } }}>
|
||||
<button slot="invoker">Dialog ${placement}</button>
|
||||
<div slot="content" class="dialog">
|
||||
Hello! You can close this notification here:
|
||||
<button
|
||||
class="close-button"
|
||||
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
|
||||
>
|
||||
⨯
|
||||
</button>
|
||||
</div>
|
||||
</lion-dialog>
|
||||
`;
|
||||
return html`
|
||||
<style>
|
||||
${demoStyle}
|
||||
</style>
|
||||
<div class="demo-box_placements">
|
||||
${dialog('center')} ${dialog('top-left')} ${dialog('top-right')} ${dialog('bottom-left')} ${dialog(
|
||||
'bottom-right',
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
Configuration passed to `config` property:
|
||||
|
||||
```js
|
||||
{
|
||||
viewportConfig: {
|
||||
placement: ... // <-- choose a position
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Other overrides
|
||||
|
||||
No backdrop, hides on escape, prevents scrolling while opened, and focuses the body when hiding.
|
||||
|
||||
```js preview-story
|
||||
export const otherOverrides = () => html`
|
||||
<style>
|
||||
${demoStyle}
|
||||
</style>
|
||||
<lion-dialog
|
||||
.config=${{
|
||||
hasBackdrop: false,
|
||||
hidesOnEscape: true,
|
||||
preventsScroll: true,
|
||||
elementToFocusAfterHide: document.body,
|
||||
}}
|
||||
>
|
||||
<button slot="invoker">Click me to open dialog</button>
|
||||
<div slot="content" class="dialog">
|
||||
Hello! You can close this dialog here:
|
||||
<button
|
||||
class="close-button"
|
||||
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
|
||||
>
|
||||
⨯
|
||||
</button>
|
||||
</div>
|
||||
</lion-dialog>
|
||||
`;
|
||||
```
|
||||
|
||||
Configuration passed to `config` property:
|
||||
|
||||
```js
|
||||
{
|
||||
hasBackdrop: false,
|
||||
hidesOnEscape: true,
|
||||
preventsScroll: true,
|
||||
elementToFocusAfterHide: document.body
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "../../scripts/npm-prepublish.js",
|
||||
"start": "cd ../../ && yarn dev-server --open packages/dialog/README.md",
|
||||
"test": "cd ../../ && yarn test:browser --grep \"packages/dialog/test/**/*.test.js\"",
|
||||
"test:watch": "cd ../../ && yarn test:browser:watch --grep \"packages/dialog/test/**/*.test.js\""
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,279 +0,0 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
import demoStyle from './demo-dialog-style.js';
|
||||
import './styled-dialog-content.js';
|
||||
import '../lion-dialog.js';
|
||||
|
||||
<Meta title="Overlays/Dialog" parameters={{ component: 'lion-dialog' }} />
|
||||
|
||||
# Dialog
|
||||
|
||||
`lion-dialog` is a component wrapping a modal dialog controller.
|
||||
Its purpose is to make it easy to use our Overlay System declaratively.
|
||||
|
||||
<Story name="Default">
|
||||
{html`
|
||||
<style>${demoStyle}</style>
|
||||
<lion-dialog>
|
||||
<button slot="invoker">Click me to open dialog</button>
|
||||
<div slot="content" class="dialog">
|
||||
Hello! You can close this dialog here:
|
||||
<button
|
||||
class="close-button"
|
||||
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
|
||||
>
|
||||
⨯
|
||||
</button>
|
||||
</div>
|
||||
</lion-dialog>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-dialog>
|
||||
<button slot="invoker">Click me to open dialog</button>
|
||||
<div slot="content" class="dialog">
|
||||
Hello! You can close this dialog here:
|
||||
<button
|
||||
class="close-button"
|
||||
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
|
||||
>
|
||||
⨯
|
||||
</button>
|
||||
</div>
|
||||
</lion-dialog>
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Show content when clicking the invoker
|
||||
- Respond to close event in the slot="content" element, to close the content
|
||||
- Have a `.config` object to set or update the OverlayController's configuration
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
npm i --save @lion/dialog
|
||||
```
|
||||
|
||||
```js
|
||||
import '@lion/dialog/lion-dialog.js';
|
||||
```
|
||||
|
||||
## Usage notes
|
||||
|
||||
- Your `slot="content"` node will be moved to the global overlay container during initialization.
|
||||
After, your content node is no longer a child of `lion-dialog`.
|
||||
If you still need to access it from the `lion-dialog` you can do so by using the `._overlayContentNode` property.
|
||||
- To close the overlay from within the content node, you need to dispatch a `close-overlay` event that bubbles.
|
||||
It has to be able to reach the content node.
|
||||
- If you need to traverse shadow boundaries, you will have to add `composed: true` as well, although this is discouraged as a practice.
|
||||
|
||||
## Changing the configuration
|
||||
|
||||
You can use the `config` property on the dialog to change the configuration.
|
||||
The documentation of the full config object can be found in the `lion/overlay` package or here in [Overlay System - Configuration](/?path=/docs/overlays-system-configuration--placement-mode-local).
|
||||
|
||||
The `config` property uses a setter to merge the passed configuration with the current, so you only **overwrite what you pass** when updating `config`.
|
||||
|
||||
### Styling content
|
||||
|
||||
It's not possible to style content from the dialog component. This is because the content slot is moved to the global root node. This is why a custom component should be created and slotted in as the content. This ensures style encapsulation on the dialog content.
|
||||
|
||||
<Story name="Styling content">
|
||||
{() => {
|
||||
return html`
|
||||
<style>${demoStyle}</style>
|
||||
<lion-dialog .config=${{ hidesOnOutsideClick: true, hidesOnEsc: true }}>
|
||||
<button slot="invoker">Styled dialog</button>
|
||||
<styled-dialog-content slot="content"></styled-dialog-content>
|
||||
</lion-dialog>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-dialog .config=${{ hidesOnOutsideClick: true, hidesOnEsc: true }}>
|
||||
<button slot="invoker">Styled dialog</button>
|
||||
<styled-dialog-content slot="content"></styled-dialog-content>
|
||||
</lion-dialog>
|
||||
```
|
||||
|
||||
```js
|
||||
class StyledDialogContent extends LitElement {
|
||||
static get styles() {
|
||||
return [
|
||||
css`
|
||||
:host {
|
||||
background-color: #fff;
|
||||
}
|
||||
.nice {
|
||||
font-weight: bold;
|
||||
color: green;
|
||||
}
|
||||
.close-button {
|
||||
color: black;
|
||||
font-size: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
`
|
||||
];
|
||||
}
|
||||
|
||||
_closeOverlay() {
|
||||
this.dispatchEvent(new Event('close-overlay', { bubbles: true }));
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div><p>Hello person who opened the dialog!</p></div>
|
||||
<div><p>Look how nice this <span class="nice">dialog</span> looks!</p>/div>
|
||||
<button
|
||||
class="close-button"
|
||||
@click=${this._closeOverlay}
|
||||
>⨯
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('styled-dialog-content',StyledDialogContent);
|
||||
```
|
||||
|
||||
### Close overlay from component slotted as content
|
||||
|
||||
The overlay cannot be closed by dispatching the `close-overlay` from a button in a styled component that is slotted in as content, because it will not cross the shadow boundary of the component. A method should be created that will dispatch the `close-overlay` event from the component.
|
||||
|
||||
<Story name="Close overlay from component">
|
||||
{() => {
|
||||
return html`
|
||||
<style>${demoStyle}</style>
|
||||
<lion-dialog .config=${{ hidesOnOutsideClick: true, hidesOnEsc: true }}>
|
||||
<button slot="invoker">Styled dialog</button>
|
||||
<styled-dialog-content slot="content"></styled-dialog-content>
|
||||
</lion-dialog>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
_closeOverlay() {
|
||||
this.dispatchEvent(new Event('close-overlay', { bubbles: true }));
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div><p>Hello person who opened the dialog!</p></div>
|
||||
<div><p>Look how nice this <span class="nice">dialog</span> looks!</p>/div>
|
||||
<button
|
||||
class="close-button"
|
||||
@click=${this._closeOverlay}
|
||||
>⨯
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
```
|
||||
### Placement overrides
|
||||
|
||||
<Story name="Placement overrides">
|
||||
{() => {
|
||||
const dialog = placement => html`
|
||||
<lion-dialog .config=${{ viewportConfig: { placement } }}>
|
||||
<button slot="invoker">Dialog ${placement}</button>
|
||||
<div slot="content" class="dialog">
|
||||
Hello! You can close this notification here:
|
||||
<button
|
||||
class="close-button"
|
||||
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
|
||||
>
|
||||
⨯
|
||||
</button>
|
||||
</div>
|
||||
</lion-dialog>
|
||||
`;
|
||||
return html`
|
||||
<style>${demoStyle}</style>
|
||||
<div class="demo-box_placements">
|
||||
${dialog('center')} ${dialog('top-left')} ${dialog('top-right')} ${dialog('bottom-left')}
|
||||
${dialog('bottom-right')}
|
||||
</div>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-dialog .config=${{ viewportConfig: { placement: 'top-left' } }}>
|
||||
<button slot="invoker">Dialog top-left</button>
|
||||
<div slot="content" class="dialog">
|
||||
Hello! You can close this notification here:
|
||||
<button
|
||||
class="close-button"
|
||||
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
|
||||
>
|
||||
⨯
|
||||
</button>
|
||||
</div>
|
||||
</lion-dialog>
|
||||
```
|
||||
|
||||
Configuration passed to `config` property:
|
||||
```js
|
||||
{
|
||||
viewportConfig: {
|
||||
placement: ... // <-- choose a position
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Other overrides
|
||||
|
||||
No backdrop, hides on escape, prevents scrolling while opened, and focuses the body when hiding.
|
||||
|
||||
<Story name="Other overrides">
|
||||
{html`
|
||||
<style>${demoStyle}</style>
|
||||
<lion-dialog .config=${{
|
||||
hasBackdrop: false,
|
||||
hidesOnEscape: true,
|
||||
preventsScroll: true,
|
||||
elementToFocusAfterHide: document.body
|
||||
}}>
|
||||
<button slot="invoker">Click me to open dialog</button>
|
||||
<div slot="content" class="dialog">
|
||||
Hello! You can close this dialog here:
|
||||
<button
|
||||
class="close-button"
|
||||
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
|
||||
>
|
||||
⨯
|
||||
</button>
|
||||
</div>
|
||||
</lion-dialog>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-dialog .config="${{}}">
|
||||
<button slot="invoker">Open dialog</button>
|
||||
<div slot="content" class="dialog">
|
||||
Hello! You can close this notification here:
|
||||
<button
|
||||
class="close-button"
|
||||
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
|
||||
>
|
||||
⨯
|
||||
</button>
|
||||
</div>
|
||||
</lion-dialog>
|
||||
```
|
||||
|
||||
Configuration passed to `config` property:
|
||||
```js
|
||||
{
|
||||
hasBackdrop: false,
|
||||
hidesOnEscape: true,
|
||||
preventsScroll: true,
|
||||
elementToFocusAfterHide: document.body
|
||||
}
|
||||
```
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Form Fundaments
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Forms/Field/Overview',
|
||||
};
|
||||
```
|
||||
|
||||
`Form control`s are the most fundamental building block of the Forms. They are the basis of
|
||||
both `field`s and `fieldset`s, and the `form` itself.
|
||||
|
||||
|
|
@ -21,17 +29,15 @@ On top of this, they feature:
|
|||
|
||||
### Platform fields (wrappers)
|
||||
|
||||
- [`LionInput`](../input/), a wrapper for `<input>`
|
||||
- [`LionTextarea`](../textarea/), a wrapper for `<textarea>`
|
||||
- [`LionSelect`](../select/), a wrapper for `<select>`
|
||||
- [`LionRadio`](../radio/), a wrapper for `<input type="radio">`
|
||||
- [`LionCheckbox`](../checkbox/), a wrapper for `<input type="checkbox">`
|
||||
- [LionInput](?path=/docs/forms-input--main), a wrapper for `<input>`
|
||||
- [LionTextarea](?path=/docs/forms-textarea--main), a wrapper for `<textarea>`
|
||||
- [LionSelect](?path=/docs/forms-select--main), a wrapper for `<select>`
|
||||
- [LionRadio](?path=/docs/forms-radio-group--main), a wrapper for `<input type="radio">`
|
||||
- [LionCheckbox](?path=/docs/forms-checkbox-group--main), a wrapper for `<input type="checkbox">`
|
||||
|
||||
### Custom fields (wrappers)
|
||||
|
||||
Whenever a native form control doesn't exist or is not sufficient, a
|
||||
[custom form field](./docs/CustomFieldsTutorial.md) should be created. One could think of components
|
||||
like:
|
||||
Whenever a native form control doesn't exist or is not sufficient, a [custom form field](?path=/docs/forms-field-custom-fields-tutorial--page) should be created. One could think of components like:
|
||||
|
||||
- slider
|
||||
- combobox
|
||||
|
|
@ -44,15 +50,13 @@ Fieldsets are groups of fields. They can be considered fields on their own as we
|
|||
partly share the normalized api via `FormControlMixin`.
|
||||
Fieldsets are the basis for:
|
||||
|
||||
- [`LionFieldset`](../fieldset/)
|
||||
- [`LionForm`](../form/)
|
||||
- [`LionRadioGroup`](../radio-group/)
|
||||
- [`LionCheckboxGroup`](../checkbox-group/)
|
||||
- [LionFieldset](?path=/docs/forms-fieldset-overview--main)
|
||||
- [LionForm](?path=/docs/forms-form-overview--main)
|
||||
- [LionRadioGroup](?path=/docs/forms-radio-group--main)
|
||||
- [LionCheckboxGroup](?path=/docs/forms-checkbox-group--main)
|
||||
|
||||
## Other Resources
|
||||
|
||||
- [Model Value](./docs/modelValue.md)
|
||||
- [Formatting and parsing](./docs/FormattingAndParsing.md)
|
||||
- [Interaction states](./docs/InteractionStates.md)
|
||||
- [Validation System](../validate/docs/ValidationSystem.md)
|
||||
- [Custom Fields](./docs/CustomFieldsTutorial.md)
|
||||
- [Form fundamentals](?path=/docs/forms-field-fundaments--page)
|
||||
- [Formatting and parsing](?path=/docs/forms-field-formatting-and-parsing--page)
|
||||
- [Custom Fields](?path=/docs/forms-field-custom-fields-tutorial--page)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Creating a custom field
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Forms/Field/Custom fields tutorial',
|
||||
};
|
||||
```
|
||||
|
||||
Custom fields can be created in just a few steps. All you need is an interaction element
|
||||
(like for instance a slider, a listbox or a combobox) and connect it to the [Field](../README.md)
|
||||
functionality.
|
||||
|
|
@ -8,9 +16,7 @@ functionality.
|
|||
|
||||
An interaction element provides the means for the end user to enter a certain value, just like
|
||||
native elements provide in this (think of `<input>`, `<textarea>` and `<select>`).
|
||||
An example of a non native element is the
|
||||
[slider design pattern](https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/#slider)
|
||||
described here.
|
||||
An example of a non native element is the [slider design pattern](https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/#slider) described here.
|
||||
|
||||
For this tutorial, we assume we have a component `<my-slider>` that exposes its value via property
|
||||
`mySliderValue` and sends an event `my-slider-changed` on every value change. To make it focusable,
|
||||
|
|
@ -19,7 +25,7 @@ it has a tabindex=“0” applied.
|
|||
## Connecting the interaction element to the field
|
||||
|
||||
Now we want to integrate the slider in our form framework to enrich the user interface, get
|
||||
validation support and get all the other [benefits of LionField](../README.md).
|
||||
validation support and get all the other [benefits of LionField](?path=/docs/forms-field-overview--page).
|
||||
We start of by creating a component `<lion-slider>` that extends from `LionField`.
|
||||
Then we follow the steps below:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Form Fundaments
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Forms/Field/Fundaments',
|
||||
};
|
||||
```
|
||||
|
||||
`Form control`s are the most fundamental building block of the Forms. They are the basis of
|
||||
both `field`s and `fieldset`s, and the `form` itself.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Formatting and parsing
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Forms/Field/Formatting and parsing',
|
||||
};
|
||||
```
|
||||
|
||||
The `FormatMixin` keeps track of the `modelValue`, `formattedValue` and `serializedValue`.
|
||||
It is designed to work in conjunction with `LionField`.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,50 @@
|
|||
|
||||
`lion-fieldset` groups multiple input fields or other fieldsets together.
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import '@lion/input/lion-input.js';
|
||||
import { localize } from '@lion/localize';
|
||||
import { loadDefaultFeedbackMessages, MinLength, Validator, Required } from '@lion/validate';
|
||||
import './lion-fieldset.js';
|
||||
import './docs/helpers/demo-fieldset-child.js';
|
||||
|
||||
export default {
|
||||
title: 'Forms/Fieldset/Overview',
|
||||
};
|
||||
```
|
||||
|
||||
We have three specific fieldset implementations:
|
||||
|
||||
- [lion-form](?path=/docs/forms-form-overview--page)
|
||||
- [lion-checkbox-group](?path=/docs/forms-checkbox-group--default-story)
|
||||
- [lion-radio-group](?path=/docs/forms-radio-group--default-story)
|
||||
|
||||
```js story
|
||||
export const main = () => html`
|
||||
<lion-fieldset name="nameGroup" label="Name">
|
||||
<lion-input name="FirstName" label="First Name"></lion-input>
|
||||
<lion-input name="LastName" label="Last Name"></lion-input>
|
||||
</lion-fieldset>
|
||||
`;
|
||||
```
|
||||
|
||||
A native fieldset element should always have a legend-element for a11y purposes.
|
||||
However, our fieldset element is not native and should not have a legend-element.
|
||||
Our fieldset instead has a label attribute or you can add a label with a div- or heading-element using `slot="label"`.
|
||||
|
||||
## Live Demo/Documentation
|
||||
|
||||
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/forms-fieldset-overview--page) for a live demo and documentation
|
||||
|
||||
## Features
|
||||
|
||||
- Easy retrieval of form data based on field names
|
||||
- Advanced user interaction scenarios via [interaction states](?path=/docs/forms-system-interaction-states--interaction-states)
|
||||
- Can have [validate](?path=/docs/forms-validation-overview--page) on fieldset level and shows the validation feedback below the fieldset
|
||||
- Can disable input fields on fieldset level
|
||||
- Accessible out of the box
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
|
@ -15,8 +55,9 @@ npm i --save @lion/fieldset
|
|||
```
|
||||
|
||||
```js
|
||||
import { LionFieldset } from '@lion/fieldset';
|
||||
// or
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
import '@lion/input/lion-input.js';
|
||||
```
|
||||
|
||||
### Example
|
||||
|
|
@ -26,3 +67,5 @@ import '@lion/input/lion-input.js';
|
|||
<lion-input name="title" label="Title"></lion-input>
|
||||
</lion-fieldset>
|
||||
```
|
||||
|
||||
For more examples please look at [Fieldset Examples](?path=/docs/forms-fieldset-examples--default-story).
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
import '@lion/input/lion-input.js';
|
||||
import { localize } from '@lion/localize';
|
||||
import { loadDefaultFeedbackMessages, MinLength, Validator, Required } from '@lion/validate';
|
||||
import '../lion-fieldset.js';
|
||||
import './helpers/demo-fieldset-child.js';
|
||||
|
||||
<Meta title="Forms/Fieldset/Overview" parameters={{ component: 'lion-fieldset' }} />
|
||||
|
||||
# Fieldset
|
||||
|
||||
`lion-fieldset` groups multiple input fields or other fieldsets together.
|
||||
|
||||
We have three specific fieldset implementations:
|
||||
|
||||
- [lion-form](?path=/docs/forms-form-overview--page)
|
||||
- [lion-checkbox-group](?path=/docs/forms-checkbox-group--default-story)
|
||||
- [lion-radio-group](?path=/docs/forms-radio-group--default-story)
|
||||
|
||||
```html
|
||||
<lion-fieldset name="nameGroup" label="Name">
|
||||
<lion-input name="FirstName" label="First Name"></lion-input>
|
||||
<lion-input name="LastName" label="Last Name"></lion-input>
|
||||
</lion-fieldset>
|
||||
```
|
||||
|
||||
A native fieldset element should always have a legend-element for a11y purposes.
|
||||
However, our fieldset element is not native and should not have a legend-element.
|
||||
Our fieldset instead has a label attribute or you can add a label with a div- or heading-element using `slot="label"`.
|
||||
|
||||
## Features
|
||||
|
||||
- Easy retrieval of form data based on field names
|
||||
- Advanced user interaction scenarios via [interaction states](?path=/docs/forms-system-interaction-states--interaction-states)
|
||||
- Can have [validate](?path=/docs/forms-validation-overview--page) on fieldset level and shows the validation feedback below the fieldset
|
||||
- Can disable input fields on fieldset level
|
||||
- Accessible out of the box
|
||||
|
||||
## Examples
|
||||
|
||||
For examples please look at [Fieldset Examples](?path=/docs/forms-fieldset-examples--default-story).
|
||||
|
|
@ -1,33 +1,39 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Form System
|
||||
|
||||
The Form System allows you to create complex forms with various validation in an easy way.
|
||||
The Form System allows you to create complex forms with various validations in an easy way.
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Forms/Intro',
|
||||
};
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Built in [validate](../validate) for error/warning/info/success
|
||||
- Built in [validate](?path=/docs/forms-validation-overview--page) for error/warning/info/success
|
||||
- Formatting of values
|
||||
- Accessible
|
||||
|
||||
For a more in depth description look into the [Form System Overview](?path=/docs/forms-system-overview--page).
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Description |
|
||||
| ----------------------------------- | ---------------------------------- |
|
||||
| [checkbox](../checkbox) | Checkbox form element |
|
||||
| [checkbox-group](../checkbox-group) | Group of checkboxes |
|
||||
| [field](../field) | Base class for all inputs |
|
||||
| [form](../form) | Wrapper for multiple form elements |
|
||||
| [input](../input) | Input element for strings |
|
||||
| [input-amount](../input-amount) | Input element for amounts |
|
||||
| [input-date](../input-date) | Input element for dates |
|
||||
| [input-email](../input-email) | Input element for e-mails |
|
||||
| [input-iban](../input-iban) | Input element for IBANs |
|
||||
| [radio](../radio) | Radio form element |
|
||||
| [radio-group](../radio-group) | Group of radios |
|
||||
| [select](../select) | Simple native dropdown element |
|
||||
| [textarea](../textarea) | Multiline text input |
|
||||
| [validate](../validate) | Validation for our form components |
|
||||
|
||||
## Meta Package
|
||||
|
||||
This is a meta package to show interaction between various form elements.
|
||||
For usage and installation please see the appropriate packages.
|
||||
| ------------------------------------------------------------------- | ---------------------------------- |
|
||||
| [checkbox](?path=/docs/forms-checkbox--default-story) | Checkbox form element |
|
||||
| [checkbox-group](?path=/docs/forms-checkbox-group--default-story) | Group of checkboxes |
|
||||
| [field](?path=/docs/forms-field--default-story) | Base class for all inputs |
|
||||
| [fieldset](?path=/docs/forms-fieldset-overview--page) | Group for form inputs |
|
||||
| [form](?path=/docs/forms-form-overview--page) | Wrapper for multiple form elements |
|
||||
| [input](?path=/docs/forms-input--default-story) | Input element for strings |
|
||||
| [input-amount](?path=/docs/forms-input-amount--default-story) | Input element for amounts |
|
||||
| [input-date](?path=/docs/forms-input-date--default-story) | Input element for dates |
|
||||
| [input-email](?path=/docs/forms-input-email--default-story) | Input element for e-mails |
|
||||
| [input-iban](?path=/docs/forms-input-iban--default-story) | Input element for IBANs |
|
||||
| [radio](?path=/docs/forms-radio--default-story) | Radio form element |
|
||||
| [radio-group](?path=/docs/forms-radio-group--default-story) | Group of radios |
|
||||
| [select](?path=/docs/forms-select--default-story) | Simple native dropdown element |
|
||||
| [textarea](?path=/docs/forms-textarea--default-story) | Multiline text input |
|
||||
| [validate](?path=/docs/forms-system-validate-system--default-story) | Validation for our form components |
|
||||
|
|
|
|||
153
packages/form-system/docs/15-features-overview.md
Normal file
153
packages/form-system/docs/15-features-overview.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Features Overview
|
||||
|
||||
This is a meta package to show interaction between various form elements.
|
||||
For usage and installation please see the appropriate packages.
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import '@lion/checkbox-group/lion-checkbox-group.js';
|
||||
import '@lion/checkbox-group/lion-checkbox.js';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
import '@lion/form/lion-form.js';
|
||||
import '@lion/input-amount/lion-input-amount.js';
|
||||
import '@lion/input-date/lion-input-date.js';
|
||||
import '@lion/input-datepicker/lion-input-datepicker.js';
|
||||
import '@lion/input-email/lion-input-email.js';
|
||||
import '@lion/input-iban/lion-input-iban.js';
|
||||
import '@lion/input-range/lion-input-range.js';
|
||||
import '@lion/input/lion-input.js';
|
||||
import '@lion/radio-group/lion-radio-group.js';
|
||||
import '@lion/radio-group/lion-radio.js';
|
||||
import '@lion/select/lion-select.js';
|
||||
import '@lion/select-rich/lion-option.js';
|
||||
import '@lion/select-rich/lion-options.js';
|
||||
import '@lion/select-rich/lion-select-rich.js';
|
||||
import '@lion/textarea/lion-textarea.js';
|
||||
import { MinLength, Required } from '@lion/validate';
|
||||
|
||||
export default {
|
||||
title: 'Forms/Features Overview',
|
||||
};
|
||||
```
|
||||
|
||||
## Umbrella Form
|
||||
|
||||
```js story
|
||||
export const main = () => {
|
||||
Required.getMessage = () => 'Please enter a value';
|
||||
return html`
|
||||
<lion-form
|
||||
@model-value-changed="${ev => {
|
||||
console.log('lion-form::', ev.target.name, ':', ev.detail.formPath);
|
||||
}}"
|
||||
>
|
||||
<form>
|
||||
<lion-fieldset
|
||||
name="full_name"
|
||||
@model-value-changed="${ev => {
|
||||
console.log('lion-fieldset::', ev.target.name, ':', ev.detail.formPath);
|
||||
}}"
|
||||
>
|
||||
<lion-input
|
||||
name="first_name"
|
||||
label="First Name"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
name="last_name"
|
||||
label="Last Name"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input>
|
||||
</lion-fieldset>
|
||||
<lion-input-date
|
||||
name="date"
|
||||
label="Date of application"
|
||||
.modelValue="${new Date('2000-12-12')}"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input-date>
|
||||
<lion-input-datepicker
|
||||
name="datepicker"
|
||||
label="Date to be picked"
|
||||
.modelValue="${new Date('2020-12-12')}"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input-datepicker>
|
||||
<lion-textarea
|
||||
name="bio"
|
||||
label="Biography"
|
||||
.validators="${[new Required(), new MinLength(10)]}"
|
||||
help-text="Please enter at least 10 characters"
|
||||
></lion-textarea>
|
||||
<lion-input-amount name="money" label="Money"></lion-input-amount>
|
||||
<lion-input-iban name="iban" label="Iban"></lion-input-iban>
|
||||
<lion-input-email name="email" label="Email"></lion-input-email>
|
||||
<lion-checkbox-group
|
||||
@model-value-changed="${ev => {
|
||||
console.log('lion-cb-group::', ev.target.name, ':', ev.detail.formPath);
|
||||
}}"
|
||||
label="What do you like?"
|
||||
name="checkers"
|
||||
.validators="${[new Required()]}"
|
||||
>
|
||||
<lion-checkbox .choiceValue=${'foo'} label="I like foo"></lion-checkbox>
|
||||
<lion-checkbox .choiceValue=${'bar'} label="I like bar"></lion-checkbox>
|
||||
<lion-checkbox .choiceValue=${'baz'} label="I like baz"></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<lion-radio-group
|
||||
@model-value-changed="${ev => {
|
||||
console.log('lion-radio-group::', ev.target.name, ':', ev.detail.formPath);
|
||||
}}"
|
||||
name="dinosaurs"
|
||||
label="Favorite dinosaur"
|
||||
.validators="${[new Required()]}"
|
||||
>
|
||||
<lion-radio .choiceValue=${'allosaurus'} label="allosaurus"></lion-radio>
|
||||
<lion-radio .choiceValue=${'brontosaurus'} label="brontosaurus"></lion-radio>
|
||||
<lion-radio .choiceValue=${'diplodocus'} label="diplodocus"></lion-radio>
|
||||
</lion-radio-group>
|
||||
<lion-select-rich name="favoriteColor" label="Favorite color">
|
||||
<lion-options slot="input">
|
||||
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
||||
<lion-option .choiceValue=${'hotpink'} checked>Hotpink</lion-option>
|
||||
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
||||
</lion-options>
|
||||
</lion-select-rich>
|
||||
<lion-select label="Lyrics" name="lyrics" .validators="${[new Required()]}">
|
||||
<select slot="input">
|
||||
<option value="1">Fire up that loud</option>
|
||||
<option value="2">Another round of shots...</option>
|
||||
<option value="3">Drop down for what?</option>
|
||||
</select>
|
||||
</lion-select>
|
||||
<lion-input-range
|
||||
name="range"
|
||||
min="1"
|
||||
max="5"
|
||||
.modelValue="${2.3}"
|
||||
unit="%"
|
||||
step="0.1"
|
||||
label="Input range"
|
||||
></lion-input-range>
|
||||
<lion-checkbox-group
|
||||
.mulipleChoice="${false}"
|
||||
name="terms"
|
||||
.validators="${[new Required()]}"
|
||||
>
|
||||
<lion-checkbox label="I blindly accept all terms and conditions"></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<lion-textarea name="comments" label="Comments"></lion-textarea>
|
||||
<div class="buttons">
|
||||
<lion-button raised>Submit</lion-button>
|
||||
<lion-button
|
||||
type="button"
|
||||
raised
|
||||
@click=${ev => ev.currentTarget.parentElement.parentElement.parentElement.resetGroup()}
|
||||
>Reset</lion-button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</lion-form>
|
||||
`;
|
||||
};
|
||||
```
|
||||
405
packages/form-system/docs/17-validation-examples.md
Normal file
405
packages/form-system/docs/17-validation-examples.md
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Validation examples
|
||||
|
||||
## Required Validator
|
||||
|
||||
The required validator can be put onto every form field element and will make sure that element is
|
||||
not empty.
|
||||
For an input that may mean that it is not an empty string,
|
||||
while for a checkbox group it means at least one checkbox needs to be checked.
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { LionInput } from '@lion/input';
|
||||
import '@lion/input-amount/lion-input-amount.js';
|
||||
import '@lion/input-date/lion-input-date.js';
|
||||
import '@lion/input-email/lion-input-email.js';
|
||||
import '@lion/input/lion-input.js';
|
||||
import {
|
||||
DefaultSuccess,
|
||||
EqualsLength,
|
||||
IsDate,
|
||||
IsEmail,
|
||||
IsNumber,
|
||||
loadDefaultFeedbackMessages,
|
||||
MaxDate,
|
||||
MaxLength,
|
||||
MaxNumber,
|
||||
MinDate,
|
||||
MinLength,
|
||||
MinMaxDate,
|
||||
MinMaxLength,
|
||||
MinMaxNumber,
|
||||
MinNumber,
|
||||
Required,
|
||||
Validator,
|
||||
} from '@lion/validate';
|
||||
|
||||
export default {
|
||||
title: 'Forms/Validation/Examples',
|
||||
};
|
||||
|
||||
loadDefaultFeedbackMessages();
|
||||
```
|
||||
|
||||
```js preview-story
|
||||
export const requiredValidator = () => html`
|
||||
<lion-input .validators=${[new Required()]} label="Required"></lion-input>
|
||||
`;
|
||||
```
|
||||
|
||||
## String Validators
|
||||
|
||||
Useful on input elements it allows to define how many characters can be entered.
|
||||
|
||||
```js preview-story
|
||||
export const stringValidators = () => html`
|
||||
<lion-input
|
||||
.validators=${[new EqualsLength(7)]}
|
||||
.modelValue=${'not exactly'}
|
||||
label="EqualsLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators=${[new MinLength(10)]}
|
||||
.modelValue=${'too short'}
|
||||
label="MinLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators=${[new MaxLength(7)]}
|
||||
.modelValue=${'too long'}
|
||||
label="MaxLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators=${[new MinMaxLength({ min: 10, max: 20 })]}
|
||||
.modelValue=${'that should be enough'}
|
||||
label="MinMaxLength"
|
||||
></lion-input>
|
||||
`;
|
||||
```
|
||||
|
||||
## Number Validators
|
||||
|
||||
Number validations assume that it's modelValue is actually a number.
|
||||
Therefore it may only be used on input that have an appropriate parser/formatter like the input-amount.
|
||||
|
||||
```js preview-story
|
||||
export const numberValidators = () => html`
|
||||
<lion-input-amount
|
||||
.validators="${[new IsNumber()]}"
|
||||
.modelValue="${'foo'}"
|
||||
label="IsNumber"
|
||||
></lion-input-amount>
|
||||
<lion-input-amount
|
||||
.validators="${[new MinNumber(7)]}"
|
||||
.modelValue="${5}"
|
||||
label="MinNumber"
|
||||
></lion-input-amount>
|
||||
<lion-input-amount
|
||||
.validators="${[new MaxNumber(7)]}"
|
||||
.modelValue="${9}"
|
||||
label="MaxNumber"
|
||||
></lion-input-amount>
|
||||
<lion-input-amount
|
||||
.validators="${[new MinMaxNumber({ min: 10, max: 20 })]}"
|
||||
.modelValue="${5}"
|
||||
label="MinMaxNumber"
|
||||
></lion-input-amount>
|
||||
`;
|
||||
```
|
||||
|
||||
## Date Validators
|
||||
|
||||
Date validators work with real javascript dates. Use them on input-date.
|
||||
|
||||
```js preview-story
|
||||
export const dateValidators = () => {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = today.getMonth();
|
||||
const day = today.getDate();
|
||||
const yesterday = new Date(year, month, day - 1);
|
||||
const tomorrow = new Date(year, month, day + 1);
|
||||
return html`
|
||||
<lion-input-date
|
||||
.validators=${[new IsDate()]}
|
||||
.modelValue=${'foo'}
|
||||
label="IsDate"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
.validators=${[new MinDate(today)]}
|
||||
.modelValue=${new Date(yesterday)}
|
||||
label="MinDate"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
.validators=${[new MaxDate(today)]}
|
||||
.modelValue=${new Date(tomorrow)}
|
||||
label="MaxDate"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
.validators=${[new MinMaxDate({ min: new Date(yesterday), max: new Date(tomorrow) })]}
|
||||
.modelValue=${new Date(today)}
|
||||
label="MinMaxDate"
|
||||
></lion-input-date>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
## Email Validator
|
||||
|
||||
```js preview-story
|
||||
export const emailValidator = () => html`
|
||||
<lion-input-email
|
||||
.validators="${[new IsEmail()]}"
|
||||
.modelValue="${'foo'}"
|
||||
label="IsEmail"
|
||||
></lion-input-email>
|
||||
`;
|
||||
```
|
||||
|
||||
## Validation Types
|
||||
|
||||
When defining your own component you can decide to allow for multiple types of validation.
|
||||
By default only `error` is used, however there are certainly use cases where warning or success messages make sense.
|
||||
|
||||
```js preview-story
|
||||
export const validationTypes = () => {
|
||||
try {
|
||||
class MyTypesInput extends LionInput {
|
||||
static get validationTypes() {
|
||||
return ['error', 'warning', 'info', 'success'];
|
||||
}
|
||||
}
|
||||
customElements.define('my-types-input', MyTypesInput);
|
||||
} catch (err) {
|
||||
// expected as it is a demo
|
||||
}
|
||||
return html`
|
||||
<style>
|
||||
.demo-types-input {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='success'] {
|
||||
background-color: #e4ffe4;
|
||||
border: 1px solid green;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='error'] {
|
||||
background-color: #ffd4d4;
|
||||
border: 1px solid red;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='warning'] {
|
||||
background-color: #ffe4d4;
|
||||
border: 1px solid orange;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='info'] {
|
||||
background-color: #d4e4ff;
|
||||
border: 1px solid blue;
|
||||
}
|
||||
</style>
|
||||
<my-types-input
|
||||
.validators="${[
|
||||
new Required(),
|
||||
new MinLength(7, { type: 'warning' }),
|
||||
new MaxLength(10, {
|
||||
type: 'info',
|
||||
getMessage: () => `Please, keep the length below the 10 characters.`,
|
||||
}),
|
||||
new DefaultSuccess(),
|
||||
]}"
|
||||
.modelValue="${'exactly'}"
|
||||
label="Validation Types"
|
||||
class="demo-types-input"
|
||||
></my-types-input>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
## Custom Validators
|
||||
|
||||
Here is an example how you can make your own validator and providing the error messages directly within.
|
||||
You can even hard code localization in there if needed or you can use a localization system.
|
||||
|
||||
```js preview-story
|
||||
export const customValidators = () => {
|
||||
class MyValidator extends Validator {
|
||||
static get validatorName() {
|
||||
return 'myValidator';
|
||||
}
|
||||
execute(modelValue, param) {
|
||||
return modelValue !== param;
|
||||
}
|
||||
static getMessage({ fieldName, modelValue, params: param }) {
|
||||
if (modelValue.length >= param.length - 1 && param.startsWith(modelValue)) {
|
||||
return 'Almost there...';
|
||||
}
|
||||
return `No "${param}" found in ${fieldName}`;
|
||||
}
|
||||
}
|
||||
return html`
|
||||
<lion-input
|
||||
label="Custom validator"
|
||||
help-text="Type 'mine' please"
|
||||
.validators="${[new MyValidator('mine')]}"
|
||||
.modelValue="${'mi'}"
|
||||
></lion-input>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
## Default messages
|
||||
|
||||
To get default validation messages you need to import and call the `loadDefaultFeedbackMessages` function once in your application.
|
||||
|
||||
Sometimes the default messages don't make sense for your input field. In that case you want to override it by adding a `getMessage` function to your validator.
|
||||
|
||||
```js preview-story
|
||||
export const defaultMessages = () => html`
|
||||
<lion-input
|
||||
.validators="${[new EqualsLength(4, { getMessage: () => '4 chars please...' })]}"
|
||||
.modelValue="${'123'}"
|
||||
label="Custom message for validator instance"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators="${[
|
||||
new EqualsLength(4, {
|
||||
getMessage: ({ modelValue, params: param }) => {
|
||||
const diff = modelValue.length - param;
|
||||
return `${Math.abs(diff)} too ${diff > 0 ? 'much' : 'few'}...`;
|
||||
},
|
||||
}),
|
||||
]}"
|
||||
.modelValue="${'way too much'}"
|
||||
label="Dynamic message for validator instance"
|
||||
></lion-input>
|
||||
`;
|
||||
```
|
||||
|
||||
## Override fieldName
|
||||
|
||||
In the scenario that the default messages are correct, but you only want to change the `fieldName`, this can both be done for a single specific validator or for all at once.
|
||||
|
||||
```js preview-story
|
||||
export const overrideFieldName = () => html`
|
||||
<lion-input
|
||||
.validators="${[new EqualsLength(4, { fieldName: 'custom fieldName' })]}"
|
||||
.modelValue="${'123'}"
|
||||
label="Custom fieldName for 1 validator"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators="${[new Required(), new EqualsLength(4)]}"
|
||||
.fieldName="${'custom fieldName'}"
|
||||
.modelValue="${'123'}"
|
||||
label="Custom fieldName for all validators"
|
||||
></lion-input>
|
||||
`;
|
||||
```
|
||||
|
||||
## Asynchronous validation
|
||||
|
||||
```js preview-story
|
||||
export const asynchronousValidation = () => {
|
||||
function pause(ms = 0) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
class AsyncValidator extends Validator {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
static get validatorName() {
|
||||
return 'asyncValidator';
|
||||
}
|
||||
static get async() {
|
||||
return true;
|
||||
}
|
||||
async execute() {
|
||||
console.log('async pending...');
|
||||
await pause(2000);
|
||||
console.log('async done...');
|
||||
return true;
|
||||
}
|
||||
static getMessage({ modelValue }) {
|
||||
return `validated for modelValue: ${modelValue}...`;
|
||||
}
|
||||
}
|
||||
return html`
|
||||
<style>
|
||||
lion-input[is-pending] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
<lion-input
|
||||
label="Async validation"
|
||||
.validators="${[new AsyncValidator()]}"
|
||||
.modelValue="${'123'}"
|
||||
></lion-input>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
## Dynamic parameter change
|
||||
|
||||
```js preview-story
|
||||
export const dynamicParameterChange = () => {
|
||||
const beginDate = new Date('09/09/1990');
|
||||
const minDateValidatorRef = new MinDate(beginDate, {
|
||||
message: 'Fill in a date after your birth date',
|
||||
});
|
||||
return html`
|
||||
<lion-input-date
|
||||
label="Birth date"
|
||||
help-text="Adjust this date to retrigger validation of the input below..."
|
||||
.modelValue="${beginDate}"
|
||||
@model-value-changed="${({ target: { modelValue, errorState } }) => {
|
||||
if (!errorState) {
|
||||
// Since graduation date is usually not before birth date
|
||||
minDateValidatorRef.param = modelValue;
|
||||
}
|
||||
}}"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
label="Graduation date"
|
||||
.modelValue="${new Date('09/09/1989')}"
|
||||
.validators="${[minDateValidatorRef]}"
|
||||
></lion-input-date>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
## Disabled inputs validation
|
||||
|
||||
According to the W3C specs, Disabled fields should not be validated.
|
||||
Therefor if the attribute disabled is present on a lion-input it will not be validated.
|
||||
|
||||
```js preview-story
|
||||
export const disabledInputsValidation = () => html`
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new EqualsLength(7)]}
|
||||
.modelValue=${'not exactly'}
|
||||
label="EqualsLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new MinLength(10)]}
|
||||
.modelValue=${'too short'}
|
||||
label="MinLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new MaxLength(7)]}
|
||||
.modelValue=${'too long'}
|
||||
label="MaxLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new MinMaxLength({ min: 10, max: 20 })]}
|
||||
.modelValue=${'that should be enough'}
|
||||
label="MinMaxLength"
|
||||
></lion-input>
|
||||
`;
|
||||
```
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
import { Meta } from '@open-wc/demoing-storybook';
|
||||
|
||||
<Meta title="Forms/System/Overview" />
|
||||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Form System Overview
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Forms/System/Overview',
|
||||
};
|
||||
```
|
||||
|
||||
This page should be used as a starting point when first using the Form System.
|
||||
It provides an overview of its essential building blocks and provides links to detailed explanations
|
||||
of most of its core concepts.
|
||||
|
|
@ -33,8 +37,8 @@ On top of this, they offer:
|
|||
- advanced [validation](?path=/docs/forms-validation-overview--page) possibilities
|
||||
- creation of advanced user interaction scenarios via [interaction states](?path=/docs/forms-system-interaction-states--interaction-states)
|
||||
- provision of labels and help texts in an easy, declarative manner
|
||||
- accessibility out of the box <!-- TODO: write how our form system manages to deliver accessible components with all challenges that shadow dom provide -->
|
||||
- advanced styling possibilities: map your own Design System to the internal HTML structure <!-- TODO: link to example of Material Design Datepicker-->
|
||||
- accessibility out of the box
|
||||
- advanced styling possibilities: map your own Design System to the internal HTML structure
|
||||
|
||||
#### Platform fields (wrappers)
|
||||
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
import { Meta } from '@open-wc/demoing-storybook';
|
||||
|
||||
<Meta title="Forms/System/ModelValue" />
|
||||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# ModelValue
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Forms/System/ModelValue',
|
||||
};
|
||||
```
|
||||
|
||||
The modelValue or model can be considered as the ‘aorta’ of our form system.
|
||||
It is the single source of truth; not only for the current state
|
||||
of the form, also for all derived states: interaction, validation, visibility and other states are
|
||||
|
|
@ -1,13 +1,18 @@
|
|||
|
||||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
import '@lion/input/lion-input.js';
|
||||
import { Unparseable } from '@lion/validate';
|
||||
import './helper-wc/h-output.js';
|
||||
|
||||
<Meta title="Forms/System/Formatting and Parsing" />
|
||||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Formatting and Parsing
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import '@lion/input/lion-input.js';
|
||||
import { Unparseable } from '@lion/validate';
|
||||
import '../docs/helper-wc/h-output.js';
|
||||
|
||||
export default {
|
||||
title: 'Forms/System/Formatting and Parsing',
|
||||
};
|
||||
```
|
||||
|
||||
> For demo purposes, below we use `<lion-input>` which is a basic extension of `<lion-field>`.
|
||||
> Almost all fields share the same functionality as `<lion-input>`.
|
||||
|
||||
|
|
@ -46,12 +51,11 @@ Examples:
|
|||
- For a `date input`: a String '20/01/1999' will be converted to new Date('1999/01/20')
|
||||
- For an `amount input`: a formatted String '1.234,56' will be converted to a Number: 1234.56
|
||||
|
||||
|
||||
You can set a parser function on the `<lion-input>` to set parsing behavior.
|
||||
In this example, we parse the input and try to convert it to a `Number`.
|
||||
|
||||
<Story name="Parser">
|
||||
{html`
|
||||
```js preview-story
|
||||
export const parser = () => html`
|
||||
<lion-input
|
||||
label="Number Example"
|
||||
help-text="Uses .parser to create model values from view values"
|
||||
|
|
@ -60,16 +64,7 @@ In this example, we parse the input and try to convert it to a `Number`.
|
|||
@model-value-changed=${({ target }) => console.log(target)}
|
||||
></lion-input>
|
||||
<h-output .show="${['modelValue']}"></h-output>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-input
|
||||
label="Number Example"
|
||||
help-text="Uses .parser to create model values from view values"
|
||||
.parser="${viewValue => Number(viewValue)}"
|
||||
.modelValue="${1234567890}"
|
||||
></lion-input>
|
||||
`;
|
||||
```
|
||||
|
||||
#### Unparseable
|
||||
|
|
@ -83,8 +78,8 @@ what the user tried to input.
|
|||
The formatted result of this that is reflected to the user will be the `viewValue` of the
|
||||
`Unparseable` instance, so basically nothing happens for the user.
|
||||
|
||||
<Story name="Unparseable">
|
||||
{html`
|
||||
```js preview-story
|
||||
export const unparseable = () => html`
|
||||
<lion-input
|
||||
label="Number Example"
|
||||
help-text="Uses .parser and return undefined if Number() cannot parse"
|
||||
|
|
@ -92,16 +87,7 @@ The formatted result of this that is reflected to the user will be the `viewValu
|
|||
value="${'1234abc567890'}"
|
||||
></lion-input>
|
||||
<h-output .show="${['modelValue']}"></h-output>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-input
|
||||
label="Number Example"
|
||||
help-text="Uses .parser and return undefined if Number() cannot parse"
|
||||
.parser="${viewValue => Number(viewValue) || undefined}"
|
||||
value="${'1234abc567890'}"
|
||||
></lion-input>
|
||||
`;
|
||||
```
|
||||
|
||||
### Formatters
|
||||
|
|
@ -115,8 +101,8 @@ The formatter then uses `Intl.NumberFormat` to format it with thousand separator
|
|||
Formatted value is reflected back to the user `on-blur` of the field, but only if the field has no
|
||||
errors (validation).
|
||||
|
||||
<Story name="Formatter">
|
||||
{() => {
|
||||
```js preview-story
|
||||
export const formatters = () => {
|
||||
const formatDate = (modelValue, options) => {
|
||||
if (!(typeof modelValue === 'number')) {
|
||||
return options.formattedValue;
|
||||
|
|
@ -134,30 +120,11 @@ errors (validation).
|
|||
</lion-input>
|
||||
<h-output .show="${['modelValue', 'formattedValue']}"></h-output>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
> The options object holds a fallback value that shows what should be presented on
|
||||
> screen when the user input resulted in an invalid `modelValue`.
|
||||
|
||||
```js
|
||||
const formatDate = (modelValue, options) => {
|
||||
if (!(typeof modelValue === 'number')) {
|
||||
return options.formattedValue;
|
||||
}
|
||||
return new Intl.NumberFormat('en-GB').format(modelValue);
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-input
|
||||
label="Number Example"
|
||||
help-text="Uses .formatter to create view value"
|
||||
.parser="${viewValue => Number(viewValue.replace(/[^0-9]/g, ''))}"
|
||||
.formatter="${formatDate}"
|
||||
.modelValue="${1234567890}"
|
||||
></lion-input>
|
||||
```
|
||||
> The options object holds a fallback value that shows what should be presented on
|
||||
> screen when the user input resulted in an invalid `modelValue`.
|
||||
|
||||
### Serializers and deserializers
|
||||
|
||||
|
|
@ -170,8 +137,8 @@ This can be useful for prefilling forms with data from APIs.
|
|||
> There is no `.deserializedValue` property that stays in sync by default.
|
||||
> You need to call `el.deserializer(el.modelValue)` manually yourself.
|
||||
|
||||
<Story name="(De)serializers">
|
||||
{() => {
|
||||
```js preview-story
|
||||
export const deSerializers = () => {
|
||||
const mySerializer = (modelValue, options) => {
|
||||
return parseInt(modelValue, 8);
|
||||
};
|
||||
|
|
@ -189,28 +156,7 @@ This can be useful for prefilling forms with data from APIs.
|
|||
></lion-input>
|
||||
<h-output .show="${['modelValue', 'serializedValue']}"></h-output>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
const mySerializer = (modelValue, options) => {
|
||||
return parseInt(modelValue, 8);
|
||||
};
|
||||
|
||||
const myDeserializer = (myValue, options) => {
|
||||
return parseInt(modelValue, 10);
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-input
|
||||
label="Date Example"
|
||||
help-text="Uses .(de)serializer to restore serialized modelValues"
|
||||
.parser="${viewValue => Number(viewValue.replace(/[^0-9]/g, ''))}"
|
||||
.serializer="${mySerializer}"
|
||||
.deserializer="${myDeserializer}"
|
||||
.modelValue="${1234567890}"
|
||||
></lion-input>
|
||||
```
|
||||
|
||||
## Flow Diagrams
|
||||
|
|
@ -218,16 +164,31 @@ const myDeserializer = (myValue, options) => {
|
|||
Below we show three flow diagrams to show the flow of formatting, serializing and parsing user input, with the example of a date input:
|
||||
|
||||
### Standard flow
|
||||
|
||||
Where a user changes the input with their keyboard
|
||||
|
||||
<Story id="forms-system-internals--standard-flow"></Story>
|
||||
```js preview-story
|
||||
export const standardFlow = () => html`
|
||||
<img src=${new URL('../dev-assets/FormatMixinDiagram-1.svg', import.meta.url)} />
|
||||
`;
|
||||
```
|
||||
|
||||
### Unparseable flow
|
||||
|
||||
Where a user sets the input to something that is not parseable by the parser
|
||||
|
||||
<Story id="forms-system-internals--unparseable-flow"></Story>
|
||||
```js preview-story
|
||||
export const unparseableFlow = () => html`
|
||||
<img src=${new URL('../dev-assets/FormatMixinDiagram-2.svg', import.meta.url)} />
|
||||
`;
|
||||
```
|
||||
|
||||
### Imperative / programmatic flow
|
||||
|
||||
Where the developer sets the modelValue of the input programmatically
|
||||
|
||||
<Story id="forms-system-internals--imperative-flow"></Story>
|
||||
```js preview-story
|
||||
export const imperativeFlow = () => html`
|
||||
<img src=${new URL('../dev-assets/FormatMixinDiagram-3.svg', import.meta.url)} />
|
||||
`;
|
||||
```
|
||||
178
packages/form-system/docs/35-system-interaction-states.md
Normal file
178
packages/form-system/docs/35-system-interaction-states.md
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Interaction States
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import { render } from '@lion/core';
|
||||
import { renderLitAsNode } from '@lion/helpers';
|
||||
import '@lion/input/lion-input.js';
|
||||
import { Validator } from '@lion/validate';
|
||||
import '../docs/helper-wc/h-output.js';
|
||||
|
||||
export default {
|
||||
title: 'Forms/System/Interaction States',
|
||||
};
|
||||
```
|
||||
|
||||
`InteractionStateMixin` saves meta information about interaction states.
|
||||
It allows for creating advanced UX scenarios.
|
||||
|
||||
Examples of such scenarios already in our fields:
|
||||
|
||||
- Show the validation message of an input only after the user has blurred the input field
|
||||
- Hide the validation message when an invalid value becomes valid
|
||||
|
||||
Something our subclassers can implement:
|
||||
|
||||
- Show a red border around the input right after the input became invalid
|
||||
|
||||
The meta information that InteractionStateMixin collects, is stored in the Boolean properties on our fields:
|
||||
|
||||
- `touched`, the user blurred the field
|
||||
- `dirty`, the value in the field has changed
|
||||
- `prefilled`, a prepopulated field is not empty
|
||||
|
||||
> You can listen to the events `touched-changed` and `dirty-changed`.
|
||||
|
||||
```js preview-story
|
||||
export const interactionStates = () => html`
|
||||
<lion-input
|
||||
label="Interaction States"
|
||||
help-text="Interact with this field to see how dirty, touched and prefilled change"
|
||||
.modelValue="${'myValue'}"
|
||||
></lion-input>
|
||||
<h-output .show="${['touched', 'dirty', 'prefilled', 'focused', 'submitted']}"></h-output>
|
||||
`;
|
||||
```
|
||||
|
||||
## Advanced use cases
|
||||
|
||||
### Overriding interaction states
|
||||
|
||||
When creating an extension of LionField or LionInput, it can be needed to override the way prefilled values are 'computed'.
|
||||
|
||||
The example below shows how this is done for checkboxes/radio-inputs.
|
||||
|
||||
```js
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
static _isPrefilled(modelValue) {
|
||||
return modelValue.checked;
|
||||
}
|
||||
```
|
||||
|
||||
## When is feedback shown to the user
|
||||
|
||||
We show the validity feedback when one of the following conditions is met:
|
||||
|
||||
- **prefilled**,
|
||||
the user already filled in something, or the value is prefilled
|
||||
when the form is initially rendered.
|
||||
|
||||
- **touched** && **dirty**
|
||||
|
||||
- When a user starts typing for the first time in a field with for instance `required` validation,
|
||||
error message should not be shown until a field becomes `touched` (a user leaves(blurs) a field).
|
||||
- When a user enters a field without altering the value (making it `dirty` but not `touched`),
|
||||
an error message shouldn't be shown either.
|
||||
|
||||
- **submitted**,
|
||||
if the form is submitted, always show the error message.
|
||||
|
||||
### Changing the feedback show condition (Subclassers)
|
||||
|
||||
You can change the condition upon which feedback gets shown.
|
||||
|
||||
In order to override the feedback show conditions, you need to create a custom field and override `_showFeedbackConditionFor` method.
|
||||
This method accepts the a `type` parameter which is a String representing the type of feedback (e.g. 'error').
|
||||
Then, it returns true of false, depending on whether feedback for that type should be shown or not. That part, you can control.
|
||||
|
||||
In the following example we will demonstrate this with interaction states, the most common use case for feedback visibility conditions.
|
||||
|
||||
```js preview-story
|
||||
export const feedbackCondition = () => {
|
||||
// 1. Initialize variables...
|
||||
// properties on InteractionStateMixin we want to check conditions for
|
||||
const props = ['touched', 'dirty', 'prefilled', 'focused', 'filled', 'submitted'];
|
||||
// Here we will store the conditions (trigger for validation feedback)
|
||||
// provided via the UI of the demo
|
||||
let conditions = [];
|
||||
// 2. Create a validator...
|
||||
// Define a demo validator that should only be visible on an odd amount of characters
|
||||
// const OddValidator = [modelValue => ({ odd: modelValue.length % 2 !== 0 })];
|
||||
class OddValidator extends Validator {
|
||||
static get validatorName() {
|
||||
return 'OddValidator';
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
execute(value) {
|
||||
let hasError = false;
|
||||
if (!(value.length % 2 !== 0)) {
|
||||
hasError = true;
|
||||
}
|
||||
return hasError;
|
||||
}
|
||||
_getMessage() {
|
||||
return 'Add or remove one character';
|
||||
}
|
||||
}
|
||||
// 3. Create field overriding .showErrorCondition...
|
||||
// Here we will store a reference to the Field element that overrides the default condition
|
||||
// (function `showErrorCondition`) for triggering validation feedback of `.validators`
|
||||
const fieldElement = renderLitAsNode(html`
|
||||
<lion-input
|
||||
name="interactionField"
|
||||
label="Only an odd amount of characters allowed"
|
||||
help-text="Change feedback condition"
|
||||
.modelValue="${'notodd'}"
|
||||
.validators="${[new OddValidator()]}"
|
||||
>
|
||||
<input slot="input" />
|
||||
</lion-input>
|
||||
`);
|
||||
fieldElement._showFeedbackConditionFor = type => {
|
||||
return conditions.every(condition => {
|
||||
/**
|
||||
* This here shows bug for focused state.
|
||||
* Focused is set to true AFTER we already evaluate feedback conditions
|
||||
* Bug report: https://github.com/ing-bank/lion/issues/455
|
||||
*/
|
||||
// setTimeout(() => console.log(fieldElement[condition])); //
|
||||
return fieldElement[condition];
|
||||
});
|
||||
};
|
||||
function fetchConditionsAndReevaluate({ currentTarget: { modelValue } }) {
|
||||
if (!modelValue['props[]']) {
|
||||
return;
|
||||
}
|
||||
// Create props list like: ['touched', 'submitted']
|
||||
conditions = modelValue['props[]'].filter(p => p.checked).map(p => p.value);
|
||||
// Reevaluate
|
||||
fieldElement.validate();
|
||||
}
|
||||
fieldElement.addEventListener('focus', () => {
|
||||
fieldElement.validate();
|
||||
});
|
||||
return html`
|
||||
<lion-form>
|
||||
<form>
|
||||
${fieldElement}
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
</lion-form>
|
||||
<h-output .field="${fieldElement}" .show="${[...props, 'hasFeedbackFor']}"> </h-output>
|
||||
<h3>
|
||||
Set conditions for validation feedback visibility
|
||||
</h3>
|
||||
<lion-checkbox-group name="props[]" @model-value-changed="${fetchConditionsAndReevaluate}">
|
||||
${props.map(
|
||||
p => html`
|
||||
<lion-checkbox .label="${p}" .choiceValue="${p}"> </lion-checkbox>
|
||||
`,
|
||||
)}
|
||||
</lion-checkbox-group>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
|
@ -1,12 +1,18 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Creating a custom field
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import { render } from '@lion/core';
|
||||
import '@lion/input/lion-input.js';
|
||||
import { Validator } from '@lion/validate';
|
||||
import './helper-wc/h-output.js';
|
||||
import '../docs/helper-wc/h-output.js';
|
||||
|
||||
<Meta title="Forms/System/Creating a Custom Field" />
|
||||
|
||||
# Creating a custom field
|
||||
export default {
|
||||
title: 'Forms/System/Creating a Custom Field',
|
||||
};
|
||||
```
|
||||
|
||||
Custom fields can be created in just a few steps. All you need is an interaction element
|
||||
(like for instance a slider, a listbox or a combobox) and connect it to the [Field](?path=/docs/forms-system-overview--page)
|
||||
|
|
@ -29,17 +35,17 @@ validation support and get all the other [benefits of LionField](/?path=/docs/fo
|
|||
We start by creating a component `<lion-slider>` that extends from `LionField`.
|
||||
Then we follow the steps below:
|
||||
|
||||
#### 1. Add your interaction element as ‘input slot'
|
||||
### 1. Add your interaction element as ‘input slot'
|
||||
|
||||
Here you return the element the user interacts with. By configuring it as a slot, it will end up
|
||||
in light DOM, ensuring the best accessibility for the end user.
|
||||
|
||||
#### 2. Proxy event `my-slider-changed` to `user-input-changed` event
|
||||
### 2. Proxy event `my-slider-changed` to `user-input-changed` event
|
||||
|
||||
The `user-input-changed` event is listened to by the FormatMixin: it should be regarded as the
|
||||
equivalent of the `input` event of the platform, but for custom built interaction elements.
|
||||
|
||||
#### 3. Proxy property `<my-slider>.mySliderValue` to `<lion-slider>.value`
|
||||
### 3. Proxy property `<my-slider>.mySliderValue` to `<lion-slider>.value`
|
||||
|
||||
Every time the `user-input-changed` fires, the value of `<my-slider>` is synchronized with the
|
||||
[`modelValue`](?path=/docs/forms-system-modelvalue--page) of `<my-slider>`. Now the cycle is complete: the modelValue connects
|
||||
23
packages/form-system/docs/50-content-inside-fields.md
Normal file
23
packages/form-system/docs/50-content-inside-fields.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import '@lion/input/lion-input.js';
|
||||
import '@lion/button/lion-button.js';
|
||||
|
||||
export default {
|
||||
title: 'Forms/System/Field integrations',
|
||||
};
|
||||
```
|
||||
|
||||
# Content inside fields
|
||||
|
||||
Due to our custom inputs being Web Components, it is possible to put HTML content inside an input.
|
||||
For example if you want to add a button as a prefix or suffix.
|
||||
|
||||
```js preview-story
|
||||
export const ButtonsWithFields = () => html`
|
||||
<lion-input label="Prefix and suffix">
|
||||
<lion-button slot="prefix" type="button">prefix</lion-button>
|
||||
<lion-button slot="suffix" type="button">suffix</lion-button>
|
||||
</lion-input>
|
||||
`;
|
||||
```
|
||||
192
packages/form-system/docs/fieldset-examples.md
Normal file
192
packages/form-system/docs/fieldset-examples.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Fieldset Examples
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import '@lion/input/lion-input.js';
|
||||
import { localize } from '@lion/localize';
|
||||
import { loadDefaultFeedbackMessages, MinLength, Validator, Required } from '@lion/validate';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
|
||||
export default {
|
||||
title: 'Forms/Fieldset/Examples',
|
||||
};
|
||||
```
|
||||
|
||||
## With Data
|
||||
|
||||
The fieldset's modelValue is an `Object` containing properties where the key is the `name` attribute of the field,
|
||||
and the value is the `modelValue` of the field.
|
||||
|
||||
```js preview-story
|
||||
export const data = () => html`
|
||||
<lion-fieldset name="nameGroup" label="Name">
|
||||
<lion-input name="firstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="lastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
<button @click=${ev => console.log(ev.target.parentElement.modelValue)}>
|
||||
Log to Action Logger
|
||||
</button>
|
||||
</lion-fieldset>
|
||||
`;
|
||||
```
|
||||
|
||||
## Disabled
|
||||
|
||||
Disabling a fieldset disables all its child fields.
|
||||
When enabling a fieldset, fields that have disabled explicitly set will stay disabled.
|
||||
|
||||
```js preview-story
|
||||
export const disabled = () => {
|
||||
function toggleDisabled() {
|
||||
const fieldset = document.querySelector('#fieldset');
|
||||
fieldset.disabled = !fieldset.disabled;
|
||||
}
|
||||
return html`
|
||||
<lion-fieldset name="nameGroup" label="Name" id="fieldset" disabled>
|
||||
<lion-input name="FirstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="LastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
<lion-fieldset name="nameGroup2" label="Name">
|
||||
<lion-input name="FirstName2" label="First Name" .modelValue=${'Foo'} disabled></lion-input>
|
||||
<lion-input name="LastName2" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
</lion-fieldset>
|
||||
</lion-fieldset>
|
||||
<button @click=${toggleDisabled}>
|
||||
Toggle disabled
|
||||
</button>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
## Nesting fieldsets
|
||||
|
||||
Fieldsets can also be nested. The level of nesting will correspond one to one with the `modelValue` object.
|
||||
|
||||
```js preview-story
|
||||
export const nestingFieldsets = () => html`
|
||||
<lion-fieldset>
|
||||
<div slot="label">Personal data</div>
|
||||
<lion-fieldset name="nameGroup" label="Name">
|
||||
<lion-input name="FirstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="LastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
</lion-fieldset>
|
||||
<lion-fieldset name="location" label="Location">
|
||||
<lion-input name="country" label="Country" .modelValue=${'Netherlands'}></lion-input>
|
||||
</lion-fieldset>
|
||||
<lion-input name="age" label="Age" .modelValue=${21}></lion-input>
|
||||
<button @click=${ev => console.log(ev.target.parentElement.modelValue)}>
|
||||
Log everything to Action Logger
|
||||
</button>
|
||||
<br />
|
||||
<button @click=${ev => console.log(ev.target.parentElement.formElements.nameGroup.modelValue)}>
|
||||
Log only Name fieldset to Action Logger
|
||||
</button>
|
||||
</lion-fieldset>
|
||||
`;
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
You can create validators that work on a fieldset level.
|
||||
Below, we mimic a `required` validator, but on the fieldset.
|
||||
Try it by typing something in the input, then removing it.
|
||||
|
||||
```js preview-story
|
||||
export const validation = () => {
|
||||
const DemoValidator = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'DemoValidator';
|
||||
}
|
||||
execute(value) {
|
||||
if (value && value.input1) {
|
||||
return false; // el.hasError = true
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static async getMessage() {
|
||||
return '[Fieldset Error] Demo error message';
|
||||
}
|
||||
};
|
||||
return html`
|
||||
<lion-fieldset id="someId" .validators="${[new DemoValidator()]}">
|
||||
<lion-input name="input1" label="Label"></lion-input>
|
||||
</lion-fieldset>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### Validating multiple inputs in a fieldset
|
||||
|
||||
You can have your fieldset validator take into consideration multiple fields.
|
||||
|
||||
```js preview-story
|
||||
export const validatingMultipleFields = () => {
|
||||
const IsCatsAndDogs = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'IsCatsAndDogs';
|
||||
}
|
||||
execute(value) {
|
||||
return !(value.input1 === 'cats' && value.input2 === 'dogs');
|
||||
}
|
||||
static async getMessage() {
|
||||
return '[Fieldset Error] Input 1 needs to be "cats" and Input 2 needs to be "dogs"';
|
||||
}
|
||||
};
|
||||
return html`
|
||||
<lion-fieldset .validators="${[new IsCatsAndDogs()]}">
|
||||
<lion-input label="An all time YouTube favorite" name="input1" help-text="cats"> </lion-input>
|
||||
<lion-input label="Another all time YouTube favorite" name="input2" help-text="dogs">
|
||||
</lion-input>
|
||||
</lion-fieldset>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
Alternatively you can also let the fieldset validator be dependent on the error states of its child fields.
|
||||
|
||||
Simply loop over the formElements inside your Validator's `execute` method:
|
||||
|
||||
```js
|
||||
this.formElements.some(el => el.hasFeedbackFor.includes('error'));
|
||||
```
|
||||
|
||||
### Validating multiple fieldsets
|
||||
|
||||
You can have your fieldset validator take into accounts multiple nested fieldsets.
|
||||
|
||||
```js preview-story
|
||||
export const validatingMultipleFieldsets = () => {
|
||||
const IsCatsDogs = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'IsCatsAndDogs';
|
||||
}
|
||||
execute(value) {
|
||||
if (
|
||||
value.inner1 &&
|
||||
value.inner1.input1 === 'cats' &&
|
||||
value.inner2 &&
|
||||
value.inner2.input1 === 'dogs'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static async getMessage() {
|
||||
return 'There is a problem with one of your fieldsets';
|
||||
}
|
||||
};
|
||||
return html`
|
||||
<lion-fieldset name="outer" .validators=${[new IsCatsDogs()]}>
|
||||
<lion-fieldset name="inner1">
|
||||
<label slot="label">Fieldset no. 1</label>
|
||||
<lion-input label="Write 'cats' here" name="input1"> </lion-input>
|
||||
</lion-fieldset>
|
||||
<hr />
|
||||
<lion-fieldset name="inner2">
|
||||
<label slot="label">Fieldset no. 2</label>
|
||||
<lion-input label="Write 'dogs' here" name="input1"> </lion-input>
|
||||
</lion-fieldset>
|
||||
</lion-fieldset>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
|
@ -66,6 +66,7 @@ export class HelperOutput extends LitElement {
|
|||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
__renderProp(p) {
|
||||
if (typeof p === 'boolean') {
|
||||
return p === true ? '✓' : '';
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
<Meta title="Forms/Intro" />
|
||||
|
||||
# Form System
|
||||
|
||||
The Form System allows you to create complex forms with various validations in an easy way.
|
||||
|
||||
## Features
|
||||
|
||||
- Built in [validate](?path=/docs/forms-validation-overview--page) for error/warning/info/success
|
||||
- Formatting of values
|
||||
- Accessible
|
||||
|
||||
For a more in depth description look into the [Form System Overview](?path=/docs/forms-system-overview--page).
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Description |
|
||||
| ---------------------------------------------------- | ------------------------------------------------- |
|
||||
| [checkbox](?path=/docs/forms-checkbox--default-story) | Checkbox form element |
|
||||
| [checkbox-group](?path=/docs/forms-checkbox-group--default-story) | Group of checkboxes |
|
||||
| [field](?path=/docs/forms-field--default-story) | Base class for all inputs |
|
||||
| [fieldset](?path=/docs/forms-fieldset-overview--page) | Group for form inputs |
|
||||
| [form](?path=/docs/forms-form-overview--page) | Wrapper for multiple form elements |
|
||||
| [input](?path=/docs/forms-input--default-story) | Input element for strings |
|
||||
| [input-amount](?path=/docs/forms-input-amount--default-story) | Input element for amounts |
|
||||
| [input-date](?path=/docs/forms-input-date--default-story) | Input element for dates |
|
||||
| [input-email](?path=/docs/forms-input-email--default-story) | Input element for e-mails |
|
||||
| [input-iban](?path=/docs/forms-input-iban--default-story) | Input element for IBANs |
|
||||
| [radio](?path=/docs/forms-radio--default-story) | Radio form element |
|
||||
| [radio-group](?path=/docs/forms-radio-group--default-story) | Group of radios |
|
||||
| [select](?path=/docs/forms-select--default-story) | Simple native dropdown element |
|
||||
| [textarea](?path=/docs/forms-textarea--default-story) | Multiline text input |
|
||||
| [validate](?path=/docs/forms-system-validate-system--default-story) | Validation for our form components |
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
import '@lion/checkbox-group/lion-checkbox-group.js';
|
||||
import '@lion/checkbox-group/lion-checkbox.js';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
import '@lion/form/lion-form.js';
|
||||
import '@lion/input-amount/lion-input-amount.js';
|
||||
import '@lion/input-date/lion-input-date.js';
|
||||
import '@lion/input-datepicker/lion-input-datepicker.js';
|
||||
import '@lion/input-email/lion-input-email.js';
|
||||
import '@lion/input-iban/lion-input-iban.js';
|
||||
import '@lion/input-range/lion-input-range.js';
|
||||
import '@lion/input/lion-input.js';
|
||||
import '@lion/radio-group/lion-radio-group.js';
|
||||
import '@lion/radio-group/lion-radio.js';
|
||||
import '@lion/select/lion-select.js';
|
||||
import '@lion/select-rich/lion-option.js';
|
||||
import '@lion/select-rich/lion-options.js';
|
||||
import '@lion/select-rich/lion-select-rich.js';
|
||||
import '@lion/textarea/lion-textarea.js';
|
||||
import { MinLength, Required } from '@lion/validate';
|
||||
|
||||
<Meta title="Forms/Features Overview" />
|
||||
|
||||
# Features Overview
|
||||
|
||||
This is a meta package to show interaction between various form elements.
|
||||
For usage and installation please see the appropriate packages.
|
||||
|
||||
## Umbrella Form
|
||||
|
||||
<Preview>
|
||||
<Story name="Example">
|
||||
{() => {
|
||||
Required.getMessage = () => 'Please enter a value';
|
||||
return html`
|
||||
<lion-form @model-value-changed="${(ev) => { console.log('lion-form::', ev.target.name, ':', ev.detail.formPath) }}">
|
||||
<form>
|
||||
<lion-fieldset name="full_name" @model-value-changed="${(ev) => { console.log('lion-fieldset::', ev.target.name, ':', ev.detail.formPath) }}">
|
||||
<lion-input
|
||||
name="first_name"
|
||||
label="First Name"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
name="last_name"
|
||||
label="Last Name"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input>
|
||||
</lion-fieldset>
|
||||
<lion-input-date
|
||||
name="date"
|
||||
label="Date of application"
|
||||
.modelValue="${new Date('2000-12-12')}"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input-date>
|
||||
<lion-input-datepicker
|
||||
name="datepicker"
|
||||
label="Date to be picked"
|
||||
.modelValue="${new Date('2020-12-12')}"
|
||||
.validators="${[new Required()]}"
|
||||
></lion-input-datepicker>
|
||||
<lion-textarea
|
||||
name="bio"
|
||||
label="Biography"
|
||||
.validators="${[new Required(), new MinLength(10)]}"
|
||||
help-text="Please enter at least 10 characters"
|
||||
></lion-textarea>
|
||||
<lion-input-amount name="money" label="Money"></lion-input-amount>
|
||||
<lion-input-iban name="iban" label="Iban"></lion-input-iban>
|
||||
<lion-input-email name="email" label="Email"></lion-input-email>
|
||||
<lion-checkbox-group
|
||||
@model-value-changed="${(ev) => { console.log('lion-cb-group::', ev.target.name, ':', ev.detail.formPath) }}"
|
||||
label="What do you like?"
|
||||
name="checkers"
|
||||
.validators="${[new Required()]}"
|
||||
>
|
||||
<lion-checkbox .choiceValue=${'foo'} label="I like foo"></lion-checkbox>
|
||||
<lion-checkbox .choiceValue=${'bar'} label="I like bar"></lion-checkbox>
|
||||
<lion-checkbox .choiceValue=${'baz'} label="I like baz"></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<lion-radio-group
|
||||
@model-value-changed="${(ev) => { console.log('lion-radio-group::', ev.target.name, ':', ev.detail.formPath) }}"
|
||||
name="dinosaurs"
|
||||
label="Favorite dinosaur"
|
||||
.validators="${[new Required()]}"
|
||||
>
|
||||
<lion-radio .choiceValue=${'allosaurus'} label="allosaurus"></lion-radio>
|
||||
<lion-radio .choiceValue=${'brontosaurus'} label="brontosaurus"></lion-radio>
|
||||
<lion-radio .choiceValue=${'diplodocus'} label="diplodocus"></lion-radio>
|
||||
</lion-radio-group>
|
||||
<lion-select-rich name="favoriteColor" label="Favorite color">
|
||||
<lion-options slot="input">
|
||||
<lion-option .choiceValue=${'red'}>Red</lion-option>
|
||||
<lion-option .choiceValue=${'hotpink'} checked>Hotpink</lion-option>
|
||||
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
||||
</lion-options>
|
||||
</lion-select-rich>
|
||||
<lion-select
|
||||
label="Lyrics"
|
||||
name="lyrics"
|
||||
.validators="${[new Required()]}"
|
||||
>
|
||||
<select slot="input">
|
||||
<option value="1">Fire up that loud</option>
|
||||
<option value="2">Another round of shots...</option>
|
||||
<option value="3">Drop down for what?</option>
|
||||
</select>
|
||||
</lion-select>
|
||||
<lion-input-range
|
||||
name="range"
|
||||
min="1"
|
||||
max="5"
|
||||
.modelValue="${2.3}"
|
||||
unit="%"
|
||||
step="0.1"
|
||||
label="Input range"
|
||||
></lion-input-range>
|
||||
<lion-checkbox-group .mulipleChoice="${false}" name="terms" .validators="${[new Required()]}">
|
||||
<lion-checkbox
|
||||
label="I blindly accept all terms and conditions"
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<lion-textarea name="comments" label="Comments"></lion-textarea>
|
||||
<div class="buttons">
|
||||
<lion-button raised>Submit</lion-button>
|
||||
<lion-button
|
||||
type="button"
|
||||
raised
|
||||
@click=${ev => ev.currentTarget.parentElement.parentElement.parentElement.resetGroup()}
|
||||
>Reset</lion-button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</lion-form>
|
||||
`;}}
|
||||
</Story>
|
||||
</Preview>
|
||||
|
||||
```js
|
||||
import '@lion/checkbox-group/lion-checkbox-group.js';
|
||||
import '@lion/checkbox-group/lion-checkbox.js';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
import '@lion/form/lion-form.js';
|
||||
import '@lion/input-amount/lion-input-amount.js';
|
||||
import '@lion/input-date/lion-input-date.js';
|
||||
import '@lion/input-datepicker/lion-input-datepicker.js';
|
||||
import '@lion/input-email/lion-input-email.js';
|
||||
import '@lion/input-iban/lion-input-iban.js';
|
||||
import '@lion/input-range/lion-input-range.js';
|
||||
import '@lion/input/lion-input.js';
|
||||
import '@lion/option/lion-option.js';
|
||||
import '@lion/radio-group/lion-radio-group.js';
|
||||
import '@lion/radio-group/lion-radio.js';
|
||||
import '@lion/select/lion-select.js';
|
||||
import '@lion/select-rich/lion-options.js';
|
||||
import '@lion/select-rich/lion-select-rich.js';
|
||||
import '@lion/textarea/lion-textarea.js';
|
||||
import { MinLength, Required } from '@lion/validate';
|
||||
```
|
||||
|
|
@ -1,694 +0,0 @@
|
|||
import { Meta, Story, html } from '@open-wc/demoing-storybook';
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { LionInput } from '@lion/input';
|
||||
import '@lion/input-amount/lion-input-amount.js';
|
||||
import '@lion/input-date/lion-input-date.js';
|
||||
import '@lion/input-email/lion-input-email.js';
|
||||
import '@lion/input/lion-input.js';
|
||||
import {
|
||||
DefaultSuccess,
|
||||
EqualsLength,
|
||||
IsDate,
|
||||
IsEmail,
|
||||
IsNumber,
|
||||
loadDefaultFeedbackMessages,
|
||||
MaxDate,
|
||||
MaxLength,
|
||||
MaxNumber,
|
||||
MinDate,
|
||||
MinLength,
|
||||
MinMaxDate,
|
||||
MinMaxLength,
|
||||
MinMaxNumber,
|
||||
MinNumber,
|
||||
Required,
|
||||
Validator,
|
||||
Pattern,
|
||||
} from '@lion/validate';
|
||||
|
||||
<Meta title="Forms/Validation/Examples" parameters={{ component: 'lion-input' }}/>
|
||||
|
||||
## Required Validator
|
||||
|
||||
The required validator can be put onto every form field element and will make sure that element is
|
||||
not empty.
|
||||
For an input that may mean that it is not an empty string,
|
||||
while for a checkbox group it means at least one checkbox needs to be checked.
|
||||
|
||||
<Story name="Required Validator">
|
||||
{() => {
|
||||
loadDefaultFeedbackMessages();
|
||||
return html`
|
||||
<lion-input .validators=${[new Required()]} label="Required"></lion-input>
|
||||
`}}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-input .validators="${[new Required()]}" label="Required"></lion-input>
|
||||
```
|
||||
|
||||
## String Validators
|
||||
|
||||
Useful on input elements it allows to define how many characters can be entered.
|
||||
|
||||
<Story name="String Validators">
|
||||
{html`
|
||||
<lion-input
|
||||
.validators=${[new EqualsLength(7)]}
|
||||
.modelValue=${'not exactly'}
|
||||
label="EqualsLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators=${[new MinLength(10)]}
|
||||
.modelValue=${'too short'}
|
||||
label="MinLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators=${[new MaxLength(7)]}
|
||||
.modelValue=${'too long'}
|
||||
label="MaxLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators=${[new MinMaxLength({ min: 10, max: 20 })]}
|
||||
.modelValue=${'that should be enough'}
|
||||
label="MinMaxLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators=${[new Pattern(/#LionRocks/)]}
|
||||
.modelValue=${'regex checks if "#Lion<NO SPACE>Rocks" is in this input #LionRocks'}
|
||||
label="Pattern"
|
||||
></lion-input>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-input
|
||||
.validators="${[new EqualsLength(7)]}"
|
||||
.modelValue="${'not exactly'}"
|
||||
label="EqualsLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators="${[new MinLength(10)]}"
|
||||
.modelValue="${'too short'}"
|
||||
label="MinLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators="${[new MaxLength(7)]}"
|
||||
.modelValue="${'too long'}"
|
||||
label="MaxLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators="${[new MinMaxLength({ min: 10, max: 20 })]}"
|
||||
.modelValue="${'that should be enough'}"
|
||||
label="MinMaxLength"
|
||||
></lion-input>
|
||||
```
|
||||
|
||||
## Number Validators
|
||||
|
||||
Number validations assume that it's modelValue is actually a number.
|
||||
Therefore it may only be used on input that have an appropriate parser/formatter like the input-amount.
|
||||
|
||||
<Story name="Number Validators">
|
||||
{html`
|
||||
<lion-input-amount
|
||||
.validators="${[new IsNumber()]}"
|
||||
.modelValue="${'foo'}"
|
||||
label="IsNumber"
|
||||
></lion-input-amount>
|
||||
<lion-input-amount
|
||||
.validators="${[new MinNumber(7)]}"
|
||||
.modelValue="${5}"
|
||||
label="MinNumber"
|
||||
></lion-input-amount>
|
||||
<lion-input-amount
|
||||
.validators="${[new MaxNumber(7)]}"
|
||||
.modelValue="${9}"
|
||||
label="MaxNumber"
|
||||
></lion-input-amount>
|
||||
<lion-input-amount
|
||||
.validators="${[new MinMaxNumber({ min: 10, max: 20 })]}"
|
||||
.modelValue="${5}"
|
||||
label="MinMaxNumber"
|
||||
></lion-input-amount>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-input-amount
|
||||
.validators="${[new IsNumber()]}"
|
||||
.modelValue="${'foo'}"
|
||||
label="IsNumber"
|
||||
></lion-input-amount>
|
||||
<lion-input-amount
|
||||
.validators="${[new MinNumber(7)]}"
|
||||
.modelValue="${5}"
|
||||
label="MinNumber"
|
||||
></lion-input-amount>
|
||||
<lion-input-amount
|
||||
.validators="${[new MaxNumber(7)]}"
|
||||
.modelValue="${9}"
|
||||
label="MaxNumber"
|
||||
></lion-input-amount>
|
||||
<lion-input-amount
|
||||
.validators="${[new MinMaxNumber({ min: 10, max: 20 })]}"
|
||||
.modelValue="${5}"
|
||||
label="MinMaxNumber"
|
||||
></lion-input-amount>
|
||||
```
|
||||
|
||||
## Date Validators
|
||||
|
||||
Date validators work with real javascript dates. Use them on input-date.
|
||||
|
||||
<Story name="Date Validators">
|
||||
{() => {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = today.getMonth();
|
||||
const day = today.getDate();
|
||||
const yesterday = new Date(year, month, day - 1);
|
||||
const tomorrow = new Date(year, month, day + 1);
|
||||
return html`
|
||||
<lion-input-date
|
||||
.validators=${[new IsDate()]}
|
||||
.modelValue=${'foo'}
|
||||
label="IsDate"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
.validators=${[new MinDate(today)]}
|
||||
.modelValue=${new Date(yesterday)}
|
||||
label="MinDate"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
.validators=${[new MaxDate(today)]}
|
||||
.modelValue=${new Date(tomorrow)}
|
||||
label="MaxDate"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
.validators=${[new MinMaxDate({ min: new Date(yesterday), max: new Date(tomorrow) })]}
|
||||
.modelValue=${new Date(today)}
|
||||
label="MinMaxDate"
|
||||
></lion-input-date>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
|
||||
```js
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = today.getMonth();
|
||||
const day = today.getDate();
|
||||
const yesterday = new Date(year, month, day - 1);
|
||||
const tomorrow = new Date(year, month, day + 1);
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-input-date
|
||||
.validators="${[new IsDate()]}"
|
||||
.modelValue="${'foo'}"
|
||||
label="IsDate"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
.validators="${[new MinDate(today)]}"
|
||||
.modelValue="${new Date(yesterday)}"
|
||||
label="MinDate"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
.validators="${[new MaxDate(today)]}"
|
||||
.modelValue="${new Date(tomorrow)}"
|
||||
label="MaxDate"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
.validators="${[new MinMaxDate({ min: new Date(yesterday), max: new Date(tomorrow) })]}"
|
||||
.modelValue="${new Date(today)}"
|
||||
label="MinMaxDate"
|
||||
></lion-input-date>
|
||||
```
|
||||
|
||||
## Email Validator
|
||||
|
||||
<Story name="Email Validator">
|
||||
{html`
|
||||
<lion-input-email
|
||||
.validators="${[new IsEmail()]}"
|
||||
.modelValue="${'foo'}"
|
||||
label="IsEmail"
|
||||
></lion-input-email>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-input-email
|
||||
.validators="${[new IsEmail()]}"
|
||||
.modelValue="${'foo'}"
|
||||
label="IsEmail"
|
||||
></lion-input-email>
|
||||
```
|
||||
|
||||
## Validation Types
|
||||
|
||||
When defining your own component you can decide to allow for multiple types of validation.
|
||||
By default only `error` is used, however there are certainly use cases where warning or success messages make sense.
|
||||
|
||||
<Story name="Validation Types">
|
||||
{() => {
|
||||
try {
|
||||
class MyTypesInput extends LionInput {
|
||||
static get validationTypes() {
|
||||
return ['error', 'warning', 'info', 'success'];
|
||||
}
|
||||
}
|
||||
customElements.define('my-types-input', MyTypesInput);
|
||||
} catch (err) {
|
||||
// expected as it is a demo
|
||||
}
|
||||
return html`
|
||||
<style>
|
||||
.demo-types-input {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='success'] {
|
||||
background-color: #e4ffe4;
|
||||
border: 1px solid green;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='error'] {
|
||||
background-color: #ffd4d4;
|
||||
border: 1px solid red;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='warning'] {
|
||||
background-color: #ffe4d4;
|
||||
border: 1px solid orange;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='info'] {
|
||||
background-color: #d4e4ff;
|
||||
border: 1px solid blue;
|
||||
}
|
||||
</style>
|
||||
<my-types-input
|
||||
.validators="${[
|
||||
new Required(),
|
||||
new MinLength(7, { type: 'warning' }),
|
||||
new MaxLength(10, {
|
||||
type: 'info',
|
||||
getMessage: () => `Please, keep the length below the 10 characters.`,
|
||||
}),
|
||||
new DefaultSuccess(),
|
||||
]}"
|
||||
.modelValue="${'exactly'}"
|
||||
label="Validation Types"
|
||||
class="demo-types-input"
|
||||
></my-types-input>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
try {
|
||||
class MyTypesInput extends LionInput {
|
||||
static get validationTypes() {
|
||||
return ['error', 'warning', 'info', 'success'];
|
||||
}
|
||||
}
|
||||
customElements.define('my-types-input', MyTypesInput);
|
||||
} catch (err) {
|
||||
// expected as it is a demo
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<style>
|
||||
.demo-types-input {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='success'] {
|
||||
background-color: #e4ffe4;
|
||||
border: 1px solid green;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='error'] {
|
||||
background-color: #ffd4d4;
|
||||
border: 1px solid red;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='warning'] {
|
||||
background-color: #ffe4d4;
|
||||
border: 1px solid orange;
|
||||
}
|
||||
.demo-types-input[shows-feedback-for~='info'] {
|
||||
background-color: #d4e4ff;
|
||||
border: 1px solid blue;
|
||||
}
|
||||
</style>
|
||||
<my-types-input
|
||||
.validators="${[
|
||||
new Required(),
|
||||
new MinLength(7, { type: 'warning' }),
|
||||
new MaxLength(10, {
|
||||
type: 'info',
|
||||
getMessage: () => `Please, keep the length below the 10 characters.`,
|
||||
}),
|
||||
new DefaultSuccess(),
|
||||
]}"
|
||||
.modelValue="${'exactly'}"
|
||||
label="Validation Types"
|
||||
class="demo-types-input"
|
||||
></my-types-input>
|
||||
```
|
||||
|
||||
## Custom Validators
|
||||
|
||||
Here is an example how you can make your own validator and providing the error messages directly within.
|
||||
You can even hard code localization in there if needed or you can use a localization system.
|
||||
|
||||
<Story name="Custom Validators">
|
||||
{() => {
|
||||
class MyValidator extends Validator {
|
||||
static get validatorName() {
|
||||
return 'myValidator';
|
||||
}
|
||||
execute(modelValue, param) {
|
||||
return modelValue !== param;
|
||||
}
|
||||
static getMessage({ fieldName, modelValue, params: param }) {
|
||||
if (modelValue.length >= param.length - 1 && param.startsWith(modelValue)) {
|
||||
return 'Almost there...';
|
||||
}
|
||||
return `No "${param}" found in ${fieldName}`;
|
||||
}
|
||||
}
|
||||
return html`
|
||||
<lion-input
|
||||
label="Custom validator"
|
||||
help-text="Type 'mine' please"
|
||||
.validators="${[new MyValidator('mine')]}"
|
||||
.modelValue="${'mi'}"
|
||||
></lion-input>
|
||||
`;
|
||||
}
|
||||
}
|
||||
</Story>
|
||||
|
||||
|
||||
```js
|
||||
class MyValidator extends Validator {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
static get validatorName() {
|
||||
return 'myValidator';
|
||||
}
|
||||
|
||||
execute(modelValue, param) {
|
||||
return modelValue !== param;
|
||||
}
|
||||
|
||||
static getMessage({ fieldName, modelValue, params: param }) {
|
||||
if (modelValue.length >= param.length - 1 && param.startsWith(modelValue)) {
|
||||
return 'Almost there...';
|
||||
}
|
||||
return `No "${param}" found in ${fieldName}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-input
|
||||
label="Custom validator"
|
||||
help-text="Type 'mine' please"
|
||||
.validators="${[new MyValidator('mine')]}"
|
||||
.modelValue="${'mi'}"
|
||||
></lion-input>
|
||||
```
|
||||
|
||||
## Default messages
|
||||
|
||||
To get default validation messages you need to import and call the `loadDefaultFeedbackMessages` function once in your application.
|
||||
|
||||
Sometimes the default messages don't make sense for your input field. In that case you want to override it by adding a `getMessage` function to your validator.
|
||||
|
||||
<Story name="Override default messages">
|
||||
{html`
|
||||
<lion-input
|
||||
.validators="${[new EqualsLength(4, { getMessage: () => '4 chars please...' })]}"
|
||||
.modelValue="${'123'}"
|
||||
label="Custom message for validator instance"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators="${[
|
||||
new EqualsLength(4, {
|
||||
getMessage: ({ modelValue, params: param }) => {
|
||||
const diff = modelValue.length - param;
|
||||
return `${Math.abs(diff)} too ${diff > 0 ? 'much' : 'few'}...`;
|
||||
},
|
||||
}),
|
||||
]}"
|
||||
.modelValue="${'way too much'}"
|
||||
label="Dynamic message for validator instance"
|
||||
></lion-input>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-input
|
||||
.validators="${[new EqualsLength(4, { getMessage: () => '4 chars please...' })]}"
|
||||
.modelValue="${'123'}"
|
||||
label="Custom message for validator instance"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators="${[
|
||||
new EqualsLength(4, {
|
||||
getMessage: ({ modelValue, params: param }) => {
|
||||
const diff = modelValue.length - param;
|
||||
return `${Math.abs(diff)} too ${diff > 0 ? 'much' : 'few'}...`;
|
||||
},
|
||||
}),
|
||||
]}"
|
||||
.modelValue="${'way too much'}"
|
||||
label="Dynamic message for validator instance"
|
||||
></lion-input>
|
||||
```
|
||||
|
||||
## Override fieldName
|
||||
|
||||
In the scenario that the default messages are correct, but you only want to change the `fieldName`, this can both be done for a single specific validator or for all at once.
|
||||
|
||||
<Story name="Override fieldName">
|
||||
{html`
|
||||
<lion-input
|
||||
.validators="${[new EqualsLength(4, { fieldName: 'custom fieldName' })]}"
|
||||
.modelValue="${'123'}"
|
||||
label="Custom fieldName for 1 validator"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators="${[new Required(), new EqualsLength(4)]}"
|
||||
.fieldName="${'custom fieldName'}"
|
||||
.modelValue="${'123'}"
|
||||
label="Custom fieldName for all validators"
|
||||
></lion-input>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-input
|
||||
.validators="${[new EqualsLength(4, { fieldName: 'custom fieldName' })]}"
|
||||
.modelValue="${'123'}"
|
||||
label="Custom fieldName for 1 validator"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
.validators="${[new Required(), new EqualsLength(4)]}"
|
||||
.fieldName="${'custom fieldName'}"
|
||||
.modelValue="${'123'}"
|
||||
label="Custom fieldName for all validators"
|
||||
></lion-input>
|
||||
```
|
||||
|
||||
## Asynchronous validation
|
||||
|
||||
<Story name="Asynchronous validation">
|
||||
{() => {
|
||||
function pause(ms = 0) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
class AsyncValidator extends Validator {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
static get validatorName() {
|
||||
return 'asyncValidator';
|
||||
}
|
||||
static get async() {
|
||||
return true;
|
||||
}
|
||||
async execute() {
|
||||
console.log('async pending...');
|
||||
await pause(2000);
|
||||
console.log('async done...');
|
||||
return true;
|
||||
}
|
||||
static getMessage({ modelValue }) {
|
||||
return `validated for modelValue: ${modelValue}...`;
|
||||
}
|
||||
}
|
||||
return html`
|
||||
<style>
|
||||
lion-input[is-pending] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
<lion-input
|
||||
label="Async validation"
|
||||
.validators="${[new AsyncValidator()]}"
|
||||
.modelValue="${'123'}"
|
||||
></lion-input>
|
||||
`}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
function pause(ms = 0) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
class AsyncValidator extends Validator {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
static get validatorName() {
|
||||
return 'asyncValidator';
|
||||
}
|
||||
static get async() {
|
||||
return true;
|
||||
}
|
||||
async execute() {
|
||||
console.log('async pending...');
|
||||
await pause(2000);
|
||||
console.log('async done...');
|
||||
return true;
|
||||
}
|
||||
static getMessage({ modelValue }) {
|
||||
return `validated for modelValue: ${modelValue}...`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic parameter change
|
||||
|
||||
<Story name="Dynamic parameter change">
|
||||
{() => {
|
||||
const beginDate = new Date('09/09/1990');
|
||||
const minDateValidatorRef = new MinDate(beginDate, {
|
||||
message: 'Fill in a date after your birth date',
|
||||
});
|
||||
return html`
|
||||
<lion-input-date
|
||||
label="Birth date"
|
||||
help-text="Adjust this date to retrigger validation of the input below..."
|
||||
.modelValue="${beginDate}"
|
||||
@model-value-changed="${({ target: { modelValue, errorState } }) => {
|
||||
if (!errorState) {
|
||||
// Since graduation date is usually not before birth date
|
||||
minDateValidatorRef.param = modelValue;
|
||||
}
|
||||
}}"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
label="Graduation date"
|
||||
.modelValue="${new Date('09/09/1989')}"
|
||||
.validators="${[minDateValidatorRef]}"
|
||||
></lion-input-date>
|
||||
`}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
const beginDate = new Date('09/09/1990');
|
||||
const minDateValidatorRef = new MinDate(beginDate, {
|
||||
message: 'Fill in a date after your birth date',
|
||||
});
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-input-date
|
||||
label="Your birth date"
|
||||
help-text="Adjust this date to retrigger validation of the input below..."
|
||||
.modelValue="${beginDate}"
|
||||
@model-value-changed="${({ target: { modelValue, errorState } }) => {
|
||||
if (!errorState) {
|
||||
// Since graduation date is usually not before birth date
|
||||
minDateValidatorRef.param = modelValue;
|
||||
}
|
||||
}}"
|
||||
></lion-input-date>
|
||||
<lion-input-date
|
||||
label="Your graduation date"
|
||||
.modelValue="${new Date('09/09/1989')}"
|
||||
.validators="${[minDateValidatorRef]}"
|
||||
></lion-input-date>
|
||||
```
|
||||
|
||||
## Disabled inputs validation
|
||||
|
||||
According to the W3C specs, Disabled fields should not be validated.
|
||||
Therefor if the attribute disabled is present on a lion-input it will not be validated.
|
||||
|
||||
<Story name="Disabled Validators">
|
||||
{html`
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new EqualsLength(7)]}
|
||||
.modelValue=${'not exactly'}
|
||||
label="EqualsLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new MinLength(10)]}
|
||||
.modelValue=${'too short'}
|
||||
label="MinLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new MaxLength(7)]}
|
||||
.modelValue=${'too long'}
|
||||
label="MaxLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new MinMaxLength({ min: 10, max: 20 })]}
|
||||
.modelValue=${'that should be enough'}
|
||||
label="MinMaxLength"
|
||||
></lion-input>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new EqualsLength(7)]}
|
||||
.modelValue=${'not exactly'}
|
||||
label="EqualsLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new MinLength(10)]}
|
||||
.modelValue=${'too short'}
|
||||
label="MinLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new MaxLength(7)]}
|
||||
.modelValue=${'too long'}
|
||||
label="MaxLength"
|
||||
></lion-input>
|
||||
<lion-input
|
||||
disabled
|
||||
.validators=${[new MinMaxLength({ min: 10, max: 20 })]}
|
||||
.modelValue=${'that should be enough'}
|
||||
label="MinMaxLength"
|
||||
></lion-input>
|
||||
```
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
import { render } from '@lion/core';
|
||||
import { renderLitAsNode } from '@lion/helpers';
|
||||
import '@lion/input/lion-input.js';
|
||||
import { Validator } from '@lion/validate';
|
||||
import './helper-wc/h-output.js';
|
||||
|
||||
<Meta title="Forms/System/Interaction States" />
|
||||
|
||||
# Interaction States
|
||||
|
||||
`InteractionStateMixin` saves meta information about interaction states.
|
||||
It allows for creating advanced UX scenarios.
|
||||
|
||||
Examples of such scenarios already in our fields:
|
||||
|
||||
- Show the validation message of an input only after the user has blurred the input field
|
||||
- Hide the validation message when an invalid value becomes valid
|
||||
|
||||
Something our subclassers can implement:
|
||||
|
||||
- Show a red border around the input right after the input became invalid
|
||||
|
||||
The meta information that InteractionStateMixin collects, is stored in the Boolean properties on our fields:
|
||||
|
||||
- `touched`, the user blurred the field
|
||||
- `dirty`, the value in the field has changed
|
||||
- `prefilled`, a prepopulated field is not empty
|
||||
|
||||
> You can listen to the events `touched-changed` and `dirty-changed`.
|
||||
|
||||
<Story name="Interaction States">
|
||||
{html`
|
||||
<lion-input
|
||||
label="Interaction States"
|
||||
help-text="Interact with this field to see how dirty, touched and prefilled change"
|
||||
.modelValue="${'myValue'}"
|
||||
></lion-input>
|
||||
<h-output .show="${['touched', 'dirty', 'prefilled', 'focused', 'submitted']}"></h-output>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-input
|
||||
label="Interaction States"
|
||||
help-text="Interact with this field to see how dirty, touched and prefilled change"
|
||||
.modelValue="${'myValue'}"
|
||||
></lion-input>
|
||||
```
|
||||
|
||||
## Advanced use cases
|
||||
|
||||
### Overriding interaction states
|
||||
|
||||
When creating an extension of LionField or LionInput, it can be needed to override the way prefilled values are 'computed'.
|
||||
|
||||
The example below shows how this is done for checkboxes/radio-inputs.
|
||||
|
||||
```js
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_isEmpty() {
|
||||
return !this.checked;
|
||||
}
|
||||
```
|
||||
|
||||
## When is feedback shown to the user
|
||||
|
||||
We show the validity feedback when one of the following conditions is met:
|
||||
|
||||
- **prefilled**,
|
||||
the user already filled in something, or the value is prefilled
|
||||
when the form is initially rendered.
|
||||
|
||||
- **touched** && **dirty**
|
||||
|
||||
- When a user starts typing for the first time in a field with for instance `required` validation,
|
||||
error message should not be shown until a field becomes `touched` (a user leaves(blurs) a field).
|
||||
- When a user enters a field without altering the value (making it `dirty` but not `touched`),
|
||||
an error message shouldn't be shown either.
|
||||
|
||||
- **submitted**,
|
||||
if the form is submitted, always show the error message.
|
||||
|
||||
### Changing the feedback show condition (Subclassers)
|
||||
|
||||
You can change the condition upon which feedback gets shown.
|
||||
|
||||
In order to override the feedback show conditions, you need to create a custom field and override `_showFeedbackConditionFor` method.
|
||||
This method accepts the a `type` parameter which is a String representing the type of feedback (e.g. 'error').
|
||||
Then, it returns true of false, depending on whether feedback for that type should be shown or not. That part, you can control.
|
||||
|
||||
In the following example we will demonstrate this with interaction states, the most common use case for feedback visibility conditions.
|
||||
|
||||
<Story name="Feedback Condition">
|
||||
{() => {
|
||||
// 1. Initialize variables...
|
||||
// properties on InteractionStateMixin we want to check conditions for
|
||||
const props = ['touched', 'dirty', 'prefilled', 'focused', 'filled', 'submitted'];
|
||||
// Here we will store the conditions (trigger for validation feedback)
|
||||
// provided via the UI of the demo
|
||||
let conditions = [];
|
||||
// 2. Create a validator...
|
||||
// Define a demo validator that should only be visible on an odd amount of characters
|
||||
// const OddValidator = [modelValue => ({ odd: modelValue.length % 2 !== 0 })];
|
||||
class OddValidator extends Validator {
|
||||
static get validatorName() {
|
||||
return 'OddValidator';
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
execute(value) {
|
||||
let hasError = false;
|
||||
if (!(value.length % 2 !== 0)) {
|
||||
hasError = true;
|
||||
}
|
||||
return hasError;
|
||||
}
|
||||
_getMessage() {
|
||||
return 'Add or remove one character';
|
||||
}
|
||||
}
|
||||
// 3. Create field overriding .showErrorCondition...
|
||||
// Here we will store a reference to the Field element that overrides the default condition
|
||||
// (function `showErrorCondition`) for triggering validation feedback of `.validators`
|
||||
const fieldElement = renderLitAsNode(html`
|
||||
<lion-input
|
||||
name="interactionField"
|
||||
label="Only an odd amount of characters allowed"
|
||||
help-text="Change feedback condition"
|
||||
.modelValue="${'notodd'}"
|
||||
.validators="${[new OddValidator()]}"
|
||||
>
|
||||
<input slot="input" />
|
||||
</lion-input>
|
||||
`);
|
||||
fieldElement._showFeedbackConditionFor = type => {
|
||||
return conditions.every(condition => {
|
||||
/**
|
||||
* This here shows bug for focused state.
|
||||
* Focused is set to true AFTER we already evaluate feedback conditions
|
||||
* Bug report: https://github.com/ing-bank/lion/issues/455
|
||||
*/
|
||||
// setTimeout(() => console.log(fieldElement[condition])); //
|
||||
return fieldElement[condition];
|
||||
});
|
||||
};
|
||||
function fetchConditionsAndReevaluate({ currentTarget: { modelValue } }) {
|
||||
if (!modelValue['props[]']) {
|
||||
return;
|
||||
}
|
||||
// Create props list like: ['touched', 'submitted']
|
||||
conditions = modelValue['props[]'].filter(p => p.checked).map(p => p.value);
|
||||
// Reevaluate
|
||||
fieldElement.validate();
|
||||
}
|
||||
fieldElement.addEventListener('focus', () => {
|
||||
fieldElement.validate();
|
||||
});
|
||||
return html`
|
||||
<lion-form>
|
||||
<form>
|
||||
${fieldElement}
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
</lion-form>
|
||||
<h-output .field="${fieldElement}" .show="${[...props, 'hasFeedbackFor']}"> </h-output>
|
||||
<h3>
|
||||
Set conditions for validation feedback visibility
|
||||
</h3>
|
||||
<lion-checkbox-group name="props[]" @model-value-changed="${fetchConditionsAndReevaluate}">
|
||||
${props.map(
|
||||
p => html`
|
||||
<lion-checkbox .label="${p}" .choiceValue="${p}"> </lion-checkbox>
|
||||
`,
|
||||
)}
|
||||
</lion-checkbox-group>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
import '@lion/input/lion-input.js';
|
||||
import '@lion/button/lion-button.js';
|
||||
|
||||
<Meta title="Forms/System/Field integrations" />
|
||||
|
||||
# Content inside fields
|
||||
|
||||
Due to our custom inputs being Web Components, it is possible to put HTML content inside an input.
|
||||
For example if you want to add a button as a prefix or suffix.
|
||||
|
||||
<Story name="Buttons within fields">
|
||||
{html`
|
||||
<lion-input label="Prefix and sufix">
|
||||
<lion-button slot="prefix" type="button">prefix</lion-button>
|
||||
<lion-button slot="suffix" type="button">suffix</lion-button>
|
||||
</lion-input>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-input label="Prefix and sufix">
|
||||
<lion-button slot="prefix" type="button">prefix</lion-button>
|
||||
<lion-button slot="suffix" type="button">suffix</lion-button>
|
||||
</lion-input>
|
||||
```
|
||||
|
|
@ -1,387 +0,0 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
import '@lion/input/lion-input.js';
|
||||
import { localize } from '@lion/localize';
|
||||
import { loadDefaultFeedbackMessages, MinLength, Validator, Required } from '@lion/validate';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
|
||||
<Meta title="Forms/Fieldset/Examples" parameters={{ component: 'lion-fieldset' }} />
|
||||
|
||||
# Fieldset Examples
|
||||
|
||||
<Story name="Default">
|
||||
{html`
|
||||
<style>
|
||||
lion-fieldset {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
<lion-fieldset name="nameGroup" label="Name">
|
||||
<lion-input name="FirstName" label="First Name"></lion-input>
|
||||
<lion-input name="LastName" label="Last Name"></lion-input>
|
||||
</lion-fieldset>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-fieldset name="nameGroup" label="Name">
|
||||
<lion-input name="FirstName" label="First Name"></lion-input>
|
||||
<lion-input name="LastName" label="Last Name"></lion-input>
|
||||
</lion-fieldset>
|
||||
```
|
||||
|
||||
### With Data
|
||||
|
||||
The fieldset's modelValue is an `Object` containing properties where the key is the `name` attribute of the field,
|
||||
and the value is the `modelValue` of the field.
|
||||
|
||||
<Story name="Data">
|
||||
{html`
|
||||
<lion-fieldset name="nameGroup" label="Name">
|
||||
<lion-input name="firstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="lastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
<button @click=${ev => console.log(ev.target.parentElement.modelValue)}>
|
||||
Log to Action Logger
|
||||
</button>
|
||||
</lion-fieldset>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-fieldset name="nameGroup" label="Name">
|
||||
<lion-input name="firstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="lastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
<button @click=${ev => console.log(ev.target.parentElement.modelValue)}>
|
||||
Log to Action Logger
|
||||
</button>
|
||||
</lion-fieldset>
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Disabling a fieldset disables all its child fields.
|
||||
When enabling a fieldset, fields that have disabled explicitly set will stay disabled.
|
||||
|
||||
<Story name="Disabled">
|
||||
{() => {
|
||||
function toggleDisabled() {
|
||||
const fieldset = document.querySelector('#fieldset');
|
||||
fieldset.disabled = !fieldset.disabled;
|
||||
}
|
||||
return html`
|
||||
<lion-fieldset name="nameGroup" label="Name" id="fieldset" disabled>
|
||||
<lion-input name="FirstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="LastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
<lion-fieldset name="nameGroup2" label="Name">
|
||||
<lion-input
|
||||
name="FirstName2"
|
||||
label="First Name"
|
||||
.modelValue=${'Foo'}
|
||||
disabled
|
||||
></lion-input>
|
||||
<lion-input name="LastName2" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
</lion-fieldset>
|
||||
</lion-fieldset>
|
||||
<button @click=${toggleDisabled}>
|
||||
Toggle disabled
|
||||
</button>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
function toggleDisabled() {
|
||||
const fieldset = document.querySelector('#fieldset');
|
||||
fieldset.disabled = !fieldset.disabled;
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-fieldset name="nameGroup" label="Name" id="fieldset" disabled>
|
||||
<lion-input name="FirstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="LastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
<lion-fieldset name="nameGroup2" label="Name">
|
||||
<lion-input
|
||||
name="FirstName2"
|
||||
label="First Name"
|
||||
.modelValue=${'Foo'}
|
||||
disabled
|
||||
></lion-input>
|
||||
<lion-input name="LastName2" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
</lion-fieldset>
|
||||
</lion-fieldset>
|
||||
<button @click=${toggleDisabled}>
|
||||
Toggle disabled
|
||||
</button>
|
||||
```
|
||||
|
||||
### Nesting fieldsets
|
||||
|
||||
Fieldsets can also be nested. The level of nesting will correspond one to one with the `modelValue` object.
|
||||
|
||||
<Story name="Nesting fieldsets">
|
||||
{html`
|
||||
<lion-fieldset>
|
||||
<div slot="label">Personal data</div>
|
||||
<lion-fieldset name="nameGroup" label="Name">
|
||||
<lion-input name="FirstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="LastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
</lion-fieldset>
|
||||
<lion-fieldset name="location" label="Location">
|
||||
<lion-input name="country" label="Country" .modelValue=${'Netherlands'}></lion-input>
|
||||
</lion-fieldset>
|
||||
<lion-input name="age" label="Age" .modelValue=${21}></lion-input>
|
||||
<button @click=${ev => console.log(ev.target.parentElement.modelValue)}>
|
||||
Log everything to Action Logger
|
||||
</button>
|
||||
<br />
|
||||
<button
|
||||
@click=${ev => console.log(ev.target.parentElement.formElements.nameGroup.modelValue)}
|
||||
>
|
||||
Log only Name fieldset to Action Logger
|
||||
</button>
|
||||
</lion-fieldset>
|
||||
`}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<lion-fieldset>
|
||||
<div slot="label">Personal data</div>
|
||||
<lion-fieldset name="nameGroup" label="Name">
|
||||
<lion-input name="FirstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="LastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
</lion-fieldset>
|
||||
<lion-fieldset name="location" label="Location">
|
||||
<lion-input name="country" label="Country" .modelValue=${'Netherlands'}></lion-input>
|
||||
</lion-fieldset>
|
||||
<lion-input name="age" label="Age" .modelValue=${21}></lion-input>
|
||||
<button @click=${ev => console.log(ev.target.parentElement.modelValue)}>
|
||||
Log everything to Action Logger
|
||||
</button>
|
||||
<br />
|
||||
<button
|
||||
@click=${ev => console.log(ev.target.parentElement.formElements.nameGroup.modelValue)}
|
||||
>
|
||||
Log only Name fieldset to Action Logger
|
||||
</button>
|
||||
</lion-fieldset>
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
You can create validators that work on a fieldset level.
|
||||
Below, we mimic a `required` validator, but on the fieldset.
|
||||
Try it by typing something in the input, then removing it.
|
||||
|
||||
<Story name="Validation">
|
||||
{() => {
|
||||
const DemoValidator = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'DemoValidator';
|
||||
}
|
||||
execute(value) {
|
||||
if (value && value.input1) {
|
||||
return false; // el.hasError = true
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static async getMessage() {
|
||||
return '[Fieldset Error] Demo error message';
|
||||
}
|
||||
};
|
||||
return html`
|
||||
<lion-fieldset id="someId" .validators="${[new DemoValidator()]}">
|
||||
<lion-input name="input1" label="Label"></lion-input>
|
||||
</lion-fieldset>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
const DemoValidator = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'DemoValidator';
|
||||
}
|
||||
|
||||
execute(value) {
|
||||
if (value && value.input1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static async getMessage() {
|
||||
return '[Fieldset Error] Demo error message';
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-fieldset id="someId" .validators="${[new DemoValidator()]}">
|
||||
<lion-input name="input1" label="Label"></lion-input>
|
||||
</lion-fieldset>
|
||||
```
|
||||
|
||||
### Validating multiple inputs in a fieldset
|
||||
|
||||
You can have your fieldset validator take into consideration multiple fields.
|
||||
|
||||
<Story name="Validating 2 fields">
|
||||
{() => {
|
||||
const IsCatsAndDogs = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'IsCatsAndDogs';
|
||||
}
|
||||
execute(value) {
|
||||
return !(value.input1 === 'cats' && value.input2 === 'dogs');
|
||||
}
|
||||
static async getMessage() {
|
||||
return '[Fieldset Error] Input 1 needs to be "cats" and Input 2 needs to be "dogs"';
|
||||
}
|
||||
};
|
||||
return html`
|
||||
<lion-fieldset .validators="${[new IsCatsAndDogs()]}">
|
||||
<lion-input
|
||||
label="An all time YouTube favorite"
|
||||
name="input1"
|
||||
help-text="cats"
|
||||
>
|
||||
</lion-input>
|
||||
<lion-input
|
||||
label="Another all time YouTube favorite"
|
||||
name="input2"
|
||||
help-text="dogs"
|
||||
>
|
||||
</lion-input>
|
||||
</lion-fieldset>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
const IsCatsAndDogs = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'IsCatsAndDogs';
|
||||
}
|
||||
|
||||
execute(value) {
|
||||
return !(value.input1 === 'cats' && value.input2 === 'dogs');
|
||||
}
|
||||
|
||||
static async getMessage() {
|
||||
return '[Fieldset Error] Input 1 needs to be "cats" and Input 2 needs to be "dogs"';
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-fieldset .validators="${[new IsCatsAndDogs()]}">
|
||||
<lion-input
|
||||
label="An all time YouTube favorite"
|
||||
name="input1"
|
||||
help-text="cats"
|
||||
>
|
||||
</lion-input>
|
||||
<lion-input
|
||||
label="Another all time YouTube favorite"
|
||||
name="input2"
|
||||
help-text="dogs"
|
||||
>
|
||||
</lion-input>
|
||||
</lion-fieldset>
|
||||
```
|
||||
|
||||
Alternatively you can also let the fieldset validator be dependent on the error states of its child fields.
|
||||
|
||||
Simply loop over the formElements inside your Validator's `execute` method:
|
||||
```js
|
||||
this.formElements.some(el => el.hasFeedbackFor.includes('error'));
|
||||
```
|
||||
|
||||
### Validating multiple fieldsets
|
||||
|
||||
You can have your fieldset validator take into accounts multiple nested fieldsets.
|
||||
|
||||
<Story name="Validating 2 fieldsets">
|
||||
{() => {
|
||||
const IsCatsDogs = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'IsCatsAndDogs';
|
||||
}
|
||||
execute(value) {
|
||||
if ((value.inner1 && value.inner1.input1 === 'cats') &&
|
||||
(value.inner2 && value.inner2.input1 === 'dogs')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static async getMessage() {
|
||||
return 'There is a problem with one of your fieldsets';
|
||||
}
|
||||
};
|
||||
return html`
|
||||
<lion-fieldset name="outer" .validators=${[new IsCatsDogs()]}>
|
||||
<lion-fieldset name="inner1">
|
||||
<label slot="label">Fieldset no. 1</label>
|
||||
<lion-input
|
||||
label="Write 'cats' here"
|
||||
name="input1"
|
||||
>
|
||||
</lion-input>
|
||||
</lion-fieldset>
|
||||
<hr />
|
||||
<lion-fieldset name="inner2">
|
||||
<label slot="label">Fieldset no. 2</label>
|
||||
<lion-input
|
||||
label="Write 'dogs' here"
|
||||
name="input1"
|
||||
>
|
||||
</lion-input>
|
||||
</lion-fieldset>
|
||||
</lion-fieldset>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```js
|
||||
const IsCatsDogs = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'IsCatsDogs';
|
||||
}
|
||||
|
||||
execute(value) {
|
||||
if ((value.inner1 && value.inner1.input1 === 'cats') &&
|
||||
(value.inner2 && value.inner2.input1 === 'dogs')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static async getMessage() {
|
||||
return 'There is a problem with one of your fieldsets';
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-fieldset name="outer" .validators="${[new IsCatsDogs()]}">
|
||||
<lion-fieldset name="inner1">
|
||||
<label slot="label">Fieldset no. 1</label>
|
||||
<lion-input
|
||||
label="Write 'cats' here"
|
||||
name="input1"
|
||||
>
|
||||
</lion-input>
|
||||
</lion-fieldset>
|
||||
<hr />
|
||||
<lion-fieldset name="inner2">
|
||||
<label slot="label">Fieldset no. 2</label>
|
||||
<lion-input
|
||||
label="Write 'dogs' here"
|
||||
name="input1"
|
||||
>
|
||||
</lion-input>
|
||||
</lion-fieldset>
|
||||
</lion-fieldset>
|
||||
```
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { html } from '@open-wc/demoing-storybook';
|
||||
|
||||
export default {
|
||||
title: 'Forms/System/_internals',
|
||||
};
|
||||
|
||||
export const standardFlow = () => html`
|
||||
<img src=${new URL('../dev-assets/FormatMixinDiagram-1.svg', import.meta.url)} />
|
||||
`;
|
||||
|
||||
export const unparseableFlow = () => html`
|
||||
<img src=${new URL('../dev-assets/FormatMixinDiagram-2.svg', import.meta.url)} />
|
||||
`;
|
||||
|
||||
export const imperativeFlow = () => html`
|
||||
<img src=${new URL('../dev-assets/FormatMixinDiagram-3.svg', import.meta.url)} />
|
||||
`;
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
import { Story, Meta, html, Preview } from '@open-wc/demoing-storybook';
|
||||
import { Required, MaxLength, loadDefaultFeedbackMessages } from '@lion/validate';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
import '@lion/input/lion-input.js';
|
||||
import '@lion/form/lion-form.js';
|
||||
|
||||
<Meta title="Forms/Form/Examples" parameters={{ component: 'lion-form' }} />
|
||||
|
||||
# Form Examples
|
||||
|
||||
A form can have multiple nested fieldsets.
|
||||
|
||||
<Preview>
|
||||
<Story name="Default">
|
||||
{html`
|
||||
<lion-form id="form">
|
||||
<form>
|
||||
<lion-fieldset label="Personal data" name="personalData">
|
||||
<lion-fieldset label="Full Name" name="fullName">
|
||||
<lion-input name="firstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="lastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
</lion-fieldset>
|
||||
<lion-fieldset label="Location" name="location">
|
||||
<lion-input name="country" label="Country" .modelValue=${'Netherlands'}></lion-input>
|
||||
<lion-input name="city" label="City" .modelValue=${'Amsterdam'}></lion-input>
|
||||
</lion-fieldset>
|
||||
<lion-input name="birthdate" label="Birthdate" .modelValue=${'23-04-1991'}></lion-input>
|
||||
</lion-fieldset>
|
||||
<button type="button" @click=${() => console.log(document.querySelector('#form').serializedValue)}>
|
||||
Log to Action Logger
|
||||
</button>
|
||||
</form>
|
||||
</lion-form>
|
||||
`}
|
||||
</Story>
|
||||
</Preview>
|
||||
|
||||
## Form Submit / Reset
|
||||
|
||||
You can control whether a form gets submitted based on validation states.
|
||||
Same thing goes for resetting the inputs to the original state.
|
||||
|
||||
Be sure to call `serializedValue` when a you want to submit a form.
|
||||
This value automatically filters out disabled children and makes sure the values
|
||||
that are retrieved can be transmitted over a wire. (for instance, an input-date with a modelValue
|
||||
of type `Date` will be serialized to an iso formatted string).
|
||||
|
||||
|
||||
> Note: Offering a reset button is a bad practice in terms of accessibility.
|
||||
This button is only used here to demonstrate the `serializeGroup()` method.
|
||||
|
||||
<Preview>
|
||||
<Story name="Submit/reset">
|
||||
{() => {
|
||||
loadDefaultFeedbackMessages();
|
||||
const submit = () => {
|
||||
const form = document.querySelector('#form2');
|
||||
if (!form.hasFeedbackFor.includes('error')) {
|
||||
document.getElementById('form2_output').innerText = JSON.stringify(form.serializedValue);
|
||||
document.querySelector('#form2').resetGroup();
|
||||
}
|
||||
};
|
||||
return html`
|
||||
<lion-form id="form2" @submit="${submit}"
|
||||
><form>
|
||||
<lion-fieldset label="Name" name="name">
|
||||
<lion-input
|
||||
name="firstName"
|
||||
label="First Name"
|
||||
.validators=${[new Required(), new MaxLength(15)]}
|
||||
>
|
||||
</lion-input>
|
||||
<lion-input
|
||||
name="lastName"
|
||||
label="Last Name"
|
||||
.validators=${[new Required(), new MaxLength(15)]}
|
||||
>
|
||||
</lion-input>
|
||||
<lion-input
|
||||
name="address"
|
||||
disabled
|
||||
label="Address"
|
||||
.validators=${[new MaxLength(15)]}
|
||||
>
|
||||
</lion-input>
|
||||
</lion-fieldset>
|
||||
<button type="submit">Submit & Reset (if successfully submitted)</button>
|
||||
<button type="button" @click=${() => {
|
||||
document.querySelector('#form2').resetGroup();
|
||||
const form = document.querySelector('#form2');
|
||||
document.getElementById('form2_output').innerText = JSON.stringify(form.serializedValue);
|
||||
}}>
|
||||
reset
|
||||
</button>
|
||||
<pre id="form2_output">
|
||||
</pre>
|
||||
</form></lion-form
|
||||
>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
</Preview>
|
||||
|
||||
# clearGroup of form
|
||||
|
||||
To clear the values of the values of the fields use clearGroup().
|
||||
|
||||
<Preview>
|
||||
<Story name="clearGroup">
|
||||
{() => {
|
||||
return html`
|
||||
<lion-form id="form3"
|
||||
><form>
|
||||
<lion-fieldset label="Name" name="name">
|
||||
<lion-input
|
||||
name="firstName"
|
||||
label="First Name"
|
||||
.modelValue=${'Foo'}
|
||||
>
|
||||
</lion-input>
|
||||
<lion-input
|
||||
name="lastName"
|
||||
label="Last Name"
|
||||
>
|
||||
</lion-input>
|
||||
|
||||
</lion-fieldset>
|
||||
<button type="button" @click=${() => {
|
||||
document.querySelector('#form3').clearGroup();
|
||||
const form = document.querySelector('#form3');
|
||||
document.getElementById('form3_output').innerText = JSON.stringify(form.serializedValue);
|
||||
}}>
|
||||
Clear
|
||||
</button>
|
||||
<pre id="form3_output">
|
||||
</pre>
|
||||
</form></lion-form
|
||||
>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
</Preview>
|
||||
|
||||
|
||||
|
||||
|
||||
## Serialize in a multistep form
|
||||
|
||||
In a multistep form (consisting of multiple forms) it might be handy to wrap the serialized output
|
||||
with the name of the form.
|
||||
|
||||
<Preview>
|
||||
<Story name="Multistep">
|
||||
{() => {
|
||||
loadDefaultFeedbackMessages();
|
||||
const serializeWithName = (formId, outputId) => {
|
||||
const form = document.getElementById(formId);
|
||||
if (!form.hasFeedbackFor.includes('error')) {
|
||||
const output = { [form.name]: form.serializedValue };
|
||||
document.getElementById(outputId).innerText = JSON.stringify(output);
|
||||
}
|
||||
};
|
||||
return html`
|
||||
<lion-form name="step1FormName" id="form3"><form>
|
||||
<lion-input name="step1InputName" label="Step 1 Input"></lion-input>
|
||||
<button @click="${() => serializeWithName('form3', 'form3_output')}">
|
||||
serialize step 1 with name
|
||||
</button>
|
||||
<pre id="form3_output"></pre>
|
||||
</form></lion-form>
|
||||
<lion-form name="step2FormName" id="form4"><form>
|
||||
<lion-input name="step2InputName" label="Step 2 Input"></lion-input>
|
||||
<button @click="${() => serializeWithName('form4', 'form4_output')}">
|
||||
serialize step 2 with name
|
||||
</button>
|
||||
<pre id="form4_output"></pre>
|
||||
</form></lion-form>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
</Preview>
|
||||
|
|
@ -1,11 +1,45 @@
|
|||
# Form
|
||||
|
||||
`lion-form` is a webcomponent that enhances the functionality of the native `form` component.
|
||||
It is designed to interact with (instances of) the [form controls](?path=/docs/forms-system-overview--page).
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import '@lion/input/lion-input.js';
|
||||
import './lion-form.js';
|
||||
|
||||
export default {
|
||||
title: 'Forms/Form/Overview',
|
||||
};
|
||||
```
|
||||
|
||||
```js story
|
||||
export const main = () => html`
|
||||
<lion-form id="form">
|
||||
<form>
|
||||
<lion-input name="firstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="lastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
</form>
|
||||
</lion-form>
|
||||
`;
|
||||
```
|
||||
|
||||
## Live Demo/Documentation
|
||||
|
||||
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/forms-form-overview--page) for a live demo and documentation
|
||||
|
||||
## Features
|
||||
|
||||
- Data synchronization with models
|
||||
- Easy retrieval of form data based on field names
|
||||
- Advanced validation possibilities
|
||||
- Advanced user interaction scenarios via [interaction states](?path=/docs/forms-system-interaction-states--interaction-states)
|
||||
- Registration mechanism for [form controls](?path=/docs/forms-system-overview--page)
|
||||
- Accessible out of the box
|
||||
|
||||
For more information about fields that are designed for lion-form, please read
|
||||
[Forms](?path=/docs/forms-system-overview--page).
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
|
@ -29,3 +63,5 @@ import '@lion/form/lion-form.js';
|
|||
</form>
|
||||
</lion-form>
|
||||
```
|
||||
|
||||
For more examples please look at [Form Examples](?path=/docs/forms-form-examples--default-story).
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import { Story, Meta, html } from '@open-wc/demoing-storybook';
|
||||
|
||||
<Meta title="Forms/Form/Overview" />
|
||||
|
||||
# Form
|
||||
|
||||
`lion-form` is a webcomponent that enhances the functionality of the native `form` component.
|
||||
It is designed to interact with (instances of) the [form controls](?path=/docs/forms-system-overview--page).
|
||||
|
||||
```html
|
||||
<lion-form id="form">
|
||||
<form>
|
||||
<lion-input name="firstName" label="First Name" .modelValue=${'Foo'}></lion-input>
|
||||
<lion-input name="lastName" label="Last Name" .modelValue=${'Bar'}></lion-input>
|
||||
</form>
|
||||
</lion-form>
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Data synchronization with models
|
||||
- Easy retrieval of form data based on field names
|
||||
- Advanced validation possibilities
|
||||
- Advanced user interaction scenarios via [interaction states](?path=/docs/forms-system-interaction-states--interaction-states)
|
||||
- Registration mechanism for [form controls](?path=/docs/forms-system-overview--page)
|
||||
- Accessible out of the box
|
||||
|
||||
For more information about fields that are designed for lion-form, please read
|
||||
[Forms](?path=/docs/forms-system-overview--page).
|
||||
|
||||
## Examples
|
||||
|
||||
For examples please look at [Form Examples](?path=/docs/forms-form-examples--default-story).
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Helpers
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Helpers/Intro',
|
||||
};
|
||||
```
|
||||
|
||||
A helpers package that contains several helpers that are used inside lion but can be used outside as well.
|
||||
|
||||
These helpers are considered developer tools, not actual things to use in production.
|
||||
|
|
@ -9,6 +17,12 @@ Therefore, they may not have the same quality standards as our other packages.
|
|||
|
||||
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/helpers) for a live demo and API documentation
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Version | Description |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ---------------- |
|
||||
| [sb-action-logger](?path=/docs/helpers-storybook-action-logger--default-story) | [](https://www.npmjs.com/package/sb-action-logger) | sb-action-logger |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -1,6 +1,63 @@
|
|||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
# Storybook Action Logger
|
||||
|
||||
A visual element to show action logs in Storybook demos `sb-action-logger`.
|
||||
A visual element to show action logs in Storybook demos `sb-action-logger`
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
|
||||
import '../sb-action-logger.js';
|
||||
|
||||
export default {
|
||||
title: 'Helpers/Storybook Action Logger',
|
||||
parameters: {
|
||||
component: 'sb-action-logger',
|
||||
options: { selectedPanel: 'storybookjs/knobs/panel' },
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```js preview-story
|
||||
export const main = () => {
|
||||
const uid = Math.random().toString(36).substr(2, 10);
|
||||
return html`
|
||||
<div>To log: <code>Hello, World!</code></div>
|
||||
<button
|
||||
@click=${() => {
|
||||
document.getElementById(`logger-${uid}`).log('Hello, World!');
|
||||
}}
|
||||
>
|
||||
Click this button
|
||||
</button>
|
||||
<div>Or to log: <code>What's up, Planet!</code></div>
|
||||
<button
|
||||
@click=${() => {
|
||||
document.getElementById(`logger-${uid}`).log(`What's up, Planet!`);
|
||||
}}
|
||||
>
|
||||
Click this button
|
||||
</button>
|
||||
<sb-action-logger id="logger-${uid}"></sb-action-logger>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
You need some reference to your logger. Above example shows this by using a unique ID.
|
||||
|
||||
```js
|
||||
const uid = Math.random().toString(36).substr(2, 10);
|
||||
```
|
||||
|
||||
This connects the logger element to the trigger.
|
||||
|
||||
```html
|
||||
<div>To log: <code>Hello, World!</code></div>
|
||||
<button
|
||||
@click=${() => document.getElementById('logger-${uid}').log('Hello, World!')}
|
||||
>Click this button</button>
|
||||
<sb-action-logger id="logger-${uid}"></sb-action-logger>
|
||||
```
|
||||
|
||||
**This is a demonstrative tool**, not a debugging tool (although it may help initially).
|
||||
If you try logging complex values such as arrays, objects or promises,
|
||||
|
|
@ -10,24 +67,113 @@ you should expect to get only the string interpretation as the output in this lo
|
|||
|
||||
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/helpers-storybook-action-logger) for a live demo and API documentation
|
||||
|
||||
## Installation
|
||||
## Features:
|
||||
|
||||
- A public method `log` to log things to the action logger.
|
||||
- Overridable `title` property.
|
||||
- Clear button to clear logs
|
||||
- A counter to count the total amount of logs
|
||||
- Stacks consecutive duplicate logs and shows a counter
|
||||
- `simple` property/attribute to only show a single log
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm i @lion/helpers
|
||||
npm i sb-action-logger
|
||||
```
|
||||
|
||||
## Usage
|
||||
## API
|
||||
|
||||
## Variations
|
||||
|
||||
### Simple mode
|
||||
|
||||
Simple mode essentially means there is only ever 1 log.
|
||||
Duplicates are not counted or stacked, but you will still see the visual cue.
|
||||
|
||||
```js preview-story
|
||||
export const simpleMode = () => {
|
||||
const uid = Math.random().toString(36).substr(2, 10);
|
||||
return html`
|
||||
<div>To log: <code>Hello, World!</code></div>
|
||||
<button
|
||||
@click=${() => {
|
||||
document.getElementById(`logger-${uid}`).log('Hello, World!');
|
||||
}}
|
||||
>
|
||||
Click this button
|
||||
</button>
|
||||
<div>Or to log: <code>What's up, Planet!</code></div>
|
||||
<button
|
||||
@click=${() => {
|
||||
document.getElementById(`logger-${uid}`).log(`What's up, Planet!`);
|
||||
}}
|
||||
>
|
||||
Click this button
|
||||
</button>
|
||||
<sb-action-logger simple id="logger-${uid}"></sb-action-logger>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import '@lion/helpers/sb-action-logger/sb-action-logger.js';
|
||||
</script>
|
||||
|
||||
<sb-action-logger></sb-action-logger>
|
||||
<sb-action-logger simple></sb-action-logger>
|
||||
```
|
||||
|
||||
Then, with the sb-action-logger instance selected, call the `log` method on it.
|
||||
### Custom Title
|
||||
|
||||
```js
|
||||
myActionLoggerInstance.log('Hello, World!');
|
||||
You can customize the action logger title with the `.title` property.
|
||||
|
||||
```js preview-story
|
||||
export const customTitle = () => {
|
||||
const uid = Math.random().toString(36).substr(2, 10);
|
||||
return html`
|
||||
<button @click=${() => document.getElementById(`logger-${uid}`).log('Hello, World!')}>
|
||||
Log
|
||||
</button>
|
||||
<sb-action-logger id="logger-${uid}" .title=${'Hello World'}></sb-action-logger>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<sb-action-logger .title=${'Hello World'}></sb-action-logger>
|
||||
```
|
||||
|
||||
## Rationale
|
||||
|
||||
This component was created due to a need for a nice visual action logger in Storybook Docs mode.
|
||||
|
||||
In docs mode, you do not have access to the action logger addon component, and it is nice to be able to show actions inline in your demos and documentation.
|
||||
|
||||
### Opinionated
|
||||
|
||||
I added quite a bit of styling on this component to make it look decent.
|
||||
There are a bunch of styles that you can easily override to make it fit your design system which I believe should be enough in most cases.
|
||||
If you need more control you can always:
|
||||
|
||||
- Override the host styles, there's a few custom CSS props that you can override as well.
|
||||
- Extend the component and apply your overrides
|
||||
- Open an issue if you believe it would be good to make something more flexible / configurable
|
||||
|
||||
Maybe in the future I will abstract this component to a more generic (ugly) one with no styles, so it's more friendly as a shared component.
|
||||
|
||||
### Plugin
|
||||
|
||||
If you use an action logger inside your Story in Storybook, you will also see it in your canvas, and this may not be your intention.
|
||||
|
||||
One idea is to simplify the usage further by making this a Storybook (docs-)plugin or decorator or whatever.
|
||||
It would be cool if someone can simply enable an action logger option on a particular Story inside their .mdx,
|
||||
and then actions are automatically logged to the visual logger below it.
|
||||
Would need to figure out how to catch the action and pass it to the visual logger element.
|
||||
|
||||
Isn't investigated yet on the how, but that is the rough idea.
|
||||
|
||||
## Future
|
||||
|
||||
New planned features can be found in the test folder where they are specified as skipped tests.
|
||||
If the feature you'd like is not in the tests, feel free to make an issue so we can add it.
|
||||
|
||||
See our CONTRIBUTING.md for more guidelines on how to contribute.
|
||||
|
|
|
|||
|
|
@ -1,167 +0,0 @@
|
|||
import {
|
||||
Story,
|
||||
Preview,
|
||||
Meta,
|
||||
Props,
|
||||
html,
|
||||
} from '@open-wc/demoing-storybook';
|
||||
|
||||
import '../../sb-action-logger.js';
|
||||
|
||||
<Meta
|
||||
title="Helpers/Storybook Action Logger"
|
||||
parameters={{
|
||||
component: "sb-action-logger",
|
||||
options: { selectedPanel: "storybookjs/knobs/panel" }
|
||||
}}
|
||||
/>
|
||||
|
||||
# Storybook Action Logger
|
||||
|
||||
A visual element to show action logs in Storybook demos `sb-action-logger`
|
||||
|
||||
<Story name="Default">
|
||||
{() => {
|
||||
const uid = Math.random().toString(36).substr(2, 10);
|
||||
return html`
|
||||
<div>To log: <code>Hello, World!</code></div>
|
||||
<button
|
||||
@click=${() => {
|
||||
document.getElementById(`logger-${uid}`).log('Hello, World!');
|
||||
}}
|
||||
>Click this button</button>
|
||||
<div>Or to log: <code>What's up, Planet!</code></div>
|
||||
<button
|
||||
@click=${() => {
|
||||
document.getElementById(`logger-${uid}`).log(`What's up, Planet!`);
|
||||
}}
|
||||
>Click this button</button>
|
||||
<sb-action-logger id="logger-${uid}"></sb-action-logger>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
You need some reference to your logger. Above example shows this by using a unique ID.
|
||||
|
||||
```js
|
||||
const uid = Math.random().toString(36).substr(2, 10);
|
||||
```
|
||||
|
||||
This connects the logger element to the trigger.
|
||||
|
||||
```html
|
||||
<div>To log: <code>Hello, World!</code></div>
|
||||
<button
|
||||
@click=${() => document.getElementById('logger-${uid}').log('Hello, World!')}
|
||||
>Click this button</button>
|
||||
<sb-action-logger id="logger-${uid}"></sb-action-logger>
|
||||
```
|
||||
|
||||
## Features:
|
||||
|
||||
- A public method `log` to log things to the action logger.
|
||||
- Overridable `title` property.
|
||||
- Clear button to clear logs
|
||||
- A counter to count the total amount of logs
|
||||
- Stacks consecutive duplicate logs and shows a counter
|
||||
- `simple` property/attribute to only show a single log
|
||||
|
||||
## How to use
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm i sb-action-logger
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
<Props of="sb-action-logger" />
|
||||
|
||||
## Variations
|
||||
|
||||
### Simple mode
|
||||
|
||||
Simple mode essentially means there is only ever 1 log.
|
||||
Duplicates are not counted or stacked, but you will still see the visual cue.
|
||||
|
||||
<Story name="Simple mode">
|
||||
{() => {
|
||||
const uid = Math.random().toString(36).substr(2, 10);
|
||||
return html`
|
||||
<div>To log: <code>Hello, World!</code></div>
|
||||
<button
|
||||
@click=${() => {
|
||||
document.getElementById(`logger-${uid}`).log('Hello, World!');
|
||||
}}
|
||||
>Click this button</button>
|
||||
<div>Or to log: <code>What's up, Planet!</code></div>
|
||||
<button
|
||||
@click=${() => {
|
||||
document.getElementById(`logger-${uid}`).log(`What's up, Planet!`);
|
||||
}}
|
||||
>Click this button</button>
|
||||
<sb-action-logger simple id="logger-${uid}"></sb-action-logger>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<sb-action-logger simple></sb-action-logger>
|
||||
```
|
||||
|
||||
### Custom Title
|
||||
|
||||
You can customize the action logger title with the `.title` property.
|
||||
|
||||
<Story name="Custom Title">
|
||||
{() => {
|
||||
const uid = Math.random().toString(36).substr(2, 10);
|
||||
return html`
|
||||
<button
|
||||
@click=${() => document.getElementById(`logger-${uid}`).log('Hello, World!')}
|
||||
>Log</button>
|
||||
<sb-action-logger id="logger-${uid}" .title=${'Hello World'}></sb-action-logger>
|
||||
`;
|
||||
}}
|
||||
</Story>
|
||||
|
||||
```html
|
||||
<sb-action-logger .title=${'Hello World'}></sb-action-logger>
|
||||
```
|
||||
|
||||
## Rationale
|
||||
|
||||
This component was created due to a need for a nice visual action logger in Storybook Docs mode.
|
||||
|
||||
In docs mode, you do not have access to the action logger addon component, and it is nice to be able to show actions inline in your demos and documentation.
|
||||
|
||||
### Opinionated
|
||||
|
||||
I added quite a bit of styling on this component to make it look decent.
|
||||
There are a bunch of styles that you can easily override to make it fit your design system which I believe should be enough in most cases.
|
||||
If you need more control you can always:
|
||||
|
||||
- Override the host styles, there's a few custom CSS props that you can override as well.
|
||||
- Extend the component and apply your overrides
|
||||
- Open an issue if you believe it would be good to make something more flexible / configurable
|
||||
|
||||
Maybe in the future I will abstract this component to a more generic (ugly) one with no styles, so it's more friendly as a shared component.
|
||||
|
||||
### Plugin
|
||||
|
||||
If you use an action logger inside your Story in Storybook, you will also see it in your canvas, and this may not be your intention.
|
||||
|
||||
One idea is to simplify the usage further by making this a Storybook (docs-)plugin or decorator or whatever.
|
||||
It would be cool if someone can simply enable an action logger option on a particular Story inside their .mdx,
|
||||
and then actions are automatically logged to the visual logger below it.
|
||||
Would need to figure out how to catch the action and pass it to the visual logger element.
|
||||
|
||||
Isn't investigated yet on the how, but that is the rough idea.
|
||||
|
||||
## Future
|
||||
|
||||
New planned features can be found in the test folder where they are specified as skipped tests.
|
||||
If the feature you'd like is not in the tests, feel free to make an issue so we can add it.
|
||||
|
||||
See our CONTRIBUTING.md for more guidelines on how to contribute.
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { Meta } from '@open-wc/demoing-storybook';
|
||||
|
||||
<Meta title="Helpers/Intro" />
|
||||
|
||||
# Helpers
|
||||
|
||||
Features that aid in the development process of web components.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Version | Description |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
|
||||
| [sb-action-logger](?path=/docs/helpers-storybook-action-logger--default-story) | [](https://www.npmjs.com/package/sb-action-logger) | sb-action-logger |
|
||||
|
|
@ -2,6 +2,39 @@
|
|||
|
||||
A web component for displaying icons.
|
||||
|
||||
```js script
|
||||
import { html } from 'lit-html';
|
||||
import { icons } from './index.js';
|
||||
import './docs/icons/iconset-bugs.js';
|
||||
import './docs/icons/iconset-misc.js';
|
||||
import * as spaceSet from './docs/icons/iconset-space.js';
|
||||
|
||||
import './lion-icon.js';
|
||||
|
||||
export default {
|
||||
title: 'Icons/Icon',
|
||||
};
|
||||
|
||||
icons.addIconResolver('lion', (iconset, name) => {
|
||||
switch (iconset) {
|
||||
case 'bugs':
|
||||
return import('./docs/icons/iconset-bugs.js').then(module => module[name]);
|
||||
case 'space':
|
||||
return import('./docs/icons/iconset-space.js').then(module => module[name]);
|
||||
case 'misc':
|
||||
return import('./docs/icons/iconset-misc.js').then(module => module[name]);
|
||||
default:
|
||||
throw new Error(`Unknown iconset ${iconset}`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```js preview-story
|
||||
export const main = () => html`
|
||||
<lion-icon icon-id="lion:space:alienSpaceship" style="width: 50px; height: 50px;"></lion-icon>
|
||||
`;
|
||||
```
|
||||
|
||||
## Live Demo/Documentation
|
||||
|
||||
> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/icon-system-icon) for a live demo and documentation
|
||||
|
|
@ -15,15 +48,76 @@ npm i --save @lion/icon
|
|||
```
|
||||
|
||||
```js
|
||||
import { LionIcon } from '@lion/icon';
|
||||
// or
|
||||
import '@lion/icon/lion-icon.js';
|
||||
```
|
||||
|
||||
### Example
|
||||
### Icon sets
|
||||
|
||||
```js
|
||||
import './somewhere/my-iconset.js';
|
||||
Icons are displayed using icon sets. These are collections of icons, lazily loaded on demand for performance.
|
||||
See the [system documentation](?path=/docs/icons-system--page) to learn more about icon sets.
|
||||
|
||||
```js preview-story
|
||||
export const iconSets = () => html`
|
||||
${Object.keys(spaceSet).map(
|
||||
name => html`
|
||||
<style>
|
||||
.demo-icon__container {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
padding: 4px;
|
||||
}
|
||||
.demo-icon__name {
|
||||
font-size: 10px;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-icon__container" title="lion:space:${name}">
|
||||
<lion-icon icon-id="lion:space:${name}" aria-label="icon: ${name}"></lion-icon>
|
||||
<span class="demo-icon__name">${name}</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
`;
|
||||
```
|
||||
|
||||
```html
|
||||
<lion-icon icon-id="somewhere:myIconset:foo"></lion-icon>
|
||||
If for some reason you don't want to lazy load icons, you can still import and use them
|
||||
synchronously.
|
||||
|
||||
### Accessibility
|
||||
|
||||
It is recommended to add an `aria-label` to provide information to visually impaired users:
|
||||
|
||||
A `lion-icon` without an `aria-label` attribute will be automatically given an `aria-hidden` attribute.
|
||||
|
||||
```js preview-story
|
||||
export const accessibleLabel = () => html`
|
||||
<lion-icon icon-id="lion:misc:arrowLeft" aria-label="Pointing left"></lion-icon>
|
||||
`;
|
||||
```
|
||||
|
||||
### Styling
|
||||
|
||||
By default, a `lion-icon` will be `1em` × `1em` (the current line-height).
|
||||
|
||||
`lion-icon` uses SVGs and may be styled with CSS, including using CSS properties such as `fill`:
|
||||
|
||||
```js preview-story
|
||||
export const Styling = () => html`
|
||||
<style>
|
||||
.demo-icon {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
fill: blue;
|
||||
}
|
||||
</style>
|
||||
<lion-icon icon-id="lion:bugs:bug02" aria-label="Bug" class="demo-icon"></lion-icon>
|
||||
`;
|
||||
```
|
||||
|
||||
See <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/SVG_and_CSS" target="_blank">SVG and CSS</a> on MDN web docs for more information.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue