FormControl Taglib

Im Entwickler-Forum können Implementierungsdetails sowie Alternativen der Umsetzung diskutiert werden. // Here, developers can discuss implementation details of features of their projects.
Thalo
Beiträge: 247
Registriert: 10.08.2009, 16:56:52

FormControl Taglib

Beitrag von Thalo » 02.08.2017, 01:09:01

Hallo zusammen,

der Einsatz von Frontend-Frameworks wie Bootstrap erzeugt eine Menge Boilerplate-Code für z.B. Formulare:

Code: Alles auswählen

<div class="form-group">
  <label for="example" class="col-md-6 control-label">Example</label>
  <div class="col-md-6">
   <form:text name="example" class="form-control" id="example"/>
    <form:listener control="example">
      <span class="help-block">Example validator</span>
    </form:listener>
  </div>
</div>
Wie lässt sich das ganze hinsichtlich Wiederverwendbarkeit in einer Taglib kapseln ohne jedes Form-Control neu implementieren zu müssen?

Benutzeravatar
dr.e.
Administrator
Beiträge: 4552
Registriert: 04.11.2007, 16:13:53

Re: FormControl Taglib

Beitrag von dr.e. » 03.08.2017, 16:56:11

Hallo Thalo,

aus meiner Sicht gibt es zwei Möglichkeiten: entweder das Markup genau wie gepostet übernehmen und entsprechend mehr Code verwalten oder die Definition eines solchen (komplexeren) Formular-Elements in einer eigenen Formular-Komponente kapseln.

Für die Verwendung ist sicher Option 2 die einfachere, wenn du allerdings unterschiedliche Anforderungen an das Styling hast, könnte es sich wieder lohnen den Mehraufwand für jedes Control zu akzeptieren. Bei PAYBACK gehen wir beide Wege, zumeist sind die Formulare aber mit explizitem Markup (siehe dein Beispiel) aufgebaut. Den ersten Weg nutzen wir z.B. bei Felder, die hinsichtlich ihres Verhaltens (anzeigen: ja/nein, Länge, ...) über das CMS steuerbar sind.

Ich kann nicht abschätzen, wie viele Felder dein Formular/deine Formulare haben, die Implementierung eines solchen gekapselten Form-Controls könnte sich da allerdings schon relativ schnell rechnen. Das allerdings nur, wenn du wirklich identisches Markup hast.

Lass mich wissen, wenn ich dir Beispiele für die Implementierung geben soll. Hier könntest du beispielsweise auch eine Mischung aus Tag und Template implementieren um das Control zwar durch einen Tag zu repräsentieren, das genaue Aussehen dann allerdings wieder in ein Template auslagern. So wärst du sehr flexibel neue Formate zu erstellen.
Viele Grüße,
Christian

Thalo
Beiträge: 247
Registriert: 10.08.2009, 16:56:52

Re: FormControl Taglib

Beitrag von Thalo » 04.08.2017, 00:53:12

Hallo Christian,

Beispiele wären schön! :-) Im Backend der Applikation sind es sehr viele Formulare alle mit identischem Style. Basiert eure Implementierung auf dem Interface? Dann habt ihr auch die Mapper neu implementiert?

Benutzeravatar
dr.e.
Administrator
Beiträge: 4552
Registriert: 04.11.2007, 16:13:53

Re: FormControl Taglib

Beitrag von dr.e. » 04.08.2017, 09:17:02

Hallo Thalo,

ich stelle dir ein Beispiel zusammen.

Die Mapper setzen wir aktuell nicht ein, wären aber für solche Felder dann neu zu implementieren, wenn der Mapper das innen liegende Feld nicht via getFormElementBy*() finden kann (z.B. dann, wenn du das oben aufgeführte Text-Feld als lokale Variable in so einem Tag hälst statt als "echtes" Kind einhängst).
Viele Grüße,
Christian

Benutzeravatar
dr.e.
Administrator
Beiträge: 4552
Registriert: 04.11.2007, 16:13:53

Re: FormControl Taglib

Beitrag von dr.e. » 06.08.2017, 21:51:38

Hallo Thalo,

ich habe auf Basis deines Markup ein Beispiel für die Definition eines wiederverwendbaren Tags zusammengestellt.

