Skip to content

Commit e3a2c1d

Browse files
author
Stephen Lucero
committed
[#3453875] feat: Test Validation of Required Fields within Schema
By: minsharm By: slucero
1 parent f7a318b commit e3a2c1d

7 files changed

Lines changed: 382 additions & 31 deletions

File tree

‎js/patternkit.jsoneditor.js‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*
1818
* @todo .patternkit-editor-target .card all: initial
1919
*
20-
* cspell:ignore iconlib
20+
* cspell:ignore iconlib startval
2121
*/
2222

2323
import {patternkitEditorCKEditor5} from './patternkit.jsoneditor.ckeditor5.js';
@@ -49,8 +49,14 @@ patternkitEditorArray(jQuery, Drupal, JSONEditor);
4949

5050
this.data = {
5151
schema: JSON.parse(settings.schemaJson),
52-
starting: JSON.parse(settings.startingJson)
5352
};
53+
54+
// Only assign a starting value if a non-empty value was provided,
55+
// otherwise don't provide a value so a default may be assumed.
56+
const startVal = settings.startingJson;
57+
if (startVal !== '{}' && Boolean(startVal) === true) {
58+
this.data.starting = JSON.parse(startVal)
59+
}
5460
}
5561

5662
/**
@@ -143,8 +149,17 @@ patternkitEditorArray(jQuery, Drupal, JSONEditor);
143149
// Initialize the editor with a JSON schema.
144150
let config = {
145151
schema: this.data.schema,
146-
refs: {}
152+
refs: {},
147153
};
154+
155+
// Conditionally specify the start value only if data already exists.
156+
// Starting with an empty string or object results in all form fields
157+
// starting hidden by default.
158+
if (this.data.starting) {
159+
config.startval = this.data.starting;
160+
}
161+
162+
148163
window.patternkitEditor = new JSONEditor(this.editor_root.getElementById('editor_holder'), config);
149164
window.patternkitEditor.patternkit = this;
150165

‎patternkit.services.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ services:
135135
- '@entity_type.manager'
136136
- '@keyvalue.expirable'
137137
- '@module_handler'
138+
patternkit.helper.validation:
139+
class: Drupal\patternkit\Utility\PatternValidationHelper
140+
arguments:
141+
- '@patternkit.schema.context_builder'
142+
- '@patternkit.schema.schema_factory'
138143
patternkit.helper.layout:
139144
class: Drupal\patternkit\LayoutHelper
140145
arguments:

‎src/Element/JsonEditorForm.php‎

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
1010
use Drupal\Core\Render\Element\FormElementBase;
1111
use Drupal\patternkit\Entity\PatternInterface;
12+
use Drupal\patternkit\Exception\SchemaValidationException;
1213
use Drupal\patternkit\Exception\UnknownPatternException;
1314
use Drupal\patternkit\Form\PatternLibraryJSONForm;
1415
use Drupal\patternkit\PatternLibraryPluginManager;
@@ -21,7 +22,7 @@
2122
* Properties:
2223
* - #pattern_name: The name of the pattern to be loaded and used for the form.
2324
* - #pattern_entity: (optional) The loaded Pattern entity for the form.
24-
* - #data: (optional) A serialized string of JSON values for the form.
25+
* - #default_value: (optional) A serialized string of JSON values for the form.
2526
* - #config: (optional) An associative array of configuration options for
2627
* JSONEditor behaviors.
2728
*
@@ -33,6 +34,7 @@
3334
* '#config' => [
3435
* 'disablePropertiesButtons' => FALSE,
3536
* ],
37+
* '#default_value' => '{"text": "My text value"}',
3638
* ];
3739
* @endcode
3840
*
@@ -71,18 +73,24 @@ public function getInfo(): array {
7173
'#pre_render' => [
7274
[$this, 'loadPattern'],
7375
[$this, 'buildEditorForm'],
76+
[static::class, 'preRenderGroup'],
7477
],
7578
'#process' => [
79+
[static::class, 'processAjaxForm'],
80+
[static::class, 'processGroup'],
7681
[static::class, 'processEditorForm'],
7782
],
7883
'#theme_wrappers' => ['form_element'],
84+
'#input' => TRUE,
7985
'#pattern_name' => NULL,
8086
'#pattern_entity' => NULL,
8187
'#schema' => '',
82-
// Pattern instance configuration values.
83-
'#data' => '{}',
8488
// Editor configuration options to be overridden.
8589
'#config' => [],
90+
// Validate upon submissions.
91+
'#element_validate' => [
92+
[static::class, 'validatePatternConfig'],
93+
],
8694
];
8795
}
8896

@@ -204,7 +212,8 @@ public function buildEditorForm(array $element): array {
204212
$editor_overrides = $element['#config'] ?? [];
205213

206214
// Assemble the full instance settings for the editor.
207-
$settings = $this->editorSettings($element['#schema'], $element['#data'] ?? '{}', $editor_overrides);
215+
$value = $element['#value'] ?? $element['#default_value'] ?? '{}';
216+
$settings = $this->editorSettings($element['#schema'], $value, $editor_overrides);
208217

209218
// Load the editor from the pattern's library plugin.
210219
$pattern = $element['#pattern_entity'];
@@ -240,19 +249,75 @@ public function buildEditorForm(array $element): array {
240249
* The updated render element.
241250
*/
242251
public static function processEditorForm(&$element, FormStateInterface $form_state, array $form) {
252+
// Retrieve the form-submitted value if available, otherwise, default to the
253+
// original element data.
254+
$data = $form_state->getValue($element['#parents']);
255+
256+
// Update the element data value to persist it to the next form build.
257+
// This ensures even if it was loaded from a previous form submission, the
258+
// element initialization data is synchronized with the submitted value.
259+
$element['#value'] = $data;
260+
243261
// Add a hidden field to capture configuration from the JSONEditor form.
244262
$element['instance_config'] = [
245263
'#type' => 'hidden',
246264
'#attributes' => [
247265
'id' => 'schema_instance_config',
248266
],
249-
'#default_value' => $element['#data'] ?? '{}',
267+
'#value' => $data,
250268
'#parents' => $element['#parents'],
251269
];
252270

253271
return $element;
254272
}
255273

