Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow render function to assume direct control of updating cell DOM #425

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

Conversation

pimlottc
Copy link

@pimlottc pimlottc commented Oct 2, 2014

This is a quick proof-of-concept patch for the idea of allowing the
render function to take full control of rendering content within a cell.
Instead of returning HTML for DataTables to add to the cell, the render
function updates the DOM directly and returns null to indicate that
touching the innerHTML is unnecessary.

This allows the view rendering code to retain references to actual dom
elements on the page for event handling and UI binding.

As an example, my application is using a Backbone Marionette view for
each cell's contents, binding the view to the td created by DataTables
and then rendering to it directly.

I first tried using column.createdCell, which gives convenient access to
the cell node, but that the initial drawing; subsequent re-displays only
call data and/or display and then overwrite the innerHTML, orphaning the
previous elements.

I don't really expect this patch to be accepted directly but I wanted to
open discussion about providing some method to accomplish direct
rendering control.

Issues to considering

  • the td node could be better exposed to the rendering function

Right now, I have to dig out of the meta parameter:

meta.settings.aoData[meta.row].anCells[meta.col]

but it could be passed as a parameter, or perhaps added as e.g.
meta.node. Alternatively, the render function could be permitted to
create and return the node itself.

  • a better way to signal that the render function is handling the
    rendering and that DataTables should not modify innerHTML

Using "null" is kinda dirty. Could be signaled by return type
(HTMLElement) or by using a different function than column.render which
always handles rendering

This is a quick proof-of-concept patch for the idea of allowing the
render function to take full control of rendering content within a cell.
Instead of returning HTML for DataTables to add to the cell, the render
function updates the DOM directly and returns null to indicate that
touching the innerHTML is unnecessary.

This allows the view rendering code to retain references to actual dom
elements on the page for event handling and UI binding.

As an example, my application is using a Backbone Marionette view for
each cell's contents, binding the view to the td created by DataTables
and then rendering to it directly.

I first tried using column.createdCell, which gives convenient access to
the cell node, but that the initial drawing; subsequent re-displays only
call data and/or display and then overwrite the innerHTML, orphaning the
previous elements.

I don't really expect this patch to be accepted directly but I wanted to
open discussion about providing some method to accomplish direct
rendering control.

Issues to considering

  * the td node could be better exposed to the rendering function

Right now, I have to dig out of the meta parameter:

  meta.settings.aoData[meta.row].anCells[meta.col]

but it could be passed as a parameter, or perhaps added as e.g.
meta.node. Alternatively, the render function could be permitted to
create and return the node itself.

  * a better way to signal that the render function is handling the
    rendering and that DataTables should not modify innerHTML

Using "null" is kinda dirty.  Could be signaled by return type
(HTMLElement) or by using a different function than column.render which
always handles rendering
@pimlottc
Copy link
Author

pimlottc commented Oct 2, 2014

As an example, my render function looks something like this:

var renderDisplay = function(data, type, row, meta) {
    var cell = meta.settings.aoData[meta.row].anCells[meta.col];

    var view = new ErrorsColumnView({
        model: new Errors(data),
        el: cell
    });

    view.render();
    return null;
}

@DataTables
Copy link
Collaborator

Thanks for opening this - it is an interesting area for discussion and one I'm not fully happy with in how DataTables currently implements it, but there are a few reasons why it is done this way. Firstly the deferRender option means that the cell node might not always exist when data for the cell is required. For example if you have deferRender enabled and search for a cell that has not yet been created the data still needs to be accessible - even although the node doesn't exist.

Secondly DataTables stores cached information (for speed) about the data for the cell, such as filtering and sorting. If another controller can update the cell directly DataTables wouldn't know about that, and the cached information would be out of date, given (what would look like) random results.

The first point is why the cell isn't passed through to the columns.data or the columns.render method, while the second is a result of this.

As such I don't really see a way at the moment for an external library to fully control the HTML for a cell. It will likely need a new API (a bit like createdCell but for writing to the cell) that the external library would have to call.

So yes, I won't pull this in directly, but I would be interested in your further thoughts.

@pimlottc
Copy link
Author

pimlottc commented Oct 2, 2014

Thanks for your response, I appreciate hearing from someone with a better understanding of DataTables internals. Regarding your comments:

My current method is working with deferRender. The key

I spent some time observing when and how render is called by DataTables. I was also concerned about caching. My view-based render function is only being used when type is display; otherwise, I'm using 'traditional' render functions for sort/filter/type requests. I've tested it out with deferRender and haven't found any problems with this approach. I didn't find any caching/reuse of the actual td innerHTML within DataTables, just of the elements themselves.

