Upgrade to Pro — share decks privately, control downloads, hide ads and more …

WebComponents: Native Komponenten fürs Web - ohne und mit Frameworks

WebComponents: Native Komponenten fürs Web - ohne und mit Frameworks

Der aktuelle Single-Page Application und der frühere Windows-Entwickler kennt sie seit „Ewigkeiten”: UI-Komponenten, oder Controls. Durch Kapselung erhalten wir modulare und wiederverwendbare Baukastenelemente, aus denen unsere UI-Anwendungen zusammengesetzt werden. Bisher mussten wir uns im Web eines SPA Frameworks bedienen – ob Angular, ReactJS, Vue.js oder Polymer. Doch keines dieser Frameworks wollte oder konnte so recht mit dem anderen zusammenarbeiten. Dies könnte sich in Zukunft ändern mit WebComponents. Diese beschreiben anhand von Standards wie CustomElements ein Komponentenmodell für das Web und bietet damit erstmal die Möglichkeit, native Komponenten im Browser gänzlich ohne ein Framework oder aber über Framework-Grenzen hinweg einzusetzen.
In dieser Session klären Manuel Rauber und Patrick Jahr wie WebComponents funktionieren, wo die Vorteile und Nachteile liegen – und, vermutlich am spannendsten: sie gehen der Frage in gewohnt praktischer Manier nach, ob WebComponents mit aktuellen Single-Page Application (SPA) Frameworks wie Angular genutzt und erstellt werden können und welche Rolle Ivy dabei spielt. Abgerundet wird das Ganze mit einem Ausblick, wie wir künftig Web-Anwendungen mit Web Components entwickeln könnten. Auf zum nächsten Schritt!

GitHub: https://github.com/thinktecture/angular-days-2019-fall-web-components
GitHub (Chat-Demo): https://github.com/thinktecture-labs/web-components-chat

Manuel Rauber

October 08, 2019
Tweet

More Decks by Manuel Rauber

Other Decks in Programming

