In Part 3 of this tutorial, we start by revisiting our <Component> and <Style> definitions in learning about Shadow DOM, slots, and the corresponding CSS isolation modes. We'll learn how to use Template Tags, to optionally include various salient (or non-salient) portions of our HTML templates, to create things like accordian controls, or modals that toggle their visibility.

Slots

Introduction to shadow DOM, slots, and CSS isolation

This section introduces 3 important terms: shadow DOM, slots, and CSS isolation. These are used here to describe the way Modulo exposes these built-in browser features. This section is also not needed for some Modulo users, so if you are in a hurry and don't think more fine-grained CSS and HTML composition tools are important to you right now, feel free to skip down to Template Tags section, and come back when you hit a wall with isolating CSS or following best practices with Component design.

As a general note, it's important to know that Modulo attempts to immitate and extend the browser standard, rather than work against it. That means that "slots" work more or less the same as the browser's built-in concept of slots, but Modulo makes it easier to use and declare them without use of additional JavaScript, and also lets them get applied to components that don't use shadow DOM.

Putting elements into slots

The origin of slots Slots are a feature of the "shadow DOM", a new-ish feature of web browsers. Modulo replicates the same behavior of these "shadow" slots, making slots even easier to use without JavaScript.

Let's get back to a basic feature. We've learned about Props, which enable us to "receive" information from the HTML page (or a parent component) by adding that information as attributes. We've gone this far without addressing how to make our components handle HTML within them, or contained as children.

As we explored when talking about Props, components are "define once, re-use many times" bits of code. Often, it's useful for a component to "wrap around" other HTML, or contain other HTML. For example, a stylized button component might wrap around text, or a stylish "modal dialog" component would wrap around paragraphs and other arbitrary HTML content. You can thus imagine the button or modal in these examples as having "empty spots" or slots where this content would go. That is to say, the component is a sort of "empty picture fancy", and the web developer using the component would be able to "slide content" into the frame's empty slot.

Slots allow there to be "empty spots" in your HTML that proceed to get filled by arbitrary content supplied when your component is used. Adding a slot is as simple as including a <slot></slot> HTML element in your component's Template definition. To fill up a slot with HTML content, add the content between the opening and closing tags of your HTML element.

Here's an example of both, has a slot inside a div:

<!-- COMPONENT TEMPLATE: --> <div class="fancy-frame"> <slot></slot> </div> <!-- USAGE: --> <x-PictureFrame> <p>My cat:</p> <img src="./cat.jpg" /> </x-PictureFrame>

When the component is rendered on the page, the slot gets filled with the img tag and a p tag:

<x-PictureFrame> <div class="fancy-frame"> <slot> <p>My cat:</p> <img src="./cat.jpg" /> </slot> </div> </x-PictureFrame>

Ergonomics - When designing components, and making decisions like using "Props" vs "slots", keep in mind who is going to use the component. If it's yourself, what is easy for you? If months from now, after you forget everything, what code would you be able to understand more simpily? If it's a team member, what is easiest for them? If it's an open source project, what would people who have used similar projects expect?

Why not just use Props?

Sometimes Props and slots are both options. For example, rewriting the above:

<!-- COMPONENT TEMPLATE: --> <div class="fancy-frame"> {{ props.content|safe }} </div> <!-- USAGE: --> <x-PictureFrame content=" <p>My cat:</p> <img src='./cat.jpg' /> "></x-PictureFrame>

This could be a good solution for some things. In some cases, it may behave identically, or be desirable for it's simplicity. However, in other cases, CSS will behave differently (see next section on Shadow DOM), and in other cases, it simply saves time for future users of your components (including yourself!) to put content "within" an element, since it makes it behave and "feel" more like other HTML elements, and you don't have to worry about syntax issues like nesting quotes.

Slot features

