Toolkit components are standard web components that utilize a small library of helper code to reduce boilerplate and automate common actions. Details follow, but here is a quick run-down of the features of Toolkit components:
- one-step declaration via
component
initializer - simplified lifecycle callbacks
- declarative event-binding for standard and custom events
- public and protected interfaces
- declarative binding of data to markup (and to other components)
- automatic node finding
What follows is a discussion about these features and how to understand usage, starting from the beginning.
In the beginning, standard component declarations look like this:
<element name="tag-name">
<template>
<!-- shadow DOM here -->
</template>
<script>
// lifecycle setup here
</script>
<element>
The Toolkit kernel (g-component.html
) is a chunk of code that provides sugaring to make common tasks easier and to be as declarative as possible. In particular, Toolkit supplies the component
lifecycle initializer:
<element name="tag-name">
<template>
<!-- shadow DOM here -->
</template>
<script>
this.component();
</script>
<element>
Simply using the component
initializer prepares this component to use Toolkit conventions.
You can supply a single object-valued argument to component
to define prototypes (and tell the initializer to do things). In fact, most properties and methods defined in the argument to component
are used directly in the component's prototype.
this.component({
helloWorld: "Hallo!",
ready: function() {
// component is ready now, we can do stuff
}
});
In this example, the component has a property helloWorld and a method ready.
Now, as it happens, ready is a method we use to manage the component lifecycle. When a component has finished initializing itself, it calls the ready method automatically (if it exists).
Toolkit supports scoped declarative binding. This means you can declare event handlers in markup, and the handlers will map events to the component instance receiving the event. Here is an example,
<element name="tag-name">
<template>
<span on-click="helloAction">Hello World</span>
</template>
<script>
this.component({
helloAction: function(inEvent, inDetail, inSender) {
confirm("How are you?");
}
});
</script>
<element>
Some things to notice:
- the event handler attribute looks like on- plus the event name (on-dash syntax). This syntax is intended to be close enough to original HTML syntax to be memorable, while being different enough to avoid confusion.
- the value of an event handler is the string name of a method on the component. Unlike traditional syntax, you cannot put executeable code in the attribute.
- there are arguments to the event handler
inEvent
: the standard event objectinDetail
: convenience form of inEvent.detailinSender
: reference to the node that declared the handler, this is often different frominEvent.target
(the lowest node that received the event) andinEvent.currentTarget
(the component processing the event), so the Toolkit provides it directly.
One important Toolkit convention is that components have both public and protected aspects. The public aspect represents the API that is visible and accessible directly from a component (element) instance. The protected aspect contains the API which users of components shouldn't need to bother with, including event handlers or internal methods.
For example, let's say our design for the my-tag element calls for a method that can turn the element text blue, so a user could do like so:
myTag = document.querySelector("my-tag");
myTag.blueify();
The blueify
method is a part of my-tag's public API. It must be available to end-users on the element instance.
Now, imagine my-tag is also supposed to turn orange if clicked. As part of our set up, we attach a click listener to a method called clickHandler
which turns the element orange.
In this case, clickHandler
is not intended to be called by end-users, it's only there to service an event. In this case, clickHandler
should be part of the protected API. Then the method is not visible on the element instance and calling
myTag.clickHandler(); // error: undefined function
throws an error.
As we noted, you can supply an object-valued argument to component
. The rule is: properties and methods supplied to component
become properties on the protected interface.
this.component({
clickColor: 'orange',
clickHandler: function() {
this.node.style.backgroundColor = this.clickColor;
}
});
In this example, the component has two protected properties (one is a method). Note that the scope of the method (the this
value) is the protected scope. This is another rule: from the component's perspective we are always working with the protected scope. It's generally only the user of an instance that needs to deal with the public scope. The exception to this rule is when we need to operate on our node itself, we do this using the this.node
reference, as shown in the example.
Note: from a node reference, the protected scope is available as $protected
. So it's possible to violate encapsulation via:
someNode.$protected.protectedMethod();
To make a blueify
method that is callable on the node (public), we publish the method.
this.component({
clickColor: 'orange',
clickHandler: function() {
this.node.style.backgroundColor = this.clickColor;
},
publish: {
blueColor: 'blue',
blueify: function() {
this.node.style.color = this.blueColor;
}
}
});
Things to remember about publish
:
- There can be only one
publish
block per definition. - Published properties are actually stored on the protected prototype, then they are forwarded to the public prototype. In other words,
blueColor
is different fromclickColor
only because there is a public getter/setter pair to access it. - Published methods still operate in protected scope: the properties you can access via
this
are no different from methods declared outside the publish block.
We've tried to include all the details here, but the bottom line is simple: when building components, use this
naturally and declare properties and methods as you like. Then, if you happen to create API you want to make public, you just move it into the publish
block.
Toolkit components make their properties directly available for data binding. Most simply, you can reference a property directly in your markup (using mustache notation):
<element name="name-tag">
<template>
Hello! My name is {{myName}}
</template>
<script>
this.component({
publish: {
myName: "Scott"
}
});
</script>
<element>
This element renders like so
Hello! My name is Scott
and changes to the myName
property are automatically reflected in the DOM.
Because we declared myName
in the publish block, we can mess with the property directly on a name-tag instance. E.g.
nameTag.myName = "Frankie";
and now the display shows
Hello! My name is Frankie
We can bind to most elements of markup (except tag names!). E.g.
<element name="name-tag">
<template>
Hello! My name is <span style="color:{{nameColor}}">{{myName}}</span>
</template>
<script>
this.component({
publish: {
myName: "Scott",
nameColor: "orange"
}
});
</script>
<element>
Another Toolkit convention is that public properties are settable by attribute. For example, we could deploy the name-tag
example above like this:
<name-tag myname="Steve" namecolor="tomato"></name-tag>
When the name-tag
is created, or when the attributes change value, those attributes values are reflected into their matching properties. Remember, only public properties are settable via attribute.
Because of the importance of being able to set public properties via attribute, Toolkit supports declaring public properties directly on the element tag via the attributes attribute.
Alternate name-tag
declaration:
<element name="name-tag" attributes="myName nameColor">
<template>
Hello! My name is <span style="color:{{nameColor}}">{{myName}}</span>
</template>
<script>
this.component({
nameColor: "orange"
});
</script>
<element>
In this case, name-tag
declares two attributes, which is semantically the same as declaring them in a publish block. The one difference is that properties declared as attributes default to 'undefined' unless defaults are set in the prototype (as done for nameColor above).
Toolkit makes it possible to bind references between components via attributes. Generally, attributes are only string-valued, so the binding engine interprets reference bindings specially (in particular, interrogating an attribute for a bound dreference property will just return the binding expression [the mustache]).
Let's modify our name-tag
to take a record instead of individual properties.
<element name="name-tag" attributes="person">
<template>
Hello! My name is <span style="color:{{person.nameColor}}">{{person.name}}</span>
</template>
<script>
this.component({
person: {
name: "Scott",
nameColor: "orange"
}
});
</script>
<element>
Now, imagine we make a new component called 'visitor-creds' that uses name-tag
:
<element name="visitor-creds">
<template>
<name-tag person="{{person}}"></name-tag>
</template>
<script>
this.component({
person: {
name: "Scott",
nameColor: "orange"
}
});
</script>
<element>
When I make an instance of visitor-creds
, it's person
object is bound to the name-tag
instance, so now both components are using the same person
object.
Another useful feature of Toolkit is node reference marshalling. Every node in a component's shadow DOM that is tagged with an id attribute is automatically referenced in the this.$
hash.
Given
<template>
<input id="nameInput">
</template>
We can write code like so:
<script>
this.component({
logNameValue: function() {
console.log(this.$.nameInput.value);
}
});
</script>
As described, a reference to the <input>
node is available in this.$
hash mapped to the given id (nameInput, in this case).