Merge pull request #5239 from open-sausages/pulls/4.0/form-schema-fixes

Form schema fixes
This commit is contained in:
Damian Mooyman 2016-03-31 09:49:55 +13:00
commit 0fa7271ec2
21 changed files with 280 additions and 194 deletions

View File

@ -231,13 +231,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
$validHeaderValues = ['schema', 'state']; $validHeaderValues = ['schema', 'state'];
return in_array(trim($value), $validHeaderValues); return in_array(trim($value), $validHeaderValues);
}); });
} } else {
$schemaParts = ['schema'];
if (!count($schemaParts)) {
throw new SS_HTTPResponse_Exception(
'Invalid request. Check you\'ve set a "X-Formschema-Request" header with "schema" or "state" values.',
400
);
} }
$return = ['id' => $form->getName()]; $return = ['id' => $form->getName()];

View File

@ -7,6 +7,8 @@
*/ */
class CheckboxField extends FormField { class CheckboxField extends FormField {
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_SINGLESELECT;
public function setValue($value) { public function setValue($value) {
$this->value = ($value) ? 1 : 0; $this->value = ($value) ? 1 : 0;
return $this; return $this;

View File

@ -38,6 +38,8 @@
*/ */
class CheckboxSetField extends MultiSelectField { class CheckboxSetField extends MultiSelectField {
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_MULTISELECT;
/** /**
* @todo Explain different source data that can be used with this field, * @todo Explain different source data that can be used with this field,
* e.g. SQLMap, ArrayList or an array. * e.g. SQLMap, ArrayList or an array.

View File

@ -43,6 +43,8 @@ class CompositeField extends FormField {
*/ */
protected $legend; protected $legend;
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_STRUCTURAL;
public function __construct($children = null) { public function __construct($children = null) {
if($children instanceof FieldList) { if($children instanceof FieldList) {
$this->children = $children; $this->children = $children;
@ -394,4 +396,3 @@ class CompositeField extends FormField {
} }
} }

View File

@ -75,6 +75,8 @@ class ConfirmedPasswordField extends FormField {
*/ */
public $children; public $children;
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_STRUCTURAL;
/** /**
* @param string $name * @param string $name
* @param string $title * @param string $title

View File

@ -28,6 +28,8 @@ class CountryDropdownField extends DropdownField {
protected $extraClasses = array('dropdown'); protected $extraClasses = array('dropdown');
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_SINGLESELECT;
/** /**
* Get the locale of the Member, or if we're not logged in or don't have a locale, use the default one * Get the locale of the Member, or if we're not logged in or don't have a locale, use the default one
* @return string * @return string

View File

@ -112,4 +112,3 @@ class CurrencyField_Disabled extends CurrencyField{
. " name=\"".$this->name."\" value=\"".$valforInput."\" />"; . " name=\"".$this->name."\" value=\"".$valforInput."\" />";
} }
} }

View File

@ -60,6 +60,8 @@ require_once 'Zend/Date.php';
*/ */
class DateField extends TextField { class DateField extends TextField {
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATE;
/** /**
* @config * @config
* @var array * @var array
@ -677,4 +679,3 @@ class DateField_View_JQuery extends Object {
return preg_replace($patterns, $replacements, $format); return preg_replace($patterns, $replacements, $format);
} }
} }

View File

@ -44,6 +44,8 @@ class DatetimeField extends FormField {
*/ */
protected $timeField = null; protected $timeField = null;
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATETIME;
/** /**
* @config * @config
* @var array * @var array

View File

@ -20,12 +20,56 @@
* For example, data might be saved to the filesystem instead of the data record, or saved to a * For example, data might be saved to the filesystem instead of the data record, or saved to a
* component of the data record instead of the data record itself. * component of the data record instead of the data record itself.
* *
* A form field can be represented as structured data through {@link FormSchema},
* including both structure (name, id, attributes, etc.) and state (field value).
* Can be used by for JSON data which is consumed by a front-end application.
*
* @package forms * @package forms
* @subpackage core * @subpackage core
*/ */
class FormField extends RequestHandler { class FormField extends RequestHandler {
use SilverStripe\Forms\Schema\FormFieldSchemaTrait; /** @see $schemaDataType */
const SCHEMA_DATA_TYPE_STRING = 'String';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_HIDDEN = 'Hidden';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_TEXT = 'Text';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_HTML = 'HTML';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_INTEGER = 'Integer';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_DECIMAL = 'Decimal';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_MULTISELECT = 'MultiSelect';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_SINGLESELECT = 'SingleSelect';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_DATE = 'Date';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_DATETIME = 'DateTime';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_TIME = 'Time';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_BOOLEAN = 'Boolean';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_CUSTOM = 'Custom';
/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_STRUCTURAL = 'Structural';
/** /**
* @var Form * @var Form
@ -166,6 +210,59 @@ class FormField extends RequestHandler {
*/ */
protected $attributes = []; protected $attributes = [];
/**
* The data type backing the field. Represents the type of value the
* form expects to receive via a postback. Should be set in subclasses.
*
* The values allowed in this list include:
*
* - String: Single line text
* - Hidden: Hidden field which is posted back without modification
* - Text: Multi line text
* - HTML: Rich html text
* - Integer: Whole number value
* - Decimal: Decimal value
* - MultiSelect: Select many from source
* - SingleSelect: Select one from source
* - Date: Date only
* - DateTime: Date and time
* - Time: Time only
* - Boolean: Yes or no
* - Custom: Custom type declared by the front-end component. For fields with this type,
* the component property is mandatory, and will determine the posted value for this field.
* - Structural: Represents a field that is NOT posted back. This may contain other fields,
* or simply be a block of stand-alone content. As with 'Custom',
* the component property is mandatory if this is assigned.
*
* Each value has an equivalent constant, e.g. {@link self::SCHEMA_DATA_TYPE_STRING}.
*
* @var string
*/
protected $schemaDataType;
/**
* The type of front-end component to render the FormField as.
*
* @var string
*/
protected $schemaComponent;
/**
* Structured schema data representing the FormField.
* Used to render the FormField as a ReactJS Component on the front-end.
*
* @var array
*/
protected $schemaData = [];
/**
* Structured schema state representing the FormField's current data and validation.
* Used to render the FormField as a ReactJS Component on the front-end.
*
* @var array
*/
protected $schemaState = [];
/** /**
* Takes a field name and converts camelcase to spaced words. Also resolves combined field * Takes a field name and converts camelcase to spaced words. Also resolves combined field
* names with dot syntax to spaced words. * names with dot syntax to spaced words.
@ -1290,4 +1387,147 @@ class FormField extends RequestHandler {
return $this->dontEscape; return $this->dontEscape;
} }
/**
* Sets the component type the FormField will be rendered as on the front-end.
*
* @param string $componentType
* @return FormField
*/
public function setSchemaComponent($componentType) {
$this->schemaComponent = $componentType;
return $this;
}
/**
* Gets the type of front-end component the FormField will be rendered as.
*
* @return string
*/
public function getSchemaComponent() {
return $this->schemaComponent;
}
/**
* Sets the schema data used for rendering the field on the front-end.
* Merges the passed array with the current `$schemaData` or {@link getSchemaDataDefaults()}.
* 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.
* @return FormField
*
* @todo Add deep merging of arrays like `data` and `attributes`.
*/
public function setSchemaData($schemaData = []) {
$current = $this->getSchemaData();
$this->schemaData = array_merge($current, array_intersect_key($schemaData, $current));
return $this;
}
/**
* Gets the schema data used to render the FormField on the front-end.
*
* @return array
*/
public function getSchemaData() {
return array_merge($this->getSchemaDataDefaults(), $this->schemaData);
}
/**
* @todo Throw exception if value is missing, once a form field schema is mandatory across the CMS
*
* @return string
*/
public function getSchemaDataType() {
return $this->schemaDataType;
}
/**
* Gets the defaults for $schemaData.
* The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaData()} are ignored.
* Instead the `data` array should be used to pass around ad hoc data.
*
* @return array
*/
public function getSchemaDataDefaults() {
return [
'name' => $this->getName(),
'id' => $this->ID(),
'type' => $this->getSchemaDataType(),
'component' => $this->getSchemaComponent(),
'holder_id' => null,
'title' => $this->Title(),
'source' => null,
'extraClass' => $this->ExtraClass(),
'description' => $this->getDescription(),
'rightTitle' => $this->RightTitle(),
'leftTitle' => $this->LeftTitle(),
'readOnly' => $this->isReadOnly(),
'disabled' => $this->isDisabled(),
'customValidationMessage' => $this->getCustomValidationMessage(),
'attributes' => [],
'data' => [],
];
}
/**
* Sets the schema data used for rendering the field on the front-end.
* Merges the passed array with the current `$schemaData` or {@link getSchemaDataDefaults()}.
* 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.
* @return FormField
*
* @todo Add deep merging of arrays like `data` and `attributes`.
*/
public function setSchemaState($schemaState = []) {
$current = $this->getSchemaState();
$this->schemaState = array_merge($current, array_intersect_key($schemaState, $current));
return $this;
}
/**
* Gets the schema state used to render the FormField on the front-end.
*
* @return array
*/
public function getSchemaState() {
return array_merge($this->getSchemaStateDefaults(), $this->schemaState);
}
/**
* Gets the defaults for $schemaState.
* The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaState()} are ignored.
* Instead the `data` array should be used to pass around ad hoc data.
* Includes validation data if the field is associated to a {@link Form},
* and {@link Form->validate()} has been called.
*
* @return array
*/
public function getSchemaStateDefaults() {
$field = $this;
$form = $this->getForm();
$validator = $form ? $form->getValidator() : null;
$errors = $validator ? (array)$validator->getErrors() : [];
$messages = array_filter(array_map(function($error) use ($field) {
if($error['fieldName'] === $field->getName()) {
return [
'value' => $error['message'],
'type' => $error['messageType']
];
}
}, $errors));
return [
'id' => $this->ID(),
'value' => $this->Value(),
'valid' => (count($messages) === 0),
'messages' => (array)$messages,
'data' => [],
];
}
} }

View File

@ -1,174 +0,0 @@
<?php
namespace SilverStripe\Forms\Schema;
/**
* Class FormFieldSchemaTrait
* @package SilverStripe\Forms\Schema
*
* Allows {@link FormField} to be represented as structured data,
* including both structure (name, id, attributes, etc.) and state (field value).
* Can be used by {@link FormSchema} to represent a form in JSON,
* to be consumed by a front-end application.
*
* WARNING: Experimental API.
*/
trait FormFieldSchemaTrait {
/**
* The type of front-end component to render the FormField as.
*
* @var string
*/
protected $schemaComponent;
/**
* Structured schema data representing the FormField.
* Used to render the FormField as a ReactJS Component on the front-end.
*
* @var array
*/
protected $schemaData = [];
/**
* Structured schema state representing the FormField's current data and validation.
* Used to render the FormField as a ReactJS Component on the front-end.
*
* @var array
*/
protected $schemaState = [];
/**
* Sets the component type the FormField will be rendered as on the front-end.
*
* @param string $componentType
* @return FormField
*/
public function setSchemaComponent($componentType) {
$this->schemaComponent = $componentType;
return $this;
}
/**
* Gets the type of front-end component the FormField will be rendered as.
*
* @return string
*/
public function getSchemaComponent() {
return $this->schemaComponent;
}
/**
* Sets the schema data used for rendering the field on the front-end.
* Merges the passed array with the current `$schemaData` or {@link getSchemaDataDefaults()}.
* 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.
* @return FormField
*
* @todo Add deep merging of arrays like `data` and `attributes`.
*/
public function setSchemaData($schemaData = []) {
$current = $this->getSchemaData();
$this->schemaData = array_merge($current, array_intersect_key($schemaData, $current));
return $this;
}
/**
* Gets the schema data used to render the FormField on the front-end.
*
* @return array
*/
public function getSchemaData() {
return array_merge($this->getSchemaDataDefaults(), $this->schemaData);
}
/**
* Gets the defaults for $schemaData.
* The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaData()} are ignored.
* Instead the `data` array should be used to pass around ad hoc data.
*
* @return array
*/
public function getSchemaDataDefaults() {
return [
'type' => $this->class,
'component' => $this->getSchemaComponent(),
'id' => $this->ID,
'holder_id' => null,
'name' => $this->getName(),
'title' => $this->Title(),
'source' => null,
'extraClass' => $this->ExtraClass(),
'description' => $this->getDescription(),
'rightTitle' => $this->RightTitle(),
'leftTitle' => $this->LeftTitle(),
'readOnly' => $this->isReadOnly(),
'disabled' => $this->isDisabled(),
'customValidationMessage' => $this->getCustomValidationMessage(),
'attributes' => [],
'data' => [],
];
}
/**
* Sets the schema data used for rendering the field on the front-end.
* Merges the passed array with the current `$schemaData` or {@link getSchemaDataDefaults()}.
* 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.
* @return FormField
*
* @todo Add deep merging of arrays like `data` and `attributes`.
*/
public function setSchemaState($schemaState = []) {
$current = $this->getSchemaState();
$this->schemaState = array_merge($current, array_intersect_key($schemaState, $current));
return $this;
}
/**
* Gets the schema state used to render the FormField on the front-end.
*
* @return array
*/
public function getSchemaState() {
return array_merge($this->getSchemaStateDefaults(), $this->schemaState);
}
/**
* Gets the defaults for $schemaState.
* The keys defined here are immutable, meaning undefined keys passed to {@link setSchemaState()} are ignored.
* Instead the `data` array should be used to pass around ad hoc data.
* Includes validation data if the field is associated to a {@link Form},
* and {@link Form->validate()} has been called.
*
* @return array
*/
public function getSchemaStateDefaults() {
$field = $this;
$form = $this->getForm();
$validator = $form ? $form->getValidator() : null;
$errors = $validator ? (array)$validator->getErrors() : [];
$messages = array_filter(array_map(function($error) use ($field) {
if($error['fieldName'] === $field->getName()) {
return [
'value' => $error['message'],
'type' => $error['messageType']
];
}
}, $errors));
return [
'id' => $this->ID(),
'value' => $this->Value(),
'valid' => (count($messages) === 0),
'messages' => (array)$messages,
'data' => [],
];
}
}

View File

@ -41,11 +41,10 @@ class FormSchema {
$schema['actions'][] = $action->getSchemaData(); $schema['actions'][] = $action->getSchemaData();
} }
foreach ($form->Fields() as $fieldList) { // TODO Implemented nested fields and use Fields() instead
foreach ($fieldList->getForm()->fields()->dataFields() as $field) { foreach ($form->Fields()->dataFields() as $field) {
$schema['fields'][] = $field->getSchemaData(); $schema['fields'][] = $field->getSchemaData();
} }
}
return $schema; return $schema;
} }

View File

@ -7,6 +7,9 @@
* @subpackage fields-dataless * @subpackage fields-dataless
*/ */
class HiddenField extends FormField { class HiddenField extends FormField {
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_HIDDEN;
/** /**
* @param array $properties * @param array $properties
* *

View File

@ -15,6 +15,8 @@ use SilverStripe\Model\FieldType\DBMoney;
*/ */
class MoneyField extends FormField { class MoneyField extends FormField {
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TEXT;
/** /**
* @var string $_locale * @var string $_locale
*/ */

View File

@ -15,6 +15,8 @@ abstract class MultiSelectField extends SelectField {
*/ */
protected $defaultItems = array(); protected $defaultItems = array();
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_MULTISELECT;
/** /**
* Extracts the value of this field, normalised as an array. * Extracts the value of this field, normalised as an array.
* Scalar values will return a single length array, even if empty * Scalar values will return a single length array, even if empty

View File

@ -9,6 +9,9 @@
* @subpackage fields-formattedinput * @subpackage fields-formattedinput
*/ */
class NumericField extends TextField { class NumericField extends TextField {
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DECIMAL;
/** /**
* Override locale for this field. * Override locale for this field.
* *

View File

@ -23,6 +23,8 @@ abstract class SingleSelectField extends SelectField {
*/ */
protected $emptyString = ''; protected $emptyString = '';
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_SINGLESELECT;
/** /**
* @param boolean $bool * @param boolean $bool
* @return self Self reference * @return self Self reference

View File

@ -82,4 +82,3 @@ class Tab extends CompositeField {
); );
} }
} }

View File

@ -12,6 +12,8 @@ class TextField extends FormField {
*/ */
protected $maxLength; protected $maxLength;
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TEXT;
/** /**
* Returns an input field. * Returns an input field.
* *

View File

@ -50,6 +50,8 @@ class TimeField extends TextField {
*/ */
protected $valueObj = null; protected $valueObj = null;
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TIME;
public function __construct($name, $title = null, $value = ""){ public function __construct($name, $title = null, $value = ""){
if(!$this->locale) { if(!$this->locale) {
$this->locale = i18n::get_locale(); $this->locale = i18n::get_locale();

View File

@ -24,11 +24,11 @@ class FormSchemaTest extends SapphireTest {
'data' => [], 'data' => [],
'fields' => [ 'fields' => [
[ [
'type' => "HiddenField", 'id' => 'Form_TestForm_SecurityID',
'component' => null,
'id' => null,
'holder_id' => null,
'name' => 'SecurityID', 'name' => 'SecurityID',
'type' => "Hidden",
'component' => null,
'holder_id' => null,
'title' => 'Security ID', 'title' => 'Security ID',
'source' => null, 'source' => null,
'extraClass' => 'hidden', 'extraClass' => 'hidden',