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 newnumber
property in yourFan
type.- Note that a
ROW_NUMBER()
value is kinda meaningless without a well-definedORDER BY
criteria.
- Note that a
Don't use
indexOf
in a loop:indexOf
has 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.map
which gives you theindex
of eachelement
, and stick theindex
into eachelement
object, 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.assign
the object spread operator...obj
can be used:{ index: idx, ...e }
is almost semantically identical toObject.assign( { index: idx }, e )
.- The difference is that
Object.assign
will invoke customsetter
functions, whereas the...
syntax does not.
- The difference is that
- Caution: when immediately returning an object-literal from within
map
you 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: idx
can be simplified by renaming theidx
parameter toindex
and 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>
<% }%>