2.4 Components - Baumgaer/Game GitHub Wiki

A component in this project is a native WebComponent with a few tweaks. When a component is written, its name in the dom corresponds to the class name but in kebab-case instead of PascalCase.

Webcomponents are furthermore globally registered to be able to initialize a component in the DOM. Otherwise they won't be handy or functionally because webcomponents are based on a normal point of view in HTML which means that you just can write a tag in HTML and the element / webcomponent will be rendered.

Every component is derived from BaseComponent which is derived from BaseController so every component is also a controller with the difference that it SHOULD NOT implement busines logic. This also implies that a component has near by the same life cycle as a controller.

2.4.1 The base of a component

Every component is derived from the BaseComponent which can be extended by any HTMLElement. A BaseComponent is constructed by the BaseComponentFactory like shown in example 1.

import {BaseComponentFactory} from "~client/lib/BaseComponent";
import {baseConstructor} from "~bdo/utils/decorators";

@baseConstructor()
export default class MyComponent extends BaseComponentFactory(HTMLElement) {
  // ...
}

Example 1: Basic structure of a component to get basic functionality.

Furthermore a component is always a baseConstructor which is necessary to invoke the life cycle, which every component should have to be able to pass arguments which are defined as public class fields.

A component should also extend at least HTMLElement to get the functionality of a basic HTML-element and must be exported as a default class to be ensure that this class can be loaded automatically by the build process.

2.4.2 The life cycle

Because every component is a webcomponent and webcomponents have a life cycle by nature, this components will have the life cycle of webcomponents and some additionally hooks.

The most parts of the life cycle are already explained in chapter 2.3 so here are only component related life cycle hooks.

  • renderTemplate(): void

    This method is called after the constructor and before the connectedCallback. Like the name implies this method renders the template and scoped style of a component.

2.4.3 Initialization of a component

When you want to create a new Component programmatically you just type ExampleComponent.create({propOrAttrName: value}). Then you will get an instance of the ExampleComponent and you can place it somewhere in the DOM.

If you want to initialize a component in a DOM you just type <example-component attr1="val" attr2="val"></example-component> and the initialization is automatically done by the browser. If your component is extended by a more specialized HTMLElement like a HTMLAnchorElement, you have to add the attribute is for example <a is="example-component" />.

Furthermore you have to add the static class field extends: string to your webcomponent like shown in example 2. If this component is a default component, just leave this field empty.

import {BaseComponentFactory} from "~client/lib/BaseComponent";
import {baseConstructor} from "~bdo/utils/decorators";

@baseConstructor()
export default class ExampleComponent extends BaseComponentFactory(
  HTMLAnchorElement
) {
  public static extends: string = "a";

  // ...
}

Example 2: Component that extends a more specific HTMLElement

2.4.4 Event listener

You can assign event listeners as usually but it is not necessary to remove them when the component is removed from the DOM. Because of a component is derived from BaseController in the core and the he will destroy all event listeners, you don't have to take care of event listeners which are set on the current component.

When you add an event listener to an element which is part of your component but not a webcomponent, then you have to take care about this event listeners because they are not managed by the BaseController.

2.4.5 Controllers

As already discussed in chapter 2.3 you can add controllers to each component like you do it in other controllers. Because a component is also a controller you have access to the methods addController<T extends Constructor<BaseController>>(name: string, controller: T, arguments: ConstParams<T> and removeController(name: string): void. As like in a controller, this added controller will be assigned to the controllers: IndexStructure<BaseController> field and the owner of this controller will be this component.

When a component is removed explicitly, all controllers will be removed recursively.

2.4.6 Templates and Styles

Every component need a minimal template to get a shape and a style to get a default look. Components without a template or style doesn't make sense because they will be completely unstructured and are not shapable after the rendering.

2.4.6.1 Templates

A template in a component needs at least one root tag as a template, usually a DIV-tag like in Example 3. This can be done as a normal string in the class field templateString.. Please notice that there is exactly one root element. If you have two root elements, only the first will be taken.

import {BaseComponentFactory} from "~client/lib/BaseComponent";
import {property, baseConstructor} from "~bdo/utils/decorators";

@baseConstructor()
export default class ExampleComponent extends BaseComponentFactory(
  HTMLElement
) {
  @property({disableTypeGuard: true}) public templateString =
    "<div><span>first name: {{ firstName }}</span><span>last Name: {{ lastName }}</span></div>";
}

Example 3: A component with a simple string as template, which uses the variables firstName and lastName to show some content.

You can also use a nunjucks template which will be loaded like every other file (see example 4) and automatically rendered by nunjucks. The template has the same rules as the string from example 3 with the exception that line breaks are allowed and the template can be much bigger (see example 5). A string should only be used when there are less than 3 elements in the root element.

import {BaseComponentFactory} from "~client/lib/BaseComponent";
import {property, baseConstructor} from "~bdo/utils/decorators";
import template from "~static/views/ExampleComponent.njk";

@baseConstructor()
export default class ExampleComponent extends BaseComponentFactory(
  HTMLElement
) {
  @property({disableTypeGuard: true}) public templateString = template;
}

Example 4: A component with a loaded template as nunjucks template.

<div>
  <span>First name: {{ firstName }}</span>
  <span>Last name: {{ lastName }}</span>
  <span>Nickname: {{ nickname }}</span>
</div>

Example 5: A template in a njk-file which can be loaded in a component.

Because we are using nunjucks, we can use all the benefits of this template engine. But that doesn't mean that we should use everything of this engine because of the complexity. The more different functions are used the more complexity will be added to the logic. So here are some best practices:

  1. Use blocks, includes and extends whenever it is possible.
  2. Never use a loop to initialize several components in a component because you will loose functionality like adding parameters which are no attributes.
  3. use only pipes when the same functionality in the component code would be too much.

2.4.6.1.1 Slots

To be able to put elements inside the component you have to add a slot-element inside the template. You can also add named slots to force a certain position of a slotted element. For Example:


template.njk:

<div>
  <h2>HEADLINE!</h2>
  <slot></slot>
  <slot>I am a Fallback slot</slot>
  <!-- Only visible when no slot content is given-->
  first name: <slot name="firstName"></slot><br />
  last name: <slot name="lastName"></slot>
</div>

somewhere in the DOM:

<body>
  <!-- ... -->
  <my-component>
    <span slot="firstName">John</span>
    <span slot="lastName">Doe<span>
    <span>I am displayed in the first slot and I hide the fallback slot!</span>
  </my-component>
  <!-- ... -->
</body>

Example 6: A component with 4 slots: 1. default slot, 2. default fallback slot, 3. slot for first name, 4. slot for last name.

2.4.6.1.2 Template variables

You maybe noticed that there are variables used in example 4 and 5. This variables are class fields which will be used by the template. Currently this variables are static, so when you have a template with variables, they will get the value at startup and never change. This is still work in progress. To see those variables in action see Example 7 which defines variable values for the template in example 5.

import {BaseComponentFactory} from "~client/lib/BaseComponent";
import {property, attribute, baseConstructor} from "~bdo/utils/decorators";
import template from "~static/views/ExampleComponent.njk";

@baseConstructor()
export default class ExampleComponent extends BaseComponentFactory(
  HTMLElement
) {
  @property() firstName: string = "John";
  @property() lastName: string = "Doe";
  @attribute() nickname: string = "TheFunny";
  @property({disableTypeGuard: true}) public templateString = template;
}

Example 7: defining values for variables of example 5.

2.4.6.1.3 References

It is possible to reference a DOM-Node in the template to get a quick access to this node. To achieve this just add the attribute ref to the node you want to reference like in Example 8.

<div>
  <span ref="firstRef"></span>
  <span ref="secondRef"></span>
</div>

Example 8: Two nodes with a reference attribute.

To access this nodes, you just can type in your component this.refs.firstRef for example. Please note, that a reference must be unique inside a component just like id-attributes in the whole document. References are calculated values which has the benefit that you can change a reference and still have access via the new name.

2.4.6.2 Styles

The style of a webcomponent is always scoped. That means that it is not possible to style parts of a component from outside. So every style does only effect the port of the component itself.

This is because every HTMLElement inside a component is just a part of the new defined custom HTMLElement, so you should style your component in a way that is dynamic in sizes and colors when the main element <my-component /> is modified.

2.4.6.2.1 Host-selector

To style the root element use the :host-selector. All other selectors can be used as knows with the difference that their scope in inside the component only.

NOTE: custom elements are display: inline by default! You must style it with display: block like shown in example 9.

to style a component on certain dependencies like available attribute or class or pseudo state like :hover, you should use the .host(selector)-selector.

:host {
  // default style
  background-color: blue;
  display: block;

  a {
    // Style of an anchor tag inside the component
    color: white;
  }
}
:host(:hover) {
  // Style, when the element is hovered
  background-color: red;
}

Example 9: Styles a component with a blue background by default and changes the color when it is hovered. Anchor-tags are white colored.

2.4.6.2.2 host-context-selector

You also can style your component in a certain context with the :host-context(selector)-selector. This makes it possible, that a component can have several looks in different other components or HTMLElements.

<style>
  :host {
    background-color: white;
  }
  :host-context(.darktheme) {
    background-color: black;
  }
</style>
<body class="darkTheme">
  <my-component />
</body>

Example 10: A component which will be black when it is wrapped in an element with the class "darkTheme".

2.4.6.2.3 slotted elements

To style elements which are slotted you can use the :slotted or :slotted(selector)-selector. This selector works exactly like the :host or :host(selector)-selector with the difference that it is only possible to style top-level elements. Elements inside the slotted element will not be effected!

⚠️ **GitHub.com Fallback** ⚠️