Read the following list for a summary of the features and exact behavior of slots. Modulo's behavior is intended to mirror the HTML spec as closely as possibly:

  • Slots - placeholder elements like <slot></slot>
  • Filling slots - when the element renders, it moves its original children into its slots
  • Defaults - Specify default content inside of slot e.g.: <slot><em>None</em></slot>
  • Multiple slots / named slots - slots can get a name attribute, e.g. following HTML spec: <slot name="artbody"></slot>
  • Filling named slots - Slots are filled with slot attribute in children, e.g. following HTML spec: <div slot="artbody">...</div>
  • Slots accumulate - They do not "replace", so you can do slot= multiple times
Try it now

Examine the following two examples of slots and then follow the local file challenge below.

Example #1: Slots and Props
<Props image ></Props> <Template> <div class="fancy-frame"> <img src="{{ props.image }}" /> <slot><em>No caption provided.</em></slot> </div> </Template> <Style> :host { text-align: center; } img { height: 50px; width: 50px; } .fancy-frame { display: inline-block; border: 10px inset salmon; padding: 10px; margin: 10px; width: 100px; background: pink; } </Style>

Caption: The Return of the Hippo

  • Hint: To better examine the above sample code, try flipping to "USAGE" editing mode (go to MENU button above RUN, then toggle EDIT USAGE). Do you see the caption is the "content" of the tag? What happens when you don't specify any content (i.e. what's the "default")?
Example #2: Named Slots
<Props image ></Props> <Template> <div class="fancy-frame"> <slot name="pic"> <em>No Image Provided</em> </slot> <slot><em>No caption provided.</em></slot> </div> </Template> <Style> :host { text-align: center; } .fancy-frame { display: inline-block; border: 10px inset salmon; padding: 10px; margin: 10px; width: 100px; background: pink; } </Style>

Caption: The Return of the Hippo

  • Hint: To better examine the above sample code, try flipping to "USAGE" editing mode (go to MENU button above RUN, then toggle EDIT USAGE). Do you see how there are multiple slots in use this time? How did we specify a slot for the img?

  • Bonus Question: Why is width and height "hard-coded" into a style= attribute in this second example?

    • CSS in the Component does not affect elements in the slots. The very next section of this tutorial is on Shadow DOM, which will explain why.
Try it now
  1. Local file challenge: Edit one of your components to have a slot by adding an empty <slot></slot> placeholder element in your HTML.
  2. Edit your usage to include text or other HTML tags between your opening and closing tags to "fill" the slot
  3. Extra: Practice the other features of slot, such as using named slots.

CSS Isolation

Intro to render mode

Yet another feature of Modulo that is useful to know is component render mode. Each component render mode selects a different CSS isolation. It will also change DOM behavior, so have even more tools to write powerful, ergonomic, and efficient components.

CSS Leakage

Now that we have covered the ways for components to receive children, it's important to think about how CSS gets isolated, since we start to run into pitfalls where "parent" CSS "leaks" into "child" CSS. As was established in the first part, <Style> components "isolate" themselves by "prefixing" all CSS with the component name.

Default isolation (regular)

For an example, examine the following two snippets of code:

p { font-size: 1.3rem; } em.special { color: blue; } /* ..."prefixing" causes it to turn into... */ x-MyComponent p { font-size: 1.3rem; } x-MyComponent em.special { color: blue; }

This ensures that the p tags selected will only be contained within x-MyComponent. This prevents CSS from "leaking" into other components, and makes websites much easier to maintain, without causing CSS classes to get messy and convoluted. This brings us to the default mode for Component definitions in Modulo: "regular" mode.

Details

Component will render to the screen, CSS will be prefixed as described above. This mode is often what web designers expect when writing CSS: They want it to be "silo'ed", but not too strictly. That is why it is the default.

In general, it's not that often you need to specify anything other than the default rendering mode, but when you do need to use them, they can be super handy in fixing CSS bugs or unruly third party CSS or scripts, so let's review them now.