Ein "statisches" - also diskret implementiertes Beispiel - für komplexe Strukturen findest du im APF z.B. mit dem Tag DateSelectorTag. Dieser beinhaltet 3 Text-Felder und generiert zudem entsprechendes Markup im Tag. Ein weiteres Beispiel wäre der ReCaptchaTag.

Um der komplexen und sich verändernden HTML-Struktur Rechnung zu tragen, habe ich mich auf ein dynamisches Beispiel beschränkt.

Das "dynamische" Formular-Tag erlaubt es an Hand weniger Attribute eine komplexe HTML-Struktur zu definieren. Diese kannst du (im Rahmen der Implementierung) beliebig verändern und damit dein HTML entsprechend deinen Bedürfnissen anpassen.

Formular:
Im einfachsten Fall beinhaltet dein Formular ein Feld, das über den generischen Tag gekapselt ist:

Code: Alles auswählen

<html:form ...>
   <complex:text-field label="Example" representation="test_field" name="foo" listener="Example Validator"/>
</form>
Der Tag nimmt die variablen Informationen entgegen, die pro Feld unterschiedlich sind. Der Einfachheit wegen habe ich die variablen Anteile als Tag-Attribute übergeben. Sollen diese dynamisch evaluiert werden (z.B. in einem Controller) muss die Implementierung angepasst werden.

Tag:
Der Tag läd an Hand des Attributs representation ein HTML-Template (hier: test_field.html) und initialisiert die darin vorkommenden Elemente (Label, Text-Feld, Listener) mit den definierten Werten:

Code: Alles auswählen

class ComplexTextFieldTag extends FormGroupTag {

   public function onParseTime() {

      $this->loadDesign(__NAMESPACE__ . '\templates', $this->getAttribute('representation'));

      // re-label fields
      $name = $this->getAttribute('name');

      $label = $this->getChildNode('name', 'dummy-label', FormLabelTag::class);
      $label->setAttribute('for', $name);
      $label->deleteAttribute('name'); // maybe not relevant due to white list
      $label->setContent($this->getAttribute('label'));

      $field = $this->getChildNode('name', 'dummy-field', TextFieldTag::class);
      $field->setAttribute('name', $name);
      $field->setAttribute('id', $name);

      /* @var $listener ValidationListenerTag */
      $listener = $this->getChildNode('name', 'dummy-listener', ValidationListenerTag::class);
      $listener->setAttribute('control', $name);
      $listener->setAttribute('name', $name . '-Listener');

      // fill content of listener
      $listener->setPlaceHolder('content', $this->getAttribute('listener'));

   }

   /**
    * @return ValidationListenerTag
    */
   public function &getListener() {

      $name = $this->getAttribute('name');
      return $this->getChildNode('name', $name . '-Listener', ValidationListenerTag::class);
   }

}
Die Methode getListener() habe ich zur Manipulation des Zustands für den Unit Test eingeführt.

Unit Test:
Im folgenden Unit Test kannst du sehen, wie ein einfacher Test-Fall mit und ohne Information des Listeners so wie ein komplexer Testfall aussieht:

Code: Alles auswählen

class ComplexTextFieldTagTest extends \PHPUnit_Framework_TestCase {

   public static function setUpBeforeClass() {
      Document::addTagLib(ComplexTextFieldTag::class, 'complex', 'text-field');
   }

   public function testComplexField() {
      $form = $this->getForm();

      echo $html = $form->transformForm();

      $this->assertContains('for="foo">Example</label>', $html);
      $this->assertContains('name="foo" class="form-control" id="foo"', $html);
   }

   public function testComplexFieldWithListenerNotified() {

      $form = $this->getForm();

      /* @var $field ComplexTextFieldTag */
      $field = $form->getFormElementByName('foo');
      $listener = $field->getListener();
      $listener->notify();

      echo $html = $form->transformForm();

      $this->assertContains('<span class="help-block">Example Validator</span>', $html);
   }

