I am building an autocomplete component to search current users and add them to a team. The search works fine except I need to customize a couple things that I can't figure out how to do.
First, this component is modeled after GitHub's invite user modal. You can start typing to search for a current user and it populates any results it finds. However, if it doesn't find any results it only shows one item to invite that user via email. How can I edit the full state of the result list for a DataSearch component? All I can see is to edit the content of each suggestion via the onSuggestion prop. I need to be able to say, "If there are zero results, display this."
Second, when a suggestion from the autocomplete result list is selected, I need to be able to reset the search box because I am populating a list that the user can see. Right now, the search box is populated with the value of the suggestion. So, I am populating the list below of selected results just fine; but, I still need to be able to reset the search box when that result is selected.
Help????
CodeSandbox link
For the first part of the problem, you can use the prop onNoResults on any of the results components to render custom JSX when no results are found. From the docs:
onNoResults String or JSX [optional]
show custom message or component when no results founds.
<ResultList
...
// custom JSX when no results are found
onNoResults={
<section>
<input />
<button>Add</button>
</section>
}
/>
For the second part of the problem, there are two ways IMO you may approach this.
Using a ReactiveComponent which lets you create a custom ReactiveSearch component.
Using customQuery
I'll explain how to approach this using customQuery but it might be better to create a custom component depending on which approach suits your needs best.
In the example I've shared my DataSearch looks like this:
<DataSearch
title="DataSearch"
dataField={["original_title", "original_title.search"]}
categoryField="authors.raw"
componentId="BookSensor"
customQuery={this.customQuery}
defaultSelected={this.state.value}
onValueSelected={this.updateState}
/>
The reason for using a customQuery is to get full control of which query gets applied to retrieve the results. ReactiveSearch is designed to work reactively. When a value is set into the DataSearch, the ResultList would react to this change. Having a customQuery lets us control which query is fired for this change. Also I'm keeping the value of DataSearch in the state so I can clear it up when the query gets fired. Here's what I'm using in the example:
customQuery = (value, props) => {
// find the current query using defaultQuery from DataSearch
const currentQuery = DataSearch.defaultQuery(value, props);
// clear the value in component's state
this.setState({
value: ""
});
if (value.length) {
// store this query
this.prevQuery = currentQuery;
return currentQuery;
}
// if the value is empty, that is it was cleared, return the previous query instead
return this.prevQuery;
};
So whenever the value is cleared, I just return the previous query so the result list shows the correct results for the previous query (before the value was cleared).
Also in order to control the value of DataSearch from my component I'm using the defaultSelected and onValueSelected props. They work quite similar to how you would create a controlled component in react. Docs
Again, it might be better to create a custom component using ReactiveComponent if this approach sounds complicated to customize this flow.
Demo for this answer using customQuery
Related
I have a page in a react app that uses a radio form to filter different objects of data that are being passed to the page.
The problem I am encountering, is that when I change the filter, (click on a different option on the radio form), only some of the data in the resulting list changes. A simple example of what happens is as follows:
Option one is selected with all the old data
Option two is selected, but only some of the new data comes through
First, I use an axios request to get an array of objects that will be used for the data:
componentDidMount() {
axios.get("xxxxxxxxx")
.then(result => {
this.setState({
data: result.data
});
});
Then, I create an array that filters the data from axios by an attribute based on which radio option is selected in the form:
let filteredData = [];
filteredData = this.state.data.filter(thisData => thisData.attribute === "attribute1");
Finally, I map all of the filtered data in the render function:
filteredData.map(filteredItem => ( <MyComponent key={i++} itemInfo={filteredItem.info} /> ))
In the definition of MyComponent, I use props to access the filtered item's info and put it into the table like this:
<td>{this.props.itemInfo.thisDataPoint}</td>
I'd love to hear if anybody has any idea why some of the components data updates when the filter changes, but not all of it. It seems weird to me that some data changes but some does not.
I have tried converting the props into state so that the component re-renders on the change but that did not work.
Thanks again for any help I get :)
given that filteredData is correct, and based on your code the issue must be on key={i++}. using some kind of implementation index can often lead to rendering problems, react will have trouble to distinguish the components since it uses the key to track them.
you should provide some unique identifier to each component as key like key={filteredItem.id}. if you don't have it, you can generate it with some library like uuid.
I'm struggling with this issue for a couple of days now, can you help me figure it out?
I'm trying to set the selected items for the component but it just let me set the initial selected items, but I need to set it after it is created. I wish it had a property like 'SelectedValues' where I could pass an array..instead it has a callback function but I can only retrieve what the user selected not set the selection.
the documentation https://pnp.github.io/sp-dev-fx-controls-react/controls/ComboBoxListItemPicker/ isn't saying anything about how to do it, just to retrieve what the user chose.
The code is like this:
<ComboBoxListItemPicker listId='da8daf15-d84f-4ab1-9800-7568f82fed3f'
columnInternalName='Title'
keyColumnInternalName='Id'
filter="Title eq 'SPFx'"
defaultSelectedItems=[{Id: 2, Title:"Test"}]
onSelectedItem={this.onSelectedItem}
webUrl={this.context.pageContext.web.absoluteUrl}
spHttpClient={this.context.spHttpClient} />
The onSelectedItem change event returns the list items selected and can be implemented as follows:
private onSelectedItem(items: []) {
console.log("selected items:", items);
}
I managed to solve this by using a workaround...I force the component to be recreated by setting a new key to the component after I update the state with the values I want selected, so i just use the property initialselectedvalues.
Been working with redux in a complex react application.
I put some data in redux state and use this data to render a component.
Then, you want to change the page and I do need to reset some fields in the redux state. Those fields were used to render the previous component.
So, if I reset the state before going to the next page, the previous page rerenders and throws errors because data is missing (because of the reset). But I can't reset in the next page, because that page is reached in many flows so it can be difficult to manage when to reset and when not.
How problems like this are managed in react applications?
All the example are simplified to show the problem. in the actual application, there are many fields to reset.
so, page 1
redux state
{field: someValue}
component uses the field value
function myComponent(props) => {
return (props.field.someMappingOperation());
}
Now, when going to page 2, field should be null, so we reset it
redux state
{field: null}
the component above rerenders, throwing an error because of
props.field.someMappingOperation()
and field is null.
What can be done is to load the next page, so this component is not in the page and then reset the state. yet, this becomes very hard to manage, because you the are in page B (suppose clients list with the list saved in redux) and you want to see the details of a client you go to page C, when you press back you don't want to reset again the state. So you add conditions on the reset. But because there are many flows in the application, that page can be reached in many ways. Have conditions for each way is not the best solution I suppose.
Edit:
I would like to add that state reset was not required initially and components aren't designed for that. As the application grew and became enough complex it became necessary. I'm looking for a solution that does not require me to add props value checking in every and each component, like
{this.props.field && <ComponentThatUsesField />}
This is really a lot of work and it's bug-prone as I may miss some fields.
As the application has a lot of pages and manages a lot of different data, the store results to be big enough to not want to have clones of it.
You have to design your components in a more resilient way so they don't crash if their inputs are empty.
For example:
render() {
return (
{ this.props.field && <ComponentUsingField field={this.props.field} />}
)
}
That's a dummy example, but you get the point.
you can change your code like below:
function myComponent(props) => {
const {fields} = props;
return (fields && fields.someMappingOperation());
}
This will not throw any error and is a safe check
You don't need to use conditions or reset your state, your reducer can update it and serve it to your component. You can call an action in componentDidMount() and override the previous values so they will be available in your component. I assume this should be implied for each of your component since you are using a different field values for each.
I have two component, FormComponent and OrderListComponent. There is one field on the FormComponent to ask users to select a value from a list which is from OrderListComponent. So I put a button on the field in FormComponent, when users click on that button, I use router.push('XXXX') to navigate the page to OrderListComponent to show all the options for user. When users click one of the options on OrderListComponent, I use router.goBack() to go back to FormComponent. Here is the problem, how does FormComponent get the selected value from OrderListComponent? I know that I can save the selection as a state in redux store, but I wander whether there is a more direct way to solve this issue.
The best way is to have state manager. Redux state will solve it elegantly. Though as Random User mentioned in his answer; localStorage/sessionStorage can be used to do this.
But passing the values by URL will be tricky when there are too many items selected for Order.
The state can be changed so that having anything in state will give consistent output at OrderListCompoennt.
You can have order state and push the items to that state. And then redirect the user to OrderListComponent rather than doing router.push('XXXX').
OrderListComponent will watch the state and show the items accordingly.
The FormComponent will also watch the order state for marking the currently added items. With this way, there will not be any router.goBack(); just the redirects by router.push
And when your app will be at FormComponent, order state will automatically be responsible to mark the items which are currently in order.
Storing the selected value in redux store or component state itself would be better choice for this, but you can also
store it in localStorage
store it in sessionStorage
when redirecting the user back, pass it as a url parameter
If you're using any one of these methods, In FormComponent you can check if the value exists and perform operations accordingly.
Example code
OrderListComponent
let handleChange = (value) => {
localStorage.orderListValue = value
}
This will replace any existing value as well.., Use your brain here.
FormComponent
if ( localStorage.orderListValue ) {
console.log('Yay.. we have data');
} else {
// ask the user to select the List first.
}
I'm trying to find out the "proper" way to do the following in React:
I have a form with two fields, url and title.
Whenever url's value changes, I make an API call to Embedly to retrieve metadata about the link.
Once the metadata has been retrieved, I want to update the title field with the result.
The difficulty here is that the url and title fields are not in the same component. Here's the basic structure of the form (I'm using Formsy):
<Formsy.Form onSubmit={this.submitForm} onChange={this.updateState} ref="form">
{fields.map(field => <FormComponent
name={field.name}
type={field.type}
value={field.value}
/>)}
<Button type="submit">Submit</Button>
</Formsy.Form>
As you can see, the form loops over an array of fields and calls a generic <FormComponent/> component for each of them, which is basically a big switch that then calls the appropriate Formsy Component based on the field's type.
The logic for querying Embedly is already working inside the component for the url field, but I'm not sure if there's a way to accomplish what I want while still using the default Input component for title?
[Edit: thanks to Dan I was able to come up with a better solution for step 4 and after]
So here's the solution I came up with. I have no clue if it's the best pattern or not, but at least so far it seems to work.
Step 1: add a addToPrefilledValues method to the <Form/> component that takes a property and adds it to the prefilledValues object on that component's state.
Step 2: add addToPrefilledValues to the form component's context so that the method is passed on to all child components (note: I could also pass it as a prop but context seems easier to pass it on to grandchild components).
Step 3: make my <URLField/> component call addToPrefilledValues whenever it receives new metadata from Embedly:
this.context.addToPrefilledValues({title: result.title, body: result.description});
Step 4 [Wrong, see below]: in the <TitleField/> component's shouldComponentUpdate method, watch for changes to the context and update the field's value (if it's empty):
shouldComponentUpdate(nextProps, nextState, nextContext) {
const nextTitle = nextContext.prefilledValues && nextContext.prefilledValues.title;
const currentTitle = this.context.prefilledValues && this.context.prefilledValues.title;
if (!!nextTitle && nextTitle != currentTitle && !this.input.getValue()) {
this.input.setValue(nextTitle);
}
return true;
}
Step 4 (better): whenever the form changes, store all of the form's values in a currentValue object on the form's state.
Step 5: Look at:
the original value of the field (i.e. in the database), as passed through this.props).
the current value being typed in the field, as found in this.state.currentValue.
the prefilled value generated by the Embedly API call, as found in this.state.prefilledValue.
Figure out which one is correct (i.e. if the user hasn't entered anything in the field prefill it, if not don't) and pass that on to the form field child component.
This achieves the desired result:
<URLField/> and <TitleField/> don't have to know about or communicate with each other.
<Form/> doesn't have to know about <URLField/> or <TitleField/> either.
As #ffxsam suggested though, I should probably just use Redux Form…