MUI Autocomplete remove option after selection - javascript

I have a Material-UI Autocomplete component. In order to prevent the user from selecting the same element twice (it would double id numbers) I'd like the element removed from the drop down entirely.
For example, if "Shawshank Redemption" was selected, it should get added to the list and be removed from the drop down entirely but not change the JSON data.
I've tried using a filter on filterOptions, but that doesn't seem to be working.
import React, { useState } from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import List from "#material-ui/core/List";
import ListItem from "#material-ui/core/ListItem";
import ListItemText from "#material-ui/core/ListItemText";
import HighlightOffIcon from "#material-ui/icons/HighlightOff";
import IconButton from "#material-ui/core/IconButton";
export default function Playground() {
const defaultProps = {
options: top100Films,
getOptionLabel: (option) => option.title,
filterOptions: (options, state) => {
let newOptions = [];
options.forEach((option) => {
if (option.title.includes(state.inputValue)) {
newOptions.push(option);
}
});
return newOptions.filter((movie) => !movies.includes(movie));
}
};
const [movies, setMovies] = useState([]);
const [key, setKey] = useState(0);
return (
<div style={{ width: 300 }}>
<Autocomplete
{...defaultProps}
id="select-on-focus"
renderInput={(params) => (
<TextField {...params} label="movies" margin="normal" />
)}
onChange={(e, movie) => {
if (movie) {
// this line prevents an error if no movie is selected
setMovies([...movies, movie.title]);
}
// this setKey is supposed to clear the Autocomplete component by forcing a rerender.
// Works in my project but not here.
setKey(key + 1);
}}
/>
<List>
{movies.map((movie) => (
<ListItem key={movie.title}>
<ListItemText primary={movie} />
<IconButton
key={key}
aria-label="delete"
onClick={() => {
setMovies(() => movies.filter((m) => m !== movie));
}}
>
<HighlightOffIcon />
</IconButton>
</ListItem>
))}
</List>
</div>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: "The Shawshank Redemption", year: 1994 },
{ title: "The Godfather", year: 1972 },
{ title: "The Godfather: Part II", year: 1974 },
{ title: "The Dark Knight", year: 2008 },
];
See also: https://codesandbox.io/s/autocomplete-remove-from-list-5dvhg?file=/demo.js:0-6677
Edit: My project just got updated to MUI5, so I'm working on getting full functionality back, then I'll tackle this problem.

Set Autocomplete mode to multiple and turn on filterSelectedOptions to remove the selected option in the dropdown list. To display a list of selected options properly outside of the Autocomplete input, see this other answer.
const [movies, setMovies] = useState([]);
<Autocomplete
{...defaultProps}
multiple
filterSelectedOptions
renderTags={() => null} // don't render tag in the TextField
value={movies}
onChange={(e, newValue) => setMovies(newValue)}
/>
Live Demo

In order to prevent the user from selecting the same element twice (it would double id numbers) I'd like the element removed from the drop down entirely.
How about disabling them so that they cannot be selected again?
You can pass a getOptionsDisabled just like the getOptionLabel

Related

How do I make Material UI's TextField dropdown selector open with a limited size of items?

I am currently working on a Sign up Page, whenever a user enters their location, they will be prompted by a dropdown list of items of different countries, however, when the user opens that list, it covers the whole screen. How would I edit Material UI's TextField component to make it only list a certain number of items at a time without scrolling? Like 5 at a time maybe?
Here is the code in question:
import { useMemo } from 'react'
import {MenuItem, TextField, InputAdornment } from '#mui/material'
import countryList from 'react-select-country-list'
import AddLocationIcon from '#mui/icons-material/AddLocation';
import { useDispatch } from 'react-redux';
import { createUsers } from '../../reducers/usersReducer';
const CountryOfOrigin = () => {
const dispatch = useDispatch()
const options = useMemo(() => countryList().getData(), [])
const changeHandler = (e) => {
e.preventDefault();
dispatch(createUsers(e.target.value, 'location'))
}
return (
<TextField onChange={changeHandler} select className='users-location-input' required={true} label='Location' defaultValue='' InputProps={{
startAdornment: (
<InputAdornment>
<AddLocationIcon color='primary'/>
</InputAdornment>
)
}}>
{options.map(option => {
return (<MenuItem key={option.value} value={option.label}>
{option.label}
</MenuItem>)
})}
</TextField>
)
}
export default CountryOfOrigin
Furthermore,
Here are some screenshots showing the current size of the dropdown list when you open it:
You can try adding a maxHeight property to the SelectProps of the textfield component. Also, I believe you should rather use an MUI autocomplete if you want autocompletion and dropdown both.

