Angular 1.X ng-options equivalent using React - angularjs

Angular 1.X has ng-options for the choices in a select dropdown, each item being an object. In plain HTML, the value of an option can only be a string. When you select one option in Angular, you can see the actual selected object while in plain html, you can only get that string value.
How do you do the equivalent of that in React (+Redux)?

I came up with a solution that does not use JSON.stringify / parse for the value of the select React element nor does it use the index of the array of choice objects as the value.
The example is a simple select dropdown for a person's gender -- either male or female. Each of those choices is an actual object with id, text, and value properties. Here is the code:
MySelect component
import React, { Component } from 'react';
class MySelect extends Component {
onGenderChange = (event) => {
// Add the second argument -- the data -- and pass it along
// to parent component's onChange function
const data = { options: this.props.options };
this.props.onGenderChange(event, data);
}
render() {
const { options, selectedOption } = this.props;
// Goes through the array of option objects and create an <option> element for each
const selectOptions = options.map(
option => <option key={option.id} value={option.value}>{option.text}</option>
);
// Note that if the selectedOption is not given (i.e. is null),
// we assign a default value being the first option provided
return (
<select
value={(selectedOption && selectedOption.value) || options[0].value}
onChange={this.onGenderChange}
>
{selectOptions}
</select>
);
}
}
App component that uses MySelect
import _ from 'lodash';
import React, { Component } from 'react';
class App extends Component {
state = {
selected: null
}
onGenderChange = (event, data) => {
// The value of the selected option
console.log(event.target.value);
// The object for the selected option
const selectedOption = _.find(data.options, { value: parseInt(event.target.value, 10) });
console.log(selectedOption);
this.setState({
selected: selectedOption
});
}
render() {
const options = [
{
id: 1,
text: 'male',
value: 123456
},
{
id: 2,
text: 'female',
value: 654321
}
];
return (
<div>
<label>Select a Gender:</label>
<MySelect
options={options}
selectedOption={this.state.selected}
onGenderChange={this.onGenderChange}
/>
</div>
);
}
}
Lodash is used to look up the choice object in the array of choice objects inside the onGenderChange function in the App component. Note that the onChange passed to the MySelect component requires two arguments -- an extra data argument is added in order to be able to access the choice objects ("options"). With that, you can just set the state (or call an action creator if using Redux) with the choice object for the selected option.

I run into this same situation while migrating a angular 1 app to react. And I felt it is a much needed feature that I couldn't find so here I leave my implementation of NgOption in react using bootstrap (you can can change that to whatever you are using) for anybody that's missing the goo'old angular 1:
import React from 'react';
import { Form, InputGroup } from 'react-bootstrap';
interface props<T, U>{
options: Array<T>,
selected?: U,
onSelect: (value: T) => any,
as: (value: T) => string,
trackBy: (value: T) => U,
disabled?: boolean
}
type state = {}
export default class NgOptions extends React.Component<props<any, any>, state> {
componentDidMount() {
if (this.props.selected) {
this.props.onSelect(this.props.options.find((o) => this.props.trackBy(o)===this.props.selected))
}
}
public render() {
return ( <InputGroup>
<Form.Control as="select"
disabled={this.props.disabled}
onChange={(e) => this.props.onSelect(this.props.options.find((o) => this.props.trackBy(o)===e.target.value))}>
{this.props.options.map( option =>
<option key={this.props.trackBy(option)}
selected={this.props.selected===this.props.trackBy(option)}
value={this.props.trackBy(option)}>{this.props.as(option)}</option>
)}
</Form.Control>
</InputGroup>
)
}
}

Related

How to pass a generic to a React component and use that generic within the component

