Verwenden des Form-Builders =========================== Beim Erstellen von HTML-Formularen stellt man oft fest, dass in Views immer wieder ähnlicher Code auftaucht, den man aber nur schwer in anderen Projekten wiederverwenden kann. Für jedes Eingabefeld muss zum Beispiel immer ein Label generiert und die aufgetretenen Fehler angezeigt werden. Zwecks besserer Wiederverwendbarkeit kann man dafür den Form-Builder (engl. sinngem. "Formularersteller") einsetzen. Grundprinzip ------------ Der Yii Form-Builder verwendet ein [CForm]-Objekt, in dem sämtliche Angaben über ein HTML-Formular abgelegt sind, also u.a., welche Datenmodels mit dem Formular verknüpft sind, welche Art von Eingabefeldern verwendet werden und wie das ganze Formular gerendert werden soll. Als Entwickler braucht man somit im Wesentlichen nur noch dieses [CForm]-Objekt konfigurieren und kann dann dessen Render-Methode aufrufen, um das Formular anzuzeigen. Die Angaben zu den Eingabefeldern sind als hierarchische Struktur von Formularelementen angelegt. Den Wurzelknoten dieser Baumstruktur bildet das [CForm]-Objekt selbst. Dieses Element hat zwei Gruppen von Kindelementen: [CForm::buttons] und [CForm::elements]. [CForm::buttons] enthält alle Buttons des Formulars (z.B. Submit- oder Resetbuttons), [CForm::elements] sämtliche Eingabeelemente, statischen Texte und Subformulare. Ein Subformular ist einfach ein weiteres [CForm]-Objekt innerhalb der [CForm::elements]-Liste eines anderen Formulars. Es kann sein eigenes Datenmodel, eigene [CForm::buttons] und [CForm::elements] enthalten. Beim Absenden eines Formulars werden alle Daten in den Eingabefeldern der Formularhierarchie übergeben, inklusive der Daten in den Subformular-Feldern. [CForm] bietet einige komfortable Methoden, um die gesendeten Daten automatisch den entsprechenden Models zuzuweisen und die Validierung durchzuführen. Erstellen eines einfachen Formulars ----------------------------------- Unten sehen wir ein Beispiel, wie man mit dem Form-Builder ein Anmeldeformular erstellen kann. Zunächst wird dazu eine Controlleraction für die Anmeldung angelegt: ~~~ [php] public function actionLogin() { $model = new LoginForm; $form = new CForm('application.views.site.loginForm', $model); if($form->submitted('login') && $form->validate()) $this->redirect(array('site/index')); else $this->render('login', array('form'=>$form)); } ~~~ Hier wird ein [CForm]-Objekt mit den Angaben aus `application.views.site.loginForm` erzeugt (der Dateipfad wird als Pfadalias angegeben). Dieses [CForm]-Objekt ist hier als Beispeil mit dem `LoginForm`-Model verknüpft, das wir schon im Kapitel [Erstellen des Models](/doc/guide/form.model) verwendet haben. Der Code ist relativ einfach zu verstehen: Falls das Formular abgeschickt wurde (`$form->submitted('login')`) und alle Eingabefelder fehlerfrei sind (`$form->validate()`) wird auf die Seite `site/index` umgeleitet. Andernfalls soll der `login`-View mit diesem Formular gerendert werden. Der Pfadalias `application.views.site.loginForm` verweist auf die PHP-Datei `protected/views/site/loginForm.php`. Diese Datei muss ein Array mit der [CForm]-Konfiguration zurückliefern: ~~~ [php] return array( 'title'=>'Bitte geben Sie Ihre Anmeldedaten ein', 'elements'=>array( 'username'=>array( 'type'=>'text', 'maxlength'=>32, ), 'password'=>array( 'type'=>'password', 'maxlength'=>32, ), 'rememberMe'=>array( 'type'=>'checkbox', ) ), 'buttons'=>array( 'login'=>array( 'type'=>'submit', 'label'=>'Anmelden', ), ), ); ~~~ Wie üblich entsprechen die Schlüsselnamen dieses Arrays den Namen der Klassenvariablen von [CForm], die mit den entsprechenden Werten belegt werden sollen. Die wichtigsten Einträge sind hierbei [CForm::elements] und [CForm::buttons]. Jede dieser Eigenschaften besteht aus einem weiteren Array, das die Formularelemente definiert. Darauf gehen wir später noch genauer ein. Schließlich wird noch das `login`-Viewscript benötigt, das im einfachsten Fall so aussehen kann: ~~~ [php]

