JavaScript用フレームワークfairy support jsの基本的な使い方

# fairy support jsの特徴
仮想DOM使わない
簡単に使えて学習コストが少ない(仮想DOMではないのでJavaScriptの知識だけで使える)
昔ながらの伝統的なつくりのWebシステムに途中から導入可能
詳しくは[https://doc.fairysupport.com/js/ja/index.html](https://doc.fairysupport.com/js/ja/index.html)

# インストール
1.[ここから(https://github.com/fairysupport/js_start/archive/refs/heads/master.zip)](https://github.com/fairysupport/js_start/archive/refs/heads/master.zip)zipファイルをダウンロード
2.解凍する
3.解凍したフォルダ内のpackage.jsonのある場所にコマンドラインで移動
`npm install`
(npmは事前に使えるようにしておいてください)
完了

# 起動
`npm run watch_local`
これがビルドです
src配下のソースに変更があると、自動でビルドしてdistWorkにビルド結果を格納してくれます
開発中は起動しっぱなしでいいです
止めるときはCtrl+Cです
動かなくなったら、Ctrl+Cして再度npm run watch_localしてください

# 動作確認
コマンドラインをもう一つ起動して
`npm run server`
これで確認用の簡易サーバーが起動します
http://localhost:8000/page/index.html
にブラウザでアクセス
sampleボタンだけの簡単な画面が表示されるので、このボタン押下→Hello fairysupportがalertされればOK

# 基本的な書き方
とりあえず、書いて動かしてみる

src\page\sample.html作成

```sample.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
sample
</title>
<script id="fs-js" src="http://localhost:8000/js/fairysupport.min.js" data-page-root="http://localhost:8000/page"></script>
</head>
<body>
    <div data-obj="field1"></div>
    <div data-list="field2"></div>
    <div data-list="field2"></div>
    
    <div><input type="text" data-name="txt1"></div>
    <div><input type="text" data-obj="txt2"><input type="button" data-name="btn1" value="btn1"></div>
</body>
</html>
```

src\js\modules\sample.js作成

```sample.js
export class Sample {

    field1;
    field2;
    txt2;

    constructor() {
    }

    init() {
        /* 初期処理実装したい場合ここ */
    }
    
    txt1_input(event) {
        this.field1.textContent = event.currentTarget.value;
    }

    btn1_click(event) {
        this.field2.forEach(item => item.textContent = this.txt2.value);
    }

}
```


ファイルの構成

```
|-- Gruntfile.js
|-- package.json
`-- src
    |-- js
    |   `-- modules
    |       `-- sample.js
    `-- page
        `-- sample.html
```

http://localhost:8000/page/sample.html
にアクセス
![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/4e732537-5d65-72a5-5584-4a5ffb2f032c.png)
上のテキストフィールドに何か入力
![b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/f66ab6f9-56ef-8c1b-64c6-2b8a69b9d15d.png)
下のテキストフィールドに何か入力してbtn1押下
![c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/62e7eebc-5528-70de-67f8-411e4e48a6ba.png)
このような動きになります

src\js\modules配下にURLと同名jsファイル作成
クラス名をファイル名先頭大文字キャメルにする
そうすると、クラスフィールドとdata-obj、data-listを持つタグをひもづけしてくれる
クラスメソッドとdata-nameを持つタグをひもづけしてくれる

# ビュー
ビューを出力する

js\templates\view1.html作成
これがビュー
```view1.html
<div data-text='v.key1'></div>

<div data-prop='{"style": {"color" : v.propTest.key1, "backgroundColor": v.propTest.key2}, "title" : "sample"}'>data-prop</div>

<div data-if='v.ifTest.val === 1'>
    v.ifTest.val === 1
</div>
<div data-elseif='v.ifTest.val === 2'>
    v.ifTest.val === 2
</div>
<div data-else=''>
    v.ifTest.val else
</div>

<select>
    <option data-for-start='l.i = 0' data-for-end='l.i < v.selectLimit' data-for-step='l.i++' data-text='l.i'></option>
</select>

<ol>
    <li data-foreach='v.forTest' data-foreach-key='k' data-foreach-value='v' data-foreach-skip='l.k === "str2"' data-foreach-end='l.k === "str4"'>
        <span data-text='l.k'></span><span>:</span><span data-text='l.v'></span>
    </li>
</ol>
```

src\page\sample.html修正

```sample.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
sample
</title>
<script id="fs-js" src="http://localhost:8000/js/fairysupport.min.js" data-page-root="http://localhost:8000/page"></script>
</head>
<body>
    <div data-obj="field1"></div>
    <div data-list="field2"></div>
    <div data-list="field2"></div>
    
    <div><input type="text" data-name="txt1"></div>
    <div><input type="text" data-obj="txt2"><input type="button" data-name="btn1" value="btn1"></div>
    
    <div><input type="button" data-name="btn2" value="btn2"></div>
    <div data-obj="field3"></div>
    
</body>
</html>
```

src\js\modules\sample.js修正

```sample.js
export class Sample {

    field1;
    field2;
    field3;
    txt2;

    constructor() {
    }

    init() {
        /* 初期処理実装したい場合ここ */
    }
    
    txt1_input(event) {
        this.field1.textContent = event.currentTarget.value;
    }

    btn1_click(event) {
        this.field2.forEach(item => item.textContent = this.txt2.value);
    }

    btn2_click(event) {
        let param = {'key1'       : 'aaa', 
                     'propTest'   : {'key1': '#ffffff', 'key2': '#000000'}, 
                     'ifTest'     : {'val': 2},
                     'selectLimit': 10,
                     'forTest'    : {'str1': 'val1',
                                     'str2': 'val2',
                                     'str3': 'val3',
                                     'str4': 'val4',
                                     'str5': 'val5'}
                    };
        $f.loadTemplate(this.field3, 'view1', param)
        .then((nodeList) => console.log('load view1'))
        .catch((e) => console.error(e));
    }

}
```
loadTemplateがビューの呼び出し
第2引数にjs\templates配下のhtmlファイル名を渡す(拡張子書かずに)
第3引数に渡したparamは、ビュー内でvという変数でアクセスできる
lという変数がビュー内のみで使える。lはオブジェクト
btn2押下でビューが表示される
data-textは値出力
data-propはプロパティに値をセット
data-if、data-elseif、data-elseはif、else if、else
data-for-start、data-for-end、data-for-stepは必ずセットで使うfor文
data-for-skipはループ処理をスキップする条件
data-for-startを使う際、data-for-skipは無くてもいい
data-foreach、data-foreach-key、data-foreach-valueは必ずセットで使うforeach文
data-foreachを使う際、data-foreach-skip、data-foreach-endは無くてもいい
data-foreach-skipはループ処理をスキップする条件
data-foreach-endはループ処理を終わる条件
![i.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/a4573a64-05dd-c5d4-ce96-3ffad85a95f8.png)

ビューからビュー呼ぶ
js\templates\view2.html作成
```view2.html
<div>view2</div>
<div data-text='v.propTest.key1'></div>
<div data-text='v.propTest.key2'></div>
```

js\templates\view1.html修正
```view1.html
<div data-text='v.key1'></div>

<div data-prop='{"style": {"color" : v.propTest.key1, "backgroundColor": v.propTest.key2}, "title" : "sample"}'>data-prop</div>

<div data-if='v.ifTest.val === 1'>
    v.ifTest.val === 1
</div>
<div data-elseif='v.ifTest.val === 2'>
    v.ifTest.val === 2
</div>
<div data-else=''>
    v.ifTest.val else
</div>

<select>
    <option data-for-start='l.i = 0' data-for-end='l.i < v.selectLimit' data-for-step='l.i++' data-text='l.i'></option>
</select>

<ol>
    <li data-foreach='v.forTest' data-foreach-key='k' data-foreach-value='v' data-foreach-skip='l.k === "str2"' data-foreach-end='l.k === "str4"'>
        <span data-text='l.k'></span><span>:</span><span data-text='l.v'></span>
    </li>
</ol>

<div data-template='view2' data-template-variable='v'></div>
```
btn2押下
![k.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/9ed65e4a-7377-c198-61cb-bb71beee9157.png)
view2も出てる


ビューを追加していく形

js\templates\add.html作成
```add.html
<tr data-foreach='v.row' data-foreach-key='rowKey' data-foreach-value='rowVal'>
    <td data-foreach='l.rowVal' data-foreach-key='colKey' data-foreach-value='colVal' data-text='l.colVal' data-name='cell' style='border: 1px solid #000000;'></td>
</tr>
```

src\page\sample.html修正

```sample.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
sample
</title>
<script id="fs-js" src="http://localhost:8000/js/fairysupport.min.js" data-page-root="http://localhost:8000/page"></script>
</head>
<body>
    <div><input type="button" data-name="btn1" value="btn1"></div>
    <div>
        <table style="border-collapse:collapse">
            <tbody data-obj="field1">
            </tbody>
        </table>
    </div>
</body>
</html>
```

src\js\modules\sample.js修正

```sample.js
export class Sample {

    counter = 1;

    constructor() {
    }

    btn1_click(event) {
        let param = {'row': []};
        for (let i = 0; i < 10; i++, this.counter++) {
            let row = [];
            for (let ii = 0; ii < 4; ii++) {
                row.push(this.counter + ':' + ii);
            }
            param['row'].push(row);
        }
        
        $f.appendLoadTemplate(this.field1, 'add', param)
        .then((nodeList) => console.log('load add'))
        .catch((e) => console.error(e));
    }

    cell_click(event) {
        let cell = event.currentTarget;
        alert(cell.parentNode.sectionRowIndex + " : " + cell.cellIndex);
    }

}
```
実行btn1押下結果
![l.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/acbc9352-0d9e-4695-0409-36e23da13304.png)
btn1押下するたび行が増えていく

# ビュー独自処理
ビューに独自の処理をつける

src\page\sample.html修正

```sample.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
sample
</title>
<script id="fs-js" src="http://localhost:8000/js/fairysupport.min.js" data-page-root="http://localhost:8000/page"></script>
</head>
<body>
    <div data-obj="field1"></div>
    <div data-list="field2"></div>
    <div data-list="field2"></div>
    
    <div><input type="text" data-name="txt1"></div>
    <div><input type="text" data-obj="txt2"><input type="button" data-name="btn1" value="btn1"></div>
    
    <div><input type="button" data-name="btn2" value="btn2"></div>
    <div data-obj="field3"></div>
    
</body>
</html>
```

src\js\modules\sample.js修正

```sample.js
export class Sample {

    field1;
    field2;
    field3;
    txt2;

    constructor() {
    }

    init() {
        /* 初期処理実装したい場合ここ */
    }
    
    txt1_input(event) {
        this.field1.textContent = event.currentTarget.value;
    }

    btn1_click(event) {
        this.field2.forEach(item => item.textContent = this.txt2.value);
    }

    btn2_click(event) {
        let param = {'key1'       : 'aaa', 
                     'propTest'   : {'key1': '#ffffff', 'key2': '#000000'}, 
                     'ifTest'     : {'val': 2},
                     'selectLimit': 10,
                     'forTest'    : {'str1': 'val1',
                                     'str2': 'val2',
                                     'str3': 'val3',
                                     'str4': 'val4',
                                     'str5': 'val5'}
                    };
        $f.loadUniqueComponent(this.field3, 'sample1', param)
        .then((nodeList) => console.log('load sample1'))
        .catch((e) => console.error(e));
    }

}
```

src\js\components\sample1\controller.js作成

```components\sample1\controller.js
export class Sample1 {

    field1;
    field2;
    txt2;

    constructor() {
    }

    init(data) {
        /* 初期処理実装したい場合ここ */
    }
    
    txt1_input(event) {
        this.field1.textContent = 'component Sample1:' + event.currentTarget.value;
    }

    btn1_click(event) {
        this.field2.forEach(item => item.textContent = 'component Sample1:' + this.txt2.value);
    }

}
```

src\js\components\sample1\view.html作成

```components\sample1\view.html
<div>component sample1</div>

<div data-sample1-obj="field1"></div>
<div data-sample1-list="field2"></div>
<div data-sample1-list="field2"></div>

<div><input type="text" data-sample1-name="txt1"></div>
<div><input type="text" data-sample1-obj="txt2"><input type="button" data-sample1-name="btn1" value="btn1"></div>

<div data-text='v.key1'></div>
<div data-if='v.ifTest.val === 1'>
    v.ifTest.val === 1
</div>
<div data-else=''>
    v.ifTest.val else
</div>

<div data-unique-component='sample2' data-unique-component-variable='v'></div>
```


src\js\components\sample2\controller.js作成

```components\sample2\controller.js
export class Sample2 {

    field1;
    field2;
    txt2;

    constructor() {
    }

    init(data) {
        /* 初期処理実装したい場合ここ */
    }
    
    txt1_input(event) {
        this.field1.textContent = 'component Sample2:' + event.currentTarget.value;
    }

    btn1_click(event) {
        this.field2.forEach(item => item.textContent = 'component Sample2:' + this.txt2.value);
    }

}
```

src\js\components\sample2\view.html作成

```components\sample2\view.html
<div>component sample2</div>

<div data-sample2-obj="field1"></div>
<div data-sample2-list="field2"></div>
<div data-sample2-list="field2"></div>

<div><input type="text" data-sample2-name="txt1"></div>
<div><input type="text" data-sample2-obj="txt2"><input type="button" data-sample2-name="btn1" value="btn1"></div>

<div data-text='v.propTest.key1'></div>
<div data-text='v.propTest.key2'></div>

```


ファイルの構成

```
|-- Gruntfile.js
|-- package.json
`-- src
    |-- js
    |   |-- components
    |   |   |-- sample1
    |   |   |   |-- controller.js
    |   |   |   `-- view.html
    |   |   `-- sample2
    |   |       |-- controller.js
    |   |       `-- view.html
    |   `-- modules
    |       `-- sample.js
    `-- page
        `-- sample.html
```

http://localhost:8000/page/sample.html
にアクセス
btn2押下でsrc\js\components配下のsample1とsample2が出てる
![f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/8f9ac99a-13e9-cf36-671a-2b040363760a.png)
src\js\modules\sample.jsでloadUniqueComponentを実行している
第2引数にjs\components配下のフォルダ名を渡す
これでsrc\js\components配下を呼び出せる
src\js\components配下のcontroller.jsのクラス名はフォルダ名先頭大文字キャメル
src\js\components配下のview.htmlはdata-obj、data-list、data-nameではなく、data-フォルダ名-obj、data-フォルダ名-list、data-フォルダ名-name
data-obj、data-list、data-nameにしてしまうとsrc\js\modules\sample.jsにひもづく
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/58c1c427-2159-5d29-fb73-56f29aad1b9a.png)
値を入力、ボタン押下するとsrc\js\components配下につくったcontroller.jsの処理が実行される

# Ajax

src\page\sample.html修正

```sample.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
sample
</title>
<script id="fs-js" src="http://localhost:8000/js/fairysupport.min.js" data-page-root="http://localhost:8000/page"></script>
</head>
<body>
    <div data-obj="obj"></div>
    <div><button data-name="sample">sample</button></div>
</body>
</html>
```

src\js\modules\sample.js修正

```sample.js
export class Sample {

    obj;

    constructor() {
    }

    sample_click(event) {
        $f.ajax($f.envValue('apiRoot') + '/sample.json', {'sample_key': 'sample_value'}, 'GET')
        .setLoadend(200, this.success.bind(this))
        .setLoadend(404, this.error.bind(this))
        .setLoadend(null, this.error.bind(this))
        .send();
    }

    success(event, xhr) {
        this.obj.textContent = JSON.stringify(xhr.response);
    }

    error(event, xhr) {
        this.obj.textContent += xhr.status;
    }

}
```

src\js\env\envValue.local.json作成

```src\js\env\envValue.local.json
{
    "apiRoot" : "http://localhost:8000/dummy"
}
```

src\dummy\sample.json作成

```src\dummy\sample.json
{
    "key1": "value1",
    "key2": "value2",
    "key3": "value3"
}
```
sampleボタン押下でjsonが出ます
![h.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/15bf5214-3512-4fa8-6d5a-dac29c4ef206.png)
\\$f.envValue('apiRoot')でenvValue.local.jsonの値が取れます。拡張子がlocal.jsonなのはpackage.jsonのscriptsで引数にlocalを与えているため
\\$fはフレームワークのインスタンス。グローバル変数です
\\$f.ajax第1引数がURL、第2引数がリクエストパラメーター、第3引数がhttpメソッド
この簡易サーバーがPOSTに対応していないので、第3引数をGETにしてます
setLoadend(200, this.success.bind(this))はhttpステータスが200の場合の処理
setLoadend(null, this.error.bind(this))は設定していないhttpステータスの場合の処理。今回の場合だと、200でもない404でもない場合の処理

# エラーチェック

## 基本的な使い方

src\env\envValue.local.json修正
```src\env\envValue.local.json
{
     "jsServer" : "http://localhost:8000"
    ,"pageRoot" : "http://localhost:8000/page"
    ,"version" : (new Date()).getTime()
}
```
↑修正したのはsrc\js\env\envValue.local.jsonではなく、src\env\envValue.local.json


src\js\util\sampleValidator.js作成
```src\js\env\sampleValidator.js
export class SampleValidator {

    constructor() {
    }

    init (target, property, newValue, oldValue, arg, event) {
        arg.msgObj.textContent = '';
        return true;
    }
    
    require (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            arg.msgObj.textContent += ' require';
            return false;
        }
        return true;
    }
    
    intType (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            return true;
        }
        let reg = new RegExp('(^[-]{0,1}[1-9]{1}[0-9]*$|^[0]{1}$)', 'g');
        if (!(reg.test(newValue))) {
            arg.msgObj.textContent += ' int';
            return false;
        }
        return true;
    }
    
    minMaxLength (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            return true;
        }
        if (newValue.length < arg.min || arg.max < newValue.length) {
            arg.msgObj.textContent += ' min:' + arg.min + ' max:' + arg.max;
            return false;
        }
        return true;
    }
    
    isEmpty(value){
        if (value === '' || value === null || value === undefined) {
            return true;
        }
        return false;
    }

}
```

src\js\modules\sample.js修正
```sample.js
import {SampleValidator} from '../util/sampleValidator.js?$envValue(version)';

