THIS IS PURE JS FRAMEWORK

これは基本的なJavaScriptのみで構成された宣言型コンポーネントUI型のフレームワークです。
HTMLとCSSとJavaScriptのファイルのみが許されている特殊な環境下(ReactやVueも入れられない環境)でFlutterのようなコンポーネント型プログラミングをしたいときに最適なフレームワークです。
状態管理にfJutteSに最適化された自己ライブラリJiperesを採用しており、状態管理ライブラリを選定する必要はもうありません。

  • 現行バージョン fjuttes@2.0.1

インポート方法

npm

npm経由でfjuttesを使用するにはnpm install fjuttesをコンソールで実行してからnode_modulesで使用します。

<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="fJutteS-Container"></div>
	
    <script type="module">
        import { assembleView, Text } from './node_modules/fjuttes/dist/index.mjs';
 
        assembleView(new Text("Hello World!"));
    </script>
</body>
</html>

CDN

CDN形式でnpmを使用せずにfJutteSの機能を使用するにはunpkgを使用することができます。
以下にコード例を示します。

<script src="https://unpkg.com/fjuttes@2.0.1/dist/index.mjs"></script>

詳細は後述しますが、使用するには以下のようにすることができます。

<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="fJutteS-Container"></div>
	
    <script type="importmap">
        {
            "imports": {
                "fjuttes": "https://unpkg.com/fjuttes@2.0.1/dist/index.mjs"
            }
        }
    </script>
    <script type="module" src="./script.js"></script>
</body>
</html>
//script.js
import { assembleView, Text } from "fjuttes";
 
assembleView(new Text("Hello World!"));

このように書くとただ単にテキストが表示されます。

使用方法 - チュートリアル

ウィジェットを描画する

最初にfJutteSViewを描画する方法について解説します。
まず、index.htmlなどのhtmlファイルに以下のidのdiv要素を用意します。

<div id="fJutteS-Container"></div>

そして、jsファイルで以下の処理を行います。

import { assembleView, Text } from "fjuttes";
 
assembleView(new Text("Hello World!"));

このコードで<div id="fJutteS-Container"></div>以下にassemble関数に渡したViewが描画されます。

ウィジェットの作成

Viewの継承

まず、このフレームワークには全てのウィジェットの根幹となるViewコンポーネントが提供されています。
このクラスを継承するのが、ウィジェット作成の第一段階です。

class SampleWidget extends View {
	...
}

Viewコンストラクタの呼び出し

そして、Viewクラス側でウィジェットを描画するのに必要な処理を行うためにViewのコンストラクタを呼び出します。

class SampleWidget extends View {
	constructor(){
		super();
	}
}

ウィジェットの要素定義

次にこのウィジェットのHTMLElement要素を定義します。
これにはViewクラスで定義されているcreateWrapViewをオーバーライドして作成します。
これにはJSで使用できるdocument.createElementメソッドを使用してHTMLElementを作成できます。

class SampleWidget extends View {
	constructor(){
		super();
	}
 
	createWrapView(){
		let div = document.createElement("div");
		return div;
	}
}

因みにこのcreateWrapViewはオーバーライド必須で、オーバーライドしないとエラーが出てしまいます。

throw new TypeError("createWrapViewメソッドを必ずオーバーライドして、HTMLElement型を返り値に設定してください。");

ウィジェットのスタイル定義

createWrapViewで作成したHTMLElement要素に対してスタイルを適用するにはstyledViewメソッドをオーバーライドします。

class SampleWidget extends View {
	constructor(){
		super();
	}
 
	createWrapView(){
		let div = document.createElement("div");
		return div;
	}
 
	styledView(element){
		element.className = "sample-widget";
 
		element.style.backgroundColor = "red";
		element.style.width = "100px";
		element.style.height = "100px";
 
		return element;
	}
}

styledViewメソッドには引数として、createWrapViewで作成したHTMLElementが渡されます。
スタイルの適用の詳細にはHTMLElementを参照してください。
このメソッドの最後で必ずスタイルを適用した要素をreturnで返却してください。
返却しない場合、以下のエラーが返されます。

throw new TypeError("styledViewには必ずHTMLElenmentオブジェクトを格納してください。 渡された型:", typeof child);

なお、このメソッドに用がない場合、オーバーライドせずに無視してもらっても構いません。

embedScriptToView

