softlayer / sl-ember-components

An Ember CLI Addon that provides a variety of UI components.
http://softlayer.github.io/sl-ember-components
MIT License
114 stars 27 forks source link

Define capabilities and API for sl-grid #1261

Open notmessenger opened 8 years ago

notmessenger commented 8 years ago

This issue is where discussion about all of the things we want to change about the sl-grid component should occur and the final definition of its capabilities and API are defined. As these items are discussed and vetted, issues will be created to work any individual tasks, with this issue serving as the overall source of progress.

To indicate that a capability of the component needs to be verified, add a comment with **VERIFY** as the first line. To propose a change to the component, add a comment with **PROPOSAL** as the first line. To indicate that something needs to be researched, add a comment with **RESEARCH** as the first line.

As you are leaving comments regarding any of these VERIFY, PROPOSAL, or RESEARCH comments, link back to the comments link in your own so that the conversation can be more easily followed (in absence of threaded conversation support in Github). This should be done on the first line of the comment via **RE: <url>**

Capabilities and API

Tasks Adding them here until issues are created

Issues tracking these efforts

https://github.com/softlayer/sl-ember-components/issues?q=is%3Aopen+is%3Aissue+milestone%3A%22v0.13.0+%28Final+push+for+a+1.0.0+release%29%22+label%3Asl-grid

SpikedKira commented 8 years ago

PROPOSAL

Create a sort helper or mixin for consuming controllers

notmessenger commented 8 years ago

RESEARCH

Is there any inspiration to be had from https://github.com/shaunc/ember-grid or http://offirgolan.github.io/ember-light-table/#/ ? How is our grid to be any different/better than these?

azizpunjani commented 8 years ago

Current limitations of the Grid

The current grid implementation has a number of limitations/issues, here are some of the main ones that need to be addressed:

Proposal V1

Taking a declarative approach, leveraging contextual components and closure actions, we would be able to enhance the grid.

Things to consider

Taking a declarative approach means that we would have to split parts of the grid into different components, when doing so, we should keep in mind that some components need access to shared state.

When the user clicks on a row, if the details pane has been declared, it should open up. The open and closed state of the details pane is an example of state that needs to be shared. This state can be passed behind the scenes via contextual components so the user does not have to wire this up manually.

Simple grid without sorting
{{sl-grid columns=columns content=content}}
Simple grid with sorting

When column header is clicked, if sortable is set to true, then the sortColumn action will be called on the controller/route. This is similar to how it works right now.

{{sl-grid columns=columns content=content sortColumn=(action "sortColumn")}}
Custom row cell

Imagine the user wants to place a checkbox in the first column of each row, like the image below:

enter image description here

The columns' array declaration would look as follows:

columns: [
    {
        checkboxColumn: true
    },
    {
        title: 'Color',
        valuePath: 'name'
    },
    {
        headerClass: 'smallWidth',
        sortable: true,
        title: 'Hex Code',
        valuePath: 'hexCode'
    }
]

In the code above, the first column has an object with a key checkboxColumn, this key could be named anything unique that can identify that column. The code below shows how to customize the cell for that column to display a checkbox.

