diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 83f417bef..3016a73e0 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -231,13 +231,8 @@ class LeftAndMain extends Controller implements PermissionProvider { $validHeaderValues = ['schema', 'state']; return in_array(trim($value), $validHeaderValues); }); - } - - 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 - ); + } else { + $schemaParts = ['schema']; } $return = ['id' => $form->getName()]; diff --git a/forms/CheckboxField.php b/forms/CheckboxField.php index e88f7ca3c..b2a2895ac 100644 --- a/forms/CheckboxField.php +++ b/forms/CheckboxField.php @@ -7,6 +7,8 @@ */ class CheckboxField extends FormField { + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_SINGLESELECT; + public function setValue($value) { $this->value = ($value) ? 1 : 0; return $this; diff --git a/forms/CheckboxSetField.php b/forms/CheckboxSetField.php index 6379c749d..898ffcd9c 100644 --- a/forms/CheckboxSetField.php +++ b/forms/CheckboxSetField.php @@ -38,6 +38,8 @@ */ class CheckboxSetField extends MultiSelectField { + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_MULTISELECT; + /** * @todo Explain different source data that can be used with this field, * e.g. SQLMap, ArrayList or an array. diff --git a/forms/CompositeField.php b/forms/CompositeField.php index f137b25e1..7aa7b3ac2 100644 --- a/forms/CompositeField.php +++ b/forms/CompositeField.php @@ -43,6 +43,8 @@ class CompositeField extends FormField { */ protected $legend; + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_STRUCTURAL; + public function __construct($children = null) { if($children instanceof FieldList) { $this->children = $children; @@ -394,4 +396,3 @@ class CompositeField extends FormField { } } - diff --git a/forms/ConfirmedPasswordField.php b/forms/ConfirmedPasswordField.php index ce426708d..e5e32fc47 100644 --- a/forms/ConfirmedPasswordField.php +++ b/forms/ConfirmedPasswordField.php @@ -75,6 +75,8 @@ class ConfirmedPasswordField extends FormField { */ public $children; + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_STRUCTURAL; + /** * @param string $name * @param string $title diff --git a/forms/CountryDropdownField.php b/forms/CountryDropdownField.php index e68dd063d..b8ca28af3 100644 --- a/forms/CountryDropdownField.php +++ b/forms/CountryDropdownField.php @@ -28,6 +28,8 @@ class CountryDropdownField extends DropdownField { 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 * @return string diff --git a/forms/CurrencyField.php b/forms/CurrencyField.php index 96a8842be..baef6c8bd 100644 --- a/forms/CurrencyField.php +++ b/forms/CurrencyField.php @@ -112,4 +112,3 @@ class CurrencyField_Disabled extends CurrencyField{ . " name=\"".$this->name."\" value=\"".$valforInput."\" />"; } } - diff --git a/forms/DateField.php b/forms/DateField.php index 90065b7d7..ab6874581 100644 --- a/forms/DateField.php +++ b/forms/DateField.php @@ -60,6 +60,8 @@ require_once 'Zend/Date.php'; */ class DateField extends TextField { + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATE; + /** * @config * @var array @@ -677,4 +679,3 @@ class DateField_View_JQuery extends Object { return preg_replace($patterns, $replacements, $format); } } - diff --git a/forms/DatetimeField.php b/forms/DatetimeField.php index e57847ef7..e34ac4f6a 100644 --- a/forms/DatetimeField.php +++ b/forms/DatetimeField.php @@ -44,6 +44,8 @@ class DatetimeField extends FormField { */ protected $timeField = null; + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATETIME; + /** * @config * @var array diff --git a/forms/FormField.php b/forms/FormField.php index e324ff78a..982274537 100644 --- a/forms/FormField.php +++ b/forms/FormField.php @@ -20,12 +20,56 @@ * 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. * + * 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 * @subpackage core */ 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 @@ -166,6 +210,59 @@ class FormField extends RequestHandler { */ 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 * names with dot syntax to spaced words. @@ -1290,4 +1387,147 @@ class FormField extends RequestHandler { 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' => [], + ]; + } + } diff --git a/forms/FormFieldSchemaTrait.php b/forms/FormFieldSchemaTrait.php deleted file mode 100644 index f4b3e09d9..000000000 --- a/forms/FormFieldSchemaTrait.php +++ /dev/null @@ -1,174 +0,0 @@ -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' => [], - ]; - } -} diff --git a/forms/FormSchema.php b/forms/FormSchema.php index 9f2c00dcf..06c91f207 100644 --- a/forms/FormSchema.php +++ b/forms/FormSchema.php @@ -41,10 +41,9 @@ class FormSchema { $schema['actions'][] = $action->getSchemaData(); } - foreach ($form->Fields() as $fieldList) { - foreach ($fieldList->getForm()->fields()->dataFields() as $field) { - $schema['fields'][] = $field->getSchemaData(); - } + // TODO Implemented nested fields and use Fields() instead + foreach ($form->Fields()->dataFields() as $field) { + $schema['fields'][] = $field->getSchemaData(); } return $schema; diff --git a/forms/HiddenField.php b/forms/HiddenField.php index 052d97d5d..d03f76b18 100644 --- a/forms/HiddenField.php +++ b/forms/HiddenField.php @@ -7,6 +7,9 @@ * @subpackage fields-dataless */ class HiddenField extends FormField { + + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_HIDDEN; + /** * @param array $properties * diff --git a/forms/MoneyField.php b/forms/MoneyField.php index d8c4f15fa..5d5161a88 100644 --- a/forms/MoneyField.php +++ b/forms/MoneyField.php @@ -15,6 +15,8 @@ use SilverStripe\Model\FieldType\DBMoney; */ class MoneyField extends FormField { + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TEXT; + /** * @var string $_locale */ diff --git a/forms/MultiSelectField.php b/forms/MultiSelectField.php index be1354088..15ba9f16d 100644 --- a/forms/MultiSelectField.php +++ b/forms/MultiSelectField.php @@ -15,6 +15,8 @@ abstract class MultiSelectField extends SelectField { */ protected $defaultItems = array(); + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_MULTISELECT; + /** * Extracts the value of this field, normalised as an array. * Scalar values will return a single length array, even if empty diff --git a/forms/NumericField.php b/forms/NumericField.php index 6a40c2778..39616cb4c 100644 --- a/forms/NumericField.php +++ b/forms/NumericField.php @@ -9,6 +9,9 @@ * @subpackage fields-formattedinput */ class NumericField extends TextField { + + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DECIMAL; + /** * Override locale for this field. * diff --git a/forms/SingleSelectField.php b/forms/SingleSelectField.php index 186e006bf..567688fda 100644 --- a/forms/SingleSelectField.php +++ b/forms/SingleSelectField.php @@ -23,6 +23,8 @@ abstract class SingleSelectField extends SelectField { */ protected $emptyString = ''; + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_SINGLESELECT; + /** * @param boolean $bool * @return self Self reference diff --git a/forms/Tab.php b/forms/Tab.php index 3773528d2..14d53b5e1 100644 --- a/forms/Tab.php +++ b/forms/Tab.php @@ -82,4 +82,3 @@ class Tab extends CompositeField { ); } } - diff --git a/forms/TextField.php b/forms/TextField.php index f374500f8..e14ef47ef 100644 --- a/forms/TextField.php +++ b/forms/TextField.php @@ -12,6 +12,8 @@ class TextField extends FormField { */ protected $maxLength; + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TEXT; + /** * Returns an input field. * diff --git a/forms/TimeField.php b/forms/TimeField.php index 0dc023274..6dc7c754e 100644 --- a/forms/TimeField.php +++ b/forms/TimeField.php @@ -50,6 +50,8 @@ class TimeField extends TextField { */ protected $valueObj = null; + protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TIME; + public function __construct($name, $title = null, $value = ""){ if(!$this->locale) { $this->locale = i18n::get_locale(); diff --git a/tests/forms/FormSchemaTest.php b/tests/forms/FormSchemaTest.php index 4cf1d5564..8b586efe6 100644 --- a/tests/forms/FormSchemaTest.php +++ b/tests/forms/FormSchemaTest.php @@ -24,11 +24,11 @@ class FormSchemaTest extends SapphireTest { 'data' => [], 'fields' => [ [ - 'type' => "HiddenField", - 'component' => null, - 'id' => null, - 'holder_id' => null, + 'id' => 'Form_TestForm_SecurityID', 'name' => 'SecurityID', + 'type' => "Hidden", + 'component' => null, + 'holder_id' => null, 'title' => 'Security ID', 'source' => null, 'extraClass' => 'hidden',