[Drupal]フォームをレンダリング配列に組み込む


とある案件でノード詳細画面(node/[nid])でコンテンツの一部のフィールドをその場で編集できるフォームを表示しないといけなくなりまして。

最初に試したのが

  • Viewsでそのノードのフィールドの一覧を作る
  • Views Bulk OperationsViews Entity Form Fieldで編集できるようにする
  • ノードの詳細画面からそのビューにリダイレクトするようにする

だったんですけど、パラグラフ使ってたりフィールドごとの表示制限をする必要があったり、なんやかんやでform_alterやらテンプレート修正やらがカオスになりそうだったので、カスタムフォームを作ることにしました。

カスタムフォームを表示させる方法として最初に考えたのが「フォーム部分だけブロックで作ってnode/[nid]に表示されるように設置する」だったんですけど、よくよく考えると他の画面で

class MyController {

  public function build() {
    // 〜省略〜 

    // ノード詳細画面のレンダリング配列を取得する.
    $node_view = \Drupal::entityTypeManager()
      ->getViewBuilder('node')
      ->view($node);
    $build[] = $node_view;

    return $build;
  }
}

こんな感じでレンダリング配列の一部にノードのコンテンツを埋め込んでいた(こちらでもフォームを表示しないといけない)ので、ブロックよりもコンテンツ自体に埋め込んだほうが使い勝手がよさそうだという結論になりました。(今回の仕様的にそのフォームが無いとノード詳細画面のコンテンツがゼロになってしまうのも個人的に気持ち悪かった)

ちなみにブロックでフォームを表示する場合は、上記の箇所でコンテンツの代わりにブロックをレンダリングすれば出来るようです
ブロックのフォームを作る
ブロックをプログラムで表示する

で、レンダリング配列の中にフォームを組み込む方法についてなのですが。

Drupalのレンダリング配列とフォームAPIの要素って共通してるものが多いので、いきなりレンダリング配列の中にフォーム要素を突っ込んだらワンチャン行けるんでは?と思ってやってみたんですが、ボタンなどは表示されるけど送信ボタンを押しても何も起こらず。レンダリング配列と違ってフォームはフォームIDとか状態管理とかいろんなものが必要なので、当然といえば当然ですかね。

// theme/custom/my_theme/my_theme.theme

function my_theme_preprocess_node__article(&$variables) {
  // さり気なくフォーム要素を入れる.
  $variables['form']['submit'] = [
    '#type' => 'submit',
    '#value' => '送信',
    '#submit' => [
        '_my_theme_my_subnmit_handler',
    ],
  ];
}

// 永遠に呼び出されないサブミットハンドラー(エラーも出ない).
function _my_theme_my_subnmit_handler($form, $form_state) {
  dpm('フォームが送信されました');
}
{# theme/custom/my_theme/templates/content/node--article.html.twig #}
<div>{{ form }}</div>

調べてみると、formBuilderでフォームを取得してレンダリング配列に入れれば良いとこのこと。

// フォームは別途作成する.
use Drupal\my_module\Form\MyForm;

// レンダリング配列にフォームを追加する
function my_theme_preprocess_node__article(&$variables) {
  $variable['form'] = \Drupal::formBuilder()->getForm(MyForm::class);
}
{# theme/custom/my_theme/templates/content/node--article.html.twig #}
<div>{{ form }}</div>

これで確かに行けました。

フォームをレンダリング配列に挿入する

ただ一つ注意点が...

フォームをこんな感じにバラしてしまうとフォームが送信できなくなってしまいます。

{# 送信できないフォーム #}
<div>{{ form.field_a }}</div>
<div>{{ form.submit }}</div>

よく見ると

{{ form }}

(完成形のフォーム)でレンダリングされたDOMには一番外側のformタグにフォームIDなどの情報が付いているので、その部分が足りないのかと思ってそこだけ追加してみたんですが、駄目でした。

原因がよくわからないし時間もないので苦肉の策としてフォーム側でクラスを追加しまくってCSSでなんとかしたのですが、

// /modules/custom/my_module/src/Form/MyForm.php

class MyForm extends FormBase {

  public function buildForm($form_id, $form_state) {
    // 〜省略〜
    $form['container'] = [
      '#type' => 'container',
      '#tree' => TRUE,
      '#attributes' => [
        'class' => 'my-wrapper',
      ],
    ];
    // 〜省略〜
  }

}

他の人が既存フォームを加工したTwigを見ると、レイアウトをいじりたいフィールドだけ引き算して最後に残りのフォームの内容を全部追加、みたいなことしてるのを発見。

{# ばらしてるけどちゃんと送信できるフォーム #}
{%
  set without_filter = [
    'field_a',
  ]
%}

<div>{{ form.field_a }}</div>

{# この中に送信ボタンが含まれる #}
<div>{{ form|without(without_filter) }}</div>

フォームからフロントエンドのロジックを切り離せるのでこっちの方がいいですね。

細かいことまで検証してないのでわからないですが、ばらしたときに拾いきれてなかった要素があったんですかね というわけでフォームをばらす場合はご注意ください。