274+
/**
275+
* Validates the pattern configuration.
276+
*
277+
* @param array $element
278+
* The render element being processed.
279+
* @param \Drupal\Core\Form\FormStateInterface $form_state
280+
* The current form state.
281+
* @param array $form
282+
* The full form.
283+
*
284+
* @see \Drupal\patternkit\Element\JsonEditorForm::getInfo()
285+
*/
286+
public static function validatePatternConfig(&$element, FormStateInterface $form_state, array $form): void {
287+
$value = $form_state->getValue($element['#parents']);
288+
$pattern = $element['#pattern_entity'];
289+
$schema = $pattern->getSchema();
290+
$validationHelper = \Drupal::service('patternkit.helper.validation');
291+
$validationHelper->validateContent($schema, $value);
292+
if ($validationHelper->hasExceptions()) {
293+
294+
$message = 'Content has failed to validate. See errors below.';
295+
296+
// Log additional information if debug mode is enabled.
297+
/** @var \Drupal\patternkit\PatternkitEnvironment $environment */
298+
$environment = \Drupal::service('patternkit.environment');
299+
if ($environment->isDebug()) {
300+
$exceptions = $validationHelper->getExceptions();
301+
$exception = array_shift($exceptions);
302+
if ($exception instanceof SchemaValidationException && $previous = $exception->getPrevious()) {
303+
$message .= "\n" . $previous->getMessage();
304+
}
305+
else {
306+
$message .= "\n" . $exception->getMessage();
307+
}
308+
}
309+
310+
$form_state->setError($element, $message);
311+
}
312+
}
313+
314+
/**
315+
* {@inheritdoc}
316+
*/
317+
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
318+
return is_string($input) ? $input : NULL;
319+
}
320+
256321
/**
257322
* Statically load the configured settings for default JSON Editor behaviors.
258323
*
@@ -285,22 +350,8 @@ protected function defaultEditorSettings(): array {
285350
$config_object = $this->configFactory->get(PatternLibraryJSONForm::SETTINGS);
286351
$config_array = isset($config_object) ? $config_object->get() : [];
287352

288-
// Map config names to JS-friendly identifiers.
289353
// @todo Remove this once internal config names are renamed.
290-
$mapped_config = [];
291-
$map = PatternLibraryJSONForm::JSON_EDITOR_SETTINGS_MAP;
292-
array_walk($config_array, function ($value, $key) use (&$mapped_config, $map) {
293-
if (array_key_exists($key, $map)) {
294-
// Relabel the value if a mapped key is found.
295-
$mapped_config[$map[$key]] = $value;
296-
}
297-
else {
298-
// Maintain the existing key if no mapped key is found.
299-
$mapped_config[$key] = $value;
300-
}
301-
});
302-
303-
$this->config = $mapped_config;
354+
$this->config = $this->mapConfigNames($config_array);
304355
}
305356

306357
$this->defaults = $this->config + [
@@ -316,4 +367,33 @@ protected function defaultEditorSettings(): array {
316367
return $this->defaults;
317368
}
318369

370+
/**
371+
* Map server-side configuration values to JS names.
372+
*
373+
* @param mixed $config_array
374+
* The server-side configuration values to be mapped to JS names.
375+
*
376+
* @return array
377+
* An array of config mapped to JS-friendly names.
378+
*
379+
* @todo Remove this once internal config names are renamed.
380+
*/
381+
private function mapConfigNames(mixed $config_array): array {
382+
// Map config names to JS-friendly identifiers.
383+
$mapped_config = [];
384+
$map = PatternLibraryJSONForm::JSON_EDITOR_SETTINGS_MAP;
385+
array_walk($config_array, function ($value, $key) use (&$mapped_config, $map) {
386+
if (array_key_exists($key, $map)) {
387+
// Relabel the value if a mapped key is found.
388+
$mapped_config[$map[$key]] = $value;
389+
}
390+
else {
391+
// Maintain the existing key if no mapped key is found.
392+
$mapped_config[$key] = $value;
393+
}
394+
});
395+
396+
return $mapped_config;
397+
}
398+
319399
}