Anmeldung

~~~ > Tip|Tipp: `echo $form` ist äquivalent zu `echo $form->render();`, da [CForm] > nämlich die magische PHP-Methode `__toString` enthält. Darin wird `render()` > aufgerufen und dessen Ausgabe, also das fertige HTML-Formular, zurückgeliefert. Angeben der Formularelemente ---------------------------- Verwendet man den Form-Builder, verlagert sich der Tätigkeitsschwerpunkt weg vom Erstellen des Views, hin zur Definition der Formularelemente. Im folgenden beschreiben wir, wie diese Angaben für [CForm::elements] aussehen müssen. Für [CForm::buttons] gilt analog das selbe, weshalb wir darauf nicht weiter eingehen. [CForm::elements] erwartet ein Array als Wert, wobei jedes Element einem Formularelement entspricht. Dabei kann es sich um ein Eingabelement, statischen Text oder ein Subformular handeln. ### Definieren eines Eingabeelements Ein Eingabeelement besteht im Wesentlichen aus einem Label, einem Eingabefeld, einem Hilfstext und einer Fehleranzeige. Es muss außerdem mit einem Modelattribut verknüpft sein. Die Angaben für ein Element werden in Form einer [CFormInputElement]-Instanz festgelegt. Folgender Beispielcode aus einem [CForm::elements]-Array definiert ein solches Element: ~~~ [php] 'username'=>array( 'type'=>'text', 'maxlength'=>32, ), ~~~ Damit wird festgelegt, dass das entsprechende Modelattribut `username` heißt und das Eingabefeld vom Typ `text` mit einem `maxlength`-Attribut von 32 sein soll. So kann jede beschreibbare Eigenschaft eines [CFormInputElement]s konfiguriert werden. Mit der [hint|CFormInputElement::hint]-Option kann man so zum Beispiel einen Hilfstext angeben oder mit [items|CFormInputElement::items] die Elemente in einer DropDown-, CheckBox- oder RadioButton-List bestimmen (entsprechend den Methoden in [CHtml]). Handelt es sich bei einer Option nicht um den Namen einer [CFormInputElement]-Eigenschaft, wird sie als Attribut des entsprechenden HTML-Elements verwendet. Im obigen Beispiel wird daher z.B. das `maxlength`-Attribut als HTML-Attribut des entsprechenden Eingabefeldes gerendert. Sehen wir uns die [type|CFormInputElement::type]-Option näher an. Mit ihr wird der Typ des Eingabefelds festgelegt. Der Typ `text` steht zum Beispiel für ein normales Textfeld, `password` für ein Passwortfeld. Folgende Typen werden "von Haus aus" von [CFormInputElement] erkannt: - text - hidden - password - textarea - file - radio - checkbox - listbox - dropdownlist - checkboxlist - radiolist Von allen diesen eingebauten Typen wollen wir näher auf die Verwendung der "Listen"-Typen eingehen, also `dropdownlist`, `checkboxlist` und `radiolist`. Diese Typen erfordern, dass die [items|CFormInputElement::items]-Eigenschaft des entsprechenden Eingabeelements wie folgt angegeben wird: ~~~ [php] 'gender'=>array( 'type'=>'dropdownlist', 'items'=>User::model()->getGeschlechtOptions(), 'prompt'=>'Bitte wählen:', ), ... class User extends CActiveRecord { public function getGeschlechtOptions() { return array( 0 => 'Männlich', 1 => 'Weiblich', ); } } ~~~ Dieser Code erzeugt eine Dropdownliste mit dem Aufforderungstext "Bitte wählen:". Die Liste enthält die Optionen "Männlich" und "Weiblich", so wie sie von der `getGeschlechtOptions`-Methode in der `User`-Klasse geliefert werden. Daneben kann die [type|CFormInputElement::type]-Option auch den Namen einer Widgetklasse oder deren Pfadalias enthalten. Die Widgetklasse muss lediglich [CInputWidget] oder [CJuiInputWidget] erweitern. Beim Rendern des Elements wird eine Instanz der angegebenen Widgetklasse mit den angegebenen Elementparametern erzeugt und gerendert. ### Verwenden von statischem Text In vielen Fällen enthält ein Formular zusätzlichen, rein "dekorativen" HTML-Code, wie zum Beispiel eine horizontale Linie um Formularabschnitte voneinander zu trennen. An anderen Stellen wird eventuell ein Bild zur Auflockerung des Formulars verwendet. Solche statischen Elemente werden einfach als String an der entsprechenden Stelle des [CForm::elements]-Arrays angeben. Hier ein Beispiel: ~~~ [php] return array( 'elements'=>array( ...... 'password'=>array( 'type'=>'password', 'maxlength'=>32, ), '
', 'rememberMe'=>array( 'type'=>'checkbox', ) ), ...... ); ~~~ Zwischen die Elemente für `password` und `rememberMe` wird so eine horizontale Linie eingefügt. Statischer Text eignet sich am besten für unregelmäßig verteilte Inhalte. Sollen hingegen alle Elemente mit ähnlicher "Dekoration" versehen werden, ist es günstiger, eine eigene Rendermethode zu verwenden. Darauf werden wir weiter unten noch eingehen. ### Verwenden von Subformularen Subformulare eignen sich dazu, sehr lange Formulare in mehrere logisch zusammenhängende Blöcke zu gliedern. Ein langes Registrierungsformular könnte man so z.B. in Anmelde- und Profildaten unterteilen. Ein Subformular kann - muss aber nicht - mit einem eigenen Datenmodel verknüpft sein. Wenn beim erwähnten Registrierungsformular die Anmelde- und Profildaten in zwei unterschiedlichen Datebanktabellen (und damit zwei Datenmodels) gespeichert werden, würde man jedes Subformular mit dem entsprechenden Datenmodel verknüpfen. Speichert man alles in einer einzelnen Tabelle braucht keines der Subformulare ein Model, da sie dann das Model des Wurzelformulars verwenden. Ein Subformular ist ebenfalls ein [CForm]-Objekt. Ein Subformular wird dem [CForm::elements]-Array als Element vom Typ `form` hinzugefügt: ~~~ [php] return array( 'elements'=>array( ...... 'user'=>array( 'type'=>'form', 'title'=>'Anmeldedaten', 'elements'=>array( 'username'=>array( 'type'=>'text', ), 'password'=>array( 'type'=>'password', ), 'email'=>array( 'type'=>'text', ), ), ), 'profile'=>array( 'type'=>'form', ...... ), ...... ), ...... ); ~~~ Auch das Subformular besteht im Wesentlichen wieder aus Einträgen im [CForm::elements]-Array. Soll das Subformular mit einem eigenen Model verknüpft werden, kann dies über die [CForm::model] angegeben werden. Manchmal kann es nötig sein, eine andere Formklasse statt [CForm] zu verwenden. Wie wir in Kürze sehen werden, kann man z.B. eine eigene Klasse von [CForm] ableiten, um die Renderlogik anzupassen. Sämtliche Subformulare verwenden standardmäßig die selbe Klasse wie deren Elternelement. Soll ein Subformular eine andere Klasse verwenden, kann der Typ auf `XyzForm` gesetzt werden (also einen String, der auf `Form` endet). Das Subformular wird dann als `XyzForm`-Objekt erstellt. Zugriff auf Formularelemente ---------------------------- Auf Formularelemente kann man wie auf Arrayelemente zugreifen. Liest man die Eigenschaft [CForm::elements] aus, erhält man ein Objekt vom Typ [CFormElementCollection] zurück, das wiederum von [CMap] abgeleitet wurde. Dadurch kann es wie ein normales Array angesprochen werden. Das Element `username` des weiter oben definierten Loginformulars erhält man zum Beispiel mit: ~~~ [php] $username = $form->elements['username']; ~~~ Und auf das `email`-Element des Registrierungsformulars kann man so zugreifen: ~~~ [php] $email = $form->elements['user']->elements['email']; ~~~ [CForm] implementiert außerdem das ArrayAccess-Interface so, dass man damit direkt auf [CForm::elements] zugreifen kann. Statt den obigen Zeilen kann man daher noch einfacher schreiben: ~~~ [php] $username = $form['username']; $email = $form['user']['email']; ~~~ Erstellen eines verschachtelten Formulars ----------------------------------------- Formulare, die, wie eben beschrieben, Subformulare enthalten, nennen wir verschachtelte Formulare (engl.: nested forms). Anhand des erwähnten Registrierungsformulars zeigen wir hier, wie man ein verschachteltes Formular erstellt, das mit mehreren Datenmodels verknüpft ist. Dabei seien die Anmeldeinformation im Model `User` und die Profildaten im Model `Profile` gespeichert. Zunächst benötigen wir eine `register`-Action: ~~~ [php] public function actionRegister() { $form = new CForm('application.views.user.registerForm'); $form['user']->model = new User; $form['profile']->model = new Profile; if($form->submitted('register') && $form->validate()) { $user = $form['user']->model; $profile = $form['profile']->model; if($user->save(false)) { $profile->userID = $user->id; $profile->save(false); $this->redirect(array('site/index')); } } $this->render('register', array('form'=>$form)); } ~~~ Die Formularkonfiguration wird hier in `application.views.user.registerForm` abgelegt. Wurde das Formular abgeschickt und die Daten erfolgreich geprüft, wird versucht, die Models `User` und `Profile` zu speichern. Diese Models können über die `model`-Eigenschaft des jeweiligen Subformulars bezogen werden. Da die Validierung bereits durchgeführt wurde, wird `$user->save(false)` aufgerufen, um eine nochmalige Prüfung zu verhindern. Mit dem `Profile`-Model wird ebenso verfahren. Sehen wir uns als nächstes die Formularkonfiguration in `protected/views/user/registerForm.php` an: ~~~ [php] return array( 'elements'=>array( 'user'=>array( 'type'=>'form', 'title'=>'Anmeldedaten', 'elements'=>array( 'username'=>array( 'type'=>'text', ), 'password'=>array( 'type'=>'password', ), 'email'=>array( 'type'=>'text', ) ), ), 'profile'=>array( 'type'=>'form', 'title'=>'Profildaten', 'elements'=>array( 'firstName'=>array( 'type'=>'text', ), 'lastName'=>array( 'type'=>'text', ), ), ), ), 'buttons'=>array( 'register'=>array( 'type'=>'submit', 'label'=>'Registrieren', ), ), ); ~~~ Bei jedem Subformular wird hier auch eine [CForm::title]-Eigenschaft definiert. Standardmäßig sorgt die Renderlogik dafür, dass jedes Subformular in ein eigenes fieldset mit dieser Eigenschaft als Titel eingebettet wird. Nun fehlt nur noch das Viewscript für `register`: ~~~ [php]

