diff --git a/_config/config.yml b/_config/config.yml index 42dc3103c..77ff116b9 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -1,6 +1,9 @@ --- Name: coreconfig --- +Injector: + FormSchema: + class: SilverStripe\Forms\Schema\FormSchema Upload: # Replace an existing file rather than renaming the new one. replaceFile: false diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index b651ac957..ec4c09145 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -5,6 +5,8 @@ * @subpackage admin */ +use SilverStripe\Forms\Schema\FormSchema; + /** * 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. @@ -84,7 +86,7 @@ class LeftAndMain extends Controller implements PermissionProvider { /** * @var array */ - private static $allowed_actions = array( + private static $allowed_actions = [ 'index', 'save', 'savetreenode', @@ -97,7 +99,12 @@ class LeftAndMain extends Controller implements PermissionProvider { 'AddForm', 'batchactions', 'BatchActionsForm', - ); + 'schema', + ]; + + private static $dependencies = [ + 'schema' => '%$FormSchema' + ]; /** * @config @@ -169,6 +176,15 @@ class LeftAndMain extends Controller implements PermissionProvider { */ protected $responseNegotiator; + /** + * Gets a JSON schema representing the current edit form. + * + * @return SS_HTTPResponse + */ + public function schema() { + return $this->schema->getSchema($this->getEditForm()); + } + /** * @param Member $member * @return boolean diff --git a/forms/FormField.php b/forms/FormField.php index f041af1f8..239dba2cb 100644 --- a/forms/FormField.php +++ b/forms/FormField.php @@ -25,6 +25,8 @@ */ class FormField extends RequestHandler { + use SilverStripe\Forms\Schema\FormFieldSchemaTrait; + /** * @var Form */ @@ -80,7 +82,7 @@ class FormField extends RequestHandler { * @config * @var array $default_classes The default classes to apply to the FormField */ - private static $default_classes = array(); + private static $default_classes = []; /** @@ -162,7 +164,22 @@ class FormField extends RequestHandler { * * @var array */ - protected $attributes = array(); + protected $attributes = []; + + /** + * 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 = []; /** * Takes a field name and converts camelcase to spaced words. Also resolves combined field diff --git a/forms/FormFieldSchemaTrait.php b/forms/FormFieldSchemaTrait.php new file mode 100644 index 000000000..f78209f5f --- /dev/null +++ b/forms/FormFieldSchemaTrait.php @@ -0,0 +1,81 @@ +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 + */ + 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' => [], + ]; + } +} diff --git a/forms/FormSchema.php b/forms/FormSchema.php new file mode 100644 index 000000000..b3d61a44e --- /dev/null +++ b/forms/FormSchema.php @@ -0,0 +1,56 @@ +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 Convert::raw2json($schema); + } + + /** + * Gets the current state of this form as a nested array. + * + * @param From $form + * @return string + */ + public function getState(Form $form) { + $state = ['state' => []]; + + return Convert::raw2json($state); + } +} diff --git a/tests/forms/FormFieldTest.php b/tests/forms/FormFieldTest.php index 221091dad..7ab132327 100644 --- a/tests/forms/FormFieldTest.php +++ b/tests/forms/FormFieldTest.php @@ -241,6 +241,43 @@ class FormFieldTest extends SapphireTest { $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); + } } /** diff --git a/tests/forms/FormSchemaTest.php b/tests/forms/FormSchemaTest.php new file mode 100644 index 000000000..11b7f3697 --- /dev/null +++ b/tests/forms/FormSchemaTest.php @@ -0,0 +1,62 @@ + '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->assertJsonStringEqualsJsonString($expectedJSON, $schema); + } + + public function testGetState() { + $form = new Form(new Controller(), 'TestForm', new FieldList(), new FieldList()); + $formSchema = new FormSchema(); + $expectedJSON = json_encode(['state' => []]); + + $state = $formSchema->getState($form); + + $this->assertJsonStringEqualsJsonString($expectedJSON, $state); + } +}