Best way to avoid repeating code when using addEventListener and functions - arrays

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){})

Related

Flickity cell selector in React

Basically I have a dropdown in which each option has an attribute corresponding to the ID of an image. My goal is to go to that image when an option is chosen. For that I am trying to use:
const myCustomNext = () => {
flkty.selectCell(somevar)
};
somevar is initially set to #someid, when I click my button, it goes to the cell with that ID perfectly.
The issue starts once I update the value of somevar. As soon as I do and then click the button, I get the following Error:
"Cannot read property 'selectCell' of null"
I logged both the initital somevar and the updated one. Other than the ID itself, they are absolutely identical so I have no clue where I am going wrong. I tried switchen the static and reloadOnUpdate settings but that didn't help.
Here a more complete example that might show better what I am trying to do:
const FlickTest = () => {
const [varimg, setVarimg] = useState("#cG9zdDoxNA");
let flkty = null;
function setVariant(e) {
let index = e.target.selectedIndex;
let optionElement = e.target.childNodes[index]
let option = optionElement.getAttribute('data-imgid');
setVarimg(`#${option}`);
}
const myCustomNext = () => {
flkty.selectCell(varimg)
};
return (
<>
<button onClick={myCustomNext}>My custom next button</button>
<select onChange={setVariant}>
{variants.map((variant) =>
<option data-imgid={variant.gallerie[0].id} value={variant.farbe} key={variant.farbe}>{variant.farbe}</option>
)}
</select>
<Flickity
flickityRef={c => (flkty = c)}
className={'carousel'}
elementType={'div'}
options={flickityOptions}
disableImagesLoaded={true}
reloadOnUpdate={true}
static={true}
>
{variants.map((galitem) =>
galitem.gallerie.map((galimg) =>
<div key={galimg.id} id={galimg.id.replace(/[^\w\s]/gi, '')}>
<span>{galimg.id}</span>
<Image fluid={galimg.localFile.childImageSharp.fluid} />
</div>
)
)}
</Flickity>
</>
)
}
Any ideas or pointers would be much appreciated :)
Switched from a dropdown to buttons just to simplify the whole thing and see where it goes wrong. Seems like flickity only accepts the value directly but not from state or any other variable.
This works:
const selectSlide = (e) => {
flkty.selectCell( `.${e.target.getAttribute("data-selector")}` )
};
...
<button onClick={selectSlide} data-selector={variant.gallerie[0].id} key={variant.farbe}>{variant.farbe}</button>
If anybody knows if this is a flickity thing or (more likely) I was doing something completely wrong I'd still appreciate some pointers so I know better next time :)

How do I autosubmit text after entry?

I was wondering what's the best approach for handling an autosubmit of a code that a user types in? I have this so far but idk if this works. I tried testing it but I wasn't performing as I expected.
This is the form control:
<div className="form-group">
<input
type="tel"
autoComplete="one-time-code"
maxLength="6"
className="code-form"
onChange={this.changeCode}
value={code}
/>
</div>
And this is the onChange event:
changeCode = e => {
this.setState({ code: e.target.value });
if(this.verificationCode.length == 6) {
this.verifyCode();
}
};
If the length of what the user types in is 6, then I just want to submit it. Is this right approach?
The following code is inside a webview react project that is housed inside of react-native app.
There is no need to use debouncing for such small task,just check lenght of code in setState's call back and submit.
changeCode = e => {
this.setState({ code: e.target.value },()=>{
if(this.state.code.length == 6) {
this.verifyCode();
}
});
};
You can try with throttle and debounce:
if (this.verificationCode.length < 5) {
throttle(500, this.verifyCode)
} else {
debounce(500, this.verifyCode);
}

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 :)

How to get multiple selected options value in React JS?

<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"
/>

Check all boxes toggle method not working like expected

I'm wiriting a method to toggle a checklist as checked / unchecked, as follow:
$scope.checkAllToggle = function(dtProvider, destiny, ev) {
$(ev.currentTarget).parent().find('span').html());
data[destiny] = [];
if ($(ev.currentTarget).parent().find('span').html()=='Check all') {
for (var val in data[dtProvider]) data[destiny].push(data[dtProvider][val].id);
$(ev.currentTarget).parent().find('span').html('Uncheck all');
}
else $(ev.currentTarget).parent().find('span').html('Check all');
}
The label of the toggle button changes every time, but the state of the checkboxs only becomes checked after 2 click, from this point change in each 3 clicks.
What's wrong?
Append
if(!$scope.$$phase) $scope.$apply();
to the method make no difference, as prepending $scope to data
Code in plunker: http://plnkr.co/edit/Zf6UzLbC4osRov7IR5Na?p=preview
Rather than do it that way, this would be the easier way to do it.
Make the master input and assign it to a model.
<input type="checkbox" ng-model="master">
Then have your other inputs be tied to the master like this.
<input type="checkbox" ng-model="box1" ng-checked="master">
<input type="checkbox" ng-model="box2" ng-checked="master">
<input type="checkbox" ng-model="box3" ng-checked="master">
Here is your plunker with these changes.
Master Checkbox Plunker
I got your plunker to work by doing this.
$scope.checkToggle = function(dtProvider, destiny, ev) {
$scope.data[destiny] = [];
if ($(ev.currentTarget).parent().find('span').html() == 'Check all') {
for (var val in $scope.data[dtProvider]){
$scope.data[destiny].push($scope.data[dtProvider][val].id);
$(ev.currentTarget).parent().find('span').html('Uncheck all');
}
} else {
$(ev.currentTarget).parent().find('span').html('Check all');
}
$scope.$apply();
};
I could not get it to work if calling safely like if(!$scope.$$phase){$scope.$apply();} but it is working with straight $scope.$apply(). This might have something to do with rootScope and need to be safetly called in a different manner.
See this link: https://coderwall.com/p/ngisma
You also had some missing {} in your code that could have been causing some issues.

Resources