Regarding updates, I did run into this issue as well. I am addressing it by creating a listener on each column view's model object which calls invalidate whenever the model data is changed, which triggers a call to render (type = display). This seems to be working, although I am a little confused, as I would expect a call with type = filter and/or type = sort as well to update the cached data model.

Basically, it seems like there is the opportunity for a fairly clean division between the different render types, with "filter/sort/type" returning "plain" data that can be safely cached inside DataTable's internal model and "display" returning/rendering rich formatted data for display only. So all you could need direct DOM access for would be "display", while the others continue to return values as normal.

@pimlottc
Copy link
Author

pimlottc commented Oct 2, 2014

I should really put together a working fiddle or something to demonstrate what I'm doing.

@pimlottc
Copy link
Author

pimlottc commented Oct 2, 2014

Ah, okay, I understand the invalidate case better now - invalidate causes immediate render(display) but render(type) and render(filter) calls are deferred until they are actually needed (e.g. search request, reset, etc).

@DataTables
Copy link
Collaborator

Yup - the rendering function makes no assumption about when it will be called, although obviously external to it (still internally in DataTables) the various features of the library have their own way of handling it. type is normally used only during initialisation - filter typically at start up as well, while sort is delayed until the column is sorted upon.

Complicating it slightly is that I'm planning to add an API that will let developers get the data for each type externally. Not sure how that will work yet but it might be something like column().render( type ) and it would always be expected to return the required data. The idea being that extensions like TableTools and Responsive would be able to call column().render( type ) and get the rendered data that they can display in their own context.

@pimlottc
Copy link
Author

pimlottc commented Oct 2, 2014

Okay, that's my understanding about the type parameter. I found the column.render docs were not extremely clear on when the different value were used and hence how the render function might want to behave differently in each case. It's also a bit confusing that one of the values of type is ... "type" 😛

On that same topic, I'm not exactly clear what you mean in the second paragraph - what do you mean by "type" here?

@pimlottc
Copy link
Author

pimlottc commented Oct 2, 2014

Anyway, I put together a simple working example of what I'm doing: http://live.datatables.net/futuwife/9/edit

In this example, the Salary column is being rendered with a Backbone.Marionette view. The view also handles controls that modify the value. There's a custom data function to make sure DataTables sees the view model's latest value. There's also an invalidationManager that watches for model changes in order to notify DataTables.

Everything is working afaict - paging, sorting, filtering. Have I missed anything?

@decoursin
Copy link

Coincidentally, I just did a pull request #429 regarding this same thing. Please take a look.

Taking full control of the td element by passing it (of sorts) seems would be the best scenario if you can get that to work. However, otherwise, my approach will still enable appending and inserting html DOM elements that would maintain their bindings.

I'm also using Backbone, and I tested it with my pull request. It works retains the DOM binding.

@pimlottc
Copy link
Author

pimlottc commented Oct 3, 2014

The td element is passed in, sort of-you can dig it out of meta. That lets you pass it directly to your view at creation time. Otherwise, you will still need to call View.setElement at some point after the table is drawn in order to ensure your view is pointing to the same td.

@pimlottc
Copy link
Author

pimlottc commented Oct 3, 2014

There are also some minor issues with setElement that make it preferable to pass the actual td element directly to the view constructor: marionettejs/backbone.marionette#1940 marionettejs/backbone.marionette#1952

@pimlottc
Copy link
Author

pimlottc commented Oct 3, 2014

I linked the wrong revision of my demo, this is the latest: http://live.datatables.net/futuwife/11/edit

@DataTables
Copy link
Collaborator

Thanks for the demo - very instructive! This works because you are using the orthogonal data options such that the data can be requested at any time (sort, filter, etc) but the node is only ever used when the display type is requested - and at that point the node will exist. Clever :-).

It is a few more hoops to jump through than I think the average developer might be willing to make, but this is a change in the core that I am going to consider. Possibly between that, and the invalidation helper you use, being packable in a library, that might make it a bit smoother.

I'm not going to drop it in right now because TableTools requests the display data type, as do some other extensions and plug-ins which would break with this pattern.

I'm going to look at how to resolve that first. I'm thinking of having a tabletools type being passed in and if that returns undefined then the raw data will be used - rather than the display type (which will be reserved only for this). That's going to require a coordinated change though.

@ahamid
Copy link

ahamid commented Mar 1, 2015