‎src/Plugin/Block/PatternkitBlock.php‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,18 @@ protected function handleBlockFormUpdate(FormStateInterface $form_state): void {
377377

378378
$pattern = $this->updatePattern($pattern, $base_pattern);
379379

380+
// Capture updated pattern data in form state for reference.
380381
$form_state->set('pattern', $pattern);
381382
$form_state->set('schema_updated', TRUE);
382383
$form_state->set('configuration', $this->getConfiguration());
383-
$form_state->setRebuild(TRUE);
384+
385+
// Update the form state values to be properly reflected.
386+
$block_entity = $this->getBlockEntity();
387+
// Access data field value directly to get the serialized version.
388+
$form_state->setValue('configuration', $block_entity->data->value);
389+
390+
// Set the form to be rebuilt to reflect the changes.
391+
$form_state->setRebuild();
384392
}
385393

386394
/**
@@ -412,8 +420,8 @@ public function updatePattern(?PatternInterface $pattern = NULL, ?PatternInterfa
412420
$pattern ??= $this->getPatternEntity();
413421
$base_pattern ??= $this->getBasePattern();
414422

415-
// If a base pattern is provided we assume it is always for the same pattern
416-
// asset.
423+
// If a base pattern is provided, we assume it is always for the same
424+
// pattern asset.
417425
assert($pattern->getAssetId() === $base_pattern->getAssetId());
418426

419427
// Stop here if no update is available.
@@ -759,7 +767,7 @@ protected function editorForm(array &$form, FormStateInterface $form_state): voi
759767
$form['configuration'] = [
760768
'#type' => 'jsoneditor_form',
761769
'#pattern_entity' => $pattern,
762-
'#data' => $data ?? '{}',
770+
'#default_value' => $data ?? '{}',
763771
];
764772
}
765773

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Drupal\patternkit\Utility;
4+
5+
use Drupal\patternkit\PatternValidationTrait;
6+
use Drupal\patternkit\Schema\ContextBuilder;
7+
use Drupal\patternkit\Schema\SchemaFactory;
8+
9+
/**
10+
* A service for validating pattern content against schemas.
11+
*
12+
* This service provides a centralized way for other classes to validate
13+
* pattern content against JSON schemas. It wraps the PatternValidationTrait
14+
* functionality in a service that can be easily injected into other classes.
15+
*
16+
* @internal This service may be replaced or rewritten in the future and does
17+
* not guarantee backward compatibility.
18+
*/
19+
class PatternValidationHelper {
20+
21+
use PatternValidationTrait {
22+
hasExceptions as public;
23+
getExceptions as public;
24+
validateContent as public;
25+
validatePattern as public;
26+
}
27+
28+
/**
29+
* Constructs a PatternValidationHelper object.
30+
*
31+
* @param \Drupal\patternkit\Schema\ContextBuilder $contextBuilder
32+
* The schema context builder service.
33+
* @param \Drupal\patternkit\Schema\SchemaFactory $schemaFactory
34+
* The schema factory service.
35+
*/
36+
public function __construct(
37+
ContextBuilder $contextBuilder,
38+
SchemaFactory $schemaFactory,
39+
) {
40+
$this->setContextBuilder($contextBuilder);
41+
$this->setSchemaFactory($schemaFactory);
42+
}
43+
44+
/**
45+
* Clear all stored exceptions.
46+
*
47+
* This method allows the service to be reused for multiple validations
48+
* without carrying over exceptions from previous validation attempts.
49+
*/
50+
public function clearExceptions(): void {
51+
$this->exceptions = [];
52+
}
53+
54+
}

