Add a New Component Using TypeScript and Enact
Now we can look at using TypeScript with the Enact kind() factory to accomplish the same goal.
Counter Component using TypeScript with Enact
We can use the same Counter.tsx file. Replace the previous contents with the following:
// Counter.tsx
import kind from '@enact/core/kind';
import Button from '@enact/sandstone/Button';
const CounterBase = kind({
name: 'Counter',
// Add propTypes until we replace with interface?
defaultProps: {
count: 0
},
render: ({onIncrementClick, onDecrementClick, onResetClick, count, ...rest}) => (
<div {...rest}>
<h1>{count}</h1>
<Button onClick={onDecrementClick}>Decrement --</Button>
<Button onClick={onResetClick}>Reset</Button>
<Button onClick={onIncrementClick}>Increment ++</Button>
</div>
)
});
export default CounterBase;The above code holds the definition of the Counter component. This is a simple stateless component that does not leverage any of the features of TypeScript. It relies on the parent managing the state by passing in the event handlers.
Please check API documentation in the Core Library to know more about kind.
Let’s extend this simple component to be properly typed and add state handling using the reusable higher-order components Enact provides.
View the Counter in the Browser
Adding Typing
This does work, mainly because we haven’t modified the MainView component to pass the additional props this component needs. If we did, we would likely get TypeScript errors because the props we are expecting aren’t typed. Let’s add an interface to CounterBase that will describe the props we expect:
// Counter.tsx
import kind from '@enact/core/kind';
import Button from '@enact/sandstone/Button';
import {MouseEvent} from 'react';
interface Props {
count? : number,
onDecrementClick? : (ev: MouseEvent) => void,
onIncrementClick? : (ev: MouseEvent) => void,
onResetClick? : (ev: MouseEvent) => void
}
const CounterBase = kind<Props>({
name: 'Counter',
defaultProps: {
count: 0
},
render: ({onIncrementClick, onDecrementClick, onResetClick, count, ...rest}: Props) => (
<div {...rest}>
<h1>{count}</h1>
<Button onClick={onDecrementClick}>Decrement --</Button>
<Button onClick={onResetClick}>Reset</Button>
<Button onClick={onIncrementClick}>Increment ++</Button>
</div>
)
});
export default CounterBase;Note: We’ve left the event handlers optional so we don’t have to update App.js and we’ll be replacing them in the next section.
Counter View in the Browser
Adding State Handling
- So far we have a component that has
onClickevent but at this point we are unable to manage the state ofcountprop.
For state management on count prop will use ui/Changeable.
Applying
Changeableto a component will pass two additional props: the current value from state and an event callback to invoke when the value changes. For more information, read the ui/Changeable documentation
- Create a handle function for click events on the button. The
createHandlerfunction will take a function as input then use the function to update thecount. By usinghandle()we will forward the call to the callback function (onCounterChange) defined via the configuration object passed toChangeable:
// Counter.tsx
import {adaptEvent, forward, handle} from '@enact/core/handle';
import kind from '@enact/core/kind';
import Button from '@enact/sandstone/Button';
import Changeable from '@enact/ui/Changeable';
import PropTypes from 'prop-types';
interface Props {
count? : number,
onCounterChange? : void
}
type HandlerFunctionType = (count: number) => number;
const createHandler = (fn: HandlerFunctionType) => {
return handle(
adaptEvent((ev, {count}) => ({
type: 'onCounterChange',
count: fn(count)
}), forward('onCounterChange'))
);
};
const CounterBase = kind<Props>({
name: 'Counter',
propTypes: {
count: PropTypes.number
},
defaultProps: {
count: 0
},
handlers: {
onDecrementClick: createHandler(count => count - 1),
onIncrementClick: createHandler(count => count + 1),
onResetClick: createHandler(() => 0)
},
render: ({onIncrementClick, onDecrementClick, onResetClick, count, ...rest}) => {
delete rest.onCounterChange;
return (
<div {...rest}>
<h1>{count}</h1>
<Button onClick={onDecrementClick}>Decrement --</Button>
<Button onClick={onResetClick}>Reset</Button>
<Button onClick={onIncrementClick}>Increment ++</Button>
</div>
)
}
});
const Counter = Changeable({prop: 'count' , change: 'onCounterChange'}, CounterBase);
export default Counter;
export {
CounterBase,
Counter
};Note: The
createHandlerfunction is simply a shortcut to allow us to avoid duplicating the same piece of code three times (once for each of the events we need to handle). What the code does is take a function that modifies thecountvalue and returns the new value. It takes the incoming click event and then creates a new event to pass to theonCounterChangeevent fromChangeable, passing it the value modified by the function.
Note: Because the
onCounterChangeevent is being supplied byChangeable, we’ll mark it as optional since we don’t want it to be required from its parent.
Counter View in the Browser
Error Handling
Building the Counter app, you might get the following TS error, when count is added as a default prop but type is not defined.
TypeScript error: Parameter 'count' implicitly has an 'any' type.In case of above error, you need to explicitly define the count type or define the type of count in the interface.
interface IProps {
count? : number
}OR
let count : number = 0;Conclusion
Using TypeScript with Enact we were able to create a reusable counter component. Integrating TypeScript with Enact helped us to extend our knowledge in using kind() and handle() in developing a reusable component.
The code for this tutorial can be found here: https://github.com/enactjs/samples/tree/master/tutorial-typescript. Happy coding!