How to use components for a table row


#1

Hi! I’d like to get some feedback on how I should build a component for my table row.

I’d like the row to be plain text, and then when the user clicks the edit button, it turns the columns into input fields.

I have two questions:

  1. When I call the component and pass in the appropriate variables, I’m getting an error: Uncaught TypeError: Cannot read property ‘firstChild’ of null
  2. How do I tell a specific row that I’d like it to switch to the editing view?

Here’s my Template:

<table class="services table">
  <thead>
    <tr>
      <td>date</td>
      <td>type</td>
      <td>description</td>
      <td>price/per</td>
      <td>quantity</td>
      <td>total</td>
      <td>actions</td>
    </tr>
  </thead>
  <tbody class="table-striped">
  {{#each services}}
    {{service-table-row service=newService servicePer=servicePer serviceTypes=serviceTypes}}
  {{/each}}
    <tr>
      <td colspan="5" class="total-title">Total:</td>
      <td colspan="2" class="total-amount">${{controllers.project.total}}</td>
    </tr>
  </tbody>
  <tfoot class="add-service form-inline" role="form">
    {{service-table-row service=newService servicePer=servicePer serviceTypes=serviceTypes isEditing=true}}
  </tfoot>
</table>

My component template:

<tr>
  <td>
    {{#if isEditing}}
      <label for="date" class="sr-only">Date:</label>
      {{input type="date" value=date class="form-control input-sm"}}
    {{else}}
      {{date}}
    {{/if}}
  </td>
  <td>
    {{#if isEditing}}
      <label for="type" class="sr-only">Type:</label>
      {{view Ember.Select content=serviceTypes value=type class="form-control input-sm"}}
    {{else}}
      {{type}}
    {{/if}}
  </td>
  <td>
    {{#if isEditing}}
      <label for="description" class="sr-only">Description:</label>
      {{input type="text" value=description class="form-control input-sm input-description"}}
    {{else}}
      {{description}}
    {{/if}}
  </td>
  <td>
    {{#if isEditing}}
      <label for="price" class="sr-only">Price:</label>
      {{input type="number" value=price class="form-control input-sm"}}/
      {{view Ember.Select content=servicePer value=per class="form-control input-sm"}}
    {{else}}
      ${{price}}/{{per}}
    {{/if}}
  </td>
  <td>
    {{#if isEditing}}
      <label for="quantity" class="sr-only">Quantity:</label>
      {{input type="number" value=quantity class="form-control input-sm"}}
    {{else}}
      {{quantity}}
    {{/if}}
  </td>
  <td>
    ${{total}}
  </td>
  <td>
    {{#if isEditing}}
      <button {{action 'addService'}} class="btn btn-primary btn-xs">Add</button>
    {{else}}
      <a {{action 'editService' this}} class="btn btn-primary btn-xs"><span class="glyphicon glyphicon-pencil"></span> Edit</a> <a {{action 'removeService' this}} class="btn btn-warning btn-xs"><span class="glyphicon glyphicon-remove-circle"></span> Delete</a>
    {{/if}}
  </td>
</tr>

This is my first post, so if I’m missing something, just let me know. :smile: Thanks!


#2

Is there a reason why you went with the component route in this case and not an itemController?

Edit:

That said, the context of the current item in the {{#each}} will be {{this}}, so you should pass that into your component to represent the row. You could by setting content=this when using the component, which would then make it available within the component.

IIRC components don’t proxy calls to content like an ObjectController does, so if you want to use a value passed into the component you’ll need to do so explicitly. (note: i might be wrong about this…)


#3

I agree that this smells like the wrong job for a component. It may be more appropriate to use itemControllers like @jonnii suggested. But design considerations aside…

Have you looked at the DOM output from your component? It’s likely adding <div> tags for each instance of the component. Try creating an Ember.Component subclass for your component and setting it’s tagName property to "tr":

var ServiceTableRowComponent = Ember.Component.extend({ 
    tagName: 'tr',
});

#4

Thanks guys - it’s been a bit of time since I’ve managed to get back to this - and I should’ve probably shared a lot more code.

However, I’d like to understand more about what you mean by itemControllers … I didn’t find anything with a quick google.

zackangelo - funny enough, it isn’t using 'div’s - it’s using 'tr’s - automatically. :smile:


#5

Very helpful !!! thank you for your answer :slight_smile:


#6

New to Ember here, I am trying to so a similar thing. Can you explain why you would use an item controller over a component? I feel like a component would be the right tool for the job here, but maybe thats just because I have this new shiny hammer.


#7

With the upcoming deprecation of Ember.ArrayController and therefore item controllers, it looks like @joelataylor’s approach of using components to render table rows is the way to go. This works well until you want to make the entire row a clickable link. In the past (currently), using item controllers allows for rendering partials that don’t insert extra elements into the DOM, so this was easy:

app/templates/thing/index.hbs

<table>
  <tbody>
    {{#each thing in things}}
      {{partial "thing/row"}}
    {{/each}}
  </tbody>
</table>

app/templates/thing/row.hbs

{{#link-to "thing.edit" thing tagName="tr"}}
  <td>{{thing.name}}</td>
{{/tr}}

See this github issue

Unfortunately taking the component approach, this breaks down since components insert their own element (the one that the link-to was in the partial):

app/routes/thing/index.hbs

export default Ember.Route.extend({
  actions: {
    transitionToEditRoute: function(thing) {
      this.transitionTo('thing.edit', thing);
    }
  }
});

app/templates/thing/index.hbs

<table>
  <tbody>
    {{#each thing in things}}
      {{thing-row model=thing action="transitionToEditRoute"}}
    {{/each}}
  </tbody>
</table>

app/components/thing-row.js

export default Ember.Component.extend({
  tagName: 'tr'

  click: function() {
    this.sendAction('action', this.get('model'));
  }
});

app/templates/components/thing-row.hbs

{{! this is implicitly wrapped in a tag that we cannot control from the template }}
<td>{{model.name}}</td>

Although the item controller / partial approach always seemed like a bit of a hack to me, using the component approach and having to create an action whose sole purpose is to call this.transitionTo(...) in a route seems even worse. It is arguably more idiomatic / easier to reason about, which is a good thing, but the router action still feels wrong. Does anyone else have a better solution?


#8

I had the same problem, and I absolutely agree with @slindberg about the fact that it feels wrong to define a transitionTo in the route. Currently I’m using another solution which also doesn’t feel right, but feels better than the solutions mentioned. Instead of using a row component I now use a tbody component. This component holds a list of all the table rows wrapped in a object. This object has the same purpose as an ItemController, namely to wrap the content in an object with extra properties. By defining the component as a tbody element I’m still able to define a row as a link.

I’m still looking for a better solution though, because it requires a lot of code decoupling and it seems that it is just easier to define all properties on the model itself, so I don’t even need a component at all for the tables.


#9

To follow up: I believe the element/fragment RFC will address this issue, e.g.

app/templates/thing/index.hbs

<table>
  <tbody>
    {{#each thing in things}}
      {{thing-row model=thing}}
    {{/each}}
  </tbody>
</table>

app/components/thing-row.js

export default Ember.Fragment.extend({
  // item controller logic
});

app/templates/thing-row.hbs

<fragment>
  {{#link-to "thing.edit" model tagName="tr"}}
    <td>{{model.name}}</td>
  {{/tr}}
</fragment>

#10

thanks, this really helped me out


#11

use tagName: 'tr', and inside the component just have td tags, works