How to number shuffled elements in array? - javascript

In my application, I randomized the sequence of specific views. However, I have a problem with displaying the correct sequence number of view.
So, the first view should get number 1, second view number 2, etc.. Since they get shuffled in an array, I have no idea how to access the order number of the view beforehand. The correct number should then get passed as a prop to the specific view component in order to display it.
shuffle(arr) {
var i, j, temp;
for (i = arr.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
};
constructor() {
const componentArray = [<Tasks incrementSequenceCounter={this.incrementSequenceCounter} count={this.state.count} />, <APM incrementSequenceCounter={this.incrementSequenceCounter} count={this.state.count} />, <ICAA incrementSequenceCounter={this.incrementSequenceCounter} count={this.state.count} />]
const shuffledArray = this.shuffle(componentArray);
this.state.shuffledArray = shuffledArray;
}
Somehow the component should be aware of its index in the array, but I don't know where to start.

Firstly I don't like the idea of initialising components in the constructor. That basically makes them static, but that may be what you're after. Here's my attempt:
constructor() {
const componentArray = [
{
type: Tasks,
props: {
incrementSequenceCounter: this.incrementSequenceCounter,
count: this.state.count
}
},
{
type: APM,
props: {
incrementSequenceCounter: this.incrementSequenceCounter,
count: this.state.count
}
},
{
type: ICAA,
props: {
incrementSequenceCounter: this.incrementSequenceCounter,
count: this.state.count
}
}
const shuffledArray = this.shuffle(componentArray);
this.state.shuffledArray = shuffledArray.map(
(componentConstructor, index) => React.createElement(componentConstructor.type, { ...componentConstructor.props, index })
);
}
The basic idea is to construct them after you've determined the order. The obvious disadvantage here is that since this is happening in the constructor any state changes in the parent component are not reflected in these children components. If you don't want that then this should be moved in render and/or componentDidMount/Update
Note: Based on other answers I need to clarify that in my understanding the question is how to pass the index the component ends up in after shuffling to the component itself. This differs from how others have answered so if I am understanding it incorrectly let me know

Your array's element should be wrapped in an object before shuffling:
const componentArray = [
{
index: 0,
component: <MyComponent />
}
];
You could create it from your array with a simple map():
const indexArray = componentArray.map((el, i) => ({ index: i, component: el });
Then you can safely shuffle your indexArray

Related

Javascript - Add Item in the middle of an array inside map function

I'm trying to add an item in a specific index inside an array inside a map function and it's been behaving unexpectedly. Here's the code for it
const addItemToLevelTwoArray= (uniqueID, arrayID )=> {
const reportObject = {
id:arrayID,
title:'',
}
data.map(section=>{
section.content.map((report, reportIndex)=>{
if(report.id===uniqueID){
section.content.splice(reportIndex, 0, reportObject);
}
return report;
})
return section;
})
}
Here's a working pen - https://codepen.io/raufabr/pen/vYZYgOV?editors=0011
Expected behaviour is that it would insert an object in the specific index, right above the object where the ID matches.
However, it's acting weirdly and sometimes I'm getting 2 items being added instead of one.
Any tip on what I'm doing would be massively appreciated! I know I'm close but I've been stuck on this for a while now and can't figure out what I'm doing wrong!
Preface: You're using map incorrectly. If you're not using the array that map builds and returns, there's no reason to use it; just use a loop or forEach. More in my post here. And one reason to use an old-fashioned for loop is that you're in control of iteration, which matters because...
However, it's acting weirdly and sometimes I'm getting 2 items being added instead of one.
That's because you're inserting into the array being looped by the map, so on the next pass, it picks up the entry you're adding.
If you do a simple loop, you can easily avoid that by incrementing the index when you insert, or by looping backward; here's the looping backward approach:
const addItemToLevelTwoArray = (uniqueID, arrayID) => {
const reportObject = {
id: arrayID,
title: "",
};
for (const section of data) {
for (let reportIndex = section.content.length - 1; reportIndex >= 0; --reportIndex) {
const report = section.content[reportIndex];
if (report.id === uniqueID) {
section.content.splice(reportIndex, 0, reportObject);
}
}
}
};
Because we're looping backward, we won't pick up the entry we just added on the next pass.
Since the outer loop doesn't have that problem, I used the more convenient for-of.
Since you asked about map, if you do use the array map returns, you can do this by returning an array with the two entries, and then calling flat on the array map builds. (This only works if the array doesn't already contain arrays, because they'll get flattened to.) This is common enough that it's combined in one function: flatMap. It's not what I'd do (I'd do a loop), but it's certainly feasible. Sticking with forEach and flatMap rather than using for-of and for:
const addItemToLevelTwoArray = (uniqueID, arrayID) => {
const reportObject = {
id: arrayID,
title: "",
}
data.forEach(section => {
section.content = section.content.flatMap(report => {
if (report.id === uniqueID) {
// Return the new one and the old one
return [reportObject, report];
}
// Return just the old one
return report;
});
});
};
That assumes it's okay to modify the section object. If it isn't, Alberto Sinigaglia's answer shows creating a new replacement object instead, which is handy in some sitautions.
You can just use flatMap:
const data = [
{
content: [
{
id: 1,
title: "a"
},{
id: 3,
title: "c"
},
]
}
]
const addItemToLevelTwoArray= (uniqueID, arrayID )=> {
const reportObject = {
id:arrayID,
title:'',
}
return data.map(section=> {
return {
...section,
content: section.content.flatMap( report =>
report.id === uniqueID
? [reportObject, report]
: report
)
}
}
)
}
console.log(addItemToLevelTwoArray(3, 2))
The following will extend the inner array .contentwithout modifying the original array data:
const data = [ {id: 0,title:'main',content:[{id:1,title:'Hello'},
{id:2,title:"World"}] } ];
const addItemToLevelTwoArray= (uniqueID, arrayID )=> {
const reportObject = {
id:arrayID,
title:'something new!',
}
return data.map(d=>(
{...d, content:d.content.reduce((acc, rep)=>{
if(rep.id===uniqueID) acc.push(reportObject);
acc.push(rep)
return acc;
},[]) // end of .reduce()
})); // end of .map()
}
const res=addItemToLevelTwoArray(1,123);
console.log(res);

How to prevent duplicate data from for loop

I am wondering what the best way is to prevent duplicate data from getting into a new array. I have a service call that returns the same array 3 times. I'm trying to take a number from inside the objects in the array and add them up to create a "total" number (fullRentAmt), but since the array gets returned 3 times I'm getting the total*3. I am thinking maybe .some() or .filter() could be of use here but I've never used those/am not sure how that would be implemented here. Thanks for any help!
What I tried, but it's not working/the new array isn't getting populated:
Component
properties = [];
fullRentAmt: number = 0;
const propertyDataSub = this.mainService.requestPropertyData()
.subscribe((pData: PropertyData[]) => {
if (pData) {
const propertyData = pData;
for (let i = 0; i < propertyData.length; i++) {
if (this.properties[i].propertyId !== propertyData[i].propertyId) {
this.properties.push(propertyData[i]);
}
}
for (let i = 0; i < this.properties.length; i++) {
this.fullRentAmt += this.properties[i].tenancyInformation[0].rentAmount;
}
});
Data returned from backend (array of 2 objects):
[
{
"tenantsData":[
{
"email":null,
"tenantNames":null,
"propertyId":2481,
}
],
"tenancyInformation":[
{
"id":2487,
"rentAmount":1000,
}
],
},
{
"tenantsData":[
{
"email":null,
"tenantNames":null,
"propertyId":3271,
}
],
"tenancyInformation":[
{
"id":3277,
"rentAmount":1200,
}
],
},
I'm not an angular developer, but I hope my answer will help you.
let the for loop duplicate the data as much as it wants. you just have to change the idea of storing the stuff from an array to a JavaScript Set
basically, it's very similar to arrays they're lists and iteratables that are very similar to arrays, the only difference is that they don't allow duplication,
usage:
const properties = new Set()
properties.add("yellow")
properties.add("blue")
properties.add("orange")
console.log(properties) // yellow, blue, orange
properties.add("blue")
properties.add("blue")
properties.add("blue")
console.log(properties) // yellow, blue, orange
after your for loop finishes, you may want to convert this set into a normal array, all you have to do is to use destructuring:
const propertiesArray = [...properties]
#YaYa is correct. I added this to show the correct code in Angular
properties = [];
fullRentAmt: number = 0;
const propertyDataSub = this.mainService.requestPropertyData()
.subscribe((pData: PropertyData[]) => {
if (pData && pData.length) {
let arrSet = new Set()
const propertyData = pData;
for (let i = 0; i < propertyData.length; i++) {
if (this.properties[i].propertyId !== propertyData[i].propertyId) {
arrSet.add(propertyData[i])
}
}
this.properties = Array.from(arrSet);
for (let i = 0; i < this.properties.length; i++) {
this.fullRentAmt += this.properties[i].tenancyInformation[0].rentAmount;
}
});
First thing you need to do is to fix your server and return the list once.
If server is out of your reach, you can leverage distinctUntilChanged pipe in combination with isEqual method in the frontend. You can either implement it yourself, or use a library such as lodash.
Also you do not have to subscribe, use async pipe in the template.
this.properties$ = this.mainService.requestPropertyData()
.pipe(
distinctUntilChanged(isEqual) // provide isEqual function somehow
);
this.totalRentAmount$ = properties$.pipe(
map(getTotalRentAmount)
);
// maybe in some other utility file:
export const getTotalRentAmount = (properties: Property[]): number => {
return properties
.map(property => property.tenancyInformation.rentAmount)
.reduce((total, amount) => total + amount, 0);
}
Then in the template:
<div>Total Rent Amount: {{ totalRentAmount | async }}</div>
Also if you really need to subscribe in the component and are only interested in the first emitted value of an observable, you can use first() or take(1) pipe to automatically unsubscribe after first value.
this.mainService.requestPropertyData()
.pipe(
first() // or take(1)
)
.subscribe(properties => this.properties = properties);
See the difference between first() and take(1)

How to display a specific amount of items from an array using React

Instead of showing every single item inside 'portfolioComponents', I want to show a specific amount of items, from a specific number ('startArrayHere') in the array, and raise and lower from which number ('startArrayHere') I start showing items from the Array. I know the For-Loop is wrong, but I just can't figure out how to do it - can anybody help me out here?
class Portfolio extends Component {
constructor(){
super ()
this.state = {
portitems: portfolioItems,
startArrayHere: 0,
amountOfItemsToShow: 6
}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
if(this.state.startArrayHere < (portfolioItems.length -
this.state.amountOfItemsToShow))
this.setState(prevState => {
return {
startArrayHere: startArrayHere + 1
}
})
}
render(){
const portfolioComponents = this.state.portitems.map(item =>
<PortfolioTemp key={item.id} portfolioitem={item} />)
return (
<div>
<button onClick={() => this.handleClick()}>STATE CHANGE</button>
<div className="portfolio-container">
{
for (let i = 0; i < this.state.amountOfItemsToShow; i++){
portfolioComponents[i + this.state.startArrayHere]
}
}
</div>
</div>
)
}
}
export default Portfolio;
To get a subset of an Array in JS, use the .slice method
Array.slice(startIndexInclusive, endIndexExclusive)
Reference:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
Example:
const a = ['Hello', 'World', 'Foo', 'Bar']
console.log(a.slice(0, 1)) // prints: ['Hello']
console.log(a.slice(0, 2)) // prints: ['Hello', 'World']
console.log(a.slice(2, a.length) // prints: ['Foo', 'Bar']
console.log(a.slice(0, a.length) // prints the entire array ... note: this would be pointless, as you could just print 'a' itself
So, to incorporate your amountOfItems you'd have to just do
a.slice(startIndex, startIndex + amountOfItems)
I hope this helps somewhat.
You can create a shallow copy of an array between any two indicies with Array.prototype.slice(). In practice, you would use this to make a truncated copy of portfolioItems from startArrayHere to startArrayHere + amountOfItemsToShow + 1, and render those items only.

React JS Card Game, counting values in state

I am making a card game in React JS that requires 3 sets of unique cards.
The way the format works is there are ingredient cards that can create potions. The ingredients are dealt in the Top Row of the game, so I have the component called TopRow.
Since these are not normal playing cards I had to generate arrays with 10 of each of 5 different cards for the deal. ( shuffle(a) )
Then I am splicing the deal to only get 5 cards ( a.splice(5); )
So I want the value of the ingredients to increment based on the number of times the ingredients appear, example: function handleHoneyIncrement should increase countHoney by 1.
I've tried a couple different things and I guess I am having a brain fart on how to make a for loop for this.
function TopRow(props) {
let a=["Honey0", "Bone0", "Herbs0", "Mushroom0", "Seeds0",
"Honey1", "Bone1", "Herbs1", "Mushroom1", "Seeds1",
"Honey2", "Bone2", "Herbs2", "Mushroom2", "Seeds2",
"Honey3", "Bone3", "Herbs3", "Mushroom3", "Seeds3",
"Honey4", "Bone4", "Herbs4", "Mushroom4", "Seeds4",
"Honey5", "Bone5", "Herbs5", "Mushroom5", "Seeds5",
"Honey6", "Bone6", "Herbs6", "Mushroom6", "Seeds6",
"Honey7", "Bone7", "Herbs7", "Mushroom7", "Seeds7",
"Honey8", "Bone8", "Herbs8", "Mushroom8", "Seeds8",
"Honey9", "Bone9", "Herbs9", "Mushroom9", "Seeds9"
];
shuffle(a);
function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a.splice(5);
}
let imageIngredients = a.map(image => {
return <img key={image} src={require(`../pngs/${image}.png`)}
alt="ingredients" className="img-responsive"
style={{width:"15%", float:"left"}}
/>
});
let handleHoneyIncrement = () => {
if (shuffle.length= "Honey0" ||"Honey1" ||"Honey2" ||"Honey3" ||"Honey4" ||"Honey5" ||"Honey6" ||"Honey7" || "Honey8" || "Honey9" ){
this.setState({countHoney: this.state.countHoney + 1})
};
};
return (
<div className="row" id="topRow"
style={{WebkitBorderRadius:2, WebkitTextStrokeColor: "red", width:"90%", maxHeight:"30%", padding:0}} >
<div className="col-6-md">
<img src={require('../pngs/IngredientBacks.png')} alt="ingredientsBack" style={{width:"15%", float:"left"}} />
</div>
<div className="col-6-md">
{imageIngredients}
{handleHoneyIncrement}
{a}
</div>
</div>
);}
export default TopRow;
Not 100% sure if this is what you were going for, but it sounds like you just need to turn the ingredients list into a collection of ingredient/count pairs?
const ingredientCounts = a.reduce((obj, curr) => ({
...obj,
[curr]: obj[curr] ? obj[curr] + 1 : 1
}), {})
ingredientCounts["Honey0"] // 1
If you're looking to count all Honeys together like Honey0 + Honey1, etc., this should work:
const ingredientCounts = a.reduce((obj, curr) => {
const keys = ["Honey", "etc"]; // maybe this list should be somewhere else, IDK
const key = keys.find(k => curr.includes(k)); // missing null check :)
return {
...obj,
[key]: obj[key] ? obj[key] + 1 : 1
}
}, {})
ingredientCounts["Honey"] // 10
Then we can set state for all of them like:
this.setState({
counts: ingredientCounts
})
And have a state of counts like:
{
Honey: 10,
etc: 0
}
I'm not 100% sure that I understand your goals correctly, but I think a simplified version is that you want to show:
5 random cards from your deck
A button or trigger that shuffles the deck and displays a new hand of 5 cards from the same deck
A count of the total number of honey cards accumulated as the hand is updated
There are a number of confusing things in your code sample, so rather than try to make corrections I threw up a quick demo of how I would approach that problem with some comments explaining what I did differently, given these assumptions. https://codesandbox.io/s/trusting-mclean-kwwq4
import React, { useState, useEffect } from "react";
// The deck of cards is probably a constant whose values never change directly.
// It's possible that I'm wrong and the deck *does* change, but even so I imagine
// it would come from a prop or context from a parent component. Either way the
// cards array should not be mutable.
const CARDS = [
"Honey0", "Bone0", "Herbs0", "Mushroom0", "Seeds0",
"Honey1", "Bone1", "Herbs1", "Mushroom1", "Seeds1",
"Honey2", "Bone2", "Herbs2", "Mushroom2", "Seeds2",
"Honey3", "Bone3", "Herbs3", "Mushroom3", "Seeds3",
"Honey4", "Bone4", "Herbs4", "Mushroom4", "Seeds4",
"Honey5", "Bone5", "Herbs5", "Mushroom5", "Seeds5",
"Honey6", "Bone6", "Herbs6", "Mushroom6", "Seeds6",
"Honey7", "Bone7", "Herbs7", "Mushroom7", "Seeds7",
"Honey8", "Bone8", "Herbs8", "Mushroom8", "Seeds8",
"Honey9", "Bone9", "Herbs9", "Mushroom9", "Seeds9"
];
const initialCards = [];
function TopRow(props) {
// Keep the current hand of cards in state rather than mutating an array
// directly in the function body. React function components should be pure,
// with all side effects occurring inside of effect hooks.
let [cards, setCards] = useState(initialCards);
let [honeyCount, setHoneyCount] = useState(
countSubstrings(initialCards, "Honey")
);
let imageIngredients = cards.map(image => (
<img
key={image}
src={require(`../pngs/${image}.png`)}
alt={humanReadableAltTag}
className="img-responsive"
style={{ width: "15%", float: "left" }}
/>
));
function shuffleCards() {
// Reset your hand of cards with the original array (the deck)
setCards(shuffleArray(CARDS));
}
// Return all state to initial values
function reset() {
setCards(initialCards);
setHoneyCount(countSubstrings(initialCards, "Honey"));
}
// Any time our cards are updated, we want to increment the number of Honey
// cards in our hand. useState accepts a lazy initializer to access the
// previous state, which is very useful for effects like this!
useEffect(() => {
setHoneyCount(count => count + countSubstrings(cards, "Honey"));
}, [cards]);
return (
<div
{...props}
className="row"
id="topRow"
style={
{
WebkitBorderRadius: 2,
WebkitTextStrokeColor: "red",
width: "90%",
maxHeight: "30%",
padding: 0
}
}
>
<button onClick={shuffleCards}>
{cards.length ? "Shuffle" : "Deal"}
</button>
<button onClick={reset}>Reset</button>
<hr />
<div className="col-6-md">
<img
src={require("../pngs/IngredientBacks.png")}
alt="Back of ingredient card"
style={{ width: "15%", float: "left" }}
/>
</div>
<div className="col-6-md">
{imageIngredients}
</div>
<hr />
<div>
<strong>TOTAL HONEY COUNT:</strong> {honeyCount}
</div>
</div>
);
}
export default TopRow;
// I put these utility functions outside of the component body since there is no
// real reason to recreate them on each render.
/**
* #param {any[]} array
*/
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
// Use slice instead of splice here to prevent mutating the original array
return array.slice(0, 5);
}
/**
* #param {string[]} array
* #param {string} subs
*/
function countSubstrings(array, subs) {
return array.filter(card => card.includes(subs)).length;
}

Is it possible to map only a portion of an array? (Array.map())

I am building a project using React.js as a front-end framework. On one particular page I am displaying a full data set to the user. I have an Array which contains this full data set. It is an array of JSON objects. In terms of presenting this data to the user, I currently have it displaying the whole data set by returning each item of data using Array.map().
This is a step in the right direction, but now I need to display only a portion of the data-set, not the whole thing, I also want some control in terms of knowing how much of the total data set has been displayed, and how much of the data set is yet to be displayed. Basically I am building something like a "view more" button that loads more items of data to the user.
Here is what I am using now where 'feed' represents my Array of JSON objects. (this displays the whole data set.)
return (
<div className={feedClass}>
{
feed.map((item, index) => {
return <FeedItem key={index} data={item}/>
})
}
</div>
);
I am wondering if it is possible to use .map() on only a portion of the array without having to break up the array before hand? I know that a possible solution would be to hold the full data set, and break it off into portions, and then .map() those portions, but is there a way to .map() a portion of the array without having to break it up?
Any and all feedback is appreciated. Thanks!
Do not try to solve this problem with a hack in your mapping step.
Instead, slice() the list to the right length first before the mapping:
class Feed extends React.Component {
constructor(props) {
super(props)
this.handleShowMore = this.handleShowMore.bind(this)
this.state = {
items: ['Item A', 'Item B', 'Item C', 'Item D'],
showItems: 2
}
}
handleShowMore() {
this.setState({
showItems:
this.state.showItems >= this.state.items.length ?
this.state.showItems : this.state.showItems + 1
})
}
render() {
const items = this.state.items.slice(0, this.state.showItems).map(
(item) => <div>{item}</div>
)
return (
<div>
{items}
<button onClick={this.handleShowMore}>
Show more!
</button>
</div>
)
}
}
ReactDOM.render(
<Feed />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='root'></div>
The easiest way in my head is just to use a filter and map
const feed = [1,2,3,4,5,6,7,8,9,10,11]
feed.filter((item, index) => index < 5).map((filteredItem) => //do somthing with filtred item here//)
where 5 is just a number of items you want to get
you could use the slice function before to map the array, it looks like you want to do some pagination there.
var fruits = ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango'];
var citrus = fruits.slice(1, 3);
// fruits contains ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango']
// citrus contains ['Orange','Lemon']
Array.reduce should do what you're asking for. Just change the if statement depending on which range you want.
var excludeAfterIndex = 5;
feed.reduce((mappedArray, item, index) => {
if (index > excludeAfterIndex) { // Whatever range condition you want
mappedArray.push(<FeedItem key={index} data={item}/>);
}
return mappedArray;
}, []);
If you just want to map a portion of an array, you should first filter() your array to obtain the expected portion according to conditions :
array.filter(item => <condition>).map();
Yes, you can map portion of array, based on index. For example:
yourArray = yourArray.map(function (element, index, array) {
if (array.indexOf(element) < yourIndex) {
return {
//logic here
};
} else {
return {
//logic here
};
}
});
You can use slice to get portion of an array:
const data = [1,2,3,4,5,6,7,8,9,10,11]
var updatedData = data.slice(0, 3);
Array#map iterates over all items.
The map() method creates a new array with the results of calling a provided function on every element in this array.
You could use Array#filter
The filter() method creates a new array with all elements that pass the test implemented by the provided function.
for the wanted items and then apply map for the wanted format.
There is no version of the map() function that only maps a partial of the array.
You could use .map() in conjunction with .filter().
You get the index of the current element as the second arg of map and if you have a variable for current page and page size you can quite easily filter the right page from your array without having to really slice it up.
var currentPage = 1;
var pageSize = 25;
dataArray.filter(function(elt, index) {
var upperThreshold = currentPage * pageSize;
var lowerThreshold = currentPage * pageSize - pageSize;
return index < upperThreshold && index > lowerThreshold;
});
Using slice() is better than adding a condition to your map or reduce function, but it still creates an additional, unused copy of that segment of the array. Depending on what you're doing, that might not be desired. Instead, just use a custom map function:
function sliceMap(fn, from, toExclusive, array) {
const len = toExclusive - from;
const mapped = Array(len);
for (let i = 0; i < len; i++) {
mapped[i] = fn(array[i + from], i);
}
return mapped;
};
Note that fn receives the array value and the (now) zero-based index. You might want to pass the original index (i + from). You might also want to pass the full array as a third parameter, which is what Array.map does.
Use this, easy approach
const [limit, setLimit] = useState(false);
const data = [{name: "john}, {name: 'Anna'}]
Here we will have 2 cases:
Display only first data which is John
Display all
data.slice(0, extended ? data.length : 1).map((item, index) => <Text>{item.name}</Text>)
....

Categories