Create a Custom Element with different sub-types

2019-01-20 04:54发布

问题:

I am currently implementing a data-table element using custom elements (web components). The table can have different types of cells (text, number, date, etc) that are used to render each row.

E.g

<my-table>
    <my-table-cell-text column="name"></my-table-cell-text>
    <my-table-cell-date column="dob" format="YYYY-MM-DD"></my-table-cell-date>
    <my-table-cell-number column="salary" decimals="2"></my-table-cell-number >
</my-table>

I also have a MyTableCell class which all cell elements extends. This works fine for sharing common functionality, however styling can be a pain, because each cell type is its own html tag. Currently, I am adding a css class when extending MyTableCell, but for argument's sake, lets say I don't want to do that.

The ideal solution would be to be able to extend a custom element, using the is keyword, e.g <my-table-cell is="my-table-cell-text">, but that's only allowed for built in html elements.


I can think of 3 approaches of solving this issue:

  1. Have syntax similar to <input type="">, but that's a lot more work, since you are no longer extending a base class, but rather creating variations of the same element and this means you need a custom way of registering the different variations, something like a static MyTableCell.registerType

  2. A composable approach, where I wrap a renderer element, <my-table-renderer-text>, inside a generic <my-table-cell>. This avoids the custom register method, but it's harder to write and results in more elements and more boilerplate code, which in turn means a performance hit.

  3. A mix of both, where the user writes <my-table-cell type="text"> and the cell uses something like document.createElement('my-table-rendener-'+ type) internally. This keeps the simpler syntax of option 1, while still avoiding the custom register method, but it has the same performance implications of option 2.


Can you suggest any better alternatives? Am I missing anything?

回答1:

What could be done is using <td> Customized Built-in Element:

<table is="data-table>
   <tr>
       <td is="data-string">Bob</td>
       <td is="data-date">11/1/2017</td>
       <td is="data-number">44<td>
   </tr>
</table>

All extensions share the same prototype ancestor. Example:

//common cell
class DataCell extends HTMLTableCellElement {...}

//typed cell
class StringCell extends DataCell {
    renderContent() { ... }
} 
customElements.define( 'data-string', StringCell, { extends: 'td' } )

This way all cells extend the same <td> element, share a common prototype but have their own method implementations.

You can override shared method, and shared method can call specific method of the derived prototype object.

See a running example here:

//table
class DataTable extends HTMLTableElement {
    constructor() { 
        super()
        console.info( 'data-table created' )
    }
} 
customElements.define( 'data-table', DataTable, { extends: 'table' } );

//cell
class DataCell extends HTMLTableCellElement {
    connectedCallback() { 
        console.info( 'cell connected' )
        if ( typeof this.renderContent === 'function' ) 
            this.renderContent()
    }
} 

//cell string
class StringCell extends DataCell {
    renderContent()
    {
        console.info( 'data-string render' )
        this.innerHTML = '"' + this.textContent.trim() + '"'
    }
} 
customElements.define( 'data-string', StringCell, { extends: 'td' } )
table {
    border-collapse: collapse ;
}
td, th {
    border: 1px solid gray ;
    padding: 2px
}
<h4>Test Table Extension v1</h4>
<table is="data-table">
    <tr>
        <th>Id      <th>Name    <th>Age
    <tr>    
        <td>1       <td is="data-string">An      <td>20
    <tr>
        <td>2       <td is="data-string">Bob     <td>31

Note: If you don't want type extension you can also do it with custom tags. The idea is to have a common prototype and different custom elements that share it (thanks to standard prototype inheritance).



回答2:

NB: This answer is separeted from the other as it's quite extensive by itself and totally independant.

If you use Autonomous Custom Elements (i.e. custom tags) with a (optionnal) type attribute:

<data-table>
    <data-row>    
        <data-cell>1</data-cell>       
        <data-cell type="string">An</data-cell>
        <data-cell type="number">20</data-cell>
    </data-row>
</data-table>

...you could use the MVC pattern:

  • define a class for the generic cell View (and/or Model)
  • define a subclass for the specialized Views (Date, Number, String)

Example with generic and string view:

class CellView {
    constructor ( view ) {
        this.view = view
    }
    render () {
        //default rendering
    }       
}

//String View
class CellStringView extends CellView {
    render () {
        console.info( 'special rendering', this.view )
        this.view.innerHTML = '"' + this.view.textContent + '"'
    }
}

In the custom element definition (which can be viewed as the Controller):

  • on creation, instantiate the View (or Model).
  • when you want to render the cell (or process the data), call the (overridden, or not) method of the View (or Model).

Example with a Custom Element v1 class:

class CellElement extends HTMLElement {
    constructor () {
        super()
        //create cell
        switch ( this.getAttribute( 'type' ) )
        {
            case 'string': 
                this.view = new CellStringView( this ) 
                break

            default:
                this.view = new CellView( this )
        }
    }
    connectedCallback () {
        //render cell
        this.view.render()
    }
} 

Below is a live snippet:

//View (MVC View)
class CellView {
  constructor(view) {
    this.view = view
  }
  render() {}
}

//String View
class CellStringView extends CellView {
  render() {
    console.info('special rendering', this.view)
    this.view.innerHTML = '"' + this.view.textContent + '"'
  }
}

//Element (MVC controller)
class CellElement extends HTMLElement {
  constructor() {
    super()
    //create cell
    switch (this.getAttribute('type')) {
      case 'string':
        this.view = new CellStringView(this)
        break

      default:
        this.view = new CellView(this)
    }
  }
  connectedCallback() {
    //render cell
    this.view.render()
  }
}
customElements.define('data-cell', CellElement)
data-table {
  display: table ;
  border-collapse: collapse ;
  border: 1px solid gray ;
}

data-row {
  display: table-row ;
}

data-cell {
  display: table-cell ;
  border: 1px solid #ccc ;
  padding: 2px ;
}
<h4>Custom Table v1</h4>
<data-table>
  <data-row>
    <data-cell>Id</data-cell>
    <data-cell>Name</data-cell>
    <data-cell>Age</data-cell>
  </data-row>
  <data-row>
    <data-cell>1</data-cell>
    <data-cell type="string">An</data-cell>
    <data-cell>20</data-cell>
  </data-row>
  <data-row>
    <data-cell>2</data-cell>
    <data-cell type="string">Bob</data-cell>
    <data-cell>31</data-cell>
  </data-row>
</data-table>