Components define templates that are used to create objects. They extend the functionality of a javascript class by adding additional member types, such as events, listeners, property bindings, nested children, and others.
Components are declared using the component
keyword:
component A{}
By default, component A
will inherit from BaseElement
, which must be at the root of the components inheritance hierarchy. (Similarly to how the Object
class is in javascript.)
Creating the component is done by adding curly braces after the component name, but without the component
keyword in front:
let a = A{} // a is now an instance of component A
To inherit from a component other than BaseElement
, the <
symbol is used:
component A < B{}
The inheritance acts the same as in javascript, making functionality from B
available in A
.
We can add members inside the components body. They can be one of the following types:
component A{ /*
* constructor
* function
* property declaration
* property assignment
* property accessors
* scope identifier
* event
* listener
* default children
*/}
To give you a quick overview on how each one would look, the component below contains an example for each of the above member types:
component A{
constructor(){} // constructor
number x: 10 // property declaration
y: 10 // property assignment
get y(){} // property accessor (getter)
set y(value:any){} // property accessor (setter)
fn toString() string{ return ""; } // function
id: a // scope identifier
event triggered()
on triggered: () => {} // listener
B{} // default child 0
C{} // default child 1
}
The following sections go through each member type in detail.
Components can declare properties similar to how classes in javascript do:
component A{
number x: 20
}
They can also be declared similar to a typescript notation:
component A{
prop x:number: 20
}
Component A
declares a property x
that has a value of 20
. The value can be accessed the same way as an it would for any other javascript object:
const a = A{}
console.log(a.x) // 20
Properties can also be declared without an assignment:
component A{
number x
}
const a = A{}
console.log(a.x) // undefined
Assigning a declared property to another will create a binding between the 2 properties. In this following example, property y
is bound to property x
. This means, each time x
changes its value, y
will also change its value:
component A{
number x: 10
number y: this.x
}
const a = A{}
a.x = 20
console.log(a.y) // 20
Assigning without binding properties is possible by using the equals (=
) sign instead of the colon (:
):
component A{
number x: 10
number y: this.x
number z= this.x // won't bind to x
}
const a = A{}
a.x = 20
console.log(a.z) // 10
Properties declared in parent components can be assigned directly. Below, B
extends A
, which was previously declared with x
, y
and z
properties:
component B < A{
x: 30
}
const b = B{}
console.log(b.x) // 30
Bindings work on component assignments as well:
component B < A{
x: 20
y: this.x
}
const b = B{}
b.x = 30
console.log(b.y) // 30
Assinging an expression to a property will also create a binding to each property of that expression:
component A{
number x: 20
number y: 10
number z: this.x + this.y
}
const a = A{}
a.x = 200
console.log(a.z) // 210
Complex expressions can be surrounded by braces and use a return
statement:
component A{
number x: 20
number y: 10
number z: {
return this.x + this.y
}
}
const a = A{}
a.x = 200
console.log(a.z) // 210
Inside these complex expressions, bindings will not work if they are nested further in function, component or class scopes:
component A{
number x: 20
number y: 10
number z: {
function run(){
return this.x + this.y
}.bind(this)
return run()
}
}
let a = A{}
a.x = 200
console.log(a.z) // 30
Properties can have custom getters and setters. It's important to note that the default ones manage bindings and event notifications. So, to not loose this behaviour, BaseElement.getProperty
can be used inside the getter function and BaseElement.setProperty
inside the setter function. Custom functionality can be added around these calls. This is how it would look like:
component A{
number x: 0
get x(){
console.log('Getting property x')
return BaseElement.getProperty(this, 'x')
}
set x(val:number){
console.log('Setting property x')
BaseElement.setProperty(this, 'x', val)
}
}
Ids are used to reference a component from other components:
component A{
id: a
number x: 10
number x1: B{
id: b
number y: a.x // a is accessible from here
}
number x2: C{ number z: b.y } // b is accessible from here
}
Component ids are only accessible within a component scope. All components created within that scope can see each other:
component A{
id: a
Object x1: B{ id: b }
Object x2: C{ id: c }
Object x3: component D{
id: d
E{ id: e }
}
}
a
, b
and c
can see each other, and therefore can access each others properties. However, component D, creates a new component scope, and when created, will not be able to see a
,b
and c
, nor the other way around. e
and d
are available in the same scope.
By default, components have a constructor that takes no arugments. Custom constructors can be defined like this:
component A{
constructor(){
super()
this{}
}
}
All constructors need a call to super
, and an initializer expression this{}
. Functionality can be added after the call to super
:
component A{
constructor(){
super()
console.log("Constructor called: before initializing.")
this{}
console.log("Constructor called: after initializing.")
}
}
Inside this{}
the component registers its members, assignmets and other live elements related functionality:
component A{
constructor(){
super()
console.log(this.x) // undefined
this{}
console.log(this.x) // 100
}
number x: 100
}
Constructors can have arguments the same way as functions do:
component A{
constructor(x:number, y:number){
super()
this{}
this.x = x
this.y = y
}
number x: 0
number y: 0
}
let a = A.(100, 200){}
console.log(a.x) // 100
console.log(a.y) // 200
Additionaly to javascript constructors, in Live Elements constructors require parameter types, declared like in Typescript.
You can also initialize properties directly inside this{}
initializer:
component A{
constructor(param1:number, param2:number){
super()
this{
x = param1
y = param2
}
}
number x
number y
}
This will initialize properties when creating them, and can be referenced inside child constructions:
component A{
id: a
constructor(param:number){
super()
this{ x = param }
}
number x
B.(a.x){} // can now reference x, as it's already defined
}
See also creating with constructors.
Methods are declared using the fn
keyword:
component A{
fn toString(){
return "A{}"
}
}
In comparison to javascript methods, parameters are required to declare their type (We can use any
to refer to any type):
component A{
fn sum(a:number, b:number){
return a + b
}
}
Events are preceeded by the event
keyword, and, similar to functions, can have arguments. Events are triggered using the emit()
function:
component A{
event event1()
event event2(x:number, y:number)
fn triggerEvent1(){
this.event1.emit()
}
fn triggerEvent2(){
this.event2.emit(10, 20)
}
}
Listeners are preceeded by the on
keyword. They assign a function to be executed when the event was triggered:
component A{
event event1()
event event2(x:number, y:number)
on event1: () => {
console.log("Event 1 triggered")
}
on event2: (x, y) => {
console.log("Event 2 triggered with: x=" + x + " and y=" + y)
}
}
An event can have more than one listener attached.
All properties automatically add an event named <property_name>Changed
, which is triggered when the property changes. Below, property x
declares an xChanged
event:
component A{
number x: 0
on xChanged: () => {
console.log("x changed to " + this.x)
}
}
const a = A{}
a.x = 10 // will log: x changed to 10
A component can have children assigned by default. To enable this, the children
property has to be declared with a default
type:
component A{
default children
B{} // child 0
C{} // child 1
}
const a = A{}
console.log(a.children.length) // 2
Components can declare a completed
function, which is called when the component is created, but after it finishes adding all of its members and children:
component A{
default children
B{} // child 0
C{} // child 1
fn completed(){
super()
console.log("Component completed with number of children: " + this.children.length)
}
}
const a = A{} // Component completed with number of children: 2
The difference between the completed
function and the constructor is that the completed function is triggered after all members and children have been assigned during construction of the object. See also completed hook.
In javascript, components end up being used like classes. You can test whether an object is of a certain type using the instanceof
expression.
component A{}
const a = A{}
console.log(a instanceof A) // true
To get the component name is also the same as in javascript:
component A{}
let a = A{}
console.log(a.constructor.name) // A
To get the property names, it's better to use BaseElement.propertyNames(object)
rather than Object.keys(object)
, since the first one focuses just on the component properties:
component A{
number x
number y
}
const a = A{}
console.log(BaseElement.propertyNames(a)) // [x, y]
The next page goes through creating components in detail.