Registrierung

~~~ Anpassen der Formularausgabe ---------------------------- Der eigentliche Nutzen des Form-Builders liegt in der Trennung von Logik (Formularkonfiguration in einer eigenen Datei) und Präsentation ([CForm::render]-Methode). Dadurch kann die Anzeige des Formulars angepasst werden. Entweder, indem man [CForm::render] überschreibt oder indem man einen Teilview zum Rendern des Formulars angibt. Beide Ansätze sind unabhängig von der Formularkonfiguration und lassen sich so einfach wiederverwenden. Überschreibt man [CForm::render], so müssen dort eigentlich nur [CForm::elements] und [CForm::buttons] in einer Schleife durchlaufen und die [CFormElement::render]-Methode jedes Elements aufgerufen werden: ~~~ [php] class MyForm extends CForm { public function render() { $output = $this->renderBegin(); foreach($this->getElements() as $element) $output .= $element->render(); $output .= $this->renderEnd(); return $output; } } ~~~ Zum Rendern des Formular kann man auch ein Viewscript `_form` verwenden: ~~~ [php] renderBegin(); foreach($form->getElements() as $element) echo $element->render(); echo $form->renderEnd(); ~~~ Dieses Script kann so aufgerufen werden: ~~~ [php]
$this->renderPartial('_form', array('form'=>$form));
~~~ Falls ein Formular nicht mit diesem allgemeinen Renderansatz dargestellt werden kann (z.B. weil einige Elemente vollkommen anders aussehen müssen), kann man im Viewscript auch so verfahren: ~~~ [php] Einige komplexe UI-Elemente hier Einige komplexe UI-Elemente hier einige komplexe UI-Elemente hier ~~~ Bei dieser Methode scheint der Form-Builder nicht viel zu nützen, da man immer noch fast genauso viel Code wie bisher braucht. Trotzdem kann sich der Einsatz lohnen, da das Formular in einer separaten Datei definiert wird und sich der Entwickler so besser auf die Logik konzentrieren kann.
$Id: form.builder.txt 2890 2011-01-18 15:58:34Z qiang.xue $