この記事では、標準のテーブルレイアウト、それが持つ代替案を示します。 独自のテーブルとレイアウトの例を挙げ、その実装の一般的なポイントを説明します。
標準HTML4テーブル
テーブルを表示するためにHTMLマークアップが必要になったとき、 <table>
タグが考案されました。
ブラウザのテーブルは何を提供しますか? 主な機能は次のとおりです。
テーブル/列の幅を指定しなかった場合、テーブルの幅は、列の内容に合わせて調整および拡大されます。
- テーブルの幅を指定し、指定した幅がコンテンツよりも大きい場合、コンテンツは引き伸ばされます。 最も興味深いのは、コンテンツがどのように拡大されるかです。
この場合、合計幅に対する各列の割合が計算され、割合に従って各列が引き伸ばされます。
最初の例では、テーブル全体の幅は(およそ)= 387px
、 Company
columns = 206px
、 Contact
columns = 115px
です。
パーセントで、 Company
= 206px/387px * 100% = 53%
、 Contact
= 115px/387px * 100% = 30%
。
テーブルの内容が引き伸ばされたので、テーブル全体の幅(おおよそ画面上)= 1836px
、列Company
= 982px
、列Contact
= 551px
です。
パーセントで、 Company
= 982px/1836px * 100% = 53%
、 Contact
= 551px/1836px * 100% = 30%
。
- テーブルの幅を指定し、指定した幅がコンテンツより小さい場合、テーブルは狭くなります。 ただし、コンテンツの幅は可能な限り小さくなります 。
CSS table-layout: fixed
プロパティを指定することで、 テーブルを 「絞る」ことができます。 プロパティの説明 。
したがって、テーブルの幅の自動調整を解除し、テーブルは各列(またはテーブル全体)に対して指定された幅をリッスンしますが、テーブルは指定された幅に正確に適合します。
列の幅を指定しなかった場合、 「壊れた」テーブルを使用して 、 = /
。
セルの境界線を指定する場合、セル/列の
border-collapse: collapse
線を折り畳む(重ね合わせる)border-collapse: collapse
。 つまり セルの接触場所では、二重の境界線はありません。
- グループ化キャップ。 属性
colspan
、rowspan
によって実装されます。
標準テーブルを使用する
上記のすべての例では、テーブルレイアウトで、簡略レイアウトを使用しました。
<table> <tr> <th>Header 1</th> <th>Header 2</th> </tr> <tr> <td>1.1</td> <td>1.2</td> </tr> <tr> <td>2.1</td> <td>2.2</td> </tr> </table>
ただし、「標準」マークアップを使用できます。
<table> <thead> <tr> <th>Header 1</th> <th>Header 2</th> </tr> </thead> <tbody> <tr> <td>1.1</td> <td>1.2</td> </tr> <tr> <td>2.1</td> <td>2.2</td> </tr> </tbody> </table>
ヘッダーのないテーブルが必要な場合、同時に列の幅を制御する必要があります。
<table> <tbody> <colgroup> <col width="100px"></col> <col width="150px"></col> </colgroup> <tr> <td>1.1</td> <td>1.2</td> </tr> <tr> <td>2.1</td> <td>2.2</td> </tr> </tbody> </table>
ほとんどの場合、マークアップで以下を取得する必要があります。 特定の幅または特定の最大幅を持つ特定のコンテナがあります。 その中にテーブルを入力します。
テーブルの幅がコンテナよりも大きい場合、コンテナのスクロールを表示する必要があります。 テーブルの幅がコンテナよりも小さい場合は、テーブルをコンテナの幅まで拡張する必要があります。
しかし、どのような場合でも、指定した幅よりもコンテナの幅を広くする必要はありません。
このリンクにより、テーブルが動作しているコンテナを削除できます。 コンテナを狭めると、テーブルを狭めることができなくなった瞬間にスクロールが表示されます。
テーブル調整
テーブルと列の幅の設定
フロントエンド開発者が直面する最初のジレンマは、列の幅を指定するかどうかです。
指定しない場合、各列の幅はコンテンツに応じて計算されます。
ロジックに基づいて、この場合ブラウザには2つのパスが必要であることを理解できます。 最初は、テーブル内のすべてを表示し、列の幅(最小、最大)をカウントします。 2つ目は、テーブルの幅に応じて列の幅を調整します。
時間が経つにつれて、彼らはテーブルがいように見えると言うでしょう、なぜなら 列の1つが広すぎて、
,
そして最も一般的な「機能」:
- これは
...
を使用したセル内のテキストの略語...
つまり セル内のテキストが列の幅を超えて上昇する場合は、短くして最後に追加する必要があります...
最初の失望は、列の幅を設定しないと、縮小が機能しないことです。 これには独自のロジックがあります。なぜなら 最初のパスで、ブラウザーは縮小せずに最小/最大列幅を計算します。ここでは、テキストを縮小しようとしています。 すべてを再計算するか、削減を無視する必要があります。
削減は簡単です。セルのCSSプロパティを指定する必要があります。
td { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
それに応じて列幅を設定します。 このリンクから、すべてが構成されていることがわかりますが、削減は機能しません。
仕様書には、削減が機能しない理由を少し説明するメモがあります。
If column widths prove to be too narrow for the contents of a particular table cell, user agents may choose to reflow the table
繰り返しますが、テーブルはコンテンツの最小幅まで狭くなります。 ただし、 table-layout: fixed
プロパティを適用すると、テーブルは「従う」ようになり、縮小が機能します。 ただし、列幅の自動調整は機能しなくなりました。
テーブルスクロールジョブ
上記の例はスクロールで動作し、使用できます。 ただし、次の要件が発生します。
, ,
フロントエンド開発者が直面する2番目のジレンマ:
- テーブル内のスクロール/スクロールタスク
テーブルの仕様には、テーブル本体にヘッダーとフッターを含めることができるという直接の指示があります。 つまり キャップと地下室は常に表示されます。
User agents may exploit the head/body/foot division to support scrolling of body sections independently of the head and foot sections. When long tables are printed, the head and foot information may be repeated on each page that contains table data
また、テーブルの本体をスクロールでき、ヘッダーと地下室が所定の位置に残ることを示す表示があります。
Table rows may be grouped into a table head, table foot, and one or more table body sections, using the THEAD, TFOOT and TBODY elements, respectively. This division enables user agents to support scrolling of table bodies independently of the table head and foot
しかし実際には、ブラウザーはこれを行わず、テーブルのスクロールは手動で考案/構成する必要があります。
これを行うには多くの方法がありますが、それらはすべて次の事実に要約されます。
- 追加のマークアップを作成せず、スクロールをそれが何であるか(テーブルの本体に、またはコンテナーにラップし、ヘッダーのセルの値を絶対位置に配置する )にねじ込むことを試みません。
テーブル本体の制限された高さを指定できます。 次の例は、テーブル本体の高さを設定できることを示しています。
結果として、CSSテーブルの本体の表形式表示をdisplay: block
プロパティで分割し、同時にヘッダーのスクロールをテーブルの本体と同期させる必要があります。
- 追加のマークアップ(複合テーブル)を作成し、元のアイテムをスクロールするときに追加のマークアップを同期します
これは、誰もがソリューションを提供/構築するオプションです。
ピボットテーブルの例
テーブルの本文をスクロールする必要がある場合、複合マークアップなしではできません。 複合テーブルのすべての例は、カスタムマークアップを使用します。
最も有名なデータテーブルの 1つは、次のマークアップを使用します。
<div class="dataTables_scroll"> <div class="dataTables_scrollHead"> <div class="dataTables_scrollHeadInner"> <table> <thead> <tr> <th></th> <th></th> <th></th> </tr> </thead> </table> </div> </div> <div class="dataTables_scrollBody"> <table> <thead> <tr> <th><div class="dataTables_sizing"></div></th> <th><div class="dataTables_sizing"></div></th> <th><div class="dataTables_sizing"></div></th> </tr> </thead> <tbody> <tr> <td></td> <td></td> <td></td> </tr> </tbody> </table> </div> </div>
マークアップがどのように見えるかの一般的な画像を取得できるように、意図的にマークアップを短くします。
マークアップには2つのテーブルがありますが、ユーザーにとっては1つとして「見られます」。
次のReact Bootstrap Tableの例では、マークアップを見ると、2つのテーブルも使用されています。
<div class="react-bs-table-container"> <div class="react-bs-table"> <div class="react-bs-container-header table-header-wrapper"> <table class="table table-hover table-bordered"> <colgroup><col class=""><col class=""><col class=""></colgroup> <thead> <tr> <th></th> <th></th> <th></th> </tr> </thead> </table> </div> <div class="react-bs-container-body"> <table class="table table-bordered"> <colgroup><col class=""><col class=""><col class=""></colgroup> <tbody> <tr class=""> <td></td> <td></td> <td></td> </tr> </tbody> </table> </div> </div> </div>
上部の表にはヘッダーが表示され、下部には本文が表示されます。 ユーザーにとっては、これは1つのテーブルのように見えますが。
繰り返しますが、この例ではスクロール同期を使用しています。テーブルの本体をスクロールすると、ヘッダーが同期されます。
しかし、テーブルの本体(1つのテーブル)とヘッダー(別のテーブル)がコンテナの幅に合わせて調整され、幅が離れて移動せず、互いに一致することがどうしてわかりますか?
次に、誰かがその方法を知って同期します。たとえば、上記のライブラリの幅同期関数を次に示します。
componentDidUpdate() { ... this._adjustHeaderWidth(); ... } _adjustHeaderWidth() { ... // , <col> // }
論理的な疑問が生じます。標準テーブルの幅の自動調整のみを使用する場合、なぜ<table>
タグを使用するのでしょうか?
そして、ここは私たちが最初ではなく、表形式のマークアップをまったく使用しない人もいます。 たとえば、 Fixed Data TableまたはReact Table 。
例のマークアップは次のようなものです。
<div class="table"> <div class="header"> <div class="row"> <div class="cell"></div> <div class="cell"></div> </div> </div> <div class="body"> <div class="row"> <div class="cell"></div> <div class="cell"></div> </div> </div> </div>
したがって、名前fixed table
、つまり このようなマークアップの場合、すべての列の幅(テーブルの幅、場合によっては行の高さ)を事前に指定する必要があります。 テキストを削減したい場合でも、通常のテーブルであっても、列の幅を設定する必要があります。
次のReactabularテーブルは、興味深い同期アプローチを使用しています。
著者はさらに進んで、本文だけでなくテーブルの頭もスクロール可能にしました。 スクロールスライダーを表示するブラウザーではひどく見えますが、 touch
ブラウザーでは非常にクールで機能的です。
テーブルの本文をスクロールすると、ヘッダーが同期され、ヘッダーをスクロールすると、本文が同期されます。
しかし、要求する複合テーブルの列幅をどのように自動調整しますか? 追加のブラウザパスを使用する興味深い方法を次に示します。 たとえば、このagグリッドテーブルでは、適切な列幅を自動的に計算できます。
public getPreferredWidthForColumn(column: Column): number { // <span style="position: fixed;"> // // span ( ) // <span style="position: fixed;"> }
独自のテーブルの実装
複合テーブルでは、部品間で追加の同期が必要であるため、ユーザーにとってはすべてが1つのテーブルのように見えます。
すべての複合テーブル(および私のテーブル)には欠陥があり、それらのカスタマイズ/構成方法に関する標準がありません(HTML4テーブルは実装中に破棄されたため、これは論理的です)。
1つの複合テーブルの学習を開始すると、カスタマイズに時間を費やし始めます。
次に、別のプロジェクトでは、別のテーブルを調べて(たとえば、Angular1からReactに、またはjQueryからVueに切り替えるとき)、カスタマイズはまったく異なります。
論理的な質問が発生しますが、それだけの価値がある時間を費やしていますか? 多くのフレームワークテーブルを何度も学ぶ価値はありますか?
複合テーブルの基本的なポイントを自分でマスターする方が簡単な場合があります。その後、任意のフレームワーク(Angular / React / Vue / future ...)でテーブルを作成できますか? たとえば、テーブルの開始に2日間を費やしてから、30分以内にカスタマイズします。
また、既製のフレームワークテーブルを30分で接続し、1日で各機能をカスタマイズできます。
例として、Reactで複合テーブルを作成する方法を示します。
テーブルは次のようになります。
- 複合、テーブルの本文に応じてヘッダーを同期
- コンテナの幅より小さい場合、幅を調整します
さらに、開発の一部の側面のみの説明があり、 すぐに結果を見ることができます。
マークアップ
マークアップには、 div
要素を使用します。 セルにdisplay: inline-block
を使用する場合、次のマークアップがあります。
<div class="row"> <div class="cell" style="width: 40px; display: inline-block;"></div> <div class="cell" style="width: 40px; display: inline-block;"></div> </div>
ただし、1つの問題があります。ブラウザ(すべてのブラウザではない)は、セル間の空のスペースをテキストノードとして解釈します。
これに対処する方法に関する素晴らしい記事があります。
そして、テンプレートエンジン(EJS、JSX、Angular、Vue)を使用する場合、これは簡単に解決できます。
<div class="row"> <div class="cell" style="width: 40px;">{value}</div><div class="cell" style="width: 40px;">{value}</div> </div>
ただし、2017年には既にflexboxが長い間サポートされており、 IE11の 2014年にプロジェクトをやり直しました。
そして今日、あなたは恥ずかしがり屋ではありません。 これによりタスクが簡素化され、必要な数の空のノードを作成できます。
<div class="row" style="display: flex; flex-direction: row;"> <div class="cell" style="width: 40px; flex: 0 0 auto;">{value}</div> <!-- --> <div class="cell" style="width: 40px; flex: 0 0 auto;">{value}</div> </div>
一般的な使用ポイント
テーブルはReduxアーキテクチャに組み込む必要があります。このようなテーブルの例では、 reducers
接続することをお勧めします 。
私はこのアプローチが好きではありません。 私の意見では、開発者はソート、フィルタリングのプロセスを制御する必要があります。 これには追加のコードが必要です。
そのような「ブラックボックス」の代わりに、カスタマイズが困難です。
render() { return ( <div> <Table filter={...} data={...} columns={...} format={...} etc={...} /> </div> ) }
開発者は書く必要があります:
render() { const descriptions = getColumnDescriptions(this.getTableColumns()), filteredData = filterBy([], []), sortedData = sortBy(filteredData, []); return ( <div> <TableHeader descriptions={descriptions} /> <TableBody data={sortedData} descriptions={descriptions} keyField={"Id"} /> </div> ) }
開発者自身が手順を規定する必要があります:列の説明の計算、フィルター、並べ替え。
すべての関数/コンストラクターgetColumnDescriptions, filterBy, sortBy, TableHeader, TableBody, TableColumn
は、テーブルからインポートされます。
オブジェクトの配列がデータとして使用されます:
[ { "Company": "Alfreds Futterkiste", "Cost": "0.25632" }, { "Company": "Francisco Chang", "Cost": "44.5347645745" }, { "Company": "Ernst Handel", "Cost": "100.0" }, { "Company": "Roland Mendel", "Cost": "0.456676" }, { "Company": "Island Trading Island Trading Island Trading Island Trading Island Trading", "Cost": "0.5" }, ]
jsxで要素として列の説明を作成するアプローチが気に入りました 。
同じ考え方を使用しますが、テーブルのヘッダーと本文を独立させるために、説明を一度計算してヘッダーと本文に渡します。
getTableColumns() { return [ <TableColumn row={0} width={["Company", "Cost"]}>first header row</TableColumn>, <TableColumn row={1} dataField={"Company"} width={200}> Company </TableColumn>, <TableColumn row={1} dataField={"Cost"} width={100}> Cost </TableColumn>, ]; } render() { const descriptions = getColumnDescriptions(this.getTableColumns()); return ( <div> <TableHeader descriptions={descriptions} /> <TableBody data={[]} descriptions={descriptions} keyField={"Id"} /> </div> ) }
getTableColumns
関数getTableColumns
は、列の説明を作成します。
propTypes
を使用してすべての必要なプロパティを記述できますが、別のライブラリにpropTypes
した後、この決定は疑わしいようです。
必ずrow
を指定してください-ヘッダー内の行のインデックスを示す番号(ヘッダーがグループ化される場合)。
dataField
パラメーターは、値を取得するために使用するオブジェクトのキーを決定します。
幅width
も必須パラメーターであり、幅または幅が依存するキーの配列として設定できます。
この例では、テーブルrow={0}
最上行row={0}
は、2つの列["Company", "Cost"]
の幅に依存します。
TableColumn
要素はTableColumn
、表示されることはありませんが、そのコンテンツthis.props.children
はヘッダーセルに表示されます。
開発
列の説明に基づいて、説明を行とキーに分割し、説明を結果の配列の行に並べ替える関数を作成します。
function getColumnDescriptions(children) { let byRows = {}, byDataField = {}; React.Children.forEach(children, (column) => { const {row, hidden, dataField} = column.props; if (column === null || column === undefined || typeof row !== 'number' || hidden) { return; } if (!byRows[row]) { byRows[row] = [] } byRows[row].push(column); if (dataField) { byDataField[dataField] = column } }); let descriptions = Object.keys(byRows).sort().map(row => { byRows[row].key = row; return byRows[row]; }); descriptions.byRows = byRows; descriptions.byDataField = byDataField; return descriptions; }
次に、処理された説明をヘッダーと本文に渡して、セルを表示します。 ヘッダーは次のようなセルを作成します。
getFloor(width, factor) { return Math.floor(width * factor); } renderChildren(descriptions) { const {widthFactor} = this.props; return descriptions.map(rowDescription => { return <div className={styles.tableHeaderRow} key={rowDescription.key}> {rowDescription.map((cellDescription, index) => { const {props} = cellDescription; const {width, dataField} = props; const _width = Array.isArray(width) ? width.reduce((total, next) => { total += this.getFloor(descriptions.byDataField[next].props.width, widthFactor); return total; }, 0) : this.getFloor(width, widthFactor); return <div className={styles.tableHeaderCell} key={dataField || index} style={{ width: _width + 'px' }}> {cellDescription.props.children} </div> })} </div> }) } render() { const {className, descriptions} = this.props; return ( <div className={styles.tableHeader} ref={this.handleRef}> {this.renderChildren(descriptions)} </div> ) }
テーブルの本体も、処理された列の説明に基づいてセルを構築します。
renderDivRows(cellDescriptions, data, keyField) { const {rowClassName, widthFactor} = this.props; return data.map((row, index) => { return <div className={`${styles.tableBodyRow} ${rowClassName}`} key={row[keyField]} data-index={index} onClick={this.handleRowClick}> {cellDescriptions.map(cellDescription => { const {props} = cellDescription; const {dataField, dataFormat, cellClassName, width} = props; const value = row[dataField]; const resultValue = dataFormat ? dataFormat(value, row) : value; return <div className={`${styles.tableBodyCell} ${cellClassName}`} key={dataField} data-index={index} data-key={dataField} onClick={this.handleCellClick} style={{ width: this.getFloor(width, widthFactor) + 'px' }}> {resultValue ? resultValue : '\u00A0'} </div> })} </div> }); } getCellDescriptions(descriptions) { let cellDescriptions = []; descriptions.forEach(rowDescription => { rowDescription.forEach((cellDescription) => { if (cellDescription.props.dataField) { cellDescriptions.push(cellDescription); } }) }); return cellDescriptions; } render() { const {className, descriptions, data, keyField} = this.props; const cellDescriptions = this.getCellDescriptions(descriptions); return ( <div className={`${styles.tableBody} ${className}`} ref={this.handleRef}> {this.renderDivRows(cellDescriptions, data, keyField)} </div> ) }
テーブルの本文では、 dataField
プロパティを持つ説明が使用されるため、 getCellDescriptions
関数を使用して説明をフィルター処理します。
テーブルの本体は、画面のサイズ変更のイベント、およびテーブル自体の本体のスクロールをリッスンします。
componentDidMount() { this.adjustBody(); window.addEventListener('resize', this.adjustBody); if (this.tb) { this.tb.addEventListener('scroll', this.adjustScroll); } } componentWillUnmount() { window.removeEventListener('resize', this.adjustBody); if (this.tb) { this.tb.removeEventListener('scroll', this.adjustScroll); } }
テーブルの幅の調整は次のとおりです。
表示後、すべてのセルの幅と比較してコンテナの幅が取られます。コンテナの幅が大きい場合、すべてのセルの幅が増加します。
これを行うには、開発者は幅係数の状態を保存する必要があります(これは変化します)。
次の関数はテーブルに実装されていますが、開発者は独自の関数を使用できます。 既に実装されているものを使用するには、それらをインポートし、現在のコンポーネントにリンクする必要があります。
constructor(props, context) { super(props, context); this.state = { activeSorts: [], activeFilters: [], columnsWidth: { Company: 300, Cost: 300 }, widthFactor: 1 }; this.handleFiltersChange = handleFiltersChange.bind(this); this.handleSortsChange = handleSortsChange.bind(this); this.handleAdjustBody = handleAdjustBody.bind(this); this.getHeaderRef = getHeaderRef.bind(this, 'th'); this.getBodyRef = getBodyRef.bind(this, 'tb'); this.syncHeaderScroll = syncScroll.bind(this, 'th'); }
幅調整機能:
adjustBody() { const {descriptions, handleAdjustBody} = this.props; if (handleAdjustBody) { const cellDescriptions = this.getCellDescriptions(descriptions); let initialCellsWidth = 0; cellDescriptions.forEach(cd => { initialCellsWidth += cd.props.width; }); handleAdjustBody(this.tb.offsetWidth, initialCellsWidth); } }
ヘッダー同期機能:
adjustScroll(e) { const {handleAdjustScroll} = this.props; if (typeof handleAdjustScroll === 'function') { handleAdjustScroll(e); } }
redux
のテーブルの主要な機能は、内部状態を持たないことです(状態を持たなければなりませんが、開発者が指示する場所のみ)。
また、 adjustBody
の幅の調整とadjustBody
同期のadjustScroll
は、リンクされたコンポーネントの状態を変更する関数です。
TableColumn
jsx . : , .
/ .
this.state = { activeSorts: [], activeFilters: [], };
/:
getTableColumns() { const {activeFilters, activeSorts, columnsWidth} = this.state; return [ <TableColumn row={0} width={["Company", "Cost"]}>first header row</TableColumn>, <TableColumn row={1} dataField={"Company"} width={300}> <MultiselectDropdown title="Company" activeFilters={activeFilters} dataField={"Company"} items={[]} onFiltersChange={this.handleFiltersChange} /> </TableColumn>, <TableColumn row={1} dataField={"Cost"} width={300}> <SortButton title="Cost" activeSorts={activeSorts} dataField={"Cost"} onSortsChange={this.handleSortsChange} /> </TableColumn>, ]; }
SortButton
MultiselectDropdown
"" /, . activeSorts
activeFilters
, .
:
- ( )
- ( )
. , , — .