もしウィジェットに何らかのJSで標準用意されているスクリプトを埋め込みたいならembedScriptToView内で行ってください。
例えば、ラジオボタンのイベントの発火などです。

    embedScriptToView(element){
        this._setEventListenerToRadioBtn(element);
        return element;
    }
 
    _setEventListenerToRadioBtn(radioBtn) {
        radioBtn.addEventListener("change", (e) => {   
            if (e.target.checked) {
                //イベントの発火により動作するコード
            }
        });
    }

このメソッドでも最後に要素をreturnで返却してください。
返却しない場合、以下のエラーが返されます。

throw new TypeError("embedScriptToViewには必ずHTMLElenmentオブジェクトを格納してください。 渡された型:", typeof child);

なお、このメソッドに用がない場合、オーバーライドせずに無視してもらっても構いません。

ウィジェットの子要素を作成する。

createWrapViewで作成した要素の中に子要素を入れていくにはbuildメソッドをオーバーライドして使用します。
ここには自身で作成したウィジェットやfJutteSで用意されているコンポーネントが使用できます。

class SampleWidget extends View {
	constructor(){
		super();
	}
 
	createWrapView(){
		let div = document.createElement("div");
		return div;
	}
 
	styledView(element){
		element.className = "sample-widget";
 
		element.style.backgroundColor = "red";
		element.style.width = "100px";
		element.style.height = "100px";
 
		return element;
	}
 
	build(){
		return new Text("Hello World");
	}
}

ここではTextコンポーネントを使用して文字を表示してみます。
このとき必ず、コンポーネントやウィジェットをreturnで返却してください。
これで一つの基本的なウィジェットを作成することができました。

ウィジェットに値の受け渡し

例えば、ウィジェットに子要素を渡して、それを子要素でビルドして欲しい時や親要素のプロパティを子要素に渡して表示して欲しい時があるかもしれません。
その際のやり方をこのセクションでは解説します。

まず皆さんが親要素から渡された文字列をTextコンポーネントで表示したいとき、このように書くかもしれません。

class SampleWidget extends View {
	constructor(text){
		super();
		this.text = text;//ここでSampleWidgetのインスタンス変数に格納
	}
 
	createWrapView(){
		let div = document.createElement("div");
		return div;
	}
 
	styledView(element){
		element.className = "sample-widget";
 
		element.style.backgroundColor = "red";
		element.style.width = "100px";
		element.style.height = "100px";
 
		return element;
	}
 
	build(){
		return new Text(this.text);//ここで使用
	}
}

しかし、これを実行してみるとundefinedと表示されてしまいます。
これはViewクラス側で、createWrapViewbuildメソッドがコンストラクタで実行されているのが原因です。そのため、buildが実行し終わってからthis.text = textのコードを実行してしまいます。

この問題を回避するため、Viewクラスのコンストラクタにはpropsという引数を渡すことができます。

propsを使用して、もう一度上のコードを書き直してみます。

class SampleWidget extends View {
	constructor(text){
		super({text: text});
	}
 
	createWrapView(){
		let div = document.createElement("div");
		return div;
	}
 
	styledView(element){
		element.className = "sample-widget";
 
		element.style.backgroundColor = "red";
		element.style.width = "100px";
		element.style.height = "100px";
 
		return element;
	}
 
	build(){
		return new Text(this.props.text);//ここで使用
	}
}

propsはオブジェクトとして渡します。
これはViewクラスのインスタンス変数としてcreateWrapViewなどのメソッドが実行される前に格納されるので、buildメソッドなどで値が使用可能になります。

同様に子要素を渡された場合でも、

class SampleWidget extends View {
	constructor(child){
		super({child: child});
	}
 
	createWrapView(){
		let div = document.createElement("div");
		return div;
	}
 
	styledView(element){
		element.className = "sample-widget";
 
		element.style.backgroundColor = "red";
		element.style.width = "100px";
		element.style.height = "100px";
 
		return element;
	}
 
	build(){
		return this.props.child;
	}
}

と書くことで、簡単に子要素を描画することができます。

Providerによる状態管理

このfJutteSフレームワークにはJiperesという状態管理ライブラリが付属しています。
値が変更されたことによって、ウィジェットをリビルド、再描画したい際にはProviderを使用して行います。

Providerの作成

Providerを作成するにはProviderクラスのファクトリメソッドcreateProvider()を使用して行います。
以下に試しに作成してみます。

const sampleProvider = Provider.createProvider(() => {
	return 0;
})

引数には関数オブジェクトを渡し、その中で初期値をreturnで返却します。
(これは基本的な数値を管理するProviderであり、Providerには依存関係などの機能もあります。)

