Skip to content

Stages: Base Levels

We will now create an application with which we can test the base stage levels background, base and foreground.

We will explain the special levels block, error and hint later by loading special components into these levels. Some of the stages have special features and furthermore it makes no sense to load whatever component into these levels.

What are we learning here?

  • A component can be loaded with load(Level, ComponentType, Id)
  • Component with an id cannot be loaded multiple times - they will be fronted instead
  • A component can be destroyed with destroy()
  • A component can be adressed with getComponent()

Let's start a new layout canvas. It is a flexbox layout with a header element like before, one stage, many buttons above this and an empty element for debuging output:

src/demo/apps/stage-levels/layout.html

 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
35
36
37
<div class="h-100 d-flex flex-column">
  <div class="p-2 flex-grow-1 border border-success d-flex flex-row">
    <div class="w-25 flex-grow-1 d-flex flex-column p-2">
      <div data-role="widget1" class="mb-1">
        Widget 1:
        <button class="btn btn-success btn-xs">Base</button>
        <button class="btn btn-primary btn-xs">Background</button>
        <button class="btn btn-danger btn-xs">Destroy</button>
      </div>
      <div data-role="widget2" class="mb-1">
        Widget 2:
        <button class="btn btn-success btn-xs">Base</button>
        <button class="btn btn-primary btn-xs">Background</button>
        <button class="btn btn-danger btn-xs">Destroy</button>
      </div>
      <div data-role="widget3" class="mb-1">
        Widget 3:
        <button class="btn btn-success btn-xs">Base</button>
        <button class="btn btn-primary btn-xs">Background</button>
        <button class="btn btn-danger btn-xs">Destroy</button>
      </div>
      <div
        data-cc-stage="mario"
        class="flex-grow-1 border border-primary"
      ></div>
    </div>
    <div class="w-25 flex-grow-1 d-flex flex-column p-2">
      <div class="mb-1">
        Components in Stage
        <span class="text-danger" data-role="debug-progress"></span>
      </div>
      <div class="flex-grow-1 border border-primary">
        <pre data-role="debug"></pre>
      </div>
    </div>
  </div>
</div>

Let's start with a simple canvas class as before. We will extend then this step by step:

src/demo/apps/stage-levels/layout.js

1
2
3
4
5
6
7
8
import { Canvas, Tools } from "@codecoupler/cc-ui";
import template from "./layout.html";
export default class extends Canvas {
   async start() {
      this.element.html(template);
      Tools.registerStages(this, this.element);
   }
}

We use now the canvas in a new application. We will load our new flexbox layout canvas and load the the header widget:

src/demo/apps/stage-levels/index.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { Application } from "@codecoupler/cc-ui";
import MainLayout from "./layout.js";
export default class extends Application {
  static defaults = {
    "@codecoupler": {
      panel: {
        panelSize: "800 600",
        headerTitle: "Test Stage Levels"
      }
    }
  };
  async start() {
    await this.stage.load(MainLayout);
  }
}

We load the new application in our system.js file (just by replacing the previous one):

src/system.js

1
2
3
4
5
6
7
import { Component } from "@codecoupler/cc-ui";
import TestApp from "./demo/apps/stage-levels";
export default class extends Component {
  async boot() {
    await this.stage.load(TestApp);
  }
}

Ok, now you see an empty stage with buttons above. None of the buttons do anything for now. Let's add the handlers:

src/demo/apps/stage-levels/layout.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 { Canvas, Tools } from "@codecoupler/cc-ui";
import template from "./layout.html";
export default class extends Canvas {
  async start() {
    this.element.html(template);
    Tools.registerStages(this, this.element);
    for (const widgetId of ["widget1", "widget2", "widget3"]) {
      const btnContainer = this.element.find(`[data-role=${widgetId}]`);
      btnContainer.find(".btn-success").on("click", async () => {
        if (this.options[widgetId]) {
          await this.getStage("mario").load(
            this.options[widgetId].component,
            this.options[widgetId].options(),
            widgetId
          );
        }
      });
      btnContainer.find(".btn-primary").on("click", async () => {
        if (this.options[widgetId]) {
          await this.getStage("mario").load(
            "background",
            this.options[widgetId].component,
            this.options[widgetId].options(),
            widgetId
          );
        }
      });
      btnContainer.find(".btn-danger").on("click", async () => {
        await this.getStage("mario").getComponent(widgetId)?.destroy();
      });
    }
  }
}

Ok, clicking these buttons they still do nothing, but let us analyze the code before we finish the implementation.

You see here that clicking the green button (with the class btn-success) would load a component with the load method using the signature load(ComponentType, Options, Id) into the stage (which is called mario). The component type and component options will be fetched from the passed options and the id will be have a fixed value.

