I am using my lit element like this
<my-header>MyHeading</my-header>
And I have my lit element's render method:
render() {
return html`
<h3><slot></slot></h3>
`;
}
which is working perfectly. Now I want the inner content i.e. "MyHeading" in my lit element's class as a value(not to render). Is there any way to get that innerHTML or as a text?
Note: my use case can be to set another property of rendered content like
render() {
return html`
<h3 id="${//How to get that 'MyHeading' here??}"><slot></slot></h3>
`;
}
Is it possible to get inner content as a value?
This is what you get when you learn new stuff starting with a Library or Framework;
You learn the Tool, not the Technology.
The child-elements of your customElement are not available yet
when the connectedCallback fires
so you wait till the EventLoop is empty (and thus know all children are parsed)
Or use any of the Library methods that (usually) fire even later than a setTimeout
Or, even more blunty, like many blogs show, execute the script that creates your Element
after the whole DOM is parsed by marking it a type="module" or async or defer
<script>
customElements.define("my-element", class extends HTMLElement {
constructor(){
super().attachShadow({mode:"open"}).innerHTML = `<h3><slot></slot></h3>`
}
connectedCallback() {
setTimeout(() => { // wait till innerHTML is parsed
let title = this.innerText;
console.log("Reflected from lightDOM:" , title);
this.shadowRoot.querySelector("h3").id = title;
})
}
})
</script>
<my-element>
Hello Web Components!
</my-element>
Related
I'm currently learning Web Components and I wonder if it is possible to have a Component load its own data dynamically, similar to how <img> does from its src attribute, i.e. something like this:
<my-fancy-thingy src='/stuff.json'></my-fancy-thingy>
Obviously this functionality would be useful if stuff.json could be rather large, so it should also be possible to make use of the browser's caching mechanism so the referenced file doesn't get reloaded every time we request the page, unless changed.
Can this be done?
Sure, take inspiration from <load-file> See Dev.to Post
/*
defining the <load-file> Web Component,
yes! the documenation is longer than the code
License: https://unlicense.org/
*/
customElements.define("load-file", class extends HTMLElement {
// declare default connectedCallback as sync so await can be used
async connectedCallback(
// attach a shadowRoot if none exists (prevents displaying error when moving Nodes)
// declare as parameter to save 4 Bytes: 'let '
shadowRoot = this.shadowRoot || this.attachShadow({mode:"open"})
) {
// load SVG file from src="" async, parse to text, add to shadowRoot.innerHTML
shadowRoot.innerHTML = await (await fetch(this.getAttribute("src"))).text()
// append optional <tag [shadowRoot]> Elements from inside <load-svg> after parsed <svg>
shadowRoot.append(...this.querySelectorAll("[shadowRoot]"))
// if "replaceWith" attribute
// then replace <load-svg> with loaded content <load-svg>
// childNodes instead of children to include #textNodes also
this.hasAttribute("replaceWith") && this.replaceWith(...shadowRoot.childNodes)
}
})
Change .text() to .json() and it parses JSON files
Caching can be done by storing the String in localStorage (but a 5MB limit total, I think):
https://en.wikipedia.org/wiki/Web_storage
https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
You need to come up with "data has changed" strategy; as the Client has no clue when data actually was changed. Maybe an extra semaphore file/endpoint that provides info if the (large) JSON file was changed.
This works like a charm
export class MonElement extends HTMLElement {
constructor(){
super();
this.attachShadow({mode:'open'});
(...)
this.shadowRoot.appendChild(atemplate);
}
connectedCallback(){...}
static get observedAttributes(){
return ['src'];
}
attributeChangedCallback(nameattr,oldval,newval)
{
if (nameattr==='src') {
this[nameattr]=newval;
here do the fetch for the src value which is newval then update what you got in the innerdom
}
(...)
In lit-html we have the firstUpdated() method to run one-time initialisations once an element is rendered.
What if you need to run a function only once all children within that template are updated?
And what if your template includes native form elements and custom ones?
Right now I am doing a terrible:
firstUpdated () {
super.firstUpdated()
setTimeout(() => this.onceChildrenAreUpdated(), 100)
}
Surely there is a better way?
I realise it's tricky, because for lit-element "rendered" means that the DOM is done; it doesn't mean that all elements inside have done whichever initialisation then want to do.
But still...
You can wait for all the children to be updated:
async firstUpdated () {
const children = this.shadowRoot.querySelectorAll('*'));
await Promise.all(Array.from(children).map((c) => c.updateComplete));
this.onceChildrenAreUpdated();
}
Replace querySelectorAll('*') with something more specific if possible.
How would I go about extending an element that has a slot in its template, and stamp my child element’s dom in that slot?
I’m trying to override the child’s template method (like that) but so far I was unsuccessful.
What I’m trying to do is extending a paper-dropdown-menu to always have a certain dropdown content, while keeping all of paper-dropdown-menu input features (validation, etc.) without wiring all by hand with a "wrapper" component.
Found a way! It's just replacing the parent's slot node with the child's node you want to stamp instead, here's an example:
<dom-module id="custom-child">
<template>
<what-you-want-to-stamp slot="parent-slot-name"></what-you-want-to-stamp>
</template>
<script>
(() => {
const CustomParent = customElements.get('custom-parent')
let memoizedTemplate
class CustomChild extends CustomParent {
static get is() {
return 'custom-child'
}
static get template() {
if (!memoizedTemplate) {
memoizedTemplate = Polymer.DomModule.import(this.is, 'template')
let whatYouWantToStamp = memoizedTemplate.content.querySelector('what-you-want-to-stamp')
let parentTemplateContent = document.importNode(CustomParent.template.content, true)
let slot = parentTemplateContent.querySelector('slot')
memoizedTemplate.content.insertBefore(parentTemplateContent, whatYouWantToStamp)
memoizedTemplate.replaceChild(whatYouWantToStamp, slot)
}
return memoizedTemplate
}
}
customElements.define(CustomChild.is, CustomChild)
})()
</script>
</dom-module>
My final objective is don't have to write HTML like this:
<div id='counter'>
{{counter}}
</div>
<div>
<button
id="startButton"
on-click="{{start}}">
Start
</button>
<button
id="stopButton"
on-click="{{stop}}">
Stop
</button>
<button
id="resetButton"
on-click="{{reset}}">
Reset
</button>
</div>
I would like to know if it is possible to create a Polymer-element without using HTML. For example I tried this:
#CustomTag('tute-stopwatch')
class TuteStopWatch extends PolymerElement {
ButtonElement startButton,
stopButton,
resetButton;
#observable String counter = '00:00';
TuteStopWatch.created() : super.created() {
createShadowRoot()..children = [
new DivElement()..text = '{{counter}}',
new DivElement()..children = [
startButton = new ButtonElement()..text = 'Start'
..onClick.listen(start),
stopButton = new ButtonElement()..text = 'Stop'
..onClick.listen(stop),
resetButton = new ButtonElement()..text = 'Reset'
..onClick.listen(reset)
]
];
}
}
Previous code creates HTML and shadow root correctly, but it doesn't create the binding between the #observable counter and the text of the DivElement.
I know that this is caused because I am trying to create the shadow root after the element has been instantiated/created. So that I should create the template of the element in other place before the template has been bound with its observable.
You can write a manual data binding like this:
changes.listen((changes) {
for (var change in changes) {
if (change.name == #counter) {
myDivElement.text = change.newValue;
}
}
});
changes is a property of the Observable class, which PolymerElement mixes in. (This is difficult to see in the API reference, as it currently doesn't show a class' mixins or the mixed in properties and methods.)
Polymer seems to be mostly about enabling declarative html based bindings. It may be worth exploring using custom elements and shadow dom directly, as you're not really using polymer for anything in this example. To do this you need to change the class definition to:
class TuteStopWatch extends HtmlElement with Observable {
...
}
And register your element with document.register(). You also need to include the polymer.js polyfill for custom elements.
Say I have a DOM that looks like this in my Document:
<body>
<div id="outer">
<custom-web-component>
#shadow-root (open)
<div id="inner">Select Me</div>
</custom-web-component>
</div>
</body>
Is it possible to select the inner div inside the shadow root using a single querySelector argument on document? If so, how is it constructed?
For example, something like document.querySelector('custom-web-component > #inner')
You can do it like this:
document.querySelector("custom-web-component").shadowRoot.querySelector("#inner")
In short, not quite. The TL:DR is that, depending on how the component is set up, you might be able to do something like this:
document.querySelector('custom-web-component').div.innerHTML = 'Hello world!';
Do do this - if you have access to where the web component is created, you can add an interface there to access inner content. You can do this the same way you would make any JavaScript class variable/method public. Something like:
/**
* Example web component
*/
class MyComponent extends HTMLElement {
constructor() {
super();
// Create shadow DOM
this._shadowRoot = this.attachShadow({mode: 'open'});
// Create mock div - this will be directly accessible from outside the component
this.div = document.createElement('div');
// And this span will not
let span = document.createElement('span');
// Append div and span to shadowRoot
this._shadowRoot.appendChild(span);
this._shadowRoot.appendChild(this.div);
}
}
// Register component
window.customElements.define('custom-web-component', MyComponent);
// You can now access the component 'div' from outside of a web component, like so:
(function() {
let component = document.querySelector('custom-web-component');
// Edit div
component.div.innerHTML = 'EDITED';
// Edit span
component._shadowRoot.querySelector('span').innerHTML = 'EDITED 2';
})();
<custom-web-component></custom-web-component>
In this instance, you can access the div from outside of the component, but the span is not accessible.
To add: As web components are encapsulated, I don't think you can otherwise select internal parts of the component - you have to explicitly set a way of selecting them using this, as above.
EDIT:
Saying that, if you know what the shadow root key is, you can do this: component._shadowRoot.querySelector() (added to demo above). But then that is quite a weird thing to do, as it sorta goes against the idea of encapsulation.
EDIT 2
The above method will only work is the shadow root is set using the this keyword. If the shadow root is set as let shadowRoot = this.attachShadow({mode: 'open'}) then I don't think you will be able to search for the span - may be wrong there though.
This code will behave like query selector and work on nested shadowDoms:
const querySelectorAll = (node,selector) => {
const nodes = [...node.querySelectorAll(selector)],
nodeIterator = document.createNodeIterator(node, Node.ELEMENT_NODE);
let currentNode;
while (currentNode = nodeIterator.nextNode()) {
if(currentNode.shadowRoot) {
nodes.push(...querySelectorAll(currentNode.shadowRoot,selector));
}
}
return nodes;
}