export class Sample {

    constructor() {
    }

    init() {

        let v = new SampleValidator();

        $f.validate('group1', this.text, 'value', ['blur'], [v.init.bind(v), v.require.bind(v), v.intType.bind(v), v.minMaxLength.bind(v)], {"min": 2, "max": 4, "msgObj": this.bind1});

    }

    btn_click(event) {
        console.log(this.text.value);
    }

}
```

src\page\sample.html修正
```sample.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
sample
</title>
<script id="fs-js" src="http://localhost:8000/js/fairysupport.min.js" data-page-root="http://localhost:8000/page"></script>
</head>
<body>
    <div data-obj="bind1"></div>
    <div><input type='text' data-obj='text' value='22'></div>
    <div><button data-name="btn">btn</button></div>
</body>
</html>
```
動作確認してみます
テキストフィールドのblurイベントの時にValidatorが実行されます
![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/3f1f96b8-6513-cf3f-e340-1e9ebb8e3ef5.png)
![b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/5bad39bd-b5d2-6f79-492b-619fc92b81d3.png)
\\$f.validateの第2引数に第4引数イベントが発生すると、第5引数に指定したValidatorが実行されます
Validatorの引数はtarget, property, newValue, oldValue, arg, event
target:：\\$f.validateの第2引数
property：\\$f.validateの第3引数
newValue：\\$f.validateの第2引数の第3引数プロパティに新たに入れようとしている値
oldValue：focusされたときの値
arg：\\$f.validateの第6引数
event：発生したイベント

全てのValidatorがtrueを返すとnewValueが格納される。Validatorが1つでもfalseを返すとoldValueが格納される

## 入力中のエラーチェック
blurイベントだけでなく、入力中もエラーチェックが実行されるようにしてみます

src\js\util\sampleValidator.js修正
```src\js\env\sampleValidator.js
export class SampleValidator {

