How to refactor this double forLoop with lodash? - javascript

I have selectedTags which holds up to 3 tags.
vm.tags Could contain thousands, most likely just hundreds of tags that I need to compare too.
If the ids of the 3 tags match the id of a tag inside of vm.tags I need to turn their borders on. There are 3 borders too: border1, border2, border3.
const tagsColorCheck = () => {
let name, selected, its_ticker;
let selectedTags = TagsFactory.retrieveTickerTags('onlyTags');
if (selectedTags.length > 0) {
for (let i=0; i<vm.tags.length; i++) {
for (let j=0; j<selectedTags.length; j++) {
if (selectedTags[j].term_id == vm.tags[i].term_id) {
name = 'border'+ ( j + 1 );
selected = 'selected';
its_ticker = 'its_ticker';
vm.tags[i][name] = true;
vm.tags[i][selected] = true;
vm.tags[i][its_ticker] = selectedTags[j].its_ticker;
}
}
}
}
};
So far here is what I have in process (_.each):
const tagsColorCheck = () => {
let name, selected, its_ticker, vmTerm, term_1, term_2, term_3, ticker_1, ticker_2, ticker_3;
let selectedTags = TagsFactory.retrieveTickerTags('onlyTags');
if (!_.isEmpty(selectedTags)) {
vmTerm = R.findIndex(R.propEq('term_id', selectedTags[0].term_id))(vm.tags);
}
if (selectedTags[0]) { term_1 = parseInt(selectedTags[0].term_id); ticker_1 = selectedTags[0].its_ticker; }
if (selectedTags[1]) { term_2 = parseInt(selectedTags[1].term_id); ticker_2 = selectedTags[1].its_ticker; }
if (selectedTags[2]) { term_3 = parseInt(selectedTags[2].term_id); ticker_3 = selectedTags[2].its_ticker; }
_.each(vm.tags, (tag) => {
if (tag.term_id === term_1) {
tag.selected = true;
tag.border1 = true;
tag.its_ticker = ticker_1;
}
if (tag.term_id === term_2) {
tag.selected = true;
tag.border2 = true;
tag.its_ticker = ticker_2;
}
if (tag.term_id === term_3) {
tag.selected = true;
tag.border3 = true;
tag.its_ticker = ticker_3;
}
})
};
And this (for of loop):
const tagsColorCheck = () => {
let name, selected, its_ticker, vmTerm, term_1, term_2, term_3, ticker_1, ticker_2, ticker_3;
let selectedTags = TagsFactory.retrieveTickerTags('onlyTags');
const borderRizeTag = (tag) => {
if (tag.term_id === term_1) {
tag.selected = true;
tag.border1 = true;
tag.its_ticker = ticker_1;
}
if (tag.term_id === term_2) {
tag.selected = true;
tag.border2 = true;
tag.its_ticker = ticker_2;
}
if (tag.term_id === term_3) {
tag.selected = true;
tag.border3 = true;
tag.its_ticker = ticker_3;
}
return tag;
}
if (!_.isEmpty(selectedTags)) {
vmTerm = R.findIndex(R.propEq('term_id', selectedTags[0].term_id))(vm.tags);
}
if (selectedTags[0]) { term_1 = parseInt(selectedTags[0].term_id); ticker_1 = selectedTags[0].its_ticker; }
if (selectedTags[1]) { term_2 = parseInt(selectedTags[1].term_id); ticker_2 = selectedTags[1].its_ticker; }
if (selectedTags[2]) { term_3 = parseInt(selectedTags[2].term_id); ticker_3 = selectedTags[2].its_ticker; }
for (let tag of vm.tags) {
console.log(tag);
tag = borderRizeTag(tag);
}
console.log('vmTerm',vmTerm);
};

