API Support composite react formfields

Cleanup and standardise CompositeField API and subclasses
This commit is contained in:
Damian Mooyman 2016-07-06 16:34:09 +12:00
parent d19955afc8
commit 9e1b12a891
10 changed files with 322 additions and 184 deletions

View File

@ -57,10 +57,24 @@ class CompositeField extends FormField {
}
$this->children->setContainerField($this);
// Skipping FormField::__construct(), but we have to make sure this
// doesn't count as a broken constructor
$this->brokenOnConstruct = false;
Object::__construct();
parent::__construct(null, false);
}
/**
* Merge child field data into this form
*/
public function getSchemaDataDefaults() {
$defaults = parent::getSchemaDataDefaults();
$children = $this->getChildren();
if($children && $children->count()) {
$childSchema = [];
/** @var FormField $child */
foreach($children as $child) {
$childSchema[] = $child->getSchemaData();
}
$defaults['children'] = $childSchema;
}
return $defaults;
}
/**
@ -72,11 +86,6 @@ class CompositeField extends FormField {
return $this->children;
}
public function setID($id) {
$this->id = $id;
return $this;
}
/**
* Accessor method for $this->children
*
@ -88,6 +97,7 @@ class CompositeField extends FormField {
/**
* @param FieldList $children
* @return $this
*/
public function setChildren($children) {
$this->children = $children;
@ -95,7 +105,8 @@ class CompositeField extends FormField {
}
/**
* @param string
* @param string $tag
* @return $this
*/
public function setTag($tag) {
$this->tag = $tag;
@ -111,7 +122,8 @@ class CompositeField extends FormField {
}
/**
* @param string
* @param string $legend
* @return $this
*/
public function setLegend($legend) {
$this->legend = $legend;
@ -147,7 +159,6 @@ class CompositeField extends FormField {
'tabindex' => null,
'type' => null,
'value' => null,
'type' => null,
'title' => ($this->tag == 'fieldset') ? null : $this->legend
)
);
@ -158,38 +169,46 @@ class CompositeField extends FormField {
* list.
*
* Sequentialisation is used when connecting the form to its data source
*
* @param array $list
* @param bool $saveableOnly
*/
public function collateDataFields(&$list, $saveableOnly = false) {
foreach($this->children as $field) {
if(is_object($field)) {
if($field->isComposite()) $field->collateDataFields($list, $saveableOnly);
if($saveableOnly) {
$isIncluded = ($field->hasData() && !$field->isReadonly() && !$field->isDisabled());
} else {
$isIncluded = ($field->hasData());
}
if($isIncluded) {
$name = $field->getName();
if($name) {
$formName = (isset($this->form)) ? $this->form->FormName() : '(unknown form)';
if(isset($list[$name])) {
user_error("collateDataFields() I noticed that a field called '$name' appears twice in"
. " your form: '{$formName}'. One is a '{$field->class}' and the other is a"
. " '{$list[$name]->class}'", E_USER_ERROR);
}
$list[$name] = $field;
if(! $field instanceof FormField) {
continue;
}
if($field instanceof CompositeField) {
$field->collateDataFields($list, $saveableOnly);
}
if($saveableOnly) {
$isIncluded = ($field->hasData() && !$field->isReadonly() && !$field->isDisabled());
} else {
$isIncluded = ($field->hasData());
}
if($isIncluded) {
$name = $field->getName();
if($name) {
$formName = (isset($this->form)) ? $this->form->FormName() : '(unknown form)';
if(isset($list[$name])) {
user_error("collateDataFields() I noticed that a field called '$name' appears twice in"
. " your form: '{$formName}'. One is a '{$field->class}' and the other is a"
. " '{$list[$name]->class}'", E_USER_ERROR);
}
$list[$name] = $field;
}
}
}
}
public function setForm($form) {
foreach($this->children as $f)
if(is_object($f)) $f->setForm($form);
foreach($this->children as $field) {
if ($field instanceof FormField) {
$field->setForm($form);
}
}
parent::setForm($form);
return $this;
}
@ -234,20 +253,23 @@ class CompositeField extends FormField {
/**
* @uses FieldList->insertBefore()
*
* @param string $insertBefore
* @param FormField $field
* @return false|FormField
*/
public function insertBefore($insertBefore, $field) {
$ret = $this->children->insertBefore($insertBefore, $field);
$this->sequentialSet = null;
return $ret;
return $this->children->insertBefore($insertBefore, $field);
}
/**
* @uses FieldList->insertAfter()
* @param string $insertAfter
* @param FormField $field
* @return false|FormField
*/
public function insertAfter($insertAfter, $field) {
$ret = $this->children->insertAfter($insertAfter, $field);
$this->sequentialSet = null;
return $ret;
return $this->children->insertAfter($insertAfter, $field);
}
/**
@ -281,9 +303,10 @@ class CompositeField extends FormField {
public function performReadonlyTransformation() {
$newChildren = new FieldList();
$clone = clone $this;
if($clone->getChildren()) foreach($clone->getChildren() as $idx => $child) {
if(is_object($child)) $child = $child->transform(new ReadonlyTransformation());
$newChildren->push($child, $idx);
if($clone->getChildren()) foreach($clone->getChildren() as $child) {
/** @var FormField $child */
$child = $child->transform(new ReadonlyTransformation());
$newChildren->push($child);
}
$clone->children = $newChildren;
@ -303,9 +326,10 @@ class CompositeField extends FormField {
public function performDisabledTransformation() {
$newChildren = new FieldList();
$clone = clone $this;
if($clone->getChildren()) foreach($clone->getChildren() as $idx => $child) {
if(is_object($child)) $child = $child->transform(new DisabledTransformation());
$newChildren->push($child, $idx);
if($clone->getChildren()) foreach($clone->getChildren() as $child) {
/** @var FormField $child */
$child = $child->transform(new DisabledTransformation());
$newChildren->push($child);
}
$clone->children = $newChildren;
@ -332,12 +356,19 @@ class CompositeField extends FormField {
* be found.
*/
public function fieldPosition($field) {
if(is_string($field)) $field = $this->fieldByName($field);
if(!$field) return false;
if(is_string($field)) {
$field = $this->fieldByName($field);
}
if(!$field) {
return false;
}
$i = 0;
foreach($this->children as $child) {
if($child->getName() == $field->getName()) return $i;
/** @var FormField $child */
if($child->getName() == $field->getName()) {
return $i;
}
$i++;
}
@ -348,25 +379,23 @@ class CompositeField extends FormField {
* Transform the named field into a readonly feld.
*
* @param string|FormField
* @return bool
*/
public function makeFieldReadonly($field) {
$fieldName = ($field instanceof FormField) ? $field->getName() : $field;
// Iterate on items, looking for the applicable field
foreach($this->children as $i => $item) {
if($item->isComposite()) {
$item->makeFieldReadonly($fieldName);
} else {
// Once it's found, use FormField::transform to turn the field into a readonly version of itself.
if($item->getName() == $fieldName) {
$this->children->replaceField($fieldName, $item->transform(new ReadonlyTransformation()));
// Clear an internal cache
$this->sequentialSet = null;
// A true results indicates that the field was found
if($item instanceof CompositeField) {
if($item->makeFieldReadonly($fieldName)) {
return true;
}
};
} elseif($item instanceof FormField && $item->getName() == $fieldName) {
// Once it's found, use FormField::transform to turn the field into a readonly version of itself.
$this->children->replaceField($fieldName, $item->transform(new ReadonlyTransformation()));
// A true results indicates that the field was found
return true;
}
}
return false;
@ -389,7 +418,8 @@ class CompositeField extends FormField {
*/
public function validate($validator) {
$valid = true;
foreach($this->children as $idx => $child){
foreach($this->children as $child){
/** @var FormField $child */
$valid = ($child && $child->validate($validator) && $valid);
}
return $valid;

View File

@ -59,20 +59,42 @@ class FieldGroup extends CompositeField {
protected $zebra;
public function __construct($arg1 = null, $arg2 = null) {
if(is_array($arg1) || is_a($arg1, 'FieldSet')) {
$fields = $arg1;
/**
* Create a new field group.
*
* Accepts any number of arguments.
*
* @param mixed $titleOrField Either the field title, list of fields, or first field
* @param mixed ...$otherFields Subsequent fields or field list (if passing in title to $titleOrField)
*/
public function __construct($titleOrField = null, $otherFields = null) {
$title = null;
if(is_array($titleOrField) || $titleOrField instanceof FieldList) {
$fields = $titleOrField;
} else if(is_array($arg2) || is_a($arg2, 'FieldList')) {
$this->title = $arg1;
$fields = $arg2;
// This would be discarded otherwise
if($otherFields) {
throw new InvalidArgumentException(
'$otherFields is not accepted if passing in field list to $titleOrField'
);
}
} else if(is_array($otherFields) || $otherFields instanceof FieldList) {
$title = $titleOrField;
$fields = $otherFields;
} else {
$fields = func_get_args();
if(!is_object(reset($fields))) $this->title = array_shift($fields);
if(!is_object(reset($fields))) {
$title = array_shift($fields);
}
}
parent::__construct($fields);
if($title) {
$this->setTitle($title);
}
}
/**
@ -80,12 +102,13 @@ class FieldGroup extends CompositeField {
* In some cases the FieldGroup doesn't have a title, but we still want
* the ID / name to be set. This code, generates the ID from the nested children
*/
public function Name(){
public function getName(){
if(!$this->title) {
$fs = $this->FieldList();
$compositeTitle = '';
$count = 0;
foreach($fs as $subfield){
/** @var FormField $subfield */
$compositeTitle .= $subfield->getName();
if($subfield->getName()) $count++;
}
@ -101,6 +124,7 @@ class FieldGroup extends CompositeField {
* Set an odd/even class
*
* @param string $zebra one of odd or even.
* @return $this
*/
public function setZebra($zebra) {
if($zebra == 'odd' || $zebra == 'even') $this->zebra = $zebra;
@ -142,8 +166,4 @@ class FieldGroup extends CompositeField {
return (isset($MessageType)) ? implode(". ", $MessageType) : "";
}
public function php($data) {
return;
}
}

View File

@ -651,7 +651,6 @@ class FieldList extends ArrayList {
* @return String Rewritten path, based on {@link tabPathRewrites}
*/
protected function rewriteTabPath($name) {
$isRunningTest = (class_exists('SapphireTest', false) && SapphireTest::is_running_test());
foreach($this->getTabPathRewrites() as $regex => $replace) {
if(preg_match($regex, $name)) {
$newName = preg_replace($regex, $replace, $name);

View File

@ -648,7 +648,8 @@ class FormField extends RequestHandler {
*
* Caution: this doesn't work on all fields, see {@link setAttribute()}.
*
* @return null|string
* @param string $name
* @return string
*/
public function getAttribute($name) {
$attributes = $this->getAttributes();
@ -1239,8 +1240,7 @@ class FormField extends RequestHandler {
*
* @todo Make this abstract.
*
* @param Validator
*
* @param Validator $validator
* @return bool
*/
public function validate($validator) {
@ -1297,7 +1297,7 @@ class FormField extends RequestHandler {
* @return bool
*/
public function Required() {
if($this->form && ($validator = $this->form->Validator)) {
if($this->form && ($validator = $this->form->getValidator())) {
return $validator->fieldIsRequired($this->name);
}
@ -1451,14 +1451,14 @@ class FormField extends RequestHandler {
'id' => $this->ID(),
'type' => $this->getSchemaDataType(),
'component' => $this->getSchemaComponent(),
'holder_id' => null,
'holder_id' => $this->HolderID(),
'title' => $this->Title(),
'source' => null,
'extraClass' => $this->ExtraClass(),
'extraClass' => $this->extraClass(),
'description' => $this->getDescription(),
'rightTitle' => $this->RightTitle(),
'leftTitle' => $this->LeftTitle(),
'readOnly' => $this->isReadOnly(),
'readOnly' => $this->isReadonly(),
'disabled' => $this->isDisabled(),
'customValidationMessage' => $this->getCustomValidationMessage(),
'attributes' => [],
@ -1472,7 +1472,7 @@ class FormField extends RequestHandler {
* Any passed keys that are not defined in {@link getSchemaDataDefaults()} are ignored.
* If you want to pass around ad hoc data use the `data` array e.g. pass `['data' => ['myCustomKey' => 'yolo']]`.
*
* @param array $schemaData - The data to be merged with $this->schemaData.
* @param array $schemaState The data to be merged with $this->schemaData.
* @return FormField
*
* @todo Add deep merging of arrays like `data` and `attributes`.
@ -1514,6 +1514,7 @@ class FormField extends RequestHandler {
'type' => $error['messageType']
];
}
return null;
}, $errors));
return [

View File

@ -2,8 +2,8 @@
namespace SilverStripe\Forms\Schema;
use Convert;
use Form;
use FormField;
/**
* Class FormSchema
@ -38,11 +38,12 @@ class FormSchema {
];
foreach ($form->Actions() as $action) {
/** @var FormField $action */
$schema['actions'][] = $action->getSchemaData();
}
// TODO Implemented nested fields and use Fields() instead
foreach ($form->Fields()->dataFields() as $field) {
foreach ($form->Fields() as $field) {
/** @var FormField $field */
$schema['fields'][] = $field->getSchemaData();
}

View File

@ -35,7 +35,6 @@ class PhoneNumberField extends FormField {
*/
public function Field($properties = array()) {
$fields = new FieldGroup( $this->name );
$fields->setID("{$this->name}_Holder");
list($countryCode, $areaCode, $phoneNumber, $extension) = $this->parseValue();
if ($this->value=="") {

View File

@ -38,8 +38,6 @@ class SelectionGroup extends CompositeField {
* @param mixed $value
*/
public function __construct($name, $items, $value = null) {
$this->name = $name;
if($value !== null) {
$this->setValue($value);
}
@ -62,7 +60,7 @@ class SelectionGroup extends CompositeField {
parent::__construct($selectionItems);
Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/SelectionGroup.css');
$this->setName($name);
}
public function FieldSet() {
@ -138,15 +136,18 @@ class SelectionGroup_Item extends CompositeField {
/**
* @param String $value Form field identifier
* @param FormField $field Contents of the option
* @param FormField|array $fields Contents of the option
* @param String $title Title to show for the radio button option
*/
function __construct($value, $fields = null, $title = null) {
$this->value = $value;
$this->title = ($title) ? $title : $value;
if($fields && !is_array($fields)) $fields = array($fields);
$this->setValue($value);
if($fields && !is_array($fields)) {
$fields = array($fields);
}
parent::__construct($fields);
$this->setTitle($title ?: $value);
}
function getTitle() {

View File

@ -17,55 +17,98 @@
* @subpackage fields-structural
*/
class Tab extends CompositeField {
/**
* @var TabSet
*/
protected $tabSet;
/**
* @var string
*/
protected $id;
/**
* @uses FormField::name_to_label()
*
* @param string $name Identifier of the tab, without characters like dots or spaces
* @param string $title Natural language title of the tab. If its left out,
* the class uses {@link FormField::name_to_label()} to produce a title from the {@link $name} parameter.
* @param FormField All following parameters are inserted as children to this tab
* @param string|FormField $titleOrField Natural language title of the tabset, or first tab.
* If its left out, the class uses {@link FormField::name_to_label()} to produce a title
* from the {@link $name} parameter.
* @param FormField ...$fields All following parameters are inserted as children to this tab
*/
public function __construct($name) {
$args = func_get_args();
$name = array_shift($args);
if(!is_string($name)) user_error('TabSet::__construct(): $name parameter to a valid string', E_USER_ERROR);
$this->name = $name;
$this->id = preg_replace('/[^0-9A-Za-z]+/', '', $name);
// Legacy handling: only assume second parameter as title if its a string,
// otherwise it might be a formfield instance
if(isset($args[0]) && is_string($args[0])) {
$title = array_shift($args);
public function __construct($name, $titleOrField = null, $fields = null) {
if(!is_string($name)) {
throw new InvalidArgumentException('Invalid string parameter for $name');
}
$this->title = (isset($title)) ? $title : FormField::name_to_label($name);
parent::__construct($args);
// Get following arguments
$fields = func_get_args();
array_shift($fields);
// Detect title from second argument, if it is a string
if($titleOrField && is_string($titleOrField)) {
$title = $titleOrField;
array_shift($fields);
} else {
$title = static::name_to_label($name);
}
// Remaining arguments are child fields
parent::__construct($fields);
// Assign name and title (not assigned by parent constructor)
$this->setName($name);
$this->setTitle($title);
$this->setID(Convert::raw2htmlid($name));
}
public function id() {
return ($this->tabSet) ? $this->tabSet->id() . '_' . $this->id : $this->id;
public function ID() {
if($this->tabSet) {
return $this->tabSet->ID() . '_' . $this->id;
} else {
return $this->id;
}
}
/**
* Set custom HTML ID to use for this tabset
*
* @param string $id
* @return $this
*/
public function setID($id) {
$this->id = $id;
return $this;
}
/**
* Get child fields
*
* @return FieldList
*/
public function Fields() {
return $this->children;
}
/**
* Assign to a TabSet instance
*
* @param TabSet $val
* @return $this
*/
public function setTabSet($val) {
$this->tabSet = $val;
return $this;
}
/**
* Returns the named field
* Get parent tabset
*
* @return TabSet
*/
public function fieldByName($name) {
foreach($this->children as $child) {
if($name == $child->getName()) return $child;
}
public function getTabSet() {
return $this->tabSet;
}
public function extraClass() {
@ -76,7 +119,7 @@ class Tab extends CompositeField {
return array_merge(
$this->attributes,
array(
'id' => $this->id(),
'id' => $this->ID(),
'class' => 'tab ' . $this->extraClass()
)
);

View File

@ -1,4 +1,6 @@
<?php
use SilverStripe\ORM\FieldType\DBHTMLText;
/**
* Defines a set of tabs in a form.
* The tabs are build with our standard tabstrip javascript library.
@ -28,44 +30,88 @@
class TabSet extends CompositeField {
/**
* @param string $name Identifier
* @param string $title (Optional) Natural language title of the tabset
* @param Tab|TabSet $unknown All further parameters are inserted as children into the TabSet
* @var TabSet
*/
public function __construct($name) {
$args = func_get_args();
protected $tabSet;
$name = array_shift($args);
if(!is_string($name)) user_error('TabSet::__construct(): $name parameter to a valid string', E_USER_ERROR);
$this->name = $name;
/**
* @var string
*/
protected $id;
$this->id = $name;
// Legacy handling: only assume second parameter as title if its a string,
// otherwise it might be a formfield instance
if(isset($args[0]) && is_string($args[0])) {
$title = array_shift($args);
}
$this->title = (isset($title)) ? $title : FormField::name_to_label($name);
if($args) foreach($args as $tab) {
$isValidArg = (is_object($tab) && (!($tab instanceof Tab) || !($tab instanceof TabSet)));
if(!$isValidArg) user_error('TabSet::__construct(): Parameter not a valid Tab instance', E_USER_ERROR);
$tab->setTabSet($this);
/**
* @param string $name Identifier
* @param string|Tab|TabSet $titleOrTab Natural language title of the tabset, or first tab.
* If its left out, the class uses {@link FormField::name_to_label()} to produce a title
* from the {@link $name} parameter.
* @param Tab|TabSet ...$tabs All further parameters are inserted as children into the TabSet
*/
public function __construct($name, $titleOrTab = null, $tabs = null) {
if(!is_string($name)) {
throw new InvalidArgumentException('Invalid string parameter for $name');
}
parent::__construct($args);
// Get following arguments
$tabs = func_get_args();
array_shift($tabs);
// Detect title from second argument, if it is a string
if($titleOrTab && is_string($titleOrTab)) {
$title = $titleOrTab;
array_shift($tabs);
} else {
$title = static::name_to_label($name);
}
// Normalise children list
if(count($tabs) === 1 && (is_array($tabs[0]) || $tabs[0] instanceof FieldList)) {
$tabs = $tabs[0];
}
// Ensure tabs are assigned to this tabset
if($tabs) {
foreach($tabs as $tab) {
if ($tab instanceof Tab || $tab instanceof TabSet) {
$tab->setTabSet($this);
} else {
throw new InvalidArgumentException("TabSet can only contain instances of other Tab or Tabsets");
}
}
}
parent::__construct($tabs);
// Assign name and title (not assigned by parent constructor)
$this->setName($name);
$this->setTitle($title);
$this->setID(Convert::raw2htmlid($name));
}
public function id() {
if($this->tabSet) return $this->tabSet->id() . '_' . $this->id . '_set';
else return $this->id;
public function ID() {
if($this->tabSet) {
return $this->tabSet->ID() . '_' . $this->id . '_set';
} else {
return $this->id;
}
}
/**
* Set custom HTML ID to use for this tabset
*
* @param string $id
* @return $this
*/
public function setID($id) {
$this->id = $id;
return $this;
}
/**
* Returns a tab-strip and the associated tabs.
* The HTML is a standardised format, containing a &lt;ul;
*
* @param array $properties
* @return DBHTMLText|string
*/
public function FieldHolder($properties = array()) {
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
@ -84,78 +130,73 @@ class TabSet extends CompositeField {
}
/**
* Return a dataobject set of all this classes tabs
* Return a set of all this classes tabs
*
* @return FieldList
*/
public function Tabs() {
return $this->children;
}
/**
* @param FieldList $children Assign list of tabs
*/
public function setTabs($children){
$this->children = $children;
}
/**
* Assign to a TabSet instance
*
* @param TabSet $val
* @return $this
*/
public function setTabSet($val) {
$this->tabSet = $val;
return $this;
}
/**
* Get parent tabset
*
* @return TabSet
*/
public function getTabSet() {
if(isset($this->tabSet)) return $this->tabSet;
return $this->tabSet;
}
public function getAttributes() {
return array_merge(
$this->attributes,
array(
'id' => $this->id(),
'id' => $this->ID(),
'class' => $this->extraClass()
)
);
}
/**
* Returns a named field.
*
* @param string $name Name of the field you want to find. Allows for dot notation.
* @return FormField|null
*/
public function fieldByName($name) {
if(strpos($name,'.') !== false) list($name, $remainder) = explode('.',$name,2);
else $remainder = null;
foreach($this->children as $child) {
if(trim($name) == trim($child->Name) || $name == $child->id) {
if($remainder) {
if($child->isComposite()) {
return $child->fieldByName($remainder);
} else {
user_error("Trying to get field '$remainder' from non-composite field $child->class.$name",
E_USER_WARNING);
return null;
}
} else {
return $child;
}
}
}
return null;
}
/**
* Add a new child field to the end of the set.
*
* @param FormField $field
*/
public function push(FormField $field) {
if ($field instanceof Tab || $field instanceof TabSet) {
$field->setTabSet($this);
}
parent::push($field);
$field->setTabSet($this);
}
/**
* Add a new child field to the beginning of the set.
*
* @param FormField $field
*/
public function unshift(FormField $field) {
if ($field instanceof Tab || $field instanceof TabSet) {
$field->setTabSet($this);
}
parent::unshift($field);
$field->setTabSet($this);
}
/**
@ -163,10 +204,12 @@ class TabSet extends CompositeField {
*
* @param string $insertBefore Name of the field to insert before
* @param FormField $field The form field to insert
* @return FormField|null
* @return FormField|null
*/
public function insertBefore($insertBefore, $field) {
if($field instanceof Tab) $field->setTabSet($this);
if ($field instanceof Tab || $field instanceof TabSet) {
$field->setTabSet($this);
}
return parent::insertBefore($insertBefore, $field);
}
@ -178,7 +221,9 @@ class TabSet extends CompositeField {
* @return FormField|null
*/
public function insertAfter($insertAfter, $field) {
if($field instanceof Tab) $field->setTabSet($this);
if ($field instanceof Tab || $field instanceof TabSet) {
$field->setTabSet($this);
}
return parent::insertAfter($insertAfter, $field);
}
}

View File

@ -25,10 +25,9 @@ class ToggleCompositeField extends CompositeField {
* @param array|FieldList $children
*/
public function __construct($name, $title, $children) {
$this->name = $name;
$this->title = $title;
parent::__construct($children);
$this->setName($name);
$this->setTitle($title);
}
/**