-
Notifications
You must be signed in to change notification settings - Fork 0
Controller
Controller components determine the behavior of your application.
Controllers are stored in:
src
└───components
└───app
└───controllers
Usually controllers will be the most frequently used building block in your application. You use them to define logic for user interactions and update UI accordingly. There are two types of controllers: non-relational and relational.
Exampler controller included in the source code:
import Controller, { Relation } from "../../common/controller.abstract";
/**
* Example of a controller.
* It has `Relation.Default` which means association with DOM, but
* no data binding. And the controller cannot emit any events, because
* type template says `never`, it can be changed to any union string type
* like `"event1" | "event2" | "event3"`.
*
* Best practice to name controllers as `something(er/or)`
*/
export default class Exampler extends Controller<never>(Relation.Default) {
/**
* Initialization of Exampler controller
*/
public async initialize(): Promise<void> {
///Controller initialization logic goes here
}
}
This is a simple relational controller without data-binding and any events to emit.
Controllers of this type can have only a single instance at a time. Usually they are created at the first page load and persist till the end. They have a relation of null
. This is the only and default type for node apps. In order to make a browser controller non-relational you have to explicitly specify Relation.None
in class definition:
import Controller, { Relation } from "../../common/controller.abstract";
export default class Exampler extends Controller<never>(Relation.None) {
...
}
Non-relational controllers have the same features as services. Which are events and exposing.
This is the type of controllers you want to use for something similar to services but with some DOM interactions or window access. Checkout some prebuild non-relational components for browser: Hasher, Offliner.
This is the type of controllers you usually create in browser. They are related to all the elements which have a controller attribute:
<div controller="exampler">
<p controller="exampler texter"></p>
</div>
As you see in the example above, exampler
and texter
are some controllers. You can define multiple elements with the same controller. Each of them will have an independent state (even if the controller is inside itself). Besides, you may add multiple controllers to an element as shown with the paragraph element.
Usage is same as with service's events. Can be used in both relational and non-relational controllers.
Usage is same as with service's exposing. Can be used in both relational and non-relational controllers.
For relational controllers a global shortcut is available: ctrl
. It lets you call an exposed function on an a particular controller (the one that has the calling element in the scope) from an element's event handler:
div(controller="exampler") //- ← `.say()` will be called on this controller
button(onclick="ctrl.say('hello')")
div(controller="exampler") //- ← `.say()` will not be called on this controller
ctrl
tries to find the nearest parent controller which has the requested function exposed.
This feature is only for relational controllers.
this.container
can be used to refer to the related HTML element from a controller. For example:
div(controller="exampler" data-something="42")
import Controller from "../../common/controller.abstract";
export default class Exampler extends Controller<never>() {
public initialize(): void {
//this.container - referes to the `div` element
this.container //<div controller="exampler" data-something="42"></div>
this.container.dataset.something //42
}
}
This feature is only for relational controllers.
Lets you write @element
or @elements
property decorators. Which use query selector within the controller's container to find one or all the matching elements respectively.
The default behavior is to use class selector with the property name, but you can specify a custom selector with @element(<selector>)
.
If no matching elements were found, @elements
will return an empty array while @element
will throw an exception.
Example:
div(controller="exampler")
span(class="text")
div(class="epic")
div
import Controller, {
element,
elements
} from "../../common/controller.abstract";
export default class Exampler extends Controller<never>() {
@element("span")
private span!: HTMLSpanElement;
@element
private text!: HTMLSpanElement;
@element
private nothing!: HTMLSpanElement;
@elements("div")
private divs!: HTMLDivElement[];
public initialize(): void {
this.span //<span class="text"></span>
this.text; //<span class="text"></span>
this.divs; //[div.epic, div]
this.nothing; //Error: Element ".nothing" does not exist!
}
}
The exclamation marks tell TypeScript that the values will be defined later than the construction. Alternatively, you might set them to null if you wish.
This feature is only for relational controllers.
Allows you to bind some data to the UI. To enable data-bidning on a controller use Relation.Binding
:
export default class Exampler extends Controller<never>(Relation.Binding) { ... }
Data-binded variable are case insensitive!
Data from the controller bounds to pug's template variables compiled by the view-loader. You can access this data from this.data
proxy:
div(controller="exampler")
div=value
...
public initialize(): void {
this.data.value = 42;
}
...
This will render:
<div controller="exampler">
<div>
<data-text bind-value="42" bind="value">42</data-text>
</div>
</div>
You can pass arrays or objects:
div(controller="exampler")
- obj = {}
- arr = []
div=arr[0]
div=obj.a
...
public initialize(): void {
this.data.arr = [1];
this.data.obj.a = 42;
}
...
Keep in mind that the pug compiler does not know if the value is an array or an object. So, it will throw an error when we try to access some properties of undefined. That is why we need to supply default render-time values for our data: obj = {}
and arr = []
. When the controller will have been initialized obj
will become {a: "42"}
and arr - ["1"]
. Note that all the data is being converted to strings before being passed to DOM.
There is an important difference between writing - arr = []
and - let arr = []
. In the first case we set a default value for the arr
datapoint, whereas in the second we make arr
a constant (it means compile-time only variable), it will render as an empty array and will be unable to be change by data-binding.
Apart from binding content, you are able to bind attributes and even use expressions!
div(controller="exampler" class=good ? "good" : "bad")
this.data.good = true; //<div class="good" controller="exampler"></div>
this.data.good = false; //<div class="bad" controller="exampler"></div>
Any object-like data can be iterated with pug each loop (nested loops are also supported).
- array = []
each item, key in array
p=`${key}: ${item}`
this.data.array = [1,2,3] //<p>0: 1</p> <p>1: 2</p> <p>2: 3</p>
this.data.array[0] = 42; //<p>0: 42</p> <p>1: 2</p> <p>2: 3</p>
If we want the data to be rendered without a controller. We should define array
as a compile-time variable instead: - const array = [1,2,3]
. Then:
- const array = [1,2,3]
each item, key in array
p=`${key}: ${item}`
//↓ Does nothing
this.data.array[0] = 42; //<p>0: 1</p> <p>1: 2</p> <p>2: 3</p>
Since we have a 2-way data-binding implemented in TheFramework. You can simply change the data by passing it as the value attribute to any input element:
p=text
input(value=text)
Now we write something in the input element, for example "hello". The text in the paragraph instantly updates with the input. Also we can access text
from a controller:
this.data.text; //"hello"
Bind decorators provide you a simpler and more elegant approach to data-binding. When you decorate your controller's properties with @bind
they are automatically bound to this.data.<property>
value. Or you could specify a custom path with @bind(<path>)
.
div(controller="exampler")
p=string
p=number
import Controller, { bind, Relation } from "../../common/controller.abstract";
export default class Exampler extends Controller<never>(Relation.Binding) {
@bind
private number: number = 0;
@bind("string")
private text: string = "hello";
public initialize(): void {
setTimeout(() => {
this.number = 42;
this.text = "bye";
}, 1000);
}
}
In this example we set initial values of number
and string
to 0
and "hello"
. After a second we change them to 42
and "bye"
.
The most significant difference from this.data
is that binded attributes retain their type. This is achieved with convertTo
function from Utils
. Moreover, the prototype is also preserved, so you can bind really complex objects. Check out our language model example to see this in action.