I'm building an input component that takes a displayName that is a string and a value that should be a generic. It also takes a function as a prop that needs to fire and handle that generic value when the input is changed.
So, let's say MyInterfaceXYZ holds the displayName and value like so:
interface MyInterfaceXYZ<T> {
displayName: string
value: T
}
We then want this generic value to be used in our main props interface that wraps our first interface... Like the one below (but my syntax is wrong I think):
interface MyComponentProps<T> {
myFunctionThatTakesMyGeneric: (value: T) => void
options: MyInterfaceXYZ<T>[]
}
We then want to pass use this as our props in our react component - where we will receive a generic from an onChange listener.
// The syntax on the line below is totally invalid
const InputSelect: React.FC<MyComponentProps<T>> = ({
myFunctionThatTakesMyGeneric,
options,
}) => {
// `<T extends MyInterfaceXYZ>` requires 1 argument
// that I don't know how to pass here
function onChangeHandler<T extends MyInterfaceXYZ>(option: T) {
myFunctionThatTakesMyGeneric(option.value)
}
return (
<>
<myElement onChange={onChangeHandler}>
Blah blah!
</myElement>
</>
)
How can I modify the syntax so the above code is valid?
You don't actually need to make the onChangeHandler generic.
import React from 'react'
interface MyInterfaceXYZ<T> {
displayName: string
value: T
}
interface MyComponentProps<T> {
myFunctionThatTakesMyGeneric: (value: T) => void
options: MyInterfaceXYZ<T>[]
}
// The syntax on the line below is totally invalid
function InputSelect<T>({
myFunctionThatTakesMyGeneric,
options,
} : MyComponentProps<T>) {
// `<T extends MyInterfaceXYZ>` requires 1 argument
// that I don't know how to pass here
const onChangeHandler = (option: MyInterfaceXYZ<T>) => {
myFunctionThatTakesMyGeneric(option.value)
}
return (
<>
<div onChange={() => onChangeHandler(options[0])}>
Blah blah!
</div>
</>
)
}
<>
<InputSelect myFunctionThatTakesMyGeneric={(a:string) => {console.log(a);}} options={[{displayName: 'string', value:'1'}]} />
<InputSelect myFunctionThatTakesMyGeneric={(a:number) => {console.log(a);}} options={[{displayName: 'number', value:1}]} />
</>
Playground link
Also I changed the onChange to onChange={() => onChangeHandler(options[0])}, because you need to pass an option to the function. options[0] is probably not what you want.

Keeping state of variable mapped from props in functional react component after component redraw

Recently I started learning react and I decided to use in my project functional components instead of class-based. I am facing an issue with keeping state on one of my components.
This is generic form component that accepts array of elements in order to draw all of necessary fields in form. On submit it returns "model" with values coming from input fields.
Everything working fine until I added logic for conditionally enabling or disabling "Submit" button when not all required fields are set. This logic is fired either on component mount using useEffect hook or after every input in form input. After re-render of the component (e.g. conditions for enabling button are not met, so button becomes disabled), component function is fired again and my logic for creating new mutable object from passed props started again, so I am finished with empty object.
I did sort of workaround to make a reference of that mutated object outside of scope of component function, but i dont feel comfortable with it. I also dont want to use Redux for that simple sort of state.
Here is the code (I am using Type Script):
//component interfaces:
export enum FieldType {
Normal = "normal",
Password = "password",
Email = "email"
}
export interface FormField {
label: string;
displayLabel: string;
type: FieldType;
required: boolean;
}
export interface FormModel {
model: {
field: FormField;
value: string | null;
}[]
}
export interface IForm {
title: string;
labels: FormField[];
actionTitle: string;
onSubmit: (model: FormModel) => void;
}
let _formState: any = null;
export function Form(props: IForm) {
let mutableFormModel = props.labels.map((field) => { return { field: field, value: null as any } });
//_formState keeps reference outside of react function scope. After coponent redraw state inside this function is lost, but is still maintained outside
if (_formState) {
mutableFormModel = _formState;
} else {
_formState = mutableFormModel;
}
const [formModel, setFormModel] = useState(mutableFormModel);
const [buttonEnabled, setButtonEnabled] = useState(false);
function requiredFieldsCheck(formModel: any): boolean {
let allRequiredSet = true;
formModel.model.forEach((field: { field: { required: any; }; value: string | null; }) => {
if (field.field.required && (field.value === null || field.value === '')) {
allRequiredSet = false;
}
})
return allRequiredSet;
}
function handleChange(field: FormField, value: string) {
let elem = mutableFormModel.find(el => el.field.label === field.label);
if (elem) {
value !== '' ? elem.value = value as any : elem.value = null;
}
let submitEnabled = requiredFieldsCheck({ model: mutableFormModel });
setFormModel(mutableFormModel);
setButtonEnabled(submitEnabled);
}
useEffect(() => {
setButtonEnabled(requiredFieldsCheck({ model: mutableFormModel }));
}, [mutableFormModel]);
function onSubmit(event: { preventDefault: () => void; }) {
event.preventDefault();
props.onSubmit({ model: formModel })
}
return (
<FormStyle>
<div className="form-container">
<h2 className="form-header">{props.title}</h2>
<form className="form-content">
<div className="form-group">
{props.labels.map((field) => {
return (
<div className="form-field" key={field.label}>
<label>{field.displayLabel}</label>
{ field.type === FieldType.Password ?
<input type="password" onChange={(e) => handleChange(field, e.target.value)}></input> :
<input type="text" onChange={(e) => handleChange(field, e.target.value)}></input>
}
</div>
)
})}
</div>
</form>
{buttonEnabled ?
<button className={`form-action btn btn--active`} onClick={onSubmit}> {props.actionTitle} </button> :
<button disabled className={`form-action btn btn--disabled`} onClick={onSubmit}> {props.actionTitle} </button>}
</div>
</FormStyle >
);
}
So there is quite a lot going on with your state here.
Instead of using a state variable to check if your button should be disabled or not, you could just add something render-time, instead of calculating a local state everytime you type something in your form.
So you could try something like:
<button disabled={!requiredFieldsCheck({ model: formModel })}>Click me</button>
or if you want to make it a bit cleaner:
const buttonDisabled = !requiredFieldsCheck({model: formModel});
...
return <button disabled={buttonDisabled}>Click me</button>
If you want some kind of "caching" without bathering with useEffect and state, you can also try useMemo, which will only change your calculated value whenever your listeners (in your case the formModel) have changes.
const buttonDisabled = useMemo(() => {
return !requiredFieldsCheck({model: formModel});
}, [formModel]);
In order to keep value in that particular case, I've just used useRef hook. It can be used for any data, not only DOM related. But thanks for all inputs, I've learned a lot.

Material UI - Autocomplete with React Hook Form

I am building a form using Material UI's Autocomplete feature and using recoil for state management. I am also using react-hook-form. I am needing the following criteria met:
Need typeahead that allows first letter typed to return a list of options from an API to show in Autocomplete. Each letter will return a different list of options to select from.
Need to also allow freeSolo so that user can enter a value manually
Needs to be required and follow a pattern to validate the form.
I am using react-hook-form's <Controller> to control the input and allow for features like validation, displaying error messages in the helper text, etc.
The Problems: I am having issues with the typeahead filtering out options based on what I type, along with allowing freeSolo. As I type a new value, the list of options do not filter. The popup just stays open. I also need to validate on change of input for the pattern validation. I have tried with the following example with onInputChange to make use of react-hook-form's useForm and setValue to manually set the value of the field and validate the form. ({shouldValidate: true}). The below example is a custom, reusable component I created for Autocomplete, as well as using that custom component in other parent components. I hope I included as much details as possilbe, but if not, please let me know if you need anything more. Any assistance would be very appreciative!
Parent Component:
const setTrainType = useSetRecoilState(TrainType)
// Trains would return a list of trains based on letter of train type that was passed from input
const trainsList = useRecoilValue(Trains)
const trainOptions = useMemo(() => trainsList.map(trainIDFormatted), [
trainsList,
])
const handleInputChange = useCallback(
(_e: unknown, option: string, reason: string) => {
const capitalized =
option === capitalize(option) ? option : capitalize(option)
setValue('trainID', capitalized, {shouldValidate: true})
if (['input', 'reset'].includes(reason) && capitalized !== '') {
setTrainType(capitalized.charAt(0))
} else {
setTrainType(undefined)
}
},
[setTrainType, setValue],
)
<Autocomplete
autoSelect
freeSolo
disabled={disabled}
helperText=" "
label="Select a train"
name="trainID"
options={trainOptions}
rules={{
pattern: {
message: 'Must match train ID pattern',
value: /^(?:[A-Z]-?[A-Z ]{6}-?[0-9 ]-?[0-9 ]{2}[A-Z ])?$/,
},
required: 'Train is required',
}}
onInputChange={handleInputChange}
/>
Custom autocomplete component:
import {
AutocompleteProps,
Autocomplete as MuiAutocomplete,
} from '#material-ui/lab'
import {get} from 'lodash'
import React, {ReactNode, useCallback} from 'react'
import {
Controller,
ControllerProps,
FieldError,
useFormContext,
} from 'react-hook-form'
import {useRenderInput} from './hooks'
interface Props
extends Pick<ControllerProps<'select'>, 'rules'>,
Omit<
AutocompleteProps<string, false, false, true>,
'error' | 'onChange' | 'required' | 'renderInput'
> {
helperText?: ReactNode
label?: string
name: string
}
/**
* Render controlled autocomplete. Use react-form-hook's FormProvider.
* #param props Component properties
* #param props.helperText Default helper text for error
* #param props.label Input label
* #param props.name Name identifier for react-hook-form
* #param props.required If true then item is required
* #param props.rules Select rules
* #return React component
*/
export const Autocomplete = ({
helperText,
label,
name,
rules,
...props
}: Props) => {
// eslint-disable-next-line #typescript-eslint/unbound-method
const {control, errors, watch} = useFormContext()
const error: FieldError | undefined = get(errors, name)
const required = get(rules, 'required') !== undefined
const value = watch(name)
const renderAutocompleteInput = useRenderInput({
error: error !== undefined,
helperText: get(error, 'message', helperText),
label,
required,
})
const handleOnChange = useCallback(
(_e: unknown, option: string | null) => option,
[],
)
const renderAutocomplete = useCallback(
params => (
<MuiAutocomplete
{...props}
{...params}
renderInput={renderAutocompleteInput}
onChange={handleOnChange}
/>
),
[handleOnChange, props, renderAutocompleteInput],
)
return (
<Controller
control={control}
defaultValue={value ?? ''}
name={name}
render={renderAutocomplete}
rules={rules}
/>
)
}
What it looks like:

How to write Test cases for useEffect Hook in React using Jest & Enzyme?

import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
const InputComponent = ({ item, data }) => {
const [value, setValue] = useState('');
// Binding values for Edit based on unique Key
useEffect(() => {
if (data && data[item.field] && data[item.field] !== 'undefined') {
setValue(data[item.field]);
}
}, [data,item.field]);
//On change setting state
const setState = e => {
setValue(e.target.value);
};
return (
<div className='Input-Containter' data-test='InputBox-tbl-evt'>
<input
data-test='input-ele'
type='text'
value={value}
onChange={e => setState(e)}
/>
</div>
);
};
InputComponent.propTypes = {
item: PropTypes.object,
data: PropTypes.object
};
InputComponent.defaultProps = {
data: {
id: '1',
name: 'd'
},
item:{
field: 'id',
}
};
export default InputComponent;
Can someone help me How to test for setValue() in useEffect
-> Updated complete code for this Component
-> Component will take some data for binding values into input element & in the same
component we can edit values in it as-well.
First, let's take closer look onto useEffect section. What does it mean?
if any of prop is changed
and combination of new values are given some meaningful value(not undefined)
we initialize input's value based on those props even if we have to override custom value
How to test that? Changing prop(s) and validating input's value.
Based on that we may have up to 3(changed only first prop, only second or both) * 2 (if result is undefined or not) * 2 (if there has been already custom value provided and stored in useState or not) = 12. But I believe exhaustive testing is not a good way. So I'd put most checks in single test case:
it('re-init value for nested input after props changes', () => {
const wrapper = mount(<InputComponent />);
function getInput(wrapper) {
return wrapper.find("input").prop("value");
}
expect(getInput(wrapper).props("value")).toEqual("1"); // based on default props
getInput(wrapper).props().onChange({ target: {value: "initial"} }); // imitating manual change
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({data: {a: "some-a", b: "undefined"} });
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({ item: { field: "c" } }); // data[item.field] is undefined
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({ item: {field: "b" } }); // data[item.field] is "undefined"
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({ item: {field: "a" } }); // data[item.field] is meaningful
expect(getInput(wrapper).props("value")).toEqual("some-a");
});
As for getValue helper - it's needed cause we cannot just memoize input element itself like:
const wrapper = mount(...);
const input = wrapper.find("input");
...
expect(input.prop("value")).toEqual();
...
expect(input.prop("value")).toEqual();
Details can be found in Enzyme's docs. Or just know we need to re-run find after any update.
Also beware Enzyme's setProps does not replace current props but update them by merging(as well as React's setState does with state).

