Running two functions on onIonChange in Ionic4 React - javascript

I have been using react hooks form but have come in situation where i need to set the state and also call onChange method to get rid of error for which i have made a validation. Here is my code
<Controller
render={({ onChange, onBlur, value }) => (
<IonSelect
placeholder="Select a State"
onIonChange={onChange}
onIonChange={(e:any)=>{
handleChange(
"selectedStateForBilling",
String(e.detail.value)
);
}}
>
{state.states.map((item) => (
<IonSelectOption value={item} key={item}>
{item}
</IonSelectOption>
))}
</IonSelect>
)}
control={control}
name="billingState"
defaultValue=""
rules={{
required: true,
}}
/>
So looking at this code you may see i want to actually run two onIonChange method one for updating a state variable and the other to update the "billingState" so that i could get rid of error required:true (which is defined in my rules={{}}) is also required to throw error as for the first if users clicks on submit leaving this value empty he sees it. So is there a way i can run onChange and handleChange both in onIonChange method?

I don't use ionic but can't you just write it like this?
onIonChange={(e: any) => {
onChange();
handleChange(
"selectedStateForBilling",
String(e.detail.value)
);
}}

Related

Issue testing functional component with ternary return using React 18 and RTL

I have tried a few ways to do this correctly, but lack the testing experience to catch what I'm missing. I have a LoginForm.tsx component that inside holds a few event handlers and a couple bits of local state using React.useState(). The component returns a ternary statement conditionally rendering two components, and within one of them, that component renders different content based on another boolean condition.
authSuccess: when false, main component returns a <Card /> component; when true, the component returns <Navigate to={...} replace /> to redirect user to account.
isLoading: when false, children of <Card /> is form content, when true, children is a <Spinner /> component.
The problem is, I can't seem to find how to change those useState values in my tests and mock the behavior of this component. I would like to test that errors are rendering correctly as well. I am not using Enzyme since it seems it is dead for anything after React 17, so I have been trying to find a way to do this using just React Testing Library out of the box with Create React App Typescript.
The component code looks like this:
import * as React from 'react'
// ...
export default function LoginForm() {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [errors, setErrors] = React.useState<{ [key: string]: string } | any>({});
const [authSuccess, setAuthSuccess] = React.useState<boolean>(false);
const initialState: LoginFormInitialState = {
email: '',
password: '',
};
// Form Hook
const { values, onChange, onSubmit } = useForm({ callback: handleLogin, initialState });
// ==> HANDLERS
function successCallback(data) {
// ...
return setAuthSuccess(true); // <---- Changes state to return redirect
}
function errorHandler(e: any) {
// ...
setErrors(errors); // <---- // sets errors object to render errors
return setIsLoading(false); // <---- Changes contents of <Card />
}
function handleLogin() {
setIsLoading(true); // <---- Changes content of card
setErrors({}); // <---- Clears errors
// Passes handlers as callbacks into api
return userAccountAPI.login({ data: values, successCallback, errorHandler });
}
return authSuccess ? (
// If authentication was successful, redirect user to account page
<Navigate to={ACCOUNT_OVERVIEW} replace />
) : (
// No auth success yet, keep user on login page
<Card
title={<h1 data-testid="form-header">Login To Account</h1>}
data-testid={'login-card'}
bodyStyle={{
display: isLoading ? 'flex' : 'block',
justifyContent: 'center',
padding: isLoading ? '100px 0 ' : '',
}}>
{isLoading ? (
<Spin size='large' data-testid={'login-spinner'}></Spin>
) : (
<Form name='login_form' data-testid={'login-form'} initialValues={{ remember: true }}>
<Form.Item name='email' rules={[{ required: true, message: 'Please input your email!' }]}>
<Input
name='email'
onChange={onChange}
prefix={<UserOutlined className='site-form-item-icon' />}
placeholder='Email'
/>
</Form.Item>
<Form.Item name='password' rules={[{ required: true, message: 'Please input your Password!' }]}>
<Input
name='password'
onChange={onChange}
prefix={<LockOutlined className='site-form-item-icon' />}
type='password'
placeholder='Password'
/>
</Form.Item>
<Form.Item>
<Button
data-testid='login-button'
onClick={onSubmit}
type='primary'
name='login'
htmlType='submit'
className='login-form-button'
block>
Log in
</Button>
</Form.Item>
</Form>
)}
{/* Render out any form errors from the login attempt */}
{Object.entries(errors).length > 0 && (
<Alert
type='error'
message={
<ul style={{ margin: '0' }}>
{Object.keys(errors).map((er, i) => {
return <li key={i}>{errors[er]}</li>;
})}
</ul>
}
/>
)}
</Card>
);
}
I would like to be able to make an assertion about the card, but not if authSuccess=true, in which case I'd want to assert that we do not have the card and that the redirect has been rendered. I would want to test that the Spinner is a child of the card if isLoading = true, but also that I have the form as a child if it is false.
I have tried some of the approaches I've seen in other issues, but many of them have a button in the UI that directly changes the value and the solution is typically "grab that button and click it" and there you go. But the only button here is the login, and that doesn't directly change the local state values I need to mock.
I have also tried something like this which seems to have worked for some people but not for me here..
import * as React from 'react'
describe('<LoginForm />', () => {
const setup = () => {
const mockStore = configureStore();
render(
<Provider store={mockStore()}>
<LoginForm />
</Provider>
);
};
it('should have a spinner as child of card', () => {
setup();
jest.spyOn(React, 'useState')
.mockImplementationOnce(() => ['isLoading', () => true])
.mockImplementationOnce(() => ['errors', () => {}])
.mockImplementationOnce(() => ['authSuccess', () => false]);
const card = screen.getAllByTestId('login-card');
const spinner = screen.getAllByTestId('login-spinner');
expect(card).toContainElement(spinner);
});
});
It seems like Enzyme provided solutions for accessing and changing state, but as mentioned, I am not using Enzyme since I am using React 18.
How can I test this the way I intend to, or am I making a fundamental mistake with how I am approaching testing this? I am somewhat new to writing tests beyond that basics.
Thanks!
From the test I see that you are using react testing library. In this case you should "interact" with your component inside the test and check if the component reacts properly.
The test for spinner should be like that:
render the component
find the email input field and "type" there an email - use one of getBy* methods and then type with e.g. fireEvent.change(input, {target: {value: 'test#example.com'}})
find the password input field and "type" there a password - same as above
find the submit button and "click" it - use one of getBy* methods to find it and then use fireEvent to click it
this (I assume) should trigger your onSubmit which will call the handleLogin callback which will update the state and that will render the spinner.
check if spinner is in the document.
Most probably you would need some mocking for your userAccountAPI so it calls a mock function and not some real API. In here you can also mock that API to return whatever response you want and check if component displays correct content.

