How to get multiple selected options value in React JS? - reactjs

<Multiselect label='Select College' ref="collegeList" onChange={this.handleChange} multiple >
<option value='college1'>college1</option>
<option value='college2'>college2</option>
</Multiselect>
This component is from https://github.com/skratchdot/react-bootstrap-multiselect
What should be written inside the handleChange() function ?

Here's a much cleaner, es6 way to do it :)
let selected = [...this.refs.select]
.filter(option => option.selected)
.map(option => option.value)
There you go, all of the selected options!

Unfortunately, react-bootstrap-multiselect doesn't seem to expose any sort of API for getting the currently selected items, so you'll have to query them from the DOM directly. Try something like this:
handleChange: function () {
var node = React.findDOMNode(this.refs.collegeList);
var options = [].slice.call(node.querySelectorAll('option'));
var selected = options.filter(function (option) {
return option.selected;
});
var selectedValues = selected.map(function (option) {
return option.value;
});
console.log(selectedValues);
}
If you're using jQuery, this can be simplified a bit to:
handleChange: function () {
var node = $(React.findDOMNode(this.refs.collegeList));
var selectedValues = node.children('option:selected').map(function(option) {
return option.value;
});
console.log(selectedValues);
}

I would suggest to have a state in your component called selectedItems
The onChange callback then, takes as parameters element and checked, from the Bootstrap Multiselect docs. Element has the val() method, which returns the value assigned to the option.
Therefore handleChange could be implemented in the following way
handleChange: function (element, checked) {
var newSelectItems = _.extend({}, this.state.selectItems);
newSelectItems[element.val()] = checked;
this.setState({selectItems: newSelectItems})
},
getInitialState: function () {
return {selectItems: {}};
}
In this way, every time an element is clicked, its checked attribute is saved in the component state, which is quite handy if you need to change anything based on the MultiSelect selected values.
Please note that for the above code you need either the Underscore or the Lodash library. This is necessary as React cannot merge nested objects, as answered here.

A much simpler and a direct way to get the values:
handleChange: function () {
var selectedValues = this.refs.collegeList.$multiselect.val();
console.log(selectedValues);
}

you can use selectedOption that return a htmlCollection like this
onchange(e){
let selected=[];//will be selected option in select
let selected_opt=(e.target.selectedOptions);
console.log(selected_opt)
for (let i = 0; i < selected_opt.length; i++){
selected.concat(selected_opt.item(i).value)
}
}
and our select
<select onChange={this.onchange.bind(this)} className="selectpicker w-100 rtl" multiple >
{this.state.list.map(obj=><option value={obj[this.props.val]}>{obj[this.props.name]}</option>)}
</select>

clean es6 code using selectedOptions
let selected = [...this.refs.collegeList.selectedOptions].map(o => o.value);

React 16 with TS.
Note: refs are deprecated, that's why I used callback function to set Ref.
private selectInput: any;
private setSelectRef = element => {
this.selectInput = element;
};
private handleMultiSelectChange = () => {
const selected = [...this.selectInput.refs.MultiselectInternal.selectRef].filter(option => option.selected)
.map(option => option.value);
// there you can update your state using this.setState()
};
...
<Multiselect data={data}
onChange={this.handleMultiSelectChange}
multiple={true}
ref={this.setSelectRef}
buttonClass="btn btn-default"
/>

Related

Best way to avoid repeating code when using addEventListener and functions