    constructor() {
    }

    init (target, property, newValue, oldValue, arg, event) {
        arg.msgObj.textContent = '';
        return true;
    }
    
    require (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            arg.msgObj.textContent += ' require';
            return false;
        }
        return true;
    }
    
    intType (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            return true;
        }
        let reg = new RegExp('(^[-]{0,1}[1-9]{1}[0-9]*$|^[0]{1}$)', 'g');
        if (!(reg.test(newValue))) {
            arg.msgObj.textContent += ' int';
            return false;
        }
        return true;
    }
    
    minMaxLength (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            return true;
        }
        if (newValue.length < arg.min || arg.max < newValue.length) {
            arg.msgObj.textContent += ' min:' + arg.min + ' max:' + arg.max;
            return false;
        }
        return true;
    }
    
    isEmpty(value){
        if (value === '' || value === null || value === undefined) {
            return true;
        }
        return false;
    }

    textFinalize (target, property, newValue, oldValue, arg, event, valid, validResult) {
        let result = {};
        if (event && 'type' in event && 'input' === event.type) {
            result['oldValue'] = newValue;
        }
        return result;
    }
    
}
```

src\js\modules\sample.js修正
```sample.js
import {SampleValidator} from '../util/sampleValidator.js?$envValue(version)';

export class Sample {

