はじめに
仕事内でもReactを利用することも増えてきて、パッケージやらコンポーネントやら便利だよね、って思い始めて久しい。
一方で、古き良きjavascriptオンリーのページを触るときにも「あれ使えたら楽に実装できるかもなあ」と思って何とかならんもんかと試行錯誤してみた、という話。
概要
- ガントチャートを作るにあたって「frappe-gantt」を使ってみた
- nodeなどは使わずに、シンプルな単一ページのみでReactを読み込んで完結するように実装してみた
こんな感じになる
準備するもの
- ウェブページが開けるもの(つまりエディタとブラウザ)
最初に成果物
なんと以下のhtmlページを「gantt.html」とかで保存してブラウザで開けば閲覧できます。
(当たり前といえば当たり前だけど、このご時世に環境構築なしで動くものの確認っていうのもお手軽でいいよね)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ガントチャートのデモ</title>
<!-- ReactとReactDOMの読み込み -->
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<!-- frappe-ganttのスタイルシート -->
<link rel="stylesheet" href="https://unpkg.com/frappe-gantt@0.5.0/dist/frappe-gantt.css">
<!-- frappe-ganttのスクリプト(ミニファイ版を使用) -->
<script src="https://unpkg.com/frappe-gantt@0.5.0/dist/frappe-gantt.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
}
#gantt {
margin: 50px;
}
</style>
</head>
<body>
<h1>ガントチャートのデモ</h1>
<!-- ガントチャートを表示する要素 -->
<div id="gantt"></div>
<script type="text/javascript">
// Ganttがグローバルスコープで使用可能か確認
document.addEventListener('DOMContentLoaded', function() {
if (typeof Gantt === 'undefined') {
console.error('Ganttが読み込まれていません');
return;
}
// デモ用のタスクデータ
const tasks = [
{
id: 'Task 1',
name: 'デザイン',
start: '2023-10-05',
end: '2023-10-05',
progress: 20,
},
{
id: 'Task 2',
name: '開発',
start: '2023-10-06',
end: '2023-10-15',
progress: 40,
dependencies: 'Task 1',
},
{
id: 'Task 3',
name: 'テスト',
start: '2023-10-16',
end: '2023-10-20',
progress: 10,
dependencies: 'Task 2',
},
];
// Reactコンポーネント(JSXを使用しない)
class GanttChart extends React.Component {
componentDidMount() {
this.gantt = new Gantt(this.ganttContainer, this.props.tasks, {
on_click: task => {
alert('タスクがクリックされました: ' + task.name);
},
on_date_change: (task, start, end) => {
console.log(`${task.name} の日付が変更されました: ${start} - ${end}`);
},
});
}
render() {
return React.createElement(
'svg',
{
ref: element => {
this.ganttContainer = element;
},
},
null
);
}
}
// レンダリング(JSXを使用しない)
ReactDOM.createRoot(document.getElementById('gantt')).render(
React.createElement(GanttChart, { tasks: tasks }, null)
);
});
</script>
</body>
</html>
頑張って解説
全体の構造
HTMLコードは以下のような構成:
-
<head>
セクション- 必要な外部ライブラリやスタイルシートの読み込み
- カスタムスタイルの定義
-
<body>
セクション- ガントチャートを表示するための要素
- スクリプトによるガントチャートの生成とレンダリング
<head>
セクションの詳細
1. DOCTYPE宣言とHTML要素の開始
そっから!?みたいなね。まあこれもこの際。
<!DOCTYPE html>
<html lang="ja">
-
<!DOCTYPE html>
は、この文書がHTML5で書かれていることを宣言します。 -
<html lang="ja">
は、文書の言語を日本語(ja
)と指定しています。
2. メタデータとタイトル
<head>
<meta charset="UTF-8">
<title>ガントチャートのデモ</title>
-
<meta charset="UTF-8">
は、文書の文字エンコーディングをUTF-8と指定しています。これにより、日本語などのマルチバイト文字が正しく表示されます。 -
<title>
は、ブラウザのタブやブックマークで表示されるページのタイトルを指定します。
3. ReactとReactDOMの読み込み
<!-- ReactとReactDOMの読み込み -->
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
- Reactは、ユーザーインターフェースを構築するためのJavaScriptライブラリ
- ReactDOMは、ReactコンポーネントをブラウザのDOM(Document Object Model)にレンダリングするためのライブラリ
-
https://unpkg.com/
は、NPMパッケージをCDN(コンテンツデリバリネットワーク)経由で提供するサービス -
crossorigin
属性は、CORS(クロスオリジンリソース共有)のための設定で、外部リソースを安全に読み込むために使用 - 用語がわかんない方はこちら
4. frappe-ganttのスタイルシートの読み込み
<!-- frappe-ganttのスタイルシート -->
<link rel="stylesheet" href="https://unpkg.com/frappe-gantt@0.5.0/dist/frappe-gantt.css">
- frappe-ganttは、ガントチャートを描画するためのJavaScriptライブラリ
-
<link rel="stylesheet">
タグで、frappe-ganttのCSSスタイルシートを読み込んでいる - これにより、ガントチャートの見た目(色、フォント、レイアウトなど)が適用される
5. frappe-ganttのスクリプトの読み込み
<!-- frappe-ganttのスクリプト(ミニファイ版を使用) -->
<script src="https://unpkg.com/frappe-gantt@0.5.0/dist/frappe-gantt.min.js"></script>
- frappe-ganttのJavaScriptファイルを読み込んでいる
- ミニファイ版とは、不要なスペースやコメントを削除してファイルサイズを小さくしたもの
- このスクリプトによって、ガントチャートを生成・操作するための機能が利用可能になる
6. カスタムスタイルの定義
<style>
body {
font-family: Arial, sans-serif;
}
#gantt {
margin: 50px;
}
</style>
-
<style>
タグ内で、ページ全体のカスタムスタイルを定義している -
body
セレクタで、ページのフォントをArial
に設定、sans-serif
はゴシック体を指定する一般的なフォントファミリーに指定 -
#gantt
セレクタで、ガントチャートを表示する要素に対してmargin: 50px;
を指定し、上下左右に50ピクセルの余白を設置
<body>
セクションの詳細
1. ページのタイトル表示
<body>
<h1>ガントチャートのデモ</h1>
-
<h1>
タグで、ページの見出しを表示 - "ガントチャートのデモ"というテキストが大きな文字で表示
2. ガントチャートを表示する要素
<!-- ガントチャートを表示する要素 -->
<div id="gantt"></div>
-
<div id="gantt"></div>
は、ガントチャートを描画するための空の要素 -
id="gantt"
で、この要素をJavaScriptから参照できるようにしている
3. ガントチャートの生成とレンダリング
<script type="text/javascript">
// スクリプト内容
</script>
-
<script>
タグ内に、ガントチャートを生成してページに表示するためのJavaScriptコードが書かれている
スクリプトの詳細解説
1. DOMContentLoadedイベントのリスナー登録
document.addEventListener('DOMContentLoaded', function() {
// コード本体
});
-
document.addEventListener('DOMContentLoaded', function() { ... });
は、ページのDOM(HTML要素のツリー構造)が完全に読み込まれたときに実行される関数を登録する - これにより、スクリプトがページの要素を安全に操作できるタイミングで実行される
2. Ganttクラスの存在確認
if (typeof Gantt === 'undefined') {
console.error('Ganttが読み込まれていません');
return;
}
-
typeof Gantt === 'undefined'
で、Gantt
というグローバル変数が定義されているか確認する - これは、frappe-ganttのスクリプトが正しく読み込まれているかをチェックするため
- もし
Gantt
が未定義なら、エラーメッセージをコンソールに表示して、スクリプトの実行を中断する
3. デモ用のタスクデータの定義
const tasks = [
{
id: 'Task 1',
name: 'デザイン',
start: '2023-10-05',
end: '2023-10-05',
progress: 20,
},
// 他のタスク
];
-
tasks
という配列に、ガントチャートに表示するタスクのデータを定義した -
各タスクはオブジェクトで表現され、以下のプロパティを持ちます:
-
id
: タスクのユニークな識別子 -
name
: タスクの名前 -
start
: タスクの開始日(YYYY-MM-DD
形式) -
end
: タスクの終了日(YYYY-MM-DD
形式) -
progress
: タスクの進捗率(0〜100の数値) -
dependencies
(オプション): 他のタスクのid
を指定し、このタスクが依存するタスクを表示
-
-
サンプルとして、3つタスクを定義:
- デザイン:10月5日に開始・終了、進捗率20%
- 開発:10月6日から10月15日まで、進捗率40%、"デザイン"に依存
- テスト:10月16日から10月20日まで、進捗率10%、"開発"に依存
4. GanttChartコンポーネントの定義
// Reactコンポーネント(JSXを使用しない)
class GanttChart extends React.Component {
// ...
}
-
GanttChart
という名前のReactコンポーネントを定義 - このコンポーネントは、ガントチャートを描画する役割を持つ
componentDidMountメソッド
componentDidMount() {
this.gantt = new Gantt(this.ganttContainer, this.props.tasks, {
on_click: task => {
alert('タスクがクリックされました: ' + task.name);
},
on_date_change: (task, start, end) => {
console.log(`${task.name} の日付が変更されました: ${start} - ${end}`);
},
});
}
-
componentDidMount()
は、コンポーネントがDOMにマウント(追加)された直後に呼び出されるライフサイクルメソッド -
ここで、
Gantt
クラスのインスタンスを生成-
this.ganttContainer
は、後述するrender
メソッドで参照を取得したsvg
要素 -
this.props.tasks
は、コンポーネントのプロパティとして渡されたタスクデータ - オプションとして、ユーザーがタスクをクリックしたり、タスクの日付を変更したときのコールバック関数を定義している:
-
on_click
: タスクがクリックされたときに呼ばれる関数。ここでは、アラートダイアログを表示 -
on_date_change
: タスクの日付が変更されたときに呼ばれる関数。ここでは、コンソールに変更内容をログ出力
-
-
-
正直ここまでインプットする必要はここではないが
renderメソッド
render() {
return React.createElement(
'svg',
{
ref: element => {
this.ganttContainer = element;
},
},
null
);
}
-
render()
メソッドは、コンポーネントが描画すべき要素を返す -
React.createElement
を使って、<svg>
要素を生成している- 第1引数:生成する要素のタイプ(ここでは
'svg'
) - 第2引数:要素のプロパティ。ここでは
ref
を設定しています-
ref
は、DOM要素への参照を取得するための特別なプロパティです -
element => { this.ganttContainer = element; }
という関数を渡すことで、このsvg
要素の参照をthis.ganttContainer
に保存する
-
- 第3引数:子要素。ここでは
null
なので、子要素はなし
- 第1引数:生成する要素のタイプ(ここでは
-
この
svg
要素が、ガントチャートが描画されるコンテナになる
5. ガントチャートのレンダリング
// レンダリング(JSXを使用しない)
ReactDOM.createRoot(document.getElementById('gantt')).render(
React.createElement(GanttChart, { tasks: tasks }, null)
);
-
ReactDOM.createRoot()
は、React 18以降で導入されたメソッドで、Reactアプリケーションのルートを作成-
document.getElementById('gantt')
で、先ほどの<div id="gantt"></div>
要素を取得し、Reactのルートに指定
-
-
render()
メソッドで、実際にコンポーネントをレンダリングする -
React.createElement(GanttChart, { tasks: tasks }, null)
で、GanttChart
コンポーネントのインスタンスを作成し、tasks
プロパティに先ほど定義したタスクデータを渡す - これにより、
GanttChart
コンポーネントが<div id="gantt">
の中に描画され、ガントチャートが表示される
まとめ
ガントチャートを表示する流れをおおまかにまとめ:
- 必要なライブラリ(React、ReactDOM、frappe-gantt)とスタイルシートを読み込む
- ガントチャートを表示するための
<div id="gantt">
要素を用意する - タスクデータを定義する
- Reactコンポーネント
GanttChart
を定義し、componentDidMount
でGantt
インスタンスを作成する -
render
メソッドで、<svg>
要素を返し、それをref
で参照する - ReactDOMを使って、
GanttChart
コンポーネントを<div id="gantt">
にレンダリングする
ポイント:
- Reactの使用:JSXを使わずに純粋なJavaScriptでReactコンポーネントを定義できた
-
frappe-ganttの統合:
Gantt
クラスを使用して、Reactコンポーネント内でガントチャートを生成できた - イベントハンドリング:タスクがクリックされたときや、日付が変更されたときにイベントを処理できた
-
DOMの参照:
ref
を使って、svg
要素への参照を取得し、Gantt
の描画先として指定した
補足:JSXを使用しないとはどういうことだったか
- 今回、JSXという記法を使用せずにReactコンポーネントを定義した
- JSXは、JavaScriptの中にHTMLみたいに書けて便利だけど、ブラウザで直接実行するためにはトランスパイル(変換)が必要
- トランスパイルにはBabelなどのツールが必要で、設定が面倒なので、今回純粋なJavaScriptでReactを使った
-
React.createElement
を使うことで、JSXを使わずに同等のコンポーネントを作成できた
意外と勉強になった話
- 外部ライブラリの読み込み:CDNを使って、外部のJavaScriptライブラリやCSSを簡単に読み込むことができた、NPM周りがあったのは驚き
- Reactの基本:コンポーネントの定義、ライフサイクルメソッド、レンダリング方法など、Reactの基本的な使い方を用いる結果になった、復習
-
クラスベースのコンポーネント:
class
を使ってReactコンポーネントを定義するやり口もここまでできると思わなかった -
DOM要素の参照取得:
ref
属性を使って、レンダリングされたDOM要素への参照を取得し、それを外部ライブラリに渡す方法を知った
おわりに
思い付きで無駄なことをやってみたものの、実現に向けてあれやこれややってみるとやっぱり知らないままやってたことは多いもんだと思いました。
復習にもなったし、やってよかった。誰かがちょっと遊ぶ気持ちで復習するときの参考になるといいな。