Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Form schema fixes #5239

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions admin/code/LeftAndMain.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,8 @@ protected function getSchemaForForm(Form $form) {
$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()];
Expand Down
2 changes: 2 additions & 0 deletions forms/CheckboxField.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions forms/CheckboxSetField.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion forms/CompositeField.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -394,4 +396,3 @@ public function validate($validator) {
}

}

2 changes: 2 additions & 0 deletions forms/ConfirmedPasswordField.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class ConfirmedPasswordField extends FormField {
*/
public $children;

protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_STRUCTURAL;

/**
* @param string $name
* @param string $title
Expand Down
2 changes: 2 additions & 0 deletions forms/CountryDropdownField.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion forms/CurrencyField.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,3 @@ public function Field($properties = array()) {
. " name=\"".$this->name."\" value=\"".$valforInput."\" />";
}
}

3 changes: 2 additions & 1 deletion forms/DateField.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
*/
class DateField extends TextField {

protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATE;

/**
* @config
* @var array
Expand Down Expand Up @@ -677,4 +679,3 @@ public static function convert_iso_to_jquery_format($format) {
return preg_replace($patterns, $replacements, $format);
}
}

2 changes: 2 additions & 0 deletions forms/DatetimeField.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class DatetimeField extends FormField {
*/
protected $timeField = null;

protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATETIME;

/**
* @config
* @var array
Expand Down
242 changes: 241 additions & 1 deletion forms/FormField.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so this comment is about this entire commit but I have to say that this is quite surprising. I don't see the fact that the trait is used in 1 place as a reason not to break the code into smaller pieces.

Perhaps the trait should mark abstract function getSchemaDataType(), and the implementation of only that method is pulled into FormField?

*/
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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1290,4 +1387,147 @@ public function getDontEscape() {
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' => [],
];
}

}
Loading