Providerの使用-ProviderScope-read

Providerの値の変更を監視するためにはView単位で行います。
fJutteSでは、値の変更を自動的に監視し、再描画を行うProviderScopeというインターフェースを提供しています。
ProviderScopeコンポーネントを継承してウィジェットを作成します。

class SampleWidget extends ProviderScope {
	constructor(child){
		super({
			child: child,
			watchingProviders: [ sampleProvider ]
		});
	}
 
	createWrapView(){
		let div = document.createElement("div");
		return div;
	}
 
	styledView(element){
		element.className = "sample-widget";
 
		element.style.backgroundColor = "red";
		element.style.width = "100px";
		element.style.height = "100px";
 
		return element;
	}
 
	build(){
		let num = sampleProvider.read();
 
		return Row([
			this.props.child,
			new Text(num)
		]);
	}
}

ProviderScopeクラスにはコンストラクタとして、三つのプロパティを渡すことができます。
propswatchingProviderchildです。
propsにはViewと同じ役割を持ちます。
watchingProviderには、Providerの配列を渡します。
ProviderScopeに渡されたProviderは自動的にリッスン状態になり、配列のProviderの一つでも値が変更されると、ProviderScopeを継承したウィジェットが再ビルドされます。

ここではProviderクラスのreadメソッドを使用して値を読み取っています。
readメソッドはただ値を読み取るためのメソッドです。

Providerの使用-ProviderScope-update

Providerの値を変更してウィジェットを再描画するにはProviderクラスのupdateメソッドを使用します。
今回はElevatedButtonを押したらcounterの値をインクリメントして、Textに反映されるコードを作成してみます。

import { 
    assembleView, 
    Text, 
    Card, 
    Column,
    ElevatedButton,
    BaseCSS,
    SpaceBox,
    Center, 
    TextCSS, 
    FontCSS, 
    Provider, 
    ProviderObserver, 
    ProviderScope,
    ShadowLevel,
} from './node_modules/fjuttes/dist/index.mjs';
 
const counter = Provider.createProvider((ref) => {
    return 0;
}, "counter");
 
class ProviderExample extends ProviderScope {
    constructor(){
        super({
            watchingProviders: [ counter ]
        });
    }
 
    createWrapView(){
        return document.createElement("div");
    }
 
    styledView(element){
        element.style.height = "90vh";
 
        return element;
    }
 
    build(){
        counter.read()
 
        return new Center(
            new Card({
                radius:"16px",
                padding: "15px",
                background: "wheat",
                elevation: ShadowLevel.LVL5,
                child: new Column([
                    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プロパティにてProviderupdateを実行しています。

全てのコードを見る場合はこちらから確認することができます。
https://github.com/Rerurate514/fJutteS/blob/main/example-code/providerExample.html

Provider例-依存関係

Providerクラスには依存関係を管理する機能があります。
ここでは簡単なユーザを管理するProviderを作成します。

//プロバイダーを作成
const userProvider = Provider.createProvider(ref => {
    return { name: "Jhon", age: 25 };
});

そしてuserProvider内のageを監視するには以下のようにrefを使用してproviderを作成します。

const userAgeProvider = Provider.createProvider(ref => {
    ref.watch(userProvider, (user, currentValue) => {
        return user.age;
    });
    return ref.read(userProvider).age;
});

このように記述すると、自動的にuserProviderがリッスン状態になり、userProviderageが変更された際にuserAgeProviderの値を自動的に変更します。これはwatchまたはProviderScopeuserAgeProviderの変更を監視することができます。

ProviderObserverによる値の変更確認

JiperesにはProviderObserverというProviderの値の変更履歴や依存関係を記録するクラスが実装されています。
このクラスを使用するにはまず、ログの出力を有効にします。

new ProviderObserver().outLogs()

ログの出力を有効にすると以下のコードを使用してログを確認することができます。

  • Providerの更新履歴:console.log(new ProviderObserver().getAllUpdateHistory());
  • 特定のProviderの更新履歴:console.log(new ProviderObserver().getFilteredUpdateHistory(userProvider));
  • Providerの依存関係を表示:console.log(new ProviderObserver().getDependencyGraph());

用語集

  • View(ビュー):Viewクラスまたはその他UI構築クラスから継承して作成されたUI部品
  • コンポーネント:fJutterS側から提供されるViewのこと
  • ウィジェット:fJutteS使用者がコンポーネントを組み合わせて作成したViewのこと

最後に余談

//TODO

ライセンス

MIT