ES6 fiddle to run: http://www.es6fiddle.net/is0prsq9/ (note, copy the entire text, and paste it inside a browser console or a node REPL, and then examine the value of tags to see the result)
It's not lodash, and you don't really need it with ES6 constructs. Relevant code:
const tagsColorCheck = () => {
let tags = TagsFactory.retrieveTickerTags('onlyTags')
sel.forEach( (s,i) =>
tags.filter(t => t.term_id === s.term_id).forEach( t => {
t['border' + (i+1)] = true
t.selected = true
t.its_ticker = s.its_ticker
})
)
return tags
}
If you were writing this in a functional language, you would have access to a list comprehension and it would be a bit cleaner. Essentially, this is a pretty clear case of (for every x in a and y in b) so a list comprehension is what you need, but you don't have it in javascript (mozilla has it, but not useful outside of that realm).
The result is a somewhat functional approach -- however, it can never really be functional in pure javascript. Possibly the most important benefit of the functional paradigm are immutable data structures where you would compose your new list. Instead, you just modify them in place here, which really is not very functional at all. Still, if you prefer the each approach to a literal incremental one, as you have done above and as I did in my post, then it's a (albeit slower but arguably cleaner) better approach.

Figured out an awesome solution! :D using both _lodash and ramda.
So below, immediately each is quicker to reason about, then using R.equals to compare if the term_ids match. Then setting the values of the keys on the correct tag object.
if (!_.isEmpty(selectedTags)) {
_.each(vm.tags, tag => {
_.each(selectedTags, (selectedTag, index) => {
let areTermIDsSame = R.equals;
if (areTermIDsSame(parseInt(selectedTag.term_id), parseInt(tag.term_id))) {
name = 'border'+ ( index + 1 );
selected = 'selected';
its_ticker = 'its_ticker';
tag[name] = true;
tag[selected] = true;
tag[its_ticker] = selectedTag.its_ticker;
}
});
})
}

The idea is simple - create an index of all tags by term_id. Iterate the selected tags. If a tag is found by id in the tags index, mutate it by assigning an object with the new properties.
btw - The only thing lodash is needed for is _.keyBy(), and you can easily do that using Array.prototype.reduce if you don't want to use lodash.
/** mocked vm **/
const vm = {
tags: [{ term_id: 1 }, { term_id: 2 }, { term_id: 3 }, { term_id: 4 }, { term_id: 5 }, { term_id: 6 }]
}
/** mocked TagsFactory **/
const TagsFactory = {
retrieveTickerTags: () => [{ term_id: 1, its_ticker: 'ticker 1' }, { term_id: 4, its_ticker: 'ticker 4' }, { term_id: 5, its_ticker: 'ticker 5' }]
};
const tagsColorCheck = () => {
const selectedTags = TagsFactory.retrieveTickerTags('onlyTags');
if (selectedTags.length === 0) { // if selectedTags is empty exit
return;
}
const vmTagsIndex = _.keyBy(vm.tags, (tag) => tag.term_id); // create an index of tags by term_id
selectedTags.forEach(({
term_id, its_ticker
}, index) => { // loop through selectd tags and retreive term_id and its_ticker from the current selected tag
const tag = vmTagsIndex[term_id]; // find the tag in the vmTagsIndex
if (!tag) { // if the id doesn't exist in vmTagsIndex exit
return;
}
Object.assign(tag, { // mutate the tag by assigining it an object with the available properties
selected: true,
[`border${index + 1}`]: true,
its_ticker
});
});
};
tagsColorCheck();
console.log(vm.tags);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js"></script>

Related

Problem making an object iterable using an arrow function

