EC-CUBE4系(Symfony)のtwigをAjax( jQuery)を使ってValidationしてみた。

## FormのValidationをする時には。。。
EC-CUBE４系でフォームを書く時に、`twig`では下記のように書くことがほとんどでしょう。

```twig
<div>
    {{ form_widget(form.name.name01) }}
    {{ form_errors(form.name.name01) }}
</div>
```

こうすることによって、`form_widget`で動的に`input type`が生成され、かつ`validation`のエラーがあった時には、`form_errors`でエラー用の文言が生成されます。

```html
<input type="text" id="entry_name_name01" name="entry[name][name01]" required="required"><!-- ここがform_widget で生成されて-->
<p class="ec-errorMessage">入力されていません。</p><!-- ここがform_errorsで生成される-->
```

これでも一つの完成された形なのですが、この`form-errors`にチェック内容を反映させるためには、一度、form全体を`submit`をさせる必要があります。(当然ですが、、、)

## formをAPIで処理してみよう！
昨今のUI要件は複雑でform全体を`submit`するのではなく、例えばダイアログからユーザに入力をさせて、`form`全体を更新することなくデータの取得・更新をする要望も多いかと思います。
その時は、下記のように`Controller`から`JSON`を返すようにして、`javascript`を使って画面の内容を動的に変化させるようにしましょう。


### Controllerの例

```php
//Controllerの一例
class SomeController extends AbstractController
{
    /**
     * 編集画面.
     *
     * @Route("/api/edit", name="api_edit")
     */
    public function editApi(Request $request)
    {
        if (!$request->isXmlHttpRequest()) {
            throw new BadRequestHttpException();//API通信のみ受け付ける
        }

        $builder = $this->formFactory
            ->createBuilder(SomeDataType::class, $SomeDataClass);

        $form = $builder->getForm();
        $form->handleRequest($request);//ここでValidation

        if($form->isSubmitted() && $form->isValid() ){
            $this->entityManager->persist($SomeDataClass);//何かを更新する
            //成功した時のレスポンス(HTTPステータスはデフォルトの200で返却)
            return $this->json([
                'id'          => $SomeDataClass->getId(),
                'name'        => $SomeDataClass->getName(),
            ]);
        }
        $errors = [];
        //該当する要素のキー(この場合はnameなど)=>エラーメッセージの配列を作成する
        foreach($form->getErrors(true) as $error){
            if($error->getCause()){
                preg_match_all(
                    '/\[.*?\]/',
                    $error->getCause()->getPropertyPath(),
                    $matches);
                $element = str_replace(['[',']'],'',array_pop($matches[0]));
            }else{
                $element          = $error->getOrigin()->getConfig()->getName();
            }
            $errors[$element] = $error->getMessage();
        }
        //HTTPステータス500を返却する。ステータスコードは、任意で好きなものに変更してください。
        return $this->json($errors,500);
}
```

### 出力されるJSON
上記のContollerによって生成される`JSON`は下記の通りです。

#### 成功した時
```JSON
{
	id: 1, 
	name: "サンプル 例です"
}
```


####失敗した時
```JSON
{
	name: "入力されていません。"
}
```

レスポンス(500)で返却される

```
POST http://localhost/api/sample/edit 500 (Internal Server Error)
```

## Twig側の記述
ポイントとなるのは、いかにform-widgetで生成された`name`属性をAPIに渡してやるかだと思います。

### Twigサンプル
```twig
<div id="change_something_div">
    <div class="main">
        <div class="panel">
            <div class="u-mg-t__20">
                {# お名前 #}
                <dl>
                    <dt class="c-title__grayHeading">
                        <h3>お名前</h3>
                        <span>必須</span>
                    </dt>
                    <dd class="ec-halfInput">
                        <div>
                            姓
                        </div>
                        <div>
                            {{form_widget(form.name.name01 | e )}}
                            <p class="ec-errorMessage name01"></p><!-- class属性に項目名を入れておく -->
                        </div>
                        <div>
                            名
                        </div>
                        <div>
                            {{form_widget(form.name.name02 | e )}}
                            <p class="ec-errorMessage name02"></p><!-- class属性に項目名を入れておく -->
                        </div>
                    </dd>
                </dl>
            </div>
            <div id="change_something">
                <a class="button">変更する</a>
            </div>
        </div>
    </div>
</div>
```



### jQueryサンプル
```javascript
      //変更された時の対応
      $("[id=change_something]").on("click",function(){
        //親要素の配下にある、入力項目を取得する
        const inputElements  = $(this).parent().find('input');
        const selectElements = $(this).parent().find('select');

        //エラーメッセージを消去
        const messageElements= $(this).parent().find('p');
        $.each(messageElements,function(index,messageElement){
            $(messageElement).text('');
            $(messageElement).parent().removeClass('error');
        });

        const params = {};
        //input要素を抽出してパラメータにセット
        $.each(inputElements,function(index,inputElement){
            const name     = $(inputElement).attr('name');
            params[name]   = $(inputElement).val();
        });
        //ドロップダウンリストなどの選択内容はこちらで抽出
        $.each(selectElements,function(index,selectElement){
            const name     = $(selectElement).attr('name');
            params[name]   = $(selectElement).val();
        });
        $.ajax({
            type: "POST",
            url: "{{url('api_change_somethig')}}",
            data:params
        })
        .done(function (data) {
            //成功した時は入力内容を画面に反映する
            $('#some_where').text(data.name);
        }).fail(function (data) {
            //失敗した時は、エラー内容を所定の場所に出力する
            $.each(data.responseJSON,function(targetElement,message){
                $.each(messageElements,function(index,messageElement){
                    if($(messageElement).hasClass(targetElement)){
                        $(messageElement).text(message);
                        $(messageElement).parent().addClass('error');
                    }
                });
            })        
        });
      });
```

## 最後に
今回のサンプルはjQueryを使っていますが、Contollerが返しているのはAPIのレスポンスに過ぎないので、`Vue`や`React`などにも応用可能です。
複雑なUIの要件への対応方法の一つとなるかと思います。