Transcript

  1. Web Components:
 Native Komponenten für’s Web Ohne und mit Framework

    Manuel Rauber @ManuelRauber Consultants Patrick Jahr @jahr_patrick Developer
  2. • The challenge • Web Components • What are they?

    • How to use them? • How to create them? • Take Care & a peek into the future • What about frameworks?! • Micro Frontends Agenda
  3. DRY

  4. <div id="fancyWidget">!"div> <html> <head> <script src="fancyFramework.js">!"script> !"head> <body> !"body> !"html>

    • No semantic • Intransparent • Change are difficult • Framework dependent
  5. <div id="fancyWidget"> <html> <head> <script src="fancyFramework.js">!"script> !"head> <body> !"body> !"html>

    <span id="label">!"span> <button id="incButton">+!"button> <button id="decButton"!#!"button> !"div> • Global Scoping • Naming conflicts • Styling conflicts
  6. <my-counter value="1" minimum="1" maximum="5">!"my-counter> <html> <head> <script type="module" src="my-element.js">!"script> !"head>

    <body> !"body> !"html> • Semantic • Local Scoping • Configuration • Bundle Import
  7. • Web Components are not a standard, but a collection

    of 3 technologies • Custom Elements • HTML templates • Shadow DOM • (HTML Imports) • Bring a native component model to web instead of having to use frameworks Web Components - What are they?
  8. • Create your own HTML tags • Lifecycle model (“connected”,

    “disconnected”, “attribute changed”, “adopted”) • Reusable • Decouple behavior from the document Custom Elements <my-rating value="3" stars="10">!"my-rating>
  9. • ES6 Class • Inherit from HTMLElement
 or any other

    HTML element • Need to be defined in a 
 CustomElementRegistry Custom Elements class MyRating extends HTMLElement { constructor() { super(); console.log('component constructed'); } connectedCallback() { console.log('component added to DOM'); } adoptedCallback() { console.log('component was moved in DOM'); } disconnectedCallback() { console.log('component removed from DOM'); } attributeChangedCallback(name, oldVal, newVal) { console.log('a attribute has been changed'); } } window.customElements.define('my-rating', MyRating); const myRating = document.createElement('my-rating'); document.body.appendChild(myRating); !% this moves the node! otherElement.appendChild(myRating); otherElement.removeChild(myRating);
  10. • ES6 Class • Inherit from HTMLElement
 or any other

    HTML element • Need to be defined in a 
 CustomElementRegistry Custom Elements class MyRating extends HTMLElement { constructor() { super(); console.log('component constructed'); } connectedCallback() { console.log('component added to DOM'); } adoptedCallback() { console.log('component was moved in DOM'); } disconnectedCallback() { console.log('component removed from DOM'); } attributeChangedCallback(name, oldVal, newVal) { console.log('a attribute has been changed'); } } window.customElements.define('my-rating', MyRating); const myRating = document.createElement('my-rating'); document.body.appendChild(myRating); !% this moves the node! otherElement.appendChild(myRating); otherElement.removeChild(myRating);
  11. • ES6 Class • Inherit from HTMLElement
 or any other

    HTML element • Need to be defined in a 
 CustomElementRegistry Custom Elements class MyRating extends HTMLElement { constructor() { super(); console.log('component constructed'); } connectedCallback() { console.log('component added to DOM'); } adoptedCallback() { console.log('component was moved in DOM'); } disconnectedCallback() { console.log('component removed from DOM'); } attributeChangedCallback(name, oldVal, newVal) { console.log('a attribute has been changed'); } } window.customElements.define('my-rating', MyRating); const myRating = document.createElement('my-rating'); document.body.appendChild(myRating); !% this moves the node! otherElement.appendChild(myRating); otherElement.removeChild(myRating);
  12. • ES6 Class • Inherit from HTMLElement
 or any other

    HTML element • Need to be defined in a 
 CustomElementRegistry Custom Elements class MyRating extends HTMLElement { constructor() { super(); console.log('component constructed'); } connectedCallback() { console.log('component added to DOM'); } adoptedCallback() { console.log('component was moved in DOM'); } disconnectedCallback() { console.log('component removed from DOM'); } attributeChangedCallback(name, oldVal, newVal) { console.log('a attribute has been changed'); } } window.customElements.define('my-rating', MyRating); const myRating = document.createElement('my-rating'); document.body.appendChild(myRating); !% this moves the node! otherElement.appendChild(myRating); otherElement.removeChild(myRating);
  13. • ES6 Class • Inherit from HTMLElement
 or any other

    HTML element • Need to be defined in a 
 CustomElementRegistry Custom Elements class MyRating extends HTMLElement { constructor() { super(); console.log('component constructed'); } connectedCallback() { console.log('component added to DOM'); } adoptedCallback() { console.log('component was moved in DOM'); } disconnectedCallback() { console.log('component removed from DOM'); } attributeChangedCallback(name, oldVal, newVal) { console.log('a attribute has been changed'); } } window.customElements.define('my-rating', MyRating); const myRating = document.createElement('my-rating'); document.body.appendChild(myRating); !% this moves the node! otherElement.appendChild(myRating); otherElement.removeChild(myRating);
  14. • Static getter observedAttributes
 returns all attributes which
 should be

    observed • All those attributes will trigger
 attributeChangedCallback
 upon a change • Does not execute on property
 change! Custom Elements - Observed Attributes class MyRating extends HTMLElement { constructor() { super(); } static get observedAttributes() { return [ 'value' ]; } attributeChangedCallback(name, oldVal, newVal) { if (name !!& 'value') { this.innerHTML = `Rating value ${newVal}`; } } } <my-rating value="5">!"my-rating>
  15. • HTML tag: <template> • Parsed, but not rendered •

    Instantiated via JavaScript HTML Templates <template> <header>My Image Card!"header> <img src="!!'" !( <p>Description!"p> !"template> const template = document.querySelector('template'); const templateInstance = document.importNode(template.content, true); const img = templateInstance.querySelector('img'); img.setAttribute('src', 'http:!%a-link.com/foo.png'); document.body.appendChild(templateInstance);
  16. • Encapsulates HTML and CSS • Isolated DOM: elements within

    Shadow DOM are not selectable from outside • e.g. document.querySelector() won’t return a result • Scoped CSS: CSS styles stay within Shadow DOM, no leakage, no bleed-in • Composition: declarative, markup-based API, put elements from outside in • Simplifies CSS: no conflicts with existing classes and IDs • Productivity: app consists of chunks of components instead of one big page Shadow DOM https://developers.google.com/web/fundamentals/web-components/shadowdom
  17. • Shadow host
 DOM node that the 
 shadow DOM

    is attached to • Shadow tree
 DOM tree inside shadow DOM • Shadow boundary
 Place where the shadow DOM
 ends and the regular DOM begins (“Light DOM”) • Shadow root
 Root node of the shadow tree
 Shadow DOM document shadow host Document Tree Shadow Tree shadow root Shadow boundary
  18. • attachShadow() attaches a shadow DOM to any HTML element

    • open and closed mode Shadow DOM <html> <head>!"head> <body> <div>!"div> <script> const shadowHost = document.querySelector('div'); const shadowRoot = shadowHost.attachShadow({ mode: 'open' }); shadowRoot.innerHTML = '<h1>Hello World!!"h1>'; !"script> !"body> !"html>
  19. • open allows access to the HTML element’s shadow DOM

    • closed does not allow access to the HTML element’s shadow DOM Shadow DOM !% !!' shadowHost.attachShadow({ mode: 'open' }); document.querySelector('div').shadowRoot.querySelector('h1'); !% !) [HTMLHeadingElement] !% !!' shadowHost.attachShadow({ mode: 'closed '}); document.querySelector('div').shadowRoot.querySelector('h1'); !% !) Can not read 'querySelector' of null
  20. • A (named) placeholder 
 to fill with your own

    
 markup • Basically mixes together 
 two DOM trees: 
 the shadow & light tree Shadow DOM - Slots <template> <style> :host { border: 1px solid black; display: block; } !"style> <div> <slot>!"slot> <slot name="another-slot">With Default Content!"slot> !"div> !"template> <my-element>!"my-element> !!!* No slot filled !!+ <my-element> <div>Unnamed Slot Content!"div> !"my-element> <my-element> <div>Unnamed Slot Content 2!"div> <div slot="another-slot">Another Slot Content!"div> !"my-element>
  21. • Slots emit an event, whenever its content changes: slotchange

    • Get the slotted elements via assignedNodes() • https://drafts.csswg.org/css-scoping/#slotted-pseudo Shadow DOM - Slots const slot = this.shadowRoot.querySelector('slot'); slot.addEventListener('slotchange', () !, { const assignedNodes = slot.assignedNodes(); !% Do something with assignedNodes });
  22. • Styling is done via <style>-tags • All stylings are

    local and do not overwrite styles from other shadow DOMs • No other stylings will bleed into the shadow DOM Shadow DOM - Styling <template> <style> h1 { color: red; } !"style> <h1>Hello World!!"h1> !"template> const shadowRoot = document.querySelector('div') .attachShadow({ mode: 'open' }); shadowRoot.innerHTML = '<h1>Hello World!!"h1>' + '<style>h1 { color: red; }!"style>';
  23. • Inside shadow DOM • :host, :host(), :host-context(), ::slotted() •

    Outside shadow DOM • CSS Shadow Parts: ::part() • In Discussion: ::theme() • Deprecated: >>> (“shadow piercing”) Shadow DOM - CSS Selectors
  24. • :host: Selects the shadow host element Shadow DOM -

    CSS Selectors - Inside Shadow DOM document shadow host Document Tree Shadow Tree shadow root Shadow boundary <div>!"div>
  25. • :host: Selects the shadow host element • :host(): Selects

    the shadow host element only, if it has a certain class Shadow DOM - CSS Selectors - Inside Shadow DOM document shadow host Document Tree Shadow Tree shadow root Shadow boundary <div class="my-class">!"div>
  26. • :host: Selects the shadow host element • :host(): Selects

    the shadow host element only, if it has a certain class • :host-context(): Selects the shadow host 
 element only, if the selector given as the 
 function's parameter matches the shadow 
 host's ancestor(s) in the place it sits inside 
 the DOM hierarchy Shadow DOM - CSS Selectors - Inside Shadow DOM document shadow host Document Tree Shadow Tree shadow root Shadow boundary <body class="my-host-context"> <div>!"div> !"body>
  27. • :host: Selects the shadow host element • :host(): Selects

    the shadow host element only, if it has a certain class • :host-context(): Selects the shadow host 
 element only, if the selector given as the 
 function's parameter matches the shadow 
 host's ancestor(s) in the place it sits inside 
 the DOM hierarchy • "#slotted(): Selects a slotted element 
 if it matches the selector Shadow DOM - CSS Selectors - Inside Shadow DOM document shadow host Document Tree Shadow Tree shadow root Shadow boundary <div slot="my-slot">!"div> <template> <style> !-slotted(div) {} !"style> <slot name="my-slot">!"slot> !"template>
  28. Shadow DOM - CSS Selectors - Inside Shadow DOM <template>

    <style> :host { display: block; background-color: blue; } :host(.red) { background-color: red; } :host-context(main) { background-color: yellow; } !-slotted(article) { background-color: black; } !"style> <div> <p>Before slot!"p> <slot>Default Slot content!"slot> <p>After slot!"p> !"div> !"template> <my-element>!"my-element> <my-element class="red">!"my-element> <main> <my-element>!"my-element> !"main> <my-element> <article> Article Content !"article> !"my-element>
  29. • CSS Shadow Parts (Working Draft): "#part() • Allows to

    selectively expose elements from the shadow tree to the outside page for styling purposes • https://www.w3.org/TR/css-shadow-parts-1/ • CSS Shadow Parts (Unofficial Draft): "#theme() • Matches all given parts, no matter how deeply nested they are • http://tabatkins.github.io/specs/css-shadow-parts/#part-theme Shadow DOM - CSS Selectors - Outside Shadow DOM
  30. • Allows to selectively expose
 elements from the shadow tree

    to 
 the outside page for styling 
 purposes Shadow DOM - CSS Selectors - Outside Shadow DOM - ::part <template> <style> header { background-color: crimson; } !"style> <div> <header part="my-header">Style me!"header> !"div> !"template> <style> my-element.styled!-part(my-header) { background-color: black; color: white; } !"style> <my-element>!"my-element> <my-element class="styled">!"my-element>
  31. • If a nested Web Component 
 will expose an

    inner Web 
 Component’s part, it has to 
 use exportparts=“part1 part2 …”
 instead of part=“name” Shadow DOM - CSS Selectors - Outside Shadow DOM - ::part <style> my-element!-part(textspan) { color: red; } !"style> <template id="my-element-outer-template"> <my-element-inner exportparts="innerspan textspan”> !"my-element-inner> !"template> <template id="my-element-inner-template"> <span part="innerspan">Innerspan!"span> <span part="textspan">Textspan!"span> !"template> <my-element>!"my-element>
  32. • W3C: “A collection of interesting ideas” • Currently, styling

    must be specified in each shadow DOM • As we know, current web pages have hundreds and thousands CSS styles • Long parsing time and memory costs! • But most Web Components (or at least within a component library) will use the same/shared stylings • However, component libraries may update CSS during runtime, which may thwart style sheet sharing • API allows to create, remove, update, replace style sheet rules Constructable Stylesheets - Motivation
  33. Constructable Stylesheets - Demo Code const myElementSheet = new CSSStyleSheet();

    class MyElement extends HTMLElement { constructor() { super(); const shadowRoot = this.attachShadow({ mode: "open" }); shadowRoot.adoptedStyleSheets = [myElementSheet]; } connectedCallback() { !% Only actually parse the stylesheet when the first instance is connected. if (myElementSheet.cssRules.length !. 0) { myElementSheet.replaceSync(styleText); } } } https://wicg.github.io/construct-stylesheets/
  34. • ES6 Import • Via Code • Deprecated: HTML Import

    Importing Web Components <link rel="import" href="my-element.html"> <script type="module" src="my-element.js">!"script> !% static import import './my-element.js'; !% dynamic import import('./my-element.js') .then(!!'); !% or lazy-loading via const script = document.createElement('script'); script.src = './my-element.js'; document.body.appendChild(script); const myElement = document.createElement('my-element'); document.body.appendChild(myElement);
  35. • Window global registry for all custom elements • API

    • define(): defines a new element • get(): returns the constructor of an element (or undefined) • whenDefined(): returns a promise which is fulfilled, when the custom element is available • upgrade(): upgrades an element before it is connected to its shadow root CustomElementRegistry
  36. Versioning My library A My library B 3rd party library

    1.0 3rd party library 2.0 Application <my-web-component /> <my-web-component /> defines defines Uncaught DOMException: Failed to execute ‘define’ on ‘CustomElementRegistry’: the name “my-web-component” has already been used with this registry.
  37. • Problem: Custom Element Registry is a window global object

    • Possible solution currently discussed by W3C • Scoped Custom Element Registry • https://github.com/w3c/webcomponents/issues/716 • Current usable solutions • Version your HTML tags • The host application is the only one loading WCs, even 3rd-party, but this could break other Web Components expecting a certain version Versioning
  38. • If you wish to sync them, then you’ll need

    to sync them! • “Reflecting properties to attributes” or “Reflected DOM Attributes” • Helps to keep DOM representation in sync with its JavaScript state • Watch out! Properties can hold objects, numbers etc. Attributes only strings • Attributes vs Properties input = document.createElement('input'); console.log(input.getAttribute('value')); !% null input.value = 'one'; console.log(input.getAttribute('value')); !% null input.setAttribute('value', 'two'); console.log(input.value); !% one
  39. • Shadow DOM encapsulates the DOM only • JavaScript is

    still global scoped • Use common techniques for scoping JavaScript • May use the global scope to your advantage for app-wide configurations • But global polyfills can cause problems (e.g. if you load zone.js twice) JavaScript is still global scoped
  40. • Easy things may require more code • Attributes vs

    Properties • addEventListener etc. • Bigger component libraries need a strong foundation • Think of when and how to render your component • There is no virtual DOM like in React or Angular • You may need to think of partial DOM updates for bigger components Boilerplate & render loops
  41. • There is no built-in template engine, you may be

    used to from SPA frameworks like Angular, React, Vue etc. • JavaScript template strings are preferred for better readability • Can get complex and unreadable when conditions are used heavily No built-in template engine
  42. No built-in template engine - Lit Element example get previewTemplate()

    { return html` <div @click="${this.clickHandler}" class="preview"> ${this.preview.image ? html`<img src="${this.preview.image}" alt="Preview" !(` : html`` } <div> <header>${this.preview.title}!"header> ${this.preview.description ? html`<p>${this.preview.description}!"p>` : html`` } !"div> !"div> `; }
  43. • Currently there is no auto completion support for Web

    Components • Stencil tries to solve this by generating TypeScript Definition files • VS Code & W3C are discussing possible solutions • https://github.com/w3c/webcomponents/issues/776 • Stencil is currently generating a possible solution with its compiler Editor support // type definitions
  44. • There is no standard build process • You can

    use any tool you want, e.g. webpack, parcel, etc. • Be aware: • CSS post processing • Asset management Build process // custom asset management
  45. • Forms and Web Components don’t play well yet •

    FormData is not populated • <button type=“submit”> does not work • Form Participation API aka “Form Attached Controls” is discussion • https://github.com/w3c/webcomponents/issues/815 • https://github.com/w3c/webcomponents/issues/814 • https://github.com/github/image-crop-element/pull/15 • https://github.com/axa-ch/patterns-library/issues/1258 Form Participation API
  46. • Elements can not escape their stacking context • Highly

    needed for (modal) dialogs, tooltips, right-click menus, select, … • Current <dialog> element does not fit for all use cases • Possible workaround: API in the shell Stacking UI <body> <my-wc-a style="width: 100px; height: 100px; position: absolute; transform: rotate(90deg)"> !!!* Will be transformed, but should be a top layer modal dialog !!+ <my-modal-dialog>!"my-modal-dialog> !"my-wc-a> <my-wc-b>!"my-wc-b> !"body>
  47. • Already good possibilities to create framework-independent components • Decide

    between view-only and data-controlling Web Components • Keep in mind, what is not working yet, but will come in the future • Leverage the shadow DOM to encapsulate your Web Components • Think of your Web Components API! Web Components - Summary
  48. • Web Components are an uprising good alternative to build

    frontends without frameworks involved • But! A lot of stuff is done easier with frameworks, e.g. Form Validation, State Handling, Data Management, CSS post processing • Why not wrap framework components into Web Components? • Angular: Angular Elements, @angular/elements • React: Wrap manually or use a community wrapper • Vue: Vue Web Components, @vue/web-components What about frameworks?!
  49. • Bundle sizes per framework is quite high for small

    components • Possible long loading and parsing times • A solution could be: • Bundle Web Components without their framework • Load the framework in the hosting application • Watch out! All Web Components have to use the same framework version • Will get better over time with less and less framework involved • or frameworks compile to much smaller bundle sizes (e.g. Angular Ivy) What about frameworks?! - Bundle Sizes
  50. • Angular Ivy could be used for creating Web Components

    • … as long as you don’t consume other Web Components! • Ivy will complain about other Web Components not knowing about properties and throw an exception during compilation time
 
 
 • Be aware, that Ivy is not yet production ready! • Think of removing zone.js and handle everything OnPush What about frameworks?! - Angular Property ‘propertyName' does not exist on type 'HTMLElement'.
  51. • Web Component != Micro Frontend • A Micro Frontend

    can be built by multiple Web Components • The whole Micro Frontend could be wrapped into a Web Component • Micro Frontend(s) are wrapped into a (SPA) shell Micro Frontends https://example.com Micro Frontend Dashboard (SPA) Shell
  52. • Eager Loading • Server-side composition • Build-time integration •

    Lazy Loading • iFrames • JavaScript • Web Components Micro Frontends - Composition
  53. • “Does this approach fit my company structure?” • SPA-Shell

    API depending on the composition approach • Data exchange / Data API • Routing • A single Micro Frontend routing is easy • A multi Micro Frontend routing is complex • Let the user F5 your application! • Routing is also a possibility for data exchange Micro Frontends - Challenges
  54. • Interesting possibility to have small customized UIs per feature

    • Think of how you’ll compose Micro Frontends (Micro Frontend Architecture) • Think of data exchange in Micro Frontends (if needed) • Think of single or multi routing possibilities (let the user F5 your app!) • Good for decoupling teams! Micro Frontends - Summary
  55. • Web Components help in giving a semantic structure in

    apps • They encapsulate HTML and CSS • Improves maintainability and reusability • Take care for stuff not working yet (e.g. Form Participation API) • SPA Frameworks can help in building Web Components • … with the odd of having bigger bundles • Micro Frontends help decouple teams Web Components - Summary
  56. And… now? • Try it out! Fiddle around with Web

    Components • Use them! Include first small Web Components into your application • Be prepared! Keep up to date with the development of Web Components • Slides: https://speakerdeck.com/manuelrauber • Repository: https://github.com/thinktecture/angular-days-2019-fall-web-components • Contact: [email protected], [email protected]