React-hook-form working with multiple array data and conditional fields within a map array

I'm lost in the weeds on this, but I think the issues are straightforward enough, and its just my lack of understanding as to why I cant get this working right. I have a form using react-hook-form that is part of a scheduling/ documentation feature. The initial data is pulled from 1 api endpoint which sets the initial info in the parent level of the form- the standard date/time info and the subsequent conditional goal info if the event has already been interacted with- as an 'event' prop. For the child component within the form (GoalInput), the goal titles are pulled from a separate api endpoint to ensure the available goal fields match the current report. Since the first time a user will interact with any given event, the goal fields should be un-toggled and have no associated user information, however, if they are returning to an event previously interacted with, I want the previously set information (contained in the event initial data mentioned earlier) displayed as the default.
Heres the parent form
/.../
const { register, unregister, handleSubmit, watch, control, setValue, formState: { errors } } = useForm({
defaultValues: {
visitStart: event?.visitStart,
visitEnd: event?.visitEnd,
location: event?.location,
goals: [{
title: '',
marked: false,
note: ''
}]
},
shouldUnregister: true
});
const onSubmit= async (data) => {
/.../
}
return (
<div>
<Button color='primary' variant='contained' onClick={handleClickOpen}>
Edit Visit
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Edit Visit</DialogTitle>
<DialogContent>
<DialogContentText>
Visit for {event.client.fullName}
</DialogContentText>
<form id="editVisit"
onSubmit={(e) =>
handleSubmit(onSubmit, onError)(e).catch((e) => {
console.log("e", e);
})}>
<br></br>
<section>
/... initial fields .../
</section>
{goals && goals.map((goal, index) => (
<GoalInput
key={goal._id}
goal={goal}
index={index}
register={register}
control={control}
errors={errors}
visitGoals={event.goals}
setValue={setValue}
unregister={unregister}
/>
))}
/... end of form/ action buttons .../
And the child component:
function GoalInput({ goal, index, register, unregister, setValue, control, errors, visitGoals }) {
const [toggle, setToggle] = useState(false)
console.log("goal: ", goal)
console.log("visitGoals: ", visitGoals)
const goalData = visitGoals?.filter((value)=> {
if ( value.marked === true) {
return value
}
})
console.log("goalData: ", goalData)
useEffect(() => {
if(!toggle) {
unregister(`goals.${index}.title`)
unregister(`goals.${index}.marked`)
unregister(`goals.${index}.note`)
}
}, [unregister, toggle])
return (
<>
<FormControlLabel
{...register(`goals.${index}.title`, goal.title)}
value={goal.title}
name={`goals.${index}.title`}
control={
<Switch
key={index}
{...register(`goals.${index}.marked`)}
checked={goalData.marked || toggle}
// checked={toggle}
name={`goals.${index}.marked`}
value={goalData.marked || toggle}
onClick={console.log("marked? ", goalData.marked, "toggle ", toggle)}
// value={toggle}
onChange={
() => {
setToggle(!toggle);
setValue(`goals.${index}.title`, goal.title)
}}
/>
}
label={goal.title}
/>
<br />
{toggle ? (
<>
<Controller
control={control}
name={`goals.${index}.note`}
id={`goals.${index}.note`}
render={({field}) => (
<TextField
index={index}
error={!!errors.note}
value={goalData.note || field.value}
// value={field.value}
onChange={(e)=>field.onChange(e)}
label="Progress Note"
/>
)}
/>
<br />
</>
) : <></>}
</>
)
}
The visitGoals prop is passing down the event information if it already contains existing goals. Currently the log is showing that the component is correctly filtering out if the goals had been marked: true previously, however, the actual Switch component is not registering the goalData value. I tried setting it as state and having a useEffect set the state, but I was getting just an empty array. I'm sure theres something simple I'm missing to get the input fields to recognize the values, but I cant figure it.
As an extra question if I may, I'd also like to unregister any fields if the Switch input is not toggled, so that its false, so that I'm not storing a bunch of empty objects. Following the docs and video, I thought I've set it up correctly, even trying shouldUnregister: true in the parent form, but I can't seem to navigate that either. The submission data shows the fields are being registered fine by RHF, so I figured the unregister by the same syntax should have worked.
React Hook Form's unregister docs: https://react-hook-form.com/api/useform/unregister
Any direction or guidance would be greatly appreciated.

Having an issue getting vaues to onClickHandler Due to MUI

So I want to get a category navigation which contains icons and labels.
I've tried with Chips:
{categories.map((category) => (
<Chip
key={category._id}
avatar={<Avatar alt={category.name} src={category.icon} />}
label={category.name}
variant="outlined"
onClick={(e) => handleClick(e)}
value={category._id}
/>
))}
Ive tried using Tabs -> Tab. But they don't produce a "value" when i
const handleClick = (e) => {
const valueTargeting= e.target.value;
console.log(valueTargeting);
};
Is there a way to use any of these, or do I have to resort to designing a button?
Also notice they do output a "value" when clicked at a certain area(which is small surface area). Is that a bug on my part?
Chip is not returning the expected value is because the Chip does not explicitly maintain a value. In order for it to return a value to your event handler, you'll need to wrap the value that you want it to return in the onClick handler itself. For example:
{categories.map((category) => {
return (
<Chip
label={category.name}
// Notice 'category._id' being sent from the handler, not `e`
onClick={handleClick(category._id})}
/>
);
})}
Working MUI 4 Code Sandbox: https://codesandbox.io/s/chip-click-mui4-ggl0z?file=/demo.js:940-1325
Working MUI 5 Code Sandbox: https://codesandbox.io/s/chip-click-mui5-y5xkk?file=/demo.js

