Creating Components the Enact Way
The Enact framework is built upon the foundation of React.
As a result, any component that is built for React can be used in Enact and any of the patterns for
creating a component in React can also be used in Enact. To address some of the common use cases we
encountered, we’ve included a few modules with @enact/core
that standardize the “Enact way” for
and provide some ‘sugar’ to reduce developer boilerplate.
Creating Stateless Functional Components
We’ve found that Stateless Functional Components (SFCs) are a great way to decompose your application. A primary role of a user interface component is to provide a mapping of the Computer’s model into the User’s mental model. SFCs are a pure implementation of this responsibility. They accept an object of properties and map them into a component hierarchy using JSX.
In very simple components, a single function is sufficient to perform that mapping. However, as
components grow more complex, you often find that you need to merge incoming data with fixed data or
transform incoming data into other formats. This often leads you to decompose logic out of your main
render method for improved clarity and maintainability. This process led us to create
@enact/core/kind
.
The kind()
factory creates SFCs from a configuration object. It adds declarative sugar for setting
the display name, merging incoming className
and style
properties with component values, and computing property
values, allowing you to merge or transform incoming data outside of your render function.
Unlike simple functions which must be declared before any metadata can be attached, kind()
components encourage a consistent ordering of keys for top-down readability:
The
name
component accepts thesepropTypes
, which have the following default values,defaultProps
. It is formatted according to the CSS modules map andclassName
instyles
. That data is used to produce severalcomputed
properties which ultimately are provided torender
to create the final component hierarchy.
For the following sample, the 'Badge'
component accepts the children
and greeting
properties, with
greeting
having a default of 'Hello, my name is ...'
. It applies the 'badge'
className
(combined
with any passed-in className
), it computes a new value for children
and renders the result.
import kind from '@enact/core/kind';
import PropTypes from 'prop-types';
const Badge = kind({
name: 'Badge',
propTypes: {
children: PropTypes.string.isRequired,
greeting: PropTypes.string,
},
defaultProps: {
greeting: 'Hello, my name is ...'
},
styles: {
css,
className: 'badge'
},
computed: {
children: ({children, greeting}) => `${greeting} ${children}`
},
render: ({children, ...rest}) => {
delete rest.greeting;
return (
<div {...rest}>
{children}
</div>
);
}
});
Note: You should be sure to delete any properties that are only used in calculations to avoid warnings during rendering.
SFCs Compared with React Components
While SFCs have some important benefits, not every problem can be effectively solved with them
alone. Sometimes creating component instances that extend React.Component
is necessary. Here are a
few possible reasons:
- You need access to the component lifecycle methods
- You need to maintain some component state (and it’s not managed by something like Redux)
- You need consistent event handler references to prevent unnecessary renders
- You need to expose imperative APIs (though you should always avoid this when possible)
The way in which you would achieve each of these is beyond the scope of this discussion but each are
valid reasons to create a custom instance of React.Component
. However, these reasons do not
preclude you from creating an SFC for the primary render logic. Your React.Component
can do
whatever work it needs to do and defer the property-to-user-interface mapping to the SFC.
Higher-Order Components
Higher-Order Components (HOCs) are useful for adding behavior or chrome UI around other independent
components. In the general sense, they are functions that accept a component (a native DOM node like
div
, an SFC, or a React.Component
), decorate that component in some way, and return either the
original component or a new component that wraps the original component.
Enact provides several HOCs within @enact/ui
and @enact/sandstone
that allow us to provide consistent
behaviors across components. All of these HOCs were created using the hoc()
factory from
@enact/core/hoc
. This factory gives them a couple key features:
- HOCs can be configurable by passing an object with parameters to the HOC function. This object is merged with a set of default configuration parameters.
- HOCs are flexible in their usage. They can:
- Accept a configuration object and a component
const ToggleableWidget = Toggleable({toggle: 'onClick', prop: 'selected'}, Widget);
- Accept only a component and use the default configuration:
const ToggleableWidget = Toggleable(Widget);
- Accept a configuration object in one invocation and a component in a second invocation. This
allows you to reuse a pre-configured HOC on multiple components:
const ToggleDecorator = Toggleable({toggle: 'onClick', prop: 'selected'}); const ToggleableWidget = ToggleDecorator(Widget); const ToggleableFrob = ToggleDecorator(Frob);
- Accept a configuration object and a component
If you need to create your own HOCs, you can import the hoc()
factory to take advantage of these
features. The factory accepts an optional default configuration object and a function. The
function will receive the merged configuration object and the Wrapped component and should return
a component.
Here’s a simple example to illustrate:
import {Component} from 'react';
const Countable = hoc({prop: 'data-count'}, (config, Wrapped) => {
return class extends Component {
constructor (props) {
super(props);
this.state = {
count: 0
};
},
inc = () => this.setState({count: this.state.count + 1}),
render () {
const props = Object.assign({}, this.props, {
[config.prop]: this.state.count,
onClick: this.inc
});
return <Wrapped {...props} />
}
}
});
const CountableAsDataNumber = Countable({prop: 'data-number'});
const CountableDiv = Countable('div');
const CountableDivAsDataNumber = CountableAsDataNumber('div');
Customizing Components at Design-Time
Occasionally, you’ll want to modify the appearance of an Enact component, and usually, simply applying external styling to the outer-most element of the component, via className
or style
will work just fine. However, what if you need to customize one of the deeper child elements?
We’ve got you covered! Since Enact 2.0 we’ve added a built-in theming capability to make this significantly easier and even safer. Using the theming system is as straight-forward as importing your CSS/LESS file and passing it to the css
prop on the component you want to customize. The class names defined in your CSS file that match the published class names of the target component will be applied directly to the internal elements of the component. They will be applied in addition to the existing class names, not in lieu of, so you can simply add your customizations, rather than repeat the existing styling. Each customizable component will include documentation for the css
prop, which will list what classes are available and a brief description of what role they play.
How about an example to make this more clear. Let’s customize the background color of a sandstone/Button
. Button
exposes several classes for customization: ‘button’, ‘bg’, ‘small’, and ‘selected’, and in this case we’re interested in ‘button’ and ‘bg’. In our customized component LESS file, the following should do the trick:
// CustomButton.less
//
@import '~@enact/sandstone/styles/skin.less';
.button {
.applySkins({
.bg {
background-color: orange;
}
});
}
The .applySkins
is added here because Sandstone uses our skinning system too, which is in charge of applying colors independent from measurements, layout, and metrics.
Then, in our component we’ll just apply the imported LESS file to the component with the css
property.
import kind from '@enact/core/kind';
import Button from '@enact/sandstone/Button';
import css from './CustomButton.less';
const CustomButton = kind({
name: 'CustomizedButton',
render: ({children, ...rest}) => (
<Button {...rest} css={css}>{children}</Button>
)
});
export default CustomButton;
Now, all we do in our app is import this CustomButton
like any other, and it will be styled with our custom styling.
import CustomButton from './CustomButton';
...
<CustomButton>Our Orange Button</CustomButton>
For more details and advanced theming features and recommendations, see our Theming Guide.
Customizing Sandstone skin at Runtime
Sometimes, you might want to change the appearance of components even after the application is built, for example, changing the color of the text in components or the background color of focused components globally.
As we mentioned earlier, Enact provides several ways to customize the appearance of components but these methods are at design time. Since Sandstone 2.1, we’ve added Sandstone skin customization feature to support runtime customization. Thanks to this feature, you can customize the appearance of Sandstone components after the application is built. It allows you to customize the skin of your app without code modification. Moreover, you can even let app users define skin customization for your app.
You might wonder how does this work? We’ve made a list of CSS variables and made those variables can override the Sandstone skin. This approach makes style changes work properly and safely after the build. As of now, we support colors but we hope we could expand this feature beyond that.
All you need to do is build your app with --custom-skin
option and add a CSS file named custom_skin.css
which includes a preset of colors, under the customizations
folder in the build result like below. Make sure you’ve installed Enact CLI 5.1.0 or later.
enact pack --custom-skin
my-app/
README.md
.gitignore
package.json
dist/
customizations/
custom_skin.css
main.css
main.js
...
node_modules/
src/
resources/
webos-meta/
You can make custom_skin.css
file from the Sandstone custom-skin sample. The sample also supports a preview of your customized skin. Pressing SHOW OUTPUT
button will pop up the customized CSS and DOWNLOAD
button will download your customized custom_skin.css
file. The content of an example of custom_skin.css
file looks like this:
// custom_skin.css
//
.sandstone-theme {
--sand-bg-color: #000000;
--sand-text-color-rgb: 230, 230, 230;
--sand-component-text-color-rgb: 230, 230, 230;
--sand-component-bg-color: #7D848C;
--sand-component-active-indicator-bg-color: #E6E6E6;
--sand-component-inactive-indicator-bg-color: #9DA2A7;
}
Note: You should be sure to put RGB-separated values in the CSS variable names ending with
-rgb
if you edit the value in the file directly.