   public function testFormWithMultipleInstances() {

      $form = new HtmlFormTag();
      $form->setContent('<complex:text-field label="Example 1" representation="test_field" name="foo1" listener="Example 1 Validator"/>
<complex:text-field label="Example 2" representation="test_field" name="foo2" listener="Example 2 Validator"/>
<complex:text-field label="Example 3" representation="test_field" name="foo3" listener="Example 3 Validator"/>');

      $doc = new Document();
      $form->setParentObject($doc);

      $form->onParseTime();
      $form->onAfterAppend();

      /* @var $field ComplexTextFieldTag */
      $field = $form->getFormElementByName('foo2');
      $listener = $field->getListener();
      $listener->notify();

      echo PHP_EOL . PHP_EOL . 'Form w/ 3 fields: ' . PHP_EOL . PHP_EOL;

      echo $html = $form->transformForm();

      $this->assertContains('<span class="help-block">Example 2 Validator</span>', $html);

      $this->assertContains('for="foo1">Example 1</label>', $html);
      $this->assertContains('for="foo2">Example 2</label>', $html);
      $this->assertContains('for="foo3">Example 3</label>', $html);

      $this->assertContains('name="foo1" class="form-control" id="foo1', $html);
      $this->assertContains('name="foo2" class="form-control" id="foo2', $html);
      $this->assertContains('name="foo3" class="form-control" id="foo3', $html);

   }

   /**
    * @return HtmlFormTag
    */
   protected function getForm() {
      $form = new HtmlFormTag();
      $form->setContent('<complex:text-field label="Example" representation="test_field" name="foo" listener="Example Validator"/>');

      $doc = new Document();
      $form->setParentObject($doc);

      $form->onParseTime();
      $form->onAfterAppend();

      return $form;
   }

}
Template:
Das Formular-Element-Template ist nach deinem Beispiel oben aufgebaut und wurde in einigen Punkten leicht modifiziert um die Initialisierung durch den Tag zu ermöglichen (z.B. feste Werte für Namen, ...):

Code: Alles auswählen

<div class="form-group">
    <form:label name="dummy-label" class="col-md-6 control-label"/>
    <div class="col-md-6">
        <form:text name="dummy-field" class="form-control" />
        <form:listener name="dummy-listener">
            <span class="help-block">${content}</span>
        </form:listener>
    </div>
</div>
Durch die festen Werte kann der Tag die entsprechenden Instanzen konfigurieren und ihnen die "richtigen" Werte geben, die später im Formular relevant sind.


Ich hoffe das Beispiel hilft dir! :)
Viele Grüße,
Christian

Thalo
Beiträge: 247
Registriert: 10.08.2009, 16:56:52

Re: FormControl Taglib

Beitrag von Thalo » 07.08.2017, 23:28:50

Hallo Christian,

vielen Dank für das schöne Beispiel! :-)

Wie könnte man dieses adaptieren um das Markup nicht zwischen verschiedenen Typen (text, select, textearea, ..) kopieren zu müssen?

Ich würde gerne abhängig davon ob die FormControl-Value leer ist, eine CSS-Klasse setzen. Die Value ist aber zur Parse-Time noch nicht bekannt. Korrekt?

Benutzeravatar
dr.e.
Administrator
Beiträge: 4552
Registriert: 04.11.2007, 16:13:53

Re: FormControl Taglib

Beitrag von dr.e. » 14.08.2017, 14:51:19

Hi Thalo,

entschuldige meine späte Antwort, ich war im Urlaub und habe deinen Beitrag erst jetzt wahrgenommen! :roll:
Wie könnte man dieses adaptieren um das Markup nicht zwischen verschiedenen Typen (text, select, textearea, ..) kopieren zu müssen?
Dazu könntest du die Implementierung des Tags so anpassen, dass er erkennt, welcher Feld-Typ gewünscht ist. Im Wesentlichen betrifft das ja die Zeile

Code: Alles auswählen

$field = $this->getChildNode('name', 'dummy-field', TextFieldTag::class);
Die Unterscheidung könnte beispielsweise an einem zusätzlichen Attribut im Tag erfolgten.
Ich würde gerne abhängig davon ob die FormControl-Value leer ist, eine CSS-Klasse setzen. Die Value ist aber zur Parse-Time noch nicht bekannt. Korrekt?
Korrekt. Statische Werte (Formular ist noch nicht abgeschickt) sind zu diesem Zeitpunkt noch nicht bekannt. Sofern das Formular abgeschickt ist, übernimmt die onParseTime() in den meisten Controls das Presetting und du kannst entscheiden, ob leer oder gefüllt. Um das Verhalten konsistent für alle Fälle umzusetzen schlage ich vor das in der transform()-Methode des ComplexTextFieldTag zu implementieren. Dort könntest du mit einem zusätzlichen setPlaceHolder() eine entsprechende Klasse setzen.