    constructor() {
    }

    init() {

        let v = new SampleValidator();

        $f.validate('group1', this.text, 'value', ['input', 'blur'], [v.init.bind(v), v.require.bind(v), v.intType.bind(v), v.minMaxLength.bind(v)], {"min": 2, "max": 4, "msgObj": this.bind1}, v.textFinalize.bind(v));

    }

    btn_click(event) {
        console.log(this.text.value);
    }

}
```
\\$f.validateの第4引数にinputを追加
\\$f.validateの第7引数に全てのValidator実行後に実行される終了処理を指定できます
終了処理はnewValue、oldValueの書き換えができます
inputイベント時はまだ入力している途中なので、終了処理内でエラー時でもnewValueに戻るようにしています
inputイベント時はまだ入力している途中なので、エラーメッセージは表示されますが、不正値であってもテキストフィールドに出力してます
blurイベント時は不正値の場合、focusされたときの値に戻ります

## 入力中でも正しい値だったら有効にする
先ほどの修正ですと、入力中に有効値だったとしても、最終的に不正値だったら、blurイベント時のValidatorによって、focusされたときの値に戻りました
フレームワークでoldValueがイベントごとに管理されているためです
inputイベント時有効値だったら、blurイベント時のValidatorのoldValueを書き換えるように修正します

src\js\util\sampleValidator.js修正
```src\js\env\sampleValidator.js
export class SampleValidator {

