11

I like the simplicity of hyperHtml and lit-html that use 'Tagged Template Literals' to only update the 'variable parts' of the template. Simple javascript and no need for virtual DOM code and the recommended immutable state.

I would like to try using custom elements with hyperHtml as simple as possible with support of the <slot/> principle in the templates, but without Shadow DOM. If I understand it right, slots are only possible with Shadow DOM?

Is there a way or workaround to have the <slot/> principle in hyperHTML without using Shadow DOM?

    <my-popup>
      <h1>Title</h1>
      <my-button>Close<my-button>
    </my-popup>

Although there are benefits, some reasons I prefer not to use Shadow DOM:

  • I want to see if I can convert my existing SPA: all required CSS styling lives now in SASS files and is compiled to 1 CSS file. Using global CSS inside Shadow DOM components is not easily possible and I prefer not to unravel the SASS (now)
  • Shadow DOM has some performance cost
  • I don't want the large Shadow DOM polyfill to have slots (webcomponents-lite.js: 84KB - unminified)
Nolo
  • 846
  • 9
  • 19
Jo VdB
  • 2,016
  • 18
  • 16

3 Answers3

7

Let me start describing what are slots and what problem these solve.

Just Parked Data

Having slots in your layout is the HTML attempt to let you park some data within the layout, and address it later on through JavaScript.

You don't even need Shadow DOM to use slots, you just need a template with named slots that will put values in place.

    <user-data>
      <img  src="..." slot="avatar">
      <span slot="nick-name">...</span>
      <span slot="full-name">...</span>
    </user-data>

Can you spot the difference between that component and the following JavaScript ?

    const userData = {
      avatar: '...',
      nickName: '...',
      fullName: '...'
    };

In other words, with a function like the following one we can already convert slots into useful data addressed by properties.

    function slotsAsData(parent) {
      const data = {};
      parent.querySelectorAll('[slot]').forEach(el => {
        // convert 'nick-name' into 'nickName' for easy JS access
        // set the *DOM node* as data property value
        data[el.getAttribute('slot').replace(
          /-(\w)/g,
          ($0, $1) => $1.toUpperCase())
        ] = el; // <- this is a DOM node, not a string ;-)
      });
      return data;
    }

Slots as hyperHTML interpolations

Now that we have a way to address slots, all we need is a way to place these inside our layout.

Theoretically, we don't need Custom Elements to make it possible.

    document.querySelectorAll('user-data').forEach(el => {
      // retrieve slots as data
      const data = slotsAsData(el);
      // place data within a more complex template
      hyperHTML.bind(el)`
        <div class="user">
          <div class="avatar">
            ${data.avatar}
          </div>
          ${data.nickName}
          ${data.fullName}
        </div>`;
    });

However, if we'd like to use Shadow DOM to keep styles and node safe from undesired page / 3rd parts pollution, we can do it as shown in this Code Pen example based on Custom Elements.

As you can see, the only needed API is the attachShadow one and there is a super lightweight polyfill for just that that weights 1.6K min-zipped.

Last, but not least, you could use slots inside hyperHTML template literals and let the browser do the transformation, but that would need heavier polyfills and I would not recommend it in production, specially when there are better and lighter alternatives as shown in here.

I hope this answer helped you.

Nolo
  • 846
  • 9
  • 19
Andrea Giammarchi
  • 3,038
  • 15
  • 25
  • There's a typo in the last code example (unnecessary ',' on the first line) – ZogStriP Feb 12 '18 at 09:17
  • Thanks, It feels hacky with the regex, but that's what I asked, a workaround... I will do some experiments with this and see if it feels right. – Jo VdB Feb 12 '18 at 10:02
  • 1
    the regex is there only to make `nick-name` accessible as `nickName`. You don't need it for anything else. If you don't write slot names with dashes you can just set `data[el.getAttribute('slot')] = el` and that's it. You are setting DOM nodes, not changing HTML as string ... I hope this is clear. – Andrea Giammarchi Feb 12 '18 at 10:09
  • @AndreaGiammarchi: I did a small proof of concept and it doesn't feel as dirty as I first thought, it looks promising :-) https://codepen.io/jovdb/pen/vdZmEz In my test the 'smiley-button' only supports 1 slot, so it is a bit clumsy to specify the named slot for each button. It would be nice if it could work with an "unnamed" (default) slot. Any ideas to implement that? I couldn't get that to work, after a second render the original content is already overridden and gone. Do I need to remember the original content the first time as a member variable and use it at rerender? – Jo VdB Feb 12 '18 at 23:18
  • I think the next answer from @mrpix solved this problem for me. – Jo VdB Feb 12 '18 at 23:47
  • FYI: I adjusted my proof of concept with the solution of @mrpix https://codepen.io/jovdb/pen/ddRZKo It looks much better now! All suggestions/remarks welcome... – Jo VdB Feb 13 '18 at 00:10
2

I have a similar approach, i created a base element (from HyperElement) that check the children elements inside a custom element in the constructor, if the element doesn't have a slot attribute im just sending them to default slot

    import hyperHTML from 'hyperhtml/esm';

    class HbsBase extends HyperElement {
        constructor(self) {
            self = super(self);
            self._checkSlots();
        }
        _checkSlots() {
            const slots = this.children;
            this.slots = {
                default: []
            };
            if (slots.length > 0) {
                [...slots].map((slot) => {
                     const to = slot.getAttribute ? slot.getAttribute('slot') : null;
                    if (!to) {
                         this.slots.default.push(slot);
                    } else {
                        this.slots[to] = slot;
                    }
                 })
            }
        }
    }

custom element, im using a custom rollup plugin to load the templates

    import template from './customElement.hyper.html';
    class CustomElement extends HbsBase {
        render() {
            template(this.html, this, hyperHTML);
        }
    }

Then on the template customElement.hyper.html

    <div>
         ${model.slots.body}
    </div>

Using the element

    <custom-element>
       <div slot="body">
         <div class="row">
             <div class="col-sm-6">
                 <label for="" class="">Name</label>
                 <p>
                      <a href="#">${model.firstName} ${model.middleInitial}      ${model.lastName}</a>
                 </p>
             </div>
         </div>
         ...
       </div>
    </custom-element>
Nolo
  • 846
  • 9
  • 19
mrpix
  • 61
  • 4
0

Slots without shadow dom are supported by multiple utilities and frameworks. Stencil enables using without shadow DOM enabled. slotted-element gives support without framework.

Sasha Firsov
  • 699
  • 8
  • 9