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.
|
||||
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
|
||||
|
||||
Feel free to create a github issue for any feedback or questions you might have.
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
"bundlesize": [
|
||||
{
|
||||
"path": "./bundlesize/dist/core/core-*.js",
|
||||
"maxSize": "10 kB"
|
||||
"maxSize": "11 kB"
|
||||
},
|
||||
{
|
||||
"path": "./bundlesize/dist/all/all-*.js",
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@
|
|||
"@lion/input-date": "0.5.17",
|
||||
"@lion/localize": "0.8.9",
|
||||
"@lion/overlays": "0.12.3",
|
||||
"@lion/validate": "0.7.1"
|
||||
"@lion/validate": "0.7.1",
|
||||
"@open-wc/scoped-elements": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@open-wc/demoing-storybook": "^1.10.4",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
import '@lion/calendar/lion-calendar.js';
|
||||
import { html, ifDefined, render } from '@lion/core';
|
||||
import { LionCalendar } from '@lion/calendar/src/LionCalendar';
|
||||
import { html, ifDefined, ScopedElementsMixin } from '@lion/core';
|
||||
import { LionInputDate } from '@lion/input-date';
|
||||
import { OverlayMixin, withModalDialogConfig } from '@lion/overlays';
|
||||
import './lion-calendar-overlay-frame.js';
|
||||
import { LionCalendarOverlayFrame } from './LionCalendarOverlayFrame.js';
|
||||
|
||||
/**
|
||||
* @customElement lion-input-datepicker
|
||||
* @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() {
|
||||
return {
|
||||
/**
|
||||
|
|
@ -47,7 +55,10 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
|
|||
...super.slots,
|
||||
[this._calendarInvokerSlot]: () => {
|
||||
const renderParent = document.createElement('div');
|
||||
render(this._invokerTemplate(), renderParent);
|
||||
this.constructor.render(this._invokerTemplate(), renderParent, {
|
||||
scopeName: this.localName,
|
||||
eventContext: this,
|
||||
});
|
||||
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.
|
||||
// This would make first paint quicker
|
||||
return html`
|
||||
<lion-calendar-overlay-frame>
|
||||
<lion-calendar-overlay-frame class="calendar__overlay-frame">
|
||||
<span slot="heading">${this.calendarHeading}</span>
|
||||
${this._calendarTemplate()}
|
||||
</lion-calendar-overlay-frame>
|
||||
|
|
@ -349,7 +360,7 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
|
|||
if (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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
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 { formRegistrarManager } from '@lion/field/src/registration/formRegistrarManager.js';
|
||||
import { OverlayMixin, withDropdownConfig } from '@lion/overlays';
|
||||
import { ValidateMixin } from '@lion/validate';
|
||||
import '../lion-select-invoker.js';
|
||||
import './differentKeyNamesShimIE.js';
|
||||
|
||||
import { LionSelectInvoker } from './LionSelectInvoker.js';
|
||||
|
||||
function uuid() {
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
|
|
@ -46,13 +54,22 @@ function isInView(container, element, partial = false) {
|
|||
* @customElement lion-select-rich
|
||||
* @extends {LitElement}
|
||||
*/
|
||||
export class LionSelectRich extends ChoiceGroupMixin(
|
||||
OverlayMixin(
|
||||
FormRegistrarMixin(
|
||||
InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement)))),
|
||||
export class LionSelectRich extends ScopedElementsMixin(
|
||||
ChoiceGroupMixin(
|
||||
OverlayMixin(
|
||||
FormRegistrarMixin(
|
||||
InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement)))),
|
||||
),
|
||||
),
|
||||
),
|
||||
) {
|
||||
static get scopedElements() {
|
||||
return {
|
||||
...super.scopedElements,
|
||||
'lion-select-invoker': LionSelectInvoker,
|
||||
};
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
disabled: {
|
||||
|
|
@ -94,7 +111,10 @@ export class LionSelectRich extends ChoiceGroupMixin(
|
|||
get slots() {
|
||||
return {
|
||||
...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.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 () => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { html, css } from '@lion/core';
|
||||
import { html, css, ScopedElementsMixin, getScopedTagName } from '@lion/core';
|
||||
import { LionField } from '@lion/field';
|
||||
import { ChoiceInputMixin } from '@lion/choice-input';
|
||||
import { LionSwitchButton } from './LionSwitchButton.js';
|
||||
|
||||
import '../lion-switch-button.js';
|
||||
|
||||
export class LionSwitch extends ChoiceInputMixin(LionField) {
|
||||
export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField)) {
|
||||
static get styles() {
|
||||
return [
|
||||
super.styles,
|
||||
|
|
@ -16,10 +15,20 @@ export class LionSwitch extends ChoiceInputMixin(LionField) {
|
|||
];
|
||||
}
|
||||
|
||||
static get scopedElements() {
|
||||
return {
|
||||
...super.scopedElements,
|
||||
'lion-switch-button': LionSwitchButton,
|
||||
};
|
||||
}
|
||||
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
input: () => document.createElement('lion-switch-button'),
|
||||
input: () =>
|
||||
document.createElement(
|
||||
getScopedTagName('lion-switch-button', this.constructor.scopedElements),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@lion/core": "0.4.4",
|
||||
"@lion/localize": "0.8.9"
|
||||
"@lion/localize": "0.8.9",
|
||||
"@open-wc/scoped-elements": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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 */
|
||||
|
||||
import { dedupeMixin, SlotMixin } from '@lion/core';
|
||||
import { dedupeMixin, SlotMixin, ScopedElementsMixin, getScopedTagName } from '@lion/core';
|
||||
import { localize } from '@lion/localize';
|
||||
import { LionValidationFeedback } from './LionValidationFeedback.js';
|
||||
import { ResultValidator } from './ResultValidator.js';
|
||||
import { Unparseable } from './Unparseable.js';
|
||||
import { AsyncQueue } from './utils/AsyncQueue.js';
|
||||
|
|
@ -24,7 +25,14 @@ function arrayDiff(array1 = [], array2 = []) {
|
|||
export const ValidateMixin = dedupeMixin(
|
||||
superclass =>
|
||||
// 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() {
|
||||
return {
|
||||
/**
|
||||
|
|
@ -108,7 +116,10 @@ export const ValidateMixin = dedupeMixin(
|
|||
get slots() {
|
||||
return {
|
||||
...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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be overridden by subclasses if a different validation-feedback component is used
|
||||
*/
|
||||
async _loadFeedbackComponent() {
|
||||
await import('../lion-validation-feedback.js');
|
||||
}
|
||||
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.__validateInitialized = true;
|
||||
this.validate();
|
||||
this._loadFeedbackComponent();
|
||||
}
|
||||
|
||||
updateSync(name, oldValue) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue