public
Last active

  • Download Gist
gistfile1.md
Markdown

DataTable design overview

These are my early thoughts on how to structure DataTable in 3.5.0. Feedback is welcome in the comments.

Instantiable classes out of the box

new Y.DataTable({...});
new Y.DataTable.Base({...});

Class generation

Y.DataTable.Base = Y.Base.create('datatable', Y.Widget, [Y.DataTable.Core]);
Y.DataTable = Y.mix(
    Y.Base.create('datatable', Y.DataTable.Base, []),
    Y.DataTable, true);

Class extensions CAN (if non-invasive by default)

Y.Base.mix(Y.DataTable, [ Y.DataTable.Sortable ]);

Primary configuration

new Y.DataTable({
    data: [{ ... }, { ... }, ... ]
    data: modelList
    data: {
        source: url, function, xml
        type: 'json'
        schema: { ... }        
    }

    columns: (optional) [ key, key, { key: key, config: stuff, ... }, ... ]

    recordType: (optional) ModelClass

    headerView: (optional) ViewClass
    bodyView: (optional) ViewClass
    footerView: (optional) ViewClass

    summary: (optional) 'string'
    caption: (optional) 'string'
});

Instance structure

instance
    .data = modelListInstance

    .head = viewInstance
    .body = viewInstance
    .foot = viewInstance

Component classes

  • Y.DataTable.Core

    Everything added to Y.Widget to make DataTable.Base

  • Y.DataTable.HeaderView

  • Y.DataTable.BodyView
  • Y.DataTable.FooterView

    Used by DataTable(.Base) to render the table contents.

    Referenced via configuration, not composition.

  • Y.DataTable.Source

    Optional class extension.

    Adds support data config to generate ML with DataSource load.

  • Y.DataTable.Scrollable

    Optional class extension.

    Adds support for scrolling table. May be used to create a third instantiable class.

  • Y.DataTable.ScrollingBodyView

    Used in place of DataTable.BodyView to render the scrolling table. May not be necessary?

  • Y.DataTable.Sortable

    Adds support for sorting headers and sortable config.

Render lifecycle

instance.render(...)

  1. build <table> (markup only or off DOM node?)
  2. instantiate header, body, footer views passing
    1. the DT instance
    2. the created table?
    3. the data ModelList?
  3. call each view's render()

Logic for reacting to user input is moved to the View classes

Concern: string based rendering would have viewX.render() return a string or populate a property, but it has requirements of the DT renderer

Load lifecycle

instance.load(...)

Pass through to this.data.load(...). Let the ModelList be responsible for data interaction.

Default Model generation

  • No ModelList is provided in data configuration AND
  • No recordType is provided in configuration

  1. Use the first item in the data array to build ATTRS and Y.Base.create(...)
  2. If data is empty, use columns?

I'm on the fence about having DT (or DT.Base) include Y.Model in its class extensions. IO with the server should happen through the sync layer defined by Model, but everything other than load seems more than core functionality, and updating individual records could be handled by the sync layer at the Model instance for a given record.

So having a class extension to add a save() implementation onto the DT prototype, but including a pass through load() that calls this.data.load() seems preferable as a starting point.

I think you're on the right track with having this.load() pass through to this.data.load() (provided by ModelList), and performing individual row updates on the Model instances themselves. I don't see a compelling reason for DataTable to mix in Model.

The general structure you've laid out looks good.

Note that View.render() is expected to be chainable, so while you could have it return a string, that technically wouldn't be kosher.

I suspect we'll want to create a ModelList subclass or extension that Y.DataTable.Scrollable can use to avoid having to hold a live instance of every single model in the list, which would get unwieldy once a table starts reaching hundreds of rows. I've got some thoughts on how to do this, so we can flesh that out when you're ready.

re: View.render() - yeah, that was the origin of my concern, I don't want to break the contract that it be chainable. It also seems odd to have a render() not create DOM, and append into container. This expects container to be a Node, which means DT renderer needs to generate a Node for the table and pass that to the views, making at least the table + thead and table + tbody DOM operations rather than string concat. This is probably fine for initial render, since the table can be off DOM (unless srcNode is set). I'll worry about runtime modifications when I get there.

Re: Flyweight ModelList - I'm curious to see the real impact of this, if the rendering logic becomes string based by default. I expect a Model for every row will be unacceptable, but if DOM is taken out of the picture, how bad will it be? If the performance at the end of the day is comparable to the current DT, it could be ok for 3.5.0 (though I don't want it to be), allowing time in subsequent PRs into early 3.6.0 to make changes to the App classes if we are approaching 3.5.0 launch. The arch update alone should be a big win, but it sure would be nice if it were also fast as hell :)

The initial render call and subsequent render calls don't necessarily need to do the same thing, so that frees you up to, say, render the full tbody initially and then only update row contents on subsequent renders. Whatever makes the most sense. Since you're planning to have a view each for head, body, and footer, it seems sensible for the containers to be <thead>, <tbody>, and <tfoot> nodes respectively rather than the <table> itself, no?

I think a flyweight ModelList could have a significant impact, especially on mobile devices where both memory and processing power are limited. When rendering something like a mailbox containing hundreds or even thousands of messages, instantiating a Model for each one up front would be extremely costly. If, instead, the ModelList were to hold an array of data objects and only create Model instances on demand and cache those instances in a size-limited LRU cache, it could keep memory usage to a minimum while only instantiating models for rows that are actually used, thus avoiding a large up-front CPU hit as well.

Tables can have multiple <tbody>s so that I think is best left to the view to decide. For the others, perhaps. It'll get tricky with the multiple <table> approach for scrollable DT, but who's to say if passing the <table> vs the <thead> would have any practical benefit when it comes down to it? I'm open to either way. I'll make the decision when I get to that part of the implementation. I went for consistency as a starting point based on the multiple <tbody> scenario.

No need to convince me about flyweight ML, I've aways thought that was a good idea :)

I suspect the implementation may be daunting, and I don't want that to gate other DT work. Flyweight ML will happen, and it will be awesome, but if it doesn't make it into 3.5.0 DT, I consider that a reasonable trade-off.

a) When we talked about it the other day, you were keen on flattening the Model(List) API onto the DT instance (which led into the discussion about Y.Model(List) being mixed in as an extension, and multiple-inheritance). Based on the above, it seems like DT is going to hold onto a instance of a Model(List) instead. Just wanted to confirm that the change from a mix-in was conscious, and not just something I'm missing when reading through the writeup (since it influences the timeline for multiple-inheritance support).

b) Are the header/footerViews bound to any part of the Model(List)? Is the bodyView bound to any part of the Model(List)?

c) Are the default Views going to extend the base View implementation? Either way, it may be worth documenting that in the design docs so there's some idea of the kind of API they may have.

d) I assume we are planning on using the base Model/ModelList classes as the default classes to represent rows. It seems like it's worth documenting how that maps onto the current DT implementation's Record/Recordset/Column/Columnset. e.g. are we dropping Column/Columsets (or does DT not use them currently)? That probably applies in general - a migration table may be useful for folks already using DT.

I'm interested to hear more thoughts on your Instance structure section. It looks like data, head, body, foot are all properties, why not ATTRS?

I could imagine something like this for data:

initializer: function (config) {
    // Imagine just the ML case.
    this._data = new Y.ModelList();
},

bindUI: function () {
    // Would basically re-render the table or something.
    this._data.after('reset', this._afterDataChange, this);
},

_getData: function () {
    return this._data;
},

_setData: function (data) {
    return this._data.reset(data);
}
ATTRS: {
    data: {
        getter: '_getData',
        setter: '_setData'
    }
}

Also, are you planning to use the WidgetStdMod extension, and having the views provide the content for each of the sections? If you're not planning on using WidgetStdMod, why not (just curious and what could be done to entice you to)?