Clicking the blue button (with the class btn-primary) will also load a component into the same stage using the load method signature load(Level, ComponentType, Options, Id). The first argument is used to load the component into the level background.

Start Definition Object vs Shortcut Signature

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

load({
  component: this.options[widgetId].component,
  id: widgetId,
  options: this.options[widgetId].options(),
  level: "background"
})

Clicking the red button (with the class btn-danger) will destroy the corresponding component. For this we use the method destroy() which is available for every component. To find a component you can use its id and the method getComponent(Id) of a stage.

Now the application have to pass the options widget1, widget2 and widget3 to our component. We will use here a very simple built-in component CenteredMessage. The component shows only a message. Furthermore the passed options will be everytime new calculated inclusing a random number.

src/demo/apps/stage-levels/index.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
35
36
37
import { Application, CenteredMessage } from "@codecoupler/cc-ui";
import MainLayout from "./layout.js";
export default class extends Application {
  static defaults = {
    "@codecoupler": {
      panel: {
        panelSize: "800 600",
        headerTitle: "Test Stage Levels"
      }
    }
  };
  async start() {
    await this.stage.load(MainLayout, {
      widget1: {
        component: CenteredMessage,
        options: () => ({
          text: `Component Number 1\nRandom: ${Math.random()}`,
          css: { backgroundColor: "#fff" }
        })
      },
      widget2: {
        component: CenteredMessage,
        options: () => ({
          text: `Component Number 2\nRandom: ${Math.random()}`,
          css: { backgroundColor: "#eee" }
        })
      },
      widget3: {
        component: CenteredMessage,
        options: () => ({
          text: `Component Number 3\nRandom: ${Math.random()}`,
          css: { backgroundColor: "#ddd" }
        })
      }
    });
  }
}

Now you can click around to load and destroy the three widgets in the stage. But maybe it's not quite clear why some buttons don't seem to work. Therefore we will now add a debugging output.

src/demo/apps/stage-levels/layout.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
35
36
37
38
39
40
41
42
import { Canvas, Tools } from "@codecoupler/cc-ui";
import template from "./layout.html";
export default class extends Canvas {
  async start() {
    this.element.html(template);
    Tools.registerStages(this, this.element);
    this.getStage("mario").on("change", () => this.#printDebug());
    for (const widgetId of ["widget1", "widget2", "widget3"]) {
      const btnContainer = this.element.find(`[data-role=${widgetId}]`);
      btnContainer.find(".btn-success").on("click", async () => {
        if (this.options[widgetId]) {
          await this.getStage("mario").load(
            this.options[widgetId].component,
            this.options[widgetId].options(),
            widgetId
          );
        }
      });
      btnContainer.find(".btn-primary").on("click", async () => {
        if (this.options[widgetId]) {
          await this.getStage("mario").load(
            "background",
            this.options[widgetId].component,
            this.options[widgetId].options(),
            widgetId
          );
        }
      });
      btnContainer.find(".btn-danger").on("click", async () => {
        await this.getStage("mario").getComponent(widgetId)?.destroy();
      });
    }
  }
  #printDebug() {
    const debugElement = this.element.find(`[data-role=debug]`);
    debugElement.text("");
    this.element.find(`[data-role=debug-progress]`).text("");
    for (const line of this.getStage("mario").getComponentsDebug()) {
      debugElement.append(`${line}\n`);
    }
  }
}

Now whenever the stage throw the event change we will write an output into the empty element with the attribute data-role=debug.

We will discuss the event system and some possible events later. For now you just have to know that the change event always occurs after an adjustment of the order of the components or after deleting a component.

For the debug output we use the method getComponentsDebug() of the stage which deliver all components order by the z-index and level. So we have always an overview of all layers of the stage. The topmost component in the debug output is also the topmost and visible component in the stage.

Let's try the following combinations (destroy everytime all components and start each point with an empty stage):

  • Click all of the base buttons. Every button will load a separate widget into the base stage. Clicking the same button again will not load the widget again. It will only front it. You also see a new random number. This is because the component will front and the new options will be passed to the instance. The component CenteredMessage can handle such a change and refresh its text.

Now click on all three destroy buttons.

  • Click the first base button and then the second background button. Now you will not see the second component because first component was loaded in the level above. You have to destroy the first component for this to appear.

Now click on all three destroy buttons.

  • Click the first both background buttons and the third base button. Now try any of the first and second base or background buttons. The component will never appear in the foreground. This is because they already was loaded into the background. The only thing that happens is changing their layer position. But you will never see them as the third component will always stay in front of them.