‎tests/src/Unit/Element/JsonEditorFormTest.php‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ public function testProcessEditorForm() {
289289
'#type' => 'jsoneditor_form',
290290
'#pattern_name' => 'my_pattern',
291291
'#schema' => $random->string(50),
292-
'#data' => $random->string(50),
292+
'#default_value' => $random->string(50),
293293
'#config' => (array) $random->object(),
294294
'#parents' => [$random->string(), $random->string()],
295295
];
@@ -298,11 +298,15 @@ public function testProcessEditorForm() {
298298
$element_copy = $render_element;
299299

300300
// Mock additional arguments for the method.
301-
$form_state = $this->prophesize(FormStateInterface::class);
301+
$form_state = $this->getMockBuilder(FormStateInterface::class)
302+
->getMock();
303+
$form_state->method('getValue')
304+
->with($render_element['#parents'])
305+
->willReturn($render_element['#default_value']);
302306
$form = [];
303307

304308
// Run the process method for testing.
305-
$result = $this->element->processEditorForm($element_copy, $form_state->reveal(), $form);
309+
$result = $this->element->processEditorForm($element_copy, $form_state, $form);
306310

307311
// Expect all pre-existing values to be persisted.
308312
$this->assertArraySubset($render_element, $result);
@@ -313,7 +317,7 @@ public function testProcessEditorForm() {
313317
'#attributes' => [
314318
'id' => 'schema_instance_config',
315319
],
316-
'#default_value' => $render_element['#data'],
320+
'#value' => $render_element['#default_value'],
317321
'#parents' => $render_element['#parents'],
318322
];
319323
$this->assertArrayHasKey('instance_config', $result);

0 commit comments

Comments
 (0)