These are some design issues and thoughts from my hokey version of Datatable with ModelLists.
some may not apply because lsmith has already anticipated them in his design.
Please excuse me if i missed something above which has already been mentioned, warning: wall of text.

  • Extensions that affect head and body views such as Sortable. Previously the sortable plugin would add its sprites and padding to the th, and to each td cell in the column
    that was sortable (this is in order to give TH enough space for the sprite, and for the td size to match). How could an extension affect one or many BodyViews and a HeaderView?

  • In DT3.3, Plugins weren't aware of each other (but to be fair that's by design). Classic example was the combination of scroll and sortable plugins that produced an unworkable table. This was because sortable used a node query that NOW returned the wrong tbody node (Again, it couldn't anticipate the existence of two tbody (table?) nodes). I guess I'm saying that there has to be a standard way to access every part of the table from the base, and in some cases it might be a list of views (BodyViews?).

  • With regards to placing yuid's on table elements: I ran with clientIds as the TR id, and kept the headers="" attribute that was added to each cell to indicate which column it was part of. Using the clientId on the row makes it pretty easy to translate table events back into an item from your model list, if you want some kind of change to occur on the model.
    I'm thinking I'd run into limitations when there are extensions for records with two rows for instance :) The tableevents extension that i lifted from lsmith would just feed me the TR element and the app takes care of the model manipulation.

  • Performance regarding updates of single models or single fields is much much better (obviously) than RecordSet causing the table to re-render. Iterating through the modelList and creating the nodes did cause a bit of a performance problem, probably most noticeable due to multiple calls of .get(field). flattening the attributes would help (as would a flyweight modellist). Surprisingly, even with modelList and using individual models the performance wasn't too bad (vs DT3 RecordSet).

  • Performance was pretty awful vs the jQuery camp. I think initially i suspected their document.createElement() vs Node.create() was the issue, but in reality the piecemeal creation of table elements wasn't the whole problem, it was the individual calls to model and in some cases formatters.. the jQ camp already had the data as a 2d array. To expand on formatters: I've had some that do a bit of date munging for instance that have a pretty big impact on the rendering time.

  • DT3.3 formatters had a chicken and egg problem where you couldn't access the TD element before it was created, but the formatter sometimes affected some kind of TD attribute.
    At the moment I settled on the formatter returning a string only, but some people might need to set TD attributes somehow. I guess if you have control over the entire template via the BodyView this doesn't become such a problem.

  • Formatting values and footer total calculation: One problem with treating the footer view and body view as separate entities is that when you go to calculate columns of data, you end up re-iterating through the modelList. Sometimes those calculations can end up adding a bit of time when the table gets huge. I would propose maybe two kinds of formatters? (this is assuming the way it builds rows even resembles the previous process at all). One to formulate the value of a cell (because sometimes they don't bear a 1:1 relation to the model attribute), and one to format the visual representation of the cell data. Of course, if you had the entire row as a template then that can be ignored.

  • One design that could alleviate the formatting and node construction issues would be to have a 1:1 relationship between a single model and a view template. I guess then you could throw the smarts into the view, and also cater for a multi-row representation of a single model. Certain placeholders could be established as a part of the template string (like the surrounding TD elements) which could then be overridden by further extensions, providing for example the extra padding on the sortable cells.

  • Eventually I had to implement a formatter for TH cells as well, to get input elements into those cells. cue lsmith telling me to go away and make it a gallery module :)

  • I didn't have any issues with the columnSet design, but the possibility of columns that dont derive from the model attribute directly always comes up, so I guess that's something in mind. (At the moment i solve those problems using custom formatters which produce the value for the cell).

  • Anything is better than Recordset, don't be afraid of a pre-release with no flyweight ML.

Looks like a great start at a complete rewrite of DT. I'm probably a little out of my element here ( !== professional developer) but have a lot of hours / sweat invested in DT projects.

1) I agree with mosen's remarks regarding Sortable / Scrollable and other combinations of features (pagination!) ... the component will need to incorporate the union of capabilities in some fashion (e.g. a sortable and scrollable and inline editable and paginated capability). I assume from the remarks above that this is a given.

2) I'm not yet proficient or smart enough to understand if the YUI3 ML framework will provide a mechanism for designing a data abstraction layer to permit plugging in back-end (i.e. server) updating of data from cell editing. Perhaps something like asyncSubmitter, just more elegant.

3) As a related remark, I think mosen mentioned this, but will there be ability to "update" (i.e. render) just a single record / TR? What if the server sends an updated record to the DT (i.e. like a stock price update), could this capability be incorporated in some way?

4) A big missing element of YUI2 DT IMHO has been a clean data "grouping" capability, wherein a TR forms a parent-child relationship to underlying data. There have been many incarnations of "nested" and "treeble" concepts but the NEED is to have a smart DT that can provide "grouping" of lower level data in either the header or the footer. Seems like the "headerView, bodyView, footerView" concept is clean ... is there any way to provide capability to chain or allow a single row to contain another DT with it's own headerView, bodyView, footerView, etc... Note the parent-child relationship would want to be "data-rich", wherein for example, a summary group TR with numeric totals would actively update upon a child record value changing.

Certainly some of my remarks if ever implemented would be relegated to 3.5+ release or later if at all, but maybe some design input for "wants and needs" could be helpful right now at the preliminary design stage for DT.

I would also like to see the treeble concept taken into consideration (not necessarily part of the feature set, but just thinking about how row groups could be displayed and dynamically loaded). Maybe something like multiple BodyViews per table would be enough for grouping.. and I guess since the treeble concept sorta includes some hierarchy, you could establish a parent-child relationship between BodyViews. Maybe a WidgetParent/WidgetChild type extension could apply to Y.View? It sorta plays well into the structure of the elements and how they are nested. However, rgrove has recommended to me the nesting happens in the models so... this might not fit with the App guys' design.

would love to see split pane functionality as well as the ability to config the table "stretch" to 100% of it the available width

@desai

a) ... Based on the above, it seems like DT is going to hold onto a instance of a Model(List) instead. Just wanted to confirm that the change from a mix-in was conscious, and not just something I'm missing when reading through the writeup

Yeah, I backed off wanting Model mixed into DT. The important model state is defined by the records, I figure. Other features might introduce state, but at this point, I suspect they would be UI related, making it a less compelling argument. We'll see as development progresses if this will be an issue.

b) Are the header/footerViews bound to any part of the Model(List)? Is the bodyView bound to any part of the Model(List)?

