In this chapter you can learn how to implement a custom Trait through an example of a Timer Trait.
Trait development and Component have many similarities.
Trait | Component | |
---|---|---|
Owned Sepc | ✅ | ✅ |
Has properties, state, methods | ✅ | ✅ |
Has Implementation | ✅ | ✅ |
Implementation is a function | ✅ | ✅ |
Trait is essentially a function. The role of Trait is to increase the capabilities of the Component. If the object-oriented analogy is still used, Trait is a decorator of Component. For example, adding styles, adding state, etc.
The main idea of Trait is to accept a series of parameters, run some logic, and return TraitResult, thereby enhancing the capabilities of the Component. Obviously, this is the logic of a pure function. Therefore, we recommend implementing Trait as a pure function. Even if it is not a pure function, the implementation needs to consider the result when the Trait is repeatedly executed, and try to avoid unexpected side effects.
⚠️ Since Trait is a pure function, React hooks cannot be used in Trait.
Since Trait doesn't have its own id, Trait and Component share id. That is to say, all the States and Methods exposed by the Trait will be added to the Component, and accessing and calling them is no different from what the Component implements itself.
Next, let's learn how to develop a Trait with an example. We want to implement a Timer Trait that has the following capabilities:
- Specify a timer for a period of time, and an alert will pop up after the timer expires
- Provides the function of clearing the timer
- You can choose whether to start a timer immediately after the Component is mounted
- Get timer status (
waiting
,finished
,stopped
)
First, let's write a Trait Spec and define some basic information for the Trait, such as version and name:
{
version: 'custom/v1',
metadata: {
name: 'timer',
description: 'Create a timer to alert, and could be clear.',
},
spec: {
properties: PropsSpec, // see below for details
state: StateSpec, // see below for details
methods, // see below
},
}
Next, we can start thinking about how to design our Timer Trait. First, we need to design the parameters that need to be passed into the Timer Trait, that is, the Spec of Properties.
First you need a time
parameter to control the time of the timer, and a content
parameter to display the content of the alert.
In addition, you need to control whether to trigger immediately after the Component is mounted, so there is also an immediate
parameter.
So we can get our Props Spec:
const PropsSpec = Type.Object({
time: Type.Number(),
content: Type.String(),
immediate: Type.Boolean(),
});
Trait's Props Spec is also defined by TypeBox.
Since Timer Trait also needs to expose state, we also need to add State Spec to define the State exposed by Timer Trait and its type.
const StateSpec = Type.Object({
status: Type.KeyOf(
Type.Object({
waiting: Type.Boolean(),
finished: Type.Boolean(),
stopped: Type.Boolean(),
})
),
});
We also need to provide Method to call externally to start or clear the timer, so we also need to define Method. Here we provide start
and stop
two methods.
const methods = [
{
name: 'start',
parameters: Type.Object({}),
},
{
name: 'clear',
parameters: Type.Object({}),
},
];
After defining the complete Trait Spec, we can start implementing the logic of Timer Trait. The first thing to do is to implement a factory function of a Trait, which returns the Implementation of the real Trait.
In the factory function, we can declare some variables to store data, such as Timer Trait Here we need to create a Map to record the Timer corresponding to each Component so that it can be used to clear the timer.
Since Trait is essentially a pure function and cannot have state, Trait needs a factory function. The factory function provides a closure to store state for the trait. It should be noted that for the same Trait, all Trait Implementation instances share the same js closure.
Let's take a look at the complete implementation code:
const TimerTraitFactory = () => {
const map = new Map();
return props => {
const {
time,
content,
immediate,
services,
componentId,
subscribeMethods,
mergeState,
} = props;
// implement clear method
const clear = () => {
const timer = map.get(componentId);
if (timer) {
clearTimeout(timer);
mergeState({
status: 'stopped',
});
map.delete(componentId);
}
};
// implement start method
const start = () => {
clear();
const timer = setTimeout(() => {
// When the timer expires, an alert pops up
alert(content);
mergeState({
status: 'finished',
});
}, time || 0);
mergeState({
status: 'waiting',
});
map.set(componentId, timer);
};
// register the method to the component
subscribeMethods({
start,
clear,
});
// return Trait Result
// Use the componentDidMount and componentDidUnmount lifecycles to start and clear timers
return {
props: {
// start the timer after the Component hangs
componentDidMount: [
() => {
if (immediate) {
start();
}
},
],
componentDidUnmount: [
() => {
clear();
},
],
},
};
};
};
Finally, generate the final Trait through the implementRuntimeTrait
provided by runtime
. So far, the complete implementation of Timer Trait is completed here.
export default implementRuntimeTrait({
version: 'custom/v1',
metadata: {
name: 'timer',
description: 'Create a timer to alert, and could be clear.',
},
spec: {
properties: PropsSpec,
state: StateSpec,
methods,
},
})(TimerTraitFactory);
Trait Spec and Component are basically the same, with only a few differences.
| parameter name | type | description |
| -------- | --------- | ------------------------------ -------- |
| version | string | |
| kind | "Trait"
| Fixed, indicating that this is a Trait Spec. |
| metadata | | See below for details |
| spec | | See below for details |
The metadata content of Trait Spec is less than that of Component.
parameter name | type | remarks |
---|---|---|
name | string | Trait's name |
annotations | Record<string, any> |
parameter name | type | remarks |
---|---|---|
properties | JSONSchema | |
state | JSONSchema | |
methods | Record<KMethodName, JSONSchema> |
The parameters accepted by Trait Implementation are almost the same as Component, you can refer to the relevant chapter of Component.
TraitResult is the most important part of Trait. It is the return result of the Trait function and will be passed directly to the Component as a parameter. For more details, please refer to the relevant chapters of the parameters of Component Implementation.
TraitResult is an object with the following properties:
Parameter name | Type | Required | Description |
---|---|---|---|
customStyle | Record<string, string> | No | Style map passed to the Component. The value of the object should be a CSS string. |
callbackMap | Record<string, Function> | No | Map of callback functions for Component. Mainly used for Event Trait. |
componentDidUnmount | Function[] | No | Lifecycle hook. Will be executed after the Component is unloaded. |
componentDidMount | Function[] | No | Lifecycle hook. Will be executed after the Component is mounted. |
componentDidUpdate | Function[] | No | Lifecycle hook. Will be executed after the Component is updated. |