Yii 2.0:マルチモデルフォームの「ジャケット」(pjax)を介して検証済みフォームフィールドを動的に追加する

こんにちは、Habr!

少し前まで、フィールドを動的に追加する機能を備えたフォームを開発するタスクに直面しました。各フィールドは個別のデータベースエンティティ、つまりデータベース内のフィールド=レコードです。 私の仕事は些細なことではなかったという事実にもかかわらず、誰もが何らかの程度に似た何かに出くわすかもしれません。 たとえば、GridView内に直接新しい要素を追加し、その後に編集して保存します。



それでは始めましょう。





叙情的な余談



このソリューションの開発中に、私はインターネット全体を検索しましたが、英語のフォーラム、SO、またはGitHubで価値のあるレシピは1つも見つかりませんでした。 さらに、それまでにYiiによる動的フィールドの検証のサポートはまだ準備ができていませんでした。 詳細はこちら 。 そして今、それは私には思える、それは私に合っていません。

解決策自体は、非常に洗練されたふりをするものではないため、建設的、批判的、アドバイスをお聞きします。



初期設定



まず、1つ以上のテーブルから多くのレコードを取得できるモデルが必要です。 私の場合、hasMany関係で十分でした。 したがって、次のようにすべてのアドレスのモデルの配列を取得できます。



$addresses = $model->addresses;
      
      







例として、アドレスのリストを表示したり、それぞれを編集したり、新しいアドレス(天井から取得したエンティティ)を追加したいユーザー向けのビューがあると想像できます。

フォーム自体を準備します(コントローラーがユーザーモデルとして$モデルのみを提供すると仮定します)。

 <?php use yii\widgets\ActiveForm; use yii\helpers\Url; use yii\helpers\Html; ?> <?php $form = ActiveForm::begin([ 'action' => Url::toRoute(['addresses/update', 'userId' => $model->id]), 'options' => [ 'data-pjax' => '1' ], 'id' => 'adressesUpdateForm' ]); ?> <?php foreach ($model->adresses as $key => $address): ?> <?= $form->field($address, "[$key]name") ?> <?= $form->field($address, "[$key]value") ?> <?php endforeach ?> <?= Html::submitButton('', ['class' => 'btn btn-primary']) ?> <?php ActiveForm::end(); ?>
      
      







フォームの準備ができました。 上記のコードでは、最初に必要なクラス-ActiveFormウィジェットと2つのヘルパーを接続します。

次に、次のパラメーターを使用してActiveFormを作成します。





フォームを作成した後、アドレスでループを開始し、ゲッターを直接使用しました。各反復でデータベースリクエストが発生することを恐れてはいけません。Yiiはすべてのリレーションリクエストをプライベートリレーションアレイに保存します。 次に、テーブル(またはその他のフィールドとより複雑なマークアップ)から名前と値を導き出します。



熱心な読者はおそらく尋ねるでしょう:「しかし、新しいアドレスを追加するためのボタンはどうですか?」-急いではいけません、すべてを順番に。



基本的な準備があります。ビューファイルを内部ビューファイルとしてユーザーの統合ビューに接続しましょう。

ユーザープロフィールページがあり、そのすぐ下に住所が表示されているとします。住所のビューを追加すると同時に、「ジャケットを着て」:

 <?php use yii\widgets\Pjax; ?> <?= $this->render('_profile', ['model' => $model]) ?> <?php Pjax::begin(['enablePushState' => false]); ?> <?= $this->render('_addresses', ['model' => $model]) ?> <?php Pjax::end(); ?>
      
      







enablePushStateパラメーターに注目してください。これがないと、ブラウザーのアドレスバーのアドレスが変更されます。 コントローラーのすべての作業がrenderAjaxを経由するため、これは必要ありません。この部分のレイアウトとともに完全なビューはありません。



コントローラー



コントローラーを別の章として具体的に強調しました。

