Blog-Archiv

Samstag, 29. Juli 2017

Sneaking into HTML Web Components

The specification of the HTML document object model (DOM) is now subject to several Internet sites:

  1. The old w3c (World Wide Web Consortium) has been reinforced by
  2. whatwg's (Web Hypertext Application Technology Working Group) DOM Living Standard - this can change every day!

Together with the upcoming ES-6 language, Web Components provide web page composition from reusable HTML fragments, with their CSS shielded against the host page's CSS. Grandparent of all web components is the <video> player. It was implemented by browser vendors as custom-element, and now this implementation way gets standardized and usable for all.


This Blog is a short sneak into the current state of Web Components technology. It shows how we can use it in browsers that already support it, or together with various "polyfill" libraries like Polymer. Chrome already implements all of it, on Firefox you must enable it explicitly, but it does not yet provide HTML imports, and only version 0 of the shadow-DOM. Mind that when you enable this in Firefox, the Web Components page will not render any more!

Example

Here is a screenshot of what my example code below will provide.

The first blue element is a normal DIV inside the main page. It is there to show how a DIV is styled by the main CSS: blue background color, 1 em padding, black border.

The second green DIV is a web component loaded from an external source. It has its own CSS, green color, 2 em padding, and no border, demonstrating that the main CSS can not intrude it.
It also has a script that is executed as soon as the component gets connected to the DOM. This script can access the whole document where it got imported, it is not restricted to its component.

The third blue DIV is also an imported component, but it has no own styling. It is there to show that the main CSS can affect it in this case, i.e. give it the CSS-inherited blue color, although not the 1 em padding (non-inherited CSS property).

The red DIV finally is a component that is inside a template element, directly in the document, not imported. This is the only one you would see in Firefox, because it did not implement the HTML import specification yet.

Host Page

I will present the source code of the main page in four parts. Please assemble them all into a file webbcomponents.html to try it out.

Head

First the main page head, containing the imports of two web components, and a CSS styling that, although strong, should not intrude the web components.

<!DOCTYPE HTML>
<html>
<head>
    <meta name="viewport" content="initial-scale=1"/>
    <meta charset="UTF-8"/>
    <title>Web Components Experiments</title>

    <link id="link-1" rel="import" href="webcomponent-1.html">
    
    <link id="link-2" rel="import" href="webcomponent-2.html">
    
    <style>
        /* try to affect the CSS of used web-components by some strong rules */
        div    {
            background-color: lightBlue !important;
            padding: 1em !important;
            border: 1px solid black !important;
        }
    </style>
    
</head>

Hosts

Next are the HTML elements that will receive the web components. They are called "host elements". Their content can be put into the web component using "slot" attributes. That means that the web component would take over the styling of that content, exclusively.

If there are no slot references, the content would get lost as soon as the web component is put into the shadow root of the host element. You can see this at host-1, its <h1> is not visible (see screenshot above).

<body>

    <div>This is a blue DIV in main page.</div>
    
    <div id="host-1">    <!-- must not be self-closing !? -->
     <h1>I am content of host-1.</h1> 
    </div>
    
    <div id="host-2">
     <h2 slot="slot-2">I am content of host-2.</h2>
    </div>
    
    <div id="host-3">
     <h3 slot="slot-3">I am content of host-3.</h3>
    </div>

Internal Web Component (3)

Here is the red DIV, as embedded web component inside a template element. The browser's DOM parser finds templates, but it ignores them and everything that is inside. They must be handled by JavaScript.

    <template id="component-3">
        <style>
            div    {
                background-color: Tomato;
                padding: 1em;
            }
        </style>
    
        <div>
            <div id="inner">I am the embedded "component-3" DIV, in red, and I show a "slot" of my "host" element:</div>
        
            <slot name="slot-3"></slot>
        </div>
        
    </template>

Mind how slot-3 refers to the content of the host element!

Script

