I am trying to avoid using react state for better performance. However, i have no idea on how can i sort table data. I tried to have sort function in the column that needs sorting with the type("asc" or "desc") and based on name or title or like that. This is the configuration and code for my table
code
const sortBy = (data, key, type = "asc") => data.sort((a, b) => a[key].toLowerCase() < b[key].toLowerCase());
const columns = {
name: {
key: "name",
label: "Listing Name",
css: "color: #444; font-size: 1.1rem; font-weight: bold;",
content: (item: Object) => (
<NameColumn>
{item.url && <Avatar name={item.name} url={item.url} size={80} type="square" />}
<NameWrapper>
<Name>{item.name}</Name>
{item.location && <SubName color='#797979'>{item.location}</SubName>}
</NameWrapper>
</NameColumn>
)
},
agent: {
key: "agent",
label: "Agent",
sort: (data: Array<Object>, item: Object) => sortBy(data, item, 'desc'),
isSortable: true,
hideOnPhone: true
},
price: {
key: "price",
label: "Prices",
hideOnPhone: true
},
};
const userData = [
{
id: 1,
name: "Rahul",
location: 'Delhi',
agent: "hello man",
price: '$15000',
},
{
id: 2,
name: "Sachin Tendulkar",
location: 'Delhi',
agent: "Mumbai Inc",
price: '$15000',
},
];
const rowConfig = {
uniqueKey: "id",
css: `
height: 100px;
&:hover {
background-color: rgba(216, 216, 216, 0.2)};
}
`,
onClick: (e, item) => {
console.log("row was clicked", item);
}
};
type Props = {
location: Object
};
const Tables = ({ location }: Props) => {
const queries = new URLSearchParams(location.search);
return (
<Main>
<Table
rowConfig={rowConfig}
columns={columns}
data={userData}
totalPages={10}
currentPage={
queries.has("page") ? parseInt(queries.get("page"), 10) : 1
}
basePageLink={""}
/>
</Main>
);
};
export default Tables;
const Table = ({
columns,
data = [],
rowConfig: { uniqueKey = "id", css , onClick } = {},
currentPage,
totalPages,
basePageLink
}: Props) => {
const headerColumns = () =>
Object.keys(columns).map(key => (
<Th
key={key}
align={columns[key].align}
width={columns[key].width}
onClick={() => columns[key].isSortable && columns[key].sort(data, key)}
css={columns[key].cssHeader}
>
{columns[key].label ? columns[key].label : ""}
</Th>
));
const cell = (key, item) => (
<Td
key={key}
align={columns[key].align}
width={columns[key].width}
css={columns[key].css}
>
{columns[key].content ? columns[key].content(item) : item[key]}
</Td>
);
const row = (item: Object) => (
<Tr
key={item[uniqueKey]}
css={css}
onClick={onClick ? (e: Event) => onClick(e, item) : null}
>
{Object.keys(columns).map(key => cell(key, item))}
</Tr>
);
return (
<Main>
<T>
<thead>
<tr>{headerColumns()}</tr>
</thead>
<tbody>{data.map(i => row(i))}</tbody>
</T>
<TablePagination
currentPage={currentPage}
totalPages={totalPages}
basePageLink={basePageLink}
/>
</Main>
);
};
export default Table;
The way i am doing wont work cause i am not using the react state which will notify react that the state has changed so re-render the agent column(for now sorting is used in agent column only so).
Any suggesstions? Help would be appreciated!
Related
I have an array of shoe products stored in useState hook, I would like to:
Return a new array of filtered shoes when the checkboxes with the shoe brand is checked.
Remove an item from the filtered array when the checkbox is unchecked.
Render the filtered product(array) to the DOM
Here is the code
import type { NextPage } from 'next'
import { useState } from "react";
import Pagination from '../../../components/Pagination';
import Modal from '../../../components/Modal';
type MenSneakersPageProps = {
result: any[]
}
const MenSneakers: NextPage<MenSneakersPageProps> = ({ result }) => {
const [fetchedData, setFetchData] = useState(result)
const [modal, dispatch] = useReducer(reducer, initialState)
const listOfBrands = function() {
const arr: string[] = [];
for (let i = 0; i < result.length; i++) {
if (!arr.includes(result[i].brand?.name))
arr.push(result[i].brand?.name);
}
return arr;
};
const brandList = listOfBrands()
// tried this but it appends the filtered array to the existing array
const filterBrandHandler = (event: any) => {
const { value, checked } = event;
console.log(`${value} is ${checked}`);
if (checked) {
const filterByBrand = fetchedData.filter(item => item.brand?.name == value)
setFetchData((current) => [...current, filterByBrand])
}else {
const filterByBrand = fetchedData.filter(item => item.brand?.name !== value)
setFetchData((current) => [...current, filterByBrand])
}
}
return (
<div className='px-2'>
<Modal onOpen={modal.secondModal} setPopUp={() => dispatch({ type:'closeModal2', value: false})}>
<h2 className='text-2xl border-bottom border-b-4 border-black w-20 font-semibold pb-2'>Brands</h2>
<hr/>
{brandList.sort().filter(item => item !== undefined).map((item, index) => (
<ul className='mt-6' key={index}>
<li className='my-6'>
<input className="mr-2" type='checkbox' id='brand' value={item} name='brand' onChange={(e) => filterBrandHandler(e.target)}/>
{item}
</li>
</ul>
))}
</Modal>
<Pagination data={fetchedData} title="" pageLimit={2} dataLimit={30}/>
</div>
)
}
export default MenSneakers
the array of shoe product takes this form:
[
{
id: 18657677,
shortDescription: 'VL7N panelled logo-print sneakers',
merchantId: 9564,
brand: { id: 534369, name: 'Valentino Garavani' },
gender: 'men',
images:[Array],
all: [Array]
},
priceInfo: {
formattedFinalPrice: '$689',
formattedInitialPrice: '$689',
finalPrice: 689,
initialPrice: 689,
currencyCode: 'USD',
isOnSale: false,
discountLabel: null,
installmentsLabel: null
},
merchandiseLabel: 'New Season',
merchandiseLabelField: 'NewSeason',
isCustomizable: false,
availableSizes: null,
stockTotal: 170,
url: '/ng/shopping/men/valentino-garavani-vl7n-panelled-logo-print-sneakers-
item-18657677.aspx?storeid=9564',
promotionLabel: null,
type: 'Product',
properties: { rankingTrackingId: 'M5JfpuxHhMlCJl' }
},
]
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 am trying to sort the table data based on the field selected from the dropdown. It should alternatively sort between ascending or descending whenever the same field is clicked. I am maintaining a sort object that decides which field is selected and what is the sort order. Then it is sent to lodash orderBy with the field and the order. It does not work
This is what I have tried. Can some one tell me what I am doing wrong. Help is really appreciated.
https://codesandbox.io/s/simple-react-class-component-1n3f9?file=/src/index.js:0-3000
import React from "react";
import ReactDOM from "react-dom";
import "semantic-ui-css/semantic.min.css";
import { Dropdown } from "semantic-ui-react";
import moment from "moment";
import orderby from "lodash.orderby";
const options = [
{ key: 1, text: "Name", value: "name", icon: "sort" },
{ key: 2, text: "Time", value: "time", icon: "sort" },
{ key: 3, text: "Type", value: "type", icon: "sort" }
];
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
original: [],
sortObject: { field: "", order: "" }
};
}
componentDidMount() {
let list = [
{
name: "namev1",
time: 1583295463213,
type: 14
},
{
name: "namea2",
time: 1582885423296,
type: 15
},
{
name: "namea3",
time: 1581295463213,
type: 16
}
];
this.setState({ list, original: list });
}
handleSearch = e => {
let searchInput = e.target.value;
let filteredData = this.state.original.filter(value => {
return (
value.name.toLowerCase().includes(searchInput.toLowerCase()) ||
value.type.toString().includes(searchInput.toString())
);
});
this.setState({ list: filteredData });
};
formSortObject = fieldName => {
let { sortObject } = this.state;
if (!sortObject.field || sortObject.field !== fieldName) {
Object.assign(sortObject, {
field: fieldName,
order: "asc"
});
return sortObject;
} else if (sortObject.field === fieldName) {
Object.assign(sortObject, {
...sortObject,
order: sortObject.order === "desc" ? "asc" : "desc"
});
return sortObject;
}
};
handleSort = (e, data) => {
let dropdDownValue = data.value;
let currentField = this.formSortObject(dropdDownValue);
let result = orderby(
this.state.list,
currentField.field,
currentField.order
);
this.setState({ list: result });
};
render() {
return (
<>
Search: <input type="text" onChange={this.handleSearch} />
<Dropdown text="Sort By" options={options} onChange={this.handleSort} />
<h1>List</h1>
<table>
<tbody>
{this.state.list.map((item, index) => (
<tr key={index}>
<td>
<p>{index + 1}</p>
</td>
<td>
<p>{item.name}</p>
</td>
<td>
<p>{moment().diff(item.time, "days")}</p>
</td>
<td>
<p>{item.type}</p>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}
}
Your code isnt's working as you spected because you are calling the handleSort function only when the value of the select change (see the onChange of your <Dropdown />).
What you need is the function executed when an option is clicked.
I searched for the documentation of the library you are using and I came to the conclusion that what you need is this.
<Dropdown text='Sort By'>
<Dropdown.Menu>
{options.map(item=>
<Dropdown.Item text={item.text} value={item.value} icon={item.icon} key={item.key} onClick={this.handleSort}/>
)}
</Dropdown.Menu>
</Dropdown>
I tried it in your codesandbox and it works perfectly!
I hope it helps!
I have this scenario where I have a table(react-table), where I am applying column level filtering. I have extracted this as a separate component(DropdDown Component) and this can be attached to any column . I am maintaining a method inside parent component which picks up the union of all the values i.e., selected values of all the dropdowns and then apply server side filtering.
Now the challenge here is , How can i get this consolidated values inside the parent component method?
This DropDown component has list of unique values with respect to that column, there is an Apply button , which applies the server side filtering. Now if I jump onto another column, I need to get the previously checked values and also the current values.
Inside handleSetData() filtering logic is written, I need to get the data from DropDown Component. Everytime I click on Apply on a column filter, I need to get the previously checked values as well.
Can someone help me with this:
Code Sandbox: https://codesandbox.io/s/quizzical-glitter-np8iw
App Component
import * as React from "react";
import { render } from "react-dom";
import ReactTable from "react-table";
import "./styles.css";
import "react-table/react-table.css";
import DropDownComponent from "./DropDown";
interface IState {
data: {}[];
columns: {}[];
}
interface IProps {}
export default class App extends React.Component<IProps, IState> {
constructor(props: any) {
super(props);
this.state = {
data: [
{ firstName: "aaaaa", status: "Pending", visits: 155 },
{ firstName: "aabFaa", status: "Pending", visits: 155 },
{ firstName: "adaAAaaa", status: "Approved", visits: 1785 },
{ firstName: "aAaaaa", status: "Approved", visits: 175 },
{ firstName: "adaSaaa", status: "Cancelled", visits: 165 },
{ firstName: "aaaaa", status: "Cancelled", visits: 157 },
{ firstName: "aaaaa", status: "Approved", visits: 153 },
{ firstName: "aaaaa", status: "Pending", visits: 155 }
],
columns: []
};
}
handleSetState = (columns: any) => {
this.setState({ columns });
};
handleSetData = (value: any) => {
console.log(value); // Here filtering logic is written, I need to get the data from DropDown Component. Everytime I click on Apply on a column filter, I need to get the previously checked values as well
};
componentDidMount() {
let columns = [
{
Header: () => (
<div>
<div style={{ position: "absolute", marginLeft: "10px" }}>
<DropDownComponent
data={this.state.data}
handleSetData={this.handleSetData}
param="firstName"
/>
</div>
<span>First Name</span>
</div>
),
accessor: "firstName",
sortable: false,
show: true,
displayValue: " First Name"
},
{
Header: () => (
<div>
<div style={{ position: "absolute", marginLeft: "10px" }}>
<DropDownComponent
data={this.state.data}
handleSetData={this.handleSetData}
param="status"
/>
</div>
<span>Status</span>
</div>
),
accessor: "status",
sortable: false,
show: true,
displayValue: " Status "
},
{
Header: "Visits",
accessor: "visits",
sortable: false,
show: true,
displayValue: " Visits "
}
];
this.setState({ columns });
}
render() {
const { data, columns } = this.state;
return (
<div>
<ReactTable
data={data}
columns={columns}
defaultPageSize={10}
className="-striped -highlight"
/>
</div>
);
}
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);
DropDown Component
import * as React from "react";
import { Button, Checkbox, Icon } from "semantic-ui-react";
interface IProps {
data: {}[];
handleSetData(arr: any): void;
param: string;
}
interface IState {
showList: boolean;
optionsArr: {}[];
originalState: {}[];
}
export default class DropDownComponent extends React.Component<IProps, IState> {
constructor(props: any) {
super(props);
this.state = {
showList: false,
optionsArr: [],
originalState: []
};
}
toggleList = () => {
this.setState(prevState => ({ showList: !prevState.showList }));
};
handleItemClick = (event: React.FormEvent<HTMLInputElement>, data: any) => {
const index = this.state.optionsArr.findIndex(
(item: any) => item.text === data.name
);
const optionsArr = this.state.optionsArr.map((prevState: any, i: any) =>
i === index
? {
key: prevState.key,
text: prevState.text,
checked: !prevState.checked
}
: prevState
);
this.setState({ optionsArr });
};
submitSelection = () => {
console.log(this.state.optionsArr.filter((item: any) => item.checked)); // This gives me selecte ones
let checkedValues: any = this.state.optionsArr.filter(
(item: any) => item.checked
);
this.setState({ originalState: this.state.optionsArr }, () =>
this.props.handleSetData(checkedValues)
);
};
componentDidMount() {
if (this.props.data) {
let arr = this.props.data;
let uniqueValues = Array.from(
new Set(arr.map((arr: any) => arr[this.props.param]))
);
var optionsArr = [];
for (let i = 0; i < uniqueValues.length; i++) {
var options: any = {};
options["key"] = uniqueValues[i];
options["text"] = uniqueValues[i];
options["checked"] = false;
optionsArr.push(options);
}
this.setState({ optionsArr: optionsArr, originalState: optionsArr });
}
}
clearSelection = (event: any) => {
// Push it to previous state, before cancel was clicked
this.setState({ showList: false, optionsArr: this.state.originalState });
};
render() {
let { showList } = this.state;
let visibleFlag: string;
if (showList === true) visibleFlag = "visible";
else visibleFlag = "";
return (
<div>
<div style={{ position: "absolute" }}>
<div
className={
"ui scrolling dropdown column-settings customized " +
visibleFlag +
" " +
this.props.menuDirection
}
>
<Icon className="filter" onClick={this.toggleList} />
{this.state.optionsArr.length > 0 ? (
<>
<div className="menu-item-holder">
{this.state.optionsArr.map((item: any, i: number) => (
<div className="menu-item" key={i}>
<Checkbox
name={item.text}
onChange={this.handleItemClick}
checked={item.checked}
label={item.text}
/>
</div>
))}
</div>
<div className="menu-btn-holder">
<Button size="small" onClick={this.submitSelection}>
Apply
</Button>
<Button size="small" onClick={this.clearSelection}>
Cancel
</Button>
</div>
</>
) : (
""
)}
</div>
</div>
</div>
</div>
);
}
}
I have modified your DropDown component. I make it receive a list of items to generate the checkboxes. Instead of sending the whole data object to the DropDown component, I think it makes more sense send a ready list to them, the Main component should generate the right data structure (I haven't done that, you have to create those functions). In the component, I create three states to manage the component.
Obs: I removed typescript to make faster for me.
condesandbox
So, I have a property (fields), within which I wish to change the value of an element (countries). Alerting the value of countries currently displays the value 2, but I want to change the value to 100, so that re-alerting fields.countries.value, after the change, displays the new value.
How do I do this?
import type { State } from '../../common/types';
import DynamicField from './DynamicField';
import R from 'ramda';
import React from 'react';
import buttonsMessages from '../../common/app/buttonsMessages';
import linksMessages from '../../common/app/linksMessages';
import { FormattedMessage } from 'react-intl';
import { ValidationError } from '../../common/lib/validation';
import { connect } from 'react-redux';
import { fields } from '../../common/lib/redux-fields';
import {
Block,
Box,
Button,
Checkbox,
FieldError,
Flex,
Form,
Heading,
Input,
PageHeader,
Pre,
Radio,
Select,
Space,
Title,
View,
} from '../app/components';
// The example of dynamically loaded editable data.
// cato.org/publications/commentary/key-concepts-libertarianism
const keyConceptsOfLibertarianism = [
'Individualism',
'Individual Rights',
'Spontaneous Order',
'The Rule of Law',
'Limited Government',
'Free Markets',
'The Virtue of Production',
'Natural Harmony of Interests',
'Peace',
].map((concept, index) => ({
id: index,
name: concept,
}));
// Proof of concept. Country list will be read from firebase
const countryArray = [
{ label: 'Select Country', value: 0 },
{ label: 'France', value: 2 },
{ label: 'England', value: 4 },
{ label: 'Swizterland', value: 8 },
{ label: 'Germany', value: 16 },
{ label: 'Lithuania', value: 32 },
{ label: 'Romania', value: 64 },
].map((countryName, index) => ({
id: index,
name: countryName,
}));
// Dynamically create select list
const countryOptions = [];
countryArray.map(countryItem =>
countryOptions.push({ label: countryItem.name.label, value: countryItem.name.value }),
);
// Proof of concept. Country list will be read from firebase
const cityArray = [
{ label: 'Select City', value: 0 },
{ label: 'London', value: 50 },
{ label: 'Paris', value: 75 },
].map((cityName, index) => ({
id: index,
name: cityName,
}));
// Dynamically create select list
const cityOptions = [];
cityArray.map(cityItem =>
cityOptions.push({ label: cityItem.name.label, value: cityItem.name.value }),
);
// Proof of concept. Country list will be read from firebase
const gymArray = [
{ label: 'Select Gym', value: 0 },
{ label: 'Virgin Sport', value: 23 },
{ label: 'Sports Direct', value: 45 },
].map((gymName, index) => ({
id: index,
name: gymName,
}));
// Dynamically create select list
const gymOptions = [];
gymArray.map(gymItem =>
gymOptions.push({ label: gymItem.name.label, value: gymItem.name.value }),
);
type LocalState = {
disabled: boolean,
error: ?Object,
submittedValues: ?Object,
};
class FieldsPage extends React.Component {
static propTypes = {
fields: React.PropTypes.object.isRequired,
dynamicFields: React.PropTypes.object,
// getCities: React.PropTypes.object,
};
state: LocalState = {
disabled: false,
error: null,
submittedValues: null,
};
onFormSubmit = () => {
const { dynamicFields, fields } = this.props;
const values = {
...fields.$values(),
concepts: {
...dynamicFields,
},
};
// This is just a demo. This code belongs to Redux action creator.
// Disable form.
this.setState({ disabled: true });
// Simulate async action.
setTimeout(() => {
this.setState({ disabled: false });
const isValid = values.name.trim();
if (!isValid) {
const error = new ValidationError('required', { prop: 'name' });
this.setState({ error, submittedValues: null });
return;
}
this.setState({ error: null, submittedValues: values });
fields.$reset();
}, 500);
};
handleSelectedCountryChange = () => {
// Pass in the selected country value to get associated cites
const { fields, getCities } = this.props;
getCities('country', fields.$values());
};
/*
handleSelectedCityChange = (event => {
// Pass in the selected city value to get associated gyms
this.setState({secondLevel: event.target.value});
});
*/
render() {
const { fields } = this.props;
const { disabled, error, submittedValues } = this.state;
return (
<View>
<Title message={linksMessages.fields} />
<PageHeader
description="New clients enter their gym details here."
heading="New user entry form."
/>
<Form onSubmit={this.onFormSubmit}>
<Input
{...fields.name}
aria-invalid={ValidationError.isInvalid(error, 'name')}
disabled={disabled}
label="Your Name"
maxLength={100}
type="text"
/>
<FieldError error={error} prop="name" />
<Heading alt>Key Concepts of Libertarianism</Heading>
<Block>
<Flex wrap>
{keyConceptsOfLibertarianism.map(item =>
<Box mr={1} key={item.id}>
<DynamicField
disabled={disabled}
item={item}
path={['fieldsPage', 'dynamic', item]}
/>
</Box>,
)}
</Flex>
</Block>
<Block>
<Checkbox
{...fields.isLibertarian}
checked={fields.isLibertarian.value}
disabled={disabled}
label="I'm libertarian"
/>
<Checkbox
{...fields.isAnarchist}
checked={fields.isAnarchist.value}
disabled={disabled}
label="I'm anarchist"
/>
</Block>
<Block>
<Flex>
<Radio
{...fields.gender}
checked={fields.gender.value === 'male'}
disabled={disabled}
label="Male"
value="male"
/>
<Space x={2} />
<Radio
{...fields.gender}
checked={fields.gender.value === 'female'}
disabled={disabled}
label="Female"
value="female"
/>
<Space x={2} />
<Radio
{...fields.gender}
checked={fields.gender.value === 'other'}
disabled={disabled}
label="Other"
value="other"
/>
</Flex>
</Block>
<Block>
<Select
{...fields.countries}
disabled={disabled}
label="Countries"
onChange={this.handleSelectedCountryChange}
options={countryOptions}
/>
</Block>
<Block>
<Select
{...fields.cities}
disabled={disabled}
label="Cities"
// onChange={this.handleSelectedCityChange}
options={cityOptions}
/>
</Block>
<Block>
<Select
{...fields.gyms}
disabled={disabled}
label="Gyms"
// onChange={this.handleSelectedCityChange}
options={gymOptions}
/>
</Block>
{/*
Why no multiple select? Because users are not familiar with that.
Use checkboxes or custom checkable dynamic fields instead.
*/}
<Button disabled={disabled} type="submit">
<FormattedMessage {...buttonsMessages.submit} />
</Button>
{submittedValues &&
<Pre>
{JSON.stringify(submittedValues, null, 2)}
</Pre>
}
</Form>
</View>
);
}
}
FieldsPage = fields({
path: 'fieldsPage',
fields: [
'countries',
'cities',
'gyms',
'gender',
'isAnarchist',
'isLibertarian',
'name',
],
getInitialState: () => ({
countries: '0',
cities: '0',
gyms: '0',
gender: 'male',
isAnarchist: false,
isLibertarian: false,
}),
})(FieldsPage);
export default connect(
(state: State) => ({
dynamicFields: R.path(['fieldsPage', 'dynamic'], state.fields),
}),
)(FieldsPage);
=====================================================================
fields.js
/* #flow weak */
import R from 'ramda';
import React from 'react';
import invariant from 'invariant';
import { resetFields, setField } from './actions';
type Path = string | Array<string> | (props: Object) => Array<string>;
type Options = {
path: Path,
fields: Array<string>,
getInitialState?: (props: Object) => Object,
};
const isReactNative =
typeof navigator === 'object' &&
navigator.product === 'ReactNative'; // eslint-disable-line no-undef
// Higher order component for huge fast dynamic deeply nested universal forms.
const fields = (options: Options) => (WrappedComponent) => {
const {
path = '',
fields = [],
getInitialState,
} = options;
invariant(Array.isArray(fields), 'Fields must be an array.');
invariant(
(typeof path === 'string') ||
(typeof path === 'function') ||
Array.isArray(path)
, 'Path must be a string, function, or an array.');
return class Fields extends React.Component {
static contextTypes = {
store: React.PropTypes.object, // Redux store.
};
static getNormalizePath(props) {
switch (typeof path) {
case 'function': return path(props);
case 'string': return [path];
default: return path;
}
}
static getFieldValue(field, model, initialState) {
if (model && {}.hasOwnProperty.call(model, field)) {
return model[field];
}
if (initialState && {}.hasOwnProperty.call(initialState, field)) {
return initialState[field];
}
return '';
}
static lazyJsonValuesOf(model, props) {
const initialState = getInitialState && getInitialState(props);
// http://www.devthought.com/2012/01/18/an-object-is-not-a-hash
return options.fields.reduce((fields, field) => ({
...fields,
[field]: Fields.getFieldValue(field, model, initialState),
}), Object.create(null));
}
static createFieldObject(field, onChange) {
return isReactNative ? {
onChangeText: (text) => {
onChange(field, text);
},
} : {
name: field,
onChange: (event) => {
// Some custom components like react-select pass the target directly.
const target = event.target || event;
const { type, checked, value } = target;
const isCheckbox = type && type.toLowerCase() === 'checkbox';
onChange(field, isCheckbox ? checked : value);
},
};
}
state = {
model: null,
};
fields: Object;
values: any;
unsubscribe: () => void;
onFieldChange = (field, value) => {
const normalizedPath = Fields.getNormalizePath(this.props).concat(field);
this.context.store.dispatch(setField(normalizedPath, value));
};
createFields() {
const formFields = options.fields.reduce((fields, field) => ({
...fields,
[field]: Fields.createFieldObject(field, this.onFieldChange),
}), {});
this.fields = {
...formFields,
$values: () => this.values,
$setValue: (field, value) => this.onFieldChange(field, value),
$reset: () => {
const normalizedPath = Fields.getNormalizePath(this.props);
this.context.store.dispatch(resetFields(normalizedPath));
},
};
}
getModelFromState() {
const normalizedPath = Fields.getNormalizePath(this.props);
return R.path(normalizedPath, this.context.store.getState().fields);
}
setModel(model) {
this.values = Fields.lazyJsonValuesOf(model, this.props);
options.fields.forEach((field) => {
this.fields[field].value = this.values[field];
});
this.fields = { ...this.fields }; // Ensure rerender for pure components.
this.setState({ model });
}
componentWillMount() {
this.createFields();
this.setModel(this.getModelFromState());
}
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() => {
const newModel = this.getModelFromState();
if (newModel === this.state.model) return;
this.setModel(newModel);
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return (
<WrappedComponent {...this.props} fields={this.fields} />
);
}
};
};
export default fields;