Beyond "regular"

There is still a lot of potential for leakage. For example, external, global styles can "leak" into the component. Similarly, the component's styles can "leak" into it's children, e.g. children created with slots or Props. Often both of these are "good leakage". But sometimes, it's undesirable, and you want more fine-graned isolation.

This is one of the uses of Component render modes, and their accompanying Style isolation modes: You can isolate your CSS with one of 2 techniques: class-based ("vanish") and shadow-based isolation. They behave differently and will result in radically different HTML and CSS being generated.

mode="vanish"

How does vanish work?- How can CSS still select, when the component "vanishes" from the DOM? Well, the "secret to the vanishing act" is that after that first render, the Style CPart attaches the class to all sub-elements that are referenced in CSS.

When <Component mode="vanish"> is activated, then the Style CPart switch to isolate="class", or class-based isolation. This behaves similarly to prefix based, but ends up having a few important differences. First, and most importantly, mode="vanish" will cause the Component to vanish after rendering, or remove itself from the DOM. This means that it cannot ever "rerender": Vanishing components are one-time things, and if the parent component rerenders, the old vanishing component will be destroyed and rebuilt again.

Second, class-based isolation will rewrite your CSS like the following:

div > h1 { font-size: 2.3rem; } a.special:hover { color: orange; } /* ..."vanish + class isolation" causes it to turn into... */ .MyComponent:is(div > h1) { font-size: 2.3rem; } .MyComponent:is(a.special:hover) { color: orange; }

This CSS will not affect children, but the CSS will "stop" at the immediately rendered template. Thus, "vanish" and class-based isolation isolates in one direction: Parent to children. Another big difference is since mode="vanish" removes the Component :host selector changes behavior. Instead, it will match all children, so you can still use it for CSS variables.

mode="shadow"

When <Component mode="shadow"> is activated, then the component switches to using a somewhat new feature of HTML: the Shadow DOM. This dramatically named feature allows a Web Component to create a protected or hidden DOM fragment (e.g. portion of an HTML page) that can be manipulated, while "protecting it" from outside CSS. Like vanish, this will dramatically change how the component renders, since the browser will treat it quite differently, and all of it's children will be "removed" from the DOM in the hidden Shadow DOM, making it appear "empty" when inspected (but with more clicking, you can find the "shadow root" where the hidden content is).

When a component is in shadow mode, and you launch your site or build your site, it's CSS will NOT be put into the main CSS file. That means that it will be stored in the JavaScript file and only added whenever that component appears on the screen.

Furthermore, due to the what the browser works, the CSS will be fully isolated in both directions: Outside CSS won't affect your component's children, and inner CSS is incapable of affecting anythign else, and won't even be added until the component gets on the screen. Since it's fully isolated, there is no need to prefix, so CSS will be inserted as is.

Try it now
  1. Examine the following demo of a "Shadow DOM" slot-based component, and then answer the questions below.
<Props image ></Props> <Template> <div class="fancy-frame"> <slot name="pic"> <em>No Image Provided</em> </slot> <slot><em>No caption provided.</em></slot> </div> </Template> <Style> /* Why does it not work?? */ img { height: 50px; width: 50px; } :host { text-align: center; } .fancy-frame { display: inline-block; border: 10px inset salmon; padding: 10px; margin: 10px; width: 100px; background: pink; } slot:first-child { /* slots can be styled like any HTML element */ overflow: hidden; max-height: 50px; display: block; } </Style>

Caption: The Image TOO BIG

  1. Local file challenge: Try each of the different component modes and CSS isolation features on your local file.