Not sure what you mean. They will be instantiated with the DataTable instance, the data ModelList, and the column configuration.

c) Are the default Views going to extend the base View implementation?

Yes, or at least DT will expect them to honor at least a part of the View API by duck type. I don't plan on testing with instanceof. I thought I made it fairly clear in the gist:

    headerView: (optional) ViewClass
    bodyView: (optional) ViewClass
    footerView: (optional) ViewClass

d) I assume we are planning on using the base Model/ModelList classes as the default classes to represent rows. It seems like it's worth documenting how that maps onto the current DT implementation's Record/Recordset/Column/Columnset. e.g. are we dropping Column/Columsets (or does DT not use them currently)? That probably applies in general - a migration table may be useful for folks already using DT.

Agreed, there will need to be a migration guide. Re columns and columnset, at this point, I want to see if I can get away with leaving the columns config as a plan array of objects. This would allow feature modules to read properties from the column config without needing to also modify the Column.ATTRS to have the custom configs preserved. Right now, sortable, for example, is defined in the Column class even though it does nothing unless datatable-sort is also used.

@ericf,

I'm interested to hear more thoughts on your Instance structure section. It looks like data, head, body, foot are all properties, why not ATTRS?

They are properties for convenient access, and because IMO, a DT is a composite of interesting parts. Attributes define state and configuration, but not composition. Specifically instance.data will refer to the same object returned from instance.get('data');, but it will be accessed to get to its properties or methods, so instance.data.someMethod() feels more direct than instance.get('data').someMethod(). The data attribute starts feeling less like state/configuration when interacted with though get().

Similarly, head, body, and foot are parts of the DT. I wanted to minimize the amount of class creation needed to instantiate a DT, but I plan on the bodyView etc configs accepting either a View class or instance. The config would keep the passed value, but the property would be the instance. If the DT is instantiated with bodyView: viewInstance, then instance.get('bodyView') would be equivalent to instance.body.

Also, are you planning to use the WidgetStdMod extension, and having the views provide the content for each of the sections? If you're not planning on using WidgetStdMod, why not (just curious and what could be done to entice you to)?

No, because the analogy would be header, body, footer => thead, tbody, tfoot, but a) the container is at least one table, which defines the dimensions of the children as part of its native behavior, b) rendering order for tables is thead, tfoot, tbody, and c) there are other children of the table element as well such as caption and column/colgroup. In the end, I expect I would get little benefit from StdMod's artificial structure and would wind up rewriting a bunch of it to fit the table ordering rules. Also, I don't like its API :)

I thought I made it fairly clear in the gist:

The fact that they derive from Y.View is clear.

What I was trying to ask was will they extend, as in, add to, the API, and how:

c) Are the default Views going to extend the base View implementation? Either way, it may be worth documenting that in the design docs so there's some idea of the kind of API they may have.

Since the Y.View API is generic. I'm trying to get an idea of whether you're breaking them out just for the base View functionality [ template/render/container ] or is there additional model-bound API to them.

Which leads into the other question...

Not sure what you mean.

Views can (optionally) have references to Model/ModelLists.

Related to the above question about the default Head/FootView API, I was asking if they would have a reference to the Model/ModelList driving the core DT Data - and how the View API would interact with that Model/ModelList reference.

I'm trying to get a handle on what kind of API the HeadView, FootView will have, and if/how they bind to a Model. Or whether they are just encapsulated "renderer" implementations without any interaction with the Model/ModelList driving the Data.

The answer to both questions probably boils down to what the Head/FootView API will look like, and how (if at all) it responds to changes in the Model.

@sdesai

will they extend, as in, add to, the API, and how

Aha, Gotcha. I'm not planning on relying on any additional API at this point. When I get into the implementation details, in particular when I start working on feature modules, some DT related render/bind related methods might emerge as being helpful or even a soft requirement for custom Views targeting DT consumption. I'll report back when I get into that (should be Mon/Tue).

[ template/render/container ] or is there additional model-bound API to them

I expect the Views to be able to respond to data changes and update the portions of the table they are responsible for, but that seems like expected View behavior. I am anticipating a need for some sort of uiUpdate custom event (or suite of common events) fired from each of the views when they apply DOM updates in response to user interaction or data modification. But again, I'm not there yet, so I'll report back.

if/how they bind to a Model. Or whether they are just encapsulated "renderer" implementations

At this point, I do plan on having the View instances bound to the data ML, and possibly (though loosely if necessary) to the DT itself.

@mosen I agree with this, and think it is a good idea:

With regards to placing yuid's on table elements: I ran with clientIds as the TR id, and kept the headers="" attribute that was added to each cell to indicate which column it was part of. Using the clientId on the row makes it pretty easy to translate table events back into an item from your model list, if you want some kind of change to occur on the model.