    constructor() {
    }

    init (target, property, newValue, oldValue, arg, event) {
        arg.msgObj.textContent = '';
        return true;
    }
    
    require (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            arg.msgObj.textContent += ' require';
            return false;
        }
        return true;
    }
    
    intType (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            return true;
        }
        let reg = new RegExp('(^[-]{0,1}[1-9]{1}[0-9]*$|^[0]{1}$)', 'g');
        if (!(reg.test(newValue))) {
            arg.msgObj.textContent += ' int';
            return false;
        }
        return true;
    }
    
    minMaxLength (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            return true;
        }
        if (newValue.length < arg.min || arg.max < newValue.length) {
            arg.msgObj.textContent += ' min:' + arg.min + ' max:' + arg.max;
            return false;
        }
        return true;
    }
    
    isEmpty(value){
        if (value === '' || value === null || value === undefined) {
            return true;
        }
        return false;
    }

    textFinalize (target, property, newValue, oldValue, arg, event, valid, validResult) {
        let result = {};
        if (event && 'type' in event && 'input' === event.type) {
            result['oldValue'] = newValue;
            if (valid) {
                result['oldValueAllEvent'] = newValue;
            }
        }
        return result;
    }
    
}
```
終了処理でoldValueAllEventを返すと、全てのイベントのoldValueが書き換えられます
終了処理の第7引数validはValidatorの結果です。全てのValidatorがtrueの場合true、Validatorが1つでもfalseの場合falseが渡されます

## =演算子で直接格納時のエラーチェック
src\js\modules\sample.js修正
```sample.js
import {SampleValidator} from '../util/sampleValidator.js?$envValue(version)';