まず、それがどのように機能するかを考えましょう。 シンプルに見えます。 ユーザーIDに関する情報を含むアクション更新のリクエストを取得し、モデルを更新してrenderAjax( '_ addresses'、['model' => $ user]); 次に、User :: findOne($ userId)を介して$ userを取得します。これは、フォームとともに慎重に渡されます。

ただし、実際には、すべてが少し複雑です。

  1. モデルは1つではなく、複数あります
  2. バッチダウンロードが必要です
  3. バッチ検証が必要




それでは、行きましょう:

 <?php namespace backend\controllers; use Yii; use common\models\User; use common\models\Addresses; use yii\base\Model; use yii\filters\AccessControl; use yii\web\Controller; use yii\web\NotFoundHttpException; /** * Addresses controller */ class AddressesController extends Controller { }
      
      







これは、メソッドのないコントローラークラスのように見えます。



コントローラーメソッドとしてバッチロードを追加します(モデルメソッドで取得できますが、私の例では、モデルだけでなく、リンク()を介したユーザーテーブルへの接続も保存する必要があります):



  /** * Update all addresses * @param Model $items * @return nothing */ protected function batchUpdate($items) { if (Model::loadMultiple($items, Yii::$app->request->post()) && Model::validateMultiple($items)) { foreach ($items as $key => $item) { $item->save(); } } }
      
      







成功した場合はtrueまたは更新されたレコードの数を返し、更新するデータがない場合はfalseを返すことにより、メソッドを改善できます。 必要ありませんでした。



モデルを検索する2つのメソッドを追加します。 1つ目はユーザー用、2つ目はアドレス用です(これら2つのメソッドを1つにまとめることができると思っただけです)。

  /** * Finds the Addresses model based on its primary key value. * If the model is not found, a 404 HTTP exception will be thrown. * @param integer $id * @return Addresses the loaded model * @throws NotFoundHttpException if the model cannot be found */ protected function findModel($id) { if (($model = Addresses::findOne($id)) !== null) { return $model; } else { throw new NotFoundHttpException('The requested page does not exist.'); } } /** * Finds the User model based on its primary key value. * If the model is not found, a 404 HTTP exception will be thrown. * @param integer $id * @return User the loaded model * @throws NotFoundHttpException if the model cannot be found */ protected function findUser($id) { if (($model = User::findOne($id)) !== null) { return $model; } else { throw new NotFoundHttpException('The requested page does not exist.'); } }
      
      







最後に、アクションを記述します。



  public function actionUpdate($userId) { $user = $this->findUser($userId); $this->batchUpdate($user->addresses); return $this->renderAjax('_addresses', ['model' => $user]); }
      
      







アクセス制御を忘れずに追加してください。



  /** * @inheritdoc */ public function behaviors() { return [ 'access' => [ 'class' => AccessControl::className(), 'rules' => [ [ 'actions' => ['create', 'update', 'delete'], 'allow' => true, 'roles' => ['@'], ], ], ] ]; }
      
      





上記のメソッドでは、急いですぐに作成メソッドと削除メソッドを示しました。 それらはまだありませんが、アクセス制御に2つのメソッドを事前に追加することは、後で拒否されたアクセスに関する例外をキャッチするよりも優れています。



これで、pjaxデータを介してすべてのアドレスに更新する素晴らしいフォームができました。 通常の場合、「追加」ボタンと「削除」ボタンをフォームに追加し、特定のアクションのリクエストを送信できます。「追加」の場合は、別のビューも送信できます。



検証付きの動的フィールド



それで、最も重要なことに到達しました。

新しいエンティティを追加するだけで、次のアクションになります。





偽レコードの作成は、addOne()モデルメソッドを介して行われます

  public function addOne() { $this->name = self::DEFAULT_NAME; $this->value = self::DEFAULT_VALUE; }
      
      





モデルクラスで定数を作成することを忘れないでください。



