I'm learning web components. When designing a custom element, I have to decide what is going to be hidden in the the shadow DOM. The remainder will then be exposed in the light DOM.
As far as I understand, the APIs allow two extreme use cases with different tradeoffs:
hide almost nothing in the shadow DOM, most of the element's content is in the light DOM and in the element's attributes:
this allows an HTML author to provide anything for the component to display without writing JS;
this is close to the status quo regarding searchability and accessibility
but there is little reward for the work involved; I add complexity with components but they don't encapsulate anything (everything is exposed).
hide almost everything in the shadow DOM, the element's innerHTML is empty:
this requires the element to be instantiated from JS;
this locks usage a lot more because instantiating from JS is more strict (type-wise) than using HTML slots and attributes;
this may be less searchable and accessible (I'm not sure whether this is the case);
I currently lean toward hiding everything in the shadow DOM for the following reasons:
I intend to instantiate everything from JS. I'm not going to author pages in HTML manually. It would be more work to code both an HTML API and a JS API.
It's less cognitive work to hide everything. I don't need to find a right balance about which information is visible in the light DOM.
It's closer to most JS frameworks I'm familiar with.
Am I missing something?
Edit
Thank you, I am answered that it depends on the use case which partially answers my question. But I'm still missing an answer regarding the case I'm in: I'd rather not support slots for some of my components.
I'll add an example for each extreme of the spectrum:
Light-DOM-heavy component: the component user has to insert elements into slots
<template id=light-email-view>
<div>
<div><slot name=from></slot></div>
<ul><slot name=to></slot></ul>
<h1><slot name=subject></slot></h1>
<div><slot name=content></slot></div>
<ul><slot name=attachements></slot></ul>
<div class=zero-attachment-fallback>no attachments</div>
</div>
</template>
Shadow-DOM-heavy component: the component user has to use the JS API
<template id=shadow-email-view>
<div></div>
</template>
<script>
...
let view = document.createElement('shadow-email-view');
// this method renders the email in the shadow DOM entirely
view.renderFromOject(email);
container.appendChild(view);
</script>
In the first example, the component author has more work to do since they need to "parse" the DOM: they have to count attachments to toggle the fallback; basically, any transformation of input that isn't the browser copying an element from the light DOM into the matching shadow DOM slot. Then they need to listen for attribute changes and whatnot. The component user also has more work, they have to insert the right elements into the right slots, some of them non-trivial (the email content may have to be linkified).
In the second example, the component author doesn't need to implement support for instantiating from HTML with slots. But the component user has to instantiate from JS. All the rendering is done in the .renderFromObject method by the component author. Some additional methods provide hooks to update the view if needed.
One may advocate for a middle ground by having the component offer both slots and JS helpers to fill those. But I don't see the point if the component isn't to be used by HTML authors and that's still more work.
So, is putting everything with the shadow DOM viable or should I provide slots because not doing so isn't standard compliant and my code is going to break on some user agent expecting them (ignoring older UAs that are not at all aware of custom elements)?
#supersharp has nailed it.
One thing I see with Web Components is that people tend to have their component do way too much instead of breaking into smaller components.
Let's consider some native elements:
<form> there is no shadow DOM and the only thing it does is read values out of its children form elements to be able to do an HTTP GET, POST, etc.
<video> 100% shadowDOM and the only thing it uses the app supplied children for is to define what video will be playing. The user can not adjust any CSS for the shadow children of the <video> tag. Nor should they be allowed to. The only thing the <video> tag allows is the ability to hide or show those shadow children. The <audio> tag does the same thing.
<h1> to <h6> No shadow. All this does is set a default font-size and display the children.
The <img> tag uses shadow children to display the image and the Alt-Text.
Like #supersharp has said the use of shadowDOM is based on the element. I would go further to say that shadowDOM should be a well thought out choice. I will add that you need to remember that these are supposed to be components and not apps.
Yes, you can encapsulate your entire app into one component, but the browsers didn't attempt to do that with Native components. The more specialized you can make your components to more reusable they become.
Avoid adding anything into your Web Components that is not vanilla JS, in other words, do not add any framework code into your components unless you never want to share them with someone that does not use that framework. The components I write are 100% Vanilla JS and no CSS frameworks. And they are used in Angular, React and vue with no changes to the code.
But chose the use of shadowDOM for each component written. And, if you must work in a browser that does not natively support Web Components that you may not want to use shadowDOM at all.
One last thing. If you write a component that does not use shadowDOM but it has CSS then you have to be careful where you place the CSS since your component might be placed into someone else's shadowDOM. If your CSS was placed in the <head> tag then it will fail inside the other shadowDOM. I use this code to prevent that problem:
function setCss(el, styleEl) {
let comp = (styleEl instanceof DocumentFragment ? styleEl.querySelector('style') : styleEl).getAttribute('component');
if (!comp) {
throw new Error('Your `<style>` tag must set the attribute `component` to the component name. (Like: `<style component="my-element">`)');
}
let doc = document.head; // If shadow DOM isn't supported place the CSS in `<head>`
// istanbul ignore else
if (el.getRootNode) {
doc = el.getRootNode();
// istanbul ignore else
if (doc === document) {
doc = document.head;
}
}
// istanbul ignore else
if (!doc.querySelector(`style[component="${comp}"]`)) {
doc.appendChild(styleEl.cloneNode(true));
}
}
export default setCss;
The choice is 100% dependent on the use case.
Also:
if you want the user to be able to format your custom element with global CSS style attributes, you may opt for the normal, light DOM.
you're right: in the Shadow DOM, "this may be less searchable": the document.querySelector() method won't inspect the Shadow DOM content.
as a consequence, some third-pary JS library may fail to integrate easily with Shadow DOM
if you intend to use a Custom Element polyfill for legacy browsers, you may avoid Shadow DOM because some of its features cannot be really polyfilled.
in many cases, the answer is to provide a mix of Light DOM and Shadow DOM. As suggested by #JaredSmith:
Shadow DOM for the Web Component author,
Light DOM for the Web Compoent user, intergrated in the Shadow DOM with <slot>.
As a conclusion, you should consider the context in which your Web Component will be used to decide whether Shadow DOM is required or not.
Answer to the Edit
Considering your use case, I would create a custom element and:
let the user populate the light DOM with atomic value(s): type element <div class="mail-to"> or custom sub-components <mail-to> as suggested by #Intervalia,
use a Shadow DOM to mask the light DOM,
use Javascript: this.querySelectorAll('.mail-to') or this.querySelectorAll('mail-to') instead of <slot> to extract data from the light DOM and copy (or move) them to the Shadow DOM.
This way users won't have to learn the <slot> working, and the developer will be able to format the web component rendering with more freedom.
<email-view>
<mail-to>guillaume#stackoverflow.com</mail-to>
<mail-to>joe#google.fr</mail-to>
<mail-from>supersharp#cyber-nation.fr</mail-from>
<mail-body>hello world!</mail-body>
<email-view>
Alright. Setting aside for a moment that I think this is a bad questionable idea, here's some code that should do what you want (I didn't run it, but it should work):
class FooElement extends HTMLElement {
constructor () {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(document.importNode(template.content, true));
}
_xformObject (object) {
// turn the obj into DOM nodes
}
renderFromObject (object) {
// you may need to do something fancier than appendChild,
// you can always query the shadowRoot and insert it at
// a specific point in shadow DOM
this.shadowRoot.appendChild(this._xformObject(object));
}
}
You'll have to register the custom element of course.
Now sometimes you really can't get away from doing something like this. But it should be the absolute last resort. See below:
Why I think this is a bad questionable idea, and how to make it better:
One of the main draws to web components is that it enables declarative HTML markup rather than procedural JS DOM manipulations. While providing an API like what you're talking about is certainly a big step up from e.g. creating a table by creating a table node, creating a row node, creating some tds, appending them to the row, then appending that to the table, I (and I think most) developers are of the idea that if your custom element requires direct JavaScript manipulation by the user, then it's not really an HTML element: it's a JavaScript interface.
Let me qualify that a little. When I say "requires" JavaScript I mean there's no way to drop it on the page with some appropriate attributes and wind up with the thing you want. When I say "direct" I mean by calling methods of the object representation of the element directly rather than say toggling an element attribute. To put my point in code:
// good
myCustomElement.setAttribute("toggled-on", true);
// this isn't *bad*, but don't *force* people to do this
myCustomElement.toggleState();
You may want to still provide the second as part of your public API as a convenience to your users, but requiring it seems beyond the pale. Now one issue is that you obviously can't easily pass complex data structures to an HTML attribute (Polymer has helpers for this if you're using Polymer).
But if that's the case, rather than have that be part of the element API, I'd provide a standalone function that returns the appropriate DOM structure rather than baking that in to an element. You could even make it a class method of your custom element class if that's how you roll.
Consider the case where you have a List element that renders an arbitrary number of Item elements. I think it's great to provide a convenience method that takes an array and updates the (light) DOM. But users should be able to append them directly as well.
Your use case may require hacking around the problem. Sometimes you really do legit need to use an antipattern. But consider carefully whether that's the case in your element.
This is purely dependent on your case. But as a general rule, if you find yourself buried in a hell of nested shadow roots, then you may consider going easy on using shadow doms.
Like the follow example illustrates:
<my-outer-element>
shadowRoot
<slot1> ---Reveal dom
<my-inner-element>
shadowRoot
....
Related
As example, I dynamically create instance of some custom HTML element.
After this, I twice use document.body.appendChild() and this work, but this should no be. What I may do for prevent this?
I put sample about this situation below in the text. RandomParagraphSizePlaceholder is simple web-component from some book. I use it only for illustration purposes.
https://jsfiddle.net/h6cxjae5/
P.S. I see, that is something wrong with web component, may be.
Only one instance of web component exists in web-page.
But web component content is duplicated. May be, because it created in connectedCallback. I dont very famimiliar with web component :(.
You get two instances, because your code:
this.appendChild(document.importNode(placeholder.content, true));
effectively makes a copy (importNode) and then appends another element.
connectedCallback (corretcly running twice) has (in this case) nothing to do with it.
https://developer.mozilla.org/en-US/docs/Web/API/Document/importNode
I have to implement somehow the subject. My idea is simply add special attribute to the real nodes mapping to the corresponding virtual ones. However for some reason I have issues with huge enough web apps. For example, I've found out sometimes there is sequence of mutations where firstly the parent element remove and then child is removed (so I have exception).
Maybe there is some open source library for this (mutation-summary is abounded and doesn't work really good in my case)? Or maybe there is another simple solution?
Thanks.
I am trying to support the same type of thing as React.Children
My code looks like
const elem = document.getElementById("profile")
const render = hyperHTML.bind(elem);
const name = elem.textContent
render`<b>Hi ${name}</b>`
So the API looks like
<div id="profile">alax</div> 🢂 <div id="profile"><b>Hi alax</b></div>
and I am using MutationObserver to rerender on content change
But if the content is changed. hyperHTML says its rending to the right element.. but the element keeps its innerHtml(No update)
I can see the <!--_hyper: -2001947635;--> is removed then the content is set but setting up the render & hyperHTML.bind again does nothing
Any thoughts would be great! Thx
Update
The fix to the above problem is to call hyperHTML.bind`` then your normal render using hyperHTML will work
Context -
I am using hyperHTML to create a custom element library(hyper-element)
My use case: I work in a mix-tech project (some people use jQuery)
Side note, on the why. I want to support something like partial templates
Example of a partial template:
<user-list data="[{name:'ann',url:''},{name:'bob',url:''}]">
<div>{#name}</div>
</user-list>
Output:
<user-list data="[{name:'ann',url:''},{name:'bob',url:''}]">
<div>ann</div>
<div>bob</div>
</user-list>
This is one use of setting custom content in an element you control
At the moment I have the setting of the content by 3-party working/re-rending
https://jsfiddle.net/k25e6ufv/16/
My problem is now: it is rending another custom element and getting the pass content to child element
It looks like hyperHTML is setting the child element's content in front to the element and creating the element without setting the content
Scroll down to bottom of source to see implementation!
https://jsfiddle.net/k25e6ufv/14/
Rending crazy-cats:
Html`
xxx: ${this.wrapedContent} zzzz
`
Current output:
wrapedContent: ppp time:11:35:48 ~ crazy-cats: **Party 11:35:48** xxx: zzzz
<crazy-cats>Party 11:37:21 xxx: <!--_hyper: -362006176;--> zzzz </crazy-cats>
Desired output:
wrapedContent: ppp time:11:35:48 ~ crazy-cats: xxx: **Party 11:35:48** zzzz
<crazy-cats> xxx: Party 11:37:21 zzzz </crazy-cats>
I will try to answer as best as I can, but I'll start saying that when asking for help, it'd be much easier/better to show the simplest use case you are trying to solve.
There is a lot of "surrounding" code in your fiddles so that I'll try to answer only to hyperHTML related bits.
hyper-element ?
I am not sure what's the goal of the library but hyperHTML exposes hyper.Component, and there's also an official HyperHTMLElement class to extend, which does most of the things you manually implement in your examples.
I'll keep answering your questions but please consider trying, at least, the official alternative and maybe push some change there if needed.
partial templates
hyperHTML pattern and strength is the Template Literal standard. Accordingly, to generate TL from the DOM would require either parsing of the content or code evaluation. Both solutions aren't the way to go.
Custom Elements require JavaScript to work, and without JS your partial template is useless and also potentially confusing for the user/consumer.
You don't want to define what to do with the data in the layout, you want to define a Custom Element behavior within the class that defines it.
That means: get rid of old-style in-DOM output, and simply use the Custom Element class to define its content. You maintain the related class only instead of maintaining a layout that has no knowledge about how the CE should represent that data.
TL;DR the following is a bad hyperHTML pattern:
<user-list data="[{name:'ann',url:''},{name:'bob',url:''}]">
<div>{#name}</div>
</user-list>
all you want to do is to write this:
<user-list data="[{name:'ann',url:''},{name:'bob',url:''}]"></user-list>
but be careful, the data attribute in hyperHTML is special only if passed through the template literal. If you want to pass JSON to the component, call the attribute differently.
// hyperHTML data is special, no need to use JSON
render`<c-e data=${{as: 'it is'}}></c-e>`
Above snippet is different from having JSON as data attribute text so your example should use data-json name, and the class should remember to JSON.parse(this.dataset.json) in its constructor (or have an attribute observer that does that for you)
hyperHTML owns elements
When you write:
it looks like hyperHTML is setting the child element's content in front to the element and creating the element without setting the content
you are assuming you should care at all what hyperHTML does: you shouldn't.
The only thing you should understand is that hyperHTML owns the node it handles. If you trash those nodes via different libraries or manually, you are doing something wrong.
hyperHTML(document.body)`<p>hello ${'world'}</p>`;
// obtrusive libraries ... later on ...
document.body.textContent = 'bye bye';
// hyperHTML still owns the body content
hyperHTML(document.body)`<p>hello ${'world'}</p>`;
Above snippet is perfectly fine and totally wrong at the same time.
You don't update the body content manually, you don't interfere with its content via jQuery or other libraries, and you should never trash the content at all.
Once you chose hyperHTML to handle a bound context, that's it, you've made your choice.
This is true for pretty much every library on this world. If you use Angular to create something and you mess it all via jQuery, that breaks. If you write backbone templates and you mess later on with their content manually, that breaks.
If you bind an element to hyperHTML and you mess it up with other libraries, that breaks.
The only thing that won't break are wires, meaning the moment you create a wire, you can append it directly and that's actually a DOM node so it will be there, and it will be handled by hyperHTML.
Yet you should use hyperHTML to handle those changes, never jQuery or JS itself.
The output is right
When you say that the output should not contain the comment you are assuming you should care what output is produced via hyperHTML: you shouldn't!
hyperHTML uses comments as delimiters and these are absolutely fine for both performance, being unaffected by repaint and reflows, and for partial changes like the following one:
hyperHTML(document.body)`<p>${'a'} b ${'c'}</p>`
Both a and c will have a comment as anchor node to be able to update their content with anything later on.
hyperHTML(document.body)`<p>${[list, of, nodes]} b ${otherThing}</p>`
You change interpolations? All good, hyperHTML knows what to replace and where.
force-own the content
If you use a different template literal to re-populate a bound node you are trashing the cache and creating new content.
At that point you are better off with innerHTML because all the features of hyperHTML will be gone.
To start with, if your content can change so much, use an array.
hyper(document.body)`${['text']}`;
// you can clean up the text through empty array
hyper(document.body)`${[]}`;
// re-populate it with new content
hyper(document.body)`${['a', 'b', 'c']}`;
Above example is still better than changing template because all the optimizations for the content will be already there.
However, if you want to be sure the node the initial one created via hyperHTML, assuming no third parts script mutate/trash that node, you can use a wire.
const body = hyper()`<p>my ${'content'}</p>`;
document.body.textContent = '';
document.body.appendChild(body);
It's a bit extreme but at least faster.
As Summary
It looks like you are trying to sneak in hyperHTML into an application that trashes layout all the time through different third parts libraries.
Unless you create a closed Shadow DOM reference and you drop partial template through layout, you'll always have issues with libraries based on side effects with DOM content, libraries that mutates elements they don't own.
In hyperHTML the ownership concept is key, like in React you cannot change at runtime the defined JSX for the component, you should never try to change at runtime the defined template literal for hyperHTML.
Now, as much as I'd like to solve all your issues, I feel like it's right to ask you: are you sure hyperHTML is really the solution for your current app? It looks like surrounding side-effects caused by third parts libraries would constantly break your expectations if you don't use closed mode Shadow DOM and hyperHTML only to update your DOM.
I'm a front-end developer, and I'm worried about the best way to target my DOM.
Let's imagine a tiny form to create a new zombie :
<h1>Add a new zombie</h1>
<form id="create-zombie">
<input id="zombie" type="text" name="zombie" />
<input id="lvl" type="text" name="lvl" />
<button type="submit">Add</button>
</form>
...and if I want to get the values of zombie and lvl, I will code something like this:
class Zombie_Add extends Controller
# Dom References
el:
'form': '#create-zombie'
'zombie': '#zombie'
'lvl': '#lvl'
run: ->
#on 'submit', #el.form, #validate
validate: (e) =>
e.preventDefault()
zombie = $(#el.zombie).val()
lvl = $(#el.lvl).val()
module.exports = Zombie_Add
That's "ok" and it does the job, but I have some problems with that "structure" :
If somebody touches the DOM and removes an ID, I'm just fucked, it breaks my code (Captain Obvious spotted !)
For more complicated selectors, it's just a mess (I'm thinking about some stuff like that [name^="dummy-"] input:first). I guess it's easy to imagine how shitty the names of el are.
Anyway, what I want to learn today is what's the best way to target the DOM from JS. Is it better to use IDs, class values or, data-* attributes? How we can prettify a selector with plain English, etc...
If somebody touches the DOM and removes an ID, I'm just ****ed, it
breaks my code (Captain Obvious spotted !)
The best way to target a single, unique element in the DOM is with an ID like you are doing with zombie and lvl. It is both fast to execute and simple to code.
It is a given that if someone messes with the id values in your HTML, that will break your Javascript. That's just the way it is. Anyone messing with your HTML has to be smart enough to know that an ID value is there for a reason so if they want to change that or remove it, then it is their responsibility to find someone who can make corresponding changes in the Javascript. That's just the way it is.
For more complicated selectors, it's just a mess (I'm thinking about
some stuff like that [name^="dummy-"] input:first). I guess it's easy
to imagine how ****ty the names of el are.
The same goes for more complicated selectors. Like it or not, a modern web page is a melding of server-side stuff, presentation HTML and Javascript. The three have to work together to deliver the final solution. While you strive to avoid unnecessary dependencies with good design techniques and practices, you cannot avoid all dependencies, nor would even trying to avoid all possible dependencies be an efficient way to develop.
Example
There are coding practices that can make your code less sensitive to edits to the HTML and I consider those to be desirable practices. For example, consider this HTML snippet:
<div class="header">
<div class="headerBorder">
<div class="actions>
Hide
</div>
<div class="contentContainer">
<div class="content">
... content here
</div>
</div>
</div>
</div>
<div class="header">
... more of these repeated
</div>
And you want to have code that, when you click on the Hide link, it will hide the content.
If you code that like this:
$(".header .actions .hide").click(function() {
$(this).parent().next().children().first().hide();
});
Then, you have embedded in your Javascript some extremely detailed knowledge of the HTML structure in the area of your button and content and pretty much any structural change to that HTML (even just adding one more level of div to help with some layout) will break the Javascript. This is bad.
Instead, you could write this:
$(".header .actions .hide").click(function() {
$(this).closest(".header").find(".content").hide();
});
This depends only upon one HTML structural concept - that the .content that corresponds to the .hide button is somewhere in the common .header parent element. The entire structure within that .header parent can be changed and this code will not break. This is robust code that tries to be as independent of the details of the HTML structure as possible.
Anyway, what I want to learn today is what's the best way to target
the DOM from JS. Is it better to use IDs, class values or, data-*
attributes? How we can prettify a selector with plain English, etc...
It's best to use IDs to target elements for which there is only one unique element in the page.
It's best to use class names to target elements for which there can be more than one element in the page and you can combine that with other parts of a selector if you want to target only a specific item with a class name.
data attributes can be used in selectors, but that isn't really their primary purpose. I can't think of any reason why it would be better to use a data attribute instead of a class name. I use data attributes for storing actual data on an object that will be used by scripts. This tends to allow the code to be more generic and let the content describe itself.
When you talk about classes getting removed to change the state of the element, that is a legitimate use of a class, but it would just be a bad design decision to use the same class for selecting an element as for add/removing state. Use different class names for those two purposes. For example, you could have a class name called "selected" that indicates a selection state, but you would only use that in a selector if you wanted just the selected objects. If you wanted all line items in a table, you wouldn't use ".selected", you'd create a more descriptive class name for that object such as "lineitem". Remember, you can have multiple class names on an object so you can use different class names on the same object for different purposes.
You seem to be searching for some magic bullet here that prevents changes in the HTML from affecting Javascript in any way. That simply does not exist. Selecting a DOM element or finding a DOM element relative to something that was clicked will rely on some inherent knowledge of how the HTML is structured or tagged. What is important is that you minimize that dependence to only what is required and that anyone messing with the HTML has an understanding of how to best change things and not break the Javascript or discusses changes with someone who knows the Javascript. If you're using different skills/people on different portions of the project, they have to coordinate their efforts in order to not break things or be aware of what dependencies there are with the other parts of the system. That's just the way it is. There is no magic answer here that removes all dependencies between HTML and Javascript. Smart designs have smaller and more obvious dependencies. Poor designs have large and often hidden depedencies.
Your question about "plain English" isn't particularly clear to me. It seems obvious to use descriptive ID names, class names or attribute names that make it as obvious to the reader what that particular name is being used for. I'm not sure what you were looking for beyond that.
I built an application using Polymer and its working as intended. I'd like to style it with a totally custom look and feel.
Is it possible to disable default styling of Polymer elements via a flag or some roundabout way, or will I have to manually override everything I want to change?
To override an element's styles from the outside, you can use ::shadow and /deep/:
http://www.polymer-project.org/articles/styling-elements.html#style-fromoutside
Those pierce through the Shadow DOM boundaries and allow you to target nodes internal to the element. Unfortunately, this means you need to explicitly write rules that target these nodes. This is sort of the deal with components...an author defines the look and feel, but you're welcome to override it as consumer/developer.
It's also worth noting that the visual elements use the non-visual core-*/polymer-* elements to get their job done. If you need a completely different UI, I'd create an element that reuses those core elements.