Migration guide: List to ListComposition - Talend/ui GitHub Wiki
- 0. Pre-requisite
- 1. HomeListView
- 2. List composition starter
- 3. didMountActionCreator / collectionId
- 4. Column definitions (list > columns)
- 5. Toolbar
- 6. Selection
- 7. Actions
- 8. Clean
You need to know how it works in your app. You'll likely
- use
HomeListView
component - have a
HomeListView
settings that is specialized via props settings. ExampleHomeListView#preparations
What you need to do:
- gather the props settings (they will become just props)
- gather the actions the props settings refer to
If you use the HomeListView
component, we need to inject the custom composed list. To do so:
- Create a
HomeListView
wrapper. Example:PreparationHomeListView
. It will instanciate aHomeListView
component. - Pass the props directly the
HomeListView
component props, except thedidMountActionCreator
andlist
part. Both will be managed by your specialized composed list. - Pass a
list
prop, that is the new instance of List (don't worry, we are going to build it).
import React from 'react';
import HomeListView from '@talend/react-containers/lib/HomeListView';
import PreparationList from '../PreparationList.connect';
export default function PreparationHomeListView(props) {
return (
<HomeListView
id="home-preparations"
hasTheme
header={{}}
sidepanel={{}}
{...props}
list={<PreparationList />}
/>
);
}
PreparationHomeListView.displayName = 'PreparationHomeListView';
- Register it in cmf, and refers to it in your router settings.
cmf.bootstrap({
...otherConfigurations
components: {
...otherComponents,
PreparationHomeListView,
}
});
{
"routes": {
"path": "/",
"component": "App",
"childRoutes": [
{
"path": "preparations/:folderId",
"component": "PreparationHomeListView"
}
]
}
}
If you don't use HomeListView, it's even easier, you have to register and refer to the new composed List instead of the List container in your cmf settings.
Let's set the base files to start implementing our custom list.
- Create a custom list composition file
// PreparationList.component.js
import React from 'react';
import List from '@talend/react-components/lib/List/ListComposition';
import './PreparationList.scss';
export default function PreparationList(props) {
return (
<List.Manager id="preparations" collection={collectionWithActions}>
<List.Toolbar></List.Toolbar>
<List.VList id="preparations-list"></List.VList>
</List.Manager>
);
}
- Create a connect file
// PreparationList.connect.js
import { connect } from 'react-redux';
import cmf from '@talend/react-cmf';
import PreparationList from './PreparationList.component';
function mapStateToProps(store) {
return {};
}
const mapDispatchToProps = {};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(PreparationList);
If you use didMountActionCreator
to fetch your collection and collectionId
to inject them back, let's handle that.
As a matter of fact, instead of registering all the actions that will be only use in your list component, import them.
To fetch on component mount, use useEffect
hook.
// action creators file
export default {
fetchPreparations,
};
// connect file
import actions from './preparation.actions';
const mapDispatchToProps = {
fetchPreparations: actions.fetchPreparations,
};
// Custom list component file
export default function PreparationList(props) {
useEffect(() => {
props.fetchPreparations();
}, []);
}
To inject the collection, use cmf collection selector
// connect
function mapStateToProps(store) {
return {
collection: cmf.selectors.collections.toJS(store, 'preparations'),
};
}
// Custom list component
export default function PreparationList(props) {
const { collection } = props;
}
VirtualizedList has now a better api to use the different cell types.
<List.Manager id="preparations" collection={props.collection}>
<List.VList id="preparations-list">
<List.VList.Title
dataKey="name"
label={t('tdp-app:NAME', { defaultValue: 'Name' })}
/>
<List.VList.Text
dataKey="author"
label={t('tdp-app:AUTHOR', { defaultValue: 'Author' })}
/>
<List.VList.Datetime
dataKey="created"
label={t('tdp-app:CREATED', { defaultValue: 'Created' })}
columnData={{ mode: 'ago' }}
/>
</List.VList>
</List.Manager>
For old titleProps
settings, set them directly as Title cell columnData
props. Notice that you don't need the key prop anymore, it was used to identify the title column. Now it's identified by the use of List.VList.Title
.
If you need to set columns data depending on the item, you can pass a function as columnData.
// static columnData to apply to every items
<List.VList.Title
dataKey="name"
label={t('tdp-app:NAME', { defaultValue: 'Name' })}
columnData={{
'data-feature': 'entity.open'
}}
/>
// dynamic columnData
<List.VList.Title
dataKey="name"
label={t('tdp-app:NAME', { defaultValue: 'Name' })}
columnData={item => ({
'data-feature': `entity.open.${item.id}`
onClick: openEntity(event, { model: item }),
})}
/>
For sort/filter/displayMode, use the new List toolbar api
<List.Manager id="preparations" collection={collection}>
<List.Toolbar>
<List.TextFilter />
<List.SortBy
options={[
{ key: 'name', label: labels.name },
{ key: 'author', label: labels.author },
{ key: 'created', label: labels.created },
{ key: 'modified', label: labels.modified },
{ key: 'datasetName', label: labels.dataset },
{ key: 'nbSteps', label: labels.steps },
]}
initialValue={{ sortBy: 'name', isDescending: false }}
/>
<List.DisplayMode />
</List.Toolbar>
</List.Manager>
If you don't need to store them (most of the time you don't), let them uncontrolled, like the example above. It just works.
If you want to manage a custom filter function for some columns, you have to switch to controlled mode. But using the useCollectionFilter
hook, you can still have the default filter for the columns you don't override.
const filterFunctions = {
quality: (qualityValue, filterValue) => {
/* ... */
}, // custom filter function for "quality" column
};
const { filteredCollection, textFilter, setTextFilter } = useCollectionFilter(
collection,
'', // initialTextFilter
filterFunctions, // override filter for "quality" column, leaving the default for the other columns
);
<List.Manager id="preparations" collection={filteredCollection}>
<List.Toolbar>
<List.TextFilter value={textFilter} onChange={(event, value) => setTextFilter(value)} />
</List.Toolbar>
</List.Manager>;
Same as filter, you can override the default sort function for the columns you want, leaving the default sort function for the rest. For that you have to switch to controlled mode using useCollectionSort
.
const sortFunctions = {
quality: ({ sortBy, isDescending }) => (a, b) => {
/* ... */
}, // custom sort function for "quality" column
};
const { sortedCollection, sortParams, setSortParams } = useCollectionSort(
collection,
{}, // initialSortParams
sortFunctions, // override sort for "quality" column, leaving the default for the other columns
);
<List.Manager id="preparations" collection={filteredCollection}>
<List.Toolbar>
<List.SortBy value={sortParams} onChange={(event, value) => setSortParams(value)} />
</List.Toolbar>
<List.VList
sortBy={sortParams.sortBy}
sortDirection={sortParams.isDescending ? 'DESC' : 'ASC'}
sort={({ sortBy, sortDirection }) =>
setSortParams({ sortBy, isDescending: sortDirection === 'DESC' })
}
>
{/* ... */}
</List.VList>
</List.Manager>;
We have a new hook to manage selection
import { hooks } from '@talend/react-components/lib/List/ListComposition';
const {
selectedIds,
isSelected,
onToggleAll,
onToggleItem,
} = hooks.useCollectionSelection(
collection, // your array of items
initialSelectedIds, // it's controlled, so you can provide the selected ids at start
idKey = 'id', // the property name in each item containing the id
);
<List.Manager id="preparations" collection={collection}>
<List.VList
isSelected={isSelected}
onToggleAll={onToggleAll}
selectionToggle={onToggleItem}
>
{/* ... */}
</List.VList>
</List.Manager>
If you have different sets of actions for selection/no-selection, you can condition your Actions components based on selectedIds variable.
In your cmf settings, the list actions contain
- title: the action on item title click
- left: the actions in the toolbar
- items: the actions in title cell that appear on hover
- editSubmit/cancelSubmit: the name edition callbacks
How is it done today ?
- Those actions reference action ids. Those are actions definitions in the
actions
part of the settings. - The actions definitions are basically Action component props, with a reference to the action creator id to dispatch.
- The action creators are registered in the cmf registry in your app js code.
For each one of them
- Remove the action register. We won't refer to them in the settings anymore
- Import the action creator in the custom list redux-connect file, and map them to the props
// PreparationList.connect.js
import folder from 'path/to/actions/folder';
import preparation from 'path/to/actions/preparation';
import PreparationList from './PreparationList.component';
function mapStateToProps(state) {}
const mapDispatchToProps = {
fetchPreparations: preparation.fetch,
exportPreparation: preparation.exportPreparation,
importPreparation: preparation.importPreparation,
openAddPreparationForm: preparation.openAddForm,
openAddFolderForm: folder.openFolderCreatorModal,
openMoveForm: preparation.openMoveModal,
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(PreparationList);
- Now your actions dispatcher are in the custom list component props, injected by redux
- Each kind of actions (title, left, items, edit/cancel) must be added in their right places, in jsx.
The toolbar is a place where you can add the provided controls (sort, filter, ...), and any other control you'd like.
- Get the left action ids
"actions": {
"left": [
"preparation:add:open",
"preparation:add:upload",
"folder:add:open",
"preparation:import"
],
}
- Identify them in the
actions
definitions part of cmf settings. You can remove those settings if they are used only in the list.
"preparation:add:open": {
"id": "preparation:add:open",
"bsStyle": "info",
"icon": "talend-plus-circle",
"label": {
"i18n": {
"key": "tdp-cmf:PREPARATION_ADD",
"options": {
"defaultValue": "Add preparation"
}
}
},
"actionCreator": "add:preparation:open"
},
- Those are props, to give to the action component. Just use the Action component
import { Action } from '@talend/react-components/lib/Actions';
<List.Manager id="preparations" collection={collectionWithActions}>
<List.Toolbar>
<Action
id="preparation:add:open"
bsStyle="info"
icon="talend-plus-circle"
label={t('tdp-app:PREPARATION_ADD', { defaultValue: 'Add preparation' })} // use react-i18next
onClick={props.openAddPreparationForm} // from mapDispatchToProps
/>
{/* ... */}
</List.Toolbar>
<List.Manager
- Get the action creator id
"actions": {
"title": "item:open"
}
- Just inject the action dispatch function via mapDispatchToProps
- Set the Title columnData onClick function
<List.VList.Title
dataKey="name"
columnData={rowData => ({
onClick: props.open, // from mapDispatchToProps
})}
/>
Those actions are set in each item of the collection. The reason is that you can set different actions for different items.
- Get the items action ids
{
"list": {
"actions": {
"items": [
"folder:share:open",
"item:rename",
"item:remove:open",
"preparation:copy:open",
"preparation:move:open",
"preparation:export"
],
}
}
}
- Identify them in the
actions
defintions part of cmf settings. You can remove those settings if they are used only in the list.
{
"folder:share:open": {
"id": "folder:share:open",
"icon": "talend-share-alt",
"label": {
"i18n": {
"key": "tdp-cmf:FOLDER_SHARE",
"options": {
"defaultValue": "Share folder"
}
}
},
"actionCreator": "preparation:share:open",
"availableExpression": {
"id": "isShareableFolder",
"args": []
}
},
"item:rename": {
"id": "item:rename",
"icon": "talend-pencil",
"label": {
"i18n": {
"key": "tdp-cmf:PREPARATION_RENAME",
"options": {
"defaultValue": "Rename"
}
}
},
"data-featureExpression": {
"id": "getDataFeature",
"args": ["rename"]
},
"actionCreator": "item:rename"
}
}
- Convert that into javascript, and use the useCollectionActions hook to inject them. Notice that expressions are usable directly in javascript, you may want to deregister them from cmf registry if they are only used here.
import { hooks } from '@talend/react-components/lib/List/ListComposition';
const collectionWithActions = hooks.useCollectionActions(collection, item =>
[
itemExpressions.isShareableFolder({ payload: item }) && { // expression invoked in js that replace the available prop
id: `folder:share:open:${item.id}`,
icon: 'talend-share-alt',
label: t('tdp-app:FOLDER_SHARE', { defaultValue: 'Share folder' }),
onClick: event => openSharing(event, { model: item }), // from mapDispatchToProps, passing the item as "model" in the payload
},
{
id: `item:rename:${item.id}`,
icon: 'talend-pencil',
label: t('tdp-app:PREPARATION_RENAME', { defaultValue: 'Rename' }),
'data-feature': `${item.type}.rename`, // replace the data-featureExpression, just execute some js
onClick: event => setTitleEditionMode(event, { model: item }),
},
].filter(Boolean) // if the availableExpression is false, the action is removed from the list
);
<List.Manager collection={collectionWithActions}>
{/* ... */}
</List.Manager>
Those 2 actions refer to action creators
- You can deregister them from registry
- Inject them via redux mapDispatchToProps
import item from 'path/to/actions/item';
const mapDispatchToProps = {
cancelRename: item.cancelRename,
submitRename: item.rename,
};
- Add them in title cell columnData
<List.VList.Title
dataKey="name"
label={headerLabels.name}
columnData={rowData => ({
onEditCancel: cancelRename,
onEditSubmit: submitRename,
})}
/>
Finally, you can remove your old List or HomeListView cmf settings.