I'm struggling with something that should be more obvious to me.
when someone clicks on a antd popconfirm's "yes" option, it's supposed to trigger the onConfirm function and then update a record in my redux store via a dispatch action. All I want to do is to change one field (archived) from false to true in this record. I know that props are immutable so i can't just change the prop. But how to approach this?
I vaguely recall a way to pass a record on and modify a field using a spread operator? is there some easy way to do this? Or do i need to somehow convert my prop object to state so i can modify it and pass it on somehow?
I map my selector via the following.. AFAIK I just need to pass the id of the property i care about along with the object that i want it to replace:
const mapDispatchToProps = (dispatch) => ({
archiveProperty: (id, property) => dispatch(startEditProperty(id, property))
});
The antd popconfirm block that calls onConfirm. Probably not terrible interesting:
{
title: 'Action',
key: 'action',
render: (text, record) => (
<span>
<a>Edit</a>
<Divider type="vertical" />
<Popconfirm
title="Are you sure?"
onConfirm={() => this.confirm(record)}
onCancel={this.cancel}
okText="Yes"
cancelText="No"
>
Archive
</Popconfirm>
</span>
),
},
Here is where I think my problem is. It works but i am currently only passing the existing record in. Not a modified version of it. How to pass a version of it that has it's archived field set to true?
confirm = (record) => {
message.success('Archived');
console.log("confirm function.. record");
this.props.archiveProperty(record.id, record);
}
The entire file if it's useful looks like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import selectProperties from '../selectors/properties';
import { startEditProperty } from '../actions/properties';
import { Table, Tag, Divider, Popconfirm, message } from 'antd';
export class PropertyList extends React.Component {
constructor(){
super();
this.columns = [
{
title: 'Address',
dataIndex: 'street',
key: 'street',
render: text => <a>{text}</a>,
},
{
title: 'City',
dataIndex: 'city',
key: 'city',
},
{
title: 'State',
dataIndex: 'state',
key: 'state',
},
{
title: 'Workflow',
key: 'workflow',
dataIndex: 'workflow',
sorter: (a, b) => a.workflow.length - b.workflow.length,
sortDirections: ['descend'],
render: workflow => {
let color = 'geekblue';
if (workflow === 'Inspection' || workflow === 'Maintenance' || workflow === 'Cleaning') {
color = 'volcano';
}
else if (workflow === 'Rented') {
color = 'green';
}
return (
<span>
<Tag color={color} key={workflow}>
{workflow.toUpperCase()}
</Tag>
</span>
);
},
},
{
title: 'Action',
key: 'action',
render: (text, record) => (
<span>
<a>Edit</a>
<Divider type="vertical" />
<Popconfirm
title="Are you sure?"
onConfirm={() => this.confirm(record)}
onCancel={this.cancel}
okText="Yes"
cancelText="No"
>
Archive
</Popconfirm>
</span>
),
},
];
}
confirm = (record) => {
message.success('Archived');
console.log(record);
this.props.archiveProperty(record.id, record);
}
cancel = () => {
message.error('Cancelled');
console.log("cancel function..");
}
render() {
return (
<div className="content-container">
<div className="list-body">
{
this.props.properties.length === 0 ? (
<div className="list-item list-item--message">
<span>No properties. Add some!</span>
</div>
) : (
<Table
rowKey="id"
dataSource={this.props.properties}
columns={this.columns}
pagination = { false }
footer={() => ''}
/>
)
}
</div>
</div>
)
}
};
const mapStateToProps = (state) => {
console.log("PropertyList mapStateToProps..");
console.log(state);
return {
properties: selectProperties(state.properties, state.filters)
};
};
const mapDispatchToProps = (dispatch) => ({
archiveProperty: (id, property) => dispatch(startEditProperty(id, property))
});
export default connect(mapStateToProps, mapDispatchToProps)(PropertyList);
You should copy the record object and change its one field (archived) from false to true in copied object.
Try this in your confirm method.
confirm = (record) => {
message.success('Archived');
console.log("confirm function.. record");
// create a new modified object
const updatedRecord = Object.assign({}, record, {archived: true});
//now pass this updatedRecord object as a new record to store
this.props.archiveProperty(record.id, updatedRecord);
}
And in reducer just replace old record object with this updatedRecord object.
Hope it helps.
Related
I am using react-querybuilder what I need is another Add Rule button to be add next to the original one and want to add differnt set of fields and operators when using the new button. Here is some part of my code:
import { HBButton, HBIcon } from '#hasty-bazar/core'
import { FC } from 'react'
import { useIntl } from 'react-intl'
import queryBuilderMessages from '../HBQueryBuilder.messages'
interface AddRuleActionProps {
handleOnClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
}
const AddGroupAction: FC<AddRuleActionProps> = ({ handleOnClick }) => {
const { formatMessage } = useIntl()
return (
<>
<HBButton
onClick={handleOnClick}
size="small"
leftIcon={<HBIcon type="plus" />}
sx={{ marginRight: 2, minWidth: 50 }}
>
{formatMessage(queryBuilderMessages.rule)}
</HBButton>
// >>> ANOTHER HBButton with different implementation to be added here
</>
)
}
export default AddGroupAction
Adding a new answer based on your feedback and because this one is very different from the other. I'm about to release v5.0 of react-querybuilder that has the feature I mentioned in the first paragraph of the other answer. This makes achieving the desired result much more straightforward and also eliminates the need for external state management (i.e. Redux).
TL;DR: working codesandbox example here (uses react-querybuilder#5.0.0-alpha.2).
React Query Builder only takes one fields prop, but you can organize the fields into an array of option groups instead of a flat array. I set the operators property on each field to the default operators, filtered appropriately for the type of field (text vs numeric).
import { Field, OptionGroup } from 'react-querybuilder';
import { nameOperators, numberOperators } from './operators';
export const fields: OptionGroup<Field>[] = [
{
label: 'Names',
options: [
{ name: 'firstName', label: 'First Name', operators: nameOperators },
{ name: 'lastName', label: 'Last Name', operators: nameOperators },
],
},
{
label: 'Numbers',
options: [
{ name: 'height', label: 'Height', operators: numberOperators },
{ name: 'weight', label: 'Weight', operators: numberOperators },
],
},
];
Next I set up a custom field selector component to only allow fields that are part of the same option group. So if a "name" field is chosen, the user can only select other "name" fields.
const FilteredFieldSelector = (props: FieldSelectorProps) => {
const filteredFields = fields.find((optGroup) =>
optGroup.options.map((og) => og.name).includes(props.value!)
)!.options;
return <ValueSelector {...{ ...props, options: filteredFields }} />;
};
This custom Add Rule button renders a separate button for each option group that calls the handleOnClick prop with the option group's label as context.
const AddRuleButtons = (props: ActionWithRulesAndAddersProps) => (
<>
{fields
.map((og) => og.label)
.map((lbl) => (
<button onClick={(e) => props.handleOnClick(e, lbl)}>
+Rule ({lbl})
</button>
))}
</>
);
The context is then passed to the onAddRule callback, which determines what field to assign based on the context value.
const onAddRule = (
rule: RuleType,
_pP: number[],
_q: RuleGroupType,
context: string
) => ({
...rule,
context,
field: fields.find((optGroup) => optGroup.label === context)!.options[0].name,
});
Put it all together in the QueryBuilder props, and voilĂ :
export default function App() {
const [query, setQuery] = useState(initialQuery);
return (
<div>
<QueryBuilder
fields={fields}
query={query}
onQueryChange={(q) => setQuery(q)}
controlElements={{
addRuleAction: AddRuleButtons,
fieldSelector: FilteredFieldSelector,
}}
onAddRule={onAddRule}
/>
<pre>{formatQuery(query, 'json')}</pre>
</div>
);
}
Update: see my other answer
This is a little tricky because the onAddRule callback function only accepts the rule to be added (which is always the default rule), and the parent path. If we could pass custom data into it this question would be much easier to answer.
The best way I can think to do it today is to externalize the query update methods out of the QueryBuilder component and manage them yourself (for the most part). In the example below, I've used Redux Toolkit (overkill for this use case but it's what I'm familiar with) to manage the query and replaced the Add Rule button with a custom component that renders two buttons, one to add a new rule for First Name and one to add a new rule for Last Name.
Working CodeSandbox example.
The redux store:
import { configureStore, createSlice, PayloadAction } from '#reduxjs/toolkit';
import { RuleGroupType } from 'react-querybuilder';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
interface State {
query: RuleGroupType;
}
export const getQuery = (state: State) => state.query;
const initialState: State = {
query: {
combinator: 'and',
rules: [],
},
};
const querySlice = createSlice({
name: 'query',
initialState,
reducers: {
setQuery(state: State, action: PayloadAction<RuleGroupWithAggregation>) {
state.query = action.payload;
},
},
});
const { reducer } = querySlice;
export const { setQuery } = querySlice.actions;
export const store = configureStore({ reducer });
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
The App component:
import {
ActionWithRulesProps,
add,
Field,
formatQuery,
QueryBuilder,
} from 'react-querybuilder';
import 'react-querybuilder/dist/query-builder.scss';
import { getQuery, setQuery, useAppDispatch, useAppSelector } from './store';
const fields: Field[] = [
{ name: 'firstName', label: 'First Name' },
{ name: 'lastName', label: 'Last Name' },
];
const AddRuleButtons = (props: ActionWithRulesProps) => {
const dispatch = useAppDispatch();
const query = useAppSelector(getQuery);
const onClickFirst = () =>
dispatch(
setQuery(
add(
query,
{ field: 'firstName', operator: '=', value: 'First' },
props.path
)
)
);
const onClickLast = () =>
dispatch(
setQuery(
add(
query,
{ field: 'lastName', operator: '=', value: 'Last' },
props.path
)
)
);
return (
<>
<button onClick={onClickFirst}>+Rule (First Name)</button>
<button onClick={onClickLast}>+Rule (Last Name)</button>
</>
);
};
export default function App() {
const dispatch = useAppDispatch();
const query = useAppSelector(getQuery);
return (
<div>
<QueryBuilder
fields={fields}
query={query}
onQueryChange={(q) => dispatch(setQuery(q))}
controlElements={{
addRuleAction: AddRuleButtons,
}}
/>
<pre>{formatQuery(query, 'json')}</pre>
</div>
);
}
I have an editable AgGrid in my functional component as below.
On the last column, I have buttons to Add/Remove rows.
Now I want the Add row to be displayed only for the last row. I am using cellRenderer for the last column.
With the below change, I get the Add button for the last row (i.e. on 2nd row in my case) on initial render. But if I click on Add for this 2nd row, while I get the Add button for the new 3rd row, but it does not get removed for the 2nd row. not sure if I am implementing this in the wrong way.
const MyCmp = (props) => {
const getData = () => {
return [{
id: 0,
firstName: 'F1',
lastName: 'L1'
}, {
id: 1,
firstName: 'F2',
lastName: 'L2',
}];
}
const [myCols, setMyCols] = useState(null);
const [gridData, setGridData] = useState(getData());
const [gridApi, setGridApi] = useState('');
let cellRules = {
'rag-red': params => {
if (params.data.lastName === 'INVALID_VAL') {
return true;
}
}
};
const handleGridReady = (params) => {
setGridApi(params.api);
setMyCols([{
headerName: 'F Name',
field: 'firstName',
editable: true
}, {
headerName: 'L Name',
field: 'lastName',
cellClassRules: cellRules,
editable: true
}, {
headerName: '',
field: 'buttonCol',
cellRenderer: 'customColRenderer',
cellRendererParams: {
addItems: addItems
}
}]
);
};
const createNewRowData = () => {
const newData = {
id: newCount,
firstName: '',
lastName: ''
};
newCount++;
return newData;
}
let newCount = getData().length;
const addItems = (addIndex, props) => {
const newItems = [createNewRowData()];
const res = props.api.applyTransaction({
add: newItems,
addIndex: addIndex,
});
setGridData(...gridData, res.add[0].data); // IS THIS CORRECT ?
if (props.api.getDisplayedRowCount() > props.api.paginationGetPageSize()) {
props.api.paginationGoToPage(parseInt((props.api.getDisplayedRowCount() / props.api.paginationGetPageSize())) + 1);
}
}
const onCellClicked = (e) => {
}
const frameworkComponents = () => {
return {customColRenderer: customColRenderer}
}
return (
<>
<MyAgGrid
id="myGrid"
columnDefs={myCols}
rowData={gridData}
frameworkComponents={{customColRenderer: customColRenderer}}
{...props}
/>
</>
)
}
My customColRenderer is as below;
export default (props) => {
let isLastRow = (props.rowIndex === (props.api.getDisplayedRowCount() -1)) ? true: false;
const addItems = (addIndex) => {
props.addItems(addIndex, props);
}
return (
<span>
{isLastRow ? <button onClick={() => addItems()}>Add</button> : null}
<span><button onClick={() => props.api.applyTransaction({remove: props.api.getSelectedRows()})}>Remove</button>
</span>
);
};
Within the AgGrid React internals a transaction is generated automatically when your rowData is updated, as such you can choose to apply the transaction either through the api, or by updating your state - you shouldn't need to do both (as you're currently doing). Generally with React I'd suggest updating the state to keep your state true to the data displayed in the grid, however that can depend on use case.
As for the further issue of your cell not updating - that'll be due to the fact AgGrid has detected no change in the 'value' of the cell, as it attempts to reduce the amount of unnecessary cell rendering done.
You could attempt to call:
api.refreshCells({ force: true });
After your api call applying the transaction (I'm not sure where this would need to happen using the setState approach).
I have the following component. I used react hooks (useHistory, useState) in my component.
export default function ClassTheoryDataTable() {
const dataSource = [
{
key: '1',
date: '18.03.2021',
subject: 'Revision',
inst: 'HASHEL',
edit: 'edit',
delete: 'delete'
}
];
let history = useHistory();
const [tableData, setTableData] = useState(dataSource);
const handleRedirect = (data) => {
history.push("/ClassTheoryDetails");
};
const handleDelete = (key) => {
let dataSource1 = [...tableData];
dataSource1 = dataSource1.filter((item) => item.key !== key);
setTableData(dataSource1);
}
const columns = [
{
title: 'Date',
dataIndex: 'date',
key: 'date',
render: (text, record) => (
<a onClick={() => handleRedirect(record)} type="text">{text}</a>
)
},
{
title: 'Subject',
dataIndex: 'subject',
key: 'subject',
editable: true
},
{
title: 'Inst.',
dataIndex: 'inst',
key: 'inst',
editable: true
},
{
title: '',
dataIndex: 'edit',
key: 'edit',
width: '50px',
render: (text, record) => (
<Space size="middle">
<EditOutlined style={{ color: '#1589FF', fontSize: '15px' }} />
</Space>
)
},
{
title: '',
dataIndex: 'delete',
key: 'delete',
width: '50px',
render: (text, record) => (
dataSource.length >= 1 ?
<Popconfirm title="Sure to delete ?" onConfirm={() => handleDelete(record.key)}>
<CloseCircleFilled style={{ color: 'red', fontSize: '15px' }} />
</Popconfirm>
: null
)
}
];
return (
<>
<Table columns={columns} dataSource={tableData} pagination={false} bordered />
</>
);
}
Essentially I want to delete table row by clicking the delete icon in the last column. But, I'm getting the "Rendered more hooks than during the previous render" error when I load the page. I don't know how to fix it. Can someone help me?
The error is being thrown in the AntD component code, but is only manifesting because of how you've specified your table props.
The issue is in your ClassTheory component in the expandRowRender you are not instantiating the ClassTheoryDataTable subtable correctly.
const expandedRowRender = () => {
const table = new ClassTheoryDataTable();
return table;
};
Here you are invoking the function component directly and returning it, but this isn't how React components are instantiated. In React you describe the UI via JSX and pass this to React and React handles the entire component lifecycle, from instantiation, mounting, rerendering, and unmounting.
This function should return valid JSX.
const expandedRowRender = () => {
return <ClassTheoryDataTable />;
};
I am using Ant Design for my React project and I'm having trouble with the Table component. I have a list of tasks to which I add a new task based on a Form content - currently just by adding to an array of objects (taskListMock in the code snippets), the app is not linked to any backend. The form works fine, however, the Table does not refresh, even though the dataSource prop of the Table gets its content directly from the state and the state updates correctly - confirmed by logging and devtools. Curiously, the table refreshes with the new task when I initiate the implemented sorting, so my suspicion is that the Table somehow does not refresh its content from the state change, only on onChange hooks or something, but I'm feeling in a bit of a dead-end - any help would be greatly appreciated since I'm planning to use similar functionality in other Tables.
The structure is pretty simple, I have a TasksIndex.js with the Table as an individual component in TaskListTable.js
TaskListTable.js:
const TaskListTable = (props) => {
const { t } = useTranslation();
const [tableContent, setTableContent] = useState(props.tasks);
return (
<React.Fragment>
<Table
pagination={false}
dataSource={tableContent}
columns={[
{
title: t("tasks.name"),
key: "name",
render: (text) => {
return <p>{text.slug}</p>;
},
},
{
title: t("tasks.dateDue"),
dataIndex: "dateDue",
key: "dateDue",
sorter: (a, b) =>
new Date(a.dateDue).getTime() - new Date(b.dateDue).getTime(),
render: (dateDue) => {
let dateFormatted = moment(dateDue);
return <>{dateFormatted.format("LL")}</>;
},
defaultSortOrder: "ascend",
},
{
title: t("tasks.priority"),
key: "priority",
dataIndex: "priority",
render: (priority) => (
<React.Fragment>
{priority === "low" ? (
<Tag color="geekblue">{t("tasks.lowPriority")}</Tag>
) : (
""
)}
{priority === "normal" ? (
<Tag color="green">{t("tasks.normalPriority")}</Tag>
) : (
""
)}
{priority === "high" ? (
<Tag color="volcano">{t("tasks.highPriority")}</Tag>
) : (
""
)}
</React.Fragment>
),
sorter: (a, b) => {
const priorityOrder = ["low", "normal", "high"];
return (
priorityOrder.indexOf(a.priority) -
priorityOrder.indexOf(b.priority)
);
},
},
{
title: t("tasks.options"),
key: "options",
render: (item) => {
return (
<Checkbox value={item.id}>{t("tasks.setCompleted")}</Checkbox>
);
},
},
]}
></Table>
</React.Fragment>
);
};
export default TaskListTable;
TaskIndex.js:
const TasksIndex = () => {
const [isModalOpen, setModalOpen] = useState(false);
const [taskList, updateTaskList] = useState(taskListMock);
const [form] = Form.useForm();
const addTask = useCallback(
(values) => {
const newTaskList = taskList;
newTaskList.push({
id: taskList[taskList.length - 1] + 1,
slug: values.name,
description: values.description,
dateDue: values.dateDue.format("YYYY-MM-DD"),
priority: values.priority,
checked: false,
});
form.resetFields();
updateTaskList(newTaskList);
closeModal();
},
[taskList, form]
);
const openModal = () => {
setModalOpen(true);
};
const closeModal = () => {
setModalOpen(false);
};
const { t } = useTranslation();
return (
<React.Fragment>
<Title>{t("tasks.tasksOverviewHeader")}</Title>
<Row gutter={[16, 24]}>
<Col className="gutter-row" span={24}>
<TaskListTable tasks={taskList}></TaskListTable>
</Col>
</Row>
...
...
I finally fixed it - it seems that creating a new array and pushing the new task to it was not considered a state change (or perhaps a Table change trigger), unlike using the spread operator. The working code looks like this:
const addTask = (values) => {
const newTask = {
id: taskList[taskList.length - 1] + 1,
slug: values.name,
description: values.description,
dateDue: values.dateDue.format("YYYY-MM-DD"),
priority: values.priority,
checked: false,
};
updateTaskList([...taskList, newTask]);
closeModal();
form.resetFields();
};
i have a problem with my actionsFormatter.
When I click on the DELETE button, I get the error:
Uncaught ReferenceError: dispatch is not defined at onClick
How could I fix this problem?
import { removeEnvironnement } from '../../actions/environnement';
const EnvironnementList = (props) => (
<BootstrapTable
keyField='id'
data={ props.store.environnements }
columns={ columns }
selectRow={selectRow}
pagination={ paginationFactory() }
filter={ filterFactory() }
striped hover condensed
/>
);
const actionsFormatter = (cell, row) => {
const id=row.id
return (
<button className="btn btn-danger"
onClick={() => {
dispatch(removeEnvironnement({ id }));}}
>Delete</button>
);
};
const columns = [{
dataField: 'id',
text: 'ID'
}, {
dataField: 'nom',
text: 'Nom',
filter: textFilter()
}, {
dataField: 'actions',
text: 'Action',
formatter: actionsFormatter
} ];
const selectRow = {
mode: 'checkbox',
clickToSelect: true,
bgColor: '#00BFFF'
};
const mapStateToProps = (state) => {
return {
store: state
};
}
export default connect(mapStateToProps)(EnvironnementList);
Here is my code to do the remove :
Should I remove the dispatch part?
const _removeEnvironnement = ({ id } = {}) => ({
type: 'REMOVE_ENVIRONNEMENT',
id
});
export const removeEnvironnement = ({ id } = {}) => {
return (dispatch) => {
return axios.delete(`environnements/${id}`).then(() => {
dispatch(_removeEnvironnement({ id }));
})
}
};
What is dispatch in your actionsFormatter? It is defined neither on actionsFormatter scope nor on out of actionsFormatter scope. That's the problem and that's the javascript interpreter talking you about.
One of the possible fix is to import you redux store
store.js
export const store = createStore(...)
EnvironmentList.js
import { store } from './path/to/store.js'
// ...
const actionsFormatter = (cell, row) => {
const { dispatch } = store
const id = row.id
// ...
};
This way you'll get dispatch available in actionsFormatter body.
Another way is to provide mapped method via connect -> EnvironmentList -> actionsFormatter chain. Do what Arnaud Christ suggested in his reply and then refactor the code:
const EnvironmentList = (props) => (
<BootstrapTable
keyField='id'
data={ props.store.environnements }
columns={ columns(props.removeEnvironment) }
selectRow={selectRow}
pagination={ paginationFactory() }
filter={ filterFactory() }
striped hover condensed
/>
);
const actionsFormatter = (removeEnvironment) => (cell, row) => {
const id=row.id
return (
<button className="btn btn-danger"
onClick={() => {
removeEnvironment({ id });
}}
>Delete</button>
);
};
const columns = (removeEnvironment) => [{
dataField: 'id',
text: 'ID'
}, {
dataField: 'nom',
text: 'Nom',
filter: textFilter()
}, {
dataField: 'actions',
text: 'Action',
formatter: actionsFormatter(removeEnvironment)
} ];
So, the connected EnvironmentList got necessary removeEnvironment method on it's props. Then we passes it to columns creator, which passed it to actionsFormatter creator.
You have to link your component with the dispatch method.
As you are already using react-redux to connect your component to your Redux store, you can easily do that through mapping dispatch to props.
Just add the following:
const mapStateToProps = (state) => {
return {
store: state
};
}
const mapDispatchToProps = dispatch => {
return {
removeEnvironnement: id => {
dispatch(removeEnvironnement({ id }));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(EnvironnementList);
And then in your onClick handler, just call this.props.removeEnvironnement(id)