コントローラーのアクションは次のようになります。

  /** * action call by AJAX to create new fake address * @param integer $userId * @return mixed */ public function actionCreate($userId) { $user = $this->findUser($userId); $model = new Addresses; $model->addOne(); $user->link('addresses', $model); // link      ,   return $this->renderAjax('_addresses', ['model' => $user]); }
      
      







pjax内でループの外側にあるビューにエントリを追加するためのボタン:

  <?= Html::a(' ', Url::toRoute(['addresses/create', 'userId' => $model->id]), [ 'class' => 'btn btn-success', ]) ?>
      
      







実際には、それだけです。 [アドレスの追加]ボタンをクリックすると、初期データを含む偽のレコードがデータベースに作成され、ビューが新しい検証ルールとともに再びレンダリングされます。

コードのこの部分は、値がデフォルトと同等ではないという検証ルールを追加することで改善できます。 リンクメソッドは検証なしで保存されるため、これは非常に実現可能ですが、残りについてはsave(false)をお勧めします-falseはモデルを保存するときに検証を無効にします。



削除ボタンについても同じことをしましょう。最後に、ビューはループの内部を次のように表示します。

  <?= $form->field($address, "[$key]name") ?> <?= $form->field($address, "[$key]value") ?> <?= Html::a('', Url::toRoute(['addresses/delete', 'id' => $address->id]), [ 'class' => 'btn btn-danger', ]) ?>
      
      







およびコントローラーアクション:

  public function actionDelete($id) { $model = $this->findModel($id); $user = $model->user; $model->delete(); return $this->renderAjax('_addresses', ['model' => $user]); }
      
      







しかし、変更された値とUXはどうですか?



わかった。 上記の機能は標準的な状況では十分ですが、ユーザーはフィールドを動的に追加するときに、その前にデータを保存することを心配する必要がないことに慣れています。 その結果、ユーザーは5つのフィールドに入力し、十分な数がないことを理解し、6番目を追加します...それで終わりです。 さようなら5分間、ユーザーのために、さようならユーザーのためにリソースを使いましょう。



この状況で考えることができる唯一のことは、ユーザーがボタンをクリックするたびにフォームを保存することでした(どちらでもかまいません)。

これに必要なもの:





最初のきしみ:

 $(function(){ $(document).on('click', '[data-toggle=reroute]', function(e) { e.preventDefault(); var $this = $(this); var data = $this.data(); var action = data.action; var $form = $this.closest('form'); if ($form && action) { $form.attr('action', action).submit(); } else { alert('! ,  .'); } }); });
      
      





私の強さで1分かかった簡単なコードスニペット。 data-toggle = reroute属性を持つリンクまたは要素がハンドラーに入り、それに最も近いフォーム(もちろん、親の間)がそのアクションをdata-actionに保存されているものに変更してから送信します。 htmlテンプレートからのハンドラーの構成が正しくない場合、アラートがクラッシュします。



ビュー内のボタンを次のように変更します。

  <?= Html::a(' ', null, [ 'class' => 'btn btn-success', 'data' => [ 'toggle' => 'reroute', 'action' => Url::toRoute(['addresses/create', 'userId' => $model->id]) ] ]) ?> <?= Html::a('', null, [ 'class' => 'btn btn-danger', 'data' => [ 'toggle' => 'reroute', 'action' => Url::toRoute(['addresses/delete', 'id' => $variable->id]) ] ]) ?>
      
      







改善できるもの



いつものように、努力するものがあります。





誰かがこのレシピの改善点や修正点を教えてくれたら嬉しいです。 すべてに良い!



便利なリンク
demos.krajee.com/builder-details/tabular-form-似ていますが、非常に巨大で、GridView用に作成されています。 さらに、新規の削除/追加時にフィールドの保存はありません

www.yiiframework.com/wiki/666/handling-tabular-data-loading-and-validation-in-yii-2は、同じ作者による悪いデモではありません。これはバッチ読み込みの例として役立ちました




All Articles