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 ShadowRoot.
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
Related
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!
I am using Polymer 1.0, and I'm having a problem retrieving and inserting into the dom and showing. I can get the HTML "li" to print to the console, but I am unable to get polymer to render the HTML.
<example-app appjs="appJsFile.js">
<ul id="content">
//I want Content Here from Polymer
</ul>
</example-app>
appJsFile.js gets imported into "example-app". I have a function for this:
for(var i in this.listData){
var _ele = parent.document.createElement('li');
_ele.innerHTML = this.listData[i].listDataName;
return _ele;
}
Any ideas?
Your code doesn't show where you add the element to the DOM.
You create the element and then return it. In your case this would leave the function after the first li element was created.
Where do you return it to?
parent.document.createElement('li') just creates an element. For it to be shown you need to add it to the DOM (like someElement.append(_ele)).
Direct DOM manipulation should be avoided.In your case you could use <template is="dom-repeat">. If you want to support browsers that don't yet allow <template> inside <ul> you can of course do direct DOM manipulation.
For performance reasons, Polymer 1 doesn't do full DOM API polyfill. You have to use Polymer DOM API to access DOM elements to ensure the polyfills are used.
Form the code in your question it's not clear what the context is.
Do you want to add the elements into the template of a Polymer element or add it as child elements or even outside Polymer elements.
In your case this could look like
var content = this.$.content;
for(var i in this.listData) {
var ele = parent.document.createElement('li');
ele.innerHTML = this.listData[i].listDataName;
content.append(_ele);
}
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.
I have:
<k-myelement>
<k-anotherelement></k-anotherelement>
</k-myelement>
When I define the template like this:
<polymer-element name="k-myelement">
<template>
<content select="k-anotherelement" id="anotherelement"></content>
</template>
</polymere-element>
I can access the inner element with this.$['anotherelement']
But with this approach I have to predefine, which inner elements can be used.
What I want is a template technique, that allows me to access all the inner elements.
<content> (insertion points) are for rendering elements in the light DOM at specific locations in the Shadow DOM. Using <content select="k-anotherelement"></content> says "render any <k-anotherelement> elements here. If you want all light DOM nodes to be invited into the rendering party, simply use <content></<content>.
The other issues with your snippet:
The name of the element needs to be defined on <polymer-element>, not as <template name="k-myelement">
To get the list of nodes that pass through a <content>, use content.getDistributedNodes(). You may also want to consider if you even need <content>. Light DOM children nodes can be access with .children and the other accessors. From the Polymer docs:
For a <content>, you can iterate through content.getDistributedNodes() to get the list of nodes distributed at the insertion point.
In Polymer, the best place to call this method is in the attached() callback so you’re guaranteed that the element is in the DOM tree.
Also remember that you can access the light DOM as the element’s normal children (i.e. this.children, or other accessors). The difference with this approach is that it’s the entire set of potentially distributed nodes; not those actually distributed.
Right now, I'm binding events to the parent element of my custom tag's rendered content, then using classes to target the event onto the element which my custom tag actually renders. I feel this is likely to cause strange bugs. For instance, if anyone on my team places two custom tags using the same targeting-classes under the same immediate parent element, it would cause multiple events to fire, associated with the wrong elements.
Here's a sample of the code I'm using now:
$.views.tags({
toggleProp: {
template: '<span class="toggle">{{include tmpl=#content/}}</span>',
onAfterLink: function () {
var prop = this.tagCtx.view.data;
$(this.parentElem).on('click', '.toggle', function () {
prop.value(!prop.value());
});
},
onDispose: function () {
$(this.parentElem).off('click', '.toggle');
}
}
// ... other custom tags simply follow the same pattern ...
});
By the time we hit onAfterLink, is there any reliable way to access the rendered DOM Element (or DOM Elements) corresponding to the custom tag itself? With no risk of hitting the wrong element by mistake? I understand that the custom tag may be text without an HTML Element, but it would still be a text node, right? (Could I even bind events to text nodes?)
In other places, and using (far) older versions of JsViews, I've bound events after the render using (sometimes a lot of) targeting logic built into the rendered elements as data- attributes. Not only is this a far more fragile method than I like for accessing the rendered data, it would be incredibly risky and convoluted to try to apply this approach to some of our deeply-nested-and-collection-ridden templates.
I also don't like needing to insert a span with my custom tag, just so I can apply classes to it, but if it's still necessary for the event, I'll cope.
I ask, then, what is a safe, modular way to bind events to the DOM so that I also have access to the data rendered directly against those elements?
Edit: As an additional concern, using onAfterLink won't let me bind events to non-data-linked rendered content. This may be part of the design intent of JsViews vs pure JsRender, but I don't yet understand why that would be the case.
Rather than using this.parentElem, you can use
this.contents()
which is a jQuery object containing all immediate content elements within the tag.
You can also provide a selector argument,
this.contents("someselector")
to "filter" , and include an optional boolean "deep" flag to both "filter" and "find" - i.e.
this.contents("someselector", true).
Using the above APIs ensures you are only taking elements that are actually within the tag content.
You may not need to remove the handlers in onDispose, if the tag is only deleted along with its content, you can rely on the fact that jQuery will dispose handlers when the elements are removed from the DOM.
You can only attach events to elements, not to text nodes. So if your content does not include elements, you would need to add your wrapper element, but not otherwise.
$.views.tags({
toggleProp: {
template: '{{include tmpl=#content/}}',
onAfterLink: function () {
var prop = this.tagCtx.view.data;
this.contents().on('click', function () {
prop.value(!prop.value());
});
},
onDispose: function () {
this.contents().off('click');
}
}
});
Also take a look at samples such as http://www.jsviews.com/#samples/tagcontrols/tabs which use the above approach.