{{#sl-grid columns=columns content=sortedModel as |grid|}}
    {{#grid.table as |table|}}
        {{table.header}}

        {{#table.body as |body row column|}}
            {{#if column.checkboxColumn}}
                {{#body.cell}}
                    {{input type="checkbox"}}
                {{/body.cell}}
            {{else}}
                {{body.cell record=row column=column}}
            {{/if}}
        {{/table.body}}
    {{/grid.table}}
{{/sl-grid}}

Let's break down the code:

{{#sl-grid columns=columns content=sortedModel as |grid|}}

Above, an object named grid is yielded through. Here is what the template code for sl-grid.hbs looks like:

{{#if hasBlock}}
    {{yield
        (hash
            table=(
                component "sl-grid-table"
                rowClick=(action "rowClick")
                columns=columns
                content=content
            )
            details=(
                component "sl-grid-details"
                detailPaneOpen=detailPaneOpen
                closeDetailPane=(action "closeDetailPane")
            )
            activeRecord=activeRecord
        )
    }}
{{else}}
    {{sl-grid-table 
        sortColumn=sortColumn 
        rowClick=(action "rowClick") 
        columns=columns 
        content=content}}
{{/if}}

As you can see the object that is yielded through has three keys table, details and activeRecord. grid.table is nothing more than an sl-grid-table with columns, content and rowClick pre-populated. The same goes for grid.details. The detailPaneOpen state as well as the closeDetailPane action is passed along.

After this, we declare the table:

{{#grid.table as |table|}}

The table declaration yields an object that we named table above. The table object has two keys header and body. Header and body are components that are passed columns and content and other actions behind the scenes using contextual components. Again, this is necessary so that the user does not have to deal with passing this data along.

Since no customization is needed to the table header in the use case above, the inline form of table.header is used.

{{table.header}}

Later we will see, if the header needs to be customized, the block version of the header can be used.

The header is automatically passed the columns from the table so columns does not need to be set.

Moving onto the table body

{{#table.body as |body row column|}}

table.body yields through an object called body with the key cell. Cell is an sl-grid-cell component. The other arguments that are passed through are row, column and two other arguments that are not displayed in the code above rowIndex and columnIndex.

Further in the code, an if statement is used to determine if the current column being looped over is the row cell that needs to be customized.

{{#if column.checkboxColumn}}
    {{#body.cell}}
        {{input type="checkbox"}}
    {{/body.cell}}
{{else}}
    {{body.cell record=row column=column}}
{{/if}}

The checkboxColumn key that was set on the column is checked, if it's true then body.cell yields to anything the user provides, otherwise body.cell is rendered with the current row and column data passed in.

Alternatively, we might consider passing in the row and column in the background to body.cell, this would condense the body.cell declaration to {{body.cell}} from {{body.cell record=row column=column}}. The tradeoff is that, it might be confusing how body.cell is rendering without being passed any data.

The example above shows how the user can customize a row cell to display anything they desire, even other components.

Custom column header

Just as the user can customize a row cell, the user would be able to customize a column header cell. In the previous code examples you may remember that we used the inline version of the table header {{table.header}}. The block version would give the user the ability to customize the header. The code below demonstrates this:

{{table.header as |header column index|}}
     {{#if column.checkboxColumn}}
         {{#header.cell}}
             Custom column header
         {{/header.cell}}
     {{else}}
         {{header.cell column=column}}
     {{/if}}
{{/table.header}}

The header.cell code block above in the else block, could possible be done in a more compact way e.g {{header.cell}}, where column is passed behind the scenes. Again, the tradeoff to this is that it might be confusing as to how the cell is getting populated with the data if it's not being passed explicitly.

When the user clicks on a column header, if the column is sortable, the click would trigger an action to get fired. This action would get wired up via the table.header as the code below shows.

{{#table.header sortColumn=(action 'sortColumn') as |header column index|}}
     {{#if column.checkboxColumn}}
         {{#header.cell}}
             Custom column header
         {{/header.cell}}
     {{else}}
         {{header.cell column=column}}
     {{/if}}
{{/table.header}}

The sortColumn action on the controller/route would get called and passed the column that was clicked on, similar to how it works in the current implementation of the grid.

Record details

The current detailsHeaderComponent, detailsFooterComponent and detailComponent would all be encapsulated under a sl-grid-details component. If you recall the grid object that is yielded as an argument contains a details key. Below is an example of how a user would supply a details body, header and footer.

{{#sl-grid columns=columns content=sortedModel as |grid|}}
    {{grid.table}}

    {{#grid.details as |details|}}
        {{#details.header}}
            <h3>grid.activeRecord.name</h3>
        {{/details.header}}

        {{#details.body}}
            <h3>grid.activeRecord.fruit</h3>
        {{/details.body}}

        {{#details.footer}}
            <h3>grid.activeRecord.hexCode</h3>
        {{/details.footer}}
    {{/grid.details}}
{{/sl-grid}}

The grid object has an activeRecord key that has a reference to the record that belongs to the row that was clicked on.

Filter panel

The filter panel implementation would be left up to the user. We could possible create a filter panel component for demo purposes so the user gets an idea of how it would work.

Pagination & footer

The pagination and footer would not be baked into the grid, but would instead be declared and wired up by the user. The details of how exactly the user would wire this up still needs to be discussed.

Proposal V2

In this version the header columns are declared inline instead of getting passed to the grid. The user will have to loop over the content array and declare the row and cells, much like declaring a HTML table.

{{#sl-grid as |grid|}}
    {{#grid.table sortColumn=(action "sortColumn") as |table|}}
        {{#table.header as |header|}}
            {{header.column title="Fruit" key="fruit" sortable=true sorted="asc" class="smallWidth"}}
            {{header.column title="Hex Code" key="hexCode" sortable=true}}
        {{/table.header}}

        {{#table.body as |body|}}
            {{#each content as |row|}}
                {{#body.row row=row}}
                    {{#body.cell}} {{row.fruit}} {{/body.cell}}
                    {{#body.cell}} {{row.hexCode}} {{/body.cell}}
                {{/body.row}}
            {{/each}}
        {{/table.body}}
    {{/grid.table}}
{{/sl-grid}}

Custom header

A custom header can be declared using the block version of {{header.column}} like the code below shows:

{{#header.column}}
    {{#header.cell}}
        Custom
    {{/header.cell}}
{{/header.column}}

If no customization is needed to the row, a one line declaration can be made as shown below:

{{#sl-grid as |grid|}}
    {{#grid.table sortColumn=(action "sortColumn") as |table|}}
        {{#table.header as |header|}}
            {{header.column title="Fruit" key="fruit" sortable=true sorted="asc" class="smallWidth"}}
            {{header.column title="Hex Code" key="hexCode" sortable=true}}
        {{/table.header}}

        {{table.body content=content}}
    {{/grid.table}}
{{/sl-grid}}

The above might or might not be possible because of the way the grid currently works.

The rest of the parts of the grid would be the same as V1.

notmessenger commented 8 years ago

RE: https://github.com/softlayer/sl-ember-components/issues/1261#issuecomment-190405887

Another user has indicated interest in customizable cells - https://github.com/softlayer/sl-ember-components/issues/1652