ReactSelect - closeOnMenuSelect closes the menu on select even when set to false

I have a react-select component field. I have provided react-select with the following props closeOnMenuSelect={false} && isMulti. Which, theoretically should make the select component not close the menu on selecting an item, but for some reason it does close.
Very weird thing is, everywhere else, I have used that same config for the select component I get it working just fine.
Here is the react-select config:
<Field
name={`${keyField}.${index}.permissions`}
render={({ field: { value, name }, form: { setFieldValue, setFieldTouched } }) => (
<div>
<label htmlFor="namespace-permissions">
Permissions in Namespace <span className="text-danger">*</span>
</label>
<Select
isMulti
closeMenuOnSelect={false}
id="namespace-permissions"
defaultValue={convertNamespaceToDefaultValue(
dependencies.namespacePermissions,
value
)}
options={convertNamespaceToSelect(dependencies.namespacePermissions)}
onChangeCallback={values => {
setFieldValue(name, convertSelectToNamespacesData(values));
setFieldTouched(name, true);
}}
/>
<ErrorMessage name={name} component={FormErrorMessage} />
</div>
)}
/>
Why is it NOT working? And why is the exact same config on another react-select works perfectly without a hitch?
I have update the Description, to show that the react-select is wrapped in a Formik Field. As I said, this is a technique I have used in other parts of my code, but this one, isn't working.

