feat: use Scoped Elements
Co-authored-by: Alex Ghiu <Alex.Ghiu@ing.com> Co-authored-by: Thijs Louisse <Thijs.Louisse@ing.com>
This commit is contained in:
parent
d1beffbff5
commit
15b4a5ebb3
10 changed files with 558 additions and 552 deletions
94
README.md
94
README.md
|
|
@ -135,6 +135,100 @@ Check out our [coding guidelines](./docs/README.md) for more detailed informatio
|
||||||
Lion Web Components are only as good as its contributions.
|
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.
|
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(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 `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, 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).
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
Feel free to create a github issue for any feedback or questions you might have.
|
Feel free to create a github issue for any feedback or questions you might have.
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
"bundlesize": [
|
"bundlesize": [
|
||||||
{
|
{
|
||||||
"path": "./bundlesize/dist/core/core-*.js",
|
"path": "./bundlesize/dist/core/core-*.js",
|
||||||
"maxSize": "10 kB"
|
"maxSize": "11 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./bundlesize/dist/all/all-*.js",
|
"path": "./bundlesize/dist/all/all-*.js",
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@
|
||||||
"@lion/input-date": "0.5.17",
|
"@lion/input-date": "0.5.17",
|
||||||
"@lion/localize": "0.8.9",
|
"@lion/localize": "0.8.9",
|
||||||
"@lion/overlays": "0.12.3",
|
"@lion/overlays": "0.12.3",
|
||||||
"@lion/validate": "0.7.1"
|
"@lion/validate": "0.7.1",
|
||||||
|
"@open-wc/scoped-elements": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@open-wc/demoing-storybook": "^1.10.4",
|
"@open-wc/demoing-storybook": "^1.10.4",
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,22 @@
|
||||||
import '@lion/calendar/lion-calendar.js';
|
import { LionCalendar } from '@lion/calendar/src/LionCalendar';
|
||||||
import { html, ifDefined, render } from '@lion/core';
|
import { html, ifDefined, ScopedElementsMixin } from '@lion/core';
|
||||||
import { LionInputDate } from '@lion/input-date';
|
import { LionInputDate } from '@lion/input-date';
|
||||||
import { OverlayMixin, withModalDialogConfig } from '@lion/overlays';
|
import { OverlayMixin, withModalDialogConfig } from '@lion/overlays';
|
||||||
import './lion-calendar-overlay-frame.js';
|
import { LionCalendarOverlayFrame } from './LionCalendarOverlayFrame.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @customElement lion-input-datepicker
|
* @customElement lion-input-datepicker
|
||||||
* @extends {LionInputDate}
|
* @extends {LionInputDate}
|
||||||
*/
|
*/
|
||||||
export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
|
export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionInputDate)) {
|
||||||
|
static get scopedElements() {
|
||||||
|
return {
|
||||||
|
...super.scopedElements,
|
||||||
|
'lion-calendar': LionCalendar,
|
||||||
|
'lion-calendar-overlay-frame': LionCalendarOverlayFrame,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
|
|
@ -47,7 +55,10 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
[this._calendarInvokerSlot]: () => {
|
[this._calendarInvokerSlot]: () => {
|
||||||
const renderParent = document.createElement('div');
|
const renderParent = document.createElement('div');
|
||||||
render(this._invokerTemplate(), renderParent);
|
this.constructor.render(this._invokerTemplate(), renderParent, {
|
||||||
|
scopeName: this.localName,
|
||||||
|
eventContext: this,
|
||||||
|
});
|
||||||
return renderParent.firstElementChild;
|
return renderParent.firstElementChild;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -207,7 +218,7 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
|
||||||
// When not opened (usually on init), it does not need to be rendered.
|
// When not opened (usually on init), it does not need to be rendered.
|
||||||
// This would make first paint quicker
|
// This would make first paint quicker
|
||||||
return html`
|
return html`
|
||||||
<lion-calendar-overlay-frame>
|
<lion-calendar-overlay-frame class="calendar__overlay-frame">
|
||||||
<span slot="heading">${this.calendarHeading}</span>
|
<span slot="heading">${this.calendarHeading}</span>
|
||||||
${this._calendarTemplate()}
|
${this._calendarTemplate()}
|
||||||
</lion-calendar-overlay-frame>
|
</lion-calendar-overlay-frame>
|
||||||
|
|
@ -349,7 +360,7 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
|
||||||
if (this._cachedOverlayContentNode) {
|
if (this._cachedOverlayContentNode) {
|
||||||
return this._cachedOverlayContentNode;
|
return this._cachedOverlayContentNode;
|
||||||
}
|
}
|
||||||
this._cachedOverlayContentNode = this.shadowRoot.querySelector('lion-calendar-overlay-frame');
|
this._cachedOverlayContentNode = this.shadowRoot.querySelector('.calendar__overlay-frame');
|
||||||
return this._cachedOverlayContentNode;
|
return this._cachedOverlayContentNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
import { ChoiceGroupMixin } from '@lion/choice-input';
|
import { ChoiceGroupMixin } from '@lion/choice-input';
|
||||||
import { css, html, LitElement, SlotMixin } from '@lion/core';
|
import {
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
SlotMixin,
|
||||||
|
ScopedElementsMixin,
|
||||||
|
getScopedTagName,
|
||||||
|
} from '@lion/core';
|
||||||
import { FormControlMixin, FormRegistrarMixin, InteractionStateMixin } from '@lion/field';
|
import { FormControlMixin, FormRegistrarMixin, InteractionStateMixin } from '@lion/field';
|
||||||
import { formRegistrarManager } from '@lion/field/src/registration/formRegistrarManager.js';
|
import { formRegistrarManager } from '@lion/field/src/registration/formRegistrarManager.js';
|
||||||
import { OverlayMixin, withDropdownConfig } from '@lion/overlays';
|
import { OverlayMixin, withDropdownConfig } from '@lion/overlays';
|
||||||
import { ValidateMixin } from '@lion/validate';
|
import { ValidateMixin } from '@lion/validate';
|
||||||
import '../lion-select-invoker.js';
|
|
||||||
import './differentKeyNamesShimIE.js';
|
import './differentKeyNamesShimIE.js';
|
||||||
|
|
||||||
|
import { LionSelectInvoker } from './LionSelectInvoker.js';
|
||||||
|
|
||||||
function uuid() {
|
function uuid() {
|
||||||
return Math.random()
|
return Math.random()
|
||||||
.toString(36)
|
.toString(36)
|
||||||
|
|
@ -46,13 +54,22 @@ function isInView(container, element, partial = false) {
|
||||||
* @customElement lion-select-rich
|
* @customElement lion-select-rich
|
||||||
* @extends {LitElement}
|
* @extends {LitElement}
|
||||||
*/
|
*/
|
||||||
export class LionSelectRich extends ChoiceGroupMixin(
|
export class LionSelectRich extends ScopedElementsMixin(
|
||||||
|
ChoiceGroupMixin(
|
||||||
OverlayMixin(
|
OverlayMixin(
|
||||||
FormRegistrarMixin(
|
FormRegistrarMixin(
|
||||||
InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement)))),
|
InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement)))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
|
static get scopedElements() {
|
||||||
|
return {
|
||||||
|
...super.scopedElements,
|
||||||
|
'lion-select-invoker': LionSelectInvoker,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
disabled: {
|
disabled: {
|
||||||
|
|
@ -94,7 +111,10 @@ export class LionSelectRich extends ChoiceGroupMixin(
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
invoker: () => document.createElement('lion-select-invoker'),
|
invoker: () =>
|
||||||
|
document.createElement(
|
||||||
|
getScopedTagName('lion-select-invoker', this.constructor.scopedElements),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,7 @@ describe('lion-select-rich', () => {
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(el._invokerNode).to.exist;
|
expect(el._invokerNode).to.exist;
|
||||||
expect(el._invokerNode.tagName).to.equal('LION-SELECT-INVOKER');
|
expect(el._invokerNode.tagName).to.include('LION-SELECT-INVOKER');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the first option as the selectedElement if no option is checked', async () => {
|
it('sets the first option as the selectedElement if no option is checked', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { html, css } from '@lion/core';
|
import { html, css, ScopedElementsMixin, getScopedTagName } from '@lion/core';
|
||||||
import { LionField } from '@lion/field';
|
import { LionField } from '@lion/field';
|
||||||
import { ChoiceInputMixin } from '@lion/choice-input';
|
import { ChoiceInputMixin } from '@lion/choice-input';
|
||||||
|
import { LionSwitchButton } from './LionSwitchButton.js';
|
||||||
|
|
||||||
import '../lion-switch-button.js';
|
export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField)) {
|
||||||
|
|
||||||
export class LionSwitch extends ChoiceInputMixin(LionField) {
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
super.styles,
|
super.styles,
|
||||||
|
|
@ -16,10 +15,20 @@ export class LionSwitch extends ChoiceInputMixin(LionField) {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get scopedElements() {
|
||||||
|
return {
|
||||||
|
...super.scopedElements,
|
||||||
|
'lion-switch-button': LionSwitchButton,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
input: () => document.createElement('lion-switch-button'),
|
input: () =>
|
||||||
|
document.createElement(
|
||||||
|
getScopedTagName('lion-switch-button', this.constructor.scopedElements),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,8 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lion/core": "0.4.4",
|
"@lion/core": "0.4.4",
|
||||||
"@lion/localize": "0.8.9"
|
"@lion/localize": "0.8.9",
|
||||||
|
"@open-wc/scoped-elements": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@open-wc/demoing-storybook": "^1.10.4",
|
"@open-wc/demoing-storybook": "^1.10.4",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
/* eslint-disable class-methods-use-this, camelcase, no-param-reassign, max-classes-per-file */
|
/* eslint-disable class-methods-use-this, camelcase, no-param-reassign, max-classes-per-file */
|
||||||
|
|
||||||
import { dedupeMixin, SlotMixin } from '@lion/core';
|
import { dedupeMixin, SlotMixin, ScopedElementsMixin, getScopedTagName } from '@lion/core';
|
||||||
import { localize } from '@lion/localize';
|
import { localize } from '@lion/localize';
|
||||||
|
import { LionValidationFeedback } from './LionValidationFeedback.js';
|
||||||
import { ResultValidator } from './ResultValidator.js';
|
import { ResultValidator } from './ResultValidator.js';
|
||||||
import { Unparseable } from './Unparseable.js';
|
import { Unparseable } from './Unparseable.js';
|
||||||
import { AsyncQueue } from './utils/AsyncQueue.js';
|
import { AsyncQueue } from './utils/AsyncQueue.js';
|
||||||
|
|
@ -24,7 +25,14 @@ function arrayDiff(array1 = [], array2 = []) {
|
||||||
export const ValidateMixin = dedupeMixin(
|
export const ValidateMixin = dedupeMixin(
|
||||||
superclass =>
|
superclass =>
|
||||||
// eslint-disable-next-line no-unused-vars, no-shadow
|
// eslint-disable-next-line no-unused-vars, no-shadow
|
||||||
class ValidateMixin extends SyncUpdatableMixin(SlotMixin(superclass)) {
|
class ValidateMixin extends ScopedElementsMixin(SyncUpdatableMixin(SlotMixin(superclass))) {
|
||||||
|
static get scopedElements() {
|
||||||
|
return {
|
||||||
|
...super.scopedElements,
|
||||||
|
'lion-validation-feedback': LionValidationFeedback,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
|
|
@ -108,7 +116,10 @@ export const ValidateMixin = dedupeMixin(
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
feedback: () => document.createElement('lion-validation-feedback'),
|
feedback: () =>
|
||||||
|
document.createElement(
|
||||||
|
getScopedTagName('lion-validation-feedback', this.constructor.scopedElements),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,18 +192,10 @@ export const ValidateMixin = dedupeMixin(
|
||||||
localize.addEventListener('localeChanged', this._updateFeedbackComponent);
|
localize.addEventListener('localeChanged', this._updateFeedbackComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Should be overridden by subclasses if a different validation-feedback component is used
|
|
||||||
*/
|
|
||||||
async _loadFeedbackComponent() {
|
|
||||||
await import('../lion-validation-feedback.js');
|
|
||||||
}
|
|
||||||
|
|
||||||
firstUpdated(changedProperties) {
|
firstUpdated(changedProperties) {
|
||||||
super.firstUpdated(changedProperties);
|
super.firstUpdated(changedProperties);
|
||||||
this.__validateInitialized = true;
|
this.__validateInitialized = true;
|
||||||
this.validate();
|
this.validate();
|
||||||
this._loadFeedbackComponent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSync(name, oldValue) {
|
updateSync(name, oldValue) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue