2.3 Controllers - Baumgaer/Game GitHub Wiki

A controller in this framework manages default behaviors of components such like pageing, lazy loading or other things which can be implemented for a good webcomponent.

It should contain all the business logic of a component which does not manage the appearance of a component directly. Logic which does manage the appearance indirectly such like a number of people in a chatroom which controls the color of the name of this room is still welcome.

Furthermore a controller implements the default live cycle of a component with hooks a a method in the corresponding class. It also implements logic for handling event listeners and other controllers which must be removed explicitly when a component is removed from the DOM.

A special behavior is the generation of an unique ID for webcomponents when no ID is explicitly given by an attribute of the DOM-element. When an ID is set, it will be always check for unique appearance.

A controller can only be used in a component or another controller. It is not possible to use a controller as stand alone without another controller or component.

2.3.1 The Base of a controller

Every controller is derived from the BaseController which is constructed by the BaseControllerFactory.

import {BaseControllerFactory} from "~client/lib/BaseController";
import {baseConstructor} from "~bdo/utils/decorators";

@baseConstructor()
export class MyController extends BaseControllerFactory(EventTarget) {
  // ...
}

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

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

A controller should also extend the EventTarget to get the functionality to dispatch events or register and remove event listeners.

2.3.2 The life cycle

Every controller will be inherited by a component and derived from BaseController so there is the life cycle of such a component to be able to interact with components in special situations with a default behavior.

  1. constructor(...args: any[])

    this is constructor like every constructor but its meaning is different. In this part of the life cycle you should update default values of class fields or set calculated default values which depends on the given arguments. NOTE: Never set an argument to a class field explicitly because they are managed by the baseConstructor.

  2. constructedCallback(): void

    This hook is called when the constructor is ready. All default values of class fields are assigned and also given arguments which are external values for class fields are assigned after updating the default values.

    In this part of the life cycle you should set bindings of models to the class fields when they are not assignable from external or do other things which depends on a fully constructed instance.

    NOTE: The construction is still in progress! "Constructed" means that the usually construction of a class is ready now and you can extend this behavior now.

  3. connectedCallback(): void

    This life cycle hook is called when the corresponding component is connected to the DOM. Here you should add event listener or busines logic controllers because you have access to the dom node of the component and you can navigate through the DOM.

  4. disconnectedCallback(): void

    This hook is called when the component which is derived from the BaseController is removed in some way from the DOM without placing it in another part in the document.

  5. adoptedCallback(): void

    This is called when the corresponding component is moved to another document. This always happens when the component is moved to the shadowRoot of another component or into an iFrames document.

  6. remove(): void

    Remove is called when the component is explicitly removed from the DOM or when the controller itself is removed from the component by removeController-method. All controllers and event listeners will be removed from the component / controller.

2.3.3 Add a controller to a component or another controller

You can add a controller by calling addController<T extends Constructor<BaseController>>(name: string, controller: T, arguments: ConstParams<T>). When you add a controller, this controller will be added to the controllers: IndexStructure<BaseController> of the component or controller. The ConstParams<T> will be extended by an argument owner: typeof this which is the current instance of the component or controller.

The owner is useful to get access to the component or controller which initializes the controller. When the current instance is a component instead of a controller, owner will always be undefined.

2.3.4 Event listener

In a normal case, a controller should be extended by EventTarget to be able to add event listener (see Example 1).

When your controller is derived from BaseController and EventTarget you can use dispatchEvent(event: Event): void to trigger event listeners, added by addEventListener(type: string, listener: (event: Event) => void): void, as like HTMLElements do it.

When you want to remove an event listener just use removeEventListener(name: string, listener: (event: Event) => void): void exactly like HTMLElements.

When a controller will be removed from a component or the component will be removed from the DOM, all event listeners of all controllers will be removed recursively.

2.3.5 Remove a controller from a controller or component

To remove a controller just call removeController(name: string): void. When the controller with the name name is available in controllers: IndexStructure<BaseController>, all controllers which are initialized by this controller will be removed recursively. Furthermore all event listeners will be removed, too. Only the event listeners which are registered by this controller on a component (owner) won't be removed. This must be done manually.

2.3.6 Special functionality

The BaseController offers a special functionality for all controllers and components, which are the following:

  1. Methods

    • toJSON(): IndexStructure<this[keyof this]>

      This will return a plain object with all properties of the current instance. Useful for saving the state in localStorage or other databases.

    • getNamespacedStorage(key: string, nsProp?: string, forceNS?: string): any

      This is used by several decorators (see chapter 2.2) to get persistent data from localStorage but can also be used in normal program logic.

    • setUpdateNamespacedStorage(key: string, newVal: any, nsProp?: string): any

      This is used by the baseConstructor decorator for example to persist or update information in localStorage. Furthermore this can be used manually.

    • deleteFromNamespacedStorage(key: string, nsProp?: string): void

      This removes information from the localStorage the same way like they are added with setUpdateNamespacedStorage.

  2. Properties

    • id: string

      An unique identifier generated with the class name and the number of appearance.

    • owner: this extends HTMLElement ? undefined : IOwnerType

      usually this is the constructor class of the current controller which initializes this controller. If this is a component, it will be undefined.

    • properties: Map<string, Property<this>>

      This returns all class fields which are decorated with the property decorator.

    • attributes: Map<string, Attribute<this>>

      This returns all class fields which are decorated with the attribute decorator.

      NOTE: This will especially overwrite the attributes property of a webcomponent!

    • bindings: Map<string, Binding<this, DefNonFuncPropNames<this>>>

    This will return all properties which are bound to a models property.

2.3.7 Fully functionality example

This example is a controller which takes a model of BDOModel type with no default value. After usual construction, the protected watched property changeableProperty, which does not have a default value during the usual construction, gets its default value as a binding from the given model. When this property is initialized the first time, the controller writes "changeableProperty initialized" into the reference node initHint of the owner of this controller , which is a component in this case (could be another controller, too).

Every time when the changeableProperty is changed after the first initialization, this controller will write into the reference node changeHint of the owner of this controller, that the value of the changeableProperty is changed by the new value, that is already assigned to this field.

Furthermore a CustomEvent will be dispatched, that is eventually listened by the component.

import {BaseControllerFactory} from "~client/lib/BaseController";
import {baseConstructor, property, watched} from "~bdo/utils/decorators";
import {BDOModel} from "~bdo/lib/BDOModel";

@baseConstructor()
export class MyController extends BaseControllerFactory(EventTarget) {
  @property() public model!: BDOModel;

  @watched() @property() protected changeableProperty!: string;

  constructor(params: ConstParams<MyController>) {}

  constructedCallback() {
    this.changeableProperty = this.model.bind("aProperty");
  }

  onChangeablePropertyInit(value: string) {
    this.owner.refs.initHint.innerText = "changeableProperty initialized";
  }

  onChangeablePropertyChange(_oldValue: string) {
    this.owner.refs.changeHint.innerText =
      "changeableProperty was changed by value: " + this.changeableProperty;
    this.dispatchEvent(
      new CustomEvent("changeablePropertyChange", {
        detail: this.changeableProperty,
      })
    );
  }
}

Example 2: Fully functionally example of a very basic controller.

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