Comprehension Questions
  • Why does the width and height in the CSS not apply to the img?

    • ANSWER: CSS in the Component does not affect elements in the slots.
    • Explanation: The component is using "shadow DOM" to make it ONLY apply to stuff within the component, but not in it's slots. You can confirm this by going into "Inspect", and seeing how the img tag is indeed outside of the shadowRoot. By default, when a component is in shadow mode, it will only apply it's CSS to it's "own" elements (e.g. not slotted).
  • How is this useful?

    • ANSWER: You can write CSS that only affects what's in the component, making it better isolated
    • Explanation: This is another important difference between slots and using HTML strings (e.g. via Props): Slots are easier to isolate with CSS.
  • How does the default isolation mode (prefix isolation) affect this?

    • ANSWER: If you don't specify mode=shadow or mode=vanish, the default behavior will apply it's CSS to slot content! Often that is desired, in fact (e.g. a modal component that neatly styles it's content)
    • Explanation: Only in shadow (what's used by the modulo website) and class (vanish) isolation modes, it will isolate it's slots.

Template Tags

Built-in Template Tags and Custom Template Tags - Modulo comes with many template-tags pre-installed: Read Templating Reference - Built-In Tags for examples of all template-tags available. Utilizing JavaScript, you can also define custom tags, although it is more challenging than with filters.

In addition to filters, the Modulo templating language also support powerful "template tags", which allow for more complicated custom behavior. This includes the next two topics in this tutorial: "if" template-tag, which allows for conditional rendering (e.g. "only show the submit button if a form is filled correctly"), and the "for" template-tag, which allows for HTML to be repeated for each item of some given data (e.g. "every blog post get's it's own <li> element").

Syntax

Unlike template variables or filters, they use {% and %} (instead of {{ and }}) to designate where they are in the Template code.

Template tags are in the format of {% tag %}. They allow for more complicated transformations to the HTML code generated. For example, here are a few:

{% include other_template %} {% comment %} ... {% endcomment %} {% if state.expanded %} ... {% endif %}

The if-tag

One of the most useful template tags you will use is the if tag, written like this: {% if %}. The if-tag allows for conditional rendering of HTML code based on the condition supplied.

See below for two examples of using the "if" tag:

<h1>{{ props.title }}</h1> {% if state.expanded %} <div><p>Details: {{ props.details }}</p></div> {% endif %} <hr /> {% if props.link %} <a href="{{ props.link }}">Read more...</a> {% else %} <em>(No link provided)</em> {% endif %}

Note that there are two other tags in the mix. These a related to the if tag, and will only show up after the if tag. The first is the {% endif %} tag. This is required, since it shows Modulo what you want the if tag to conditionally include. The second is the {% else %} tag. This is optional: You don't need it for all uses of the if tag.

Try It Now
  1. Examine the code below. Do you see how the show checkbox toggles the State value, which in turn toggles the visibility of the second div?
<Template> <div> <label> <input [state.bind] name="show" type="checkbox" /> Show details </label> </div> {% if state.show %} <div>Look, more stuff here!</div> {% endif %} </Template> <State show:=false ></State> <Style> div { border: 1px black solid; padding: 3px; margin: 3px; } </Style>
  1. Examine the code below. Do you understand how the if template-tag checks the value of state.word to see if it is filled in, and only shows it back to the user when the user has started typing?
<Template> <div> <label>Enter a word to see it reversed: <input [state.bind] name="word" /></label> {% if state.word %} <div><p>Reversed: <tt>{{ state.word|reversed }}</tt></div> {% else %} <p><em>Nothing entered!</em></p> {% endif %} </div> </Template> <State word="" ></State>
  1. Local file challenge: Try writing your own if-statements based on other state variables of your creations.

Conditional operators

Let's finish off this part of the tutorial with a quick tour of the assortment of conditional "if operators". Conditional operators let us check for more complicated conditions in if statements. There are many operators available, but we'll go over just a few popular ones: is (check for equality), in (check for , gt (greater than) and lt (less than).

Operators for strings

Equality: is

<Template> <h3>Super secret stuff</h3> <input placeholder="enter password" [state.bind] name="password" /> {% if state.password is "hunter2" %} <h1>ur in, t1me for hax0ring</h1> <p><a href="#">SECRET LINK</a></p> {% endif %} </Template> <State password="" ></State>

Just for clarity, this is a coding example, but this is not a secure password system. Anyone can learn Modulo and JavaScript and "hack it" by using the same skills you are learning now. So, don't use this for something that you want to actually keep a secret!

Inclusion: in

<Template> <textarea [state.bind] name="text"></textarea> <p> {% if state.secret in state.text %} Uh oh, the secret word is in the text! {% else %} The text is safe: {{ state.text }} {% endif %} </p> </Template> <State text="Don't say s3kr1tw0rd" secret="s3kr1tw0rd" ></State>

Note that the in operator can also work on Arrays, Objects, and more. Read more in the in, not in operators section in the full docs.

Operators for numbers

<Template> <p>A: <input [state.bind] name="a" type="range" min="0" max="20" step="1" /></p> <p>B: <input [state.bind] name="b" type="range" min="0" max="20" step="1" /></p> <p>{% if state.a is state.b %}A = B (is){% endif %}</p> <p>{% if state.a gt state.b %}A > B (gt){% endif %}</p> <p>{% if state.a lt state.b %}A < B (lt){% endif %}</p> </Template> <State a:=10 b:=0 ></State>

Example: Using "gt" with |length filter

Operators can be combined with filters:

<Template> <h5>Subject length checker</h5> <input [state.bind] name="text" style="width: 100%" /> {% if state.text|length gt 20 %} <p>Warning, too long! Your subject will look truncated: {{ state.text|truncate:20 }}</p> {% else %} <p>Your subject is short enough. Good job!</p> {% endif %} </Template> <State text="Use no more than 20 characters" ></State>

More useful template features

Comment tag

Disable broken template code with {% comment %} and {% endcomment %}, or a single line comment with {# and #}:

{% comment %} {% if state.num gt 20 %} <p>Your subject is short enough. Good job!</p> {% endif %} {% endcomment %} {# (and this syntax is good for one line) #}

Note that unlike HTML comments, this will disable templating code. HTML comments will actually not disable templating code, e.g.: <!-- {{ state.data }} --> doesn't actually disable the variable, it just creates an HTML comment with the content of the text of the variable.

Safe filter: |safe

You can mark text as |safe to let it get interpreted as HTML.

Toggle filter: |yesno

Sometimes what's even quicker than an if statement is the humble |yesno filter:

<Template> <label>Show: <input [state.bind] name="active" type="checkbox" /></label> <section style=" display: {{ state.active|yesno:'block,none' }} "> <p>(Showing!)</p> </section> </Template> <State active:=false ></State>

Part 3: Summary

We learned about Shadow DOM and isolating CSS from children using slots. We learned about the Template Tag "if" that lets us optionally include code. We learned the similarly useful Template Filter "|yesno", which lets us quickly add or remove classes.

Key terms

  • slot - A placeholder for content to be inserted when a component is used. Useful for writing components that "wrap around" other content.
  • Style isolation - The Style CPart has several "strategies" or modes for isolating the CSS it contains to prevent it from affecting other components in ways you do not intend
  • Component mode - A way to set components to render in a certain way, automatically setting Style to isolate in a certain way as well
    • mode="vanish" - A way to make the component "vanish" after rendering, and make the Style use classes instead of prefixes
    • mode="shadow" - A browser-enforced way to isolate CSS to a component so it can't affect other CSS and other CSS can't affect it
  • Template tag - A more advanced type of Template code that lets us repeat or optionally include other template code based on State data
    • if-statement - template tags for optionally including portions of template code

Next step

At this point, you've half way through, and have learend enough to make some basic, minor interactive apps! In the next step, you'll learn more advanced features: How to assemble more complicated data as "data props", the "for loop" template tag, and finally how to add static or API data.

Part 4: Data Props, For Loops, and StaticData »