创建具有不同子类型的自定义元素

Create a Custom Element with different sub-types

我目前正在使用自定义元素(Web 组件)实现 data-table 元素。 table 可以有不同类型的单元格(文本、数字、日期等)用于呈现每一行。

例如

<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>

我还有一个 MyTableCell class,所有单元格元素都会扩展。这对于共享通用功能来说效果很好,但是样式可能会很麻烦,因为每个单元格类型都有自己的 html 标签。目前,我在扩展 MyTableCell 时添加了 css class,但为了争论起见,假设我不想那样做。

理想的解决方案是能够使用 is 关键字扩展自定义元素,例如 <my-table-cell is="my-table-cell-text">.


我可以想到 3 种方法来解决这个问题:

  1. 具有类似于 <input type=""> 的语法,但需要做更多的工作,因为您不再扩展基础 class,而是创建相同元素的变体和这意味着您需要一种自定义方式来注册不同的变体,例如静态 MyTableCell.registerType

  2. 一种可组合的方法,我将渲染器元素 <my-table-renderer-text> 包装在通用 <my-table-cell> 中。这避免了自定义寄存器方法,但更难编写并导致更多元素和更多样板代码,这反过来又意味着性能下降。

  3. 两者的混合,用户写 <my-table-cell type="text"> 而单元格在内部使用类似 document.createElement('my-table-rendener-'+ type) 的东西。这保留了选项 1 的更简单语法,同时仍然避免了自定义寄存器方法,但它具有与选项 2 相同的性能影响。


你能推荐更好的选择吗?我错过了什么吗?

可以使用<td>自定义内置元素:

<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>

所有扩展共享同一个原型祖先。示例:

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

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

这样所有单元格都扩展相同的 <td> 元素,共享一个通用原型但有自己的方法实现。

可以覆盖共享方法,共享方法可以调用派生原型对象的特定方法。

在此处查看 运行 示例:

//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

注意:如果您不想使用类型扩展,您也可以使用自定义标签来实现。这个想法是有一个共同的原型和共享它的不同自定义元素(感谢标准原型继承)。

注意:这个答案与另一个答案是分开的,因为它本身非常广泛并且完全独立。

如果您使用 自主自定义元素(即自定义标签)和(可选)type 属性:

<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>

...您可以使用 MVC 模式:

  • 为通用单元格定义一个 class View (and/or Model)
  • 为专门的视图(日期、数字、字符串)定义子class

带有 genericstring 视图的示例:

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 + '"'
    }
}

在自定义元素定义中(可以看作是Controller):

  • 创建时,实例化 View(或 Model)。
  • 当你想渲染单元格(或处理数据)时,调用 View(或 Model).

带有自定义元素的示例 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()
    }
} 

下面是一个实时片段:

//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>