Создайте пользовательский элемент с различными подтипами

В настоящее время я реализую элемент таблицы данных, используя пользовательские элементы (веб-компоненты). В таблице могут быть разные типы ячеек (текст, число, дата и т. д.), которые используются для отображения каждой строки.

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>

У меня также есть класс MyTableCell, который расширяет все элементы ячейки. Это прекрасно работает для совместного использования общих функций, однако стилизация может быть проблемой, потому что каждый тип ячейки представляет собой собственный HTML-тег. В настоящее время я добавляю класс css при расширении MyTableCell, но ради аргумента, допустим, я не хочу этого делать.

Идеальным решением было бы иметь возможность расширить пользовательский элемент, используя ключевое слово is, например <my-table-cell is="my-table-cell-text">, но это разрешено только для встроенных html-элементов.


Я могу придумать 3 подхода к решению этой проблемы:

  1. Имейте синтаксис, аналогичный <input type="">, но это намного больше работы, поскольку вы больше не расширяете базовый класс, а создаете варианты одного и того же элемента, а это означает, что вам нужен собственный способ регистрации различных вариантов, что-то вроде статического MyTableCell.registerType

  2. Компонуемый подход, при котором я оборачиваю элемент рендерера <my-table-renderer-text> в общий <my-table-cell>. Это позволяет избежать метода пользовательского регистра, но его сложнее написать, и в результате получается больше элементов и больше стандартного кода, что, в свою очередь, означает снижение производительности.

  3. Сочетание обоих, где пользователь пишет <my-table-cell type="text">, а ячейка использует что-то вроде document.createElement('my-table-rendener-'+ type) внутри. Это сохраняет более простой синтаксис варианта 1, но при этом избегает метода пользовательской регистрации, но имеет те же последствия для производительности, что и вариант 2.


Можете ли вы предложить лучшие альтернативы? Я что-то пропустил?


person Dogoku    schedule 11.01.2017    source источник
comment
Вы хотите создавать пользовательские элементы со спецификацией v0 или v1?   -  person Supersharp    schedule 11.01.2017
comment
Я доволен обоими. В настоящее время используется v0, пока другие браузеры не добавят встроенную поддержку v1. Насколько я могу судить, обе спецификации имеют одинаковое ограничение: невозможность использовать is для расширения пользовательских элементов.   -  person Dogoku    schedule 11.01.2017


Ответы (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

Примечание. Если вам не нужно расширение типа, вы также можете сделать это с помощью пользовательских тегов. Идея состоит в том, чтобы иметь общий прототип и различные пользовательские элементы, которые его разделяют (благодаря стандартному наследованию прототипов).

person Supersharp    schedule 11.01.2017
comment
Я не понимаю твоего примечания внизу. Использование пользовательских тегов — это мое существующее решение (мой общий прототип — MyTableCell, как упоминалось выше). Это работает для совместного использования функций, но имеет проблему со стилем, которую я пытаюсь избежать, поскольку все типы ячеек определяют свои собственные теги. - person Dogoku; 11.01.2017
comment
Мой узел был о поведении. Что касается стиля, с пользовательскими тегами используйте один элемент ‹table-cell›. Он по-прежнему отличается от вашего решения № 2-3, потому что вам не нужно вставлять внутрь настраиваемый элемент. - person Supersharp; 11.01.2017

Примечание: этот ответ отделен от другого, поскольку он сам по себе достаточно обширен и полностью независим.

Если вы используете автономные настраиваемые элементы (т. е. настраиваемые теги) с (необязательным) атрибутом 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:

  • определить класс для общей ячейки View (и/или Model)
  • определить подкласс для специализированных представлений (дата, число, строка)

Пример с общим и строковым представлением:

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

В определении пользовательского элемента (которое можно рассматривать как Контроллер):

  • при создании создайте экземпляр представления (или модели).
  • когда вы хотите отобразить ячейку (или обработать данные), вызовите метод (переопределенный или нет) представления (или модели).

Пример с пользовательским элементом 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>

person Supersharp    schedule 11.01.2017
comment
По сути, это то же самое, что и вариант 1 в моем примере, с той разницей, что представления предопределены в этом операторе switch. Как и в случае с вариантом 1, если вы хотите разрешить пользователям определять свои собственные представления, вам нужен способ регистрации представлений, чтобы CellElement мог создавать их экземпляры. Ваш предыдущий ответ не имеет отношения к этому, поскольку представления изначально зарегистрированы как расширения td - person Dogoku; 12.01.2017
comment
Спасибо за ваше время, потраченное на написание 2 разных, очень подробных ответов. Я начал этот вопрос, больше как обсуждение, в том, что, по-видимому, является вариантом использования пользовательских элементов, который либо был пропущен архитекторами, либо они решили, что его не стоит поддерживать. - person Dogoku; 12.01.2017
comment
Да, это ваш вариант 1, реализованный с помощью MVC. Я предполагаю, что они решили не поддерживать его, потому что это может быть достигнуто изначально с помощью стандартного наследования прототипа (или класса) (или для простоты). - person Supersharp; 12.01.2017