@lsmith

I expect the Views to be able to respond to data changes and update the portions of the table they are responsible for, but that seems like expected View behavior. I am anticipating a need for some sort of uiUpdate custom event (or suite of common events) fired from each of the views when they apply DOM updates in response to user interaction or data modification. But again, I'm not there yet, so I'll report back.

I think if you can, you should try to avoid this two-way syncing. I think the custom View events are the right way to go, but I think they should merely inform the DT about what changes to make the to Models, and have the DT carry out those changes (one source of truth).

It seems like you probably want the DT to be a bubble target for the View events, so that the View-related custom events bubble back to the DT, and the DT can subscribe to them in a loose manner.

It also seems like you'll want to pass/set the ModelList to these Views so they can respond to changes as @sdesai was saying. I can imagine a FooterView that displays the total number of records my calling this.get('modelList').size() and rendering the result.

@ericf

I think if you can, you should try to avoid this two-way syncing.

Agreed, thanks for clearing my head up. The DT instance will serve in a more formal Controller role. Views will subscribe to Model events to propagate changes to the UI, and to UI events to report user input to the DT/Controller, which then updates the Models. It's like an MVC pattern, or something!

DT to be a bubble target for the View events

Yes

pass/set the ModelList to these Views so they can respond to changes

That was the plan, yes.

@mosen Thanks for the feedback!

Extensions that affect head and body views such as Sortable/Plugins weren't aware of each other

Definitely I'll make sure the two are compatible. As to the larger issue of plugins ont being aware of one another, I'm hoping the uiChange event (or suite of events) will help bridge the communication gap, because, you're right, they should not be aware of one another by design. I'm hoping there will be few cases where this manifests, but I'm hoping that the new structure will help isolate the conflict areas. The proof in the pudding will come when I start working on features (PR2 and 3).

there has to be a standard way to access every part of the table from the base

That's part of the reason for having the head, body, foot properties, because the APIs that appropriate for a specific row visualization can be sequestered onto the View class. If there's a reasonable, consistent way to expose the tbody or tbodies (I don't see a problem with the thead or tfoot) that will support the wide variety of row visualizations, then I'm all for it. I'm open to suggestions.

Using the clientId on the row

I agree with @ericf that this sounds like a good idea. I worry this will conflict with the flyweight ML, though, since there wouldn't be a permanent Model instance for each row. I don't want to reinvent any wheels.

updates of single models or single fields is much much better (obviously) than RecordSet causing the table to re-render. Iterating through the modelList and creating the nodes did cause a bit of a performance problem, probably most noticeable due to multiple calls of .get(field). flattening the attributes would help (as would a flyweight modellist).

Yeah, it's high on my performance related priority list that record/Model changes will affect isolated UI changes. That's run time behavior. The hit coming from Attribute during render time is definitely an issue. Having the record data in Attributes gives us a single place to define extra behavior like setters (aka parser) and validators and getters (formatters? --seems like View functionality), but is costly for what I would suspect is the majority use case of having a simple data map like a POJO. I have some incomplete ideas on this that I'll enumerate in a separate comment or gist.

it was the individual calls to model and in some cases formatters

The current structure has each cell going through several functions in the render process. Actually, all cells go through a formatter, just most through the default one. I'm pretty sure we can avoid a lot of these function calls.

At the moment I settled on the formatter returning a string only, but some people might need to set TD attributes somehow. I guess if you have control over the entire template via the BodyView this doesn't become such a problem.

Yep, that's the idea. If time allows, I'd like to create a default BodyView that uses exclusively string concatenation, but provide another that supports cell Node creation. The latter is more likely to be in 3.5.0 for backward compatibility.

I would propose maybe two kinds of formatters? (this is assuming the way it builds rows even resembles the previous process at all). One to formulate the value of a cell (because sometimes they don't bear a 1:1 relation to the model attribute), and one to format the visual representation of the cell data.

I don't follow. Can you give an example of when there would need to exist a value state for a cell that was neither the attribute value or the rendered cell data?

One design that could alleviate the formatting and node construction issues would be to have a 1:1 relationship between a single model and a view template. I guess then you could throw the smarts into the view, and also cater for a multi-row representation of a single model.

This was part of the reason to have the bodyView break out. One View implementation can use row templates + Y.Lang.sub(), another could use Node creation, and another could have sub-Views for things like expandable rows. I sure hope it works out!

cue lsmith telling me to go away and make it a gallery module :)

Go make a gallery module. What are we talking about? Eh, doesn't matter. Go make a gallery module!

Similar to the bodyView for custom implementations, make a custom headView.

@stlsmiths

#1 - Pagination is in the plan for 3.5.0. Data syncing with the server will be delegated to the ModelList.

#2 - The specific implementations haven't been worked out, but I plan on taking full advantage of the work @rgrove and @ericf have been doing in the App architecture.

#3 - Per my reply to @mosen, I'm definitely planning to have individual model updates only touch that part of the UI. No full table rerenders for this DT!

#4 - I think I covered my initial thoughts on this in the reply to @mosen, but yeah, official grouping support is targeted for 3.7.0

Keep the feedback coming!

@mundizzle

100% width has been a thorn in the side of DT for a long time. It's mostly (exclusively?) been an issue with scrollable tables, and I have that specific task targeted for 3.7.0.

I don't have any plans for split pane functionality, unless this is another way of saying freeze columns, like the Excel functionality. That's another one that's been asked for a lot (it never existed for YUI 2, either). I have it loosely targeted for 3.8.0, but the big issue has been accessibility. We'll see when we get closer. I'm sure a lot will have changed by then. In the mean time, all suggestions and feedback are welcome.

@lsmith sorry I was a bit vague with the dual formatters idea.
It's something I ran across, but now I think that it's out of the scope of your development (wrt calculation in footer elements).

Hi,

Awesome stuff, fantastic to see YUI3 DT seeing more activity =).

