(React-redux) BlueprintJs Suggest cannot backspace in input after making a selection - reactjs

class MySuggest extends React.Component<Props, State> {
....
....
private handleClick = (item: string, event: SyntheticEvent<HTMLElement, Event>) => {
event.stopPropagation();
event.preventDefault();
this.props.onChange(item);
}
public render() {
const { loading, value, error} = this.props;
const { selectValue } = this.state;
const loadingIcon = loading ? <Icon icon='repeat'></Icon> : undefined;
let errorClass = error? 'error' : '';
const inputProps: Partial<IInputGroupProps> = {
type: 'search',
leftIcon: 'search',
placeholder: 'Enter at least 2 characters to search...',
rightElement: loadingIcon,
value: selectValue,
};
return (
<FormGroup>
<Suggest
disabled={false}
onItemSelect={this.handleClick}
inputProps={inputProps}
items={value}
fill={true}
inputValueRenderer={(item) => item.toString()}
openOnKeyDown={true}
noResults={'no results'}
onQueryChange={(query, event) => {
if (!event) {
this.props.fetchByUserInput(query.toUpperCase());
}
}}
scrollToActiveItem
itemRenderer={(item, { modifiers, handleClick }) => (
<MenuItem
active={modifiers.active}
onClick={() => this.handleClick(item) }
text={item}
key={item}
/>
)}
/>
</FormGroup>
);
}
}
Everything works fine, I am able to make a selection from drop-down list, however I cannot use backspace in input if I made a selection. I checked the official documentation(https://blueprintjs.com/docs/#select/suggest), it has the same issue in its example. Does anyone has the similar problems and solutions?

The reason for this is once you type something in the field, it becomes an element of the page, so once you make a selection, it assumes you highlighted an element, so will assume you are trying to send the page a command for that selection (backspace is the default page-back command for most browsers).
Solution:
Create a new dialog input every time the user makes a selection, so the user can continue to make selections and edits.

It took forever.. post my solution here:
be careful about two things:
1. query = {.....} needed to control the state of the input box
2. openOnKeyDown flag, it makes the delete not working

Related

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.

autosuggest not showing item immediately

I am looking into fixing a bug in the code. There is a form with many form fields. Project Name is one of them. There is a button next to it.So when a user clicks on the button (plus icon), a popup window shows up, user enters Project Name and Description and hits submit button to save the project.
The form has Submit, Reset and Cancel button (not shown in the code for breviety purpose).
The project name field of the form has auto suggest feature. The code snippet below shows the part of the form for Project Name field.So when a user starts typing, it shows the list of projects
and user can select from the list.
<div id="formDiv">
<Growl ref={growl}/>
<Form className="form-column-3">
<div className="form-field project-name-field">
<label className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated custom-label">Project Name</label>
<AutoProjects
fieldName='projectId'
value={values.projectId}
onChange={setFieldValue}
error={errors.projects}
touched={touched.projects}
/>{touched.projects && errors.v && <Message severity="error" text={errors.projects}/>}
<Button className="add-project-btn" title="Add Project" variant="contained" color="primary"
type="button" onClick={props.addProject}><i className="pi pi-plus" /></Button>
</div>
The problem I am facing is when some one creates a new project. Basically, the autosuggest list is not showing the newly added project immediately after adding/creating a new project. In order to see the newly added project
in the auto suggest list, after creating a new project,user would have to hit cancel button of the form and then open the same form again. In this way, they can see the list when they type ahead to search for the project they recently
created.
How should I make sure that the list gets immediately updated as soon as they have added the project?
Below is how my AutoProjects component looks like that has been used above:
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import axios from "axios";
import { css } from "#emotion/core";
import ClockLoader from 'react-spinners/ClockLoader'
function escapeRegexCharacters(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Use your imagination to render suggestions.
const renderSuggestion = suggestion => (
<div>
{suggestion.name}, {suggestion.firstName}
</div>
);
const override = css`
display: block;
margin: 0 auto;
border-color: red;
`;
export class AutoProjects extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
projects: [],
suggestions: [],
loading: false
}
this.getSuggestionValue = this.getSuggestionValue.bind(this)
this.setAutoSuggestValue = this.setAutoSuggestValue.bind(this)
}
// Teach Autosuggest how to calculate suggestions for any given input value.
getSuggestions = value => {
const escapedValue = escapeRegexCharacters(value.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp(escapedValue, 'i');
const projectData = this.state.projects;
if (projectData) {
return projectData.filter(per => regex.test(per.name));
}
else {
return [];
}
};
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
getSuggestionValue = suggestion => {
this.props.onChange(this.props.fieldName, suggestion.id)//Update the parent with the new institutionId
return suggestion.name;
}
fetchRecords() {
const loggedInUser = JSON.parse(sessionStorage.getItem("loggedInUser"));
return axios
.get("api/projects/search/getProjectSetByUserId?value="+loggedInUser.userId)//Get all personnel
.then(response => {
return response.data._embedded.projects
}).catch(err => console.log(err));
}
setAutoSuggestValue(response) {
let projects = response.filter(per => this.props.value === per.id)[0]
let projectName = '';
if (projects) {
projectName = projects.name
}
this.setState({ value: projectName})
}
componentDidMount() {
this.setState({ loading: true}, () => {
this.fetchRecords().then((response) => {
this.setState({ projects: response, loading: false }, () => this.setAutoSuggestValue(response))
}).catch(error => error)
})
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: this.getSuggestions(value)
});
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
render() {
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: value,
value,
onChange: this.onChange
};
// Finally, render it!
return (
<div>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
/>
<div className="sweet-loading">
<ClockLoader
css={override}
size={50}
color={"#123abc"}
loading={this.state.loading}
/>
</div>
</div>
);
}
}
The problem is you only call the fetchRecord when component AutoProjects did mount. That's why whenever you added a new project, the list didn't update. It's only updated when you close the form and open it again ( AutoProjects component mount again)
For this case I think you should lift the logic of fetchProjects to parent component and past the value to AutoProjects. Whenever you add new project you need to call the api again to get a new list.

Fetching label value of switch in react native

I want to achieve functionality of a checkbox with a switch in react-native, as checkbox isn't supported in ios devices. I am not getting on how can I fetch the label value associated with a particular switch.
So, the idea is, I have a bunch of options, say A,B,C and each is associated with a switch, and there is a submit button. On click of submit I want to get the labels of those options which are toggled on.
This is the code for selecting various options and each is associated with a switch,
<Text>A</Text>
<Switch
onValueChange = {this.handleToggle}
value = {toggleValue}
/>
<Text>B</Text>
<Switch
onValueChange = {this.handleToggle}
value = {toggleValue}
/>
<Text>C</Text>
<Switch
onValueChange = {this.handleToggle}
value = {toggleValue}
/>
And handleToggle code is this,
handleToggle = event => {
this.setState(state => ({
toggleValue: !state.toggleValue
}));
}
Thanks a lot.
You are using the same function for different switches, clicking on one of them won't give you access to the label of that particular switch. To do so i would suggest to build it like this. This could be a working solution:
Starting with an array that looks like:
this.state = {
data = [
{
label: "firstSwitch",
checked: false,
},
{
label: "secondSwitch",
checked: false,
}
]
}
Then, in your render:
this.state.data.map((item, index) => <Fragment>
<Text>{item.label}</Text>
<Switch
onValueChange = {() => this.handleToggle(index)}
/>
</Fragment>
)
The handleToggle will update the array in the position passed as argument:
handleToggle = index => {
let tempArr= this.state.data
tempArr[index].checked = !tempArr[index].checked
this.setState({data:tempArr})
}
The submit button will then check for the checked switches:
onSubmit = () => {
let arrOfCheckedSwitches= []
this.state.data.forEach (item => item.checked && arrOfCheckedSwitches.push(item.label))
console.log("array of labels :", arrOfCheckedSwitches)
}
Let me know if there's something that's unclear

How to set ErrorMessage on TextField dynamically on Button onClick method

