Material ui pickers, how to prevent closing on redraw - reactjs

I have a picker which is a part of a component where the date is given from another, super, component.
If the super component is redrawn (say due to a date change) the component that holds the datepicker is destroyed and recreated, thus losing the "open" state.
How would I make this managed? - I can manually state if a picker is "open" or not. However I cannot seem to hook up to the "click on date bar" or "click outside calendar", thus I cannot actually control this.
The component containing the picker:
export class DateDisplay extends React.Component {
render() {
const {date, onChange, onAccept, onOpen, label, openState} = this.props;
return <>
{label && <Typography variant={'subtitle2'} display={'inline'} style={{marginRight: '1ch'}}>
{label}
</Typography>}
<DatePicker
value={date}
onOpen={() => {
this.setState({open: true},onOpen)
}}
onChange={onChange}
onAccept={onAccept}
format={'ddd, D MMMM YYYY'}
variant={'inline'}
open={openState}
onClick={e => {
console.log('we clicked the picker')
}}
/>
</>
}
}
And the super component which would be responsible for "managing" the datepicker:
export class DatesList extends React.Component {
this.onSave = async () {/*code to request a save to the backend*/};
onStartEditing = () {/*code to visually indicate we're editing a field*/};
render() {
const {dates} = this.props; //date is a moment + meta data.
const sorted_dates = Array.from(eventData.dates.values()).sort(
(l, r) =>
(!l.start_date ? 0 : l.start_date.valueOf()) - (!r.start_date ? 0 : r.start_date.valueOf())
);
sorted_dates.map(date => {<div>
<DateDisplay
label={'Event start date:'}
date={date.start_date}
onOpen={(e) => {
this.onStartEditing(e, 'start_date');
}}
onChange={(newDate) => {
date.start_date = !nullOrUndefined(newDate) ? newDate.toDate() : newDate;
}}
onAccept={async () => {
await this.onSave('start_date');
}}
/>
</div>});
}
}
As a side question: I notice that the components are not rebuild if the sorting is removed, even though there is a single element in the list thus sorting would always give the same "order", why's that?

You can use your own state for opening/closing.
Use open prop and onClose and onOpen callbacks. Deeper example from documentation: https://material-ui-pickers.dev/guides/controlling-programmatically

Related

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.

Is it possible to partially apply a React component?

Say I have a <Button> component which takes two properties: text and id e.g.,
<Button text="delete" id="123"/>
Now say I have a list of user ids: [101, 102, 103, …]
Would it be possible to partially apply <Button>? e.g.,
ids.map(<Button text="delete" id={__}>)
Where __ is just a placeholder waiting to be replaced with the current id.
If it was possible, would partially applying a React component have any adverse effect on the React Reconciliation Algorithm?
You could use two ways
one, which is not really a partial
ids.map((id)=><Button text="delete" id={id} />)
and the partial one which is really extracting the function above and using it
const PartialDeleteButton = (id) => <Button text="delete" id={id} />
ids.map(PartialDeleteButton)
which you could also use as
<PartialDeleteButton id={5} />
i cannot see how these would affect the reconciliation algorithm
There is no partial render of a component in React.
A component watches on state and props. Whenever you change either one, it will refresh the component. So if you change id dynamically, it will re-render the component.
However that would be 1 extra re-render.
You can however choose to write functions to prevent that like
React.memo: For latest react
shouldComponentUpdate: For older version.
Following is a demo for React.memo:
What to look in fiddle, notice I have added a setTimeout that updates data and it will call the render of ToDoApp but since components are memoised, it will not be called
function Button({id, value}) {
const onClick = () => {
console.log(`${id} - ${value}`)
}
console.log(`Rendering Btn ${value}`)
return (<button id={id || '__'} onClick={onClick}>{ value }</button>);
}
const MyButton = React.memo(
Button,
(prevProps, nextProps) => {
return prevProps.value === nextProps.value
}
)
Note: Since you will stop rendering of a component, you will not get updated props in it.
You could use useCallback to get a similar effect to partial application:
const HelloGreeter = useCallback(({name}: {name: string}) =>
(<Greeter name={name} greet="hello" />), []);
So, in context:
interface GreeterProps {
greet: string
name: string
}
const Greeter = ({greet, name}: GreeterProps) => (
<div>{greet}, {name}</div>
);
const MyComponent = () => {
const [name1, setName1] = useState<string>("world")
const HelloGreeter = useCallback(({name}: {name: string}) =>
(<Greeter name={name} greet="hello" />), []);
const setNameCallback = useCallback((e: ChangeEvent<HTMLInputElement>) =>
setName1(e.target.value), []);
return(
<>
<HelloGreeter name={name1} >
<input value={name1} onChange={setNameCallback} />
</>
);
}
This would not confuse the React renderer, because useCallback defines the function once only.

How to render a different component with React Hooks

I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.

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

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

React function not working from child component

I am trying to get a function working which removes an image uploaded using React Dropzone and react-sortable.
I have the dropzone working, and the sort working, but for some reason the function I have on the sortable item which removes that particular item from the array does not work.
The onClick event does not seem to call the function.
My code is below.
const SortableItem = SortableElement(({value, sortIndex, onRemove}) =>
<li>{value.name} <a onClick={() => onRemove(sortIndex)}>Remove {value.name}</a></li>
);
const SortableList = SortableContainer(({items, onRemove}) => {
return (
<ul>
{items.map((image, index) => (
<SortableItem key={`item-${index}`} index={index} value={image} sortIndex={index} onRemove={onRemove} />
))}
</ul>
);
});
class renderDropzoneInput extends React.Component {
constructor (props) {
super(props)
this.state = { files: [] }
this.handleDrop = this.handleDrop.bind(this)
}
handleDrop (files) {
this.setState({
files
});
this.props.input.onChange(files)
}
remove (index){
var array = this.state.files
array.splice(index, 1)
this.setState({files: array })
this.props.input.onChange(array)
}
onSortEnd = ({oldIndex, newIndex}) => {
this.setState({
files: arrayMove(this.state.files, oldIndex, newIndex),
});
};
render () {
const {
input, placeholder,
meta: {touched, error}
} = this.props
return (
<div>
<Dropzone
{...input}
name={input.name}
onDrop={this.handleDrop}
>
<div>Drop your images here or click to open file picker</div>
</Dropzone>
{touched && error && <span>{error}</span>}
<SortableList items={this.state.files} onSortEnd={this.onSortEnd} onRemove={(index) => this.remove(index)} />
</div>
);
}
}
export default renderDropzoneInput
Update: This was caused by react-sortable-hoc swallowing click events. Setting a pressDelay prop on the element allowed the click function to fire.
This is old question, but some people, like me, who still see this issue, might want to read this: https://github.com/clauderic/react-sortable-hoc/issues/111#issuecomment-272746004
Issue is that sortable-hoc swallows onClick events as Matt found out. But we can have workarounds by setting pressDelay or distance.
For me the best option was to set minimum distance for sortable list and it worked nicely
You can also use the distance prop to set a minimum distance to be dragged before sorting is triggered (for instance, you could set a distance of 1px like so: distance={1})

Resources