Using slots in WebComponents without using shadow DOM - javascript

I'm trying to build a WebComponent without using ShadowDOM - so far it mostly just worked, but now I want to build a component that wraps other components like you would do with Angular's #ViewChild / #ViewChildren. (the library I'm using here to render is uhtml similar to lit-html)
export class Dropdown extends HTMLElement {
private open: boolean = false;
static observedAttributes = ["open"]
constructor() {
super();
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
switch (name) {
case "open":
this.open = Boolean(newValue);
break;
}
this.display()
}
connectedCallback() {
this.display()
}
display = () => {
render(this, html`
<div>
<slot name="item">
</slot>
</div>
`)
}
static register = () => customElements.define("my-dropdown", Dropdown)
}
If I now use this component
Dropdown.register();
render(document.body, html`
<my-dropdown open="true">
<strong slot="item">test</strong>
</my-dropdown>
`)
I'm expecting to see
<my-dropdown>
<div>
<strong>test</test>
</div>
</my-dropdown>
but the slot part is not working.
If I switch to ShadowDOM, it just works, but now I have to deal with ShadowDOM's sandbox with regards to styling which I'd rather not do.
constructor() {
super();
this.attachShadow({mode: "open"})
}
display = () => {
render(this.shadowRoot as Node, html`
Is it possible to make slots work without shadowDOM?
If not, is there different way to grab the content defined inside the component and use it inside display?
<my-component>
<div>some content here</div>
</my-component>
should render as
<my-component>
<header>
random header
</header>
<section>
<!-- my custom content -->
<div>some content here</div>
</section>
</my-component>
Any suggestions?

No, <slot> are part of the shadowDOM API
You can fake it, but since there is no shadowDOM you would have to store that content someplace else.
Could be a <template> you read and parse your (light)DOM content into.
That is a sh*load of DOM mutations.
Might be easier to just learn to style shadowDOM with:
CSS properties
inheritable styles
::part
constructable stylesheets

Related

Problem with using template on customElement together with <slot>

I'm trying to learn more on how to work with customElements and looking in to using together with it. When I'm trying to use with and surrounding the by another element the DOM seems to no look as expected, the shadowRoot is not getting the content for ?
I would love to know what I'm doing wrong and what is going on here? Hope the question makes sense!
Best regards
The customElement
customElements.define(
'test-element',
class extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow( {mode: 'open'} );
}
connectedCallback() {
this.shadow.appendChild(this._template().cloneNode(true));
}
_template() {
let template = document.createElement('template');
template.innerHTML = `
<div class="surrounding-element">
<slot name="content"></slot>
</div>
`;
return template.content;
}
}
)
In the rendered DOM
<test-element>
#shadow-root
<div class="surrounding-element">
<slot name="content"></slot>
</div>
<div slot="content" class="non-template">
<p>Some kind of content!</p>
</div>
</test-element>
I have been trying to using , and customElements together to have more control of where the content is put, and I would like to be able to wrap the content given to .

Can a text node be slotted?

Is it possible to assign a text node to a slot when using <template> and <slot>s?
Say I have a template that looks like
<template>
<span>
<slot name="mySlot"></slot>
</span>
</template>
I would like to be able to add only text to the slot, instead of having to add a <span> open and close tag each time I use the template. Is this possible? If not in pure HTML, in JavaScript?
It is also better to pass in the text content only so that no styling is applied on the way in. Currently I'm using an invalid tag <n> to avoid that issue.
Sure, you can with imperative slot assignment, but not yet in Safari.
You can not slot a text node into a named slot (declarative).
Mixing declarative and imperative slots is not possible.
::slotted(*) can not target text nodes.
https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Imperative-Shadow-DOM-Distribution-API.md
https://caniuse.com/mdn-api_shadowroot_slotassignment
<script>
customElements.define("slotted-textnodes", class extends HTMLElement {
constructor() {
super().attachShadow({
mode: 'open',
slotAssignment: 'manual' // imperative assign only
}).innerHTML = `<style>::slotted(*){color:red}</style>
Click me! <slot name="title"></slot> <slot>NONE</slot>!!!`;
}
connectedCallback() {
let nodes = [], node;
setTimeout(() => { // wait till lightDOM is parsed
const nodeIterator = document.createNodeIterator(
this, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => node.parentNode == this && (/\S/.test(node.data))
}
);
while ((node = nodeIterator.nextNode())) nodes.push(node);
this.onclick = (e) => {
this.shadowRoot.querySelector("slot:not([name])").assign(nodes[0]);
nodes.push(nodes.shift());
}
})
}
})
</script>
<slotted-textnodes>
Foo
<hr separator>Bar<hr separator>
<b>text INSIDE nodes ignored by iterator filter!</b>
Baz
<span slot="title">Can't mix Declarative and Imperative slots!!!</span>
</slotted-textnodes>

Using Javascript to create html custom Tag

class Headers extends React.Component {
render() {
const selected = this.props.selectedPane;
const headers = this.props.panes.map((pane, index) => {
const title = pane.title;
const klass = index === selected ? 'active' : '';
return (
<li
key={index}
className={klass}
onClick={() => this.props.onTabChosen(index)}>
{title}{' '}
</li>
);
});
return (
<ul className='tab-header'>
{headers}
</ul>
);
}
}
export default class Tabs extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedPane: 0
};
this.selectTab = this.selectTab.bind(this);
}
selectTab(num) {
this.setState({selectedPane: num});
}
render() {
const pane = this.props.panes[this.state.selectedPane];
return (
<div>
<h1>Tabs</h1>
<div className='tabs'>
<Headers
selectedPane={this.state.selectedPane}
//onTabChosen={this.selectTab}
panes={this.props.panes}>
</Headers>
<div className='tab-content'>
<article>
hellooooo
{pane.content}
</article>
</div>
</div>
</div>
);
}
}
I'm currently creating a 3 tab section where if you click on a tab, it gives you a new pane.
When looking at the render function I see a custom tag called Headers.
I know it coming from the Headers class at the beginning, but how does that format work? Is that a custom tag we building?
Also when looking at its properties such as onTabChosen, when it is deleted in the render method (for learning purposes) and I click on a selected tab, an error comes up saying
"_this.props.onTabChosen is not a function".
this.props.onTabChosen(index).. was written in the Headers class but not as a function correct?
I guess because I am also confused on how this.props.onTabChosen(index) works since onTabChosen was never declared anywhere, just input after props.
When looking at the render function I see a custom tag called "Headers".
That is not a custom tag. That is a React Component.
I know it coming from the Headers class at the beginning, but how does that format work?
Headers is either a function or a class (i.e. a constructor function).
The function will be called and the first argument passed to it will be an object with properties and values that match the props on the JSX element.
If you're going to use React then read a tutorial, this is very introductory level stuff for the framework.
It is covered very early on in both the MDN tutorial and the official React tutorial.
I guess because I am also confused on how this.props.onTabChosen(index) works since onTabChosen was never declared anywhere, just input after props.
It was declared, just not in the piece of code you shared.

Slots does not work on a html web component without shadow dom

I have a html web component without shadow dom and I try to add a slot. For some reason it does not work.
I expected it to switch "Foo bar" to "Hello world" but that does not happen.
Does slots only works with shadow dom and a template?
How can I get it to work?
class HelloWorld extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `
<div>
<slot name="element-name">Foo Bar</slot>
</div>
`;
}
}
customElements.define("hello-world", HelloWorld);
<hello-world>
<span slot="element-name">Hello World</span>
</hello-world>
Yes, <slot> only works in shadowDOM
Slotted content is reflected lightDOM content
See: ::slotted CSS selector for nested children in shadowDOM slot
A Web Component without shadowDOM only has innerHTML
If you do this.innerHTML= on such a Web Component it replaces the innerHTML, just like on any other HTML tag
with shadowDOM:
<hello-world>
<b slot="none">Mighty</b>
<span slot="title">Web Components</span>
Hello!
</hello-world>
<script>
customElements.define("hello-world", class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:"open"})
.innerHTML = `<div><slot></slot><slot name="title">Foo Bar</slot></div>`;
this.onclick = (evt) => this.querySelector('b').slot = "title";
}
});
</script>

Lit-Element: which event to use for DOM updates?

The documentation over at github.com/Polymer/lit-element describes the lifecycl, if a property of some lit-element is changed. However, I can not seem to find any documentation about a lifecycle if the DOM content of the element is changed.
So assume I have some nested DOM structure and my outermost element should display something based on the DOM content. For sake of simplicity the example below will just display the number of child-elements of the given type.
Now at some point my application inserts a new nested element (click the test button below). At this point I would like to update the shown count.
From my tests it seems that render() is not called again in that case, neither is updated().
Which event do I need to listen or which function do I need to implement for to recognize such a change?
My only current workaround is to use requestUpdate() manually after the DOM update, but I think such changes should be handled by lit-element itself.
document.querySelector( 'button' )
.addEventListener( 'click', () => {
const el = document.querySelector( 'my-element' );
el.insertAdjacentHTML( 'beforeend', '<my-nested-element>new addition</my-nested-element>' );
})
my-element, my-nested-element {
display: block;
}
<script src="https://unpkg.com/#webcomponents/webcomponentsjs#latest/webcomponents-loader.js"></script>
<!-- Works only on browsers that support Javascript modules like Chrome, Safari, Firefox 60, Edge 17 -->
<script type="module">
import {LitElement, html} from 'https://unpkg.com/#polymer/lit-element/lit-element.js?module';
class MyElement extends LitElement {
constructor(){
super();
this.number = this.querySelectorAll( 'my-nested-element' ).length;
}
render() {
return html`<p>number of my-nested-element: ${this.number}</p>
<slot></slot>`;
}
}
customElements.define('my-element', MyElement);
class MyNestedElement extends LitElement {
render() {
return html`<slot></slot>`;
}
}
customElements.define('my-nested-element', MyNestedElement);
</script>
<my-element>
<my-nested-element>first</my-nested-element>
<my-nested-element>second</my-nested-element>
</my-element>
<button>test</button>
In order to detect a new element inserted from the Light DOM through a <slot> element, you can listen to slotchange events on the <slot> element, or on the Shadow DOM root itself.
See the running example below:
document.querySelector('button').onclick = () =>
document.querySelector('my-element').insertAdjacentHTML('beforeend', '<my-nested-element>new addition</my-nested-element>');
my-element,
my-nested-element {
display: block;
}
<script type="module">
import {LitElement, html} from 'https://unpkg.com/#polymer/lit-element/lit-element.js?module';
class MyElement extends LitElement {
firstUpdated() {
var shadow = this.shadowRoot
var nb = shadow.querySelector( 'span#nb' )
shadow.addEventListener( 'slotchange', () =>
nb.textContent = this.querySelectorAll( 'my-nested-element').length
)
}
render() {
return html`<p>number of my-nested-element: <span id="nb"></span></p>
<slot></slot>`;
}
}
customElements.define('my-element', MyElement);
</script>
<my-element>
<my-nested-element>first</my-nested-element>
<my-nested-element>second</my-nested-element>
</my-element>
<button>test</button>

Categories