Finally, a JavaScript is needed to create shadow roots and put the web components into their hosts. (Wouldn't they be in the shadow, the CSS of the host page would affect them!)

    <script>
        var render = function(hostId, templateContent) {
            var host = document.getElementById(hostId);
            try {
              var shadowComponent = host.attachShadow({ mode: "open" });
            }
            catch (error) {
              var shadowComponent = host.createShadowRoot();
            }
            shadowComponent.appendChild(templateContent);
        };
        
        var resolve = function(querySelectorElement, componentId) {
            var template = querySelectorElement.querySelector("template#"+componentId);
            return template.content;
        };
        
        var resolveEmbedded = function(componentId) {
            return resolve(document.body, componentId);
        };
        
        var resolveLink = function(linkId, componentId) {
            var link = document.querySelector("link#"+linkId);
            var linkedDocument = link.import;  /* wasn't "import" a reserved word? */
            if ( ! linkedDocument )
                throw "HTML import not found: "+linkId;
            return resolve(linkedDocument, componentId);
        };
        
        window.addEventListener("load", function() {
            render("host-3", resolveEmbedded("component-3"));
            render("host-1", resolveLink("link-1", "component-1"));
            render("host-2", resolveLink("link-2", "component-2"));
        });
        
    </script>

</body>
</html>

The render(hostId, templateContent) function finds the host element, creates a shadow root there, and puts the given web component into it. The attachShadow({ mode: "open" }) function is shadow-specification version 1, the createShadowRoot() fallback function is the old version 0.

The resolve() function fetches a web component from its template. Some implementations use document.importNode(content) to clone it.

The resolveLink() function fetches an imported web component from its template. Mind that we need a lot of ids for a web component: the link, the host, the template, all these must have ids for the JavaScript that builds them together.

External Web Components

Put following into files webbcomponent-1.html and webbcomponent-2.html in same directory as webbcomponents.html.

Component 1

This web component demonstrates how its local CSS dominates the CSS of the main page. Although DIVs are styled to blue in main page, even using !important, the component's DIV is green.

Further a script is there. Here the global variable document is the main page (webcomponents.html), not the component. The script writes a message into the main page's first DIV. Then it gets its local DOM from document.currentScript.ownerDocument, and writes another message there. Mind that you get a "Document Fragment" from template.content, not a DOM element.

<!DOCTYPE HTML>
<html>

    <template id="component-1">
        <style>
            div    {
                background-color: lightGreen;
                padding: 2em;
            }
        </style>
        
        <div>I am the imported "component-1" DIV in green!</div>
        
    </template>

    <script>
    (function() {
        var firstDiv = document.getElementsByTagName("DIV")[0];
        firstDiv.innerHTML = firstDiv.innerHTML + "<br>Script-1 found first DIV in document!";
        
        var componentDocument = document.currentScript.ownerDocument;
        var myDiv = componentDocument.querySelector("template").content.querySelector("div");
        myDiv.innerHTML = myDiv.innerHTML + "<br>Script-1 found its own DIV!";
    })();
    </script>
        
</html>

Component 2

I added this second web component to prove that any number of components can be imported. It demonstrates that also imported components can receive the content of their host elements via slot references. It also shows that the host page's CSS is active here, because no explicit styles were set by the component.

<!DOCTYPE HTML>
<html>

 <template id="component-2">
 
     <div id="inner">I am the imported "component-2" DIV, with no explicit style, and I show a "slot" of my "host" element:</div>
     
     <slot name="slot-2"></slot>
     
 </template>

</html>

By the way: you don't need the enclosing <html> tag, not even the <!DOCTYPE HTML> ....

Resume

What to learn?

  • CSS of a web component in a shadow root is local
  • JavaScripts are not
  • Web components can render contents of the page they are imported into
  • Content in an host element must refer to a slot in the component (using the slot's name), else it will get lost

Will web components be used? When browsers will implement them according to the specification, yes.

One of the most common use cases will be to have local CSS, while content comes from the main page. This gives you separation of content and presentation, and it relieves the problems with global monolithic CSS, which aggregates to sometimes uncontrollable complexity in AJAX-driven single page applications.




1 Kommentar:

fritzthecat hat gesagt…

Firefox removed createShadowRoot() again.
Currently no browser supports attachShadow() any more.
Watch Web Components evolve here https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM and here https://www.w3.org/TR/shadow-dom/