Skip to content

Migration guide: List to ListComposition

Jimmy Somsanith edited this page Nov 14, 2019 · 1 revision

Table of content

0. Pre-requisite

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. Example HomeListView#preparations

What you need to do:

  • gather the props settings (they will become just props)
  • gather the actions the props settings refer to

1. HomeListView

If you use the HomeListView component, we need to inject the custom composed list. To do so:

  1. Create a HomeListView wrapper. Example: PreparationHomeListView. It will instanciate a HomeListView component.
  2. Pass the props directly the HomeListView component props, except the didMountActionCreator and list part. Both will be managed by your specialized composed list.
  3. 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';
  1. 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.

2. List composition starter

Let's set the base files to start implementing our custom list.

  1. 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>
	);
}
  1. 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);

3. didMountActionCreator / collectionId

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;
}

4. Column definitions (list > columns)

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 }),
	})}
/>

5. Toolbar

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.

Custom filter function

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>;

Custom sort function

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>;

6. Selection

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.

7. Actions

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 ?

  1. Those actions reference action ids. Those are actions definitions in the actions part of the settings.
  2. The actions definitions are basically Action component props, with a reference to the action creator id to dispatch.
  3. The action creators are registered in the cmf registry in your app js code.

For each one of them

  1. Remove the action register. We won't refer to them in the settings anymore
  2. 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);
  1. Now your actions dispatcher are in the custom list component props, injected by redux
  2. Each kind of actions (title, left, items, edit/cancel) must be added in their right places, in jsx.

left: the toolbar actions

The toolbar is a place where you can add the provided controls (sort, filter, ...), and any other control you'd like.

  1. Get the left action ids
"actions": {
	"left": [
		"preparation:add:open",
		"preparation:add:upload",
		"folder:add:open",
		"preparation:import"
	],
}
  1. 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"
},
  1. 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

title: the title cell click callback

  1. Get the action creator id
"actions": {
    "title": "item:open"
}
  1. Just inject the action dispatch function via mapDispatchToProps
  2. Set the Title columnData onClick function
<List.VList.Title
	dataKey="name"
	columnData={rowData => ({
		onClick: props.open, // from mapDispatchToProps
	})}
/>

items: the actions in title cell that appear on hover

Those actions are set in each item of the collection. The reason is that you can set different actions for different items.

  1. Get the items action ids
{
	"list": {
		"actions": {
			"items": [
				"folder:share:open",
				"item:rename",
				"item:remove:open",
				"preparation:copy:open",
				"preparation:move:open",
				"preparation:export"
			],
		}
	}
}
  1. 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"
	}
}
  1. 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>

editSubmit/editCancel: title cell name edition

Those 2 actions refer to action creators

  1. You can deregister them from registry
  2. Inject them via redux mapDispatchToProps
import item from 'path/to/actions/item';

const mapDispatchToProps = {
	cancelRename: item.cancelRename,
	submitRename: item.rename,
};
  1. Add them in title cell columnData
<List.VList.Title
	dataKey="name"
	label={headerLabels.name}
	columnData={rowData => ({
		onEditCancel: cancelRename,
		onEditSubmit: submitRename,
	})}
/>

8. Clean

Finally, you can remove your old List or HomeListView cmf settings.