Apologies if this is slightly off-topic, however I've found that the YUI3 Datatable examples are quite spartan:

http://yuilibrary.com/yui/docs/datatable/

In contrast, the YUI2 examples:

http://developer.yahoo.com/yui/datatable/

contain examples of how to do pagination, client-side filtering by buttons, auto-complete searching, in-line cell editing etc.

I'm guessing most of this is still achievable with YUI3 DT, right?

Is there any chance that the YUI3 examples might be updated sometime to reflect how to do the above with DT now? It'd be great to have some code examples to follow along to.

Cheers,
Victor

@victorhooi YUI 3 DT isn't as feature rich as YUI 2's DT yet. The first milestone is getting a solid foundation for the core functionality, then start working on the features. That's also why there are fewer examples for YUI 3 DT.

3.5.0 DT work will focus on revamping the architecture (as you can tell from this gist), but I'm also planning to add Pagination. Editing, highlighting, and selection are targeted for 3.6.0, then expandable rows and other features in subsequent releases.

WRT examples, though, as of 3.4.0, it's possible for anyone to create examples. The example files live in the src/{component}/docs/ directory in the yui3 project repo on github (https://github.com/yui/yui3) and are built using HandleBars templates compiled with Selleck (https://github.com/rgrove/selleck). I'll be adding examples over time, but pull requests are welcome!

@ericf, @mosen - The row id == clientId came out because I worry that the clientId won't be unique across DataTable instances or YUI instances.

@sdesai - so far, I've seen a need for the body and header views to have a getClassName method (that I cribbed from Widget) and the body view has a getCell and getRow method which the Core implementation relays the arguments to. Since it is up to the view to decide how to create the rows and cells, it should be up to the view to decide how to fetch them.

The getClassName being on the views is mainly to keep the views decoupled from DataTable. I pass the _cssPrefix to the views as a cssPrefix config property. The views' initializer then takes the config and, if set, sets the instance's _cssPrefix, shadowing the default _cssPrefix from the view's prototype. This allows the view instances to use their own getClassName to generate classes appropriate for the DT or for another consumer, falling back to classes built from the prototype _cssPrefix. I can either keep you updated here on in the pull request (https://github.com/yui/yui3/pull/63).

@lsmith: Model clientIds are guaranteed to be unique by default for all model instances on the same page (even across sandboxes), so that should work fine. It's specifically intended for cases like this, where you want to create a DOM id for something related to a model. The only catch would be if someone overrides generateClientId() on a model, has it generate a non-unique id, and then inserts that model into a DataTable. But that's a pretty clear case of taking careful aim and then shooting oneself in the foot, so I wouldn't worry about it.

Regarding the need for getClassName on views: hmm. I actually closed a ticket for this as wontfix earlier today because I didn't think it was worth giving View a dependency on ClassNameManager, but if you think it's valuable, I can consider it.

@rgrove Cool, I'll put clientId back in then.

Maybe I'm using View wrong, or maybe View isn't the right base class. In its role as a renderer for a portion of a Widget, it's not really a one-off, but a utility for library code, so naked class names (without the leading yui3-) didn't seem appropriate. That's why I went with getClassName. At this point, I don't think I'm using much of View's API. And things like initializer calling create() commits me to working with Nodes perhaps earlier than I wanted to. I can override that logic, but at what point am I inheriting from the class just to inherit from the class? Maybe View is better as a subclass for final implementation logic rather than as a middle layer class for components. Wasn't that its initial purpose?