export class Sample {

    constructor() {
    }

    init() {

        let v = new SampleValidator();

        $f.validate('group1', this.text, 'value', ['input', 'blur', null], [v.init.bind(v), v.require.bind(v), v.intType.bind(v), v.minMaxLength.bind(v)], {"min": 2, "max": 4, "msgObj": this.bind1}, v.textFinalize.bind(v));

    }

    btn_click(event) {
        this.text.value = this.text2.value;
    }

}
```

src\page\sample.html修正
```sample.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
sample
</title>
<script id="fs-js" src="http://localhost:8000/js/fairysupport.min.js" data-page-root="http://localhost:8000/page"></script>
</head>
<body>
    <div data-obj="bind1"></div>
    <div><input type='text' data-obj='text' value='22'></div>
    <div><input type='text' data-obj='text2' value=''></div>
    <div><button data-name="btn">btn</button></div>
</body>
</html>
```
![c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/c3c63842-43a4-927e-bbac-3476c3990dc3.png)
text2を追加してbtnがクリックされるとtext2.valueをtext.valueに格納するようにしました
\\$f.validateの第4引数にnullを追加。nullを指定すると=演算子の時にValidatorを実行させるということになります

## 手動実行エラーチェック
イベント発生時や=演算子の時にエラーチェックを実行してきましたが、任意のタイミングでエラーチェックを手動で実行します

src\page\sample.html修正
```sample.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
sample
</title>
<script id="fs-js" src="http://localhost:8000/js/fairysupport.min.js" data-page-root="http://localhost:8000/page"></script>
</head>
<body>
    <form action="index.html" data-name='form1'>
        <div data-obj="bind1"></div>
        <div><input type='text1' data-obj='text1' value='22'></div>
        <div data-obj="bind2"></div>
        <div><input type='text2' data-obj='text2' value='22'></div>
        <input type='submit' value='submit'>
    </form>
    <form action="index.html" data-name='form2'>
        <div data-obj="bind3"></div>
        <div><input type='text3' data-obj='text3' value='22'></div>
        <div data-obj="bind4"></div>
        <div><input type='text4' data-obj='text4' value='22'></div>
        <input type='submit' value='submit'>
    </form>
</body>
</html>
```


src\js\modules\sample.js修正
```sample.js
import {SampleValidator} from '../util/sampleValidator.js?$envValue(version)';

export class Sample {

    constructor() {
    }

    init() {

        let v = new SampleValidator();

        $f.validate('group1', this.text1, 'value', ['input', 'blur', null], [v.init.bind(v), v.require.bind(v), v.intType.bind(v), v.minMaxLength.bind(v)], {"min": 2, "max": 4, "msgObj": this.bind1}, v.textFinalize.bind(v));
        $f.validate('group1', this.text2, 'value', ['input',         null], [v.init.bind(v), v.require.bind(v), v.intType.bind(v), v.minMaxLength.bind(v)], {"min": 2, "max": 4, "msgObj": this.bind2}, v.textFinalize.bind(v));
        $f.validate('group2', this.text3, 'value', ['input', 'blur', null], [v.init.bind(v), v.require.bind(v), v.intType.bind(v), v.minMaxLength.bind(v)], {"min": 2, "max": 4, "msgObj": this.bind3}, v.textFinalize.bind(v));
        $f.validate('group2', this.text4, 'value', ['input',         null], [v.init.bind(v), v.require.bind(v), v.intType.bind(v), v.minMaxLength.bind(v)], {"min": 2, "max": 4, "msgObj": this.bind4}, v.textFinalize.bind(v));

    }

    form1_submit(event) {
        let vaild = $f.execGroupValidator('group1');
        if (!vaild) {
            console.log('error form1');
            event.preventDefault();
            event.stopImmediatePropagation();
        }
    }

    form2_click(event) {
        let vaild = $f.execGroupValidator('group2');
        if (!vaild) {
            console.log('error form2');
            event.preventDefault();
            event.stopImmediatePropagation();
        }
    }

}
```
手動実行の時エラー値つくるため、わざとtext2、text4のvalidateからblurイベント外しました
\\$f.execGroupValidatorで\\$f.validateの第1引数が同じ値のエラーチェックを一括実行できます
全てのValidatorがtrueの場合true、Validatorが1つでもfalseの場合falseが返ってきます

## クラスフィールドへのエラーチェック

src\js\util\sampleValidator.js修正
```src\js\env\sampleValidator.js
export class SampleValidator {

    constructor() {
    }

    init (target, property, newValue, oldValue, arg, event) {
        if (arg && 'msgObj' in arg) {
            arg.msgObj.textContent = '';
        }
        return true;
    }
    
