Fabric UI DetailsList control - Dynamically load group items - reactjs

I'm using grouped DetailsList Fabric UI component in custom SPFx webpart for displaying list data.
I need to load items of the group dynamically from the server by the API call after group expand, but can't find any available group expand callbacks in DetailsList component with available exposed group props (name of the group, etc.) as a method parameter for building a request string. It should look like that:
https://contoso.sharepoint.com/site/_api/web/Lists/getbytitle('ListTitle')/RenderListDataAsStream?#listUrl=&View=&IsGroupRender=TRUE&DrillDown=1&GroupString=%3B%23Exel%20Format%20Files%3B%23%3B%23&
Basically, I want to achieve behavior of the standard modern Document Library webpart existing in Sharepoint 2019. Just need a callback on group expand for update items array. Any other ways to achieve this with DetailsList component?
Code sample of the component (from documentation):
import * as React from 'react';
import {
BaseComponent,
DefaultButton,
DetailsHeader,
DetailsList,
IColumn,
IDetailsHeaderProps,
IDetailsList,
IGroup,
IRenderFunction,
IToggleStyles,
mergeStyles,
Toggle
} from 'office-ui-fabric-react';
const margin = '0 20px 20px 0';
const controlWrapperClass = mergeStyles({
display: 'flex',
flexWrap: 'wrap'
});
const toggleStyles: Partial<IToggleStyles> = {
root: { margin: margin },
label: { marginLeft: 10 }
};
export interface IDetailsListGroupedExampleItem {
key: string;
name: string;
color: string;
}
export interface IDetailsListGroupedExampleState {
items: IDetailsListGroupedExampleItem[];
groups: IGroup[];
showItemIndexInView: boolean;
isCompactMode: boolean;
}
const _blueGroupIndex = 2;
export class DetailsListGroupedExample extends BaseComponent<{}, IDetailsListGroupedExampleState> {
private _root = React.createRef<IDetailsList>();
private _columns: IColumn[];
constructor(props: {}) {
super(props);
this.state = {
items: [
{ key: 'a', name: 'a', color: 'red' },
{ key: 'b', name: 'b', color: 'red' },
{ key: 'c', name: 'c', color: 'blue' },
{ key: 'd', name: 'd', color: 'blue' },
{ key: 'e', name: 'e', color: 'blue' }
],
// This is based on the definition of items
groups: [
{ key: 'groupred0', name: 'Color: "red"', startIndex: 0, count: 2 },
{ key: 'groupgreen2', name: 'Color: "green"', startIndex: 2, count: 0 },
{ key: 'groupblue2', name: 'Color: "blue"', startIndex: 2, count: 3 }
],
showItemIndexInView: false,
isCompactMode: false
};
this._columns = [
{ key: 'name', name: 'Name', fieldName: 'name', minWidth: 100, maxWidth: 200, isResizable: true },
{ key: 'color', name: 'Color', fieldName: 'color', minWidth: 100, maxWidth: 200 }
];
}
public componentWillUnmount() {
if (this.state.showItemIndexInView) {
const itemIndexInView = this._root.current!.getStartItemIndexInView();
alert('first item index that was in view: ' + itemIndexInView);
}
}
public render() {
const { items, groups, isCompactMode } = this.state;
return (
<div>
<div className={controlWrapperClass}>
<DefaultButton onClick={this._addItem} text="Add an item" styles={{ root: { margin: margin } }} />
<Toggle label="Compact mode" inlineLabel checked={isCompactMode} onChange={this._onChangeCompactMode} styles={toggleStyles} />
<Toggle
label="Show index of first item in view when unmounting"
inlineLabel
checked={this.state.showItemIndexInView}
onChange={this._onShowItemIndexInViewChanged}
styles={toggleStyles}
/>
</div>
<DetailsList
componentRef={this._root}
items={items}
groups={groups}
columns={this._columns}
ariaLabelForSelectAllCheckbox="Toggle selection for all items"
ariaLabelForSelectionColumn="Toggle selection"
onRenderDetailsHeader={this._onRenderDetailsHeader}
groupProps={{
showEmptyGroups: true
}}
onRenderItemColumn={this._onRenderColumn}
compact={isCompactMode}
/>
</div>
);
}
private _addItem = (): void => {
const items = this.state.items;
const groups = [...this.state.groups];
groups[_blueGroupIndex].count++;
this.setState(
{
items: items.concat([
{
key: 'item-' + items.length,
name: 'New item ' + items.length,
color: 'blue'
}
]),
groups
},
() => {
if (this._root.current) {
this._root.current.focusIndex(items.length, true);
}
}
);
};
private _onRenderDetailsHeader(props: IDetailsHeaderProps, _defaultRender?: IRenderFunction<IDetailsHeaderProps>) {
return <DetailsHeader {...props} ariaLabelForToggleAllGroupsButton={'Expand collapse groups'} />;
}
private _onRenderColumn(item: IDetailsListGroupedExampleItem, index: number, column: IColumn) {
const value = item && column && column.fieldName ? item[column.fieldName as keyof IDetailsListGroupedExampleItem] || '' : '';
return <div data-is-focusable={true}>{value}</div>;
}
private _onShowItemIndexInViewChanged = (event: React.MouseEvent<HTMLInputElement>, checked: boolean): void => {
this.setState({ showItemIndexInView: checked });
};
private _onChangeCompactMode = (ev: React.MouseEvent<HTMLElement>, checked: boolean): void => {
this.setState({ isCompactMode: checked });
};
}