The value of View for DataTable is in providing an API contract. By default, DataTable's views render things a certain way. By subclassing one of DataTable's views and customizing it, a user can easily swap out the default behavior for a view that uses Handlebars for rendering, or that defines custom interactions, or any number of other things. A user could even use one set of views to render DataTables on the client, and another set of Views (but the same underlying DataTable code) to render them on the server. Or one set on desktop, another on mobile. And as users begin to write View extensions and plugins, DataTable's views will benefit from them.

The fact that DataTable doesn't use much of View's API doesn't seem relevant (especially given that View doesn't have much of an API); what's relevant is that by using Views, DataTable is opening up a world of extensibility possibilities that would otherwise be more difficult to achieve.

@rgrove I'm not arguing to subsume the content rendering into the DT widget code. I'm happy with the .head, .body, and .foot breakout. I'm suggesting that a superclass whose API provides your subclass no value is a questionable choice for a superclass. If I'm not using View's API, perhaps I should be subclassing Base directly.

The argument is theoretical because I am still using some of the API, and so far, the rest hasn't cost me anything except the Node creation requirement, which is fine on the client. Renderers for node.js would need to work around this. The whole thing is moot anyway, because I'm not requiring headerView, bodyView, and footerView to be View instances. I use duck typing. They could be simple objects with the required API for all DT cares.

The question was about if getClassName was appropriate to live on View or should it be implemented on subclasses that need it. I'm fine leaving the implementation in my subclasses, and if there's further call for it from the community, then View can adopt it and I'll remove my implementations. The only awkward part IMO is the passing of the _cssPrefix so my views can generate classes as if they were part of the Widget code directly. If that use case should be supported if getClassName is added to View is the more interesting question to me.

Again, the value of View for DataTable is less about what DataTable can get from it and more about what DataTable's users can get from it. Even so, I'm curious why you don't find View's event delegation valuable. Seems perfect for DataTable.

Regarding the Node creation requirement: there isn't one. View's create() method creates a Node by default, but it's intended to be easily overridden if you want it to do something else. Ditto the attachEvents() method. Also, create() isn't called by View's initializer() method, it's called because it's the setter for the container attribute. You can easily override the attribute config without having to override the entire initializer.

The event delegation will be useful to features, I suspect, but the core behavior of the views doesn't involve user events.

This actually raises an interesting issue: What is the cleanest way for a DT class extension, for sorting say, to add UI interaction hooks?

My initial thought was DT class extension adds method sort(...) to DT's proto, and plugs in a table sorting plugin to the headerView instance.

Since the class responsible for rendering the header is configurable, and I'm trying to keep as much knowledge of the DOM structure out of DataTable as possible, it's an unsafe assumption to modify the prototype of any one View class (presumably Y.DataTable.HeaderView). That's what led me to plugins. However, plugins don't modify their hosts, which means the sortable extension wouldn't add to the view's events config.

I'll be dealing with this challenge specifically later this week or early next week, so more on that then.

This actually raises an interesting issue: What is the cleanest way for a DT class extension, for sorting say, to add UI interaction hooks?

Custom events. I'm a big believer that the View should translate DOM events, which resulted from user interactions, into higher-level custom events; in this case custom events which make sense in the world of DataTable.

I also feel that your Views should add the DT Widget as a bubble target. Then, the sorting extension can add a listener for some documented custom event, the View can fire that event, which will kick-off the sorting logic.

var SomeSortExt, CustomHeader;

SomeSortExt = function () {};
SomeSortExt.prototype = {
    initializer: function () {
        this.on('*:toggleSort', this.sort);
    },

    sort: function () {
        // Do the sorting by calling the ML's `sort()` method or something...
    }
};

CustomHeader = Y.Base.create('customHeader', Y.View, [], {
    events: {
        'th': {
            click: 'toggleSort'
        }
    },

    render: function () {
        this.get('container').append('<button class="sort">Sort!</button>');
        return this;
    },

    toggleSort: function (e) {
        this.fire('toggleSort', e.target.get('text'));
    }
});

Y.Base.mix(Y.DataTable, [SomeSortExt]);

new Y.DataTable({headerView: CustomHeader});

It would be odd to implement sorting in an extension when ModelList already has extremely efficient insertion-time sorting (and less efficient but still fast re-sorting) functionality built in. An extension might listen for UI events and whatnot, but ModelList should do the actual sorting.

@rgrove Yeah, I just took what @lsmith was saying as a "for instance…".

I also feel that your Views should add the DT Widget as a bubble target.

Yep, been that way since the beginning.
https://github.com/lsmith/yui3/blob/dtxpr/src/datatable/js/core.js#L324

Custom events. I'm a big believer that the View should translate DOM events, which resulted from user interactions, into higher-level custom events; in this case custom events which make sense in the world of DataTable.

