Shadowed Components

Shadowed Components

What is a Shadow DOM (shadow tree)

According to Mozilla Developer Network (MDN), a shadow tree is a tree of DOM nodes whose topmost node is a shadow root; that is, the topmost node within a shadow DOM. A shadow tree is a hidden set of standard DOM nodes which is attached to a standard DOM node that serves as a host. The hidden nodes are not directly visible using regular DOM functionality, but require the use of a special Shadow DOM API to access.

Since the latest version of QCObjects you can make special components called shadowed components.

What is a Shadowed Component

A shadowed component is a QCObjects Component definition that you can extend to allow the content of your component to be shadowed in the browser. By the moment, QCObjects is only supporting the mode "open" for shadowed components. If you're not familiar yet with the concept of shadow root, shadow DOM or shadow tree, I highly recommend you to read this article of MDN about Using the Shadow DOM.

Differences between a Shadowed Component and a Lightning Component

A Lightning Component is no other thing that the same component that you're actually familiar with.

Screen Shot 2020-04-16 at 9.46.28 PM.png

On the image above, the component has a normal content, called a light DOM. This content is following the same CSS rules and scope that the rest of the document, and as a consequence, the rest of the web app. When you inject a style css rule into the content of this component, this rule will be added in runtime to the document, but it will also override any other rule that maches a conflict. This is useful most of the times, but a mess in particular others.

For that particular times when you need to enclose the CSS and markup scope you got to use shadow DOM for content injection in a component.

Shadow DOM is local to the component and defines its internal structure, scoped CSS, and encapsulates your implementation details. It can also define how to render markup that's authored by the consumer of your component.

Screen Shot 2020-04-17 at 6.25.42 PM.png

The above image is showing a card component that is filled up with shadowed content.

To make it easy to handle when and where to put the shadow content, QCObjects has Shadowed Components.

Creating Shadowed Component

You have two ways to make a shadowed component in QCObjects.

The first one, is to declare a component Class definition and use it in the component tag declaration.

// in your components package
  Class('ShadowedCardComponent',Component,{
    name:'card',
     /* If shadowed is true, the DOM will be shadowed, default is false */
    shadowed:true, 
    cached:false,
    data:{}
  })

In the above definition, you can use the hidden static property shadowed to let QCObjects to know it has to shadow the DOM of the component body.

Then, you are able to use this definition into your html component tag, like this:

<!-- in your html layout file -->
<component componentClass="ShadowedCardComponent" ></component>

If you don't want to spend a special Class definition for your component, the second option is to set an attribute straight in the component tag declaration.

<component name="card" shadowed="true"></component>

Both ways get the same result, the component is built with shadowed DOM, and the styles into it will not follow the global css rules, and the markup inside will have an independent structure. Make sure your shadowed components are only used when needed, and you are not needing to depend on any other lighting component content to make your shadowed components visible and available, as they are completely independent. Also, it is not recommended to use lighting subcomponents into shadowed components, as the purpose of lighting components is to be absolutely available to be injected everywhere and to be interfered by external behaviours, and it is not the same purpose for shadowed components, whose are meant to be restricting the global behaviour in some ways. Both, lighting components and shadowed components are accesible from the global components stack, the main difference is that in the instance of every shadowed component will be a new hidden property (readonly) called shadowedRoot, that is replacing the behaviour of the body, now, if you have any elements to add to the component content, you need to use componentInstance.shadowRoot.appendChild(element) instead of componentInstance.body.append(element)

Composition and slots

In simple and easy words, Composition is when you dynamically place elements of an object instance into blocks defined on its inherited instance. The resulting composed object instance will be mutating its behaviour as a result of the dynamic blocks mixing.

The shadow DOM is a hidden tree, that is allocated in a private scope, and it is populating its content to the light DOM, that is visible and accesible from the global scope. You can place light DOM elements over the shadow DOM elements, mutating the behaviour of the final flattened tree. This is called shadow DOM composition.

Light DOM vs Shadow DOM

The flattened DOM tree is the content that you can view opening dev tools in your browser.

The flattened DOM tree of a Light DOM inside a QCObjects component will be like this:

<component name="card" >
<!--  this content is generated by templates/components/card.tpl.html file -->
   <div class="card">
       <img src="img/avatar.png" alt="Avatar" style="width:100%">
       <div class="container">
       <h4><b>Card Title</b></h4>
       <p>Card Description</p>
       </div>
   </div>
</component>

On the other hand, a flattened DOM tree of a shadowed component with the exact same template content will be like this:

<component name="card" shadowed="true">
<!--  this content is generated by templates/components/card.tpl.html file -->
   <div class="shadowHost">
     #shadow-root
      <div class="card">
          <img src="img/avatar.png" alt="Avatar" style="width:100%">
          <div class="container">
          <h4><b>Card Title</b></h4>
          <p>Card Description</p>
          </div>
       </div>
   </div>
</component>

In this case, a light <div> component was added dynamically to enclose the shadow DOM, this is always done by QCObjects in order to asure there will be no restrictions by the HTML spec over the different browsers that support shadow DOM.

The <slot> element

Following the rules of the shadow DOM composition, we can go further defining some slots (a good additional example of the use of slots is in the MDN here).

<!-- file: templates/components/card.tpl.html -->
      <div class="card">
          <img src="img/avatar.png" alt="Avatar" style="width:100%">
          <div class="container">
          <h4><slot name="card_title"></slot></h4>
          <slot name="card_description"></slot>
          </div>
       </div>

And Omg! this is becoming a bit more interesting...

<!-- file: main-layout.html or wherever you want to place your component -->
<component name="card" shadowed="true">
   <b slot="card_title">Card Title</b>
   <p slot="card_description">Card Description</p>
</component>

In a shadowed component, you can override the content of the slot elements using custom blocks.

And the resulting flattened DOM tree will be like this:

<!-- file: main-layout.html or wherever you want to place your component -->
<component name="card" shadowed="true">
<!--  this content is generated by templates/components/card.tpl.html file -->
   <div class="shadowHost">
     #shadow-root
      <div class="card">
          <img src="img/avatar.png" alt="Avatar" style="width:100%">
          <div class="container">
          <h4>
              <b slot="card_title">Card Title</b>
          </h4>
          <p slot="card_description">Card Description</p>
          </div>
       </div>
   </div>
</component>

And yes, you should be able to use data injectors too:

<!-- file: main-layout.html or wherever you want to place your component -->
<component name="card" shadowed="true">
   <b slot="card_title">{{title}}</b>
   <p slot="card_description">{{description}}</p>
</component>

Styling

In-line context based styles for shadowed components

For shadowed components, it is recommended to place your style rules into the dynamic template file, commonly named with the extension tpl.html, so the styles will be enabled only for the scope of the shadowed component. You can reference the main element of the shadowed DOM by using :host element in your style declaration, like this:

<!-- file: templates/components/card.tpl.html -->
<style>
:host {
  display: block; 
}

/* Also you can import styles from another css file */
@import url("./css/components/card.css");
</style>

      <div class="card">
          <img src="img/avatar.png" alt="Avatar" style="width:100%">
          <div class="container">
          <h4><slot name="card_title"></slot></h4>
          <slot name="card_description"></slot>
          </div>
       </div>

Enjoy coding!