Skip to content

Options: Receive Options

What are we learning here?

┌──────────────┐ ┌─────────────────────────────────────────────────┐
│ Loading      │ │ Component                                       │
│ Component    │ │                                                 │
│ ┌──────────┐ │ │              ┌──────────────────┐   ╔═════════╗ │
│ │ options  ├─┼─┼─Transform───►│ Received Options ├──►║ Final   ║ │
│ └──────────┘ │ │ (optional)   └────────▲─────────┘   ║ Options ║ │
│              │ │                     Merge           ╚═════════╝ │
└──────────────┘ │              ┌────────┴─────────┐               │
                 │              │ Static Defaults  │               │
                 │              └──────────────────┘               │
                 └─────────────────────────────────────────────────┘

  • A component can be loaded with load(ComponentType, ComponentOptions)
  • The passed options are available over this.options[...]
  • The default values can be defined in static defaults = {...}

Any component can receive options and easily define default values for them. Options and the default values will be defined in a static variable named defaults. The received options will be available over this.options.

First we add two container to show the values in our template:

src/demo/apps/component-basics/content.ejs.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<div>
  <h2><%= locals.myHeader %></h2>
  <div class="mt-2 card">
    <div class="card-header">Loading Components</div>
    <div class="card-body"><div data-role="stage"></div></div>
  </div>
  <div class="mt-2 card">
    <div class="card-header">Pass Options</div>
    <div class="card-body">
      Option 1: <span data-role="option1"></span><br />
      Option 2: <span data-role="option2"></span>
    </div>
  </div>
</div>

Now we define two options myOption1 and myOption2 with default values in our widget using the static variable defaults.

As you will see we will define the options in a namespace @codecoupler.walktrough. You should never mess up the root of the defaults variable. Put your options always into a namespace. You'll soon understand why.

Then we could access the values from the object this.options["@codecoupler"].walkthrough.YourOptionName and pass them into our template.

I can already hear everyone screaming. Should we write such a long statement every time to determine a single value? And I agree with you. In many cases you will need to access many of your options and use this statement over and over again.

It is therefore always advisable to provide a small help function. This should be private and point to your own namespace:

get #options() {
  return this.options["@codecoupler"].walkthrough;
}

Thus the instruction from this.options["@codecoupler"].walkthrough.YourOptionName is shortened to this.#options.YourOptionName.

Why private and why in this way?

If you're wondering why the helper function has to be private and why this can't be solved any other way: Components are based on inheritance and each base class needs access to its own namespace.

All automation approaches are far too complex in relation to this simple help function.

Let's display our values in our HTML structure:

src/demo/apps/component-basics/content.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { Widget } from "@codecoupler/cc-ui";
import template from "./content.ejs.html";
import style from "./content.module.css";
import LoadingComponents from "../../components/loading-components";
export default class extends Widget {
  myHeader = "Component Basics";
  static defaults = {
    "@codecoupler": {
      walkthrough: {
        myOption1: "Default 1",
        myOption2: "Default 2"
      }
    }
  };
  get #options() {
    return this.options["@codecoupler"].walkthrough;
  }
  async start() {
    let tpl = template({
      myHeader: this.myHeader
    });
    this.element.replaceWith(tpl);
    this.element.classList.add(style.widget);
    let stage = this.registerStage(
      this.element.querySelector("[data-role=stage]")
    );
    await stage.load(LoadingComponents);
    this.element.querySelector("[data-role=option1").innerHTML =
      this.#options.myOption1;
    this.element.querySelector("[data-role=option2").innerHTML =
      this.#options.myOption2;
  }
}

Ok, you should see now the default values in the right place.

Let's start pass the option myOption1 to the widget in our application. For this we will use here a further load method signature this.load(ComponentType, ComponentOptions). This would look like:

await this.stage.load(Content, {
  "@codecoupler": { walkthrough: { myOption1: "Value 1" } }
});

And again I hear everyone screaming. Should we prescribe such large instructions to the callers of our components for very simple use cases? Just to pass a simple value?

Well, here I only agree with you in part. In fact there are very simple components. Like ours now, for example. Or even those already included in the basic package, such as a component for displaying a simple message.

In these cases it is actually a bit exaggerated. But there are also more complex components that have multiple base classes where this instruction size should be retained.

Fortunately, there is a solution for this too. For simple use cases, you define your own namespace in a property called $map of the defaults variable:

static defaults = {
  $map: "@codecoupler.walkthrough"
};

This allows the caller to pass the options directly at the lowest level and the call is shortened to:

await this.stage.load(Content, { myOption1: "Value 1" });
Not all options will be maped

The $map keyword will map all options in the root level to the given namespace. Exceptions to this are: namespaced options (starting with @) and well known options (starting with $).

A combination of mapped top level options and namespaced or well known options is valid:

await this.stage.load(Content, {
  "@someNamespace": { subspace: { anOption: "aValue" } },
  myOption1: "Value 1"
});

Let's add this property first:

src/demo/apps/component-basics/content.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { Widget } from "@codecoupler/cc-ui";
import template from "./content.ejs.html";
import style from "./content.module.css";
import LoadingComponents from "../../components/loading-components";
export default class extends Widget {
  myHeader = "Component Basics";
  static defaults = {
    $map: "@codecoupler.walkthrough",
    "@codecoupler": {
      walkthrough: {
        myOption1: "Default 1",
        myOption2: "Default 2"
      }
    }
  };
  get #options() {
    return this.options["@codecoupler"].walkthrough;
  }
  async start() {
    let tpl = template({
      myHeader: this.myHeader
    });
    this.element.replaceWith(tpl);
    this.element.classList.add(style.widget);
    let stage = this.registerStage(
      this.element.querySelector("[data-role=stage]")
    );
    await stage.load(LoadingComponents);
    this.element.querySelector("[data-role=option1").innerHTML =
      this.#options.myOption1;
    this.element.querySelector("[data-role=option2").innerHTML =
      this.#options.myOption2;
  }
}

And now let's pass the value with the short notation:

src/demo/apps/component-basics/index.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { Application } from "@codecoupler/cc-ui";
import Content from "./content.js";
export default class extends Application {
  static defaults = {
    "@codecoupler": {
      panel: {
        panelSize: "640 510",
        headerTitle: "Test Component Features"
      }
    }
  };
  async start() {
    await this.stage.load(Content, { myOption1: "Value 1" });
  }
}

Start Definition Object vs Shortcut Signature

We will use here a signature of the load method which expects a Component and its options. This is just a shortcut signature. The load method normally expects a so called start definition object. This would look like:

load({
  component: ChildComponent,
  options: { myOption1: "Value 1" }
})

Now you see the passed value of myOption1 and the default value of myOption2 as we did not pass another value for this option.