I was looking for this today also and after checking the source code I figured it out. Use the groupProps.headerPropsto set a callback on group collapsing/expanding
<DetailsList
...
groupProps={{
headerProps: {
onToggleCollapse: this._onGroupToggleCollapse
}
}}
/>

So, the basic logic for this action is (using onToggleCollapse callback):
private _onToggleCollapse(props: IGroupDividerProps): () => void {
...
if (props.group.data.isLoaded === false && props.group.isCollapsed === false && props.group.level > 0) {
...
let data: any = this._getGroupItems(props.group, isLoadAll, {}).then((resp: any) => {
resp.json().then((responseJSON: any) => {
...
updatedItems = this.state.items.map((el: any, i: number) => {
...
});
...
this.setState({
items: [...updatedItems],
groups: [...this.state.groups]
});
});
});
...
}
...
return () => {
props.onToggleCollapse!(props!.group!);
};
}
We need to check for expanding to prevent updates on collapsing of the group.

Related

Split Tables based on Effective Date (typescript)

We are adding a new feature where we can create multiple product lists. The differentiator will be the effective date of the list. So the list will have the same products, same costs and different prices/effective dates. I need to create two or more separate data tables based on the effective dates but so far I havent been able to figure out how to split them up. Here is what the table looks like now:
Here is my code:
#inject('stores')
#observer
class AdminOperatorDetails extends React.Component {
public props: any;
public operator: IOperator = new IOperator();
public effectiveDate: IOperatorRegionPrice = new IOperatorRegionPrice();
public operatorDisposer: any;
public operatorPriceDisposer: any;
public operatorExpenseDisposer: any;
public adminStore: AdminStore;
public MessageStore: MessageStore;
private UserStore: UserStore;
private TenantStore: TenantStore;
private operatorPricesHeader = [
"Product Name",
"Primary Category",
"Taxable",
"Size",
"Unit",
"Specific Gravity",
"Weight",
"Cost",
"Price",
"Effecitve Date"
];
private operatorPriceHeaderButtons: any;
public state = {
downloadSelected: 'products',
editingGroupEmail: false,
effectiveDate: this.effectiveDate,
groupEmail: new GroupEmail(),
groupEmailModal: false,
isDirty: false,
modal: false,
operator: this.operator,
operatorExpenseData: { headers: this.operatorExpenseHeader, rows: []} as IList,
operatorExpensePrices: [] as IOperatorRegionExpensePrice[],
operatorId: '',
operatorPrices: [] as IOperatorRegionPrice[],
operatorRegionData: { headers: this.operatorPricesHeader, rows: []} as IList,
redirect: false,
region: new IRegion(),
regionId: '',
regions: [] as IRegion[],
rowIndex: 0,
selectedEbp: [] as boolean[],
selectedProducts: [] as boolean[],
selectedRegions: [] as boolean[],
showDownloadModal: false,
showExpenses: false,
showPriceListModal: false,
showProducts: false,
showRegionModal: false,
};
constructor(props: any) {
super(props);
this.adminStore = this.props.stores.AdminStore;
this.MessageStore = this.props.stores.MessageStore;
this.UserStore = this.props.stores.UserStore;
this.TenantStore = this.props.stores.TenantStore;
this.initializeHeaders();
}
public componentDidMount() {
this.createDisposers();
this.operatorExportHeaderButtons = [
{
content: this.renderExportHeader,
type: 'render'
}
];
}
public componentWillUnmount() {
this.clearDisposers();
}
public createDisposers() {
this.operatorDisposer = intercept(this.adminStore, 'operator', (change: any) => {
const selectedRegions = this.setSelectedRegions(change.newValue);
this.setState({
operator: change.newValue,
selectedRegions
});
return change;
});
this.operatorPriceDisposer = intercept(this.adminStore, 'operatorProductPrices', (change: any) => {
this.setState({
operatorPrices: change.newValue
}, () => {
this.formatOperatorPriceData();
});
return change;
});
public clearDisposers() {
this.operatorDisposer();
this.operatorPriceDisposer();
}
public handleChange = (event: any) => {
const operator = this.state.operator;
operator[event.target.name] = event.target.value;
this.setState({
isDirty: true,
operator
});
}
private toggleList = (toggleName:string) => () => {
this.setState({
[toggleName]: !this.state[toggleName]
});
}
private formatOperatorPriceData = () => {
const operatorPrice = this.state.operatorPrices;
const rows:any = [];
operatorPrice.forEach((regionPrice, index) => {
const tempRow = [
{
content: regionPrice.name
},
{
content: regionPrice.category,
},
{
content: this.renderTaxableCheckbox('operatorPrices', regionPrice, index),
sortItem: regionPrice.isTaxable,
type: 'render',
},
{
content: regionPrice.size
},
{
content: regionPrice.unit
},
{
content: regionPrice.specificGravity,
},
{
content: regionPrice.weight,
},
{
content: this.renderCostInput(regionPrice, index),
sortItem: regionPrice.cost,
type: 'render',
},
{
content: this.renderPriceInput(regionPrice, index),
sortItem: regionPrice.price,
type: 'render'
},
{
content: regionPrice.effectiveDate
}
];
rows.push(tempRow);
});
this.setState({operatorRegionData: { headers: this.operatorPricesHeader, rows}});
}
{
this.state.operatorPrices.length > 0 &&
<React.Fragment>
<Row>
<Col>
{/* {
this.state.operatorPrices && this.state.operatorPrices.map((value, index) =>
<React.Fragment key={value.id}>
{
this.renderPriceLists(index)
}
</React.Fragment>
)
} */}
{
<h4 onClick={this.toggleList('showProducts')}><OpenArrow color={this.TenantStore.iconColor} open={this.state.showProducts} />Operator Product Lists</h4>
},
</Col>
</Row>
{
this.state.showProducts &&
<Row>
<Col>
<DataTable
list={this.state.operatorRegionData}
headerButtons={this.operatorPriceHeaderButtons}
maxHeight='50vh'
/>
</Col>
</Row>
}
</React.Fragment>
}

how to give navigation for list in horizontal menu of reactjs

Can any one help to get the solution for this question, for page navigation i am using react-horizontal-scrolling-menu. in the reactjs application. Just i want give page navigation where i should give navigation please tell me. this code has giving by the link https://https://www.npmjs.com/package/react-horizontal-scrolling-menu
import React, { Component } from 'react';
import ScrollMenu from 'react-horizontal-scrolling-menu';
import './App.css';
// list of items
const list = [
{ name: 'item1' },
{ name: 'item2' },
{ name: 'item3' },
{ name: 'item4' },
{ name: 'item5' },
{ name: 'item6' },
{ name: 'item7' },
{ name: 'item8' },
{ name: 'item9' }
];
// One item component
// selected prop will be passed
const MenuItem = ({text, selected}) => {
return <div
className={`menu-item ${selected ? 'active' : ''}`}
>{text}</div>;
};
// All items component
// Important! add unique key
export const Menu = (list, selected) =>
list.map(el => {
const {name} = el;
return <MenuItem text={name} key={name} selected={selected} />;
});
const Arrow = ({ text, className }) => {
return (
<div
className={className}
>{text}</div>
);
};
const ArrowLeft = Arrow({ text: '<', className: 'arrow-prev' });
const ArrowRight = Arrow({ text: '>', className: 'arrow-next' });
const selected = 'item1';
class App extends Component {
constructor(props) {
super(props);
// call it again if items count changes
this.menuItems = Menu(list, selected);
}
state = {
selected
};
onSelect = key => {
this.setState({ selected: key });
}
render() {
const { selected } = this.state;
// Create menu from items
const menu = this.menuItems;
return (
<div className="App">
<ScrollMenu
data={menu}
arrowLeft={ArrowLeft}
arrowRight={ArrowRight}
selected={selected}
onSelect={this.onSelect}
/>
</div>
);
}
}
'css code start here '
.menu-item {
padding: 0 40px;
margin: 5px 10px;
user-select: none;
cursor: pointer;
border: none;
}
.menu-item-wrapper.active {
border: 1px blue solid;
}
.menu-item.active {
border: 1px green solid;
}
.scroll-menu-arrow {
padding: 20px;
cursor: pointer;
}
You are missing specified paths (or what resolves to pathnames) from your list of routes that are passed to the Link component's to prop.
// list of items
const list = [
{ name: "item1", path: "/" },
{ name: "item2" }, // MISSING path properties!
{ name: "item3", path: "./abcd" },
{ name: "item4" },
{ name: "item5" },
{ name: "item6" },
{ name: "item7" },
{ name: "item8" },
{ name: "item9", path: "./example_1" }
];
// One item component
// selected prop will be passed
const MenuItem = ({ text, path, selected }) => {
return (
<div className={`menu-item ${selected ? "active" : ""}`}>
<NavLink exact to={path}> // All links must have a defined to prop
{text}
</NavLink>
</div>
);
};
It is a simple fix to add a defined path for each route in your config. For example:
const list = [
{ name: "item1", path: "/" },
{ name: "item2", path: "/page/1" },
{ name: "item3", path: "/abcd" },
{ name: "item4", path: "/page/2" },
{ name: "item5", path: "/page/3" },
{ name: "item6", path: "/page/4" },
{ name: "item7", path: "/page/42" },
{ name: "item8", path: "/example_1" },
{ name: "item9", path: "/page/5" }
];
DEMO I've taken the liberty of forking your sandbox, updated to specify paths, and only define the menu once and display in one location (DRY principle) in your root App.
Link to this library is not working.
You can add another property to your list like { name: 'item1', url: '/somecomponenturl' }
Then in your Menu function just pass the URL as prop just like text prop and in MenuItem function use your url with Link or NavLink like:
const MenuItem = ({text, url, selected}) => {
return <div
className={`menu-item ${selected ? 'active' : ''}`}
><Link to={url}>{text}</Link></div>;
};
export const Menu = (list, selected) =>
list.map(el => {
const {name} = el;
const {url} = el;
return <MenuItem text={name} url={url} key={name} selected={selected} />;
});

ts(1128)-Declaration or statement expected at public render() line

I have the following code (followed this sample code) which compiles without any errors. The code is making use of the sample code to use jQXWidget.
import React from "react";
import * as ReactDOM from "react-dom";
import JqxTabs from "jqwidgets-scripts/jqwidgets-react-tsx/jqxtabs";
import JqxGrid, {
IGridProps,
jqx
} from "jqwidgets-scripts/jqwidgets-react-tsx/jqxgrid";
import { properties } from ".././properties";
type Props = {
project
};
export interface MyState extends IGridProps {
visible: boolean,
assocProjectElementsVisible: boolean,
project: {},
selectedRowIndex: number,
deleteDialogVisible: boolean
}
export class ProjectElements extends React.PureComponent<Props, MyState> {
private myTabs = React.createRef<JqxTabs>();
private projectSpacesGridElement = React.createRef<HTMLDivElement>();
private pSpacesGridElement = React.createRef<JqxGrid>();
private baseUrl = properties.baseUrlWs;
constructor(props: Props) {
super(props);
/* const deleteRowClick = () => {
const selectedrowindex = this.pSpacesGridElement.current.getselectedrowindex();
if (selectedrowindex > -1) {
this.setState({
selectedRowIndex: selectedrowindex,
deleteDialogVisible: true
})
} */
this.state = {
visible: false,
project: {},
assocProjectElementsVisible: false,
selectedRowIndex: null,
deleteDialogVisible: false,
rendergridrows: (params: any): any[] => {
const data = params.data;
return data;
}
};
}
public render() {
return (
<JqxTabs
ref={this.myTabs}
theme={"arctic"}
width="1390px"
height={560}
initTabContent={this.initWidgets}
>
<ul>
<li style={{ marginLeft: 30 }}>
<div style={{ height: 20, marginTop: 5 }}>
<div style={{ float: "left" }}></div>
<div
style={{
marginLeft: 4,
verticalAlign: "middle",
textAlign: "center",
float: "left"
}}
>
Project Spaces
</div>
</div>
</li>
</ul>
<div style={{ overflow: "hidden" }}>
<div id="jqxGrid" ref={this.projectSpacesGridElement} />
<div style={{ marginTop: 10, height: "15%", width: "100%" }}></div>
</div>
</JqxTabs>
);
}
//Tab 1
private projectSpacesGrid = () => {
const source: any = {
datafields: [
{ name: "id", type: "long" },
{ name: "fileName", type: "string" }
],
datatype: "json",
root: "ProjectElements",
url: this.baseUrl + `api/ProjectElements/search/getProjectElementsByProjectId`
};
const dataAdapter = new jqx.dataAdapter(source, {
//async: false,
autoBind: true,
downloadComplete: (data: any, status: any, xhr: any): void => {
source.totalrecords = parseInt(data["page"].totalElements);
},
formatData: (data: any): any => {
data.value = this.props.project.id;
data.page = data.pagenum;
data.size = data.pagesize;
if (data.sortdatafield && data.sortorder) {
data.sort = data.sortdatafield + "," + data.sortorder;
}
return data;
},
loadError: (xhr: any, status: any, error: any) => {
alert('Error loading "' + source.url + '" : ' + error);
}
});
const columns: IGridProps["columns"] = [
{ datafield: "id", text: "ID Number", width: 100 },
{ datafield: "fileName", text: "Project Name", width: 275 }
];
const grid =
this.state.visible || this.state.assocProjectElementsVisible ? null : (
<div>
<JqxGrid
ref={this.pSpacesGridElement}
width={"100%"}
height={"100%"}
theme={"arctic"}
source={dataAdapter}
columns={columns}
/>
</div>
);
ReactDOM.render(grid, this.projectSpacesGridElement.current!);
};
private initWidgets = (tab: any) => {
switch (tab) {
case 0:
this.projectSpacesGrid();
break;
}
};
}
However,as soon as I uncomment the deleteRowClick method I get an error at public on the public render() line as shown in the screenshot below:
What am I doing wrong after uncommenting the deleteRowClick method?
You're missing a closing } at the end of the deleteRowClick function.

one react component updates all the other react components

I have a react table and one of the columns of it is another component. This component is a dropdown which get its value with an API call which I have defined in componentDidMount().
I have use case where in if user selects any value from the dropdown, I want to save that field to the DB. So I defined this post call in the handleChange function of the dropdown.
Issue is that when I change the value in any one row, every other component in other rows also calls the makes the network calls which is defined in componentDidMount(). So componentDidMount() is called for all the 4 entries. I confirmed on the server side as well. I can see four get requests(I have only 4 rows for now). I am thoroughly confused why it's behaving this way?
Parent Component
import React from 'react';
import ReactTable from 'react-table';
import 'react-table/react-table.css';
import Popup from "reactjs-popup";
export default class DetailsTable extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
shipmentDataMap : { },
selectedRow: null,
downloadableAlerts: []
};
this.setState = this.setState.bind(this);
this.handleRowClick = this.handleRowClick.bind(this);
this.handleReassignment = this.handleReassignment.bind(this);
this.handleStatusUpdate = this.handleStatusUpdate.bind(this);
this.generateFilteredArr = this.generateFilteredArr.bind(this);
this.handleDownload = this.handleDownload.bind(this);
this.updateActualEntity = this.updateActualEntity.bind(this);
};
componentDidMount() {
axios.post('/entity/getRoute', {
trackingId: this.state.tid
})
.then((response) => {
let tempRoute = [];
response.data.route.forEach(element => {
tempRoute.push({ label: element['node'], value: element['node'] });
})
this.setState({route: tempRoute});
})
.catch(function (error) {
console.log(error);
});
};
updateActualEntity = (trackingId, updatedEntity) => {
let updatedRecord = this.state.shipmentDataMap[trackingId];
updatedRecord.actualEntity = updatedEntity;
this.setState({shipmentDataMap: this.state.shipmentDataMap});
};
render() {
const TableColumns = [{
Header: 'Actions',
id: 'actionPopupButton',
filterable: false,
style: {'textAlign': 'left'},
Cell: row => (<div><ReassignPopup data={row.original} updateRowFunc={this.handleReassignment} nodeOptions={this.props.nodeOptions}/>
<br/>
<UpdateStatusPopup data={row.original} updateRowFunc={this.handleStatusUpdate} statusOptions={this.props.statusOptions}/>
</div>)
},
{
Header: 'Assigned Node',
headerStyle: {'whiteSpace': 'unset'},
accessor: 'node',
style: {'whiteSpace': 'unset'}
}, {
Header: 'TID',
headerStyle: {'whiteSpace': 'unset'},
accessor: 'tid',
width: 140,
filterMethod: (filter, row) => {
return row[filter.id].startsWith(filter.value)
},
Cell: props => {props.value}
},
{
Header: 'Predicted Entity',
headerStyle: {'whiteSpace': 'unset'},
filterable: false,
accessor: 'predictedEntity',
style: {'whiteSpace': 'unset'},
},
{
Header: 'Feedback',
headerStyle: {'whiteSpace': 'unset'},
filterable: false,
accessor: 'actualEntity',
width: 140,
style: {'whiteSpace': 'unset', overflow: 'visible'},
Cell: row => (<div><AbusiveEntityComponent entity={row.original.actualEntity}
tid={row.original.tid} trackingDetailsId={row.original.trackingDetailsId}
updateActualEntityInShipmentData={this.updateActualEntity}/></div>)
}
return <div>
<CSVLink data={this.state.downloadableAlerts} filename="ShipmentAlerts.csv" className="hidden" ref={(r) => this.csvLink = r} target="_blank"/>
<ReactTable
ref={(r)=>this.reactTable=r}
className='-striped -highlight'
filterable
data={Object.values(this.state.shipmentDataMap)}
//resolveData={data => data.map(row => row)}
columns={TableColumns}
//filtered={this.state.filtered}
filtered={this.generateFilteredArr(this.props.filterMap, this.props.searchParams)}
/*onFilteredChange={(filtered, column, value) => {
this.onFilteredChangeCustom(value, column.id || column.accessor);
}}*/
defaultFilterMethod={(filter, row, column) => {
const id = filter.pivotId || filter.id;
if (typeof filter.value === "object") {
return row[id] !== undefined
? filter.value.indexOf(row[id].toString()) > -1
: true;
} else {
return row[id] !== undefined
? String(row[id]).indexOf(filter.value) > -1
: true;
}
}}
defaultPageSize={10}
//pageSize={10}
previousText='Previous Page'
nextText='Next Page'
noDataText='No intervention alerts found'
style={{
fontSize: "12px",
height: "67.4vh" // Using fixed pixels/limited height will force the table body to overflow and scroll
}}
getTheadFilterProps={() => {return {style: {display: "none" }}}}
getTbodyProps={() => {return {style: {overflowX: "hidden" }}}} //For preventing extra scrollbar in Firefox/Safari
/*
getTrProps={(state, rowInfo) => {
if (rowInfo && rowInfo.row) {
return {
onClick: (e) => {this.handleRowClick(e, rowInfo)},
style: {
//background: rowInfo.index === this.state.selectedRow ? '#00afec' : 'white',
color: rowInfo.index === this.state.selectedRow ? 'blue' : 'black'
}
}
} else {
return {}
}
}
} */
/>
</div>;
}
}
Child Component
import React from 'react';
import axios from 'axios';
export default class AbusiveEntityComponent extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
entity: this.props.entity,
tid: this.props.tid,
trackingDetailsId: this.props.trackingDetailsId,
route: []
};
this.handleChange = this.handleChange.bind(this);
}
handleChange = (event) => {
var selected = event.target.value;
if(selected !== '' && this.state.entity !== selected) {
if (window.confirm('Are you sure you want to select: '+ selected)) {
axios.post('/entity/upateAbusiveEntity', {
trackingDetailsId: this.state.trackingDetailsId,
abusiveEntity: selected
}).then( (response) =>{
this.setState({entity: selected});
this.props.updateActualEntityInShipmentData(this.state.tid, selected);
})
.catch(function (error) {
console.log(error);
});
}
}
}
componentDidMount() {
console.log("did mount");
axios.get('/entity/getRoute', {
params: {
trackingId: this.state.tid
}
})
.then((response) => {
let tempRoute = [];
let prev="";
response.data.route.forEach(element => {
if(prev!== "") {
tempRoute.push(prev+"-"+element['node'])
}
tempRoute.push(element['node']);
prev=element['node'];
})
this.setState({route: [''].concat(tempRoute)});
})
.catch(function (error) {
console.log(error);
});
};
render() {
return (
<div className="AbusiveEntityDiv">
<select onChange={this.handleChange} value={this.state.entity===null?'':this.state.entity}
style={{width: 100}}>
{ this.state.route.map(value => <option key={value} value={value}>{value}</option>) }
</select>
</div>
);
}
}
My question is if componentDidUpdate() is not the correct place to fetch data for dropdown, where should I define the network call ?
I found the solution. In the parent component I maintain a state of shipmentstatusmap. One of the columns of this map is acutalEntity. Now in the child component, whenever user selects the value from dropdown, I callback the parent to update the shipmentStatusMap as well. This callback was my problem.
Because now the state of parent component changes, it unmount the child and re-mount it. So it's componentDidMount is called for all the rows which in turn makes the API call.
Solution
Since I want the dropdown values only once when whole parent component is loaded, I can either move the API to constructor or the in the componentDidMount() of parent. Fetching data in constructor is not a good idea .
So I moved this API call in parent and voila! everything works as expected.
updated code:
Child component
import React from 'react';
import axios from 'axios';
export default class AbusiveEntityComponent extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
entity: this.props.entity,
tid: this.props.tid,
trackingDetailsId: this.props.trackingDetailsId,
route: this.props.route
};
this.handleChange = this.handleChange.bind(this);
}
handleChange = (event) => {
var selected = event.target.value;
if(selected !== '' && this.state.entity !== selected) {
if (window.confirm('Are you sure you want to select: '+ selected)) {
axios.post('/entity/upateAbusiveEntity', {
trackingDetailsId: this.state.trackingDetailsId,
abusiveEntity: selected
}).then( (response) =>{
this.setState({entity: selected});
this.props.updateActualEntityInShipmentData(this.state.tid, selected);
})
.catch(function (error) {
console.log(error);
});
}
}
}
render() {
return (
<div className="AbusiveEntityDiv">
<select onChange={this.handleChange} value={this.state.entity===null?'':this.state.entity}
style={{width: 100}}>
{ this.state.route.map(value => <option key={value} value={value}>{value}</option>) }
</select>
</div>
);
}
}
Parent component
import React from 'react';
import ReactTable from 'react-table';
import 'react-table/react-table.css';
import Popup from "reactjs-popup";
export default class DetailsTable extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
shipmentDataMap : { },
selectedRow: null,
downloadableAlerts: []
};
this.setState = this.setState.bind(this);
this.handleRowClick = this.handleRowClick.bind(this);
this.handleReassignment = this.handleReassignment.bind(this);
this.handleStatusUpdate = this.handleStatusUpdate.bind(this);
this.generateFilteredArr = this.generateFilteredArr.bind(this);
this.handleDownload = this.handleDownload.bind(this);
this.updateActualEntity = this.updateActualEntity.bind(this);
};
// this portion was updated
componentDidMount() {
fetch('/shipment/all')
.then(res => res.json())
.then(shipmentList => {
var tidToShipmentMap = {};
var totalShipmentCount = shipmentList.length;
var loadedShipmentRoute = 0;
shipmentList.forEach(shipment => {
axios.get('/entity/getRoute', {
params: {
trackingId: shipment.tid
}
})
.then(response => {
let tempRoute = [];
let prev="";
response.data.route.forEach(element => {
if(prev!== "") {
tempRoute.push(prev+"-"+element['node'])
}
tempRoute.push(element['node']);
prev=element['node'];
})
shipment.route = [''].concat(tempRoute);
tidToShipmentMap[shipment.tid] = shipment;
loadedShipmentRoute++;
if (loadedShipmentRoute === totalShipmentCount) {
this.setState({ shipmentDataMap: tidToShipmentMap});
console.log(tidToShipmentMap);
}
})
.catch(function (error) {
console.log(error);
});
});
})
.catch(error => console.log(error));
};
updateActualEntity = (trackingId, updatedEntity) => {
let updatedRecord = this.state.shipmentDataMap[trackingId];
updatedRecord.actualEntity = updatedEntity;
this.setState({shipmentDataMap: this.state.shipmentDataMap});
};
render() {
const TableColumns = [{
Header: 'Actions',
id: 'actionPopupButton',
filterable: false,
style: {'textAlign': 'left'},
Cell: row => (<div><ReassignPopup data={row.original} updateRowFunc={this.handleReassignment} nodeOptions={this.props.nodeOptions}/>
<br/>
<UpdateStatusPopup data={row.original} updateRowFunc={this.handleStatusUpdate} statusOptions={this.props.statusOptions}/>
</div>)
},
{
Header: 'Assigned Node',
headerStyle: {'whiteSpace': 'unset'},
accessor: 'node',
style: {'whiteSpace': 'unset'}
}, {
Header: 'TID',
headerStyle: {'whiteSpace': 'unset'},
accessor: 'tid',
width: 140,
filterMethod: (filter, row) => {
return row[filter.id].startsWith(filter.value)
},
Cell: props => {props.value}
},
{
Header: 'Predicted Entity',
headerStyle: {'whiteSpace': 'unset'},
filterable: false,
accessor: 'predictedEntity',
style: {'whiteSpace': 'unset'},
},
{
Header: 'Feedback',
headerStyle: {'whiteSpace': 'unset'},
filterable: false,
accessor: 'actualEntity',
width: 140,
style: {'whiteSpace': 'unset', overflow: 'visible'},
Cell: row => (<div><AbusiveEntityComponent entity={row.original.actualEntity}
tid={row.original.tid} trackingDetailsId={row.original.trackingDetailsId}
updateActualEntityInShipmentData={this.updateActualEntity}/></div>)
}
return <div>
<CSVLink data={this.state.downloadableAlerts} filename="ShipmentAlerts.csv" className="hidden" ref={(r) => this.csvLink = r} target="_blank"/>
<ReactTable
ref={(r)=>this.reactTable=r}
className='-striped -highlight'
filterable
data={Object.values(this.state.shipmentDataMap)}
//resolveData={data => data.map(row => row)}
columns={TableColumns}
//filtered={this.state.filtered}
filtered={this.generateFilteredArr(this.props.filterMap, this.props.searchParams)}
/*onFilteredChange={(filtered, column, value) => {
this.onFilteredChangeCustom(value, column.id || column.accessor);
}}*/
defaultFilterMethod={(filter, row, column) => {
const id = filter.pivotId || filter.id;
if (typeof filter.value === "object") {
return row[id] !== undefined
? filter.value.indexOf(row[id].toString()) > -1
: true;
} else {
return row[id] !== undefined
? String(row[id]).indexOf(filter.value) > -1
: true;
}
}}
defaultPageSize={10}
//pageSize={10}
previousText='Previous Page'
nextText='Next Page'
noDataText='No intervention alerts found'
style={{
fontSize: "12px",
height: "67.4vh" // Using fixed pixels/limited height will force the table body to overflow and scroll
}}
getTheadFilterProps={() => {return {style: {display: "none" }}}}
getTbodyProps={() => {return {style: {overflowX: "hidden" }}}} //For preventing extra scrollbar in Firefox/Safari
/*
getTrProps={(state, rowInfo) => {
if (rowInfo && rowInfo.row) {
return {
onClick: (e) => {this.handleRowClick(e, rowInfo)},
style: {
//background: rowInfo.index === this.state.selectedRow ? '#00afec' : 'white',
color: rowInfo.index === this.state.selectedRow ? 'blue' : 'black'
}
}
} else {
return {}
}
}
} */
/>
</div>;
}
}