I am learning JavaScript, I created a class and now I need to make it iterable.
The class Group is a Set of values with methods to add, remove and check the elements of the set.
The part of creating the class was a previous exercise of my textbook, I saw the solution for that part and I tested it, it is correct for sure. My problem is from[Symbol.iterator] on.
Here is the code
class Group {
constructor() {
this.theGroup = [];
}
add( element ) {
if( !this.has(element) ) this.theGroup.push(element);
}
remove( element ) {
this.theGroup = this.theGroup.filter( x => x != element );
}
has( element ) {
return this.theGroup.includes(element);
}
static from( elements ) {
let group = new Group;
for( const x of elements ) {
group.add(x);
}
return group;
}
[Symbol.iterator]() {
let i = 0;
return next = () => {
if( i >= this.theGroup.length ) return { done : true };
else return { value : this.theGroup[i++], done : false };
};
}
}
// the following is here only to show that the code before [Symbol.iterator] works
const group = Group.from([10, 20]);
console.log(group.has(10));
group.add(10);
group.remove(10);
console.log(group.has(10));
group.remove(10);
console.log(group.has(20));
// end of demostration
for (let value of Group.from(["a", "b", "c"])) {
console.log(value);
}
I get the error
return next = () => {
^
ReferenceError: next is not defined
And if I make the arrow function anonymous: return () => { ...code continues then I got this other error
for (let value of Group.from(["a", "b", "c"])) {
^
TypeError: undefined is not a function
I am using an arrow function to avoid changing this.
Someone knows what's going on?
How can I make the class iterable?
You want to return an object with a key of next,.
eg.. { next: () => {}} not return next = () => {}
Fixed example below..
class Group {
constructor() {
this.theGroup = [];
}
add( element ) {
if( !this.has(element) ) this.theGroup.push(element);
}
remove( element ) {
this.theGroup = this.theGroup.filter( x => x != element );
}
has( element ) {
return this.theGroup.includes(element);
}
static from( elements ) {
let group = new Group;
for( const x of elements ) {
group.add(x);
}
return group;
}
[Symbol.iterator]() {
let i = 0;
return {next: () => {
if( i >= this.theGroup.length ) return { done : true };
else return { value : this.theGroup[i++], done : false };
}};
}
}
// the following is here only to show that the code before [Symbol.iterator] works
const group = Group.from([10, 20]);
console.log(group.has(10));
group.add(10);
group.remove(10);
console.log(group.has(10));
group.remove(10);
console.log(group.has(20));
// end of demostration
for (let value of Group.from(["a", "b", "c"])) {
console.log(value);
}

How fix warning "Expected to return a value in arrow function array-callback-return"

This is my code:
form.listPrice.map(list => {
if (list.id === listId) {
form.active = true
listPrice = parseInt(list.price)
if (list.offerPrice) {
listofferPrice = parseInt(list.offerPrice)
} else {
listofferPrice = null
}
}
})
And here:
n.listPrice.map(list => {
if (list.id === listPrice) {
valid = true;
n.active = true;
n.showPrice.price = list.price;
n.showPrice.offerPrice = list.offerPrice;
n.ladder = list.ladder;
}
And this output the same warning:
Expected to return a value in arrow function array-callback-return
You are using .map incorrectly. .map should be used only when you want to construct a new array from an old array, but your code is not doing that - you're only carrying out side-effects - the setting of form.active and listofferPrice.
The first step would be to use forEach or for..of instead, eg:
for (const list of form.listPrice) {
if (list.id === listId) {
form.active = true
listPrice = parseInt(list.price)
if (list.offerPrice) {
listofferPrice = parseInt(list.offerPrice)
} else {
listofferPrice = null
}
}
}
But since it looks like you're trying to find a possible single matching value in the array, .find would be more appropriate:
const found = form.listPrice.find(list => list.id === listId);
if (found) {
form.active = true
listPrice = parseInt(found.price)
if (found.offerPrice) {
listofferPrice = parseInt(found.offerPrice)
} else {
listofferPrice = null
}
}
const found = n.listPrice.find(list => list.id === listPrice);
if (found) {
valid = true;
n.active = true;
n.showPrice.price = found.price;
n.showPrice.offerPrice = found.offerPrice;
n.ladder = found.ladder;
}

How to reduce congnitive complexity from 10 to 6 for a function in JavaScript

I have a trouble reducing the cognitive complexity here for this function.I tried to separate the contents inside the forEach as another function and by calling it in getCars function but failed. Could anyone please help?
const getCars = (cars, config, types) => {
const {
carName
} = types;
const carObject = {};
const carsRange = () => {}
let carRange = carsRange(cars);
Object.entries(cars).forEach(([key, value]) => {
if (key === 'sedan' && value) {
const carRangeVal = value.split(' ');
const year = carRangeVal[1];
const model = carRangeVal[0].substring(1, 2);
carRange = generateCarRange(year, model);
}
if (key === 'suv' && value) {
const carRangeVal = value.split(' ');
const year = carRangeVal[1];
const model = carRangeVal[0];
carObject['model'] = true;
carRange = checkYear(year, model);
}
if (value) {
carObject[key] = value;
}
});
if (
config.header === 'TEST A' ||
config.header === 'TEST B'
) {
carObject['carName'] = carName[0].id;
}
carObject['configName'] = config.header;
carObject['contractStartDate'] = carsRange[0];
carObject['contractEndDate'] = carsRange[1];
return carObject;
};
console.log(getCars({}, {}, {}));
You could shorten the code in the .forEach function:
[year,model] = value.split(' '); // destructuring assignment
if (key === 'sedan' && value) {
model = model.substring(1, 2);
carRange = generateCarRange(year, model);
}
if (key === 'suv' && value) {
carObject['model'] = true;
carRange = checkYear(year, model);
}
It would be helpful to know the input data.
I have an inkling that you may be better off using .map() instead of .forEach().
Why do you have two functions (generateCarRange() and checkYear()) to get carRange?
(I find it helpful to use Hungarian Notation to always know the type of my variables.)

Multiple elements -> one function while the elements do not colide with each other

I have multiple selectiontags on different picture slides but same page. Each slide has a set of selectiontags and I want users to only choose 1 selectiontag. I have written the code to do this but I wonder if there is another way.
So, basically I want:
Slide1 w. selectiontags1: Choose 1 selectiontag (out of 4)
Slide2 w.selectiontags2: Choose 1 selectiontag
Slide3 w. selectiontags3: Choose 1 selectiontag
Slide4 w. selectiontags4: Choose 1 selectiontag
This is my code so far.
var prevSelectedValue = null;
var prevSelectedValue2 = null;
var prevSelectedValue3 = null;
var prevSelectedValue4 = null;
$w.onReady(function () {
//TODO: write your page related code here...
let tags = $w('#selectionTags1');
if (tags.value.length === 1) {
prevSelectedValue = tags.value[0];
} else if (tags.value.length > 1) {
tags.value = [];
}
let tags2 = $w('#selectionTags2');
if (tags2.value.length === 1) {
prevSelectedValue2 = tags2.value[0];
} else if (tags2.value.length > 1) {
tags2.value = [];
}
let tags3 = $w('#selectionTags3');
if (tags3.value.length === 1) {
prevSelectedValue3 = tags3.value[0];
} else if (tags3.value.length > 1) {
tags3.value = [];
}
let tags4 = $w('#selectionTags4');
if (tags4.value.length === 1) {
prevSelectedValue4 = tags4.value[0];
} else if (tags4.value.length > 1) {
tags4.value = [];
}
});
export function selectionTags1_change(event) {
//Add your code for this event here:
if (!event.target.value || event.target.value.length === 0) {
event.target.value = [prevSelectedValue];
} else {
event.target.value = event.target.value.filter(x => x !== prevSelectedValue);
prevSelectedValue = event.target.value[0];
}
}
export function selectionTags2_change(event) {
//Add your code for this event here:
if (!event.target.value || event.target.value.length === 0) {
event.target.value = [prevSelectedValue2];
} else {
event.target.value = event.target.value.filter(x => x !== prevSelectedValue2);
prevSelectedValue2 = event.target.value[0];
}
}
export function selectionTags3_change(event) {
//Add your code for this event here:
if (!event.target.value || event.target.value.length === 0) {
event.target.value = [prevSelectedValue3];
} else {
event.target.value = event.target.value.filter(x => x !== prevSelectedValue3);
prevSelectedValue3 = event.target.value[0];
}
}
export function selectionTags4_change(event) {
//Add your code for this event here:
if (!event.target.value || event.target.value.length === 0) {
event.target.value = [prevSelectedValue4];
} else {
event.target.value = event.target.value.filter(x => x !== prevSelectedValue4);
prevSelectedValue4 = event.target.value[0];
}
}
Just a couple notes to help clean up the code:
It is better to use event.target.value[event.target.value.length-1] instead of .filter(). While it doesn't make a huge performance difference since our array is so small, we know that the most recently selected item is the last in the array, so using length-1 is more efficient code.
It is better to manipulate the selector and not the event, so I would recommend replacing event.target.value = event.target.value.filter... with $w('#selectionTags2') = event.target.value[..... This will also make your code more readable as you know which element is being updated.
If you are not preloading anything into the $w('#selectionTags') value array, I am not sure the code in your onReady function is necessary (unless I am missing a step where you do populate that selection value array).

How do I iterate through all the id's?

Check out the api --> https://api.icndb.com/jokes/random/10
Everytime when the user clicks on a specific joke, it will be added to the favorite list.
To keep the code concise I will only show the function itself:
(function() {
"use strict";
const getJokesButton = document.getElementById('getData');
getJokesButton.addEventListener('click', getData);
loadLocalStorage();
function loadLocalStorage() {
let storage = JSON.parse(localStorage.getItem('favoList')) || [];
let listOfFavorites = document.getElementById("favorites");
let emptyArray = '';
if(storage.length > 0) {
for(let i = 0; i < storage.length; i++) {
let idNumberJoke = storage[i].id;
emptyArray +=
`<li><input type="checkbox" id='${idNumberJoke}'/> User title: ${storage[i].joke}</li>`;
listOfFavorites.innerHTML = emptyArray;
}
} else {
return false;
}
}
// fetch data from api
function getData() {
let listOfJokes = document.getElementById("list-of-jokes");
fetch('https://api.icndb.com/jokes/random/10')
.then(function(res) {
return res.json();
}).then(function(data) {
// variable is undefined because it is not initialized. Therefore at some empty single quotes
let result = '';
console.log(data.value);
data.value.forEach((joke) => {
result +=
`<li><input type="checkbox" class='inputCheckbox' id='${joke.id}'/> User title : ${joke.joke}</li>`;
listOfJokes.innerHTML = result;
});
bindCheckbox();
}).catch(function(err) {
console.log(err);
});
}
function clickedButton() {
getJokesButton.setAttribute('disabled', 'disabled');
getJokesButton.classList.add('opacity');
}
function bindCheckbox() {
let inputCheckbox = document.querySelectorAll('input[type=checkbox]');
let elems = document.getElementById('list-of-jokes').childNodes;
let favoriteList = document.getElementById('favorites');
let fav = JSON.parse(localStorage.getItem('favoList'))|| [];
if(elems.length > 0) {
inputCheckbox.forEach(function(element, index) {
inputCheckbox[index].addEventListener('change', function() {
let joke = this;
if(joke.checked && joke.parentNode.parentNode.id === 'list-of-jokes') {
joke.checked = false;
favoriteList.appendChild(joke.parentNode);
addFavorite(joke.id, joke.parentNode.innerText, fav);
}
if(joke.checked && joke.parentNode.parentNode.id === 'favorites') {
joke.checked = false;
removeFavorite(joke, index);
}
});
});
}
clickedButton();
}
function removeFavorite(favorite, index) {
let favoriteCheckBox = favorite;
let i = index;
// convert iterable object to an array, otherwise splice method would give an error.
let favoriteListItem = Array.from(favoriteCheckBox.parentNode);
favoriteListItem.splice(i, 1);
document.getElementById('list-of-jokes').appendChild(favorite.parentNode);
localStorage.setItem('favoList', JSON.stringify(favoriteListItem));
}
// store favorites in localStorage
function addFavorite(jokeId, jokeText, fav) {
let norrisJoke = {
id: jokeId,
joke: jokeText
};
let favorites = fav;
for (let i = 0; i < favorites.length; i++) {
if(favorites[i].id !== norrisJoke.id) {
favorites.push(norrisJoke);
}
}
// favorites[i].id !== norrisJoke.id
// always get the object before the push method and pass it into stringify
localStorage.setItem('favoList', JSON.stringify(favorites));
}
// function which will randomly add one joke to favorite list every 5 seconds
// function need a button which allows you to turn on and off this auto add function
})();
<div class="inner-body">
<button id="getData">GET Jokes</button>
<div class='inner-block'>
<h2>Chuck Norris Jokes</h2>
<ul class='unordered-list' id="list-of-jokes">
</ul>
</div>
<div class='inner-block'>
<h2>Favorites</h2>
<ul class='unordered-list' id="favorites">
</ul>
</div>
</div>
The keys and values would not be pushed into localStorage, the only thing I see is an empty [] in localStorage. The norrisJoke object literal will be dynamically changed. So how could I make this function works?
Too complex, but click on the link below and scroll down to the bottom:
https://codepen.io/chichichi/pen/Gyzzvb
You are trying to run through an empty list here
for (let i = 0; i < favorites.length; i++) {
if(favorites[i].id !== norrisJoke.id) {
favorites.push(norrisJoke);
}
}
This means that nothing will ever be pushed. You can reduce your list to an array of id, then check if the joke exists in the list.
const favIds = favorites.reduce((sum, element) => {
return sum.concat(element.id);
},
[]);
Now you can check if the joke doesn't exists in favorites
if(!favIds.includes(jokeId)){
favorites.push(norrisJoke);
}
The problem is the for loop, the first time it's executed favorites will be an empty array so it's length will be 0, so it will never enter the loop
Something like this should work:
favorites = favorites.filter(joke => joke.id !== norrisJoke.id).concat(norrisJoke);
let favorites = JSON.parse(localStorage.getItem('favoList'))|| {};
favorites[norrisJoke.id] =norrisJoke.joke
Why don't you use a map in place of an array?
Also as #fl9 points out your for loop will never start off! because favorites.length is 0 to begin with
But I want to check duplicates before the joke will be pushed into favorite list
By definition a hash will not allow duplicate entries, so no need to worry about duplications
Run localStorage.getItem('favoList') in the console of this fiddle :
(function() {
"use strict";
const getJokesButton = document.getElementById('getData');
getJokesButton.addEventListener('click', getData);
loadLocalStorage();
function loadLocalStorage() {
let storage = JSON.parse(localStorage.getItem('favoList')) || [];
let listOfFavorites = document.getElementById("favorites");
let emptyArray = '';
if(storage.length > 0) {
for(let i = 0; i < storage.length; i++) {
let idNumberJoke = storage[i].id;
emptyArray +=
`<li><input type="checkbox" id='${idNumberJoke}'/> User title: ${storage[i].joke}</li>`;
listOfFavorites.innerHTML = emptyArray;
}
} else {
return false;
}
}
// fetch data from api
function getData() {
let listOfJokes = document.getElementById("list-of-jokes");
fetch('https://api.icndb.com/jokes/random/10')
.then(function(res) {
return res.json();
}).then(function(data) {
// variable is undefined because it is not initialized. Therefore at some empty single quotes
let result = '';
console.log(data.value);
data.value.forEach((joke) => {
result +=
`<li><input type="checkbox" class='inputCheckbox' id='${joke.id}'/> User title : ${joke.joke}</li>`;
listOfJokes.innerHTML = result;
});
bindCheckbox();
}).catch(function(err) {
console.log(err);
});
}
function clickedButton() {
getJokesButton.setAttribute('disabled', 'disabled');
getJokesButton.classList.add('opacity');
}
function bindCheckbox() {
let inputCheckbox = document.querySelectorAll('input[type=checkbox]');
let elems = document.getElementById('list-of-jokes').childNodes;
let favoriteList = document.getElementById('favorites');
let fav = JSON.parse(localStorage.getItem('favoList'))|| [];
if(elems.length > 0) {
inputCheckbox.forEach(function(element, index) {
inputCheckbox[index].addEventListener('change', function() {
let joke = this;
if(joke.checked && joke.parentNode.parentNode.id === 'list-of-jokes') {
joke.checked = false;
favoriteList.appendChild(joke.parentNode);
addFavorite(joke.id, joke.parentNode.innerText, fav);
}
if(joke.checked && joke.parentNode.parentNode.id === 'favorites') {
joke.checked = false;
removeFavorite(joke, index);
}
});
});
}
clickedButton();
}
function removeFavorite(favorite, index) {
let favoriteCheckBox = favorite;
let i = index;
// convert iterable object to an array, otherwise splice method would give an error.
let favoriteListItem = Array.from(favoriteCheckBox.parentNode);
favoriteListItem.splice(i, 1);
document.getElementById('list-of-jokes').appendChild(favorite.parentNode);
localStorage.setItem('favoList', JSON.stringify(favoriteListItem));
}
// store favorites in localStorage
function addFavorite(jokeId, jokeText, fav) {
let norrisJoke = {
id: jokeId,
joke: jokeText
};
let favorites = fav;
for (let i = 0; i < favorites.length; i++) {
if(favorites[i].id !== norrisJoke.id) {
favorites.push(norrisJoke);
}
}
// favorites[i].id !== norrisJoke.id
// always get the object before the push method and pass it into stringify
localStorage.setItem('favoList', JSON.stringify(favorites));
}
// function which will randomly add one joke to favorite list every 5 seconds
// function need a button which allows you to turn on and off this auto add function
})();

Categories

Resources