sgcHTMLを検討している方からよく聞かれる質問があります。既製のコンポーネントがたくさん用意されているのはわかったけれど、すでに気に入っているJavaScriptライブラリがある場合はどうすればいいのか、というものです。D3.jsやChart.jsのチャートの方が好みかもしれませんし、有償のDevExtreme Gridをすでに購入していて、それを使いたいという方もいるでしょう。答えは単純で、sgcHTMLはオープンに作られています。Delphi、C++Builder、.NETのコードからサーバー上で完全なページを生成し、ブラウザで動くライブラリなら何でも、3つの小さなフックで組み込めます。同じライブラリを頻繁に使うのであれば、再利用可能なコンポーネントにラップしてしまえば、アプリの他の部分がそのJavaScriptを一切意識しなくて済みます。
3つのフック
サードパーティのウィジェットには、ページ上で常に同じ3つのものが必要です。ライブラリを読み込むこと、それが描画される要素を用意すること、そしてDOMの準備が整った時点で初期化コードを1行実行することです。TsgcHTMLComponent_Siteコンポーネントは、それぞれに対応するプロパティまたはメソッドを1つずつ提供します。
CustomHeadは<head>内にマークアップを挿入します。ライブラリを読み込む<script>や<link>を置くのに自然な場所です。AddContentは対象となる要素をページ本体に出力します。BodyEndHTMLは、ライブラリの読み込み後、</body>の直前で初期化コードを実行します。
以下はD3.jsの完全な例です。ライブラリを読み込み、コンテナを配置し、D3にそこへ描画させます。
uses
sgcHTML_Component_Site;
var
oSite: TsgcHTMLComponent_Site;
begin
oSite := TsgcHTMLComponent_Site.Create(nil);
try
oSite.Title := 'Sales Dashboard';
// 1) Load the library (a public CDN, or a file you serve yourself)
oSite.CustomHead := '<script src="https://d3js.org/d3.v7.min.js"></script>';
// 2) Emit the element your library renders into
oSite.AddContent('<div id="chart"></div>');
// 3) Run your init code at the end of the body
oSite.BodyEndHTML :=
'<script>' +
' const data = [4, 8, 15, 16, 23, 42];' +
' d3.select("#chart").selectAll("div").data(data).enter()' +
' .append("div").attr("class", "bar")' +
' .style("width", d => (d * 12) + "px").text(d => d);' +
'</script>';
WebModule.Response := oSite.HTML;
finally
oSite.Free;
end;
end;
これがパターンのすべてです。URLと初期化コードを入れ替えるだけで、同じ3行でChart.js、DevExtremeウィジェット、カレンダー、地図、その他何でも扱えます。
CDNか自前ホスティングか
上の例は公開CDNを指しており、これは何かを試す最速の方法です。イントラネットアプリ、オフライン展開、あるいは正確なバージョンを固定したい場合は、ライブラリファイルをsgcサーバーから自前で配信し、ローカルパスで参照してください。組み込みのコンポーネントの一部はすでにそうしています。同梱のChartコンポーネントはCDNではなくローカルの/chart.umd.min.jsからChart.jsを読み込むため、ページに外部への依存がありません。
// The .js and .css files sit next to your server executable and are
// served as static files, so the page references them by local path:
oSite.CustomHead :=
'<link rel="stylesheet" href="/assets/mylib.min.css">' +
'<script src="/assets/mylib.min.js"></script>';
生の文字列ではなくノードとしてheadを構築したい場合は、テンプレート層がHeadNodesとBodyEndNodesリストを公開しており、AddScriptやAddStylesheetヘルパー、さらにTsgcHTMLScript(Src、Code、Defer、Asyncを持つ)やTsgcHTMLStylesheetノードが使えます。生の文字列と型付きノードは互換性があるので、コードの読みやすい方を使ってください。
数行で使うChart.js
Chart.jsはよくリクエストされるので、同じ3つのフックで紹介します。ライブラリを読み込み、<canvas>を出力し、チャートを構築します。
oSite.CustomHead := '<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>';
oSite.AddContent('<canvas id="revenue" width="600" height="300"></canvas>');
oSite.BodyEndHTML :=
'<script>' +
' new Chart(document.getElementById("revenue"), {' +
' type: "bar",' +
' data: {' +
' labels: ["Q1", "Q2", "Q3", "Q4"],' +
' datasets: [{ label: "Revenue", data: [12, 19, 7, 15] }]' +
' }' +
' });' +
'</script>';
チャートだけが必要で特定のライブラリにこだわらないのであれば、sgcHTMLにはすでにChart.jsを組み込み済みのTsgcHTMLComponent_Chartコンポーネントが用意されており、任意の生のChart.js設定をそのまま渡せるCustomOptionsプロパティもあります。このセクションは、ライブラリを自分で操作したい場合のためのものです。
DevExtreme のGrid
DevExtremeは、より重量級の商用スイートの良い例です。DataGridにはスタイルシートとバンドルの読み込み、マウント先の要素、そして設定オブジェクトが必要です。アプローチ自体は何も変わりません。
oSite.CustomHead :=
'<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.2.3/css/dx.light.css">' +
'<script src="https://cdn3.devexpress.com/jslib/23.2.3/js/dx.all.js"></script>';
oSite.AddContent('<div id="grid"></div>');
oSite.BodyEndHTML :=
'<script>' +
' new DevExpress.ui.dxDataGrid(document.getElementById("grid"), {' +
' dataSource: [' +
' { id: 1, name: "Widget", price: 9.90 },' +
' { id: 2, name: "Gadget", price: 14.50 }' +
' ],' +
' columns: ["id", "name", "price"],' +
' showBorders: true' +
' });' +
'</script>';
興味深いのはdataSourceがどこから来るかという点です。サーバー上でデータセットをJSONにシリアライズして示した通りインラインで埋め込むことも、sgcサーバーが公開するエンドポイントをグリッドに指定してライブで読み込ませることもできます。demosフォルダには、sgc WebSocketサーバーからグリッドにデータを供給する完成済みのDevExtreme Gridサンプルがあり、ページの再読み込みなしにデータがリアルタイムで更新されます。
再利用可能なコンポーネントにラップする
一度きりなら生の文字列をそのまま渡しても問題ありません。同じライブラリをページごとに何度も使っていると気づいたら、コンポーネントに昇格させましょう。TsgcHTMLComponentをサブクラス化し、GetHTMLをオーバーライドして独自のマークアップとライブラリの接続コードを返すようにします。そうすればアプリの他の場所では、組み込みコンポーネントと全く同じように、プロパティを設定してHTMLを呼び出すだけで済みます。
uses
sgcHTML_Component;
type
TsgcHTMLComponent_D3Bars = class(TsgcHTMLComponent)
private
FData: string; // a JSON array, e.g. '[4,8,15,16,23,42]'
published
property Data: string read FData write FData;
public
function GetHTML: string; override;
end;
function TsgcHTMLComponent_D3Bars.GetHTML: string;
begin
Result :=
'<div id="d3bars"></div>' +
'<script src="https://d3js.org/d3.v7.min.js"></script>' +
'<script>' +
' const data = ' + FData + ';' +
' d3.select("#d3bars").selectAll("span").data(data).enter()' +
' .append("span").style("padding-right", d => d + "px").text(d => d);' +
'</script>';
end;
これは特別なケースではなく、同梱されている多くのコンポーネントが実際にこう作られています。MapコンポーネントはLeafletを、RichEditorはQuillを、KanbanBoardはSortableJSを、PDFViewerはpdf.jsをそれぞれラップしており、いずれもまさにこれらのフックを通じてライブラリを読み込み、初期化コードを出力しています。自作のラッパーもそれらと並んで第一級のコンポーネントとして扱われ、ライブラリにスタイルが必要な場合は対応するGetCSSオーバーライドを追加できます。これはsgcHTMLがページ全体で収集し、重複を排除してくれます。
試してみる
要するに、sgcHTMLは決してあなたを閉じ込めません。組み込みコンポーネントは多くの用途をカバーしており、D3.js、Chart.js、DevExtreme の Grid や Chart、あるいはその他どんなブラウザライブラリであれ、何か特定のものが必要になったときは、組み込むのに3つのフック、再利用可能にするのに小さなサブクラス1つで済みます。
質問やフィードバックはありますか? お問い合わせください。コードを書いた本人から返信が届きます。