React "Maximum update depth exceeded."

I have a container which is a child container. This child container handles states and it also receives some props from the parent. I have two select statements which onChange sets state in the child container. For some reason, the setState() is causing the container to re render. The weird part is that the render() and also my setState() code is called only once. (I added debugger to check). Please find my Select combobox code below:
<Select
native
name="evidenceNode"
value={nodeType}
onChange={this.handleChange('nodeType')}
className={classes.textField}
>
<option />
{NODE_TYPE.map(function (item, i) {
return <option key={i} value={item}>{item}</option>
})}
</Select>
<Select
native
name="modelType"
value={modelType}
onChange={this.handleChange('modelType')}
className={classes.textField}
>
<option />
{MODEL_TYPE.map(function (item, i) {
return <option key={i} value={item}>{item}</option>
})}
</Select>
Here is my handleChange function:
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
I am suspecting that this is a very small fix but I don't know where am I going wrong.
Things I have tried:
Change the way I am calling handle change to a arrow function and it didnt work
I removed one of the Select statement and tried running again and it worked.
I tried to remove the handleChange call from one of the Select statement and it worked fine.
Also I should mention: I have a componentWillReceiveProps function (Not sure if it matters)
componentWillReceiveProps(nextProps, nextContext) {
if(nextProps.selectedNode !== this.state.selectedNode){
this.setState({
selectedNode: nextProps.selectedNode
});
}
}
The issue is onChange={this.handleChange('modelType')}.
You're not attaching an event, you're calling a method with that.
You're entering in an infinite loop because of that.
this.handleChange('modelType') occurs a re-render which call again this.handleChange('modelType')...etc
Attach on the onChange event an anonymous function which call handleChange
onChange={e => this.handleChange('modelType', e)}
And change handleChange params declaration :
handleChange = (name, event) => {}
The problem wasn't with the handleChange listener. Apparently [Material-UI]https://material-ui.com/ (The tool I am using for the form elements) required us to add a FormControl element above every Select I add. So the component should look something like this.
<FormControl
// className={classes.formControl}
>
<Select
native
name="nodeType"
value={nodeType}
onChange={this.handleChange('nodeType')}
className={classes.textField}
inputProps={{
name: 'nodeType',
id: 'nodeType'
}}
>
<option/>
{NODE_TYPE.map(function (item, i) {
return <option key={i} value={item}>{item}</option>
})}
</Select>
</FormControl>
The mistake I made was I had one FormControl and it had two Select elements inside it. This particular thing isn't documented properly on their website.
I think Material-UI recursively calls handleChange on every Select component inside the Form control and since I had more than one, it was going into a loop. I hope this helps.

Categories

Resources