ogochan
おごちゃんです。
たまにはウェブ系の話でも。
弊社では適当な管理ソフトを作る時には、AdminLTEを使ってます。
あまり難しいことを考えなくても使えますし、Bootstrapも同じような感じなので、「別に気合い入れてデザインとかしたくなくって、動かせれば十分満足」みたいな用途には、とても便利です。
さて、BootstrapもVer 5になりました。Ver 5で一番大きな変化は、
jQuery依存がなくなった
ことではないかと思います。
ちょうど世間ではjQueryに逆風が吹いていて、あまり流行らなくなってしまいました。この辺はウェブ屋はみんな知っていることですね。
ところがそうなると困るのは、「Boostrapと併せて使うと便利だったAdminLTEをどうするか」ということです。
AdminLTEはBootstrap4に依存しています。なので、Bootstap5と組み合わせて使うのは、困難が伴います。
さらに、仮に無理やり組み合わせても、せっかくjQuery依存がなくなったBootstrap5に、AdminLTEのためにjQueryを入れるのは、どうも釈然としません。
そこでいろいろ悩むわけです。
- AdminLTEのBootstap5対応版を待つ
開発は進んでいるようで、「v4-dev」というブランチがあります - AdminLTEのv4-devを使う
そこそこ動いているようで、デモは十分動いてます - AdminLTEを捨てて自作する
他のダッシュボードフレームワークを使うなり、自作するなり
3は工数を考えると面倒臭いし、あれこれ余分なことを考えるとキリがなくなるので、早々に諦めました。もちろん、外部公開するつもりのアプリであれば、そうやってガッツリ自作するというのもアリでしょうが、雑に管理ソフトを作りたい時にそこまでやるのは合いません。
AdminLTEのBootstap5対応版を待つ
これが一番間違いのない方法です。また、「v4-dev」というブランチもあることなので、しばらく待ってみることにしました。
ところが、待てど暮せど出て来ません。Bootstrap5がαとして出たのが2020年6月、最初に私が必要性を感じた頃から半年、リリースをしたのが今年の5月頃なので、世の中のかなりのBootstrapに依存したものがVer 5に移行しているのに、我等がAdminLTEはいまだに4のまま。なかなか出て来ません。
一方、こちらはダッシュボードが欲しいシステムは、複数個あります。しょうがないので「仮」の状態で作りかけたりしました。
でも、出ないものはしょうがありません。
AdminLTEのv4-devを使う
あまりに出て来ないので、Bootstrap5に対応したブランチである、「v4-dev」を使ってみることにしました。
なぜそうしなかったかと言えば、npm一発で入ってくれないということが一番の心理的ハードルでした。npmで入れてないものがあると、管理が厄介という先入観もあり、また変な入れ方すると後でいろいろ直したりしないといけないというのもあって避けていました。「野良」であまりいろいろ入れたくありませんから。
とは言え、入れることそれ自体そんなに難しくありません。v4-devブランチを落として来て、README.mdの「Compile dist files」に従ってコンパイルすれば/distが作られますので、それを使えば良いです。「場所」の問題は、
./node_modulesの下からリンクを張る
ことで解決しました。全ての野良buildがこれで良いかどうかは別にして、AdminLTEについてはこれで足りました。
まずはデモを動かします。
いつものお馴染みの奴がちゃんと出て来ます。
最初はサイドバーがちゃんと動かなかったりするのですが、よく見るとJSがロードされてなかったり、そのJSはminifyを前提としてたりとかわかるのですが、./distの下にjsというフォルダが存在しないことに気がついたりします。どうもgulpのスクリプトを見る限り、minifyのあたりがうまくされてないようだったので、無理やりコピーするなり何なりしてやれば、動くようになります。本当はgulpfile.jsをいじってやるのが良いと思うのですが、とりあえずは動けばいいだけなので、TypeScriptからコンパイルされてテンポラリディレクトリに置かれているJSを./dist/jsの下に配置してやることにしました。
そういったことをすれば動くようになります。これで十分な人はここでやめておけば、深追いしなくて済むでしょう。古典的な(Javascriptをあまり使ってない)Railsのアプリで使うのであれば、これくらいで十分かも知れません。しかし、今使いたいのはもうちょっとフロントで頑張ったものなので、ここで終わりにはなりません。
Riot.jsで使ってみる
次はRiot.jsにnavbarとsidebarをコンポーネント化してつっこんでやることを考えます。
と言うのも、弊社でJSバリバリのフロントを書く時のフレームワークは、現在のところRiot.jsが基本だからです。この辺については、いつかまた書くかも知れませんが、とりあえずそーゆーことだと思って下さい。
通常、Riot.jsでコンポーネントをinvokeする時は、コンポーネント名をタグにして書くと思うのですが、この書き方をするとCSSがうまく当たってくれません。なぜなら、この方法でinvokeすると「コンポーネント名のタグ」そのものはそこに存在していて、その子要素としてコンポーネントの中身が展開されるからです。当然ブラウザはそのような「要素」のことは何も知りませんので、「虚無の要素」があることになります。その結果何が起きているかと言えば、「コンポーネント名のタグ」が余分に入っているため、CSSの階層の解釈の邪魔になってしまうのですね。
そこで、この方法を諦めて「is」を使う方法に改めます。なぜかこうすると「虚無の要素」が存在しなくなるため、CSSがうまく当たってくれます。
こうやると、とりあえずそれっぽい画面が作られます。テストのために、「AdminLTEのデモをRiot.jsを使ってコンポーネント化したもの」を作ってみましたが、ページそのものは正しく表示されるようになりました。また、navbarにあるdropdown等もちゃんと動いてくれます。ちなみに、navbarの中のdropdownの動きはBootstrapのJSの中に記述してあります。
ところが、sidebarにあるメニューの動きが変です。アコーディオン状に動くtreeviewが開いてくれません。要するに、AdminLTEのJSに記述された動作が全く動きません。最初は、CSSの時と同じようなことが起きているのかと思って試行錯誤してみたのですが、それではラチが明かないので、JS(TypeScript)のコードを追ってみました。まぁ、「追って」と言う程の規模でもないんですけど。
このJSやっていることは簡単で、そういった動作(treeviewが動いたりハンバーガーでメニューの大きさが変わったり)をするための関数をそれぞれの要素に貼りつけている。単にそれだけです。コンパイルされたJSで1000行もないものなので、そんなに大したことをやってるわけではありません。
問題はその貼りつけ方にあって、この処理が行われるのはDOMContentLoadedイベントが発生した時だけです。このイベントはいつ発生するかと言えば、
DOMContentLoaded イベントは、最初の HTML 文書の読み込みと解析が完了したとき、スタイルシート、画像、サブフレームの読み込みが完了するのを待たずに発生します。
つまり、ロードの完了した時に発生するわけです。
ところがRios.js(他のUIフレームワークでも同じだと思いますが)は、その後からコンテンツの構築処理(mount)が始まります。つまり、件のイベントが発生した時には、まだコンポーネントは存在していません。となると、メニューの動きを司る関数は、それぞれの(AdminLTE的)コンポーネントには貼りつけられません。その結果、メニューの動作は出来ないということになります。
要するに、現時点のAdminLTEは、Rios.js(とゆーか今風のUIフレームワーク)の中ではマトモに動かないということになります。
これが現時点だからそうなのか、将来に渡ってそうなのかはわかりません。ただ、事実としてそうだということです。つまり、
AdminLTEをBootstrap5の上で動かすことは可能だが、JS部分は動いてくれない
ということです。
そんなわけで、2の方法もダメだというとになります。
AdminLTEを捨てて自作する
しょうがないので、AdminLTEを捨てて自作することを考えます。
「ダッシュボード」のフレームワークは結構多くの機能を持っていますが、結局のところ使うのは「サイドメニュー」と「card」くらいなもので、後は「あれば便利」という程度であったり、元となるUIフレームワーク(Bootstrap)の機能で足りていたりということで、そこまでたいそうなものではありません。
そこで、「まぁ機能が低い分は追い追いつけ加えるとして、とりあえず3の方法で解決つけるか」ということで、
新しくなったBootstrap 5を使用して、管理画面のUIを実装する方法を解説
を参考にしつつ、AdminLTEのCSSを解析しつつ、CSSを自前で書くことにしました。AdminLTEのデモ画面が表現できる程度のCSSを用意してしまえば、後でBootstrap5対応のAdminLTEが出た時にもスムーズに移行できますから。
そうやってるうちに、ふと「こんなにAdminLTEからコピペしてCSSを作るんだったら、最初からAdminLTEのCSSを使えば良いんじゃないか?」ということが頭に浮かびます。件のサイトに書かれていることを忠実に作っても、出来るのはCSSだけですから。
他方、既に書いたように、treeviewのような便利なダッシュボードコンポーネントは案外使ってなかったことに気がつきます。私の書く実際のアプリの場合、開く時にコンテンツを動的にgetしたりしています。つまり、元々ついて来るJSは使ってないわけです。逆に、むしろ邪魔者扱いしていたり。そう考えると、
AdminLTEのCSSだけを使い、JS部分は自前のものだけを使う
という選択肢があるということに気がつきました。と言うか、結局それが一番楽なのではないかと。
というわけで、2と3の間のような方法で解決してしまうことにしました。結局この手のフレームワークに期待しているのは、CSSだけだったりしますからね。
ということで、とりあえず今のところ過不足ない程度には「Bootstrap5でAdminLTEを使う」ことが達成されています。
PS
さらにいろいろ進めていてわかったのですが、CSSそのものの実装は随分足りていません。
具体的に言えば、リリース版のAdminLTEでは、サンプルがいっぱいついて来ますが、v4-devにはあまりありません。なぜなら、あの大量にあるサンプルが全部ちゃんと表示できるわけではないからです。
もうちょっと具体的な例だと、Ver 3にある「ログインページ」のサンプル、一見そのままで動くように見えるのですが、入力欄の右端にあるアイコン(classで言えば、input-group-append)が綺麗に表示されません(paddingだかmarginが足りない感じになる)。なので、こういったものは避ける必要があります。
もちろん直せばいいんですが、現在のステータスを考えると、直してPRを送るくらいの気がないのであれば諦めた方が良いと思います。
要するに今のところ使えるのは、「サンプルがあるもの」だけだと思って良いでしょう。
PS2.
Javascriptがうまく動いてくれない件については、結局元のコードを参考にしつつ、自前で書くことにしました。
これは特に難しいことはなく、原理としては
class名をすげ替えてCSSをスイッチ
しているだけです。つまり、何かのイベント等をトリガーとして、異なる状態を表現しているCSSにすり替えるような処理をしているだけです。
ある程度Javascriptのわかる人であれば、元々のコードを参考に書くことは容易でしょう。今回書いたのは、以下のようなものです。
const CLASS_NAME_SIDEBAR_MINI = 'sidebar-mini';
const CLASS_NAME_SIDEBAR_MINI_HAD = 'sidebar-mini-had';
const CLASS_NAME_SIDEBAR_HORIZONTAL = 'sidebar-horizontal';
const CLASS_NAME_SIDEBAR_COLLAPSE$1 = 'sidebar-collapse';
const CLASS_NAME_SIDEBAR_CLOSE$1 = 'sidebar-close';
const CLASS_NAME_SIDEBAR_OPEN$1 = 'sidebar-open';
const CLASS_NAME_SIDEBAR_OPENING = 'sidebar-is-opening';
const CLASS_NAME_SIDEBAR_COLLAPSING = 'sidebar-is-collapsing';
const CLASS_NAME_SIDEBAR_IS_HOVER = 'sidebar-is-hover';
const CLASS_NAME_MENU_OPEN$1 = 'menu-open';
const SELECTOR_SIDEBAR = '.sidebar';
const SELECTOR_NAV_SIDEBAR = '.nav-sidebar';
const SELECTOR_NAV_ITEM$1 = '.nav-item';
const SELECTOR_NAV_TREEVIEW = '.nav-treeview';
const SELECTOR_MINI_TOGGLE = '[data-lte-toggle="sidebar-mini"]';
const SELECTOR_FULL_TOGGLE = '[data-lte-toggle="sidebar-full"]';
const bodyClass = document.body.classList;
....
//
// for AdminLTE menu operations
//
sidebarOpening() {
bodyClass.add(CLASS_NAME_SIDEBAR_OPENING);
setTimeout(() => {
bodyClass.remove(CLASS_NAME_SIDEBAR_OPENING);
}, 1000);
},
sidebarColllapsing() {
bodyClass.add(CLASS_NAME_SIDEBAR_COLLAPSING);
setTimeout(() => {
bodyClass.remove(CLASS_NAME_SIDEBAR_COLLAPSING);
}, 1000);
},
menusClose() {
const navTreeview = document.querySelectorAll(SELECTOR_NAV_TREEVIEW);
for (const navTree of navTreeview) {
navTree.style.removeProperty('display');
navTree.style.removeProperty('height');
}
const navSidebar = document.querySelector(SELECTOR_NAV_SIDEBAR);
const navItem = navSidebar === null || navSidebar === void 0 ? void 0 : navSidebar.querySelectorAll(SELECTOR_NAV_ITEM$1);
if (navItem) {
for (const navI of navItem) {
navI.classList.remove(CLASS_NAME_MENU_OPEN$1);
}
}
},
expand(event) {
this.sidebarOpening();
bodyClass.remove(CLASS_NAME_SIDEBAR_CLOSE$1);
bodyClass.remove(CLASS_NAME_SIDEBAR_COLLAPSE$1);
bodyClass.add(CLASS_NAME_SIDEBAR_OPEN$1);
},
collapse(event) {
this.sidebarColllapsing();
bodyClass.add(CLASS_NAME_SIDEBAR_COLLAPSE$1);
},
close(event) {
bodyClass.add(CLASS_NAME_SIDEBAR_CLOSE$1);
bodyClass.remove(CLASS_NAME_SIDEBAR_OPEN$1);
bodyClass.remove(CLASS_NAME_SIDEBAR_COLLAPSE$1);
if ( bodyClass.contains(CLASS_NAME_SIDEBAR_HORIZONTAL) ) {
this.menusClose();
}
},
sidebarHover() {
const selSidebar = document.querySelector(SELECTOR_SIDEBAR);
if (selSidebar) {
selSidebar.addEventListener('mouseover', () => {
bodyClass.add(CLASS_NAME_SIDEBAR_IS_HOVER);
});
selSidebar.addEventListener('mouseout', () => {
bodyClass.remove(CLASS_NAME_SIDEBAR_IS_HOVER);
});
}
},
toggleFull() {
if ( bodyClass.contains(CLASS_NAME_SIDEBAR_CLOSE$1) ) {
this.expand();
}
else {
this.close();
}
if ( bodyClass.contains(CLASS_NAME_SIDEBAR_MINI) ) {
bodyClass.remove(CLASS_NAME_SIDEBAR_MINI);
bodyClass.add(CLASS_NAME_SIDEBAR_MINI_HAD);
}
},
toggleMini() {
if ( bodyClass.contains(CLASS_NAME_SIDEBAR_MINI_HAD) ) {
bodyClass.remove(CLASS_NAME_SIDEBAR_MINI_HAD);
bodyClass.add(CLASS_NAME_SIDEBAR_MINI);
}
if ( bodyClass.contains(CLASS_NAME_SIDEBAR_COLLAPSE$1) ) {
this.expand();
}
else {
this.collapse();
}
},
....
Riot.jsの中だとこんな感じです。ね。簡単でしょう?