im pretty new at coding, currently studing front end dev. I´m on my 5:th week learning JS and got a challange to create a toDo list with Typescript.
When i create a task in my app it has a checkbox and a "trash"-button. The idéa of the trash button is pretty clear and the checkbox is going put the task last in the list when its "checked".
I noticed some repetative code while creating my event listeners. So I found a way to add the event listener to multiple elements but I can't wrap my head around how to call the different functions that corresponds to what was clicked.
this is how I found a way to add the event listener to each element. But from here, how can I call a function based on what was clicked?
document.querySelectorAll('.add-click-listener').forEach(item => {
item.addEventListener('click', event => {
})
})
this was the code from the beginning
let checkbox = Array.from(document.querySelectorAll('.hidden-checkbox'));
checkbox.forEach((item) => {
item.addEventListener('click', checkboxStatusShift);
});
let trashBtn = Array.from(document.querySelectorAll('.trash-btn'));
trashBtn.forEach((item) => {
item.addEventListener('click', deleteTask);
});
this will be the function to "trash" a task:
function deleteTask(event: any) {
const index = (event.currentTarget);
const buttonId = index.id?.replace('remove-', '');
const currentTask = todoDatabase.filter((object) => object.id === buttonId)[0];
const currentTaskId = currentTask.id;
console.log(buttonId);
console.log(currentTaskId);
if (buttonId == currentTaskId) {
todoDatabase.splice(0, 1);
printTodoList();
}
}
I haven't started the code for the checkbox function yet.
Very grateful for any tips I can get.
Thanks for all the replies, this pointed me in the right direction and I got it working as intended, the following code was the result:
document.querySelectorAll('.add-click-listener').forEach(item => {
item.addEventListener('click', (event: any) => {
const thisWasClicked = event.currentTarget;
let trashBtn = Array.from(document.querySelectorAll('.trash-btn'));
const filterTrashBtn: any = trashBtn.find((btn) => btn.id === thisWasClicked.id);
const findTask = trashBtn.indexOf(filterTrashBtn);
if (filterTrashBtn?.id == thisWasClicked.id) {
todoDatabase.splice(findTask, 1);
printTodoList();
}
});
});
Now I can just write the code for my checkboxes with another if and some variables etc.
From my experience, you can avoid repeating code by using an event delegation pattern. You have to attach an event listener to the parent element, and then inside the handler to check if it matches your child element. The event by clicking on child will bubble so you will catch it. In code it will look like this:
document.querySelector('#parent-element').addEventListener('click', function(e) {
if (e.target && e.target.matches('.child-element')) {
// do your stuff here
}
});
You can bind to the root element where the checkboxes are located and handle the target element that fired the event.
If the HTML looks like this:
<div id="app">
<div class="checkboxes">
<input id="1" type="checkbox" /><label for="1">Checkbox 1</label>
<input id="2" type="checkbox" /><label for="2">Checkbox 2</label>
<input id="3" type="checkbox" /><label for="3">Checkbox 3</label>
</div>
</div>
An example JS code could be as shown below:
const root = document.getElementsByClassName('checkboxes')[0];
root.addEventListener('click', function (event) {
const target = event.target;
if (target.tagName == 'INPUT') {
console.log(target.id);
console.log(target.checked);
}
});
Bind a click event to the root div with the class name "checkboxes" and handle who exactly fires the event.
If you can use Jquery then the same can be done with on function
https://api.jquery.com/on
$(div.checkboxes).on("click", ".checkbox", function(event){})

React state value is not retained when value is set using useState within useEffect with dep []

I am using Fluent UI Details List and trying to make row editable on icon click.
My code is as below. On first run, it shows grid with empty data. Then it goes to useEffect (I tried both useEffect and useLayoutEffect, same behaviour) and data is fetched and stored in state. It also fires the render and the grid shows all the rows per the data. All good so far.
When row > cell 1 is double-clicked on the grid, I turned the row to editable mode. That is also working.
For each editable column, I have a different onChange event attached. So, when any input text box/dropdown cell value changes, it fires the respective onChange event callback function.
Within this cell change callback event, it gets item id and final changed value as input aurguments. And using them, data array will be updated and stored in state. This is my expectation.
But when I read the current data array from state within the callback function, it is empty.
So, basically, problem is that state value stored from useEffect is not retained.
There is no other code line where data array state is updated. So, no chance that the data array is reset by code.
If anyone has any idea or faced, solved such issue, let me know. Thanks in advance.
Adding few things which I tried,
I tried using the class component and it worked. only difference is that instead of useEffect, I used componentDidMount and instead of useState, I used this.setState. Not sure what is other difference within class and function component?
The same code works in class component but not in function component.
I tried using the same function component and instead of async call within useEffect, I made direct sync fetch call before render and loaded data in state as initial value. Then, it worked.
So, it fails only when the data is fetched async mode within useEffect and set to state from there.
My problem is resolved after converting to class component.
but want to understand what is the issue within my function component code.
/** function component */
const [dataItems, setDataItems] = useState<IScriptStep[]>([]);
const [groups, setGroups] = useState<IGroup[]>([]);
/** Component Did Mount */
useLayoutEffect(() => {
props.telemetry.log(`useEffect - []`, LogLevel.Debug);
(async () => {
let scriptSteps = await props.dataHelper.retrieveScriptSteps();
setDataItems(scriptSteps);
let groups = getGroups(scriptSteps);
setGroups(groups);
props.telemetry.log(`Data updated in state`, LogLevel.Debug);
})();
}, []);
/** Render */
return (
<div className="SubgridMain">
{props.telemetry.log(`render`, LogLevel.Debug)}
<div className="List">
<DetailsList
componentRef={_root}
items={dataItems}
groups={groups}
columns={columns}
ariaLabelForSelectAllCheckbox="Toggle selection for all items"
ariaLabelForSelectionColumn="Toggle selection"
checkButtonAriaLabel="select row"
checkButtonGroupAriaLabel="select section"
onRenderDetailsHeader={_onRenderDetailsHeader}
onRenderRow={_onRenderRow}
groupProps={{ showEmptyGroups: true }}
onRenderItemColumn={_onRenderItemColumn}
onItemInvoked={_onItemInvoked}
compact={false}
/>
</div>
</div>
);
/** on change cell value callback function */
const _onChangeCellName = (entityId : string, fieldName:string, finalValue:string) => {
let currentItems = dataItems;
// create new data array
let toUpdateState: boolean = false;
let newItems = currentItems.map((item) => {
if (item.key === entityId) {
if (item.name !== finalValue) {
toUpdateState = true;
item.name = finalValue ?? null;
}
}
return item;
});
if (toUpdateState) setDataItems(newItems);
};
/** columns configuration is set as below */
let columns : IColumn[] = [
{
key: 'name',
name: 'Name',
fieldName: 'name',
minWidth: 300,
isResizable: true,
onRender: this._onRenderCellName,
},
..
..
]
/** Render Name cell */
private _onRenderCellName(item?: IScriptStep, index?: number, column?: IColumn) {
if (item) {
let stepName = item?.name ?? '';
if (item.isEditable) {
let propsNameTextEditor: ITextEditorWrapperProps = {
entityId: item.key,
fieldName: 'name',
initialText: stepName,
multiline: true,
required: true,
setFinalValue: this._onChangeCellName,
};
return <TextEditorWrapper {...propsNameTextEditor} />;
} else {
return (
<div className="ReadOnly">
<p>{stepName}</p>
</div>
);
}
} else return <></>;
};