TS2322 error with Office UI Fabric DetailsList component

I'm trying to follow the example here from the office-ui-fabric-react repo simply to test the new focusedIndex function to scroll a selection into view.
However, WebStorm is highlighting a TS2322 error in the render() function trying to set the componentRef property to a class variable:
(short error)
TS2322: Type '{componentRef: RefObject... is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes...
The error occurs when using the full unmodified code from the link, but here's a snippet of the relevant class code for reference and the ** affected line ** in the render() function:
import * as React from 'react';
import { BaseComponent } from 'office-ui-fabric-react/lib/Utilities';
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { Fabric } from 'office-ui-fabric-react/lib/Fabric';
import { IDetailsList, DetailsList, IColumn } from 'office-ui-fabric-react/lib/DetailsList';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import './DetailsList.Grouped.Example.scss';
export class DetailsListGroupedExample extends BaseComponent<
{},
{
items: {}[];
showItemIndexInView: boolean;
}
>
{
private _root = React.createRef<IDetailsList>();
constructor(props: {}) {
super(props);
this.state = {
items: _items,
showItemIndexInView: false
};
}
public render() {
const { items } = this.state;
return (
<Fabric className="DetailsList-grouped-example">
<div>
<Checkbox
label="Show index of the first item in view when unmounting"
checked={this.state.showItemIndexInView}
onChange={this._onShowItemIndexInViewChanged}
/>
</div>
<DefaultButton onClick={this._addItem} text="Add an item" />
<DetailsList
componentRef={this._root} //**TS2322 ERROR HERE**
items={items}
groups={[
{
key: 'groupred0',
name: 'By "red"',
startIndex: 0,
count: 2
},
{
key: 'groupgreen2',
name: 'By "green"',
startIndex: 2,
count: 0
},
{
key: 'groupblue2',
name: 'By "blue"',
startIndex: 2,
count: items.length - 2
}
]}
columns={_columns}
ariaLabelForSelectAllCheckbox="Toggle selection for all items"
ariaLabelForSelectionColumn="Toggle selection"
groupProps={{
showEmptyGroups: true
}}
onRenderItemColumn={this._onRenderColumn}
/>
</Fabric>
);
}
}
What am I doing wrong or what do I need to do to resolve this compile error?
So, on the example i've get rid of
private _root = React.createRef<IDetailsList>
and all of references to this object. Then example works like a charm.
It looks like something has been changed in fabric react controls, but codesamples on their website has not been updated which is annoying.
My code:
import * as React from 'react';
import styles from './RsfDictionaries.module.scss';
import { IRsfDictionariesProps } from './IRsfDictionariesProps';
import { escape } from '#microsoft/sp-lodash-subset';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
import { DetailsList, DetailsListLayoutMode, Selection, SelectionMode, IColumn, IDetailsList } from 'office-ui-fabric-react/lib/DetailsList';
import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection';
import { IDocument, IDetailsListDocumentsExampleState } from './states';
import { BaseComponent } from 'office-ui-fabric-react/lib/Utilities';
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { Fabric } from 'office-ui-fabric-react/lib/Fabric';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
const _columns = [
{
key: 'name',
name: 'Name',
fieldName: 'name',
minWidth: 100,
maxWidth: 200,
isResizable: true
},
{
key: 'color',
name: 'Color',
fieldName: 'color',
minWidth: 100,
maxWidth: 200
}
];
const _items = [
{
key: 'a',
name: 'a',
color: 'red'
},
{
key: 'b',
name: 'b',
color: 'red'
},
{
key: 'c',
name: 'c',
color: 'blue'
},
{
key: 'd',
name: 'd',
color: 'blue'
},
{
key: 'e',
name: 'e',
color: 'blue'
}
];
export default class RsfDictionaries extends React.Component<IRsfDictionariesProps, {
items: {}[];
showItemIndexInView: boolean;
}> {
constructor(props: any) {
super(props);
this.state = {
items: _items,
showItemIndexInView: false
};
}
public componentWillUnmount() {
if (this.state.showItemIndexInView) {
const itemIndexInView = 0;//this._root!.current!.getStartItemIndexInView();
alert('unmounting, getting first item index that was in view: ' + itemIndexInView);
}
}
private _root :IDetailsList; //React.createRef<IDetailsList>();
public render(): React.ReactElement<IRsfDictionariesProps> {
const { items } = this.state;
return (
<Fabric className="DetailsList-grouped-example">
<div>
<Checkbox
label="Show index of the first item in view when unmounting"
checked={this.state.showItemIndexInView}
onChange={this._onShowItemIndexInViewChanged}
/>
</div>
<DefaultButton onClick={this._addItem} text="Add an item" />
<DetailsList
//={this._root}
items={items}
groups={[
{
key: 'groupred0',
name: 'By "red"',
startIndex: 0,
count: 2
},
{
key: 'groupgreen2',
name: 'By "green"',
startIndex: 2,
count: 0
},
{
key: 'groupblue2',
name: 'By "blue"',
startIndex: 2,
count: items.length - 2
}
]}
columns={_columns}
ariaLabelForSelectAllCheckbox="Toggle selection for all items"
ariaLabelForSelectionColumn="Toggle selection"
groupProps={{
showEmptyGroups: true
}}
onRenderItemColumn={this._onRenderColumn}
/>
</Fabric>
);
}
private _addItem = (): void => {
const items = this.state.items;
this.setState(
{
items: items.concat([
{
key: 'item-' + items.length,
name: 'New item ' + items.length,
color: 'blue'
}
])
},
() => {
//if (this._root.current) {
//this._root.current.focusIndex(items.length, true);
//}
}
);
};
private _onRenderColumn(item: any, index: number, column: IColumn) {
let value = item && column && column.fieldName ? item[column.fieldName] : '';
if (value === null || value === undefined) {
value = '';
}
return (
<div className={'grouped-example-column'} data-is-focusable={true}>
{value}
</div>
);
}
private _onShowItemIndexInViewChanged = (event: React.FormEvent<HTMLInputElement>, checked: boolean): void => {
this.setState({
showItemIndexInView: checked
});
};
}

Resources