I'm looping over an array on the front end using ejs and displaying the data in a bootstrap 5 table. The table is dynamic so rows will be added and deleted with time.
All the data is coming through without an error and the table is populating, however, I'd like to have the first column show the "count" for each row. Eg., "1, 2, 3, 4, 5, etc".
I've tried using indexOf without any success and since the code already exists in a loop, creating another loop requires me to switch my ejs syntax and I lose the ability to count the length of the array.
Below is my client side code that yields value -1 down the entire # column for each row:
<div >
<a href="" onclick="exportTableToCSV(null, 'text.csv')">Export to csv</a>
</div>
<div >
<table >
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Email</th>
<th scope="col">Gender</th>
<th scope="col">Age</th>
<th scope="col">Clicked IG</th>
<th scope="col">Date Added</th>
<th scope="col">Remove</th>
</tr>
</thead>
<tbody>
<% for (let fan of artist.fans){%>
<tr>
<th scope="row"><%= artist.fans.indexOf(fan.id) %></th>
<td><%= fan.email %></td>
<td><%= fan.gender %></td>
<td><%= fan.age %></td>
<td><%= fan.subscribed %></td>
<td><%= fan.dateAdded %></td>
<td style="padding-left: 2rem;">
<button onclick="removeRow()" ></button>
</td>
<td style="padding-left: 2rem;"><i ></i></td>
<% } %>
</tr>
</tbody>
</table>
</div>
If I switch artist.fans.indexOf(fan.id) with just fan.id I get the corresponding objectId for each fan email object.
If I switch artist.fans.indexOf(fan.id) with artist.fans.length I get 7 down the # column for each row.
Here's my db model:
const artistSchema = new Schema({
image: [ImageSchema],
genre: [ String ],
fans: [{
email: String,
subscribed: String,
gender: String,
age: Number
dateAdded: {
type: Date,
default: Date.now
}
}],
});
How do I get each row to be numbered?
CodePudding user response:
the problem is that you used fan.id as a search param in artist.fans.indexOf(fan.id) which is not directly accessible throw the artist.fans so instead you need to use another method that accepts comparing function so you can compare their id
try to use
<th scope="row"><%= artist.fans.findIndex(f=> f.id == fan.id) %></th>
CodePudding user response:
TL;DR:
Just do this:
<% for( const fan of artist.fans.map( ( e, index ) => ({ index, ...e }) ) ) { %>
<tr>
<th scope="row"><%= fan.index %></th>
<td><%= fan.email %></td>
<!-- etc -->
</tr>
<% }%>
Explanation:
I'd like to have the first column show the "count" for each row. Eg., "1, 2, 3, 4, 5, etc".
That's not a "count". I would refer to that as "the row-number" (if 1-based) or "row-index" (if 0-based).
If you're using SQL to get your data then you can just use
ROW_NUMBER()in your query and map it to some newnumberproperty in yourFantype.- Note that a
ROW_NUMBER()value is kinda meaningless without a well-definedORDER BYcriteria.
- Note that a
Don't use
indexOfin a loop:indexOfhas a runtime complexity ofO(n)so it's inadvisable to use that function inside a loop over the same array as that will give youO(n^2)runtime, which is very bad.- This also applies to other
O(n)reduction functions likefindIndex,find,includes, andlastIndexOf. - Also,
indexOf(and others) only defined onArray.prototype, so it isn't available on other kinds of iterables, such asNodeList.
- This also applies to other
Within ejs, you can use an overload of
Array.prototype.mapwhich gives you theindexof eachelement, and stick theindexinto eachelementobject, like so:
const fansWithIndex = artist.fans
.map( function( element, index ) {
element.index = index;
return element;
} );
<% for( const fan of fansWithIndex ) { %>
<tr>
<th scope="row"><%= fan.index %></th>
<td><%= fan.email %></td>
<!-- etc -->
</tr>
<% }%>
...though FP purists (like myself) would argue that the above example is bad code because it mutates source data, and instead Object.assign should be used instead. Also, the long-form function can be made simpler with an arrow-function =>, like this:
const fansWithIndex = artist.fans.map( ( e, idx ) => Object.assign( { index: idx }, e ) );
<% for( const fan of fansWithIndex ) { %>
<tr>
<th scope="row"><%= fan.index %></th>
<td><%= fan.email %></td>
<!-- etc -->
</tr>
<% }%>
This can be simplified further:
- As an alternative to
Object.assignthe object spread operator...objcan be used:{ index: idx, ...e }is almost semantically identical toObject.assign( { index: idx }, e ).- The difference is that
Object.assignwill invoke customsetterfunctions, whereas the...syntax does not.
- The difference is that
- Caution: when immediately returning an object-literal from within
mapyou will need to wrap the object-literal's braces{}in parentheses()to prevent the braces being parsed as function body delimiters, hence the=> ({ foo: bar })instead of=> { foo: bar }.
- The
index: idxcan be simplified by renaming theidxparameter toindexand putting just{ index, ...e }. - Like so:
const fansWithIndex = artist.fans.map( ( e, index ) => ({ index, ...e }) );
<% for( const fan of fansWithIndex ) { %>
<tr>
<th scope="row"><%= fan.index %></th>
<td><%= fan.email %></td>
<!-- etc -->
</tr>
<% }%>
- Because the
artist.fans.map(...)part is a single expression with a single output you can now inline it directly into yourfor(of)statement, like so:
<% for( const fan of artist.fans.map( ( e, index ) => ({ index, ...e }) ) ) { %>
<tr>
<th scope="row"><%= fan.index %></th>
<td><%= fan.email %></td>
<!-- etc -->
</tr>
<% }%>
