Is there an alternative to {{each}} for displaying iterative model data?


#1

I’ve been using the {{#each}} component in my application to create tables from models that are loaded via ember-data like:

{{#each model as |model|}}
// table date using {{model.property}} in certain fields
{{/each}}

However I recently had an idea to replace these tables with ‘info cards’ to make them more user friendly. I realise though, that I have only ever used {{each}} for this task and if I want to display cards in something like a 5x5 grid, {{each}} won’t help me as it simply creates a list.

An example card in my current components template.hbs looks like:

<div class="card text-center">
    <div class="card-header">
        {{model.customer_id}} - {{model.first_name}} {{model.last_name}} - {{model.customer_age}} years old
    </div>
    <div class="card-block">
        <h4 class="card-title">Special title treatment</h4>
        <p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
        <a href="#" class="btn btn-sm btn-primary">Go somewhere</a>
    </div>
    <div class="card-footer text-muted">
        2 days ago
    </div>
</div>

Of course, I just use {{each}} for these cards I get a long list of vertical cards which isn’t what I’m going for.

How do I handle a case where I want to display markup per model return but I don’t want it in a big ol’ vertical list?


#2

Using something like CSS Grid would allow you to use each and have 5 columns. Maybe something like:

<div class='my-grid'>
  {{#each models as |model|}}
    <div>{{model.name}}</div>
  {{/each}}
</div>

And

.my-grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
  grid-gap: 20px;
}

#3

Yeah, @knownasilya’s solution is the best option when it’s possible. Even if you don’t use display: grid, you could make your cards into floats or display: inline-block so they wrap around.

But if you really need to generate per-row markup, you can still do that too. You just need to compute the rows you want to render. Here’s one way. I’m going to use lodash, here’s a way to add it that lets you import only the parts you’re using:

npm install --save-dev lodash-es ember-auto-import

Then in your component (or controller), compute the rows:

import chunk from 'lodash-es/chunk';
import Component from '@ember/component';

export default Component.extend({
  rows: computed('models.[]', function() {
    return chunk(this.models, 5);
  })
});     
{{#each rows as |row|}}
  <div class="row">
    {{#each row as |model|}}
      <div>{{model.name}}</div>
    {{/each}}
  </div>
{{/each}}

#4

Great answers, thank you very much!


#5

If you don’t want to introduce lodash as a dependency just for that, you could write your own chunk function using Array.reduce:

let chunkSize = 3;
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce(
  (chunks, currentValue) => {
    let currentChunk = chunks.length - 1;
    if (chunks[currentChunk].length === chunkSize) {
      chunks.push([]);
      currentChunk++;
    }
    chunks[currentChunk].push(currentValue);
    return chunks;
  },
  [[]]
);

You could use reduce macro of ember-awesome-macros for implementation. Maybe a PR adding it would also be excepted.


#6

Higher order components help here a lot. HOCs can take the form of helpers or components, in the example I’ve produced here I pair an HOC helper with a component to produce a nice template API: https://ember-twiddle.com/a7ad6d451ee880400e3028f47ba67606?openFiles=templates.application.hbs%2Ctemplates.components.simple-element.hbs