SilverStripe FormFields

and the magic beyond <input type=text/>


@Zauberfisch on IRC (freenode) GitHub Twitter

What's the Problem?

  • limited number of existing field types
  • certain combinations not possible
  • bad usability (eg nested GridFields)

Simple Example

heyday/silverstripe-colorpalette

heyday/silverstripe-colorpalette


class ColorPaletteField extends OptionsetField {
  public function Field($properties = []) {
    Requirements::css('[...]/ColorPaletteField.css');
    return parent::Field($properties);
  }
  public function performReadonlyTransformation() {
    /* [...] */
  }
}
ColorPaletteField.css - 48 Lines of CSS
private static $db = [
  'Title1' => 'Varchar(225)',
  'Icon1' => 'Varchar',
  'Title2' => 'Varchar(225)',
  'Icon2' => 'Varchar',
  'Title3' => 'Varchar(225)',
  'Icon3' => 'Varchar',
  'Title4' => 'Varchar(225)',
  'Icon5' => 'Varchar',
  [...]
],
private static $has_one = [
  'Link1Page' => 'SiteTree',
  'Link2Page' => 'SiteTree',
  'Link3Page' => 'SiteTree',
  'Link4Page' => 'SiteTree',
  [...]
];

public function getCMSFields() {
  $return = parent::getCMSFields();
  for ($i = 1; $i < 10; $i++) {
    $return->push(new FieldGroup([
      new TextField("Title{$i}", [...]),
      new DropdownField("Icon{$i}", [...]),
      new TreeDropdownField("Link{$i}PageID", [...]),
    ]));
  }
  return $return;
}
Solution?
  • GridField?
GridField
  • to many clicks
  • inline*? bad layout

* symbiote/silverstripe-gridfieldextensions

Solution?
  • GridField?
  • MultiValueField?
MultiValueField*
  • max 1-2 fields per record

* symbiote/silverstripe-multivaluefield

Solution?
  • GridField?
  • MultiValueField?
  • Write your own
  • (or use the one I've written now)*

* zauberfisch/silverstripe-serialized-dataobject

Background Story

The magic in FormField

class FormField [...] {
  [...]
  function Field() {
    return sprintf('', $this->name, $this->value);
  }
  function FieldHolder() {
    return sprintf('
[...]%s[...]
', $this->Field()); } function validate($validator) { return true; } function setValue($val) { $this->value = $val; } function saveInto(DataObjectInterface $record) { $record->{$this->name} = $this->value; } [...] }
class FormField extend RequestHandler {
  [...]
  [...]
  private static $url_handlers = [
    '$Action' => '$Action',
  ];
  private static $allowed_actions = [];
  public function Link($action = null) {
    return Controller::join_links(
      $this->form->FormAction(),
      'field/' . $this->name,
      $action
    );
  }
  [...]
}
/admin
/pages/edit CMSPagesController
 /EditForm/1 Form
  /field/PageBuilder PageBuilder_FormField
   /handleContentElement/12/13 PageBuilder_ContentElementHandler
    /Form Form
     /field/LinkPageID TreeDropdownField
      /tree TreeDropdownField->tree()

Another Example

admin
/pages/edit CMSPagesController
 /EditForm/2 Form
  /field/URLSegment SiteTreeURLSegmentField
   /suggest/?value=test-123 SiteTreeURLSegmentField->suggest()
class SiteTreeURLSegmentField extends TextField {
  [...]
  private static $allowed_actions = ['suggest'];
  function suggest($request) {
    $page = $this->getPage();
    $page->URLSegment = $page->generateURLSegment($request->getVar('value'));
    $count = 2;
    while(!$page->validURLSegment()) {
      $page->URLSegment = preg_replace('/-[0-9]+$/', null, $page->URLSegment) . '-' . $count;
      $count++;
    }
    return $page->URLSegment;
  }
  [...]
}

serialized-dataobject* module

A GridField & MultiValueField alternative
  • Serializable DBField (string field storing objects)
  • Serializable DataObject (ArrayData / Fake DataObject)
  • Serializable ArrayList (list of SerializedDataObject)
  • Serializable DataList (list of IDs)
  • Serializable FormFields:
    • ArrayListField (GridField)
    • UploadField

* zauberfisch/silverstripe-serialized-dataobject

Example: UploadField

class Page extends SiteTree {
  private static $db = [
    'GalleryImagesList' => \zauberfisch\SerializedDataObject\DBField\DataListField::class,
  ];
  public function GalleryImages() {
    return $this->obj('GalleryImagesList')->getValue();
  }
  public function getCMSFields() {
    $return = parent::getCMSFields();
    $return->addFieldsToTab('Root.Gallery', [
      (new \zauberfisch\SerializedDataObject\Form\SortableUploadField(
        'GalleryImagesList',
        $this->fieldLabel('GalleryImagesList')
      ))->setItems($this->GalleryImages())
    ]);
    return $return;
  }
<% if $GalleryImages %>
  
<% end_if %>
mysql> SELECT GalleryImagesList FROM Page
  WHERE GalleryImagesList IS NOT NULL LIMIT 1;
+------------------------------------------------------------+
| GalleryImagesList                                          |
+------------------------------------------------------------+
| C:41:"zauberfisch\SerializedDataObject\DataList":76:{a:2:{ |
|   i:0;a:2:{i:0;s:5:"Image";i:1;i:14;}                      |
|   i:1;a:2:{i:0;s:5:"Image";i:1;i:16;}}}                    |
+------------------------------------------------------------+

Example: ArrayListField

class Page extends SiteTree {
  private static $db = [
    'DownloadsList' => \zauberfisch\SerializedDataObject\DBField\ArrayListField::class,
  ];
  public function Downloads() {
    return $this->obj('DownloadsList')->getValue();
  }
  public function getCMSFields() {
    $return = parent::getCMSFields();
    $return->addFieldsToTab('Root.Downloads', [
      (new \zauberfisch\SerializedDataObject\Form\ArrayListField((
        'DownloadsList',
        $this->fieldLabel('DownloadsList'),
        DownloadItem::class
      )))
    ]);
    return $return;
  }

Example: ArrayListField - DownloadItem

class DownloadItem extends \zauberfisch\SerializedDataObject\AbstractDataObject {
	private static $fields = [
		'Title',
		'FileID',
	];
	public function getCMSFields() {
		$fields = new \FieldList();
		$fields->push(new \TextField('Title', $this->fieldLabel('Title')));
		$fields->push((new UploadField('FileID', $this->fieldLabel('FileID')))->setAllowedMaxFileNumber(1));
		return $fields;
	}
	public function File() {
		$arr = $this->getFileID();
		if (isset($arr['Files']) && is_array($arr['Files'])) {
			return \File::get()->byID((int)current($arr['Files']));
		}
		return null;
	}
}
<% if $Downloads %>
  
    <% loop $Downloads %>
  • $Title
  • <% end_loop %>
<% end_if %>
mysql> SELECT DownloadsList FROM DownloadsList
  WHERE DownloadsList IS NOT NULL LIMIT 1;
+------------------------------------------------------------------+
| DownloadsList                                                    |
+------------------------------------------------------------------+
| C:42:"zauberfisch\SerializedDataObject\ArrayList":446:{a:2:{i:0; |
|   C:12:"DownloadItem":160:{a:2:{s:10:"fieldsData";a:2:{          |
|     s:5:"Title";s:6:"Test 1";                                    |
|     s:6:"FileID";a:2:{s:5:"Files";a:1:{i:0;s:4:"2955";}[...]     |
| [...]                                                            |
| [...]                                                            |
| [...]                                                            |
+------------------------------------------------------------------+