Conditional Props in React (ACCEPT only 1 of 2 props) - reactjs

I had a search bar component in react and I wanted to have either an onSubmit prop to it or an onChange prop, but not both of them together. How do I implement it in the best way possible?
I tried using if-else statements but the code doesn't look elegant to me.
class QueryBar extends PureComponent {
render() {
const { placeholder, leftIcon, onSubmit, onChange, width } = this.props;
return (
<form
style={{ width }}
onSubmit={e => {
e.preventDefault();
onSubmit(e.target[0].value);
}}
>
<InputGroup
placeholder={placeholder}
width={width}
leftIcon="search"
rightElement={
<Button
type="submit"
icon={leftIcon}
minimal={true}
intent={Intent.PRIMARY}
/>
}
/>
</form>
);
}
}
QueryBar.propTypes = {
width: PropTypes.number,
placeholder: PropTypes.string,
leftIcon: PropTypes.oneOfType(['string', 'element']),
onSubmit: PropTypes.func
};
QueryBar.defaultProps = {
placeholder: 'Search...',
leftIcon: 'arrow-right',
width: 360
};
export default QueryBar;

You can use the ternary operator (?) instead of if-else.
const { placeholder, leftIcon, onSubmit, onChange, width } = this.props;
const handleSubmit = onSubmit ? onSubmit : onChange;
And use the same function later
<form
style={{ width }}
onSubmit={e => {
e.preventDefault();
handleSubmit(e.target[0].value);
}}
>

Related

Storybook custom onChange

I'm having trouble updating knobs when adding custom onchange
export default {
component: Input,
decorators: [
(Story) => <div style={{ textAlign: 'center', width: '70%' }}><Story /></div>,
],
title: 'components/Input',
} as ComponentMeta<typeof Input>;
const Template: ComponentStory<typeof Input> = (args) => {
const [value, setValue] = React.useState(args.value);
return <Input
{...args}
onChange={e => { setValue(e.target.value); args.onChange(e); }}
onClear={() => { setValue(''); args.onClear(); }}
value={value}
/>;
};
After typing a value in knobs onchange the input value does not update and vice versa after typing something in input the value in knobs does not update

React testing library - render is not showing all of the children

Trying to write a test for my react-hook-form just so I can check if it has rendered properly. At the moment I'm failing at the first hurdle and can't get my form to render its children:
const props = {
request: true,
title: "testing title",
commentPlaceholder: "Placeholder",
tip: "helpful tip",
submitButtonText: "submit",
onSubmit: jest.fn(),
recipientName: "User one",
activeStep: 2,
};
it('Should render the CommentForm as if it was in the request flow', async () => {
const { getByText, getByTestId, debug } = render(
<CommentForm {...props} />
);
console.log(debug());
});
The console log outputs the below:
<body>
<div>
<form />
</div>
</body>
Where as the component I'm testing has lots of children in it to create the form.
I've pinned pointed it to the wrapper that I am using in the component.
<StyledForm onSubmit={onSubmit} schema={CommentSchema} onChange={handleChange}>
If I change this to form then it renders all my children. This is a styled component that extends this method below:
const Form = ({ className, onSubmit, schema, defaultValues, children, mode, onChange, style }) => {
const methods = useForm({
defaultValues,
validationSchema: schema,
validateCriteriaMode: 'all',
mode: mode || 'onSubmit',
});
const { handleSubmit, watch } = methods;
const values = watch();
useEffect(() => {
onChange && onChange(values);
}, [onChange, values]);
return (
<FormContext {...methods}>
<form style={style} className={className} onSubmit={handleSubmit(onSubmit)}>
{children}
</form>
</FormContext>
);
};
I know that this is causing the problem but I can't understand why it wouldn't render the components. This is also the reason why when I change the StyledForm to a normal form element it works.
** UPDATE **
Now found out that it seems to be because I'm extending the styledComponent like so:
export const StyledForm = styled(Form)`
width: 100%;
margin: 0 auto 50px;
`;
jest.mock('hoc/withForm', () => jest.fn(({ children }) => <form>{children}</form>));
This was the solution. I had to mock my HOC so that it returned a more basic element.

How to use my react component PlaceInput to achieve place autocomplete in the menu input box?

