Organizing Your App with Panels
In the previous step we built our list view and added some formatting to the Kitten component. Now, learn about the Panels components and move our list view into its own panel.
Creating a Panel
Let’s start by creating a new view component, Detail
, which will be the future home of a detail view when a kitten is selected from the list view. We’ll import
the Panel
component as well as its Header
. Unlike the other components we’ve encountered, the Panels-related components are all exposed as named exports on the @enact/sandstone/Panels
module. Since they are generally used together, bundling them into a single module makes importing them a bit simpler.
./src/views/Detail.js
import kind from '@enact/core/kind';
import {Header, Panel} from '@enact/sandstone/Panels';
import PropTypes from 'prop-types';
const genders = {
m: 'Male',
f: 'Female'
};
const DetailBase = kind({
name: 'Detail',
propTypes: {
color: PropTypes.string,
gender: PropTypes.string,
name: PropTypes.string,
weight: PropTypes.number
},
defaultProps: {
color: 'Tabby',
gender: 'm',
weight: 9
},
render: ({color, gender, name, weight, ...rest}) => (
<Panel {...rest}>
<Header title={name} />
<div>Gender: {genders[gender]}</div>
<div>Color: {color}</div>
<div>Weight: {weight}oz</div>
</Panel>
)
});
export default DetailBase;
export {
DetailBase as Detail,
DetailBase
};
Hopefully, the code for a stateless component is beginning to look pretty familiar. We’ve declared a few props that our component will support. Since our data is only names, we’ve also added some default values to fill out the screen. We don’t need any computed properties right now nor any custom CSS so both of those keys have been omitted. The render method simply returns a Panel with a Header and some content.
There are a couple of things to discuss, however. First, we want to add a propType
validator function on gender
. Second, there is a bit of magic going on here with Panel and Header: the Slottable
HOC.
When you define props in
propTypes
anddefaultProps
, the props names should be ordered alphabetically. See sort-prop-types for more information.
More Advanced PropTypes
We have a small problem with our Detail
view. We don’t validate that the gender we receive matches one of the genders we expect. One way we can address that is to use propTypes
to validate that we only receive the data we expect (at least, while we’re running the app in development mode). We can quickly change the validator to check the data for us:
gender: PropTypes.oneOf(['m', 'f']),
Using PropTypes.oneOf()
allows us to specify a list of acceptable values for gender
. In addition to the primitives we’ve used previously, React provides other validator functions you can use to limit possible values like above or validate more complex properties.
Validators, as mentioned, only run when in development mode. Further, they only warn if there is a problem. It’s still possible to pass bad data in. When data may come from sources you don’t control, you’ll want perform more validation, perhaps in a
computed
section.
Using Slottable
to Distribute Children
The Slottable
HOC was inspired by the Web Components Slot API as a means for consumers of a component to use a more semantic and “markup friendly” interface to its internal API. In general, you won’t need to know if a component is using Slottable
but it’s worth spending a little time understanding how it works.
Slottable
works by mapping children to props. This means that the component author is able to write idiomatic React components relying only on props whereas the component consumer can write more “markup friendly” code. The primary use case for Slottable
is when a component expects a property to receive one or more elements rather than a primitive value.
Consider the case of the header
property of Panel. The React way to specify a component for that property would be:
<Panel header={<Header title="Title" />}>
<div>Panel Body</div>
</Panel>
With Slottable
, you can write either the above code or the following:
<Panel>
<Header title="Title" />
<div>Panel Body</div>
</Panel>
This works because Panel has configured a header
slot and the Header component has been pre-configured to use the header
slot using a defaultSlot
property set on the Header component. Another way to specify the target slot for a component is to add the slot
property to your component instance. In this example, the first <div>
tag will be mapped to the header
property.
Note that the
slot
property will be removed from the<div>
before being passed to the receiving component.
<Panel>
<div slot="header">Title</div>
<div>Panel Body</div>
</Panel>
Refactoring the List View
With the basics of Panels
under our belts, refactoring our list into a Panel
should be straightforward. We’ve only declared a single property, children
, which will receive the array of kittens to display. The render method contains the same Panel
setup code as above with the addition of the Repeater code from ./src/App/App.js
.
Because we set the default image size to 300
in .src/components/Kitten/Kitten.js
, the six images of the array of kittens we set up in .src/App/App.js
may not be fully visible in Panel
. Therefore, adding a scroller within Panel
makes all images visible well. We’ll import
the Scroller
component from the @enact/sandstone/Scroller
module. Also, it is required to define width
and height
property of <img />
tag so that the Scroller
knows whether it is scrollable or not. See what happens if you reduce the size of your web browser when you don’t add a scroller.
./src/views/List.js
import kind from '@enact/core/kind';
import {Header, Panel} from '@enact/sandstone/Panels';
import Scroller from '@enact/sandstone/Scroller';
import Repeater from '@enact/ui/Repeater';
import PropTypes from 'prop-types';
import Kitten from '../components/Kitten';
const ListBase = kind({
name: 'List',
propTypes: {
children: PropTypes.array
},
render: ({children, ...rest}) => (
<Panel {...rest}>
<Header title="Kittens!" />
<Scroller>
<Repeater childComponent={Kitten} indexProp="index">
{children}
</Repeater>
</Scroller>
</Panel>
)
});
export default ListBase;
export {
ListBase as List,
ListBase
};
./src/components/Kitten/Kitten.js
import kind from '@enact/core/kind';
import PropTypes from 'prop-types';
import css from './Kitten.module.less';
const KittenBase = kind({
/* omitted */
render: ({children, size, url, ...rest}) => {
delete rest.index;
return (
<div {...rest}>
<img src={url} alt="Kitten" width={size} height={size} />
<div>{children}</div>
</div>
);
}
});
export default KittenBase;
export {KittenBase as Kitten};
Panels
@enact/sandstone/Panels
provides a simple single Panel view filling the entire screen and it allows the user to navigate to the previous Panel with a back button.
Our Kitten Browser will use Panels
, with the List
as the first view and Detail
as the second. We’ve kept the data here and will pass that down to List
as children
. It’s also notable that we’re using the spread operator on props directly. We don’t need to deconstruct any props for our App
component so we’ll pass everything on to Panels
.
import kind from '@enact/core/kind';
import {Panels} from '@enact/sandstone/Panels';
import ThemeDecorator from '@enact/sandstone/ThemeDecorator';
import Detail from '../views/Detail';
import List from '../views/List';
const kittens = [
'Garfield',
'Nermal',
'Simba',
'Nala',
'Tiger',
'Kitty'
];
const AppBase = kind({
name: 'App',
render: (props) => (
<Panels {...props}>
<List>{kittens}</List>
<Detail />
</Panels>
)
});
const App = ThemeDecorator(AppBase);
export default App;
export {
App,
AppBase
};
Conclusion
In this fourth step of Kitten Browser, we’ve introduced the Panels
components and how Slottable
makes it easy to distribute children into a component in a more semantic and markup friendly format.
You may have noticed that the Detail
view we created is not visible. Panels
have index
property that is used to navigate the Panel
. Even if we didn’t define the property, the List
component which is the first child of Panels
is being displayed since the property’s default value is 0
. In the next step, we’ll see how to manage the index
property so that we could navigate List
and Detail
.