From 69bbd0aa220f42c0bc9600319aa6118adcdc72c6 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Tue, 1 Mar 2016 16:15:47 +1300 Subject: [PATCH] Form field schema state See https://github.com/silverstripe/silverstripe-framework/issues/4938 --- forms/FormField.php | 8 ++++ forms/FormFieldSchemaTrait.php | 61 +++++++++++++++++++++++++- forms/FormSchema.php | 13 +++++- tests/forms/FormFieldTest.php | 31 ++++++++++++++ tests/forms/FormSchemaTest.php | 78 +++++++++++++++++++++++++++++++++- 5 files changed, 188 insertions(+), 3 deletions(-) diff --git a/forms/FormField.php b/forms/FormField.php index 239dba2cb50..42d0a99fa00 100644 --- a/forms/FormField.php +++ b/forms/FormField.php @@ -181,6 +181,14 @@ class FormField extends RequestHandler { */ 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. diff --git a/forms/FormFieldSchemaTrait.php b/forms/FormFieldSchemaTrait.php index f78209f5f40..035b386d635 100644 --- a/forms/FormFieldSchemaTrait.php +++ b/forms/FormFieldSchemaTrait.php @@ -58,7 +58,7 @@ public function getSchemaData() { * * @return array */ - function getSchemaDataDefaults() { + public function getSchemaDataDefaults() { return [ 'type' => $this->class, 'component' => $this->getSchemaComponent(), @@ -78,4 +78,63 @@ function getSchemaDataDefaults() { '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 2b7335b7050..17307193f84 100644 --- a/forms/FormSchema.php +++ b/forms/FormSchema.php @@ -45,7 +45,7 @@ public function getSchema(Form $form) { /** * Gets the current state of this form as a nested array. * - * @param From $form + * @param Form $form * @return array */ public function getState(Form $form) { @@ -55,6 +55,17 @@ public function getState(Form $form) { 'messages' => [] ]; + foreach ($form->Fields()->dataFields() as $field) { + $state['fields'][] = $field->getSchemaState(); + } + + if($form->Message()) { + $state['messages'][] = [ + 'value' => $form->Message(), + 'type' => $form->MessageType(), + ]; + } + return $state; } } diff --git a/tests/forms/FormFieldTest.php b/tests/forms/FormFieldTest.php index 7ab13232734..b2f235fc58a 100644 --- a/tests/forms/FormFieldTest.php +++ b/tests/forms/FormFieldTest.php @@ -278,6 +278,37 @@ public function testSetSchemaData() { $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']); + } } /** diff --git a/tests/forms/FormSchemaTest.php b/tests/forms/FormSchemaTest.php index 3aaf365339e..4cf1d556409 100644 --- a/tests/forms/FormSchemaTest.php +++ b/tests/forms/FormSchemaTest.php @@ -55,7 +55,83 @@ public function testGetState() { $formSchema = new FormSchema(); $expected = [ 'id' => 'TestForm', - 'fields' => [], + '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' => [] ];