Testing a Material ui Select component without using testid

I am currently using Material UI and react-testing-library on a form. I have a Select which is populated by an array of objects.
import React, { useState } from 'react';
import { Select, MenuItem, FormControl, InputLabel } from '#material-ui/core';
const options = [
{
label: 'Example 1',
value: 123456789,
},
{
label: 'Example 2',
value: 987654321,
},
];
const SelectTesting = () => {
const [value, setValue] = useState('');
const handleChange = (event) => {
setValue(event.target.value);
};
return (
<FormControl variant="outlined" fullWidth>
<InputLabel id="selectedOptionLabel">Select an Option</InputLabel>
<Select
labelId="selectedOptionLabel"
id="selectedOption"
value={value}
onChange={handleChange}
label="Select an Option"
>
{options.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
};
export default SelectTesting;
In my testing file what I want to be able to do is check if the actual value of the select is correct. I can always just check the label but in this scenario the value is what is import. So if I select Example 1. I want to run a test to check to see if the selectedOption input has a value of 123456789
import React from 'react';
import { screen, render } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import SelectTesting from '.';
describe('SelectComponent', () => {
it('value should be 123456789', async () => {
render(<SelectTesting />);
const select = await screen.findByLabelText(/^select an option/i);
userEvent.click(select);
const option = await screen.findByRole('option', { name: /^example 1/i });
userEvent.click(option);
// want to check to see if value is 123456789
});
});
I have seen some questions where people say use the data-testId prop but I don't really want to use that since it's the last thing react-testing-library recommends to use.
I also don't want to use Enzyme as it isn't supported with React 18 anymore
Using Enzyme, Jest & Material UI V4/V5
const wrapper = mount(<SelectTesting />);
const select = wrapper.find({ id: [select_id], role: "button" }).hostNodes();
select.simulate("mouseDown", { button: 0 });
const option = wrapper.find({id: [option_id] }).hostNodes();
option.simulate("click");

Need tabs to hold their own state, so it can hold its own number of items within

import axios from 'axios';
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
import StarIcon from '#material-ui/icons/Star';
import List from '#material-ui/core/List';
import ListItem from '#material-ui/core/ListItem';
import Paper from '#material-ui/core/Paper';
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import TwitterIcon from '#material-ui/icons/Twitter';
import CloseIcon from '#material-ui/icons/Close';
import Highlighter from 'react-highlight-words';
class TwitterBot extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleTabState = this.handleTabState.bind(this);
}
state = {
loaded: [],
searched: [],
searchedTicker: '',
actveTab: '',
addedTickers: []
};
async componentDidMount() {
//Gathering data from heroku API I built and adding tweets to loaded array state
let feed = await axios.get('https://boiling-plains-63502.herokuapp.com/');
let tweets = feed.data;
this.setState({
loaded: tweets
});
}
handleChange = (e) => {
//Watching input and changing searchedTicker string while typing
this.setState({ searchedTicker: e.target.value });
};
handleTabChange = (event, newValue) => {
//Selecting the correct tab
this.setState({ tabValue: newValue });
};
handleTabState = (e, data) => {
//This is changing searchedTicker state to the value of whichever tab is clicked
this.setState({ searchedTicker: data });
};
showAll = () => {
//Clearing searched state
this.setState({ searchedTicker: '' });
};
addTicker = () => {
// Adding ticker to saved list
if (this.state.searchedTicker.length > 0) {
this.setState((state) => {
const tickers = state.addedTickers.push(state.searchedTicker);
return {
tickers,
searchedTicker: ''
};
});
} else {
alert('Plase enter a symbol to search');
}
};
removeTicker = (e, data) => {
// Removing tab
let tickers = this.state.addedTickers;
if (tickers.indexOf(data) === 0) {
tickers.shift();
this.showAll();
console.log('zero');
} else {
tickers.splice(tickers.indexOf(data));
this.showAll();
}
};
savedTickerFilter = (f) => {
this.setState({ searchedTicker: f.target.value });
};
render() {
//Trimming searched input to all lowercase and filtering displayed post within return based on search
let loaded = this.state.loaded,
searchedTicker = this.state.searchedTicker.trim().toLowerCase();
if (searchedTicker.length > 0) {
loaded = loaded.filter(function(i) {
return i.text.toLowerCase().match(searchedTicker);
});
}
//Copying loaded state and attempting to added individual numbers of tweets to each tab
let copyOfLoaded = [ ...this.state.loaded ];
let filterCopy = copyOfLoaded.filter(function(i) {
return i.text.toLowerCase().match(searchedTicker);
});
let numOfTweets = filterCopy.length;
return (
<div className="main" style={{ marginTop: 40 + 'px' }}>
<h4>Search a stock symbol below to find relevant tweets from Stocktwitz.</h4>
<h4>You may then press Add to Favorites to create a saved tab for later reference.</h4>
<div className="main__inner">
<TextField
type="text"
value={this.state.searchedTicker}
onChange={this.handleChange}
placeholder="Search Ticker..."
id="outlined-basic"
label="Search"
variant="outlined"
/>
<Button onClick={this.addTicker} variant="contained" color="primary">
Add to favorites <StarIcon style={{ marginLeft: 10 + 'px' }} />
</Button>
</div>
{/* This will be the Filter Tabs component and that will import the list thats below the Paper component below */}{' '}
<Paper square>
<Tabs indicatorColor="primary" textColor="primary" onChange={this.handleTabChange}>
<Tab label={<div className="tabs-label">All ({loaded.length})</div>} onClick={this.showAll} />
{//Mapping through tabs that are added in TwitterBot component and passed down as props to this component
this.state.addedTickers.map((i) => {
return (
<div className="tab-container">
<Tab
label={
<div className="tabs-label">
{i}
({numOfTweets})
</div>
}
key={i}
onClick={(e) => this.handleTabState(e, i)}
/>
<CloseIcon value={i} onClick={(e) => this.removeTicker(e, i)} />
</div>
);
})}
</Tabs>
</Paper>
<List className="tweets">
{loaded.map(function(i) {
return (
<ListItem key={i.id}>
{' '}
<TwitterIcon style={{ marginRight: 10 + 'px', color: '#1da1f2' }} />
<Highlighter
highlightClassName="YourHighlightClass"
searchWords={[ searchedTicker ]}
autoEscape={true}
textToHighlight={i.text}
/>,
</ListItem>
);
})}
</List>
</div>
);
}
}
export default TwitterBot;
Above is the entire component that holds all necessary logic.
I basically want {{numOfTweets}} within the tab-label to be static to each Tab thats mapped through once created. Right now it correctly will show how many items per tab there are while searching, and if clicked on current tab, but all tabs will react. I need them to stay static after search so if clicked on another tab, the past tab will still show how many tweets there were for that searched tab. Right now it's happening just because it's referencing the global loaded state, I just need way to copy that and render each one individually. I hope I explained that clear enough. You can see what I mean on my demo here: https://5ec5b3cfc2858ad16d22bd3c--elastic-khorana-7c2b7c.netlify.app/
I understand I need to break out and componentize this more, but I know theres has to be an easy solution, somehow using a simple functional component to wrap the Tab component or simple just the number that will be displayed. (I'm using Material UI)
Thank you, anything helps, just need to wrap my head around it.
Please check the codesandbox here https://codesandbox.io/s/proud-leftpad-j0rgd
I have added an object instead of a string for Addedtickers so that the count can be tracked and remains constant throughout. You can further optimize this , if you want to search again within each individual tab, but you get the gist.
Please let me know if this works for you

Material UI Autocomplete implementation with options containing array of objects (ID and label)

I am using Material UI Autocomplete for my project. As shown in official documentation, my options are,
let options = [
{ id: "507f191e810c19729de860ea", label: "London" },
{ id: "u07f1u1e810c19729de560ty", label: "Singapore" },
{ id: "lo7f19re510c19729de8r090", label: "Dhaka" },
]
Then, I am using Autocomplete as,
import React, { Component, Fragment, useState } from "react"
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
import options from "/options"
function SelectLocation(props){
const [ input, setInput ] = useState("");
const getInput = (event,val) => {
setInput(val);
}
return (
<Autocomplete
value={input}
options={options}
renderOption={option => <Fragment>{option.label}</Fragment>}}
getOptionLabel={option => option.label}
renderInput={params => {
return (
<TextField
{...params}
label={props.label}
variant="outlined"
fullWidth
/>
)
}}
onInputChange={getInput}
/>
)
}
Now my UI (options list) is showing what I expected. The problem is, I am getting London or Singapore as a value of my input, but I want to get the selected object or ID from this input.
I've followed their documentation thoroughly, but couldn't find a way!
onInputChange get's fired with the actual content of the input.
You might want to use the onChange event exposed by the input props, which will return the selected element. The id should then be available as val.id in your getInput callback.

