A UI component library built with TypeScript
npm install ftuttes
fTutteSとは、TypeScriptで構成されたライブラリです。Flutterのような書き心地でWeb画面をデザインすることができます。fTutteSの中には、core、taterials、cssKit、tiperes、tommand、transitusという6つのライブラリを備えています。(後述)
shell
npx tommand create-ftuttes プロジェクト名
`
さらにtransitusを使用して、Webルーティングを行いたかったり、単体で動作させたい場合は以下のコマンドを使用します。
`shell
npx tommand create-transitus-ftuttes プロジェクト名
`
npxが使用できない場合はSETUP.mdを参照してください。
または、npxパッケージのダウンロード元テンプレートからファイルをローカルに落としてください。
用語集
- View(ビュー):Viewクラスまたはその他UI構築クラスから継承して作成されたUI部品
- コンポーネント:fTutteS側から提供されるViewのこと
- ウィジェット:fTutteS使用者がコンポーネントを組み合わせて作成したViewのこと
- インターフェース:fTutteSが提供している継承することで機能を使用できるクラスのこと。(ViewやProviderScopeなど)
実際の使用方法
$3
#### Viewの継承
Viewインターフェースの詳細はVIEW_INTERFACE_USAGE.mdにも示されています。このREADMEでは基本的で最も小さい形でViewを説明します。
まず、このフレームワークには全てのウィジェットの根幹となるViewインターフェースが提供されています。
このクラスを継承するのが、ウィジェット作成の第一段階です。
`ts
class SampleWidget extends View {
...
}
`
#### Viewコンストラクタの呼び出し
そして、Viewクラス側でウィジェットを描画するのに必要な処理を行うためにViewのコンストラクタを呼び出します。
`ts
class SampleWidget extends View {
constructor(){
super();
}
}
`
#### ウィジェットの要素定義
次にこのウィジェットのHTMLElement要素を定義します。
これにはViewクラスで定義されているcreateWrapViewをオーバーライドして作成します。
これにはJSで使用できるdocument.createElementメソッドを使用してHTMLElementを作成できます。
`ts
class SampleWidget extends View {
constructor(){
super();
}
override createWrapView(){
let div = document.createElement("div");
return div;
}
}
`
もし作成したい要素がdivならばオーバーライドする必要はないです。
今回の例ではオーバーライドせずに例を示します。
#### ウィジェットのスタイル定義
createWrapViewで作成したHTMLElement要素に対してスタイルを適用するにはstyledViewメソッドをオーバーライドします。
`ts
class SampleWidget extends View {
constructor(){
super();
}
override styledView(element: HTMLElement): HTMLElement {
element.className = "sample-widget";
element.style.backgroundColor = "red";
element.style.width = "100px";
element.style.height = "100px";
return element;
}
}
`
styledViewメソッドには引数として、createWrapViewで作成したHTMLElementが渡されます。
スタイルの適用の詳細にはHTMLElementを参照してください。
このメソッドの最後で必ずスタイルを適用した要素をreturnで返却してください。
返却しない場合、以下のエラーが返されます。
`error
throw new TypeError("styledViewには必ずHTMLElenmentオブジェクトを格納してください。 渡された型:", typeof child);
`
なお、このメソッドに用がない場合、オーバーライドせずに無視してもらっても構いません。
#### embedScriptToView
もしウィジェットに何らかのJSで標準用意されているスクリプトを埋め込みたいならembedScriptToView内で行ってください。
例えば、ラジオボタンのイベントの発火などです。
`ts
override embedScriptToView(element: HTMLLabelElement): HTMLLabelElement {
this._setEventListenerToRadioBtn(element);
return element;
}
private _setEventListenerToRadioBtn(radioBtn: HTMLLabelElement): void {
const radioInput = radioBtn.firstElementChild as HTMLInputElement;
radioInput.addEventListener("change", (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.checked) {
//イベントの発火により動作するコード
}
});
}
`
このメソッドでも最後に要素をreturnで返却してください。
返却しない場合、以下のエラーが返されます。
`error
throw new TypeError("embedScriptToViewには必ずHTMLElenmentオブジェクトを格納してください。 渡された型:", typeof child);
`
なお、このメソッドに用がない場合、オーバーライドせずに無視してもらっても構いません。
#### ウィジェットの子要素を作成する。
createWrapViewで作成した要素の中に子要素を入れていくにはbuildメソッドをオーバーライドして使用します。
ここには自身で作成したウィジェットやfTutteSで用意されているコンポーネントが使用できます。
`ts
class SampleWidget extends View {
constructor(){
super();
}
override styledView(element){
element.className = "sample-widget";
element.style.backgroundColor = "red";
element.style.width = "100px";
element.style.height = "100px";
return element;
}
override build() {
return new Text({
text: "Hello World"
});
}
}
`
ここではTextコンポーネントを使用して文字を表示してみます。
このとき必ず、コンポーネントやウィジェットをreturnで返却してください。
これで一つの基本的なウィジェットを作成することができました。
$3
例えば、ウィジェットに子要素を渡して、それを子要素でビルドして欲しい時や親要素のプロパティを子要素に渡して表示して欲しい時があるかもしれません。
その際のやり方をこのセクションでは解説します。
まず、ftuttesでは親要素から渡された文字列をTextコンポーネントで表示したいとき、このように記述します。
`ts
class SampleWidget extends View {
private text: string;
constructor(text: string){
super();
this.text = text;//ここでSampleWidgetのインスタンス変数に格納
}
override styledView(element: HTMLElement){
element.className = "sample-widget";
element.style.backgroundColor = "red";
element.style.width = "100px";
element.style.height = "100px";
return element;
}
override build() {
return new Text({
text: this.text
});
}
}
`
同様に子要素を渡された場合でも、
`ts
class SampleWidget extends View {
private child: View;
constructor(child: View) {
super();
this.child = child;
}
override styledView(element: HTMLElement){
element.className = "sample-widget";
element.style.backgroundColor = "red";
element.style.width = "100px";
element.style.height = "100px";
return element;
}
override build() {
return new Column({
children: [
this.child,
this.child,
this.child,
]
});
}
}
`
と書くことで、簡単に子要素を描画することができます。
$3
このfTutteSフレームワークにはTiperesという状態管理ライブラリが付属しています。
値が変更されたことによって、ウィジェットをリビルド、再描画したい際にはProviderを使用して行います。
#### Providerの作成
Providerを作成するにはProviderクラスのファクトリメソッドcreateProvider()を使用して行います。
以下に試しに作成してみます。
`ts
const sampleProvider = Provider.createProvider(() => {
return 0;
})
`
引数には関数オブジェクトを渡し、その中で初期値をreturnで返却します。これはプリミティブな数値を管理、保持、監視するProviderです。ただし、なんの設定もしていないと値の変更の監視はできません。
#### Providerの使用-ProviderScope-read
Providerの値の変更を監視するためにはView単位で行います。
fTutteSでは、値の変更を自動的に監視し、再描画を行うProviderScopeというインターフェースを提供しています。
ProviderScopeを継承してウィジェットを作成します。
`ts
import { assembleView, Column, ElevatedButton, Provider, ProviderScope, Row, Text, View } from "ftuttes";
const sampleProvider = Provider.createProvider(() => {
return 0;
});
class SampleWidget extends ProviderScope {
constructor(private child: View, providers: Provider[]){
super({
providers: providers
});
}
override styledView(element: HTMLElement): HTMLElement{
element.className = "sample-widget";
element.style.backgroundColor = "yellow";
element.style.width = "100px";
element.style.height = "100px";
return element;
}
override build(){
let num = sampleProvider.read();
return new Column({
children: [
new ElevatedButton({
onClick: () => {
sampleProvider.update((currentValue) => {
return currentValue + 1;
})
},
child: new Text({
text: "Click Here!"
}),
}),
new Row({
children: [
this.child,
new Text({
text: num.toString()
})
]
}),
]
});
}
}
assembleView(
new SampleWidget(
new Text({
text: "value="
}),
[ sampleProvider ]
);
);
`
ProviderScopeクラスにはコンストラクタとして、三つのプロパティを渡すことができます。
propsとwatchingProvider、childです。
propsにはViewと同じ役割を持ちます。
watchingProviderには、Providerの配列を渡します。
ProviderScopeに渡されたProviderは自動的にリッスン状態になり、配列のProviderの一つでも値が変更されると、ProviderScopeを継承したウィジェットが再ビルドされます。
ここではProviderクラスのreadメソッドを使用して値を読み取っています。
readメソッドはただ値を読み取るためのメソッドです。
#### Providerの使用-ProviderScope-update
Providerの値を変更してウィジェットを再描画するにはProviderクラスのupdateメソッドを使用します。
今回はElevatedButtonを押したらcounterの値をインクリメントして、Textに反映されるコードを作成してみます。
`ts
import {
assembleView,
Text,
Card,
Column,
ElevatedButton,
BaseCSS,
SpaceBox,
Center,
TextCSS,
FontCSS,
Provider,
ProviderObserver,
ProviderScope,
ShadowLevel,
} from 'ftuttes';
const counter = Provider.createProvider((ref) => {
return 0;
}, "counter");
class ProviderExample extends ProviderScope {
constructor(){
super({
watchingProviders: [ counter ]
});
}
override styledView(element: HTMLElement){
element.style.height = "90vh";
return element;
}
override build(){
return new Center(
new Card({
radius:"16px",
padding: "15px",
background: "wheat",
elevation: ShadowLevel.LVL5,
child: new Column({
children: [
new ElevatedButton({
child: new Text("CLICK!"),
baseCSS: new BaseCSS({
height: "32px",
}),
onClick: () => {
counter.update((value) => {
return value + 1;
})
}
}),
new SpaceBox({height: "16px"}),
new Text("click count : " + counter.read()),
]
}),
})
);
}
}
assembleView(
new ProviderExample()
);
`
ElevatedButtonコンポーネントのonClickプロパティにてProviderのupdateを実行しています。
updateにはそのProviderの現在の値が渡されるので、その値にインクリメントをしてreturnで返却し値を変更しています。
その結果、ProviderScopeを継承したウィジェット自身がProvider内の値の変更を検知し自身を再描画します。
#### Provider例-依存関係
Providerクラスには依存関係を管理する機能があります。
ここでは簡単なユーザを管理するProviderを作成します。
`ts
//プロバイダーを作成
const userProvider = Provider.createProvider(ref => {
return { name: "Jhon", age: 25 };
});
`
そしてuserProvider内のageを監視するには以下のようにrefを使用してproviderを作成します。
`ts
const userAgeProvider = Provider.createProvider(ref => {
ref.watch(userProvider, (user, currentValue) => {
return user.age;
});
return ref.read(userProvider).age;
});
`
このように記述すると、自動的にuserProviderがリッスン状態になり、userProviderのageが変更された際にuserAgeProviderの値を自動的に変更します。これはwatchまたはProviderScopeでuserAgeProviderの変更を監視することができます。
#### LimitedProviderScope
ProviderScopeインターフェースはViewを継承しなければならず、さらにwatchしているproviderの値が変更されるたびに再描画されてパフォーマンスが下がってしまいます。
これを解決するためにfTutteSはそのrebuildのスコープを狭めてくれるLimitedProviderScopeコンポーネントを提供しています。
`ts
import {
assembleView,
BaseCSS,
Center,
Column,
ElevatedButton,
LimitedProviderScope,
Provider,
SpaceBox,
Text,
View
} from "ftuttes";
const counter = Provider.createProvider(() => {
return 0;
}, "counter");
class ProviderExample extends View {
constructor(){
super();
}
override styledView(element: HTMLElement): HTMLElement{
element.style.height = "90vh";
return element;
}
override build(){
return new Center({
child: new Column({
children: [
new ElevatedButton({
child: new Text({
text: "CLICK!"
}),
baseCSS: new BaseCSS({
padding: "32px",
}),
onClick: () => {
counter.update((value: any) => {
return value + 1;
})
}
}),
new SpaceBox({height: "16px"}),
new LimitedProviderScope({
providers: [ counter ],
builder: ([providerValue]) => {
return new Text({
text: "click count : " + providerValue
});
}
})
]
}),
});
}
}
assembleView(
new ProviderExample()
);
`
通常のProviderScopeを継承したやり方では、このProviderExampleウィジェット全体が再描画されてしまいます。しかし、このLimitedProviderScopeを使用したやり方ではTextコンポーネントのみが再描画されます。このbuild関数オブジェクトの引数ですが、providerをprovidersで格納した順番でそれぞれのProviderの値が格納された配列が返されます。
#### ProviderObserverによる値の変更確認
TiperesにはProviderObserverというProviderの値の変更履歴や依存関係を記録するクラスが実装されています。
そして、以下のコードを使用してログを確認することができます。
##### Providerの更新時、依存関係構築時にログを出力する。
`ts
new ProviderObserver().outLogs()
`
##### Providerの更新履歴
`ts
console.log(new ProviderObserver().getAllUpdateHistory());
`
##### 特定のProviderの更新履歴
`ts
console.log(new ProviderObserver().getFilteredUpdateHistory(userProvider));
`
##### Providerの依存関係を表示
`ts
console.log(new ProviderObserver().getDependencyGraph());
`
$3
さらに、TiperesにはProviderだけでなくNotifierも作成することができます。
NotifierはProviderの値変更をもっと柔軟に定義することができるクラスです。
まず初めにNotifierを継承したクラスを作成します。
今回は簡単に値を増加させたり、初期値に戻したりする動作を想定します。
ジェネリクスには管理する値の型を入れます。
値の変更には、Notifierクラス内で実装されているupdateメソッドを使用して行います。
`typescript
class CountNotifier extends Notifier {
protected build(): number {
return 0;
}
public increment() {
this.update((arg) => {
return ++arg;
});
}
public reset() {
this.update((arg) => {
return 0;
});
}
}
`
そしてこのNotifierクラスを使用するにはNotifierProviderにこの作成したクラスを渡します。
`typescript
let cnp: NotifierProvider = new NotifierProvider(
() => new CountNotifier()
);
`
この作成したNotifierProviderはLimitedProviderScopeで監視、状態変更を行うことができます。
定義したNotifierProviderからNotifierで定義したメソッドにアクセスするにはNotifierProvider内のプロパティであるnotifierからアクセスすることができます。
例:cnp.notifier.reset()
LimitedProviderScopeにNotifierProviderを渡した際には、Notifierがbuilderメソッドの引数として渡されるのでstateにアクセスして値を表示することができます。
`typescript
new Column({
children: [
new LimitedProviderScope({
providers: [ cnp ],
builder(count) {
return new Text({
text: count[0].state,
})
},
}),
new ElevatedButton({
radius: "4px",
onClick: () => {
cnp.notifier.increment();
},
child: new Center({
child: new Text({
text: "increment",
})
})
}),
new ElevatedButton({
radius: "4px",
onClick: () => {
cnp.notifier.reset();
},
child: new Center({
child: new Text({
text: "reset",
})
})
}),
]
})
``