React Kendo Treeview scroll to item

I am using React Kendo Treeview UI. I want to try to scroll to the item that is selected in the tree. I found many examples for Javascript and JQuery but none for React version. I couldn't solve this problem by playing around with it.
Items in the tree are of type MyViewTreeModel. I have a selectOntree method that finds a node and set the selected to true. My problem is I want to scroll to that item.
export interface MyViewTreeModel {
text: string,
expanded: boolean,
employeeId : number,
treeId: number,
items?: MyViewTreeModel [],
selected: boolean
}
....
<TreeView
data={myData}
expandIcons={true}
onExpandChange={onExpandChange}
onItemClick={OnItemClick}
aria-multiselectable={false}
aria-label={'text'}
></TreeView>
....
const selectOnTree = (employeeId: number ) => {
let treeItem = recursivelyFindEmployeeInTree(myData[0], employeeId);
treeItem.selected = true;
forceUpdate();
}
}
myData is of type MyViewTreeModel .
One solution I tried : I added ref?: any to my model and tried treeItem.ref.current.focus(); in selectOnTree function, but ref was undefined.
Another solution I tried is adding this property to TreeView:
ref={component => treeViewRef.current = component}
Then tried this just to select the first 'li' tag in the TreeView:
if(!_.isNil(treeViewRef.current) ){
let domElement = ReactDOM.findDOMNode(treeViewRef.current);
let treeItemDom = domElement.firstChild.firstChild;
(treeItemDom as HTMLElement).focus();
}
This didn't work, it doesn't put the focus at that point.
I am thinking maybe I should define a custom itemRender that has a ref that I can find the offsetTop of it, but then there are more than one item, how can I create a different ref for each one? Or maybe a custom ItemRender that renders an input (with css I can make it look like a span) and then set autofocus to true if selected is true. Not sure if autofocus true make it scroll to that item.
This is the solution I could find to make it work:
Adding a reference to TreeView
let treeViewRef = useRef(null);
In return statement:
<TreeView
data={myData}
expandIcons={true}
onExpandChange={onExpandChange}
onItemClick={OnItemClick}
aria-multiselectable={false}
aria-label={'text'}
ref={component => treeViewRef.current = component}></TreeView>
2.Defined this function to scroll to a specific treeItem:
'k-in' is the className for each span that represent each item in the Kendo Treeview UI component.
const scrollToTreeViewItem = (treeItem: MyViewTreeModel ) => {
if(!_.isNil(treeViewRef.current)){
let domElement = ReactDOM.findDOMNode(treeViewRef.current);
let treeItemDoms = (domElement as Element).querySelectorAll('.k-in');
let domArray = [];
treeItemDoms.forEach((node) => {
domArray.push(node as HTMLElement);
});
let targetedDomElement = domArray.find((item) => {
return item.innerText === treeItem.text;
});
targetedDomElement.scrollIntoView();
}
}

React form validation still adds values