I have a PlaceInput component which support google place autocomplete.
class PlaceInput extends Component {
state = {
scriptLoaded: false
};
handleScriptLoaded = () => this.setState({ scriptLoaded: true });
render() {
const {
input,
width,
onSelect,
placeholder,
options,
meta: { touched, error }
} = this.props;
return (
<Form.Field error={touched && !!error} width={width}>
<Script
url="https://maps.googleapis.com/maps/api/js?key={my google api key}&libraries=places"
onLoad={this.handleScriptLoaded}
/>
{this.state.scriptLoaded &&
<PlacesAutocomplete
inputProps={{...input, placeholder}}
options={options}
onSelect={onSelect}
styles={styles}
/>}
{touched && error && <Label basic color='red'>{error}</Label>}
</Form.Field>
);
}
}
export default PlaceInput;
I also have a menu item which is an<Input> from semantic-ui-react. The frontend is like below:
The menu code is like below:
<Menu.Item>
<Input
icon='marker'
iconPosition='left'
action={{
icon: "search",
onClick: () => this.handleClick()}}
placeholder='My City'
/>
</Menu.Item>
How can I leverage the PlaceInput component to make menu <Input> box to achieve the place autocomplete? Thanks!
If you could share a working sample of your app (in e.g. codesandbox) I should be able to help you make your PlaceInput class work with the Menu.Input from semantic-ui-react.
Otherwise, you can test a fully working example of such integration with the code below, which is based off of the Getting Started code from react-places-autocomplete.
import React from "react";
import ReactDOM from "react-dom";
import PlacesAutocomplete, {
geocodeByAddress,
getLatLng
} from "react-places-autocomplete";
import { Input, Menu } from "semantic-ui-react";
const apiScript = document.createElement("script");
apiScript.src =
"https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places";
document.head.appendChild(apiScript);
const styleLink = document.createElement("link");
styleLink.rel = "stylesheet";
styleLink.href =
"https://cdn.jsdelivr.net/npm/semantic-ui/dist/semantic.min.css";
document.head.appendChild(styleLink);
class LocationSearchInput extends React.Component {
constructor(props) {
super(props);
this.state = { address: "" };
}
handleChange = address => {
this.setState({ address });
};
handleSelect = address => {
geocodeByAddress(address)
.then(results => getLatLng(results[0]))
.then(latLng => console.log("Success", latLng))
.catch(error => console.error("Error", error));
};
render() {
return (
<PlacesAutocomplete
value={this.state.address}
onChange={this.handleChange}
onSelect={this.handleSelect}
>
{({ getInputProps, suggestions, getSuggestionItemProps, loading }) => (
<div>
<Menu>
<Menu.Item>
<Input
icon="marker"
iconPosition="left"
placeholder="My City"
{...getInputProps({
placeholder: "Search Places ...",
className: "location-search-input"
})}
/>
</Menu.Item>
</Menu>
<div className="autocomplete-dropdown-container">
{loading && <div>Loading...</div>}
{suggestions.map(suggestion => {
const className = suggestion.active
? "suggestion-item--active"
: "suggestion-item";
// inline style for demonstration purpose
const style = suggestion.active
? { backgroundColor: "#fafafa", cursor: "pointer" }
: { backgroundColor: "#ffffff", cursor: "pointer" };
return (
<div
{...getSuggestionItemProps(suggestion, {
className,
style
})}
>
<span>{suggestion.description}</span>
</div>
);
})}
</div>
</div>
)}
</PlacesAutocomplete>
);
}
}
ReactDOM.render(<LocationSearchInput />, document.getElementById("root"));
Hope this helps!

How to test components wrapped in an antd Form?

