Component Properties
Note
Since version
0.1.0
of the framework, Skate / JSX componets are no longer available by default and we have switched to using lit html. The documentation below has been updated to reflect lit components but you can still see the old Skate docs here.
The main feature that makes components useful and interesting is their properties. A component with a property looks like:
export class MyFirstComponent extends BaseLitComponent<YourAppManager> {
@prop({ type: String })
name: string;
async _init(): Promise<void> {}
_setupEventListeners(): void {}
componentMarkup(): TemplateResult {
return html`<p>This is my first component</p>`;
}
}
The difference here is the line:
@prop({ type: String })
name: string;
This is a property on the class which is pretty normal in most languages, Javascript incuded. The difference is we use the decorator:
@prop({ type: String })
This wraps the property and binds it to our renderer. What this means is that when the property is changed at any point, the component will re-render itself to display the new value. This is a very useful feature and will be used in almost all of your components.
Prop Decorator
The prop
decorator is a function with takes an object
as its single argument. The object properties consists of:
type
(Constructor): This is the basic type of your property and corresponds with a Javascript data type. The possible values are:String
,Number
,Boolean
,Array
,Object
andFunction
. Note the caps here as this is theconstructor
and not the Typescripttype
.
Usage
Properties are the building blocks of your components and are used everywhere. A good example is a component that is a menu. By default the menu is closed and not displayed. When the menu is opened (by clicking a button, for example) we need something to trigger it to display.
We use a prop for this, like so:
@prop({ type: Boolean })
opened: boolean = false;
Then our markup can look like:
componentMarkup(): TemplateResult {
if (this.opened) {
return html`
<div>
/// ... menu contents
</div>
`;
}
return null;
}
When something (say, an event listener) changes this.opened
then the component will re-render itself and if that value is to true
, we draw a menu. If the property doesn't change value (e.g. we do something like this.opened = false;
when the value is already false
) then nothing will happen.
This is the basics of how we use component properties to manage how our components behave. Components will naturally have a lot of properties and values can be set from outside the component itself, which is where the power of props truly shines.
An example of this would be:
componentMarkup(): TemplateResult {
return html`<wc-my-menu opened?="${this.isMenuOpen}"></wc-my-menu>`;
}
Passing props down the hierarchy of components allows them to managed from outside itself. In the case above, this means that my-menu
doesn't need to include any buttons to open / close itself. These can live somewhere else.
The result of this is that you can create very small blocks of UI functionality and stitch them together in all kinds of different, application specific ways - allowing you to easily re-use components over and over for different scenarios.
Property Rules
1. Triggering re-renders
There are some specific rules when it comes to property watching and re-rendering. The biggest gotcha is that the watcher doesn't deep watch. For example:
@prop({ type: Array })
myNumbers: number[] = [1,2,3];
...
onClick = (): void => {
this.myNumbers.push(4);
}
When clicking onClick
in this instance, nothing will re-render. The reason why is that we're not watching for changes inside the myNumbers
array. A much more likely scenario is where you have an Array
of Objects
- changing a property on one of the Objects
also will not trigger a re-render.
The solution to this is to change the instance of the array. For example, the correct way to write the onClick
function from above would be:
onClick = (): void => {
this.myNumbers = [...this.myNumbers, 4];
}
Using the spread ...
operator, we expand myNumbers
into a new instance of an Array
and then add the next element. Creating a new instance of the array will trigger a re-render.
2. What should be a prop?
You may be thinking that by default everything should be a prop
. But this isn't necessarily the case. The general rule is:
If changing your property should require a UI update, then it should be a prop.
This is still quite a general rule, so let's talk through an example.
Say we have a list of objects that we need to display in a component. Let's also say that we can pick one of the items in this list and our component needs to track which the currently selected item is. Your first thought to create this might be to use two props:
@prop({ type: Array })
characters: Character[] = [
{ firstName: "Cloud", lastName: "Strife" },
{ firstName: "Tifa", lastName: "Lockhart" },
{ firstName: "Barret", lastName: "Wallace" },
];
@prop({ type: Object })
selectedCharacter: Character = null;
...
onClick = (idx: number): void => {
this.selectedChatacter = this.characters[idx];
}
In this example, if the list doesn't need to change, then the characters
array doesn't need to be a prop.
3. Use primitive type
s when possible
There's also another optimisation we can make to the example above. The selectedCharacter
is storing the entire object as its value when we don't need to.
@prop({ type: Number })
selectedCharacter: number = -1;
...
onClick = (idx: number): void => {
this.selectedChatacter = idx;
}
Changing selectedCharacter
to instead track the selected index rather than a reference to the object itself is usually better. In this tiny example where everything is encapsulated into a singlar component, the benefits aren't clear. But when you have a component tree and you're passing references down the tree; managing this and when updates occur can be tricky.
For example, if we changed the lastName
property on the object, we somehow have to tell the whole component tree to re-render. This is difficult as we don't deep watch and if not done properly, can lead to components each having a slightly different state for the selectedCharacter
. This results in tricky bugs to fix and gets even harder when we're using persistent state.
Where possible, you should look to use primitive type
s for properties and update the object from within the characters
array. This means that the array will always be the single source of truth; rather than references on props.
Note
You may not be able to work out from the example why this is a real problem. The example is a little too trivial to explain the concept fully.
Property Wrap Up
That's it for the basic usage of properties. Usually, components will have quite large lists of properties - especially if they are containing a lot of logic that can be influenced from outside the component itself (i.e. usually for generic components).
Don't be afraid to add props or to configure behaviour behind a prop. They are quite efficient and are the key to making powerful components.