    require (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            if (arg && 'msgObj' in arg) {
                arg.msgObj.textContent += ' require';
            }
            return false;
        }
        return true;
    }
    
    intType (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            return true;
        }
        let reg = new RegExp('(^[-]{0,1}[1-9]{1}[0-9]*$|^[0]{1}$)', 'g');
        if (!(reg.test(newValue))) {
            if (arg && 'msgObj' in arg) {
                arg.msgObj.textContent += ' int';
            }
            return false;
        }
        return true;
    }
    
    minMaxLength (target, property, newValue, oldValue, arg, event) {
        if (this.isEmpty(newValue)) {
            return true;
        }
        if (newValue.length < arg.min || arg.max < newValue.length) {
            if (arg && 'msgObj' in arg) {
                arg.msgObj.textContent += ' min:' + arg.min + ' max:' + arg.max;
            }
            return false;
        }
        return true;
    }
    
    isEmpty(value){
        if (value === '' || value === null || value === undefined) {
            return true;
        }
        return false;
    }

    textFinalize (target, property, newValue, oldValue, arg, event, valid, validResult) {
        let result = {};
        if (!event && !valid) {
            throw 'invalid';
        } else if (event && 'type' in event && 'input' === event.type) {
            result['oldValue'] = newValue;
            if (valid) {
                result['oldValueAllEvent'] = newValue;
            }
        }
        return result;
    }
    
}
```

src\page\sample.html修正
```sample.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
sample
</title>
<script id="fs-js" src="http://localhost:8000/js/fairysupport.min.js" data-page-root="http://localhost:8000/page"></script>
</head>
<body>
    <div><input type='text1' data-obj='text1' value='22'></div>
    <input type='button' value='button' data-name='button'>
</body>
</html>
```

src\js\modules\sample.js修正
```sample.js
import {SampleValidator} from '../util/sampleValidator.js?$envValue(version)';

export class Sample {

    intField = 20;

    constructor() {
    }

    init() {

        let v = new SampleValidator();

        $f.validate('group1', this, 'intField', [null], [v.init.bind(v), v.require.bind(v), v.intType.bind(v), v.minMaxLength.bind(v)], {"min": 2, "max": 4}, v.textFinalize.bind(v));

    }

    button_click(event) {
        try {
            this.intField = this.text1.value;
            console.log('success');
        } catch (e) {
            console.error(e);
        }
    }

}
```
SampleValidatorでarg.msgObj.textContentはif文で囲みました
終了処理でif (!event && !valid) 追加
sample.jsでbutton_clickでクラスフィールドに値入れました

# 多言語対応

src\js\msg\msg.jsonをコピーして
src\js\msg\msg.en.json作成
src\js\msg\msg.en.json修正
```msg.en.json
{
     "invalidValue"  : "en Invalid value"
    ,"errorRequired" : "en Required"
    ,"errorIntValue" : "en Please enter an integer"
    ,"errorMinValue" : "en Please enter value less than or equal to ${min}"
    ,"errorMaxValue" : "en Please enter value more than or equal to ${max}"
}
```
src\js\msg\msg.jsonをコピーして
src\js\msg\msg.en-US.json作成
src\js\msg\msg.en-US.json修正
```msg.en-US.json
{
     "invalidValue"  : "en-US Invalid value"
    ,"errorRequired" : "en-US Required"
    ,"errorIntValue" : "en-US Please enter an integer"
    ,"errorMinValue" : "en-US Please enter value less than or equal to ${min}"
    ,"errorMaxValue" : "en-US Please enter value more than or equal to ${max}"
}
```

src\page\sample.html修正
```sample.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
sample
</title>
<script id="fs-js" src="http://localhost:8000/js/fairysupport.min.js" data-page-root="http://localhost:8000/page"></script>
</head>
<body>
    <div data-obj='obj'></div>
    <input type='button' value='button' data-name='button'>
</body>
</html>
```

src\js\modules\sample.js修正
```sample.js
export class Sample {

    constructor() {
    }