Hoffe das hilft dir! :)
Viele Grüße,
Christian

Thalo
Beiträge: 247
Registriert: 10.08.2009, 16:56:52

Re: FormControl Taglib

Beitrag von Thalo » 14.08.2017, 23:54:59

Hallo Christian,

kein Problem. Ich hoffe du hast dich entspannen können. :-)

Bei deinem Beispiel ist mir aufgefallen, dass das Presetting nicht funktioniert und der ListenerTag nicht benachrichtigt wird.

Beispiel:

Code: Alles auswählen

<html:form name="form">
   <core:addtaglib class="APP\pres\taglib\form\ComplexTextFieldTag" prefix="complex" name="text-field"/>
   <complex:text-field label="Example" representation="test_field" name="foo" listener="Example Validator"/>
   <form:button name="send" value="Save" />
   <form:addvalidator class="APF\tools\form\validator\TextLengthValidator" control="foo" button="send" />
</html:form>
Kannst du das mal testen? :-)

Benutzeravatar
dr.e.
Administrator
Beiträge: 4552
Registriert: 04.11.2007, 16:13:53

Re: FormControl Taglib

Beitrag von dr.e. » 16.08.2017, 09:05:40

Hallo Thalo,

das kann ich mir gerne mal ansehen. Soweit habe ich die Implementierung noch nicht getestet (siehe Unit Test). Melde mich. :)
Viele Grüße,
Christian

Benutzeravatar
dr.e.
Administrator
Beiträge: 4552
Registriert: 04.11.2007, 16:13:53

Re: FormControl Taglib

Beitrag von dr.e. » 20.08.2017, 11:04:15

Hallo Thalo,

Fehler in der Implementierung gefunden - das Problem war zeitlicher Natur: die Felder werden mit Ausführung des loadDesign() bereits mit den "falschen" (=Dummy-Werten) initialisiert und damit greifen Validierungen und Presetting nicht. Ferner muss der ComplexTextFieldTag das Anhängen von Validatoren und Filtern auch an das innen liegende Text-Feld delegieren, dann ist auch die Validierung voll funktionsfähig.

Anbei poste ich dir die komplette Implementierung:

Tag:

Code: Alles auswählen

use APF\tools\form\filter\FormFilter;
use APF\tools\form\taglib\FormGroupTag;
use APF\tools\form\taglib\FormLabelTag;
use APF\tools\form\taglib\TextFieldTag;
use APF\tools\form\taglib\ValidationListenerTag;
use APF\tools\form\validator\FormValidator;

class ComplexTextFieldTag extends FormGroupTag {

   public function onParseTime() {

      $name = $this->getAttribute('name');
      $listener = $this->getAttribute('listener');
      $label = $this->getAttribute('label');

      $html = '<div class="form-group">
    <form:label name="' . $name . '" for="' . $name . '" class="col-md-6 control-label">' . $label . '</form:label>
    <div class="col - md - 6">
        <form:text name="' . $name . '" id="' . $name . '" class="form-control" />
        <form:listener name="' . $name . '-Listener" control="' . $name . '">
            <span class="help - block">' . $listener . '</span>
        </form:listener>
    </div>
</div>';

      $this->setContent($html);
      $this->extractTagLibTags();

   }

   public function addValidator(FormValidator $validator) {
      $this->getTextField()->addValidator($validator);
   }

   public function addFilter(FormFilter $filter) {
      $this->getTextField()->addFilter($filter);
   }

   /**
    * @return ValidationListenerTag
    */
   public function getListener() {
      return $this->getChildNode('name', $this->getAttribute('name') . '-Listener', ValidationListenerTag::class);
   }

   /**
    * @return TextFieldTag
    */
   public function getTextField() {
      return $this->getChildNode('name', $this->getAttribute('name'), TextFieldTag::class);
   }

}
Alternativ zum Zusammenbauen von HTML im Tag könntest du das File auch mit Platzhaltern a la {name} o.ä. ausstatten und dann mit file_get_contents() laden und ersetzen und anschließend extractTagLibTags() ausführen. Dann werden - wie auch im Beispiel-Tag - die Felder sofort mit den richtigen Werten initialisiert und die innen liegenden Feldern verhalten sich korrekt.