How to Manipulate Dropdown placeholder, onFocus?

Am new to ReactJS. I need to make the "placeholder" which is set to "State" initially to Empty/Null when onClicked or onFocus and then when it's not focused on, it goes back to "State" again. Can someone help me with this, am very new to react so any help will be appreciated.
import React from "react";
import { render } from "react-dom";
import { Container, Button, Modal, Dropdown } from "semantic-ui-react";
const stateOptions = [
{ key: "AL", value: "AL", text: "Alabama" },
{ key: "NY", value: "NY", text: "New York" }
];
const App = () => (
<Dropdown
placeholder="State"
fluid
multiple
search
selection
options={stateOptions}
/>
);
render(<App />, document.getElementById("root"));
From React's perspective, placeholder is a state that needs to be changed according to user's actions (onClick, onBlur)
So create a state to hold placeholder value that need to change.
There are two ways (since v16.8.0 with the introduction of React Hooks).
Using Class Component
class DropDown extends React.Component {
defaultPlaceholderState = "State";
state = { placeholder: this.defaultPlaceholderState };
clearPlaceholder = () => this.setState({ placeholder: "" });
resetPlaceholder = () =>
this.setState({ placeholder: this.defaultPlaceholderState });
render() {
return (
<Dropdown
onClick={this.clearPlaceholder}
onFocus={this.clearPlaceholder}
onBlur={this.resetPlaceholder}
placeholder={this.state.placeholder}
fluid
multiple
search
selection
options={stateOptions}
/>
);
}
}
In the code above, placeholder declared as a state with default value set to this.defaultPlaceholderState.
When a user clicks on the dropdown, onClick clears the placeholder value by setting it to an empty string. Same for onFocus when the Dropdown is on focus.
When a user clicks outside (onBlur), resetPlaceHolder sets the placeholder value to the default this.defaultPlaceholderState.
Using Function Component with useState hook
React v16.8.0 introduces Hooks, which enables Function Components (not a Functional Component, as it refers to Functional Programming) to hold states.
You can use React.useState hook to hold placeholder value.
const DropDownUsingHook = () => {
const defaultPlaceholderState = "State";
const [placeholder, setPlaceholder] = React.useState(defaultPlaceholderState);
const clearPlaceholder = () => setPlaceholder("");
const resetPlaceholder = () => setPlaceholder(defaultPlaceholderState);
return (
<Dropdown
onClick={clearPlaceholder}
onFocus={clearPlaceholder}
onBlur={resetPlaceholder}
placeholder={placeholder}
fluid
multiple
search
selection
options={stateOptions}
/>
);
};
⚠ Note: Unlike the Class version, clearPlaceholder, resetPlaceholder methods and placeholder state don't use this. prefix.
The implementation is similar but you use useState hook to declare the state and the setter (setPlaceholder).
Refer to the Hooks documentation, Using State Hook for more info.
You can play around with the working code on CodeSandbox.

Resources