I need to bind ErrorMessage to textfield only when user press button. In this there are nice examples how to use errormessage but the problem is that I don't know how to make append errorMeesage after user click
<TextField id='titleField' value={titleValue} required={true} label={escape(this.props.description)} onGetErrorMessage={this._getErrorMessage} validateOnLoad={false} />
And this is a call of a button:
private _buttonOnClickHandler() {
let textvalue = document.getElementById('titleField')["value"];
if(textvalue === "")
{
//call onGetErrorMessage or something that will set ErrorMeesage and input will be red
}
return false;
}
Thank you
The easiest way I can think of to accomplish this is by predicating the onGetErrorMessage on a state check, which tracks whether the button has been clicked.
<TextField
id='titleField'
value={titleValue}
required={true}
label={escape(this.props.description)}
// Only allow showing error message, after determining that user has clicked the button
onGetErrorMessage={this.state.buttonHasBeenClicked ? this._getErrorMessage : undefined}
validateOnLoad={false}
/>
Then in your button click handler, simply set that state value:
private _buttonOnClickHandler() {
this.setState({ buttonHasBeenClicked: true });
return false;
}
As long as you instantiate buttonHasBeenClicked to be false, then this method will meet the requirement that (a) before the user clicks the button, no error messages are shown by the TextField, and (b) after the user clicks the button, error messages start to be shown. You retain the ability to use _getErrorMessage(value) to customize the error message based upon the current value in the TextField.
You need to set state for displaying/hiding error messages based on user input, Check below code
import React, { Component } from 'react';
class App extends Component {
state = {
isErrorMessageHidden: true
}
clickHandler = () => {
let textValue = document.getElementById('titleField').value;
if(textValue === '')
this.setState({isErrorMessageHidden: false});
else
this.setState({isErrorMessageHidden: true});
}
render() {
return (
<div>
<button onClick={this.clickHandler}>Check</button>
<input type='text' id='titleField' />
<p
style={{'color':'red'}}
hidden={this.state.isErrorMessageHidden}
>Please fill the form</p>
</div>
);
}
}
export default App;
I hope this helps.
Yet another approach would be to handle all the validations via state values.
The control definition goes like this
<TextField placeholder="Enter a venue location" required onChange={this._onVenueChange} {...this.state.errorData.isValidVenue ? null : { errorMessage: strings.RequiredFieldErrorMessage }}></TextField>
On the Onchange event, you validate the control's value and set the error state value as below,
private _onVenueChange = (event: React.FormEvent<HTMLTextAreaElement | HTMLInputElement>, newValue?: string): void => {
this.setState(prevState => ({
errorData: {
...prevState.errorData,
isValidVenue: newValue && newValue.length > 0,
isValidForm: this.state.errorData.isValidForm && newValue && newValue.length > 0
}
}));
this.setState(prevState => ({
formData: {
...prevState.formData,
venue: newValue
}
}));
}
In the above example, I am using two objects within states, one for capturing the values and other for capturing whether the field is error-ed or not.
Hope this helps..
Cheers!!!

react-sortable-hoc: Handling of click event on list item

This question is about https://github.com/clauderic/react-sortable-hoc.
I am pretty new to React, so I find the following a bit irritating. Go to
https://jsfiddle.net/7tb7jkz5/. Notice line 4
const SortableItem = SortableElement(({value}) => <li className="SortableItem" onClick={console.debug(value)}>{value}</li>);
When you run the code, the console will log "Item 0" to "Item 99". A click on an item will log the same, but three times. Why does this happen? And is this really necessary or more like a bug?
I expected a behavior similar to an ordinary DOM, so the click event would bubble up from the clicked item and could be caught along the way through its ancestors. The observed behavior looks to me like the click event would be sent down by the list to each list item three times.
Update:
Well, this is actually as clear as crystal, thanks for pointing this out, Shubham. I did not just specify a reference, but an actual call to console.debug, which was executed every time the expression was evaluated. Common mistake.
Still, this means, each list item is rendered (I guess) three times, when I click one of them. I suspect missing optimization or even useless redrawing.
Try to use distance={1} on SortableContainer component.
Check this link https://github.com/clauderic/react-sortable-hoc/issues/461
const ListItemContainer = SortableContainer((props) => {
return <listItem />
})
<ListItemContainer
onSortEnd={this._orderingFolder}
lockAxis='y'
lockToContainerEdges={true}
lockOffset='0%'
distance={1}
/>
Another way you can define and handle click event using react-sortable-hoc:
please check below code
import { SortableContainer, SortableElement, arrayMove } from 'react-sortable-hoc';
const SortableItem = SortableElement(({ value }) => {
return (
<div >
<div id="button_value">{value}</div>
</div >
);
});
const SortableList = SortableContainer(({ items }) => {
return (
<div >
{items.map((value, index) => (
<SortableItem
key={`item-${index}`}
index={index}
value={value}
/>
))}
</div>
);
});
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'],
};
}
onSortEnd = ({ oldIndex, newIndex }) => {
this.setState(({ items }) => ({
items: arrayMove(items, oldIndex, newIndex),
}));
};
shouldCancelStart = (e) => {
var targetEle = e;
if (!targetEle.id) {
targetEle = e.target;
}
if (targetEle.id === 'button_value') {
console.log('Div button click here ');
// perform you click opration here
}
// you can define multiple click event here using seprate uniq id of html element even you can manage delay on click here also using set timeout and sortmove props and sortend props you need to manage just one boolean flag.
}
render() {
return (
<div>
<SortableList
items={this.state.items}
onSortEnd={this.onSortEnd}
onSortStart={(_, event) => event.preventDefault()}
shouldCancelStart={this.shouldCancelStart} />
</div>
);
}
}
export default App;
First define id button_value as in html element and then using this id you can get click event using this props shouldCancelStart
You need to mention the onClick action as a function. Use () => inside the handler call. Try the below method, it works although has a very slow response
const SortableItem = SortableElement(({value}) => <li className="SortableItem" onClick={() => console.debug(value)}>{value}</li>);

Resources