I'm sure this isn't surprising to some out there, but I am getting something very unexpected in the following component:
const GameBoard = ({
moves = [[1,2]]
}:Props) => {
const [sideLength, setSideLength] = useState(3);
let rows = Array(sideLength).fill(Array(sideLength).fill(''));
console.log(rows);
for(let i = 0; i < moves.length; i++){
const [y,x] = moves[i];
const mark:Mark = i % 2 === 0 ? 'X' : 'O';
rows[y][x] = mark;
}
console.log(rows);
return (
<div className="game-board">
{ rows.map(row => <Row row={row}/>)};
</div>
);
};
This component represents a tic tac toe gameboard. A prop called moves should determine where markings are made on the board. In the following line, I start with a blank board:
let rows = Array(sideLength).fill(Array(sideLength).fill(''));
And then I populate the rows with markings based on moves. [[1,2]] means that only the first move has been made (X according to the rules), and it was placed one down and 2 over.
I expect the console.log after let rows... to log:
[ '', '', '']
['', '', '']
['', '', '']
Instead, both this line and the log after the for loop logs the following:
0
:
['', '', 'X']
['', '', 'X']
['', '', 'X']
So my questions are:
why isn't the first log only full of empty strings?
why is an entire column of the second log filled instead of just one square ([2,1])?
https://codesandbox.io/s/muddy-bash-shdpoi?file=/src/App.js
Re. 1: it actually is! The issue you are probably facing is that you are looking at the console in the browser. There, the content of a printed object may change after the printing to the console. If instead of console.log(rows) you use console.log(JSON.stringify(rows)) you'll see that it actually is what you expected (all empty strings).
Re. 2: This is because you are initializing all three top level array element with the same array (it's a reference!). This is even shown in the MDN documentation of the Array.fill method, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill#using_fill:
// A single object, referenced by each slot of the array:
const arr = Array(3).fill({}); // [{}, {}, {}]
arr[0].hi = "hi"; // [{ hi: "hi" }, { hi: "hi" }, { hi: "hi" }]
You'll need to create three different arrays to fill the top level array with.
Related
I have a form, that is filled in for 18 holes on a golf course, each of the holes has the following fields, hole, par, stroke, yards, courseid
As each input field is filled in, it fires onChange and sets the value of a variable in form state, to the input fields value.
In my form object I have all these fields,
const [form, setForm] = useState([
{
hole1: 1,
par1: '',
stroke1: '',
yards1: ''
},
{
hole2: 2,
par2: '',
stroke2: '',
yards2: ''
},
{
hole3: 3,
par3: '',
stroke3: '',
yards3: ''
},
//repeated for 18 holes
When completed and submit is clicked, it fires the save function, A list is sent to the backend (SpringBoot) to be saved in dB.
As there are 18 holes, I need to loop over the data so that I can fill in 18 objects to put into the list to send to the backend.
I have made a holes object
let[holes, setHoles] = useState({
"hole": '',
"par": '',
"stroke": '',
"yards": '',
"course": {
"courseid": ''
}
});
which I now want to populate with the values from the form data.
so I want to set the holes values for each field to that for,
form.hole1,
form.par1,
form.stroke1,
form.yards1
Then add that holes object to the list, then run the loop again and add all the values for hole 2, etc etc until all 18 holes are done.
When using a loop like,
for (let i= 1; i< 19; i++) {
holes = {
"hole": index,
"par": form.par,
"stroke": form.stroke,
"yards": form.yards,
"course": {
"courseid": 3
}
}
const newList = list.concat({holes})
list = newList;
};
how is it best to tell it to take form.par1 on the first loop, then form.par2 on the second loop etc.
I feel like I need two loops running here, so that it starts off looping through all the numbers 1-18, and before moving to the next number it loops through the objects in the form,
so it starts at hole 1, get the holes object, gets the value from the first form object, sets the 4 fields in holeto those in the firstform object i.e par1, yards1 etc, then concats the hole object to the list then moves on to the number 2 and continues this till all 18 are complete, but I am not sure how I can achieve this.
Given the form array shaped as you've described, you can map this to the desired list of holes like so:
const list = form.map((hole, i) => {
const num = i + 1;
return {
hole: num,
par: hole[`par${num}`],
stroke: hole[`stroke${num}`],
yards: hole[`yards${num}`],
course: {
courseid: 3,
},
};
});
In my state I have an object called foodLog which holds all entries a user enters with one of the keys being foodSelectedKey and I'm trying to return all entries that have a matching value from that key with a different array called foodFilter.
However, this doesn't work and errors out saying foodLog.filter() isn't a function - I've looked this up and it's because it's an Object (I think). Any help would be greatly appreciated!
state = {
// log food is for the logged entries
foodLog: {},
// used for when filtering food entries
foodFilter: [],
};
findMatches = () => {
let foodLog = this.state.foodLog;
let foodFilter = this.state.foodFilter;
let matched = foodLog.filter((item) => {
return foodLog.foodsSelectedKey.map((food) => {
return foodFilter.includes(food);
});
});
};
I guess the reason behind the error Is not a function is that the object can not be looped. By that it means you can not iterate an object with differend variables inside, if it has no index to be iterated like an array. The same goes for map(), find() and similar functions which MUST be run with arrays - not objects.
As far as I understand you have an object named foodLog which has an array named foodsSelectedKey. We need to find intersected elements out of foodFilter with the array. This is what I came up with:
state = {
// log food is for the logged entries
foodLog: {
foodsSelectedKey: [
{ id: 1, name: "chicken" },
{ id: 2, name: "mashroom" }
]
},
// used for when filtering food entries
foodFilter: [
{ id: 1, name: "chicken" },
{ id: 2, name: "orange" }
]
};
findMatches = () => {
let foodLog = this.state.foodLog;
let foodFilter = this.state.foodFilter;
let matched = foodLog.foodsSelectedKey.filter((key) =>
{
for (let i=0; i<foodFilter.length;i++){
if(foodFilter[i].name===key.name)
return true
}
return false;
}
);
return matched;
};
The Output is filtered array, in this case, of one element only:
[{
id: 1
name: "chicken"
}]
In order to check the output - run console.log(findMatches()). Here is the CodeSandbox of the solution. (check console at right bottom)
// The global variable
var globalTitle = "Winter Is Coming";
// Only change code below this line
function urlSlug(title) {
return title
.split(" ")
.filter(x => x)
.join("-")
.toLowerCase();
}
// Only change code above this line
console.log(urlSlug(globalTitle))
// Result: winter-is-coming
My doubt is about this line: .filter(x => x), how is it work to filter the whitespaces in the array?
#Thefourthbird answered your question regarding filter. The solution would be better to split by one or more whitespace though .split(/\s{1,}/g), then you don't need the filter.
// The global variable
var globalTitle = "Winter Is Coming";
// Only change code below this line
function urlSlug(title) {
return title
.split(/\s{1,}/g)
.join("-")
.toLowerCase();
}
// Only change code above this line
console.log(urlSlug(globalTitle))
// Result: winter-is-coming
Or even simpler, using replace .replace(/\s{1,}/g, '-'):
// The global variable
var globalTitle = "Winter Is Coming";
// Only change code below this line
function urlSlug(title) {
return title
.replace(/\s{1,}/g, '-')
.toLowerCase();
}
// Only change code above this line
console.log(urlSlug(globalTitle))
// Result: winter-is-coming
You are splitting on a space, which will result in an array looking like this:
[
'Winter',
'',
'',
'',
'Is',
'',
'',
'',
'',
'Coming'
]
Then all the array items are passed to filter, which will try to convert the values to a Boolean. Converting an empty space to a Boolean will return false.
Using filter will keep the element if it returns true, so you will end up with:
[
'Winter',
'Is',
'Coming'
]
Another option to accomplish this is using .filter(Boolean) instead.
I currently have the following state:
this.state = {
selectProduct: [somearrayValues],
quantityProduct: [],
colorsProduct: [somearrayValues],
stockProduct: [somearrayValues],
turnaroundProduct: [],
coatingProduct: [],
attributeProduct: [somearrayValues],
attributeMetaProduct: [somearrayValues],
}
I do a fetch call to fill up the arrays with the needed data.
From here I need to get a count of Arrays that actually contain a value. I'm lost as how to accomplish this.
I first was trying to get to the state with a for each loop but I haven't even got passed this point:
let dropdownArrays = ...this.state;
dropdownArrays.forEach(function(element) {
console.log(element);
});
This gives me an error when babel attempts to compile.
I then tried the below, which returns nothing.
let dropdownArrays = [...this.state];
dropdownArrays.forEach(function(element) {
console.log(element);
});
I know I'm missing it so any help would be greatly appreciated.
Perhaps you could use the Object#values() method to access the array objects (ie the values) of the state, and then count the number of non-empty arrays like so:
// Pre-filled arrays with some values. This solution would work
// regardless of the values you populate the arrays with
const state = {
selectProduct: [1,2,3,4],
quantityProduct: [],
colorsProduct: [4,5,6,7],
stockProduct: [1,2],
turnaroundProduct: [],
coatingProduct: [],
attributeProduct: [6,7,8,9,10],
attributeMetaProduct: [5,4,6],
}
const result = Object.values(state)
.filter((array) => array.length > 0)
.length;
console.log('Number of arrays in state with values (non-empty)', result)
Because state is an object, you instead could use a couple different options. You could do
this.state.values, which will return an array of the values in state.
this.state.values.forEach(function(value) {
console.log(value);
});
Or you could use this.state.entries, which will return an array of the key, value.
this.state.entries.forEach(function(entry) {
console.log(entry);
// expected output [key, value]
});
Lastly as you appear to already be attempting to use destructuring, you can also destructure the result.
this.state.entries.forEach(function([key, value]) {
console.log(key);
console.log(value);
});
you can try with https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
Object.entries(...this.state)[1].reduce((accumulator, currentValue) => accumulator + currentValue.length, 0)
Bear in mind that to use ...this.state you need https://babeljs.io/docs/en/babel-plugin-proposal-object-rest-spread
var state = {
selectProduct: [1,2,3,4],
quantityProduct: [],
colorsProduct: [4,5,6,7],
stockProduct: [1,2],
turnaroundProduct: [],
coatingProduct: [],
attributeProduct: [6,7,8,9,10],
attributeMetaProduct: [5,4,6],
}
// In your code replace state with this.state
let emptyArrays = Object.values(state).reduce(
(acc, current)=> ( !current.length? ++acc: acc),
0
)
console.log(emptyArrays)
I have two arrays that I want to merge together to one array of objects...
The first array is of dates (strings):
let metrodates = [
"2008-01",
"2008-02",
"2008-03",..ect
];
The second array is of numbers:
let figures = [
0,
0.555,
0.293,..ect
]
I want to merge them to make an object like this (so the array items match up by their similar index):
let metrodata = [
{data: 0, date: "2008-01"},
{data: 0.555, date: "2008-02"},
{data: 0.293, date: "2008-03"},..ect
];
So far I do this like so: I create an empty array and then loop through one of the first two arrays to get the index number (the first two arrays are the same length)... But is there an easier way (in ES6)?
let metrodata = [];
for(let index in metrodates){
metrodata.push({data: figures[index], date: metrodates[index]});
}
The easiest way is probably to use map and the index provided to the callback
let metrodates = [
"2008-01",
"2008-02",
"2008-03"
];
let figures = [
0,
0.555,
0.293
];
let output = metrodates.map((date,i) => ({date, data: figures[i]}));
console.log(output);
Another option is to make a generic zip function which collates your two input arrays into a single array. This is usually called a "zip" because it interlaces the inputs like teeth on a zipper.
const zip = ([x,...xs], [y,...ys]) => {
if (x === undefined || y === undefined)
return [];
else
return [[x,y], ...zip(xs, ys)];
}
let metrodates = [
"2008-01",
"2008-02",
"2008-03"
];
let figures = [
0,
0.555,
0.293
];
let output = zip(metrodates, figures).map(([date, data]) => ({date, data}));
console.log(output);
Another option is to make a generic map function which accepts more than one source array. The mapping function will receive one value from each source list. See Racket's map procedure for more examples of its use.
This answer might seem the most complicated but it is also the most versatile because it accepts any number of source array inputs.
const isEmpty = xs => xs.length === 0;
const head = ([x,...xs]) => x;
const tail = ([x,...xs]) => xs;
const map = (f, ...xxs) => {
let loop = (acc, xxs) => {
if (xxs.some(isEmpty))
return acc;
else
return loop([...acc, f(...xxs.map(head))], xxs.map(tail));
};
return loop([], xxs);
}
let metrodates = [
"2008-01",
"2008-02",
"2008-03"
];
let figures = [
0,
0.555,
0.293
];
let output = map(
(date, data) => ({date, data}),
metrodates,
figures
);
console.log(output);
If you use lodash, you can use zipWith + ES6 shorthand propery names + ES6 Arrow functions for a one-liner, otherwise see #noami's answer.
const metrodata = _.zipWith(figures, metrodates, (data, date)=> ({ data, date }));