Nested components and scope

Components can nest. A reference such as #session.user or a handler name such as logout resolves in this order:

  1. loop variables of the surrounding repeat or <repeat-list>,
  2. the component whose markup contains the reference (its owner),
  3. each parent component, from the nearest outwards,
  4. for handler names only: globals, such as console or history.

Content belongs to its owner

Content you place inside a child component is still yours. Here #todo.remaining is written in todo-app's markup, so it reads todo-app's state, even though it is displayed inside ui-card:

<custom-element name="todo-app">
  <script>
    const todo = useState({
      items: [],
      get remaining() {
        return this.items.filter((item) => !item.done).length;
      },
    });
  </script>

  <ui-card>
    <ui-card-header>
      <ui-card-title>Today</ui-card-title>
    </ui-card-header>
    <ui-card-content><slot name="#todo.remaining"></slot> left</ui-card-content>
  </ui-card>
</custom-element>

This is standard shadow DOM. The value slot lives in todo-app's shadow root and is projected into ui-card through ui-card's own <slot>.

Parents fill in missing names

When the owner does not define a name, REALM looks in the component that contains it, then that component's parent, and so on. A deeply nested component can read shared values without passing them through every level:

Previewindex.htmlapp-shell.htmlnav-bar.html
Signed in as nobody Sign out
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Nested components and scope demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <import-element from="./app-shell.html"></import-element>
  <import-element from="./nav-bar.html"></import-element>

  <app-shell></app-shell>
</body>
</html>
<custom-element name="app-shell">
  <script>
    const session = useState({ user: "Ana" });

    function signOut() {
      session.user = "";
    }
  </script>

  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 2);
    }
  </style>

  <nav-bar></nav-bar>
</custom-element>
<custom-element name="nav-bar">
  <style>
    :host {
      display: flex;
      gap: calc(var(--spacing) * 3);
      align-items: center;
    }
  </style>

  <span>Signed in as <slot name="#session.user">nobody</slot></span>
  <ui-button size="sm" variant="outline" onclick="signOut()">Sign out</ui-button>
</custom-element>

nav-bar has no session and no signOut, so both resolve to app-shell. REALM logs a one-time console warning for each name read from a parent (<nav-bar> reads "session" from <app-shell>.), so you can see where implicit sharing happens.

Rules:

Pass data explicitly for reusable components

Parent lookup is convenient inside one app. For components you reuse elsewhere, make the contract explicit with props and events:

<user-menu _user="#session.user" onsignout="signOut()"></user-menu>

Objects and arrays passed with _ bindings arrive as properties, intact. The child declares them with defineProps and emits events with dispatch or dispatchEvent.

App-wide state

State that several unrelated components need, or that should outlive a component, belongs in a store.

Edit this page on GitHub · View as Markdown