Applikations-Template:

Code: Alles auswählen

<@controller
   class="DEV\wizard\controller\ComplexTextFieldTestController"
@>
<core:addtaglib class="DEV\wizard\tags\ComplexTextFieldTag" prefix="complex" name="text-field"/>
<html:form name="form">
   <complex:text-field label="Example" representation="text-field" name="foo" listener="Example Validator"/>
   <form:button name="send" value="Save"/>
   <form:addvalidator class="APF\tools\form\validator\TextLengthValidator" control="foo" button="send"/>
</html:form>
Controller:

Code: Alles auswählen

use APF\core\pagecontroller\BaseDocumentController;

class ComplexTextFieldTestController extends BaseDocumentController {

   public function transformContent() {

      $form = $this->getForm('form');

      if ($form->isValid()) {

      } else {

      }
      $form->transformOnPlace();
   }

}

Damit solltest du jetzt weiter kommen! :)
Viele Grüße,
Christian

Benutzeravatar
dr.e.
Administrator
Beiträge: 4552
Registriert: 04.11.2007, 16:13:53

Re: FormControl Taglib

Beitrag von dr.e. » 20.08.2017, 11:17:16

Ergänzend noch die Implementierung mit direktem Laden der Inhalte aus einem Template-File:

Form-Controls-Template:

Code: Alles auswählen

<div class="form-group">
   <form:label name="${name}" for="${name}" class="col-md-6 control-label">${label}</form:label>
   <div class="col-md-6">
      <form:text name="${name}" id="${name}" class="form-control"/>
      <form:listener name="${name}-Listener" control="${name}">
         <span class="help-block">${listener}</span>
      </form:listener>
   </div>
</div>
Tag:

Code: Alles auswählen

use APF\tools\form\filter\FormFilter;
use APF\tools\form\taglib\FormGroupTag;
use APF\tools\form\taglib\TextFieldTag;
use APF\tools\form\taglib\ValidationListenerTag;
use APF\tools\form\validator\FormValidator;

class ComplexTextFieldTag extends FormGroupTag {

   public function onParseTime() {

      $template = $this->getTemplateFilePath(__NAMESPACE__ . '\templates', $this->getAttribute('representation'));

      $html = file_get_contents($template);

      foreach ($this->getAttributes() as $key => $value) {
         $html = str_replace('${' . $key . '}', $value, $html);
      }

      $this->setContent($html);
      $this->extractTagLibTags();

   }

   public function addValidator(FormValidator $validator) {
      $this->getTextField()->addValidator($validator);
   }

   public function addFilter(FormFilter $filter) {
      $this->getTextField()->addFilter($filter);
   }

   /**
    * @return ValidationListenerTag
    */
   public function getListener() {
      return $this->getChildNode('name', $this->getAttribute('name') . '-Listener', ValidationListenerTag::class);
   }

   /**
    * @return TextFieldTag
    */
   public function getTextField() {
      return $this->getChildNode('name', $this->getAttribute('name'), TextFieldTag::class);
   }

}

Am eigentlichen Formular-Template der Applikation ändert sich dadurch natürlich nichts.
Viele Grüße,
Christian

Benutzeravatar
dr.e.
Administrator
Beiträge: 4552
Registriert: 04.11.2007, 16:13:53

Re: FormControl Taglib

Beitrag von dr.e. » 20.08.2017, 11:19:13

Da die Implementierung sehr generisch gewählt ist (siehe Ersetzung von Parametern im Template-File) könnte ich mir durchaus vorstellen, daraus ein APF-Feature zu schneiden. Lass mich gerne wissen, ob du mit der Implementierung weiter kommst und ob dir ein natives APF-Feature hier weiterhelfen würde.

Danke für deine Rückmeldung! :)
Viele Grüße,
Christian

Thalo
Beiträge: 247
Registriert: 10.08.2009, 16:56:52

Re: FormControl Taglib

Beitrag von Thalo » 20.08.2017, 23:32:57

Hallo Christian,

