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>
);
}
Related
I Install the react-dropdown https://www.npmjs.com/package/react-dropdown?activeTab=readme.
I want to add my class directly to dropdown-option
App.js
import './App.css';
import Dropdown from 'react-dropdown';
import 'react-dropdown/style.css';
import { Option } from 'react-dropdown';
const reporTypes = useSelector((state: RootState) => state.report.options.types);
function App() {
return (
<div className="App">
<Dropdown
onChange={handleTypeChange}
options={reporTypes} value={selectedType} className='mh-select-container' controlClassName='mh-dropdown mh-dropdown-rounded' menuClassName='mh-dropdown-content' />
</div>
);
}
export default App;
How can I add this?
It's in the documentation
//option 1: for static options
const reporTypes = [
{ value: 'one', label: 'One' },
{ value: 'two', label: 'Two', className: 'myOptionClassName' },
]
//option 2: mapping from selector
const reporTypes = useSelector((state: RootState) => state.report.options.types?.map(type => ({ ...type, className: 'myOptionClassName' }));
// Option 3: mapping use useMemo
const reporTypes = useSelector((state: RootState) => state.report.options.types);
const mappedReportTypes = useMemo(() => {
return reporTypes?.map(type => ({ ...type, className: 'myOptionClassName' }))
}, [reporTypes ])
function App() {
return (
<div className="App">
<Dropdown options={reporTypes} placeholder="Select an option" className='mh-class' controlClassName='myControlClassName' menuClassName='myMenuClassName' arrowClassName='myarrow'/>
</div>
);
}
But I would suggest using the library with more rating as it could have lesser bugs and will handle more use cases
I can't seem to figure out what's causing the above issue, and debug properly. From my understanding of Redux Slices, I'm able to directly mutate state in my reducer due to the Immer functionality built-in. If I hard code the redux JSON into the UI component there are no issues which leads me to believe it's a Redux issue. Any advice would be appreciated.
Slice.ts
interface LoadSchedulerState {
gridData: DataRow[] | null,
}
interface DataRow {
id: number,
dis: string,
hour: string
}
const initialState: LoadSchedulerState = {
gridData: null,
}
export const loadSchedulerSlice = createSlice({
name: 'load_scheduler',
initialState,
reducers: {
updateGridData: (state, action: PayloadAction<DataRow>) => {
let newData = [{...action.payload}]
return {...state, gridData:newData}
},
},
});
export const {updateGridData} = loadSchedulerSlice.actions;
export const gridData = (state: { loadScheduler: { gridData: any; }; }) => state.loadScheduler.gridData;
export default loadSchedulerSlice.reducer;
LoadScheduler.ts
import { AgGridColumn, AgGridReact } from "#ag-grid-community/react";
import HeaderGroupComponent from "./HeaderGroupComponent.jsx";
import LoadHeaderComponent from "./LoadHeaderComponent.jsx";
import BtnCellRenderer from './BtnCellRenderer';
import {
AllModules,
ColumnApi,
GridApi,
GridReadyEvent,
} from "#ag-grid-enterprise/all-modules";
import "../../styles/DemoGrid.css";
import { updateGridData, gridData } from "./loadSchedulerSlice";
import { useDispatch, useSelector } from 'react-redux';
const LoadSchedulerGrid = () => {
const [gridApi, setGridApi] = useState<GridApi>();
const [columnApi, setColumnApi] = useState<ColumnApi>();
const [rowData, setRowData] = useState<any>(null);
const gridStateData = useSelector(gridData);
const dispatch = useDispatch();
// PUSH TABLE CHANGES VIA WEBSOCKET TO BACKEND
const handleCellChange = (event: any) => {
}
var init_data = {
id: 0,
dis: "Mon 10/19 8:09 A",
hour: "8 a"
}
const dataSetter = (params: { newValue: any; data: any; }) => {
params.data.dis = params.newValue;
return false;
};
const onGridReady = (params: GridReadyEvent) => {
dispatch(updateGridData(init_data))
setGridApi(params.api);
setColumnApi(params.columnApi);
};
return (
<div className="ag-theme-alpine demo-grid-wrap">
<AgGridReact
onGridReady={(params) => {
onGridReady(params);
}}
immutableData={true}
rowData={gridStateData}
getRowNodeId={node => node.id}
modules={AllModules}
onCellValueChanged={handleCellChange}
defaultColDef={{
resizable: true,
sortable: true,
filter: true,
headerComponentFramework: LoadHeaderComponent,
headerComponentParams: {
menuIcon: "fa-bars",
},
}}
>
<AgGridColumn headerName="#" width={50} checkboxSelection sortable={false} suppressMenu filter={false} pinned></AgGridColumn>
<AgGridColumn headerName="Load Details" headerGroupComponentFramework={HeaderGroupComponent}>
<AgGridColumn field="dis" width={110} headerName="Dispatch" editable cellClass="dispatch" valueSetter={dataSetter} />
<AgGridColumn field="hour" width={50} headerName="Hour" cellClass="hour" />
</AgGridColumn>
</AgGridReact>
</div>
);
};
const rules = {
dc_rules:{
"cell-blue": (params: { value: string }) => params.value === 'ERD',
"cell-beige": (params: {value: string }) => params.value === 'PDC',
"cell-cyan": (params: {value: string }) => params.value === 'CRD'
},
nr_cube_rules:{
"cell-red": (params: {value: number }) => params.value > 10.0
}
}
export default LoadSchedulerGrid;
Ag-grid per default tries to directly mutate the state object outside of a reducer. You have to use Ag-Grids immutableData setting.
https://www.ag-grid.com/javascript-data-grid/immutable-data/
They even have a blog article about using RTK with Ag-Grid (even if they use immutable logic in the reducers - within the RTK reducers this is not necessary as you correctly noted): https://blog.ag-grid.com/adding-removing-rows-columns-ag-grid-with-react-redux-toolkit/
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.
I was creating a react-native component when I received this error..
Failed prop type: The prop `options` is marked as required in `signupCheckBoxes`, but its value is `undefined`.
Basically, what I am doing is passing an array which contains types for the object I want to render
const inputFields = [
{
key: 'dob',
type: 'dateTyper', //change this to Dob component
label: 'Your Date of birth',
helper: 'Your Birthdate will help us in connecting you with people of similar age',
required: true
},
{
key: 'gender',
type: 'checkboxes',
label: 'Gender',
required: true,
templateOptions: {
multipleSelect: true,
options: ['Male', 'Female', 'Others']
}
]
and then mapping the component as user iterates through the array
export const SignupFormComponent = (props) => {
const {
keyboardAutoOpenForText,
inputFields,
buttonStyle,
ProgressBarProps,
backgroundViewColor,
defaultColor,
helperTextStyle,
globalButtonText,
buttonTextStyle,
textStyle,
onButtonClick,
errorStyle,
defaultErrorMessage
} = props
// All the component
const [index, setIndex] = useState(0)
const [payload, setPayloadData] = useState({})
const [Loading, toggleLoadingData] = useState(false)
const [Error, setErrorData] = useState({status: false, message: ''})
// Current Component based on indux
const currentComponent = inputFields[index]
const {key, type, label, helper, buttonText} = currentComponent
const templateOptions = currentComponent.templateOptions || {}
// if no template options, initlalize an empty object
const {number, placeHolder, templateStyle, options} = templateOptions
const usedButtonText = buttonText || globalButtonText
// Setting up/mutating props
// --- Progress bar ---
ProgressBarProps.currentProgress = index
ProgressBarProps.totalNumberOfProgressBars = inputFields.length
ProgressBarProps.colorOfProgressBar = ProgressBarProps.colorOfProgressBar || defaultColor
const onChangeHandler = (data, errorMessage=null) => {
if (!errorMessage) {
const currentData = {...payload}
currentData[key] = data
setPayloadData(currentData)
} else {
setErrorData({status: true, message: errorMessage})
}
}
const getValueFromState = async () => {
setErrorData({status: false, message: ''})
toggleLoadingData(true)
const currentValue = payload[key]
try {
const eventTrack = await onButtonClick(index, key, currentValue, payload)
if (index < inputFields.length) setIndex(index + 1)
return toggleLoadingData(false)
} catch (error) {
if (error.message) setErrorData({status: true, message: error.message})
else setErrorData({status: false, message: defaultErrorMessage})
return toggleLoadingData(false)
}
}
const mapSignUpComponents = {
text: (
<TextInput
placeholder={placeHolder}
number={number}
style={[{color: defaultColor, borderColor: defaultColor}, styles.defaultTextInputStyle, templateStyle]}
onChangeText={text => onChangeHandler(text)}
value={payload[key] ? `${payload[key]}` : ''} // Doesn't seem right but otherwise the value of the text input also get mutate with other values
/>),
dateTyper: (
<DateTyper
textInputStyle={[{color: defaultColor, width: (Dimensions.get('window').width * 0.6)/8 }, styles.nextInputStyle, templateStyle]}
upsideEmit={onChangeHandler}/>
),
checkboxes: (
<CheckBoxes
options={options}
/>
)
}
const renderComponent = mapSignUpComponents[type]
return (
<View>
{renderComponent}
<View>
)
}
Initially, the component should be dateTyper (const renderComponent = mapSignUpComponents[type]) so the options key isn't even required, hence options are undefined
the options is a required prop for checkboxes component, but since we aren't rendering it, I am not sure why i am getting the above error
I would so appreciate if someone could help me out to solve the same.
My checkbox component looks like this
import React, {useState} from 'react'
import PropTypes from 'prop-types'
import { CheckBox } from 'react-native-elements'
import { View, Text } from 'react-native'
const signupCheckBoxes = (props) => {
const { options, multipleSelect} = props
console.log(options)
return (
<View>
<Text> Hello</Text>
</View>
)
}
signupCheckBoxes.propTypes = {
options: PropTypes.array.isRequired,
multipleSelect: PropTypes.bool
}
signupCheckBoxes.defaultProps = {
multipleSelect: true
}
export default signupCheckBoxes
It seems that you are doing <CheckBoxes options={options} /> even if options is undefined. This basically translates to initialising that component (but not calling the render function). During initialisation, react will check that all required props are available which is then throwing an error.
To fix it, I would do the following:
const mapSignUpComponents = {
text: (
<TextInput
placeholder={placeHolder}
number={number}
style={[{color: defaultColor, borderColor: defaultColor}, styles.defaultTextInputStyle, templateStyle]}
onChangeText={text => onChangeHandler(text)}
value={payload[key] ? `${payload[key]}` : ''} // Doesn't seem right but otherwise the value of the text input also get mutate with other values
/>),
dateTyper: (
<DateTyper
textInputStyle={[{color: defaultColor, width: (Dimensions.get('window').width * 0.6)/8 }, styles.nextInputStyle, templateStyle]}
upsideEmit={onChangeHandler}/>
),
checkboxes: options === undefined ? undefined : (
<CheckBoxes
options={options}
/>
)
}
I hope this helps!
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)