How to Create a Custom Element with a Shadow Root Already Attached - javascript

Having a simple custom element
document.registerElement('x-foo', {
prototype: HTMLElement.prototype;
});
I can create an HTML node
<x-foo></x-foo>
then select it in JavaScript, and attach a shadow root.
var xFoo = document.querySelector('x-foo')[0];
var root = xFoo.createShadowRoot();
root.textContent = 'I am a shadow root';
However, I would like the objects to be created with a predefined
shadow root, without any JavaScript manipulations afterwards, as it is with
<input> and other user-agent defined nodes.
How would I define a constructor or something for my element in order to achieve this?

Question is a bit old, but putting this answer here in case you haven't figured it out yet.
There are 4 lifecycle callback methods associated with custom elements. Copying from this nice tutorial on html5rocks.
So to answer you question, you can put your code to attach a shadow-root to custom element inside createdCallback and it will be executed every time your x-code element is initialized.
This is a sort of constructor for your custom element.
Hope it helps.

Related

GetElementById from within Shadow DOM

I have a custom-element with shadow DOM, which listens to attribute target change.
target is supposed to be the ID of the element which my component is supposed to be attached to.
I've tried using querySelector and getElementById to get the element of the outer DOM, but it always returns null.
console.log(document.getElementById(target));
console.log(document.querySelector('#' + target));
Both of the above return null.
Is there a way to get a reference to the element in the parent document from within shadow DOM?
You just have to call Shadow​Root.
this.shadowRoot.getElementById('target') should work.
Here's an example, the get syntax will bind an object property to a function.
get target() {
return this.shadowRoot.getElementById('target');
}
There are two use cases of shadow DOM as far as I can see:
You control the the shadow DOM solely through your hosting custom element (like in the answer of #Penny Liu). If you want make sure no other script should call and alter the nodes than this is your choice. Pretty sure some banking websites use this method. You give up on flexibility though.
You just want to scope some parts of your code for styling reasons but you like to control it via document.getElementById than you can use <slot>. After all, many libraries rely on the document object and will not work in shadow DOM.
Back to the problem, what you probably did was something like this:
shadowRoot.innerHTML = `...<script>document.getElementById('target')</script>`
// or shadowRoot.appendChild
This is NOT working! And this is not how shadow DOM was anticipated to work either.
Recalling method 2, you SHOULD fill your shadow DOM solely by <slot> tags. Most minimal example:
<!-- Custom Element -->
<scoped-playground>
<style>some scoped styling</style>
<div id="target"></div>
<script>const ☝☝☝☝ = document.getElementById('target')</script>
</scoped-playground>
<!-- Scoped playground has a shadowRoot with a default <slot> -->
...
this.shadowRoot.innerHTML = "<slot>Everything is rendered here</slot>";
...
More advanced <slot> examples can be found at:
https://developers.google.com/web/fundamentals/web-components/shadowdom#composition_slot

Do custom HTML elements inherit parent CSS styles?

When creating custom elements in HTML, does the child tag inherit the parent's CSS styles?
Here is my test case, from Chrome:
var h1bProto = document.registerElement ('h1-b',
{
prototype: Object.create (HTMLHeadingElement.prototype),
extends: "h1"
});
When I append a child using the new h1bProto it generates an H1 tag with is="h1-b", example below:
var node = document.body.appendChild (new hibProto());
node.textContent = "Hello";
<h1 is="h1-b">Hello</h1>
Hello
This gives me the parents CSS styles. However, if I add a node by creating the element first, then appending the node, the code looks like this:
var node = document.createElement ("h1-b");
node.textContent = "Hello";
document.body.appendChild (node);
<h1-b>Hello</h1-b>
Hello
Am I missing something, or do children not inherit the parent's CSS styles? If they don't, then is the best work around to use the Shadow DOM?
According to the W3 spec you aren't going crazy!
Trying to use a customized built-in element as an autonomous custom
element will not work; that is, Click
me? will simply create an HTMLElement with no special
behaviour.
Aka, in your example making a tag with <h1-b> will not apply the styling or behavior of an <h1> tag. Instead you must create an <h1> tag with the is attribute set to the name of your custom element. The section I linked you to in the spec actually does a great job explaining how to go about creating the tag.
All in all, you just need to make your element like so:
document.createElement("h1", { is: "h1-b" });
One reason that comes to mind for this is that most bots don't parse your javascript. As a result they would have a challenge to figure out what the elements in your dom really are. Imagine how much your seo would tank if a bot didn't realize that your <h1-b> elements were really <h1> elements!

How to access light DOM info from Javascript in Polymer

Question:
In Web Components specification, when you want to read elements within a Light-DOM from the template the <content select></content> element can be used. But, how can this information be retrieved from the javascript code of the component?
Example:
<wc-timer>
<wc-timer-title>I want to read this from JS</wc-timer-title>
</wc-timer>
Thanks in advance, Javier.
Remember that this inside of your prototype methods refers to the element itself. IOW, just like you could do element.innerHTML or element.firstChild you can write this.innerHTML or this.firstChild.
Simple mode:
domReady: function() {
console.log(this.textContent);
}
http://jsbin.com/bociz/2/edit
This gets more complicated if you are using <content> to project nodes through multiple levels of Shadow DOM. In this case, you will need to use getDistributedNodes api of the <content> node itself.
Before getting into that, I suggest you start with the simple version, and ask a follow up question if you get into trouble.
Use this, for accessing lightDOM and
use this.shadowRoot to access shadowDOM
I have no idea what the template renders out as to the dom, but maybe you can try this:
//jQuery
$('wc-timer-title').text();
//Plain
document.getElementsByTagName("wc-timer-title")[0].innerHTML;
You should be able to use /deep/, it is being deprecated but there is no date as to when that will happen.

How to copy a DOM node with event listeners?

I tried
node.cloneNode(true); // deep copy
It doesn't seem to copy the event listeners that I added using node.addEventListener("click", someFunc);.
We use the Dojo library.
cloneNode() does not copy event listeners. In fact, there's no way of getting hold of event listeners via the DOM once they've been attached, so your options are:
Add all the event listeners manually to your cloned node
Refactor your code to use event delegation so that all event handlers are attached to a node that contains both the original and the clone
Use a wrapper function around Node.addEventListener() to keep track of listeners added to each node. This is how jQuery's clone() method is able to copy a node with its event listeners, for example.
This does not answer the question exactly, but if the use case allows for moving the element rather than copying it, you can use removeChild together with appendChild which will preserve the event listeners. For example:
function relocateElementBySelector(elementSelector, destSelector) {
let element = document.querySelector(elementSelector);
let elementParent = element.parentElement;
let destElement = document.querySelector(destSelector);
elementParent.removeChild(element);
destElement.appendChild(element);
}
Event Delegation example.
After reading Tim Down's answer, I found delegated events are very easy to implement, solving a similar problem I had. I thought I would add a concrete example, although it's in JQuery not Dojo.
I am re-skining an application in Semantic UI, which requires a small piece of JS to make the message close buttons work. However the messages are cloned from an HTML template tag using document.importNode in a library. This meant even if I did attach the event handlers to the template in the new HTML, they are lost during the cloning.
I cannot do Tim's option 1, to simply re-attach them during cloning as the messaging library is front-end framework agnostic. (Interestingly my previous front-end was in Zurb Foundation which uses a "data-closable" attribute, the functionality of which does survive the cloning process).
The normal event handling suggested was like this:
$('.message .close').on('click', function() {
$(this)
.closest('.message')
.transition('fade');
});
The problem being ".message" at app-load only matches the single template, not the actual messages which arrive later over web-sockets.
Making this delegated, meant attaching the event to the container into which the messages get cloned <div id="user-messages">
So it becomes:
$('#user-messages').on('click', '.message .close', function() {
$(this)
.closest('.message')
.transition('fade');
});
This worked immediately, saving any complex work like the third option of wrapping the event subs.
The Dojo equivalent looks pretty similar in concept.
This is what #JeromeJ was describing in a comment. Create the initial element using this HTML code.
<DIV ONCLICK="doSomething(this)">touch me</DIV>
When you clone this element the result will have the same handler, and "this" will point to the cloned element.
It would be great if the ONCLICK handler could easily be added in JavaScript. This approach means that you have to write some of your code in HTML.
I know I'm late to the party but this a solution that worked for me:
const originalButtons = original.querySelectorAll<HTMLElement>('button');
const cloneButtons = clone.querySelectorAll<HTMLElement>('button');
originalButtons.forEach((originalButton: HTMLElement, index: number) => {
cloneButtons[index].after(originalButton);
cloneButtons[index].remove();
});
Only inline attributes would work here which are heavily, heavily discouraged because of how misused they are. That said, you can have elements bind to the same event listener.
The proper way with Web Components (and shadow root) would look like and what we would want to replicate:
static onButtonClick(event) {
const { host } = this.getRootNode();
console.log('onButtonClick', { event, host, this: this });
}
/* Or constructor */
connectedCallback() {
this.myShadowRoot.getElementById('button')
.addEventListener(MyElement.onButtonClick);
}
It's efficient because you don't create function per element like you would with .addEventListener(() => this.onButtonClick). 1000 buttons would attach to the same function instead of creating a new function per button.
To convert that to inline would look like this:
<button onclick="this.getRootNode().host.constructor.onButtonClick.call(this, event)">
Is it ugly? Yes. But does it work? Also, yes. In this case there's no need for JS to have find the element and instruct the browser to create an event handler. The inline onclick does that for you. I will note that are creating a new function for each and every element, instead of them all sharing one.
Cloning a node copies all of its attributes and their values, including intrinsic (inline) listeners. It does not copy event listeners added using addEventListener() or those assigned to element properties (e.g., node.onclick = someFunction). Additionally, for a element, the painted image is not copied.
source: MDN (https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode).

browsing through nodes

this is what an html structure of the webpage looks like:
<body>
<form>
<input type='file'/>
</form>
<div id='list'>
<div>value here<input id='delete' type='button'/></div>
</div>
</body>
i have found javascript code that triggers on 'delete' button click and removes input 'file' element. it uses this piece of code where element is input 'file' mentioned above:
deleteButton.onclick=function(){this.parentNode.element.parentNode.removeChild(
this.parentNode.element );}
i am trying to understand logic(rules) behind 'this.parentNode.element' ? why not accessing element directly 'element.parentNode.remove...'
many thanks
i am trying to understand logic(rules) behind 'this.parentNode.element' ?
There's no element property on the Node, Element, HTMLElement, or HTMLDivElement interfaces. So my guess would be that elsewhere in that code, you'll find something that's explicitly adding that property to the element instance of the div containing the button. You can do that, add arbitrary properties to element instances. These are frequently called "expando" properties and should be done very, very, very carefully.
Not the answer to the question, just opinion. It's better avoid constructions like
this.parentNode.element.parentNode
Because in case when you change your DOM structure, you will need rewrite you JS. So I think it's better to give id attributes to tags, and use next construction to get DOM element:
document.getElementById('element_id')
or if you will use some js framework (like jQuery) you can use even easier construction to get DOM element
$("#ement_id")
Ok, "removeChild" is a strange method, and quite probably, ill-conceived. It should look like:
<div>value here<input id='deleteMe' type='button'/></div>
var node = document.getElementById('deleteMe');
node.remove(); // <--- does not exist, but sure would be nice!!!
No, instead we have to do these shenanigans:
var node = document.getElementById('deleteMe');
node.parentNode.removeChild(node); // verbose! Convoluted!
We have to get the node's parent, call the method, then refer to the node again. This doesn't look like any other DOM methods as far as I recall. The good news is you can make it happen all in one line, chained, like a jQuery method.
You are best served to start over or copy somebody else's code. The use of "this" means it was within an object (or class), referring to other methods or properties within that object. You should stick to non-object variables and functions for now.
Hope that helps.

Categories