The display order of the rows can be considered a view-only concern (https://github.com/lsmith/yui3/issues/7). John Lindal raised this point a while back, and I agree on an engineering level. All the same, just sorting the DT's ML would be a lot easier, and probably less fragile.

Your code example reinforces my point about the use of View's events config. In order to use it, you've defined a new View subclass that replaces the current headerView. I do want the interaction to be decoupled, and using events to relay info between the DT and views is good, but why would the view fire a sort event if it didn't have any associated action? It will sort in response to a change in its modelList. If the DT will sort the ML, that should be the defaultFn of a sort event fired from the DT. If the sorting is kept a view concern, then... things get even more complicated I think. And how would you make the current HeaderView capable of firing a sort event? That's why I'm thinking plugins, but (circling back) that doesn't use the View's events config.

Hi, might be a bit offtopic here, but...

I'm trying to re-implement this jQuery example in YUI 3:

http://www.datatables.net/release-datatables/examples/basic_init/zero_config.html

And most of the functionality I've coded works, but I can't for the life of me see how to filter the DT data. Would it be possible to have something like:

Y.DataTable.Filter(pattern,fields);

Which applies filtering pattern "pattern" on all specified fields (and if fields are left empty it means "all of them")?

@perekstrom An example of the current implementation of row filtering in 3.4.1 is here: http://yuilibrary.com/yui/docs/recordset/recordset-filter.html

Though it doesn't show its direct application in DT, it's possible to use the plugin with the Recordset that DataTable uses.

This will change in 3.5.0. The migration (and augmentation) of this feature is being tracked here: https://github.com/lsmith/yui3/issues/10

By all means, add suggestions there.

Just listened to today's Open Hours 1/12/2012 and looks like things are going well for the re-factoring. A few questions / comments that I wondered about during the presentation;

  1. Regarding the concept of "Data Types" that was mentioned in today's Open Hours (and recalling Satyam advocating back in Aug 2010 Open Hours ... I think), we are VERY SUPPORTIVE of the idea to allow the concept of a "Data Type" (not to be confused with DataType Utility!).

    We frequently use a "Data Type" custom object to enforce preferred business logic (maybe locale-specific);

    a. Parsers ( "sql-date", "currency", "numeric", ...)
    b. Formatters ( "currency2", "comma2", "short-date", "link", "email", ...)
    c. Default values ( "None", "nbsp;", "No entry", ...)
    d. Sorting ( "numeric", "date", ...)
    e. Form INPUT display focus/blur ... (e.g. onblur show INPUT as currency2, onfocus show INPUT as numeric2 )
    f. Form Validation ( "numeric", "string", "date" ),
    g. Save template back to Server (e.g. JS Date conversion back to SQL formatted date onSubmit )

  2. With respect to Sorting, the forum gets a ton of questions where people want to "unsort" after a sort, or revert a sort back one level.

    • I've seen this done elsewhere (no reference!) on the UI where next to the up/down caret symbol an "x" symbol is provided.
    • Not sure about coding this, (wouldn't really want to have to replicate the prior sorted keys, data bloat !!), but maybe something like sortOptions : { revertable:true } for those crazies who need to do this .
  3. How would multiple TBODY be used within a bodyView, or are you keeping flexibility open for the future ?

  4. We like the idea of the column being a "free-format" Object, instead of a tightly bound Attribute !
  5. For the mutability methods ( addColumn, addRow, addRows ) will there be an option to specify an index, or will they always be pushed to the bottom ?
  6. Thinking towards the future, since the "record" is now a Model, would it be possible / feasible to;
    • use Model's .save() method to send data BACK to a Server (... similar to the asyncSubmitter concepts in days of old)?
    • likewise (... going out on a crazy pedantic limb here), could Model's sync( 'update' ) potentially be wired to a WebSocket to update DataTable INCOMING (i.e. "unrequested") via a push from the remote Server (i.e. ... thinking of inventory updating, stock price updating, etc...).

@stlsmiths

re: 2 - I'm not planning on supporting a configuration for it, but you should be able to call table.sort('clientId') to resort by insertion order. That should DWYW.

re: 3 - This was a concession for the sake of time. It may come back to bite me in the ass, but I abandoned the notion of having the views generate their own relative containers (see https://gist.github.com/1356355#gistcomment-62759) because it was more convenient to have the important Nodes as properties on the DT instance (e.g. table._theadNode) but I didn't want to have to deal with a this._tbodies collection when in the majority of cases, there would be only one. It simplified all code that dealt with the data rows to have a single Node property for the data container, and _tbodyNode matched _tfootNode and _theadNode. If a custom bodyView wants to render multiple <tbody>s, it can hack the addition of more somewhere in the table's rendering lifecycle. Ugly, I admit.

re: 5 - row insertion point is managed by ModelList's sorting algo, but addColumn does accept an insertion index. - http://lsmith.github.com/yui3-gallery/api/classes/DataTable.html#method_addColumn

re: 6 - Yes. Party at my house :)

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.