I've been struggling with this for months, now. Although there's a lot of speculation on the correct way to test antd wrapped components, none of the suggestions worked for this particular component.
So, I have a component which is a modal with an antd form. In this form, I have a few fields: an input, a select and a tree select, nothing too fancy.
It's basically this:
class FormModal extends React.Component {
static propTypes = {
data: propTypes.object,
form: propTypes.object,
scopes: propTypes.array.isRequired,
clients: propTypes.array.isRequired,
treeData: propTypes.array.isRequired,
isEditing: propTypes.bool.isRequired,
isSaving: propTypes.bool.isRequired,
onCancel: propTypes.func.isRequired,
onSave: propTypes.func.isRequired,
onFilterTreeData: propTypes.func.isRequired,
visible: propTypes.bool.isRequired
}
static defaultProps = {
data: null,
form: {}
}
state = {
selectedScopes: [],
newScopes: [],
inputVisible: false,
inputValue: ''
};
componentDidMount() {
// do stuff
}
handleSave = () => {
// do stuff
}
handleSelectedScopesChange = (event) => {
// do stuff
}
updateTreeSelect = () => {
const { form } = this.props;
const { selectedScopes } = this.state;
form.setFieldsValue({
allowedScopes: selectedScopes
});
}
handleRemoveTag = (removedTag) => {
const selectedScopes = this.state.selectedScopes.filter(scope => scope !== removedTag);
const newScopes = this.state.newScopes.filter(scope => scope !== removedTag);
this.setState({ selectedScopes, newScopes }, this.updateTreeSelect);
}
showInput = () => {
this.setState({ inputVisible: true }, () => this.input.focus());
}
handleInputChange = (e) => {
const inputValue = e.target.value;
this.setState({ inputValue });
}
handleInputConfirm = () => {
const { newScopes, inputValue } = this.state;
let tags = newScopes;
if (inputValue && tags.indexOf(inputValue) === -1) {
tags = [inputValue, ...tags];
}
this.setState({
newScopes: tags,
inputVisible: false,
inputValue: '',
});
}
saveInputRef = input => this.input = input
renderTags = (scopeArrays) => {
const tags = scopeArrays.map(scopeArray =>
scopeArray.map((permition) => {
let scopeType = null;
if (permition.includes('read') || permition.includes('get')) scopeType = 'blue';
if (permition.includes('create') || permition.includes('write') || permition.includes('send')) scopeType = 'green';
if (permition.includes('update')) scopeType = 'gold';
if (permition.includes('delete')) scopeType = 'red';
return (
<Tag
key={permition}
color={scopeType || 'purple'}
style={{ margin: '2px 4px 2px 0' }}
closable
afterClose={() => this.handleRemoveTag(permition)}
>
{permition}
</Tag>
);
})
);
return [].concat(...tags);
}
render() {
const {
selectedScopes,
newScopes,
inputValue,
inputVisible
} = this.state;
const {
form,
treeData,
clients,
isEditing,
isSaving,
onCancel,
onFilterTreeData,
visible
} = this.props;
const {
getFieldDecorator,
getFieldsError,
} = form;
const selectedScopesTags = this.renderTags([newScopes, selectedScopes]);
const clientOptions = clients.map(client => (<Option key={client._id}>{client.name}</Option>));
return (
<Modal
className="user-modal"
title={isEditing ? 'Editing Group' : 'Creating Group'}
visible={visible}
onCancel={onCancel}
footer={[
<Button key="cancel" onClick={onCancel}>Cancel</Button>,
<Button
key="save"
type="primary"
loading={isSaving}
onClick={this.handleSave}
disabled={formRules.hasErrors(getFieldsError())}
>
Save
</Button>
]}
>
<Form layout="vertical" onSubmit={this.handleSave}>
<Row gutter={24}>
<Col span={12}>
<FormItem label="Name">
{getFieldDecorator(
'name',
{ rules: [formRules.required, { max: 20, message: 'Group name can\'t excede 20 characters' }] }
)(
<Input />
)}
</FormItem>
</Col>
<Col span={12}>
<FormItem label="Client">
{getFieldDecorator(
'client', { rules: [formRules.required] }
)(
<Select placeholder="Please select client">
{clientOptions}
</Select>
)}
</FormItem>
</Col>
<Col span={24}>
<FormItem label="Scopes">
{getFieldDecorator(
'allowedScopes'
)(
<TreeSelect
treeData={treeData}
filterTreeNode={onFilterTreeData}
onChange={this.handleSelectedScopesChange}
treeCheckable
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
showCheckedStrategy="SHOW_PARENT"
searchPlaceholder="Filter by scopes"
className="groups__filter groups__filter--fill"
/>
)}
</FormItem>
</Col>
<Col span={24}>
<Card
title="Selected Scopes"
style={{ width: '100%' }}
>
<div>
{inputVisible && (
<Input
ref={this.saveInputRef}
type="text"
size="small"
style={{ width: 350 }}
value={inputValue}
onChange={this.handleInputChange}
onBlur={this.handleInputConfirm}
onPressEnter={this.handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag
onClick={this.showInput}
style={{ background: '#fff', borderStyle: 'dashed', margin: '5px 0' }}
>
<Icon type="plus" /> New Scope
</Tag>
)}
</div>
{ selectedScopesTags.length > 0 ? (
selectedScopesTags
) : <p>No scopes selected yet.</p> }
</Card>
</Col>
</Row>
</Form>
</Modal>
);
}
}
export default Form.create()(FormModal);
I know that this component is urging for a refactoring, but that's not my job right now. I need to UI test this and validate if everything is working properly.
I'm trying to test if the form fields are rendering properly. I'm using Jest and Enzyme and so far I got this:
describe('Groups modal', () => {
let props;
const groupsModal = () =>
mount(
<FormModal.WrappedComponent {...props} />
)
beforeEach(() => {
props = {
data: null,
scopes: [],
clients: [],
treeData: [],
isEditing: false,
isSaving: false,
onCancel: jest.fn(),
onSave: jest.fn(),
onFilterTreeData: jest.fn(),
visible: true,
form: {
getFieldsError: jest.fn(() => { return {} }),
getFieldDecorator: () => (component) => component
}
};
});
it('should render properly', () => {
const wrapperDivChilds = groupsModal().find('.user-modal').children();
expect(wrapperDivChilds.length).toBeGreaterThanOrEqual(1);
});
describe('form fields', () => {
it('should render name input', () => {
const nameInput = groupsModal().find(Input);
expect(nameInput.length).toBe(1);
});
it('should render clients select', () => {
const clientsSelect = groupsModal().find(Select);
expect(clientsSelect.length).toBe(1);
});
it('should render scopes tree select', () => {
const scopesTreeSelect = groupsModal().find(TreeSelect);
expect(scopesTreeSelect.length).toBe(1);
});
});
});
All of my tests that validate if the inputs were rendered are failing.
As you can see, I tried mocking the form decorator functions, but still no success...
So, my question is: how should I test this component?
If you want to assert only initially rendered info, you can directly import your wrapped component and do:
const component = mount(<WrappedComponent {...props} form={formMock} />);
On the other hand if you don't want to use mock form or want to assert any logic connected with form-observer-wrappedComponent chain you can do next:
const wrapper = mount(<FormWrapperComponent {...props} />);
// props include anything needed for initial mapPropsToFields
const component = wrapper.find(WrappedComponent);
// Do anything with form itself
const { form } = component.instance().props;
Kinda more time consuming but worked perfectly for me, spent some time to understand how to avoid using form mock. In this case you don't need to mock anything connected with Form itself and if any field is not rendered as you expect you can be sure that problem is in another piece of code.

react-select prevent menu to open onInputChange

I'm trying to use the react-select component as an input and a select component.
Doing so I would like to prevent the menu to open while the user is typing the input.
I just can't find a way to update this behavior by either a prop or updating the onInputChange method.
My problem if I decide to use a controlled state with the menuIsOpen prop is that I can't manage to reopen the Menu control is clicked.
Here is what I have so far, do you guys any idea of how this could be achieved ?
<StyledSelect
components={{ IndicatorSeparator }}
{..._.omit(this.props, [])}
filterOption={() => true}
inputValue={inputValue}
onChange={value => {
this.select.setState({ menuIsOpen: false });
this.setState({ selectValue: value });
}}
onInputChange={(value, event) => {
if (event && event.action === 'input-change') {
this.setState({ inputValue: value });
}
}}
openMenuOnClick={false}
/>
Example
I think you are in the right direction using onInputChange and I would add a controlled menuIsOpen props like the following code:
class App extends Component {
constructor(props) {
super(props);
this.state = {
menuIsOpen: false
};
}
openMenu = () => {
this.setState({ menuIsOpen: !this.state.menuIsOpen });
};
onInputChange = (props, { action }) => {
if (action === "menu-close") this.setState({ menuIsOpen: false });
};
render() {
const DropdownIndicator = props => {
return (
components.DropdownIndicator && (
<div
onClick={this.openMenu}
style={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginLeft: 12 }}>kg</span>
<components.DropdownIndicator
{...props}
onClick={() => {
console.log("ici");
}}
/>
</div>
)
);
};
return (
<Select
components={{ DropdownIndicator }}
options={options}
onChange={this.onChange}
onInputChange={this.onInputChange}
menuIsOpen={this.state.menuIsOpen}
/>
);
}
}
The idea with this code is to fire an onClick event on the DropdownIndicator custom component.
Here a live example.

Resources