zu deiner ersten Implementierung ist mir aufgefallen:

Code: Alles auswählen

$form->fillForm($model) -> funktioniert
$form->getFormElementByName('foo')->setValue('bar') -> funktioniert nicht
$form->getFormElementsByName('foo')[2]->setValue('bar') -> funktioniert
$form->fillModel($model) -> funktioniert nicht
Lösung:

Code: Alles auswählen

public function &setValue($value) {
    return $this->getTextField()->setValue($value);
}

public function getValue() {
    return $this->getTextField()->getValue();
}
Das FormControl-Interface muss als Delegate implementiert werden. Oder gibt es eine bessere Lösung?

Ferner funktioniert die Validation noch nicht wenn es das einzige invalide Control ist. Beispiel :

Code: Alles auswählen

<html:form name="form">
<complex:text-field label="Example" name="foo" listener="Example Validator"/> -> INVALID 
<form:text name="bar"/> -> VALID
</html:form>

Code: Alles auswählen

$this->getForm('form')->isValid() // TRUE
dr.e. hat geschrieben:Lass mich gerne wissen, ob du mit der Implementierung weiter kommst und ob dir ein natives APF-Feature hier weiterhelfen würde.
Auf jeden Fall! :) Lass mich wissen, falls ich dich dabei unterstützen kann.

Benutzeravatar
dr.e.
Administrator
Beiträge: 4552
Registriert: 04.11.2007, 16:13:53

Re: FormControl Taglib

Beitrag von dr.e. » 21.08.2017, 09:17:42

Hallo Thalo,
Das FormControl-Interface muss als Delegate implementiert werden. Oder gibt es eine bessere Lösung?
Theoretisch sollte es bereits ohne Eingriff funktionieren, da die Klasse FormGroupTag dafür gedacht ist, "innen" liegende Formular-Elemente zu kapseln und von aussen auch via getFormElementByName() auffindbar zu machen. Ich schaue mir das mal im Rahmen eines generischen APF-Features an.

Kurzfristig funktioniert dein Vorschlag natürlich ohne Probleme.
Ferner funktioniert die Validation noch nicht wenn es das einzige invalide Control ist. Beispiel :
Das ist auch ein korrektes Verhalten. Grund: es ist kein Validator definiert, damit wird keine Validierung ausgeführt und damit auch kein Listener informiert.
Auf jeden Fall! :) Lass mich wissen, falls ich dich dabei unterstützen kann.
Freut mich! Für mich wäre auf jeden Fall ein konkreter Anwendungsfall von Interesse, sprich wie ist dein Formular aufgebaut, wie gestaltet sich der User Flow, welche konkreten Anforderungen hast du an die Felder hinsichtlich Markup, Validierung, Filterung, ... Je mehr Details, desto besser, dann kann ich daraus einen generischen Tag extrahieren.

Anschließend hilft mir natürlich zusätzlich zu Unit Tests und eigenen manuellen Tests ein "real life"-Test von dir! Wenn du bei der Implementierung direkt mitwirken möchtest, natürlich immer gerne. Wir können uns dafür ja einen eigenen Feature-Branch anlegen.

Freue mich auf die Implementierung!
Viele Grüße,
Christian

Thalo
Beiträge: 247
Registriert: 10.08.2009, 16:56:52

Re: FormControl Taglib

Beitrag von Thalo » 21.08.2017, 12:24:13

Hallo Christian,
dr.e. hat geschrieben:
21.08.2017, 09:17:42
Das ist auch ein korrektes Verhalten. Grund: es ist kein Validator definiert, damit wird keine Validierung ausgeführt und damit auch kein Listener informiert.
Beispiel:

Code: Alles auswählen

<html:form name="form">
    <complex:text-field label="Example" name="foo" listener="Example Validator"/>
    <form:text name="bar"/>
    <form:button name="send" value="Save" />
    <form:addvalidator class="APF\tools\form\validator\TextLengthValidator" control="foo" button="send" />
    <form:addvalidator class="APF\tools\form\validator\NumberValidator" button="send" control="bar"/>
</html:form>
Erzeugt das beschriebene Verhalten:
foo -> invalid
bar -> valid
form -> valid

Erwartet:

foo -> invalid
bar -> valid
form -> invalid

Antworten

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder und 1 Gast