+1 for supporting some sort of direct DOM generation. In my case I'm using a component framework which generates DOM explicitly and encapsulates state in components...so it becomes completely unmanageable to separately render the component as a string template.

My current approach is to implement rowCallback and do all of the row/cell rendering there, and none of it from within datatable, which I'm sure has drawbacks.

@cookch10
Copy link

+1

This would be a useful feature for use with custom controls within a cell which have their own rendering functionality (e.g. a backbone view).

@ilovett
Copy link

ilovett commented May 1, 2015

👍

Using angular and it would be great to be able to pass a rendered DOM element with bindings, etc.

@foo123
Copy link

foo123 commented Jun 3, 2015

just a little comment here, was actually looking for sth else about datatables, but since i had same problem about rendering custom row (using framework/template engine etc both in server and client)

i do this:

  1. have a template engine which has blocks and plugins support (i use my own engine Contemplate, which is uniform, i.e both for php/python/node and browser)
  2. use a widget factory which renders widgets as html strings (also used as plugin in the template engine)
  3. since i need to render table data both in server (1st time load) and in client (row updates), i use same row template foreach row column (which can be used both in server and client)
  4. render each row by using the template (which in turn uses widgets to render custom elements), so we have here both a live template functionality and a widget framework factory

example code (client, but very similar for server)

        TBApp.appendTable('#projects', columns, [
          projectRow.renderBlock("col0", project)
         ,projectRow.renderBlock("col1", project)
         ,projectRow.renderBlock("col2", project)
         ,projectRow.renderBlock("col3", project)
         ,projectRow.renderBlock("col4", project)
         ,projectRow.renderBlock("col5", project)
         ,projectRow.renderBlock("col6", project)
        ]);

template code (same for both client/server)

{# #}
{#%block("col0",false)#}{#%widget("checkbox",{"name":$id, "class":"select_row"})#}{#%endblock()#}
{#%block("col1",false)#}<a href="{#%route(''/edit')#}">{#$name#}</a>{#%endblock()#}
{#%block("col2",false)#}{#$cat#}{#%endblock()#}
{#%block("col3",false)#}{#$start#}{#%endblock()#}
{#%block("col4",false)#}{#$end#}{#%endblock()#}
{#%block("col5",false)#}{#%l($status)#}{#%endblock()#}
{#%block("col6",false)#}
{#%widget('link',{'href':%route("/edit"),'icon':'edit'},{'title':%l('Edit')})#}
&nbsp;
{#%widget('link',{'href':%route("/display"),'icon':'question-circle'},{'title':%l('Display')})#}
&nbsp;
{#%widget('link',{'href':%route("/delete"),'icon':'users'},{'title':%l('Delete')})#}
{#%endblock()#}

update

the mvc framework i use (again my own modelview.js) does not need to bind to a dom element directly upon generation, it delegates so dynamic elements can be added / remove as needed and / or rendered as strings etc.., yet the mvc functionality can work without problem

@foo123
Copy link

foo123 commented Jun 3, 2015

btw the actual issue with datatables that i was looking for was a way to customize the wrapper/filters section not with javascript but at render time (meaning with actual html code before rendering the table withg datatables). Any ideas how to do that?? Thank you very much and kudos for the plugin (use it for data tables since i came accross it) :)

@foo123
Copy link

foo123 commented Jun 3, 2015

combining datatables with angular.js (an angular directive for customizabl;e datatable) check this link (stumbled upon it)

https://myjitjs.wordpress.com/2013/11/04/data-table-in-angular/

@Emptyfruit
Copy link

+1 for the feature. I'm not using backbone, but just using plain JS i managed to make it work almost as in @pimlottc example (but with dropdown buttons in cells). After first render of the table everything seems perfect. But after any filtering or sorting my tricky-handled cells become empty. My data is server-side processed, but is there a particular reason, why on first load cells are getting filled correctly, but after sorting - empty?

Also, as i understand, the main benefit of this feature would be defining proper element with listeners attached. For me it is so, anyways. I tried to avoid this workarounds by using another approach:

  1. rendering buttons in cells as html text as supposed.
  2. Keeping buttons' ids in a collection.
  3. Apply listeners later in one of the callbacks. But the issue is the same:
    3.1 on initComplete() it works well, but only once.
    3.2 After sorting, the cells get rerendered, so are my buttons.
    3.3 There is no event or callback that handles the situation. drawCallback() is called before cells created..

Is there a way out of this? Maybe i misunderstand the behaviour of the table on sorting and filtering?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants