I'm using Redux and material-ui
I'm trying to run Dialog with <Slide direction="up"/> animation using this attribute: TransitionComponent
email is a state value that came from reducer and changes when I enter value on TextField
When I try to enter some value on, animation plays but, I want to play it only one time.
interface IProps extends WithStyles<typeof styles> {
// ...
setEmail: (email: string) => void;
email: string;
// ...
}
const LoginDialog: React.SFC<IProps> = props => {
const handleClose = () => {
props.setIsOpen(false);
};
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
props.setPassword(event.target.value);
};
const handlePasswordVisibility = () => {
props.setPasswordVisibility(!props.passwordVisibility);
};
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
props.setEmail(event.target.value);
};
return (
<div>
<Dialog
open={props.isOpen}
//dialog plays animation when props.isOpen changes
TransitionComponent={props => <Slide direction="up" {...props} />}
onClose={handleClose}
aria-labelledby="login-dialog-slide-title"
aria-describedby="login-dialog-slide-description"
disableBackdropClick={true}
keepMounted
>
<DialogTitle id="login-dialog-slide-title">
<FormattedMessage id="logindialog_title" />
</DialogTitle>
<DialogContent>
<TextField value={props.email} onChange={handleEmailChange} autoFocus type="email" label={<FormattedMessage id="logindialog_email" />}/>
<TextField type="password" label={<FormattedMessage id="logindialog_password" />} />
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
<FormattedMessage id="logindialog_cancle" />
</Button>
<Button onClick={handleClose} color="primary">
<FormattedMessage id="logindialog_ok" />
</Button>
</DialogActions>
</Dialog>
</div>
);
};
export default withStyles(styles)(withRouter(LoginDialog));
I updated my container which has mapStateToProps and action, reducer for email
and also you can see my full code here: codesandbox.io/s/nkrmw3wjxj
import { connect } from "react-redux";
import { ICombineReducersState } from "../../../reducers";
import LoginDialog from "./LoginDialog";
import {
setIsOpen,
setPassword,
setPasswordVisibility,
setEmail,
setNickname,
DuplicatedEmail,
setIsEmailDuplicated
} from "../../../actions";
const mapStateToProps = (state: ICombineReducersState) => ({
isOpen: state.open.isOpen,
password: state.password.password,
passwordVisibility: state.password.passwordVisibility,
email: state.email.email,
isPasswordError: state.password.isPasswordError,
isEmailError: state.email.isEmailError,
isEmailDuplicated: state.email.isEmailDuplicated
});
const mapDispatchToProps = (dispatch: any) => ({
setIsOpen: (isOpen: boolean) => dispatch(setIsOpen(isOpen)),
setPassword: (password: string) => dispatch(setPassword(password)),
setPasswordVisibility: (passwordVisibility: boolean) =>
dispatch(setPasswordVisibility(passwordVisibility)),
setEmail: (email: string) => dispatch(setEmail(email)),
setNickname: (nickname: string) => dispatch(setNickname(nickname)),
DuplicatedEmail: () => dispatch(DuplicatedEmail()),
setIsEmailDuplicated: (isEmailDuplicated: boolean) =>
dispatch(setIsEmailDuplicated(isEmailDuplicated))
});
export const LoginDialogContainer = connect(
mapStateToProps,
mapDispatchToProps
)(LoginDialog);
export const SET_EMAIL = "SET_EMAIL";
export const SET_IS_EMAIL_DUPLICATED = "SET_IS_EMAIL_DUPLICATED";
import axios from "axios";
import config from "../config";
export interface IEmailAction {
email: string;
type: string;
isEmailDuplicated: boolean;
}
export const setEmail = (email: string) => {
return {
email,
type: SET_EMAIL,
} as IEmailAction;
};
export const setIsEmailDuplicated = (isEmailDuplicated: boolean) => {
return {
isEmailDuplicated,
type: SET_IS_EMAIL_DUPLICATED,
} as IEmailAction;
}
export const DuplicatedEmail = () => (dispatch: any):boolean => {
axios.get(`${config.REACT_APP_SERVER_URL}/users/email`)
.then(res => {
if (res.data.message.length >= 1) {
return dispatch(setIsEmailDuplicated(true));
}
})
.catch(err => {
console.log(err.response)
})
return dispatch(setIsEmailDuplicated(false));
}
import { IEmailAction, SET_EMAIL, SET_IS_EMAIL_DUPLICATED } from "../actions";
export interface IEmailState {
email: string;
isEmailError: boolean;
isEmailDuplicated: boolean;
}
const createEmpty = () => ({
email: "",
isEmailError: false,
isEmailDuplicated: false,
});
const emailRegex = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*#[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;
export const emailReducer = (state = createEmpty(), action: IEmailAction) => {
switch (action.type) {
case SET_EMAIL: {
return {
email: action.email,
isEmailError: !validateEmail(action.email),
isEmailDuplicated: false,
} as IEmailState;
}
case SET_IS_EMAIL_DUPLICATED: {
return {
email: state.email,
isEmailError: true,
isEmailDuplicated: action.isEmailDuplicated,
} as IEmailState;
}
default:
return state;
}
};
const validateEmail = (email: string):boolean => {
if (emailRegex.test(email)) {
return true;
}
return false;
}
Please let me know if you need more info about it.
Thanks.
The following line was the problem:
TransitionComponent={props => <Slide direction="up" {...props} />}
By defining this inline it means that the TransitionComponent will look like a new component type with each re-render which then causes that portion of the Dialog to re-mount and therefore redo the transition.
This is easily fixed by defining this outside of your component function as a stable component type (I've called it TransitionComponent below) and then using this for the TransitionComponent Dialog prop:
const TransitionComponent = props => <Slide direction="up" {...props} />;
const LoginDialog: React.SFC<IProps> = props => {
...
return (
<div>
<Dialog
open={props.isOpen}
TransitionComponent={TransitionComponent}
...
};
Related
I'm adding TypeScript to my React todo app right now and I'm basically done.
The problem lies in the Todo component. There I give the two event handlers: handleStatus and handleTodo the value: todos[index] as a parameter. This causes me to run into the following error message:
ERROR in src/components/Todo.tsx:26:88
TS2345: Argument of type 'Todos' is not assignable to parameter of type 'MouseEvent<any, MouseEvent>'.
Type 'Todos' is missing the following properties from type 'MouseEvent<any, MouseEvent>': altKey, button, buttons, clientX, and 29 more.
24 | {todos[index].describtion}
25 | <Button lable='' disabled= { false } onClick= { () => handleStatus(todos[index]) } />
> 26 | <Button lable='' disabled= { false } onClick= { () => handleDeleteTodo(todos[index]) } />
| ^^^^^^^^^^^^
27 | </div>
28 | </>
29 | );
How can I define the two handlers or their parameters correctly in TypeScript?
Here is my Code:
Parent Component
interface TodoTableProps {
mockTodos: Array<Todos>
}
let currentTodos: Todos [];
export const TodoTable: FunctionComponent<TodoTableProps> = ({ mockTodos }): ReactElement => {
//Data input
if(mockTodos){
currentTodos = mockTodos;
}
const [todos, setTodos] = useState<Array<Todos>>(currentTodos);
const [enterTodo, setEnterTodo] = useState<string>('');
//Enter Todo handler
const handleEnterTodo = (event: ChangeEvent<HTMLInputElement>): void => {
setEnterTodo(event.currentTarget.value);
};
//Clear Todo handler
const handleClearTodos = (): void => {
const cleanedTodos: Array<Todos> = []
todos.forEach((element: Todos, index: number) => {
if(todos[index].done == false){
cleanedTodos.push(todos[index]);
}
});
setTodos(cleanedTodos);
}
//Create Todo handler
const handleCreateTodo = (): void => {
//create new Todo
const newTodo = {
//id: todos.length+1,
id: v4(),
describtion: enterTodo,
done: false
};
setTodos((todos: Array<Todos>) =>
[
newTodo,
...todos
]
);
setEnterTodo('');
};
//Delete Todod handler
const handleDeleteTodo = (event: Todos): void => {
const newTodos = [...todos];
newTodos.splice(todos.indexOf(event), 1);
setTodos(newTodos);
}
//Status handler
const handleStatus = (event: Todos): void => {
const newStatus = event.done == true ? false : true;
const newTodos = [ ...todos];
newTodos.forEach((element, index) => {
if(newTodos[index].id == event.id){
newTodos[index].done = newStatus;
}
});
setTodos(newTodos);
}
return(
<>
<InputBar
enterTodo={ enterTodo }
handleEnterTodo={ handleEnterTodo }
handleCreateTodo={ handleCreateTodo }
handleClearTodos= { handleClearTodos }
/>
<TodosDisplay
todos={ todos }
handleDeleteTodo={ handleDeleteTodo }
handleStatus={ handleStatus }
/>
</>
);
}
There Between is The Component TodoDisplay which just map the Todos and give the functions to the next Component
Child Component:
interface TodoProps {
todos: Array<Todos>
handleDeleteTodo: MouseEventHandler,
handleStatus: MouseEventHandler,
index: number,
className: string
}
export const Todo: FunctionComponent<TodoProps> = ({ todos, handleDeleteTodo, handleStatus, index, className }): ReactElement => {
return(
<>
<div className= { className } key={ todos[index].id.toString() }>
{todos[index].describtion}
<Button lable='' disabled= { false } onClick= { () => handleStatus(todos[index]) } />
<Button lable='' disabled= { false } onClick= { () => handleDeleteTodo(todos[index]) } />
</div>
</>
);
}
I am sure that the error lies in the Todo component or the parameters that are given to the handler there.
Thank you for help
Addition:
TodoDisplay Component
interface TodosDisplayProps {
todos: Array<Todos>,
handleDeleteTodo: any,
handleStatus: any
}
export const TodosDisplay: FunctionComponent<TodosDisplayProps> = ({ todos, handleDeleteTodo, handleStatus }): ReactElement => {
// just for develope///////
const lineBreak = <hr></hr>
///////////////////////////
return(
<>
{todos.map((element: Todos, index: number) => {
if(todos[index].done == false){
return(
<Todo
key={ todos[index].id.toString() }
todos={ todos }
handleDeleteTodo={ handleDeleteTodo }
handleStatus={ handleStatus}
index={ index }
className= "openTodos"
/>
);
}
})
}
{lineBreak}
{todos.map((element: Todos, index: number) => {
if(todos[index].done == true){
return(
<Todo
key={ todos[index].id.toString() }
todos={ todos }
handleDeleteTodo={ handleDeleteTodo }
handleStatus={ handleStatus}
index={ index }
className= "doneTodos"
/>
);
}
})
}
</>
);
}
Button Component
interface ButtonProps {
lable: string,
disabled: boolean,
onClick: MouseEventHandler<HTMLButtonElement>
}
export const Button: FunctionComponent<ButtonProps> = ({ lable, disabled, onClick}): ReactElement => {
return(
<button type='button' disabled= { disabled } onClick= { onClick }>
{lable}
</button>
);
}
Our project has a dynamic Tab bar, uses redux and custom hooks to manage to add and remove and selection changed. We provide the custom hooks for all routers and actions to add a new tab and display the components relate to it. This tab bar works well with lazy loading in development but always gets 'TypeError: can't resolve read-only property _status of #Object' in production (node sripts/build.js or react-scripts build) even only using React.lazy(() => import). Below are the codes and component stack:
TabHooks:
type AddType = (tabName: string, keepComponent: JSX.Element) => void;
export const useNewAliveTab = (): AddType => {
const dispatch = useDispatch();
const aliveRef = useRef<KeepAlive>();
return (tabName: string, keepComponent: JSX.Element) => {
const now = Date.now().toString();
const keepAliveElement = (
<Suspense fallback={<Loader type="converging-spinner" size="large" />}>
<KeepAlive aliveRef={aliveRef} name={now} key={now}>
<ErrorBoundary>{ keepComponent }</ErrorBoundary>
</KeepAlive>
</Suspense>
);
dispatch(
addNewTab({
tabName: tabName,
uuid: now,
element: keepAliveElement,
})
);
};
};
type DropType = (tabId: string) => void;
export const useDropAliveTab = (): DropType => {
const dispatch = useDispatch();
const { dropScope } = useAliveController();
return (tabId: string) => {
dispatch(removeTab(tabId));
dropScope(tabId);
};
};
type DropCurrentType = () => void;
export const useDropCurrentTab = (): DropCurrentType => {
const dispatch = useDispatch();
const { dropScope } = useAliveController();
const { current } = useSelector((state: RootState) => state.aliveTabs);
return () => {
dispatch(removeTab(current));
dropScope(current);
};
};
TabComponent:
const AliveTabBarComponent = (): JSX.Element => {
const { tabAmount, tabs, current } = useSelector(
(state: RootState) => state.aliveTabs
);
const dispatch = useDispatch();
const dropTab = useDropAliveTab();
const onTabChange = (event: TabStripSelectEventArguments, newValue: string) =>
dispatch(changeSelectedTab(newValue));
return (
<>
<TabStrip selected={tabs.findIndex(item => item.id === current)} onSelect={e => onTabChange(e, tabs[e.selected].id)}>
{tabs.map((tab) => (
<TabStripTab
key={tab.id}
title={
tabAmount !== 0 && (
<GridLayout
gap={{ rows: 6, cols: 6 }}
rows={[{ height: "100%" }]}
cols={[{ width: "90%" }, { width: "10%" }]}>
<GridLayoutItem col={1} row={1}>
<Tooltip anchorElement="target" position="top">
<Typography.p textAlign="center">
{tab.tabName}
</Typography.p>
</Tooltip>
</GridLayoutItem>
<GridLayoutItem col={2} row={1}>
<Tooltip anchorElement="target" position="top">
<Button
iconClass="k-icon k-i-close"
onClick={(e) => {
e.stopPropagation();
dropTab(tab.id);
}}></Button>
</Tooltip>
</GridLayoutItem>
</GridLayout>
)
}>
{tab.keepElement}
</TabStripTab>
))}
</TabStrip>
</>
);
};
export default AliveTabBarComponent;
TabReduxInitState:
interface AliveTabs {
tabs: AliveTabContentList;
current: string;
tabAmount: number;
}
interface AliveTabContent {
tabName: string;
id: string;
keepElement: JSX.Element;
}
type AliveTabContentList = Array<AliveTabContent>;
export const initialAliveTabsState: AliveTabs = {
tabs: new Array<AliveTabContent>(),
current: "",
tabAmount: 0,
};
TabReduxReducers
interface PayloadProps {
uuid: string;
tabName: string;
element: JSX.Element;
}
export const aliveTabsSlice = createSlice({
name: "aliveTabsSlice",
initialState: initialAliveTabsState,
reducers: {
changeSelectedTab(state, action: PayloadAction<string>) {
state.current = action.payload;
},
addNewTab(state, action: PayloadAction<PayloadProps>) {
state.tabAmount++;
state.current = action.payload.uuid;
state.tabs.push({
tabName: action.payload.tabName,
id: action.payload.uuid,
keepElement: action.payload.element,
});
},
removeTab(state, action: PayloadAction<string>) {
const index = state.tabs.findIndex(
(item) => item.id === action.payload
);
const isCurrentTab = state.current === action.payload;
if (index !== -1) {
state.tabAmount--;
state.tabs.splice(index, 1);
if (index === 0) {
if (state.tabAmount > 0) {
if (isCurrentTab) {
state.current = state.tabs[index].id;
}
} else {
state.current = "0";
}
} else if (index > state.tabAmount) {
if (isCurrentTab) {
state.current = state.tabs[state.tabAmount].id;
}
} else {
if (isCurrentTab) {
state.current = state.tabs[index - 1].id;
}
}
}
},
},
});
export default aliveTabsSlice.reducer;
And we use above like this:
const Layout = (): JSX.Element => {
const newTab = useNewAliveTab();
const LazyComponent = React.lazy(() => import("./TestComponent"));
return (
<>
<Button onClick={e => newTab("Test Tab", <LazyComponent />)}>Click Me</Button>
<AliveTabBarComponent />
</>
)
}
We run the codes above very well in development but always get the TypeError in production and the component stack is below:
"
at Lazy
at i (http://localhost:3000/static/js/main.cb249e87.js:2:241027)
at t (http://localhost:3000/static/js/main.cb249e87.js:2:46905)
at t (http://localhost:3000/static/js/main.cb249e87.js:2:46905)
at Suspense
at ke (http://localhost:3000/static/js/main.cb249e87.js:2:50149)
at t (http://localhost:3000/static/js/main.cb249e87.js:2:50398)
at Oe (http://localhost:3000/static/js/main.cb249e87.js:2:51153)
at div
at div
at t (http://localhost:3000/static/js/main.cb249e87.js:2:45629)
at Suspense
at je (http://localhost:3000/static/js/main.cb249e87.js:2:53198)
at t (http://localhost:3000/static/js/main.cb249e87.js:2:53483)
at div
at t (http://localhost:3000/static/js/main.cb249e87.js:2:44132)
at J (http://localhost:3000/static/js/main.cb249e87.js:2:44736)
at t (http://localhost:3000/static/js/main.cb249e87.js:2:56400)"
No idea how to solve. We use this tab to keep alive the components using react-activation, I have tried this is not its problem. And also not the UI framework problem for we have the same issues on Material-UI V4 and Kendo-react.
I had the same error with a similar situation.
I was passing a jsx object with a lazy load element in it into a variable that got used in a dialog component separate from the component I was setting the variable in. A pattern a little like this:
setDialogBody(<><Suspense><LazyLoadedComponent/></Suspense></>);
It was also throwing an error around the 'read-only property _status of #Object' in production.
My solution was to make a new component that contained the LazyLoadComonent and the LazyLoadComponent import logic - I called it a wrapper.
import React, {Suspense} from "react";
const LazyLoadComponent = React.lazy(() => import('./LazyLoadComponent'));
export default function LazyLoadComponentWrapper () {
return <Suspense><LazyLoadComponent/></Suspense>
}
Then I passed that wrapper component into the same pattern:
setDialogBody(<LazyLoadedComponent/>);
I think this simplified it for the minimize code - or shifted the lazyload logic in the same place as where the lazy load was actually taking place in a way that resolved some complication. Anyway, it worked for me.
Perhaps it will work for you too if you try the same approach and use a wrapper component here:
<Button onClick={e => newTab("Test Tab", <LazyComponentWrapper />)}>Click Me</Button>
I am trying to learn react redux and I creating a small todo app, which has backend as REST server
most of the part is implemented, however, I am not able to understand how to pass a value from my input box to rest API call. I am able to successfully store the value of inputbox in redux state.
I am using react-thunk as middleware to handle API calls.
container
import { connect } from 'react-redux'
import {toggleAddTodoDialog, addNewTodo, handleChange} from '../actions'
import AddTodoDialog from '../components/add_todo_dialog';
class AddTodoContainer extends Component {
render() {
return (
<div>
<AddTodoDialog toggleAddTodoDialog={this.props.toggleAddTodoDialog}
addNewTodo = {this.props.addNewTodo}
newTodoList = {this.props.newTodoList}
handleChange = {this.props.handleChange}
is_add_todo_dialog_opened={this.props.is_add_todo_dialog_opened}/>
</div>
)
}
}
const mapStateToProps = (state) => {
return state
}
const bindActionsToDispatch = dispatch =>
(
{
toggleAddTodoDialog : (e) => dispatch(toggleAddTodoDialog(e)),
handleChange: (e) => dispatch(handleChange(e)),
addNewTodo : (e) => addNewTodo(e)
}
)
export default connect(mapStateToProps, bindActionsToDispatch)(AddTodoContainer)
component
export default class AddTodoDialog extends Component {
toggleAddTodoDialog = (e) => {
this.props.toggleAddTodoDialog(!this.props.is_add_todo_dialog_opened)
}
addNewTodo = (e) => {
this.props.addNewTodo()
this.toggleAddTodoDialog(e)
}
handleChange = (e) => {
this.props.handleChange(e)
}
render() {
return (
<div>
<Button color="primary" onClick={this.toggleAddTodoDialog}>Add new Todo</Button>
<Modal isOpen={this.props.is_add_todo_dialog_opened} >
{/* <Modal isOpen={false} > */}
<ModalHeader toggle={this.toggleAddTodoDialog}>Modal title</ModalHeader>
<ModalBody>
<FormGroup >
<Label for="Title">Task </Label>
<Input name="task"
value={this.props.newTodoList.task}
onChange={this.handleChange} />
</FormGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.addNewTodo}>OK</Button>{' '}
<Button color="secondary" onClick={this.toggleAddTodoDialog}>Cancel</Button>
</ModalFooter>
</Modal>
</div>
);
}
actions
export function addNewTodo() {
console.log("addNewTodo")
return function (dispatch) {
axios.post("http://localhost:5001/todos", 'task=' + this.props.addNewTodo.task)
.then(response => {
dispatch(_addTodoAction(response.data))
})
}
}
export function _addTodoAction(todos) {
console.log("_addTodoAction")
return {
type: 'ADD_TODO',
todos: todos
}
}
export function handleChange(event){
console.log("handleChange "+event)
return{
type: 'HANDLE_CHANGE',
event: event
}
}
reducer
case 'ADD_TODO':
console.log(action)
return {
...state,
todos: action.todos
}
You are dispatching like this:
addNewTodo : (e) => addNewTodo(e)
But your action doesn't take any arguments:
export function addNewTodo() {
console.log("addNewTodo")
return function (dispatch) {
axios.post("http://localhost:5001/todos", 'task=' + this.props.addNewTodo.task)
.then(response => {
dispatch(_addTodoAction(response.data))
})
}
}
If you want the value of the value of the input:
handleChange = (e) => {
this.props.handleChange(e.target.value)
}
And then dispatch:
handleChange: (value) => dispatch(handleChange(value)),
And then in the action:
export function handleChange(value) {
...
I have a weird scenario in which inside my redux dev-tools I can see that the redux state has been updated but that value is not being reflected in the component which is connected to that.
So I have a component CreateEnvironment like
export interface StateProps {
environment: EnvironmentStep;
selectProduct: SelectProductStep;
eula: EULAStep;
license: LicenseStep;
infrastructure: InfrastructureStep;
network: NetworkStep;
certificate: CertificateStep;
products: any;
}
export interface DispatchProps {
onFormValuesUpdate: (key: string, value: any) => void;
}
type Props = StateProps & DispatchProps;
interface State {
dashboard: boolean;
}
class CreateEnvironment extends Component<Props, State> {
private page: RefObject<any>;
constructor(props: Props) {
super(props);
this.state = { dashboard: false };
this.page = React.createRef();
}
public render() {
console.log("Hi"); //tslint:disable-line
if (this.state.dashboard) {
return <Redirect to="/dashboard" />;
}
const {
environment,
eula,
selectProduct,
infrastructure,
network,
license,
certificate,
onFormValuesUpdate
} = this.props;
return (
<Fragment>
<h2>Create Environment</h2>
<div style={{ height: '100%', paddingBottom: '30px' }}>
<WorkFlow
orientation="vertical"
onNext={this.onNext}
onFinish={this.onFinish}
>
<WorkFlowPage pageTitle="Environment">
<Environment
environment={environment}
onUpdate={onFormValuesUpdate}
ref={this.page}
/>
</WorkFlowPage>
<WorkFlowPage pageTitle="Products & Solutions">
<SelectProduct
ref={this.page}
selectProduct={selectProduct}
onUpdate={onFormValuesUpdate}
/>
</WorkFlowPage>
<WorkFlowPage
pageNavTitle="EULA"
pageTitle="End User License Agreement Details"
>
<Eula eula={eula} ref={this.page} onUpdate={onFormValuesUpdate} />
</WorkFlowPage>
<WorkFlowPage pageTitle="License">
<License
license={license}
ref={this.page}
onUpdate={onFormValuesUpdate}
/>
</WorkFlowPage>
<WorkFlowPage pageTitle="Infrastructure">
<Infrastructure
infrastructure={infrastructure}
ref={this.page}
onUpdate={onFormValuesUpdate}
/>
</WorkFlowPage>
<WorkFlowPage pageTitle="Network">
<Network
network={network}
ref={this.page}
onUpdate={onFormValuesUpdate}
/>
</WorkFlowPage>
<WorkFlowPage pageTitle="Certificate">
<Certificate
certificate={certificate}
ref={this.page}
onUpdate={onFormValuesUpdate}
/>
</WorkFlowPage>
<WorkFlowPage pageTitle="Products">I am products step</WorkFlowPage>
<WorkFlowPage pageTitle="Summary">I am summary step</WorkFlowPage>
<WorkFlowButton role="previous">Back</WorkFlowButton>
<WorkFlowButton role="next">Next</WorkFlowButton>
<WorkFlowButton role="finish">Finish</WorkFlowButton>
</WorkFlow>
</div>
</Fragment>
);
}
private onNext = () => {
if (this.page.current) {
this.page.current.handleSubmit();
}
}
private onFinish = () => {
this.setState({
dashboard: true
});
}
}
export default CreateEnvironment;
The corresponding container looks like:
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import {
CreateEnvironmentAction,
updateEnvironmentWorkflow
} from '../actions/CreateEnvironment';
import {
CreateEnvironment,
DispatchProps,
StateProps
} from '../components/create-environment';
import { StoreState } from '../types';
export const mapStateToProps = ({
createEnvironment
}: StoreState): StateProps => ({
...createEnvironment
});
export const mapDispatchToProps = (
dispatch: Dispatch<CreateEnvironmentAction>
): DispatchProps => ({
onFormValuesUpdate: (key: string, values: any) => {
dispatch(updateEnvironmentWorkflow(key, values));
}
});
export default connect<StateProps, DispatchProps>(
mapStateToProps,
mapDispatchToProps
)(CreateEnvironment);
All the components inside WorkflowPage like Environment are forms which use the Formik pattern.
A sample component is:
interface Props {
certificate?: CertificateStep;
onUpdate?: (key: string, value: any) => void;
}
class Certificate extends Component<Props> {
private submitForm: (
e?: React.FormEvent<HTMLFormElement> | undefined
) => void;
public render() {
const { formValues } = this.props.certificate!;
console.log(formValues); //tslint:disable-line
return (
<Form
className="license"
type="horizontal"
initialValues={formValues}
onSubmit={this.onSubmit}
>
{formProps => {
this.submitForm = formProps.handleSubmit;
return (
<Fragment>
<CheckboxField
name="productSpecific"
id="productSpecific"
type="toggle"
>
Provide Product Specific Certificate
</CheckboxField>
<SelectField name="certificate" label="Certificate">
<SelectOption value="">---Select Certificate---</SelectOption>
</SelectField>
</Fragment>
);
}}
</Form>
);
}
public handleSubmit = () => {
this.submitForm();
}
private onSubmit = (values: FormValues, actions: FormActions) => {
this.props.onUpdate!('certificate', values);
actions.setSubmitting(false);
}
}
export default Certificate;
On clicking the next WorkflowButton the redux state is updated by passing an action updateEnvironmentWorkflow. The action is like:
export interface UpdateEnvironmentWorkflow {
type: Constants.UPDATE_ENVIRONMENT_WORKFLOW;
payload: {
key: string;
value: any;
};
}
export type CreateEnvironmentAction = UpdateEnvironmentWorkflow;
export const updateEnvironmentWorkflow = (
key: string,
value: any
): UpdateEnvironmentWorkflow => ({
payload: { key, value },
type: Constants.UPDATE_ENVIRONMENT_WORKFLOW
});
And my reducer is like:
const createEnvironment = (
state: CreateEnvironment = initialState.createEnvironment,
action: CreateEnvironmentAction
) => {
switch (action.type) {
case Constants.UPDATE_ENVIRONMENT_WORKFLOW:
return {
...state,
[action.payload.key]: {
...state[action.payload.key],
formValues: action.payload.value
}
};
default:
return state;
}
};
export default createEnvironment;
Now the weird part is that if I click the next button the redux state is updated with new values. If I navigate to some other page and come back to create environment my form values are showing the updated values from the redux store state.
BUT if I just hit the previous button my state values are not showing updated values.
The console.log(formValues) inside render() of Certificate is showing initial values only.
Any help will be appreciated. Thanks.
Also initialValues of Form is Formik initialValues. It maybe a formik issue as well
I try to use redux-form in my app, how to test handleSubmit by redux-form? I use enzyme, and previously I just found some component, simulate click, and check the call args, and how much click was trigger. When I switch to redux-form,it is not quite clear for me how to write right unit test and check handleSubmit.
export const validate = device => {
const errors = {}
if (device.name && device.name.length > constants.defaultMaxTextFieldLength) {
errors.name = <FormattedMessage id={tooLongErrorMessage} />
}
if (!device.name)
errors.name = <FormattedMessage id={emptyErrorMessage} />
return errors
}
export class EditDevice extends Component {
static propTypes = {...}
update = device => {
device.miConfiguration.isMiEnabled = device.miConfiguration.miConfigurationType !== MiConfigurationTypes.AccessPointOnly
this.props.update(device).then(({success, ...error}) => {
if (!success)
throw new SubmissionError(error)
this.returnToList()
})}
returnToList = () => this.props.history.push({pathname: '/devices', state: {initialSkip: this.props.skip}})
render = () => {
let {isLoadingInProgress, handleSubmit, initialValues: {deviceType} = {}, change} = this.props
const actions = [
<Button
name='cancel'
onClick={this.returnToList}
>
<FormattedMessage id='common.cancel' />
</Button>,
<Button
name='save'
onClick={handleSubmit(this.update)}
color='primary'
style={{marginLeft: 20}}
>
<FormattedMessage id='common.save' />
</Button>]
return (
<Page title={<FormattedMessage id='devices.deviceInfo' />} actions={actions} footer={actions}>
<form onSubmit={handleSubmit(this.update)}>
{isLoadingInProgress && <LinearProgress mode='indeterminate'/>}
<Grid container>
<Grid item xs={12} sm={6} md={4} >
<Field
component={renderTextField}
name='name'
label={<FormattedMessage id='name' />}
/>
</Grid>
....
</Grid>
</form>
</Page>
)
}
}
export const mapStateToProps = (state, {match: {params: {deviceId}}}) => {
let initialValues = deviceId && state.entities.devices[deviceId]
return {
initialValues,
deviceId,
isLoadingInProgress: state.devices.isLoadingInProgress,
skip: state.devices.skip,
form: `device-${deviceId}`,
}
}
export const mapDispatchToProps = (dispatch, {match: {params: {deviceId}}}) => ({
init: () => dispatch(DeviceActions.get(deviceId)),
update: device => dispatch(DeviceActions.createOrUpdate(device)),
})
export default compose(
connect(mapStateToProps, mapDispatchToProps),
reduxForm({validate, enableReinitialize: true, keepDirtyOnReinitialize: true}),
)(EditDevice)
Previous unit test.
describe('EditDevice', () => {
let init, handleSubmit, page, push
beforeEach(() => page = shallow(<EditDevice
handleSubmit={handleSubmit = sinon.spy()}
deviceId={deviceId}
history={{push: push = sinon.spy()}}
skip={skip}
/>))
......
it('should call push back to list on successful response', async () => {
let update = sinon.stub().resolves({success: true})
page.setProps({update})
page.find(Field).findWhere(x => x.props().name === 'name').simulate('change', {}, 'good name')
await page.find(Page).props().footer.find(x => x.props.name === saveButtonName).props.onClick()
push.calledOnce.should.be.true
push.calledWith({pathname: '/devices', state: {initialSkip: skip}}).should.be.true
})
describe('mapStateToProps', () => {
const deviceId = 123
const device = {}
const isEnabled = true
const isLoadingInProgress = {}
let props
let skip = {}
beforeEach(() => props = mapStateToProps({devices: {isLoadingInProgress, skip}, entities: {devices: {[deviceId]: device}}}, {match: {params: {deviceId}}}))
it('should pass deviceId, form, isLoadingInProgress and skip from state', () => {
props.deviceId.should.be.equal(deviceId)
props.isLoadingInProgress.should.be.equal(isLoadingInProgress)
props.skip.should.be.equal(skip)
props.form.should.be.equal(`device-${deviceId}`)
})
})
describe('mapDispatchToProps', () => {
const response = {}
const deviceId = 123
let props
beforeEach(() => props = mapDispatchToProps(x=> x, {match: {params: {deviceId}}}))
it('init should call get from DeviceActions', () => {
sinon.stub(DeviceActions, 'get').returns(response)
props.init(deviceId).should.be.equal(response)
DeviceActions.get.calledOnce.should.be.true
DeviceActions.get.args[0][0].should.be.equal(deviceId)
DeviceActions.get.restore()
})
it('update should call createOrUpdate from DeviceActions', () => {
const device = {}
sinon.stub(DeviceActions, 'createOrUpdate').returns(response)
props.update(device).should.be.equal(response)
DeviceActions.createOrUpdate.calledOnce.should.be.true
DeviceActions.createOrUpdate.args[0][0].should.be.equal(device)
DeviceActions.createOrUpdate.restore()
})
})