fix(scoped-elements): elements not scoped by directives

This commit is contained in:
Manuel Martin
2020-03-28 01:22:21 +01:00
committed by Lars den Bakker
parent 94f832967f
commit 71f7438308
4 changed files with 158 additions and 14 deletions

View File

@@ -39,12 +39,13 @@
],
"dependencies": {
"@open-wc/dedupe-mixin": "^1.2.13",
"lit-element": "^2.2.1"
"lit-html": "^1.0.0"
},
"devDependencies": {
"@open-wc/building-rollup": "^0.22.11",
"@open-wc/testing": "^2.5.8",
"es-dev-server": "^1.46.0",
"lit-element": "^2.2.1",
"npm-run-all": "4.1.3",
"rollup": "^1.31.1"
},

View File

@@ -1,8 +1,9 @@
/* eslint-disable no-use-before-define */
import { TemplateResult } from 'lit-element';
import { TemplateResult } from 'lit-html';
import { dedupeMixin } from '@open-wc/dedupe-mixin';
import { transform } from './transform.js';
import { defineScopedElement, registerElement } from './registerElement.js';
import { shadyTemplateFactory } from './shadyTemplateFactory.js';
/**
* @typedef {import('lit-html/lib/shady-render').ShadyRenderOptions} ShadyRenderOptions
@@ -28,18 +29,18 @@ const tagsCaches = new WeakMap();
*
* @param {ReadonlyArray} items
* @param {Object.<string, typeof HTMLElement>} scopedElements
* @param {Map<TemplateStringsArray, TemplateStringsArray>} cache
* @param {Map<TemplateStringsArray, TemplateStringsArray>} templateCache
* @param {Map<string, string>} tagsCache
* @returns {ReadonlyArray}
*/
const transformArray = (items, scopedElements, cache, tagsCache) =>
const transformArray = (items, scopedElements, templateCache, tagsCache) =>
items.map(value => {
if (value instanceof TemplateResult) {
return transformTemplate(value, scopedElements, cache, tagsCache);
return transformTemplate(value, scopedElements, templateCache, tagsCache);
}
if (Array.isArray(value)) {
return transformArray(value, scopedElements, cache, tagsCache);
return transformArray(value, scopedElements, templateCache, tagsCache);
}
return value;
@@ -62,6 +63,17 @@ const transformTemplate = (template, scopedElements, templateCache, tagsCache) =
template.processor,
);
const scopedElementsTemplateFactory = (
scopeName,
scopedElements,
templateCache,
tagsCache,
) => template => {
const newTemplate = transformTemplate(template, scopedElements, templateCache, tagsCache);
return shadyTemplateFactory(scopeName)(newTemplate);
};
export const ScopedElementsMixin = dedupeMixin(
superclass =>
// eslint-disable-next-line no-shadow
@@ -74,6 +86,11 @@ export const ScopedElementsMixin = dedupeMixin(
* @override
*/
static render(template, container, options) {
if (!options || typeof options !== 'object' || !options.scopeName) {
throw new Error('The `scopeName` option is required.');
}
const { scopeName } = options;
if (!templateCaches.has(this)) {
templateCaches.set(this, new Map());
}
@@ -84,15 +101,17 @@ export const ScopedElementsMixin = dedupeMixin(
const templateCache = templateCaches.get(this);
const tagsCache = tagsCaches.get(this);
const { scopedElements } = this;
const transformedTemplate = transformTemplate(
template,
scopedElements,
templateCache,
tagsCache,
);
// @ts-ignore
return super.render(transformedTemplate, container, options);
return super.render(template, container, {
...options,
templateFactory: scopedElementsTemplateFactory(
scopeName,
scopedElements,
templateCache,
tagsCache,
),
});
}
/**

View File

@@ -0,0 +1,47 @@
import { templateCaches } from 'lit-html/lib/template-factory.js';
import { marker, Template } from 'lit-html/lib/template.js';
const getTemplateCacheKey = (type, scopeName) => `${type}--${scopeName}`;
let compatibleShadyCSSVersion = true;
// @ts-ignore
const { ShadyCSS } = window;
if (typeof ShadyCSS === 'undefined') {
compatibleShadyCSSVersion = false;
} else if (typeof ShadyCSS.prepareTemplateDom === 'undefined') {
compatibleShadyCSSVersion = false;
}
/**
* Template factory which scopes template DOM using ShadyCSS.
* @param scopeName {string}
*/
export const shadyTemplateFactory = scopeName => result => {
const cacheKey = getTemplateCacheKey(result.type, scopeName);
let templateCache = templateCaches.get(cacheKey);
if (templateCache === undefined) {
templateCache = {
stringsArray: new WeakMap(),
keyString: new Map(),
};
templateCaches.set(cacheKey, templateCache);
}
let template = templateCache.stringsArray.get(result.strings);
if (template !== undefined) {
return template;
}
const key = result.strings.join(marker);
template = templateCache.keyString.get(key);
if (template === undefined) {
const element = result.getTemplateElement();
if (compatibleShadyCSSVersion) {
ShadyCSS.prepareTemplateDom(element, scopeName);
}
template = new Template(result, element);
templateCache.keyString.set(key, template);
}
templateCache.stringsArray.set(result.strings, template);
return template;
};

View File

@@ -1,5 +1,7 @@
import { expect, fixture, defineCE } from '@open-wc/testing';
import { expect, fixture, defineCE, waitUntil } from '@open-wc/testing';
import { LitElement, html } from 'lit-element';
import { until } from 'lit-html/directives/until.js';
import { repeat } from 'lit-html/directives/repeat';
import { ScopedElementsMixin } from '../index.js';
import { getFromGlobalTagsCache } from '../src/globalTagsCache.js';
@@ -350,4 +352,79 @@ describe('ScopedElementsMixin', () => {
expect(el.constructor.getScopedTagName('unregistered-feature')).to.match(tagRegExp);
});
});
describe('directives integration', () => {
it('should work with until(...)', async () => {
const content = new Promise(resolve => {
setTimeout(
() =>
resolve(
html`
<feature-a id="feat"></feature-a>
`,
),
0,
);
});
const tag = defineCE(
class ContainerElement extends ScopedElementsMixin(LitElement) {
static get scopedElements() {
return {
'feature-a': FeatureA,
};
}
render() {
return html`
${until(
content,
html`
<span>Loading...</span>
`,
)}
`;
}
},
);
const el = await fixture(`<${tag}></${tag}>`);
expect(el.shadowRoot.getElementById('feat')).to.be.null;
await waitUntil(() => el.shadowRoot.getElementById('feat') !== null);
const feature = el.shadowRoot.getElementById('feat');
expect(feature).shadowDom.to.equal('<div>Element A</div>');
});
it('should work with repeat(...)', async () => {
const tag = defineCE(
class ContainerElement extends ScopedElementsMixin(LitElement) {
static get scopedElements() {
return {
'feature-a': FeatureA,
};
}
render() {
return html`
${repeat(
[...Array(10).keys()],
() => html`
<feature-a data-type="child"></feature-a>
`,
)}
`;
}
},
);
const el = await fixture(`<${tag}></${tag}>`);
el.shadowRoot.querySelectorAll('[data-type="child"]').forEach(child => {
expect(child).shadowDom.to.equal('<div>Element A</div>');
});
});
});
});