I'm developing a complex solution for building forms from a JSON contract using Formik.
By design, inputs can have tooltips. Or they could not.
Trying to make some parts of the code reusable to avoid repeating myself. Created TextFieldSSWrapper for inputs with server-side validations, forwarded a ref as docs tells. Now, this component could be wrapped with Tooltip, if there is one.
The problem is the following: now, when using this middleware component, input loses focus after each onChange call. Also I'm getting a warning on each onChange call:
The anchorEl prop provided to the component is invalid.
If drop wrapper component and do simple copy-paste, everything works fine.
What am I doing wrong? Thanks a lot.
import {useField, Field} from 'formik';
import TextField from '#mui/material/TextField';
import {asyncValidationCheckers} from '../services/validation.service';
import {Tooltip} from '#mui/material';
interface ITextItem {
id: string;
type?: string;
name: string;
value: string | number;
label: string;
placeholder: string;
tooltip?: string;
variant: 'standard' | 'outlined' | 'filled';
size: 'small' | 'medium',
serverValidation: string;
}
interface IConfigFormControl {
error?: boolean;
errorText?: string;
}
interface ITextFieldSSWrapper {
form: any;
ref: any;
}
export const TextItem = (
{
id,
type,
name,
value,
label,
placeholder,
tooltip,
variant,
size,
serverValidation
}: ITextItem
) => {
const [field, meta] = useField(name);
const configFormControl: IConfigFormControl = {
error: false,
errorText: ''
};
if (meta && meta.touched && meta.error) {
configFormControl.error = true;
configFormControl.errorText = meta.error;
}
const helperText = configFormControl.errorText;
const props = {
fullWidth: true,
id,
type,
label,
placeholder,
helperText,
variant,
size,
error: configFormControl.error,
...field
};
const TextFieldSSWrapper = React.forwardRef<HTMLDivElement, ITextFieldSSWrapper>(({ form, ...rest }, ref) => (
<TextField
{...props}
{...rest}
ref={ref}
onChange={(e) => {
const value = e.target.value;
asyncValidationCheckers[serverValidation].setInputValid();
// it the line above we remove server-side validation error
// in order to be able to run server-side validation again on blur
// because it won't run if input has an error from previous sever-side validation cycle
form.setFieldTouched(id, true);
form.setFieldValue(id, value);
}}
onBlur={(e) => {
const value = e.target.value;
if (!form.errors[id]) {
setTimeout(() => {
asyncValidationCheckers[serverValidation].validateInput(value, () => {
form.setFieldTouched(id, true);
});
}, 0);
}
}}
/>
));
const ref = React.createRef<HTMLDivElement>();
return (
serverValidation
? (
<Field
name={id}
>
{
({form} : any) => (
tooltip
? (
<Tooltip title={tooltip} placement='bottom'>
<TextFieldSSWrapper ref={ref} form={form} />
</Tooltip>
)
: (
<TextFieldSSWrapper ref={ref} form={form} />
)
)
}
</Field>
)
: (
tooltip
? (
<Tooltip title={tooltip} placement='bottom'>
<TextField
{...props}
/>
</Tooltip>
)
: (
<TextField
{...props}
/>
)
)
);
};
Related
How can I create component dynamically using React with TypeScript?
Assuming that I have some RandomComponent and I pass it as props to renderInput, how could I return <RandomComponent> from it?
TypeScript does not seem to understand something like this:
export const renderInput = (props) => {
const Field: JSX.Element = props.component;
return (
<Field {...props} />
)
}
Edit:
Made it work, but am wondering what if I wanted to add this ComponentType type. It works without it, but when I add it and the code looks like this:
const Field: ComponentType = component;
return (
<Field name={name} label={label} value={value} onChange={onChange} {...props} />
);
};
I get:
TS2322: Type '{ name: any; label: any; value: any; onChange: any; }' is not assignable to type 'IntrinsicAttributes'. Property 'name' does not exist on type 'IntrinsicAttributes'
You can use generic type
export const renderInput = <T,>(props:T & {component: React.reactNode }) => {
const Field = props.component;
return (
<Field {...props}/>
)
}
Alternatively if only need to send a component you can send it as children
export const renderInput = (props: { children: React.reactNode }) => {
const { children } = props;
return children;
};
I am using React + Typescript. I am working on a component that can return dynamic HTML element depends on props:
interface Props {
label?: string;
}
const DynamicComponent = forwardRef<
HTMLButtonElement | HTMLLabelElement,
Props
>((props, ref) => {
if (props.label) {
return (
<label ref={ref as React.ForwardedRef<HTMLLabelElement>}>
{props.label}
</label>
);
}
return (
<button ref={ref as React.ForwardedRef<HTMLButtonElement>}>BUTTON</button>
);
});
Is it possible to type ref's interface in a way that will depend on the label prop?
export default function App() {
const btnRef = useRef<HTMLButtonElement>(null); // Allowed
const labelRef = useRef<HTMLLabelElement>(null); // Allowed
// const labelRef = useRef<HTMLButtonElement>(null); // Not allowed, because `label` prop was not provided
return (
<>
<DynamicComponent ref={btnRef} />
<DynamicComponent ref={labelRef} label="my label" />
</>
);
}
Sandbox link
In order to do that we need to use function overloading, with higher order function pattern and typeguards:
FIXED
import React, { forwardRef, useRef, Ref } from 'react'
interface Props {
label?: string;
}
// typeguard
const isLabelRef = (props: Props, ref: React.ForwardedRef<any>): ref is React.ForwardedRef<HTMLLabelElement> => {
return true // ! NON IMPLEMENTED
}
// typeguard
const isButtonRef = (props: Props, ref: React.ForwardedRef<any>): ref is React.ForwardedRef<HTMLButtonElement> => {
return true // ! NON IMPLEMENTED
}
// Higher order COmponent with overloads
function DynamicComponent<T extends HTMLButtonElement>(reference: Ref<T>): any
function DynamicComponent<T extends HTMLLabelElement>(reference: Ref<T>, props: Props): any
function DynamicComponent<T extends HTMLElement>(reference: Ref<T> | undefined, props?: Props) {
const WithRef = forwardRef<HTMLElement, Props>((_, ref) => {
if (props && isLabelRef(props, ref)) {
return (
<label ref={ref}>
{props.label}
</label>
);
}
if (props && isButtonRef(props, ref)) {
return (
<button ref={ref}>BUTTON</button>
);
}
return null
});
return <WithRef ref={reference} />
}
export default function App() {
const btnRef = useRef<HTMLButtonElement>(null); // Allowed
const labelRef = useRef<HTMLLabelElement>(null); // Allowed
return (
<>
{DynamicComponent(btnRef)}
{DynamicComponent(labelRef, { label: 'sdf' })}
</>
);
}
Playground
As you might have noticed, I use only props from DynamicComponent.
Also T generic parameter serves for narrowing the ref type
I left isButtonRef and isLabelRef unimplemented
UPDATE
Seems that my previous example is useless. Sorry for that.
My bad. I have already fixed it.
As an alternative solution, you can override built in forwardRef function.
interface Props {
label: string;
}
declare module "react" {
function forwardRef<T extends HTMLButtonElement, P>(
render: ForwardRefRenderFunction<HTMLButtonElement, never>
): ForwardRefExoticComponent<
PropsWithRef<{ some: number }> & RefAttributes<HTMLButtonElement>
>;
function forwardRef<T extends HTMLLabelElement, P extends { label: string }>(
render: ForwardRefRenderFunction<HTMLLabelElement, { label: string }>
): ForwardRefExoticComponent<
PropsWithRef<{ label: string }> & RefAttributes<HTMLLabelElement>
>;
}
const WithLabelRef = forwardRef<HTMLLabelElement, Props>((props, ref) => (
<label ref={ref}>{props.label}</label>
));
const WithButtonRef = forwardRef<HTMLButtonElement>((props, ref) => (
<button ref={ref}>{props}</button>
));
function App() {
const btnRef = useRef<HTMLButtonElement>(null);
const labelRef = useRef<HTMLLabelElement>(null);
const divRef = useRef<HTMLDivElement>(null);
return (
<>
<WithButtonRef ref={btnRef} />
<WithLabelRef ref={labelRef} label="my label" />
<WithLabelRef ref={divRef} label="my label" /> //expected error
</>
);
}
I'm having this Typescript error when using react-autosuggest with a custom input styled-component.
Types of property 'onChange' are incompatible. Type '(event: FormEvent, params: ChangeEvent) => void' is not assignable to type '(event: ChangeEvent) => void'.
Code:
(note that it's not complete, just relevant portions)
// styled.ts
export const Input = styled.input`
// styles
`;
// index.tsx
function renderInput(
inputProps: Autosuggest.InputProps<SuggestionSearch>,
placeholder: string
) {
// --> error here
return <Input {...inputProps} placeholder={placeholder} />;
}
const MyComponent = () => {
const autosuggestRef = React.useRef<
Autosuggest<SuggestionSearch, SuggestionSearch>
>(null);
const onChange = (event: React.FormEvent, { newValue }: ChangeEvent) =>
setValue(newValue);
const inputProps = {
value,
onChange,
onKeyPress: onEnterPress,
};
return (
<Autosuggest
ref={autosuggestRef}
renderInputComponent={props => renderInput(props, placeholder)}
inputProps={inputProps}
// other props
/>
)
}
Not sure how to fix this, as Autosuggest onChange function overrides the base input onChange prop.
I spent about all night on this problem myself. It seems that this is a bug. Here is the signature of the InputProps:
interface InputProps<TSuggestion>
extends Omit<React.InputHTMLAttributes<any>, 'onChange' | 'onBlur'> {
onChange(event: React.FormEvent<any>, params: ChangeEvent): void;
onBlur?(event: React.FocusEvent<any>, params?: BlurEvent<TSuggestion>): void;
value: string;
}
It seems we'll need to create our own Input component that take in this type of onChange signature or wrap this onChange in the standard onChange.
So, this is my approach:
const renderInputComponent = (inputProps: InputProps<TSuggestion>): React.ReactNode => {
const onChangeHandler = (event: React.ChangeEvent<HTMLInputElement>): void => {
inputProps.onChange(event, { newValue: event.target.value, method: 'type' });
};
return <Input {...inputProps} onChange={onChangeHandler} />;
}
Two potential bugs:
Notice that the event returned is React.ChangeEvent and not the React.FormEvent. Minor difference but it can be a problem in runtime if you actually use it and particular enough about the event.
The returned params: ChangeEvent only account for type event method. If you want others, like click, esc, etc..., you must supplement your own (via onKeyUp for example, and then call `onChange(...{method: 'esc'})
According to doc:
Note: When using renderInputComponent, you still need to specify the usual inputProps. Autosuggest will merge the inputProps that you provide with other props that are needed for accessibility (e.g. 'aria-activedescendant'), and will pass the merged inputProps to renderInputComponent.
you don't have to do this:
renderInputComponent={props => renderInput(props, placeholder)}
Simply pass the placeholder directly into your inputProps and that would be given right back to you in the renderInputComponent.
const inputProps = {
value,
onChange,
onKeyPress: onEnterPress,
placeholder: 'something!`
};
My problem is the onChange() on the renderInputComponent does not trigger the onSuggestionsFetchRequested.
If I do not use custom input (via renderInputComponent), then everything works fine - onSuggestionsFetchRequested got triggered and suggestion list showing.
Main component:
const MySearchBox: FC<MySearchBoxProps> = (props) => {
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: props.placeholder,
value: props.value,
onChange: props.onChange
};
return (
<Autosuggest
suggestions={props.suggestions}
onSuggestionsFetchRequested={props.onSuggestionsFetechRequested}
onSuggestionsClearRequested={props.onSuggestionsClearRequested}
getSuggestionValue={props.getSuggestionValue}
shouldRenderSuggestions={(value) => value.trim().length > 2}
renderSuggestion={(item: ISuggestion) => {
<div className={classes.suggestionsList}>{`${item.text} (${item.id})`}</div>;
}}
renderInputComponent={(ip: InputProps<ISuggestion>) => {
const params: InputProps<ISuggestion> = {
...ip,
onChange: props.onChange
};
return renderInput(params);
}}
inputProps={inputProps}
/>
);
};
export { MySearchBox };
Custom Input Component:
import React, { FC, ChangeEvent } from 'react';
import SearchIcon from '#material-ui/icons/Search';
import useStyles from './searchInput.styles';
import { Input } from '#material-ui/core';
type SearchInputBoxProps = {
loading?: boolean;
searchIconName: string;
minWidth?: number;
placeHolder?: string;
onChange?: (e: ChangeEvent, params: { newValue: string; method: string }) => void;
};
const SearchInputBox: FC<SearchInputBoxProps> = (props) => {
const classes = useStyles();
return (
<div className={classes.root}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<Input
placeholder={props.placeHolder}
classes={{
root: classes.inputContainer,
input: classes.inputBox
}}
inputProps={{ 'aria-label': 'search' }}
onChange={(e) => {
const params: { newValue: string; method: string } = {
newValue: e.target.value,
method: 'type'
};
console.log(`e: ${JSON.stringify(params)}`);
if (props.onChange) props.onChange(e, params);
}}
//onMouseOut={(e) => alert(e.currentTarget.innerText)}
/>
</div>
);
};
export { SearchInputBox };
Render Function:
const renderInput = (inputProps: InputProps<ISuggestion>): React.ReactNode => {
return <SearchInputBox loading={false} onChange={inputProps.onChange} placeHolder={inputProps.placeholder} searchIconName={'search'} {...inputProps.others} />;
};
I had same problem.
I just had to cast HTMLElement into HTMLInputElement
Like below
const target = e.currentTarget as HTMLInputElement
Then, it works fine when you do
target.value
I can't get my component to show my autosuggestions.
It is observed in the console that my data is available and I sent it to this component using the suggestions prop, using Material UI AutoComplete component feature here I am trying to set my options, and these are changing as I type as it's handled in a parent component, but setting the values does not seem to reflect nor bring up my suggestions. I am very confused. my code is below.
import React, { FunctionComponent, FormEvent, ChangeEvent } from "react";
import { Grid, TextField, Typography } from "#material-ui/core";
import { CreateProjectModel, JobModel } from "~/Models/Projects";
import ErrorModel from "~/Models/ErrorModel";
import Autocomplete from "#material-ui/lab/Autocomplete";
type CreateProjectFormProps = {
model: CreateProjectModel;
errors: ErrorModel<CreateProjectModel>;
onChange: (changes: Partial<CreateProjectModel>) => void;
onSubmit?: () => Promise<void>;
suggestions: JobModel[];
};
const CreateProjectForm: FunctionComponent<CreateProjectFormProps> = ({
model,
errors,
onChange,
onSubmit,
suggestions,
}) => {
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState<JobModel[]>([]);
const loading = open && options.length === 0;
const [inputValue, setInputValue] = React.useState('');
React.useEffect(() => {
let active = true;
if (!loading) {
return undefined;
}
(async () => {
if (active) {
setOptions(suggestions);
}
})();
return () => {
active = false;
};
}, [loading]);
React.useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
const submit = async (event: FormEvent) => {
event.preventDefault();
event.stopPropagation();
await onSubmit();
};
const change = (name: string) => (event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
onChange({
[name]: event.target.value,
});
};
const getFieldProps = (id: string, label: string) => {
return {
id,
label,
helperText: errors[id],
error: Boolean(errors[id]),
value: model[id],
onChange: change(id),
};
};
return (
<Autocomplete
{...getFieldProps}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={(option) => option.id}
options={options}
loading={loading}
autoComplete
includeInputInList
renderInput={(params) => (
<TextField
{...getFieldProps("jobNumber", "Job number")}
required
fullWidth
autoFocus
margin="normal"
/>
)}
renderOption={(option) => {
return (
<Grid container alignItems="center">
<Grid item xs>
{options.map((part, index) => (
<span key={index}>
{part.id}
</span>
))}
<Typography variant="body2" color="textSecondary">
{option.name}
</Typography>
</Grid>
</Grid>
);
}}
/>
);
};
export default CreateProjectForm;
Example of my data in suggestions look like this:
[{"id":"BR00001","name":"Aircrew - Standby at home base"},{"id":"BR00695","name":"National Waste"},{"id":"BR00777B","name":"Airly Monitor Site 2018"},{"id":"BR00852A","name":"Cracow Mine"},{"id":"BR00972","name":"Toowoomba Updated"},{"id":"BR01023A","name":"TMRGT Mackay Bee Creek"},{"id":"BR01081","name":"Newman Pilot Job (WA)"},{"id":"BR01147","name":"Lake Vermont Monthly 2019"},{"id":"BR01158","name":"Callide Mine Monthly Survey 2019"},{"id":"BR01182","name":"Lake Vermont Quarterly 2019 April"}]
The problem in your code are the useEffects that you use.
In the below useEffect, you are actually setting the options to an empty array initially. That is because you autocomplete is not open and the effect runs on initial mount too. Also since you are setting options in another useEffect the only time your code is supposed to work is when loading state updates and you haven't opened the autocomplete dropdown.
The moment you close it even once, the state is updated back to empty and you won't see suggestions any longer.
React.useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
The solution is simple. You don't need to keep a local state for options but use the values coming in from props which is suggestions
You only need to keep a state for open
const CreateProjectForm: FunctionComponent<CreateProjectFormProps> = ({
model,
errors,
onChange,
onSubmit,
suggestions,
}) => {
const [open, setOpen] = React.useState(false);
const loading = open && suggestions.length === 0;
const [inputValue, setInputValue] = React.useState('');
const submit = async (event: FormEvent) => {
event.preventDefault();
event.stopPropagation();
await onSubmit();
};
const change = (name: string) => (event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
onChange({
[name]: event.target.value,
});
};
const getFieldProps = (id: string, label: string) => {
return {
id,
label,
helperText: errors[id],
error: Boolean(errors[id]),
value: model[id],
onChange: change(id),
};
};
return (
<Autocomplete
{...getFieldProps}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={(option) => option.id}
options={suggestions}
loading={loading}
autoComplete
includeInputInList
renderInput={(params) => (
<TextField
{...getFieldProps("jobNumber", "Job number")}
required
fullWidth
autoFocus
margin="normal"
/>
)}
renderOption={(option) => {
return (
<Grid container alignItems="center">
<Grid item xs>
{options.map((part, index) => (
<span key={index}>
{part.id}
</span>
))}
<Typography variant="body2" color="textSecondary">
{option.name}
</Typography>
</Grid>
</Grid>
);
}}
/>
);
};
export default CreateProjectForm;
i noticed a few issues with your code, getFieldProps is being called without the id or name params which cause the page to not load. More importantly, you should consider following the autocomplete docs when passing and using props to it. for example:
renderInput={(params) => <TextField {...params} label="Controllable" variant="outlined" />}
i asked a few questions, pls let me know when you can get those answers so i may address all the issues that may come up.
Q1. should the user input provide relevant matches from the name property in your suggestions or just the id? for ex. if i type "lake", do you want to show BRO1182, Lake Vermont Quarterly 2019 April as a match?
Q2. how did you want to address the error case? i see you have a error model, but unsure how you wish to use it to style the autocomplete when an error occurs
Q3. are we missing a submit button? i see the onSubmit function but it's not used in our code.
Q4. is there a particular reason why you need the open and loading states?
below is what i attempted so far to show related matches from user input
import React, { FunctionComponent, FormEvent, ChangeEvent } from "react";
import { Grid, TextField, Typography } from "#material-ui/core";
import { CreateProjectModel, JobModel } from "~/Models/Projects";
import ErrorModel from "~/Models/ErrorModel";
import Autocomplete from "#material-ui/lab/Autocomplete";
type CreateProjectFormProps = {
model: CreateProjectModel;
errors: ErrorModel<CreateProjectModel>;
onChange: (changes: Partial<CreateProjectModel>) => void;
onSubmit?: () => Promise<void>;
suggestions: JobModel[];
};
const CreateProjectForm: FunctionComponent<CreateProjectFormProps> = ({
model,
errors,
// mock function for testing
// consider a better name like selectChangeHandler
onChange = val => console.log(val),
// consider a better name like submitJobFormHandler
onSubmit,
suggestions: options = [
{ id: "BR00001", name: "Aircrew - Standby at home base" },
{ id: "BR00695", name: "National Waste" },
{ id: "BR00777B", name: "Airly Monitor Site 2018" },
{ id: "BR00852A", name: "Cracow Mine" },
{ id: "BR00972", name: "Toowoomba Updated" },
{ id: "BR01023A", name: "TMRGT Mackay Bee Creek" },
{ id: "BR01081", name: "Newman Pilot Job (WA)" },
{ id: "BR01147", name: "Lake Vermont Monthly 2019" },
{ id: "BR01158", name: "Callide Mine Monthly Survey 2019" },
{ id: "BR01182", name: "Lake Vermont Quarterly 2019 April" }
]
}) => {
const [value, setValue] = React.useState<JobModel>({});
const loading = open && options.length === 0;
// this pc of code is not used, why?
const submit = async (event: FormEvent) => {
event.preventDefault();
event.stopPropagation();
await onSubmit();
};
const handleChange = (_: any, value: JobModel | null) => {
setValue(value);
onChange({
[value.name]: value.id
});
};
// consider passing in props instead
const getFieldProps = (id: string, label: string) => {
return {
id,
label,
// not sure what this is
helperText: errors[id],
// not sure what this is
error: Boolean(errors[id]),
value: model[id],
onChange: change(id)
};
};
return (
<Autocomplete
id="placeholder-autocomplete-input-id"
// for selection, use value see docs for more detail
value={value}
onChange={handleChange}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={option => option.id}
options={options}
loading={loading}
autoComplete
includeInputInList
renderInput={params => (
// spreading the params here will transfer native input attributes from autocomplete
<TextField
{...params}
label="placeholder"
required
fullWidth
autoFocus
margin="normal"
/>
)}
renderOption={option => (
<Grid container alignItems="center">
<Grid item xs>
<span key={option}>{option.id}</span>
<Typography variant="body2" color="textSecondary">
{option.name}
</Typography>
</Grid>
</Grid>
)}
/>
);
};
export default CreateProjectForm;
and you can see the code running in my codesandbox by clicking the button below
If I understand your code and issue right, you want -
React.useEffect(() => {
let active = true;
if (!loading) {
return undefined;
}
(async () => {
if (active) {
setOptions(suggestions);
}
})();
return () => {
active = false;
};
}, [loading]);
to run each time and update options, but the thing is, [loading] dependency setted like
const loading = open && suggestions.length === 0;
and not gonna trigger changes.
Consider doing it like so -
const loading = useLoading({open, suggestions})
const useLoading = ({open, suggestions}) => open && suggestions.length === 0;
Novice.
I have a class Address which I ultimately want to split into a presentational component and container. It all works as is but when I move this particular function outside the render function from initially within the actual async.select form field -
onSuburbChange = (value) => {
this.setState({ selectedSuburb: value }, () => {
input.onChange(value)
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}
...I find I am getting hit with a number of errors based on the the fact that they are unreferenced.
The error I get is
address.jsx:56 Uncaught ReferenceError: input is not defined
If I comment this line out I get the same type of error on updatePostcodeValue.
Here is the entire address file. As you can see it would be good to move the presentational section in render off to another file but I need to move all the functions to the outside of the render function.
NOTE: I have commented out where the function orginal sat so anybody who has a crack at this question knows where it was and also where I intended to move it...
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { Field, change } from 'redux-form'
import { Col, Panel, Row } from 'react-bootstrap'
import Select from 'react-select'
import { getSuburbs } from './actions'
import FormField from '../formComponents/formField'
import TextField from '../formComponents/textField'
import StaticText from '../formComponents/staticText'
import { CLIENT_FORM_NAME } from '../clients/client/client'
export class Address extends Component {
static contextTypes = {
_reduxForm: PropTypes.object.isRequired,
}
constructor(props, context) {
super(props, context)
this.state = {
selectedSuburb: null,
}
}
// Manage Select for new data request - for suburbs.
handleSuburbSearch = (query) => {
console.group('handleSuburbSearch')
console.log('query', query)
const { addressData } = this.props
console.log('addressData', addressData)
const companyStateId = addressData.companyStateId
console.log('companyStateId', companyStateId)
if (!query || query.trim().length < 2) {
console.log('no query; bailing!')
console.groupEnd()
return Promise.resolve({ options: [] })
}
const queryString = {
query: query,
companyStateId: companyStateId,
}
console.log('queryString', queryString)
return getSuburbs(queryString)
.then(data => {
console.log('Suburbs returned!', data)
console.groupEnd()
return { options: data }
})
}
//I HAVE MOVED IT TO HERE....
onSuburbChange = (value) => {
this.setState({ selectedSuburb: value }, () => {
input.onChange(value)
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}
render() {
const { addressData, updatePostcodeValue } = this.props
const { value } = this.state
const sectionPrefix = this.context._reduxForm.sectionPrefix
return (
<Panel header={<h3>Client - Address Details</h3>}>
<Row>
<Field component={TextField}
name="address1"
id="address1"
type="text"
label="Address Line 1"
placeholder="Enter street 1st line..."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
<Field component={TextField}
name="address2"
id="address2"
type="text"
label="Address Line 2"
placeholder="Enter street 2nd line..."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
</Row>
<Row>
<Field
component={props => {
const { input, id, placeholder, type } = props
const { fieldCols, labelCols, controlCols, label, inputClass } = props
// just the props we want the inner Typeahead textbox to have
const { name, onChange } = input
const onStateChange = (state) => {
console.log('onStateChange', state)
onChange(state)
}
return (
<FormField
id={id}
label={label}
fieldCols={fieldCols}
labelCols={labelCols}
controlCols={controlCols}
inputClass={inputClass}
>
<Select
name={name}
onChange={onStateChange}
placeholder="Select state"
valueKey="id"
options={addressData.states}
labelKey="stateLabel"
optionRenderer={option => `${option.stateShortName} (${option.stateName})`}
value={input.value}
selectValue={Array.isArray(input.value) ? input.value : undefined}
/>
</FormField>
)
}}
name="state"
id="state"
label="State."
fieldCols={6}
labelCols={3}
controlCols={6}
/>
</Row>
<Row>
<Field
component={props => {
const { input, id, placeholder, type } = props
const { fieldCols, labelCols, controlCols, label, inputClass } = props
const { name, value, onChange, onBlur, onFocus } = input
const inputProps = {
name,
value,
onChange,
onBlur,
onFocus,
}
{/*onSuburbChange = (value) => {
this.setState({ selectedSuburb: value }, () => {
input.onChange(value)
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}*/}
return (
<FormField
id={id}
label={label}
fieldCols={fieldCols}
labelCols={labelCols}
controlCols={controlCols}
inputClass={inputClass}
>
<Select.Async
{...inputProps}
onChange={this.onSuburbChange}
valueKey="id"
labelKey="suburbName"
loadOptions={this.handleSuburbSearch}
backspaceRemoves={true}
/>
</FormField>
)
}}
name="suburb"
id="AddressLocation"
label="Suburb."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
</Row>
<Row>
<Field component={StaticText}
name="postcode"
id="postcode"
label="Postcode."
fieldCols={6}
labelCols={3}
controlCols={9}
/>
</Row>
</Panel>
)
}
}
const AddressContainer = connect(state => ({
addressData: state.addressData,
}), dispatch => ({
updatePostcodeValue: (postcode, sectionPrefix) => dispatch(change(CLIENT_FORM_NAME, `${sectionPrefix ? (sectionPrefix + '.') : ''}postcode`, postcode))
}))(Address)
export default AddressContainer
How do I structure the onSuburbChange so that it can sit outside the render function, update the onChange value and also update the Postcode etc?
well, if you look at the method, you'll see that... well, input is undefined in that scope.
onSuburbChange = (value) => { // <-- scope starts here
this.setState({ selectedSuburb: value }, () => {
input.onChange(value) // <-- input used here
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}
assuming Select.Async is a "magic" blackbox Component that you don't have access to/are able to change, and the only parameter you get back from it in the callback is the new value, your best bet is a ref on the input.
<Field ref={(input) => this.input = input } ... />
and then change it to this.input instead of just input
I think you could also partially apply it (it's late any I'm not thinking straight) - it would look like
onSuburbChange = (input, value) => {
this.setState({ selectedSuburb: value }, () => {
input.onChange(value)
updatePostcodeValue(value ? value.postcode : null, sectionPrefix)
})
}
--
const mOnChange = onSuburbChange.bind(null, input) while input is in scope.
updatePostcodeValue can be referenced from props in the callback - and you've already taken care of ensuring it has the correct scope by using ES6 arrow function notation. Just destructure it out of props just like you did in render at the top of the callback.
also, unrelated, but you REALLY oughta break out those component props into another file or function...