mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #5132 from open-sausages/features/form-schema
API Form schema
This commit is contained in:
commit
6d88caa9b3
@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
Name: coreconfig
|
Name: coreconfig
|
||||||
---
|
---
|
||||||
|
Injector:
|
||||||
|
FormSchema:
|
||||||
|
class: SilverStripe\Forms\Schema\FormSchema
|
||||||
Upload:
|
Upload:
|
||||||
# Replace an existing file rather than renaming the new one.
|
# Replace an existing file rather than renaming the new one.
|
||||||
replaceFile: false
|
replaceFile: false
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deals with special form handling in CMS, mainly around
|
|
||||||
* {@link PjaxResponseNegotiator}
|
|
||||||
*
|
|
||||||
* @package framework
|
|
||||||
* @subpackage admin
|
|
||||||
*/
|
|
||||||
class CMSForm extends Form {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Route validation error responses through response negotiator,
|
|
||||||
* so they return the correct markup as expected by the requesting client.
|
|
||||||
*/
|
|
||||||
protected function getValidationErrorResponse() {
|
|
||||||
$request = $this->getRequest();
|
|
||||||
$negotiator = $this->getResponseNegotiator();
|
|
||||||
|
|
||||||
if($request->isAjax() && $negotiator) {
|
|
||||||
$this->setupFormErrors();
|
|
||||||
$result = $this->forTemplate();
|
|
||||||
|
|
||||||
return $negotiator->respond($request, array(
|
|
||||||
'CurrentForm' => function() use($result) {
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
return parent::getValidationErrorResponse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the response negotiator
|
|
||||||
* @param ResponseNegotiator $negotiator The response negotiator to use
|
|
||||||
* @return Form The current form
|
|
||||||
*/
|
|
||||||
public function setResponseNegotiator($negotiator) {
|
|
||||||
$this->responseNegotiator = $negotiator;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current response negotiator
|
|
||||||
* @return ResponseNegotiator|null
|
|
||||||
*/
|
|
||||||
public function getResponseNegotiator() {
|
|
||||||
return $this->responseNegotiator;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -5,12 +5,16 @@
|
|||||||
* @subpackage admin
|
* @subpackage admin
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use SilverStripe\Forms\Schema\FormSchema;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LeftAndMain is the parent class of all the two-pane views in the CMS.
|
* LeftAndMain is the parent class of all the two-pane views in the CMS.
|
||||||
* If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain.
|
* If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain.
|
||||||
*
|
*
|
||||||
* This is essentially an abstract class which should be subclassed.
|
* This is essentially an abstract class which should be subclassed.
|
||||||
* See {@link CMSMain} for a good example.
|
* See {@link CMSMain} for a good example.
|
||||||
|
*
|
||||||
|
* @property FormSchema $schema
|
||||||
*/
|
*/
|
||||||
class LeftAndMain extends Controller implements PermissionProvider {
|
class LeftAndMain extends Controller implements PermissionProvider {
|
||||||
|
|
||||||
@ -84,7 +88,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
private static $allowed_actions = array(
|
private static $allowed_actions = [
|
||||||
'index',
|
'index',
|
||||||
'save',
|
'save',
|
||||||
'savetreenode',
|
'savetreenode',
|
||||||
@ -97,7 +101,12 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
'AddForm',
|
'AddForm',
|
||||||
'batchactions',
|
'batchactions',
|
||||||
'BatchActionsForm',
|
'BatchActionsForm',
|
||||||
);
|
'schema',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $dependencies = [
|
||||||
|
'schema' => '%$FormSchema'
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @config
|
* @config
|
||||||
@ -169,6 +178,80 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
*/
|
*/
|
||||||
protected $responseNegotiator;
|
protected $responseNegotiator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a JSON schema representing the current edit form.
|
||||||
|
*
|
||||||
|
* WARNING: Experimental API.
|
||||||
|
*
|
||||||
|
* @return SS_HTTPResponse
|
||||||
|
*/
|
||||||
|
public function schema($request) {
|
||||||
|
$response = $this->getResponse();
|
||||||
|
$formName = $request->param('ID');
|
||||||
|
|
||||||
|
if(!$this->hasMethod("get{$formName}")) {
|
||||||
|
throw new SS_HTTPResponse_Exception(
|
||||||
|
'Form not found',
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$this->hasAction($formName)) {
|
||||||
|
throw new SS_HTTPResponse_Exception(
|
||||||
|
'Form not accessible',
|
||||||
|
401
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->{"get{$formName}"}();
|
||||||
|
$response->addHeader('Content-Type', 'application/json');
|
||||||
|
$response->setBody(Convert::raw2json($this->getSchemaForForm($form)));
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a representation of the provided {@link Form} as structured data,
|
||||||
|
* based on the request data.
|
||||||
|
*
|
||||||
|
* @param Form $form
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getSchemaForForm(Form $form) {
|
||||||
|
$request = $this->getRequest();
|
||||||
|
$schemaParts = [];
|
||||||
|
$return = null;
|
||||||
|
|
||||||
|
// Valid values for the "X-Formschema-Request" header are "schema" and "state".
|
||||||
|
// If either of these values are set they will be stored in the $schemaParst array
|
||||||
|
// and used to construct the response body.
|
||||||
|
if ($schemaHeader = $request->getHeader('X-Formschema-Request')) {
|
||||||
|
$schemaParts = array_filter(explode(',', $schemaHeader), function($value) {
|
||||||
|
$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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$return = ['id' => $form->getName()];
|
||||||
|
|
||||||
|
if (in_array('schema', $schemaParts)) {
|
||||||
|
$return['schema'] = $this->schema->getSchema($form);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('state', $schemaParts)) {
|
||||||
|
$return['state'] = $this->schema->getState($form);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Member $member
|
* @param Member $member
|
||||||
* @return boolean
|
* @return boolean
|
||||||
@ -1013,6 +1096,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
* Save handler
|
* Save handler
|
||||||
*/
|
*/
|
||||||
public function save($data, $form) {
|
public function save($data, $form) {
|
||||||
|
$request = $this->getRequest();
|
||||||
$className = $this->stat('tree_class');
|
$className = $this->stat('tree_class');
|
||||||
|
|
||||||
// Existing or new record?
|
// Existing or new record?
|
||||||
@ -1033,7 +1117,16 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
$this->setCurrentPageID($record->ID);
|
$this->setCurrentPageID($record->ID);
|
||||||
|
|
||||||
$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.SAVEDUP', 'Saved.')));
|
$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.SAVEDUP', 'Saved.')));
|
||||||
return $this->getResponseNegotiator()->respond($this->getRequest());
|
|
||||||
|
if($request->getHeader('X-Formschema-Request')) {
|
||||||
|
$data = $this->getSchemaForForm($form);
|
||||||
|
$response = new SS_HTTPResponse(Convert::raw2json($data));
|
||||||
|
$response->addHeader('Content-Type', 'application/json');
|
||||||
|
} else {
|
||||||
|
$response = $this->getResponseNegotiator()->respond($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete($data, $form) {
|
public function delete($data, $form) {
|
||||||
@ -1260,14 +1353,27 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
$actionsFlattened = $actions->dataFields();
|
$actionsFlattened = $actions->dataFields();
|
||||||
if($actionsFlattened) foreach($actionsFlattened as $action) $action->setUseButtonTag(true);
|
if($actionsFlattened) foreach($actionsFlattened as $action) $action->setUseButtonTag(true);
|
||||||
|
|
||||||
$form = CMSForm::create(
|
$negotiator = $this->getResponseNegotiator();
|
||||||
|
$form = Form::create(
|
||||||
$this, "EditForm", $fields, $actions
|
$this, "EditForm", $fields, $actions
|
||||||
)->setHTMLID('Form_EditForm');
|
)->setHTMLID('Form_EditForm');
|
||||||
$form->setResponseNegotiator($this->getResponseNegotiator());
|
|
||||||
$form->addExtraClass('cms-edit-form');
|
$form->addExtraClass('cms-edit-form');
|
||||||
$form->loadDataFrom($record);
|
$form->loadDataFrom($record);
|
||||||
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
||||||
$form->setAttribute('data-pjax-fragment', 'CurrentForm');
|
$form->setAttribute('data-pjax-fragment', 'CurrentForm');
|
||||||
|
$form->setValidationResponseCallback(function() use ($negotiator, $form) {
|
||||||
|
$request = $this->getRequest();
|
||||||
|
if($request->isAjax() && $negotiator) {
|
||||||
|
$form->setupFormErrors();
|
||||||
|
$result = $form->forTemplate();
|
||||||
|
|
||||||
|
return $negotiator->respond($request, array(
|
||||||
|
'CurrentForm' => function() use($result) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Announce the capability so the frontend can decide whether to allow preview or not.
|
// Announce the capability so the frontend can decide whether to allow preview or not.
|
||||||
if(in_array('CMSPreviewable', class_implements($record))) {
|
if(in_array('CMSPreviewable', class_implements($record))) {
|
||||||
@ -1318,7 +1424,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
* @return Form
|
* @return Form
|
||||||
*/
|
*/
|
||||||
public function EmptyForm() {
|
public function EmptyForm() {
|
||||||
$form = CMSForm::create(
|
$form = Form::create(
|
||||||
$this,
|
$this,
|
||||||
"EditForm",
|
"EditForm",
|
||||||
new FieldList(
|
new FieldList(
|
||||||
@ -1337,7 +1443,6 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
),
|
),
|
||||||
new FieldList()
|
new FieldList()
|
||||||
)->setHTMLID('Form_EditForm');
|
)->setHTMLID('Form_EditForm');
|
||||||
$form->setResponseNegotiator($this->getResponseNegotiator());
|
|
||||||
$form->unsetValidator();
|
$form->unsetValidator();
|
||||||
$form->addExtraClass('cms-edit-form');
|
$form->addExtraClass('cms-edit-form');
|
||||||
$form->addExtraClass('root-form');
|
$form->addExtraClass('root-form');
|
||||||
|
@ -138,13 +138,12 @@ abstract class ModelAdmin extends LeftAndMain {
|
|||||||
$listField->getConfig()->getComponentByType('GridFieldDetailForm')->setValidator($detailValidator);
|
$listField->getConfig()->getComponentByType('GridFieldDetailForm')->setValidator($detailValidator);
|
||||||
}
|
}
|
||||||
|
|
||||||
$form = CMSForm::create(
|
$form = Form::create(
|
||||||
$this,
|
$this,
|
||||||
'EditForm',
|
'EditForm',
|
||||||
new FieldList($listField),
|
new FieldList($listField),
|
||||||
new FieldList()
|
new FieldList()
|
||||||
)->setHTMLID('Form_EditForm');
|
)->setHTMLID('Form_EditForm');
|
||||||
$form->setResponseNegotiator($this->getResponseNegotiator());
|
|
||||||
$form->addExtraClass('cms-edit-form cms-panel-padded center');
|
$form->addExtraClass('cms-edit-form cms-panel-padded center');
|
||||||
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
||||||
$editFormAction = Controller::join_links($this->Link($this->sanitiseClassName($this->modelClass)), 'EditForm');
|
$editFormAction = Controller::join_links($this->Link($this->sanitiseClassName($this->modelClass)), 'EditForm');
|
||||||
|
@ -177,13 +177,12 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
|
|||||||
|
|
||||||
$actions = new FieldList();
|
$actions = new FieldList();
|
||||||
|
|
||||||
$form = CMSForm::create(
|
$form = Form::create(
|
||||||
$this,
|
$this,
|
||||||
'EditForm',
|
'EditForm',
|
||||||
$fields,
|
$fields,
|
||||||
$actions
|
$actions
|
||||||
)->setHTMLID('Form_EditForm');
|
)->setHTMLID('Form_EditForm');
|
||||||
$form->setResponseNegotiator($this->getResponseNegotiator());
|
|
||||||
$form->addExtraClass('cms-edit-form');
|
$form->addExtraClass('cms-edit-form');
|
||||||
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
||||||
// Tab nav in CMS is rendered through separate template
|
// Tab nav in CMS is rendered through separate template
|
||||||
|
@ -79,6 +79,11 @@ class Form extends RequestHandler {
|
|||||||
*/
|
*/
|
||||||
protected $validator;
|
protected $validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var callable {@see setValidationResponseCallback()}
|
||||||
|
*/
|
||||||
|
protected $validationResponseCallback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
@ -479,16 +484,45 @@ class Form extends RequestHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return callable
|
||||||
|
*/
|
||||||
|
public function getValidationResponseCallback() {
|
||||||
|
return $this->validationResponseCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrules validation error behaviour in {@link httpSubmission()}
|
||||||
|
* when validation has failed. Useful for optional handling of a certain accepted content type.
|
||||||
|
*
|
||||||
|
* The callback can opt out of handling specific responses by returning NULL,
|
||||||
|
* in which case the default form behaviour will kick in.
|
||||||
|
*
|
||||||
|
* @param $callback
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function setValidationResponseCallback($callback) {
|
||||||
|
$this->validationResponseCallback = $callback;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the appropriate response up the controller chain
|
* Returns the appropriate response up the controller chain
|
||||||
* if {@link validate()} fails (which is checked prior to executing any form actions).
|
* if {@link validate()} fails (which is checked prior to executing any form actions).
|
||||||
* By default, returns different views for ajax/non-ajax request, and
|
* By default, returns different views for ajax/non-ajax request, and
|
||||||
* handles 'application/json' requests with a JSON object containing the error messages.
|
* handles 'application/json' requests with a JSON object containing the error messages.
|
||||||
* Behaviour can be influenced by setting {@link $redirectToFormOnValidationError}.
|
* Behaviour can be influenced by setting {@link $redirectToFormOnValidationError},
|
||||||
|
* and can be overruled by setting {@link $validationResponseCallback}.
|
||||||
*
|
*
|
||||||
* @return SS_HTTPResponse|string
|
* @return SS_HTTPResponse|string
|
||||||
*/
|
*/
|
||||||
protected function getValidationErrorResponse() {
|
protected function getValidationErrorResponse() {
|
||||||
|
$callback = $this->getValidationResponseCallback();
|
||||||
|
if($callback && $callbackResponse = $callback()) {
|
||||||
|
return $callbackResponse;
|
||||||
|
}
|
||||||
|
|
||||||
$request = $this->getRequest();
|
$request = $this->getRequest();
|
||||||
if($request->isAjax()) {
|
if($request->isAjax()) {
|
||||||
// Special case for legacy Validator.js implementation
|
// Special case for legacy Validator.js implementation
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
*/
|
*/
|
||||||
class FormField extends RequestHandler {
|
class FormField extends RequestHandler {
|
||||||
|
|
||||||
|
use SilverStripe\Forms\Schema\FormFieldSchemaTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Form
|
* @var Form
|
||||||
*/
|
*/
|
||||||
@ -80,7 +82,7 @@ class FormField extends RequestHandler {
|
|||||||
* @config
|
* @config
|
||||||
* @var array $default_classes The default classes to apply to the FormField
|
* @var array $default_classes The default classes to apply to the FormField
|
||||||
*/
|
*/
|
||||||
private static $default_classes = array();
|
private static $default_classes = [];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -162,7 +164,7 @@ class FormField extends RequestHandler {
|
|||||||
*
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $attributes = array();
|
protected $attributes = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
174
forms/FormFieldSchemaTrait.php
Normal file
174
forms/FormFieldSchemaTrait.php
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<?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' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
79
forms/FormSchema.php
Normal file
79
forms/FormSchema.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Forms\Schema;
|
||||||
|
|
||||||
|
use Convert;
|
||||||
|
use Form;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FormSchema
|
||||||
|
* @package SilverStripe\Forms\Schema
|
||||||
|
*
|
||||||
|
* Represents a {@link Form} as structured data which allows a frontend library to render it.
|
||||||
|
* Includes information about the form as well as its fields.
|
||||||
|
* Can create a "schema" (structure only) as well as "state" (data only).
|
||||||
|
*/
|
||||||
|
class FormSchema {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the schema for this form as a nested array.
|
||||||
|
*
|
||||||
|
* @param Form $form
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getSchema(Form $form) {
|
||||||
|
$request = $form->controller()->getRequest();
|
||||||
|
$params = $request->AllParams();
|
||||||
|
|
||||||
|
$schema = [
|
||||||
|
'name' => $form->getName(),
|
||||||
|
'id' => isset($params['ID']) ? $params['ID'] : null,
|
||||||
|
'action' => isset($params['Action']) ? $params['Action'] : null,
|
||||||
|
'method' => $form->controller()->getRequest()->HttpMethod(),
|
||||||
|
'schema_url' => $request->getUrl(),
|
||||||
|
'attributes' => $form->getAttributes(),
|
||||||
|
'data' => [],
|
||||||
|
'fields' => [],
|
||||||
|
'actions' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($form->Actions() as $action) {
|
||||||
|
$schema['actions'][] = $action->getSchemaData();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($form->Fields() as $fieldList) {
|
||||||
|
foreach ($fieldList->getForm()->fields()->dataFields() as $field) {
|
||||||
|
$schema['fields'][] = $field->getSchemaData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current state of this form as a nested array.
|
||||||
|
*
|
||||||
|
* @param Form $form
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getState(Form $form) {
|
||||||
|
$state = [
|
||||||
|
'id' => $form->getName(),
|
||||||
|
'fields' => [],
|
||||||
|
'messages' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($form->Fields()->dataFields() as $field) {
|
||||||
|
$state['fields'][] = $field->getSchemaState();
|
||||||
|
}
|
||||||
|
|
||||||
|
if($form->Message()) {
|
||||||
|
$state['messages'][] = [
|
||||||
|
'value' => $form->Message(),
|
||||||
|
'type' => $form->MessageType(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $state;
|
||||||
|
}
|
||||||
|
}
|
@ -241,6 +241,74 @@ class FormFieldTest extends SapphireTest {
|
|||||||
$this->assertArrayHasKey('extended', $field->getAttributes());
|
$this->assertArrayHasKey('extended', $field->getAttributes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSetSchemaComponent() {
|
||||||
|
$field = new FormField('MyField');
|
||||||
|
$field = $field->setSchemaComponent('MyComponent');
|
||||||
|
$component = $field->getSchemaComponent();
|
||||||
|
$this->assertEquals('MyComponent', $component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSchemaDataDefaults() {
|
||||||
|
$field = new FormField('MyField');
|
||||||
|
$schema = $field->getSchemaDataDefaults();
|
||||||
|
$this->assertInternalType('array', $schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSchemaData() {
|
||||||
|
$field = new FormField('MyField');
|
||||||
|
$schema = $field->getSchemaData();
|
||||||
|
$this->assertEquals('MyField', $schema['name']);
|
||||||
|
|
||||||
|
// Make sure the schema data is up-to-date with object properties.
|
||||||
|
$field->setName('UpdatedField');
|
||||||
|
$schema = $field->getSchemaData();
|
||||||
|
$this->assertEquals($field->getName(), $schema['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetSchemaData() {
|
||||||
|
$field = new FormField('MyField');
|
||||||
|
|
||||||
|
// Make sure the user can update values.
|
||||||
|
$field = $field->setSchemaData(['name' => 'MyUpdatedField']);
|
||||||
|
$schema = $field->getSchemaData();
|
||||||
|
$this->assertEquals($schema['name'], 'MyUpdatedField');
|
||||||
|
|
||||||
|
// Make user the user can't define custom keys on the schema.
|
||||||
|
$field = $field->setSchemaData(['myCustomKey' => 'yolo']);
|
||||||
|
$schema = $field->getSchemaData();
|
||||||
|
$this->assertEquals(array_key_exists('myCustomKey', $schema), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSchemaState() {
|
||||||
|
$field = new FormField('MyField');
|
||||||
|
$field->setValue('My value');
|
||||||
|
$schema = $field->getSchemaState();
|
||||||
|
$this->assertEquals('My value', $schema['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetSchemaState() {
|
||||||
|
$field = new FormField('MyField');
|
||||||
|
|
||||||
|
// Make sure the user can update values.
|
||||||
|
$field = $field->setSchemaState(['value' => 'My custom value']);
|
||||||
|
$schema = $field->getSchemaState();
|
||||||
|
$this->assertEquals($schema['value'], 'My custom value');
|
||||||
|
|
||||||
|
// Make user the user can't define custom keys on the schema.
|
||||||
|
$field = $field->setSchemaState(['myCustomKey' => 'yolo']);
|
||||||
|
$schema = $field->getSchemaState();
|
||||||
|
$this->assertEquals(array_key_exists('myCustomKey', $schema), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSchemaStateWithFormValidation() {
|
||||||
|
$field = new FormField('MyField');
|
||||||
|
$validator = new RequiredFields('MyField');
|
||||||
|
$form = new Form(new Controller(), 'TestForm', new FieldList($field), new FieldList(), $validator);
|
||||||
|
$validator->validationError('MyField', 'Something is wrong', 'error');
|
||||||
|
$schema = $field->getSchemaState();
|
||||||
|
$this->assertEquals(count($schema['messages']), 1);
|
||||||
|
$this->assertEquals('Something is wrong', $schema['messages'][0]['value']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
142
tests/forms/FormSchemaTest.php
Normal file
142
tests/forms/FormSchemaTest.php
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use SilverStripe\Forms\Schema\FormSchema;
|
||||||
|
|
||||||
|
class FormSchemaTest extends SapphireTest {
|
||||||
|
|
||||||
|
public function testGetSchema() {
|
||||||
|
$form = new Form(new Controller(), 'TestForm', new FieldList(), new FieldList());
|
||||||
|
$formSchema = new FormSchema();
|
||||||
|
$expected = [
|
||||||
|
'name' => 'TestForm',
|
||||||
|
'id' => null,
|
||||||
|
'action' => null,
|
||||||
|
'method' => '',
|
||||||
|
'schema_url' => '',
|
||||||
|
'attributes' => [
|
||||||
|
'id' => 'Form_TestForm',
|
||||||
|
'action' => 'Controller/TestForm',
|
||||||
|
'method' => 'POST',
|
||||||
|
'enctype' => 'application/x-www-form-urlencoded',
|
||||||
|
'target' => null,
|
||||||
|
'class' => ''
|
||||||
|
],
|
||||||
|
'data' => [],
|
||||||
|
'fields' => [
|
||||||
|
[
|
||||||
|
'type' => "HiddenField",
|
||||||
|
'component' => null,
|
||||||
|
'id' => null,
|
||||||
|
'holder_id' => null,
|
||||||
|
'name' => 'SecurityID',
|
||||||
|
'title' => 'Security ID',
|
||||||
|
'source' => null,
|
||||||
|
'extraClass' => 'hidden',
|
||||||
|
'description' => null,
|
||||||
|
'rightTitle' => null,
|
||||||
|
'leftTitle' => null,
|
||||||
|
'readOnly' => false,
|
||||||
|
'disabled' => false,
|
||||||
|
'customValidationMessage' => '',
|
||||||
|
'attributes' => [],
|
||||||
|
'data' => []
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'actions' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
$schema = $formSchema->getSchema($form);
|
||||||
|
$this->assertInternalType('array', $schema);
|
||||||
|
$this->assertJsonStringEqualsJsonString(json_encode($expected), json_encode($schema));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetState() {
|
||||||
|
$form = new Form(new Controller(), 'TestForm', new FieldList(), new FieldList());
|
||||||
|
$formSchema = new FormSchema();
|
||||||
|
$expected = [
|
||||||
|
'id' => 'TestForm',
|
||||||
|
'fields' => [
|
||||||
|
[
|
||||||
|
'id' => 'Form_TestForm_SecurityID',
|
||||||
|
'value' => $form->getSecurityToken()->getValue(),
|
||||||
|
'messages' => [],
|
||||||
|
'valid' => true,
|
||||||
|
'data' => []
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'messages' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
$state = $formSchema->getState($form);
|
||||||
|
$this->assertInternalType('array', $state);
|
||||||
|
$this->assertJsonStringEqualsJsonString(json_encode($expected), json_encode($state));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetStateWithFormMessages() {
|
||||||
|
$fields = new FieldList();
|
||||||
|
$actions = new FieldList();
|
||||||
|
$form = new Form(new Controller(), 'TestForm', $fields, $actions);
|
||||||
|
$form->sessionMessage('All saved', 'good');
|
||||||
|
$formSchema = new FormSchema();
|
||||||
|
$expected = [
|
||||||
|
'id' => 'TestForm',
|
||||||
|
'fields' => [
|
||||||
|
[
|
||||||
|
'id' => 'Form_TestForm_SecurityID',
|
||||||
|
'value' => $form->getSecurityToken()->getValue(),
|
||||||
|
'messages' => [],
|
||||||
|
'valid' => true,
|
||||||
|
'data' => []
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'messages' => [
|
||||||
|
[
|
||||||
|
'value' => 'All saved',
|
||||||
|
'type' => 'good'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$state = $formSchema->getState($form);
|
||||||
|
$this->assertInternalType('array', $state);
|
||||||
|
$this->assertJsonStringEqualsJsonString(json_encode($expected), json_encode($state));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetStateWithFieldValidationErrors() {
|
||||||
|
$fields = new FieldList(new TextField('Title'));
|
||||||
|
$actions = new FieldList();
|
||||||
|
$validator = new RequiredFields('Title');
|
||||||
|
$form = new Form(new Controller(), 'TestForm', $fields, $actions, $validator);
|
||||||
|
$form->loadDataFrom([
|
||||||
|
'Title' => 'My Title'
|
||||||
|
]);
|
||||||
|
$validator->validationError('Title', 'Title is invalid', 'error');
|
||||||
|
$formSchema = new FormSchema();
|
||||||
|
$expected = [
|
||||||
|
'id' => 'TestForm',
|
||||||
|
'fields' => [
|
||||||
|
[
|
||||||
|
'id' => 'Form_TestForm_Title',
|
||||||
|
'value' => 'My Title',
|
||||||
|
'messages' => [
|
||||||
|
['value' => 'Title is invalid', 'type' => 'error']
|
||||||
|
],
|
||||||
|
'valid' => false,
|
||||||
|
'data' => []
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'Form_TestForm_SecurityID',
|
||||||
|
'value' => $form->getSecurityToken()->getValue(),
|
||||||
|
'messages' => [],
|
||||||
|
'valid' => true,
|
||||||
|
'data' => []
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'messages' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
$state = $formSchema->getState($form);
|
||||||
|
$this->assertInternalType('array', $state);
|
||||||
|
$this->assertJsonStringEqualsJsonString(json_encode($expected), json_encode($state));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user