Using a Modal inside a list seems to pick the last value of the list

I have a list of cities and I'm trying to include a modal with a trash can icon to delete the city next to each item. The problem I have is that the modal seems to pick the last item of the list for EVERY item on the list.
When you click on the icon on any element on the list the confirmation modal always points to the last element on the list and I'm not sure what am I doing wrong. :(
I tried using a Confirm element instead only to find out it's using the modal underneath and I get the same results.
Any gurus around who can help me troubleshoot this will be greatly appreciated!
import React, { useState, useCallback } from "react";
import { List, Icon, Modal, Button } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
const CitiesList = () => {
const [deleteButtonOpen, setDeleteButtonOpen] = useState(false);
const cities = [{ name: "London" }, { name: "Paris" }, { name: "Porto" }];
const handleConfirmDeleteCityModal = useCallback(city => {
console.log("[handleConfirmDeleteCityModal] city", city);
// dispatch(deleteCity(city))
setDeleteButtonOpen(false);
}, []);
const showDeleteCityModal = useCallback(() => {
setDeleteButtonOpen(true);
}, []);
const handleCancelDeleteCityModal = useCallback(() => {
setDeleteButtonOpen(false);
}, []);
return (
<List>
{cities.map(c => (
<List.Item>
<List.Content className="list-item-content">
<List.Header as="h4">{c.name}</List.Header>
</List.Content>
<List.Content floated="left">
<Modal
size="tiny"
open={deleteButtonOpen}
onClose={() => handleCancelDeleteCityModal()}
trigger={
<Icon
name="trash alternate outline"
size="small"
onClick={() => showDeleteCityModal()}
/>
}
>
<Modal.Header>{`Delete City ${c.name}`}</Modal.Header>
<Modal.Content>
<p>Are you sure you want to delete this city?</p>
</Modal.Content>
<Modal.Actions>
<Button negative>No</Button>
<Button
positive
icon="checkmark"
labelPosition="right"
content="Yes"
onClick={() => handleConfirmDeleteCityModal(c)}
/>
</Modal.Actions>
</Modal>
</List.Content>
</List.Item>
))}
</List>
);
};
export default CitiesList;
Here is the example: https://codesandbox.io/s/optimistic-borg-56bwg?from-embed
The problem is this:
<Modal
size="tiny"
open={deleteButtonOpen}
onClose={() => handleCancelDeleteCityModal()}
trigger={
<Icon
name="trash alternate outline"
size="small"
onClick={() => showDeleteCityModal()}
/>
}
>
you use single flag deleteButtonOpen for controlling visibility of all modals. When you set it to true I suppose all modals are opened and you see only the latest one.
Normally I would render single modal and pass as props content of which item I want to show inside.
But if not using separate open flag for each modal should fix it, e.g. https://codesandbox.io/s/vigilant-banzai-byc4t

Categories