API design of components for rendering HTML tables or tabular data (like ember-light-table, etc.)

On the API design of components for rendering HTML tables or tabular data (like ember-light-table, etc.).

Most of these APIs (if not all) require you to pass a configuration object containing the columns you want to display.

columns: [columnSpec({...}), ..., columnSpec({...})]

Furthermore, there is often a table configuration object needed, too.

tableOptions: tableSpec(...)

Then you render the component in your template by passing those objects as options to the table component. (edited)

{{some-table-component tableOptions=myTableOptions columns=myColumns data=myData ...}}

Problems IMO with this approach:

  1. It is not clear in the template what the presentation of the data (myData) is without looking in the component’s JS file.
  2. Cannot easily customize the presentation of the table without knowledge of JavaScript
  3. it is not declarative.

The component design I would propose would like something like this:

{{#display-table data=theData as |table|}}
   {{table.column property='name'}}
   {{#table.column sortable=true sortKey='foo' as |item|}}
      {{some-aweome-component model=item.foo}}
   {{/table.column}}
{{/display-table}}

(most basic example)

Key properties are:

  1. the column specification is declarative. You can see it directly in the template. no javascript is needed.
  2. if a column’s content is more than simply rendering the property as a string, then you use a block form with the table.column contextual component.
  3. i think this API is rather elegant, terse, easy to understand.

Problems with the simple version I presented above: Where is {{table.footer, {{table.pagination? For those, would i need to wrap the columns in an outer {{table.columns?

Perhaps a more extensible but slightly more verbose for the common case (no pagination, no footer - just rows of data).

{{#display-table data=theData as |table|}}
   {{#table.empty}}This table has no data{{/table.empty}}
   {{#table.columns header-classes='some-header-classes' even-class='some-even-class' odd-class='some-odd-class'}}
     {{table.column property='name'}}
     {{#table.column title='Foo column' sortable=true sortKey='foo' as |item|}}
        {{some-aweome-component model=item.foo}}
     {{/table.column}}
   {{/table.columns}}
   {{table.pagination page=myPageNumber total=myTotalCount change=(action 'handleChangingPage')}}
{{/display-table}}

I guess even the above is missing header column customization (other than the title). But that could be added straightforwardly to the scheme above.

Questions:

  1. Is this a good idea?
  2. What challenges do folks anticipate to writing a table component with the api presented above? (The challenge i see is how to parse the column definitions out first for rendering the header and then executing the same body for rendering the actual data.)

Here’s one possible work around for the challenge I describe in question 2 above.

That would just build up the table configuration object used by all the other frameworks. Then you would need a separate component to actually render the table:

{{render-table table=tableConfig}}

If the {{render-table kludge could be eliminated, great. But even that two-step process is better than defining the column definitions in the component’s JavaScript instead of in the template.

I know that’s a lot, but I’m done writing. Thoughts?

I intend to hack on something like this when I get a chance.

Previously, I had written my thoughts about table components at this thread.

Have a look at this demo page of ember-contextual-table. Without passing columns as a configuration object, isn’t it clear enough? You can also read the README file of the project.

1 Like

IMO one problem with those approaches that you have suggested is that they lack some flexibility. With a configuration object for columns, it’s very easy to add / remove columns or switch their order keeping the template intact.

1 Like

ember-contextual-table is 100% consistent with my thoughts on the subject.

full disclosure: this pattern has been implemented before, e.g., see displaytag (by Matt Raible I think) Display tag library - Tag reference

Very convenient to use for 99% of use cases.

1 Like

Nothing stops the user from iterating over a config object in order to render the {{t.column components:

{{#foo-table ... as |t|}}
   {{#each myOptions |option|}}
      {{t.column title=option.title ...}}
   {{/each}}
{{/foo-table}}
1 Like

Here’s something I don’t understand. Why have table component? The same thing can be accomplished with {{each}} helper while giving you maximum flexibility on how you like to render it.

The only advantage I see is to allow virtual scrolling. If virtual scrolling is the goal, we should be building abstraction around virtual scrolling instead of the table.

Well, you can put the configuration right inside the template…if that’s really what you want. I actually did this with a HighCharts component at work.

{{ui-highcharts
  options=(hash
    xAxis=(array
      (hash
        title=(hash
          text="X Axis"
        )
      )
    )
    series=(array
      (hash
        data=dataOnController
      )
    )
  )
}}

Here’s something I don’t understand. Why have table component? The same thing can be accomplished with {{each}} helper while giving you maximum flexibility on how you like to render it.

The same could be said for why create components at all instead of creating a flat template that only uses native elements. We don’t do that because you frequently want isolation (and reusability).

Otherwise, you’ll find yourself reimplementing the same logic for things like sorting, searching, filtering, grouping, expanding/collapsing rows, indexing, column highlighting, row highlighting, etc. etc.

1 Like

Right! The abstraction should be focused on state (sorting, searching, filtering, grouping) and the actions that changes those states (expand/collapse).

{{! ui-table.hbs }}
{{yield
  (hash
    sortedData=sortedData
  )
  (hash
    sort=(action "sort")
  )
}}
{{#ui-table tagName="table" as |state action|}}
  <thead>
    <tr>
      <th {{action action.sort "Col 1"}}>Col 1</th>
      <th {{action action.sort "Col 2"}}>Col 2</th>
    </tr>
  </thead>
  
  <tbody>
    {{#each state.sortedData as |datum|}}
      <tr>
        <td>{{datum.key}}</td>
        <td>{{datum.label}}</td>
      </tr>
    {{/each}}
  </tbody>
{{/ui-table}}

Not just limited to table though.

I guess my opinion here is a bit biased but from my experience working with different clients, having only a declarative based solution is pretty much useless (IMHO). Maybe if your use cases are minimal and all you need to do is render a simple data set, but at that point, it would probably be more optimal and more useful to rollout your own solution with a simple {{each}}.

Once you start having slightly more needs:

  • Hiding columns / rows
  • Click events on headers, footers, rows, cells, buttons within cells, etc.
  • Custom dependent based css classes
  • Any sort of CRUD operation

The declarative syntax starts getting highly complicated with conditionals and your template starts to become unreadable.

Now what if you have external factors that need to take part in the rendering of your table? A button in a different component wants to toggle a header or a row, some service wants to add new columns to your table, or a route action wants to send your table into a loading state and begin fetching new rows.

Having access to a table instance allows you to be able to take complete control of the table and its data from anywhere in your application. Now, the implementation isn’t perfect, but it is able to handle highly complicated needs and is modular enough to render pretty much anything, something that a fully declarative solution (currently) cannot achieve.

To answer some of your concerns:

It is not clear in the template what the presentation of the data (myData) is without looking in the component’s JS file.

Honestly, this just comes down to code readability. If you create dedicated components for your tables, all your data / column definitions / layouts / etc. should all be in one place allowing anyone that looks at the JS + HBS to get a pretty clear understanding of what each column should be displaying and how the table should be rendered.

Cannot easily customize the presentation of the table without knowledge of JavaScript

IMO, if you’re building / modifying / debugging an ember app, you SHOULD have JS knowledge. If you’re code is clean, commented, and structured, then this should never really be an issue. In most cases (in the context of ELT) creating a cell or header component should do the job.

1 Like

I’m using ELT at work and it’s working well for us. I understand that at some point in complexity you’ll need something as full featured as ELT.

but for purely read-only tables that are either paginated or infinite scroll, the declarative syntax would be nice, IMO.

when i used to big Java (coined that term just now) i used a similar style of component and it was super simple to slap paginated tables wherever product managers wanted them.

you can do the same with ELT but the learning curve is a little higher.

i see the declarative approach as the reverse side of the coin of which ELT is the obverse : a sort of easy mode for doing read-only views with sorting, paging, and other basics.

ember’s component and handlebars implementation is so good it’s tempting to build everything declaratively!

2 Likes

If I have to choose between big JavaScript or big templates, I’ll choose big templates.

DDAU. The button would have to fire action into the controller and have action handler mutates a column property that gets bind down into the template.

{{#ui-table tagName="table" as |state action|}}
  <thead>
    <tr>
      {{#each columns as |column index|}}
        <th {{action action.sort column}}>Col {{index}}</th>
      {{/each}}
    </tr>
  </thead>
  
  <tbody>
    {{#each state.sortedData as |datum|}}
      <tr>
        {{#each columns as |column index|}}
          <button {{action action.toggle column}}>Button at {{index}}</button>
        {{/each}}
      </tr>
    {{/each}}
  </tbody>
{{/ui-table}}

If you’re looking for infinite scroll, smoke-and-mirror does a great job at abstracting just that.

1 Like

Maybe my initial responsive was a bit harsh. I don’t think that a complete declarative solution is useless rather highly limited. If you’re just in need of a simple read only solution, then yes, I think a fully declarative is pretty easy to get setup with and easy to maintain / read. My only concern here is future scalability. Once that read-only table needs some features added to it (ones that a declarative syntax can’t easily support), that’s when you have to decide if you need to switch to an alternate solution, create some app specific hacks, or take the time to incorporate new features into your current solution.

The reason I say this is because when starting with ELT, we too were very excited about Ember’s new declarative compatibilities when the whole notion of composable helpers was introduced. As the project grew, we quickly realized its limitations and had to come up with a solution that is flexible enough for a dev to take full control of the table from multiple places in the app as well as be composable enough to resemble a table structure.

I think this is personal preference. IMHO large templates files are really difficult to read and maintain. I dont think there is anything wrong with leaving some work and structure to the JS (again… personal preference).

Right. But my examples were highly generalized and very rudimentary. There are many cases where actions are not enough, state is required to be kept, and structure / appearance must be dynamically created.

Rolling out your table is simple. You have full control of how things are rendered, how events are bubbled, what dependencies do what, but in terms of a generalized declarative addon, I dont believe that it’s currently possible to create a solution that will cover 100% of the use cases that apps need without some sort of access to a mutable table instance.

demo link updated: new link