This question already has answers here:
Cannot get content from template
(2 answers)
Closed 3 years ago.
If I write a template node into the HTML by hand I can use it in my custom element just fine. If I create a template node and append it to the HTML using javascript when I try to use it, it's empty...
in the example below I make template-a the regular HTML way and make template-b to be the same shape using javascript. I define a very simple custom element that uses both templates. only template-a is visible.
const sandbox = document.getElementById('sandbox')
const slot = document.createElement('slot')
slot.setAttribute('name', 'b')
slot.append('slot content goes here')
const em = document.createElement('em')
em.append(slot, '?')
const createdTemplate = document.createElement('template')
createdTemplate.setAttribute('id', 'template-b')
createdTemplate.append(em)
sandbox.append(createdTemplate)
customElements.define('test-element', class extends HTMLElement {
constructor () {
super()
this.attachShadow({ mode: 'open' }).append(
...['template-a','template-b']
.map(id =>
document.getElementById(id).content.cloneNode(true)
)
)
}
})
<div id="sandbox">
<template id="template-a">
<strong><slot name="a">slot content goes here</slot>!</strong>
</template>
<test-element>
<span slot="a">some a slot content</span>
<span slot="b">some b slot content</span>
</test-element>
</div>
Some notes on your code:
this.shadowRoot
this.shadow = this.attachShadow({ mode: 'open' })
can become
this.attachShadow({ mode: 'open' })
this creates/sets this.shadowRoot for free
appendChild vs. append
Note that .appendChild(el) takes one element
and .append() takes an Array
Only difference is appendChild() returns a reference to the inserted element,
and append() returns nothing
So you can write:
em.appendChild(slot)
em.appendChild(document.createTextNode('?'))
as
em.append(slot, document.createTextNode('?'))
If you have Nodes in an Array:
let myElements = [slot, document.createTextNode('?')];
you can use the ES6 spread opperator:
em.append(...myElements)
This means you can write:
this.shadow.appendChild(document.getElementById('template-a').content.cloneNode(true))
this.shadow.appendChild(document.getElementById('template-b').content.cloneNode(true))
as:
this.shadowRoot
.append(
...['a','b']
.map(templ => document.getElementById(`template-${templ}`).content.cloneNode(true))
)
template nodes have a special content attribute which holds their children. (which I kind of knew but thought it was a little more magical than it is). If this line:
createdTemplate.append(em)
is changed to
createdTemplate.content.append(em)
then it all works
const sandbox = document.getElementById('sandbox')
const slot = document.createElement('slot')
slot.setAttribute('name', 'b')
slot.append('slot content goes here')
const em = document.createElement('em')
em.append(slot, '?')
const createdTemplate = document.createElement('template')
createdTemplate.setAttribute('id', 'template-b')
createdTemplate.content.append(em)
sandbox.append(createdTemplate)
customElements.define('test-element', class extends HTMLElement {
constructor () {
super()
this.attachShadow({ mode: 'open' }).append(
...['template-a','template-b']
.map(id =>
document.getElementById(id).content.cloneNode(true)
)
)
}
})
<div id="sandbox">
<template id="template-a">
<strong><slot name="a">slot content goes here</slot>!</strong>
</template>
<test-element>
<span slot="a">some a slot content</span>
<span slot="b">some b slot content</span>
</test-element>
</div>
Related
Below ake-class2 inherits from/extends ake-class1.
Adding <select> element to ake-class2.shadowRoot.
console.log this.clickme button to make sure it's inherited correctly.
clickme button doesn't work without adding again lines after comment These 3 lines in ake-class2.
I couldn't understand why this behavior happen.
why this happpens ?
<html>
<head>
<title>AKE Front</title>
<script>
class1_html = `
<div class="container">
<button class="clickme">Click Me</button>
</div>
`
class2_html = `
<select></select>
`
/*--------------------------------------------------------------------------------*/
class AKEclass1 extends HTMLElement { //custom-component class
constructor() {
super(); // always call super() first in the constructor.
//const root = this.createShadowRoot(); //chrome only - deprecated
const root = this.attachShadow({mode: 'open'}); //By calling attachShadow with mode: 'open', we are telling our element to save a reference to the shadow root on the element.shadowRoot property
this.shadowRoot.innerHTML = class1_html;
// These 3 lines
this.container = this.shadowRoot.querySelector("div.container");
this.clickme = this.container.querySelector("button.clickme");
this.clickme.addEventListener("click", this.clickMe.bind(this));
}
clickMe() {
alert("Hello !");
}
}
customElements.define('ake-class1', AKEclass1);
/*--------------------------------------------------------------------------------*/
class AKEclass2 extends AKEclass1 { //custom-component class
constructor() {
super(); // always call super() first in the constructor.
this.shadowRoot.innerHTML += class2_html;
// These 3 lines
//this.container = this.shadowRoot.querySelector("div.container");
//this.clickme = this.container.querySelector("button.clickme");
//this.clickme.addEventListener("click", this.clickMe.bind(this));
}
}
customElements.define('ake-class2', AKEclass2);
/*--------------------------------------------------------------------------------*/
</script>
</head>
<body>
<ake-class2 class="ake_window"></ake-class2>
</body>
</html>
As mentioned in the comments .innerHTML += is the culprit.
What it does:
Create a NEW string by concatening .innerHTML + NEWString
delete the innerHTML DOM tree
and then Garbage Collection (GC) kicks in:
Delete all existing DOM elements, thus remove all connected listeners
set the NEW String as innerHTML
Some 'gurus' say this makes innerHTML evil, I say you need to understand what it does.
In the SO snippet below you see the listener being connected twice, but only executed once when clicked
<script>
class BaseClass extends HTMLElement {
constructor() {
super().attachShadow({mode:'open'})
.innerHTML = `<button>Click ${this.nodeName}</button>`;
this.listen();// but removed by GC
}
listen(){
console.log("add listener on", this.nodeName);
this.shadowRoot
.querySelector("button")
.onclick = (evt) => this.clicked(evt);
}
clicked(evt){
console.log("clicked", this.nodeName)
}
}
//customElements.define('element-1', BaseClass);
customElements.define('element-2', class extends BaseClass {
connectedCallback(){
this.shadowRoot.innerHTML += ` with concatenated HTML`;
this.listen();
}
});
</script>
<element-2></element-2>
Notes:
Using the inline onclick handler, it only allows for one handler where addEventListener can add more (you can use it here if you like)
No need for oldskool .bind(this) by defining lexical scope with a arrow function, not a function reference
all can be chained because
super() sets AND returns the this scope
attachShadow sets AND returns this.shadowRoot
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>
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.
I have this example html, for which I have the following xpaths defined in a POM:
<button>
Foo
<i class="myClass" label="a label">Something</i>
</button>
<button>
Bar
<i class="myClass" label="another label">Something</i>
</button>
class myPage {
constructor() {
this.fooButton = () => $('//button[text()=\'Foo\']');
this.barButton = () => $('//button[text()=\'Bar\']');
}
}
I want to access the inner text from the myClass iframe, but don't want to define new constructor elements for it - feels like repeating code:
this.insideSomething = () => $('//button[text()=\'Foo\']/i[text()=\'Something\']');
Is there a way to perhaps only define the end of the xpath in its own function? Something like this:
somethingText(parentElement) {
return parentElement + '/i[text()=\'Something\']';
}
... so I can call it like this, for example:
expect(somethingText(MyPage.FooButton).isDisplayed()).to.be.true;
You could try using the "starts with" attribute selector operator ^=:
this.fooButton = () => $('//button[text()^=\'Foo\']');
More info on attribute selectors
From the custom elements page, I see that to extend an element you do:
var XFooButtonPrototype = Object.create(HTMLButtonElement.prototype);
XFooButtonPrototype.createdCallback = function() {
this.textContent = "I'm an x-foo button!";
};
var XFooButton = document.registerElement('x-foo-button', {
prototype: XFooButtonPrototype,
extends: 'button'
});
Then later in the guide it says that you can make an element by writing either:
<x-foo></x-foo>
Or:
<button is="x-foo-button"></button>
Questions:
Why is it important to specify extends: 'button' when the element is obviously_ inheriting from HTMLButtonElement (since it has HTMLButtonElement.prototype in its proto chain)
How is the link between button and x-foo-button established? Does x-foo-button become a possible option of button in terms of is="x-foo-button" thanks to that extends: 'button' ? What happens "internally", so to speak?
Why would you pick <button is="x-foo-button"></button> over <x-foo></x-foo>...?
[ADDENDUM]
Polymer saves us from this duplication:
MyInput = Polymer({
is: 'my-input',
extends: 'input',
created: function() {
this.style.border = '1px solid red';
}
});
If extends is there, Polymer will put the right prototype in the chain with Object.getPrototypeOf(document.createElement(tag));.
So, corollary question:
Why the duplication in the first place? If there is an extends, shouldn't the browser automatically do this?
You totally misunderstood how extending web components work.
Create simple elements
First of all, this is how you register a new element:
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype)
});
To create an element you can do one of these:
<x-foo></x-foo>
var xFoo = new XFoo();
document.body.appendChild(xFoo);
var xFoo = document.createElement( 'x-foo')
document.body.appendChild(xFoo);
Create extended elements
This is how you extend an existing element:
var XFooButton = document.registerElement('x-foo-button', {
prototype: Object.create(HTMLButtonElement.prototype),
extends: 'button'
});
To create one you can do one of these:
<button is="x-foo-button"></button>
var xFooButton = new XFooButton();
document.body.appendChild(xFoo);
var xFooButton = document.createElement('button', 'x-foo-button');
document.body.appendChild(xFooButton);
Note that in case of extended custom elements, when registering them you have to specify both the prototype (set to HTMLButtonElement.prototype rather than HTMLElement.prototype), and the extended tag's name (extends: 'button').
Also, when you create an extended element using markup or createElement(), you need to also specify the basic element (button) and the extended one (x-foo-button),
(Note: I am aware I am answering myself)
I think its Importent to Say here:
WARNING DEPRECATED Browser API METHOD
Here in this Question a .registerElement is Used it got Replaced by .defineElement and the Api has changed
current way to define a element
class AppDrawer extends HTMLElement {
constructor() {
super()
this.innerHTML = '<h1>UH</h1>'
}
}
window.customElements.define('app-drawer', AppDrawer);
// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer-noname', class extends HTMLElement {
constructor() {
super()
this.innerHTML = '<h1>UH AH</h1>'
}
});
Example - defining a mobile drawer panel, < app - drawer >:
Example usage:
<app-drawer></app-drawer>
<app-drawer-noname></app-drawer-noname>
```