So I have a little bit of form validation going on and I am running into an issue. When I first load the web app up and try adding a value and submitting with my button it doesn't allow me and gives me the error I want to see. However, when I add a value setState occurs and then my value is pushed to UI and I try to add another blank value it works and my conditional logic of checking for an empty string before doesn't not go through what am I doing wrong?
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
}, () => {
document.getElementById('test').value = '';
})
console.log(this.state.id);
}
}
You are checking this.state.input but no where in that code are you setting the input value on the state.
Try adding this where it makes sense in your application:
this.setState({ input: 'some value' });
Also, I recommend you use the state to define the application UI. So instead of using document.getElementById('error') or document.getElementById('test').value, have the UI reflect what you have in your state.
See here for more info: https://reactjs.org/docs/forms.html
Instead of manipulating the DOM directly:
document.getElementById('test').value = '';
you'll want to use React:
this.setState({ input: '' });
A good ground rule for React is to not manipulate the DOM directly through calls like element.value = value or element.style.color = 'red'. This is what React (& setState) is for. Read more about this on reactjs.org.
Before you look for the solution of your issue, I noticed that you are directly updating the DOM
Examples
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
document.getElementById('test').value = '';
Unless you have special use case or dealing with external plugins this isn't recommended, when dealing with React you should update using the virtual DOM. https://www.codecademy.com/articles/react-virtual-dom
Pseudo code sample
constructor(props) {
this.state = {
// retain previous states in here removed for example simplicity
errorString: ''
}
}
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
this.setState({
errorString: 'Please enter something first'
});
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
input: '',
});
}
}
// notice the "error" and "test" id this could be omitted I just added this for your reference since you mentioned those in your example.
render() {
return (
<div>
{(this.state.errorString !== '') ? <div id="error" style={{color: 'red'}}>{this.state.errorString}</div> : null}
<input id="test" value={this.state.input} />
</div>
}
Every time you invoke setState React will call render with the updated state this is the summary of what is happening but there are lot of things going behind setState including the involvement of Virtual DOM.

Get element sibling value in React

I have this method inside a React component (which I later pass to the render() method):
renderNutrientInputs: function (num) {
var inputs = [];
for (var i =0; i < num; i++) {
inputs.push(<div key={i}>
<label>Nutrient name: </label><input type="text"/>
<label>Nutrient value: </label><input type="text" />
</div>);
}
return inputs;
}
I'm trying on each change of the "Nutrient value" textbox, to also grab the current value of the "Nutrient name" textbox. I first though of assigning "ref" to both of them, but I figured there might be multiple pairs of them on the page (and the only way to identify them would be by key). I also tried something like this:
<label>Nutrient name: </label><input type="text" ref="nutName"/>
<label>Nutrient value: </label><input type="text" onChange={this.handleNutrientValueChange.bind(null, ReactDOM.findDOMNode(this.refs.nutName))}/>
but got a warning from React:
Warning: AddForm is accessing getDOMNode or findDOMNode inside its
render(). render() should be a pure function of props and state. It
should never access something that requires stale data from the
previous render
Is there some way to attach onChange event listener to Nutrient value text box and access the current value of "Nutrient name" textbox in the event listener function?
You don't want to access DOM elements directly. There is no need to do so... Work with your data, forget about DOM!
What you want is to "listen to changes to n-th nutritient. I want to know it's name and it's value". You will need to store that data somewhere, let's say in state in this example.
Implement getInitialState method. Let's begin with empty array, let user to add nutritients.
getInitialState() {
return { nutritients: [] };
},
In render method, let user add nutrition by click on "+", let's say
addNutritient() {
const nutritients = this.state.nutritients.concat();
nutritients.push({ name: "", value: undefined });
this.setState({ nutritients });
},
render() {
return (
<div>
<div onClick={this.addNutritient}>+</div>
</div>
)
}
Okay, let's focus on rendering and updating nutritients:
addNutritient() {
const nutritients = this.state.nutritients.concat();
nutritients.push({ name: "", value: undefined });
this.setState({ nutritients });
},
renderNutritients() {
const linkNutritient = (idx, prop) => {
return {
value: this.state.nutritients[idx][prop],
requestChange: (value) {
const nutritients = this.state.nutritients.concat();
nutritients[idx][prop] = value;
this.setState({ nutritients });
},
}
};
const nutritients = [];
return (
this.state.nutritients.map((props, idx) => (
<div>
<input valueLink={linkNutritient(idx, "name")} />
<input valueLink={linkNutritient(idx, "value")} />
</div>
))
)
},
render() {
return (
<div>
{ this.renderNutritients() }
<div onClick={this.addNutritient}>+</div>
</div>
)
}
Coding by hand, sorry for syntax error or typings.
Edit:
Take a look at this working Fiddle https://jsfiddle.net/Lfrk2932/
Play with it, it will help you to understand what's going on.
Also, take a look at React docs, especialy "valueLink" https://facebook.github.io/react/docs/two-way-binding-helpers.html#reactlink-without-linkedstatemixin
I prefer not to use 2 way binding with React which is kind of a flux anti-pattern. Just add a onChange listener to your input element and setState.
Your state will be something like this:
{0: {nutrientName: xyz, nutrientValue: 123},
1: {nutrientName: abc, nutrientValue: 456}}
So when you change the nutrientvalue 456 to say 654, you can say its corresponding name is abc and vice versa.
The whole thing about React is about handling the data not the DOM :)

Resources