    button_click(event) {
        this.obj.textContent = $f.msg('errorMinValue', {'min': 10});
    }

}
```

ブラウザの設定→言語で英語をトップにする
![d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/9fb10d91-c053-24bc-c015-ad2bac440c6b.png)
![e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/c6687238-af36-eeac-413f-b82bfdc17573.png)
![f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/5234ba56-f516-9282-414e-71de491a31ce.png)
http://localhost:8000/page/sample.html
でbuttonクリック
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/dca734d9-7c58-b7cc-a9be-39172c833410.png)

ブラウザの設定→言語で英語(アメリカ合衆国)をトップにする
![h.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/1d7609b5-2684-5da8-bc9e-acba22480588.png)
http://localhost:8000/page/sample.html
再読み込みしてbuttonクリック
![i.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/0c36f22b-3f40-db88-d6d0-30a1a6218e80.png)
ブラウザの設定→言語で日本語をトップにする
![j.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/f422f248-9a86-4e9d-0864-70cd0b4b1387.png)
http://localhost:8000/page/sample.html
再読み込みしてbuttonクリック
![k.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/48feae67-b957-8206-1d42-0e158111bff9.png)

# Laravelで使ってみる
PHP+Laravelにfairy support jsを使ってみる
Laravelの説明はここでは省略します

フォルダ構成はこんな感じ
Laravelプロジェクト(LaravelPJDir)とfairy support jsプロジェクト(fairysupportjsPJDir)が同階層にある
```
|-- LaravelPJDir
|   |-- app
|   |-- public
|   |-- resources
|   |-- routes
|   |-- vendor
|   |-- composer.json
|   |  ・・・・・いろいろ省略
|   `-- webpack.mix.js
`-- fairysupportjsPJDir
    |-- Gruntfile.js
    |-- package.json
    |-- distWork
    |-- node_modules
    `-- src
        |-- js
        |   `-- modules
        |       `-- sample.js
        `-- page
            `-- sample.html
```
fairy support jsの簡易確認サーバーはCtrl+Cで止めていいです
fairy support jsのビルドはとめないでください

fairysupportjsPJDir直下のGruntfile.js修正
```Gruntfile.js
module.exports = function(grunt){
  grunt.initConfig({
    copy: {
      dist: {
        files: [
          {
            cwd: 'distWork',
            src: ['img/**/*', 'js/**/*.json', 'js/**/*.min.js', 'favicon.ico'],
            expand: true,
            dest: 'dist'
          }
        ]
      },
      customize: {
        files: [
          {
            cwd: 'distWork',
            src: ['css/**/*', 'img/**/*', 'js/**/*', 'page/**/*', 'favicon.ico'],
            expand: true,
            dest: '../LaravelPJDir/public/frontResources'
          }
        ]
      }
    },
・・・・・省略
```
↑copyのcustomizeのdestを修正しています

fairysupportjsPJDir直下のpackage.json修正
```package.json
・・・・・省略
  "scripts": {
    "minify": "grunt default",
    "build_prd": "fairysupport_js prd",
    "build_stg": "fairysupport_js stg",
    "build_dev": "fairysupport_js dev",
    "build_local": "fairysupport_js local",
    "watch_local": "watch \"npm run build_local\" ./src",
    "server": "http-server ./distWork -a localhost -p 8000 -o page/index.html",
    "copy_dist_work": "grunt watch:customize"
    ,"copy_laravel": "grunt copy:customize"
  },
・・・・・省略
```
↑copy_laravelを追加しています
コマンドラインでfairysupportjsPJDirに移動し
`npm run copy_laravel`
これでLaravelPJDir/public配下にfrontResourcesディレクトリができ、その中にfairy support jsの成果物がコピーされているのを確認
確認できたらコマンドラインで
`npm run copy_dist_work`
これを起動しっぱなしにしておけば、distWork配下に変更があると自動的にコピーが実行される
自動コピーをとめるときはCtrl+C

fairy support js側の実装
fairysupportjsPJDir\src\js\modules\sample\fairysupport.js作成
```fairysupport.js
export class Fairysupport {

    constructor() {
    }

    text1_input(event) {
        this.obj1.textContent = event.currentTarget.value;
    }

}
```

Laravel側の実装
LaravelPJDir/app/Http/Controllers/SampleController.php作成
```SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SampleController extends Controller
{
    /**
     * @return \Illuminate\Http\Response
     */
    public function fairysupport()
    {
        return view('sample.fairysupport');
    }

}
```
LaravelPJDir/resources/views/layout.blade.php作成
```layout.blade.php
<html>
    <head>
        <title>sample</title>
    </head>
    <script id="fs-js" src="{{ asset('/frontResources/js/fairysupport.min.js') }}" data-page-root="{{ url('/') }}"></script>
    <script>
        function fairysupportInitFail(retryCount, error){
            return false;
        }
    </script>
    <body>
        <div>
            @yield('content')
        </div>
    </body>
</html>
```
↑scriptタグでfairysupport.min.js読み込み
fairysupport jsはURLに対応するmodules配下のjsファイルを見つけられない時、fairysupportInitFailを実行するので、実装しておく
LaravelPJDir/resources/views/sample/fairysupport.blade.php作成
```fairysupport.blade.php
@extends('layout')
<div data-obj='obj1'></div>
<div><input type='text' data-name='text1'></div>
```
LaravelPJDir/routes/web.php修正
```web.php
・・・・・・省略
Route::get('sample/fairysupport', 'App\Http\Controllers\SampleController@fairysupport');
```
↑'sample/fairysupport'追加

Laravelをデプロイしてあるwebサーバーを起動
先ほどweb.phpで追加したsample/fairysupportにアクセス
![l.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238103/b07bf786-76db-83c5-26db-83c3760dc481.png)
inputイベントでfairysupportjsPJDir\src\js\modules\sample\fairysupport.jsのメソッドが動いていることを確認