From 620a37aa82948b441105c2c01414ec3fc9651a02 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 8 Dec 2022 17:09:17 -0500 Subject: [PATCH] Handle edits within frontend fields (#7178) --- .../forms/fields/checkboxes.antlers.html | 9 +- .../extend/forms/fields/default.antlers.html | 4 +- .../extend/forms/fields/radio.antlers.html | 9 +- .../extend/forms/fields/select.antlers.html | 6 +- .../extend/forms/fields/text.antlers.html | 2 +- .../extend/forms/fields/textarea.antlers.html | 2 +- .../extend/forms/fields/toggle.antlers.html | 2 +- src/Tags/Concerns/RendersForms.php | 6 + tests/Tags/Concerns/RendersFormsTest.php | 304 ++++++++++++++++++ 9 files changed, 326 insertions(+), 18 deletions(-) diff --git a/resources/views/extend/forms/fields/checkboxes.antlers.html b/resources/views/extend/forms/fields/checkboxes.antlers.html index 5864bdb289..909a9e76cc 100644 --- a/resources/views/extend/forms/fields/checkboxes.antlers.html +++ b/resources/views/extend/forms/fields/checkboxes.antlers.html @@ -1,15 +1,14 @@ -{{ foreach:options as="value|label" }} +{{ foreach:options as="option|label" }} {{ unless inline }}
diff --git a/resources/views/extend/forms/fields/default.antlers.html b/resources/views/extend/forms/fields/default.antlers.html index dc01262b2a..44c28b9ba0 100644 --- a/resources/views/extend/forms/fields/default.antlers.html +++ b/resources/views/extend/forms/fields/default.antlers.html @@ -1,7 +1,7 @@ - {{ label !== null ? label : value }} + {{ label !== null ? label : option }} {{ unless inline }}
diff --git a/resources/views/extend/forms/fields/select.antlers.html b/resources/views/extend/forms/fields/select.antlers.html index 4fa8cc2f02..b093f6a026 100644 --- a/resources/views/extend/forms/fields/select.antlers.html +++ b/resources/views/extend/forms/fields/select.antlers.html @@ -13,9 +13,9 @@ {{ /if }} {{ /unless }} - {{ foreach:options as="value|label" }} - {{ /foreach:options }} diff --git a/resources/views/extend/forms/fields/text.antlers.html b/resources/views/extend/forms/fields/text.antlers.html index 5b7803cffb..44c28b9ba0 100644 --- a/resources/views/extend/forms/fields/text.antlers.html +++ b/resources/views/extend/forms/fields/text.antlers.html @@ -1,7 +1,7 @@ - {{ old ?? default }} + {{ value }} diff --git a/resources/views/extend/forms/fields/toggle.antlers.html b/resources/views/extend/forms/fields/toggle.antlers.html index e73417ce50..943bb307e2 100644 --- a/resources/views/extend/forms/fields/toggle.antlers.html +++ b/resources/views/extend/forms/fields/toggle.antlers.html @@ -5,7 +5,7 @@ name="{{ handle }}" value="1" {{ if js_driver }}{{ js_attributes }}{{ /if }} - {{ if ! old && default }}checked{{ /if }} + {{ if value && value !== '0' }}checked{{ /if }} {{ if validate|contains:required }}required{{ /if }} > {{ if inline_label }} diff --git a/src/Tags/Concerns/RendersForms.php b/src/Tags/Concerns/RendersForms.php index 50a5fc470e..345191628d 100644 --- a/src/Tags/Concerns/RendersForms.php +++ b/src/Tags/Concerns/RendersForms.php @@ -127,9 +127,15 @@ protected function getRenderableField($field, $errorBag = 'default', $manipulate { $errors = session('errors') ? session('errors')->getBag($errorBag) : new MessageBag; + $missing = str_random(); + $old = old($field->handle(), $missing); + $default = $field->value() ?? $field->defaultValue(); + $value = $old === $missing ? $default : $old; + $data = array_merge($field->toArray(), [ 'error' => $errors->first($field->handle()) ?: null, 'old' => old($field->handle()), + 'value' => $value, ]); if ($manipulateDataCallback instanceof Closure) { diff --git a/tests/Tags/Concerns/RendersFormsTest.php b/tests/Tags/Concerns/RendersFormsTest.php index fc17195ee0..0354ee8c9c 100644 --- a/tests/Tags/Concerns/RendersFormsTest.php +++ b/tests/Tags/Concerns/RendersFormsTest.php @@ -3,12 +3,16 @@ namespace Tests\Tags\Concerns; use Statamic\Facades\Antlers; +use Statamic\Fields\Field; +use Statamic\Support\Arr; use Statamic\Tags\Concerns; use Statamic\Tags\Tags; use Tests\TestCase; class RendersFormsTest extends TestCase { + const MISSING = 'field is missing from request'; + public function setUp(): void { parent::setUp(); @@ -89,6 +93,306 @@ public function it_minifies_space_between_field_html_elements() $this->assertEquals($expected, $this->tag->minifyFieldHtml($fields)); } + + private function createField($type, $value, $default, $old, $config = []) + { + $config = array_merge($config, ['type' => $type]); + + if ($default) { + $config['default'] = $default; + } + + $field = new Field('test', $config); + $field->setValue($value); + + if ($old !== self::MISSING) { + session()->flashInput(['test' => $old]); + $this->get('/'); // create a request so the session works. + } + + return $this->tag->getRenderableField($field); + } + + /** + * @test + * @dataProvider renderTextProvider + */ + public function renders_text_fields($value, $default, $old, $expected) + { + $this->textFieldtypeTest('text', $value, $default, $old, $expected); + } + + private function textFieldtypeTest($fieldtype, $value, $default, $old, $expected) + { + $rendered = $this->createField($fieldtype, $value, $default, $old); + + $this->assertSame($expected, $rendered['value']); + $this->assertStringContainsString('value="'.$rendered['value'].'"', $rendered['field']); + } + + /** + * @test + * @dataProvider renderTextProvider + */ + public function renders_fallback_fields_as_text_fields($value, $default, $old, $expected) + { + (new class extends \Statamic\Fields\Fieldtype + { + protected static $handle = 'testing'; + })::register(); + + $this->textFieldtypeTest('testing', $value, $default, $old, $expected); + } + + /** + * @test + * @dataProvider renderTextProvider + */ + public function renders_textarea_fields($value, $default, $old, $expected) + { + $rendered = $this->createField('textarea', $value, $default, $old); + + $this->assertSame($expected, $rendered['value']); + $this->assertStringContainsString('>'.$rendered['value'].' ['value' => null, 'default' => null, 'old' => self::MISSING, 'expectedValue' => null], + 'no value, filled' => ['value' => null, 'default' => null, 'old' => 'old', 'expectedValue' => 'old'], + 'no value, empty' => ['value' => null, 'default' => null, 'old' => null, 'expectedValue' => null], + + 'value, missing' => ['value' => 'existing', 'default' => null, 'old' => self::MISSING, 'expectedValue' => 'existing'], + 'value, filled' => ['value' => 'existing', 'default' => null, 'old' => 'old', 'expectedValue' => 'old'], + 'value, empty' => ['value' => 'existing', 'default' => null, 'old' => null, 'expectedValue' => null], + + 'no value, default, missing' => ['value' => null, 'default' => 'default', 'old' => self::MISSING, 'expectedValue' => 'default'], + 'no value, default, filled' => ['value' => null, 'default' => 'default', 'old' => 'old', 'expectedValue' => 'old'], + 'no value, default, empty' => ['value' => null, 'default' => 'default', 'old' => null, 'expectedValue' => null], + + 'value, default, missing' => ['value' => 'existing', 'default' => 'default', 'old' => self::MISSING, 'expectedValue' => 'existing'], + 'value, default, filled' => ['value' => 'existing', 'default' => 'default', 'old' => 'old', 'expectedValue' => 'old'], + 'value, default, empty' => ['value' => 'existing', 'default' => 'default', 'old' => null, 'expectedValue' => null], + ]; + } + + /** + * @test + * @dataProvider renderToggleProvider + */ + public function renders_toggles($value, $default, $old, $expected) + { + $rendered = $this->createField('toggle', $value, $default, $old); + + $this->assertSame($expected, (bool) $rendered['value']); + + if ($expected) { + $this->assertStringContainsString('checked', $rendered['field']); + } else { + $this->assertStringNotContainsString('checked', $rendered['field']); + } + } + + public function renderToggleProvider() + { + return [ + 'no value, missing' => ['value' => null, 'default' => null, 'old' => self::MISSING, 'expectedValue' => false], + 'no value, checked' => ['value' => null, 'default' => null, 'old' => '1', 'expectedValue' => true], + 'no value, unchecked' => ['value' => null, 'default' => null, 'old' => '0', 'expectedValue' => false], + + 'value true, missing' => ['value' => true, 'default' => null, 'old' => self::MISSING, 'expectedValue' => true], + 'value true, checked' => ['value' => true, 'default' => null, 'old' => '1', 'expectedValue' => true], + 'value true, unchecked' => ['value' => true, 'default' => null, 'old' => '0', 'expectedValue' => false], + + 'value false, missing' => ['value' => false, 'default' => null, 'old' => self::MISSING, 'expectedValue' => false], + 'value false, checked' => ['value' => false, 'default' => null, 'old' => '1', 'expectedValue' => true], + 'value false, unchecked' => ['value' => false, 'default' => null, 'old' => '0', 'expectedValue' => false], + + 'no value, default true, missing' => ['value' => null, 'default' => true, 'old' => self::MISSING, 'expectedValue' => true], + 'no value, default true, checked' => ['value' => null, 'default' => true, 'old' => '1', 'expectedValue' => true], + 'no value, default true, unchecked' => ['value' => null, 'default' => true, 'old' => '0', 'expectedValue' => false], + + 'no value, default false, missing' => ['value' => null, 'default' => false, 'old' => self::MISSING, 'expectedValue' => false], + 'no value, default false, checked' => ['value' => null, 'default' => false, 'old' => '1', 'expectedValue' => true], + 'no value, default false, unchecked' => ['value' => null, 'default' => false, 'old' => '0', 'expectedValue' => false], + + 'value true, default true, missing' => ['value' => true, 'default' => true, 'old' => self::MISSING, 'expectedValue' => true], + 'value true, default true, checked' => ['value' => true, 'default' => true, 'old' => '1', 'expectedValue' => true], + 'value true, default true, unchecked' => ['value' => true, 'default' => true, 'old' => '0', 'expectedValue' => false], + + 'value true, default false, missing' => ['value' => true, 'default' => false, 'old' => self::MISSING, 'expectedValue' => true], + 'value true, default false, checked' => ['value' => true, 'default' => false, 'old' => '1', 'expectedValue' => true], + 'value true, default false, unchecked' => ['value' => true, 'default' => false, 'old' => '0', 'expectedValue' => false], + + 'value false, default true, missing' => ['value' => false, 'default' => true, 'old' => self::MISSING, 'expectedValue' => false], + 'value false, default true, checked' => ['value' => false, 'default' => true, 'old' => '1', 'expectedValue' => true], + 'value false, default true, unchecked' => ['value' => false, 'default' => true, 'old' => '0', 'expectedValue' => false], + ]; + } + + /** + * @test + * @dataProvider renderSingleSelectProvider + */ + public function renders_single_select_fields($value, $default, $old, $expected) + { + $rendered = $this->createField('select', $value, $default, $old, [ + 'options' => $options = [ + 'alfa' => 'Alfa', + 'bravo' => 'Bravo', + 'charlie' => 'Charlie', + ], + ]); + + $this->assertStringContainsString('name="test"', $rendered['field']); + $this->assertStringNotContainsString('multiple', $rendered['field']); + + if ($expected) { + $unexpected = array_keys(Arr::except($options, $expected)); + $this->assertStringContainsString('value="'.$expected.'" selected', $rendered['field']); + foreach ($unexpected as $e) { + $this->assertStringNotContainsString('value="'.$e.'" selected', $rendered['field']); + } + } else { + $this->assertStringNotContainsString('selected', $rendered['field']); + } + } + + /** + * @test + * @dataProvider renderSingleSelectProvider + */ + public function renders_radio_fields($value, $default, $old, $expected) + { + $rendered = $this->createField('radio', $value, $default, $old, [ + 'options' => $options = [ + 'alfa' => 'Alfa', + 'bravo' => 'Bravo', + 'charlie' => 'Charlie', + ], + ]); + + if ($expected) { + $unexpected = array_keys(Arr::except($options, $expected)); + $this->assertTrue( + (bool) preg_match('/value="'.$expected.'"\s+checked/', $rendered['field']), + 'The "'.$expected.'" radio button was not checked within '.$rendered['field'], + ); + foreach ($unexpected as $e) { + $this->assertFalse( + (bool) preg_match('/value="'.$e.'"\s+checked/', $rendered['field']), + 'The "'.$expected.'" radio button was checked within '.$rendered['field'], + ); + } + } else { + $this->assertStringNotContainsString('checked', $rendered['field'], 'No radio button should be checked within '.$rendered['field']); + } + } + + public function renderSingleSelectProvider() + { + return [ + 'no value, no default, missing' => ['value' => null, 'default' => null, 'old' => self::MISSING, 'expectedValue' => null], + 'no value, no default, selected' => ['value' => null, 'default' => null, 'old' => 'bravo', 'expectedValue' => 'bravo'], + + 'value, no default, missing' => ['value' => 'alfa', 'default' => null, 'old' => self::MISSING, 'expectedValue' => 'alfa'], + 'value, no default, selected' => ['value' => 'alfa', 'default' => null, 'old' => 'bravo', 'expectedValue' => 'bravo'], + + 'no value, default, missing' => ['value' => null, 'default' => 'alfa', 'old' => self::MISSING, 'expectedValue' => 'alfa'], + 'no value, default, selected' => ['value' => null, 'default' => 'alfa', 'old' => 'bravo', 'expectedValue' => 'bravo'], + + 'value, default, missing' => ['value' => 'alfa', 'default' => 'bravo', 'old' => self::MISSING, 'expectedValue' => 'alfa'], + 'value, default, selected' => ['value' => 'alfa', 'default' => 'bravo', 'old' => 'charlie', 'expectedValue' => 'charlie'], + ]; + } + + /** + * @test + * @dataProvider renderMultipleSelectProvider + */ + public function renders_multiple_select_fields($value, $default, $old, $expected) + { + $rendered = $this->createField('select', $value, $default, $old, [ + 'multiple' => true, + 'options' => $options = [ + 'alfa' => 'Alfa', + 'bravo' => 'Bravo', + 'charlie' => 'Charlie', + 'delta' => 'Delta', + ], + ]); + + $this->assertStringContainsString('name="test[]"', $rendered['field']); + $this->assertStringContainsString('multiple', $rendered['field']); + + if ($expected) { + $unexpected = array_diff(array_keys($options), $expected); + foreach ($expected as $e) { + $this->assertStringContainsString('value="'.$e.'" selected', $rendered['field']); + } + foreach ($unexpected as $e) { + $this->assertStringNotContainsString('value="'.$e.'" selected', $rendered['field']); + } + } else { + $this->assertStringNotContainsString('selected', $rendered['field']); + } + } + + /** + * @test + * @dataProvider renderMultipleSelectProvider + */ + public function renders_checkboxes_fields($value, $default, $old, $expected) + { + $rendered = $this->createField('checkboxes', $value, $default, $old, [ + 'options' => $options = [ + 'alfa' => 'Alfa', + 'bravo' => 'Bravo', + 'charlie' => 'Charlie', + 'delta' => 'Delta', + ], + ]); + + if ($expected) { + $unexpected = array_diff(array_keys($options), $expected); + foreach ($expected as $e) { + $this->assertTrue( + (bool) preg_match('/value="'.$e.'"\s+checked/', $rendered['field']), + 'The "'.$e.'" box was not checked within '.$rendered['field'], + ); + } + foreach ($unexpected as $e) { + $this->assertFalse( + (bool) preg_match('/value="'.$e.'"\s+checked/', $rendered['field']), + 'The "'.$e.'" box was checked within '.$rendered['field'], + ); + } + } else { + $this->assertStringNotContainsString('checked', $rendered['field'], 'No boxes should be checked within '.$rendered['field']); + } + } + + public function renderMultipleSelectProvider() + { + return [ + 'no value, no default, missing' => ['value' => null, 'default' => null, 'old' => self::MISSING, 'expectedValue' => null], + 'no value, no default, selected' => ['value' => null, 'default' => null, 'old' => ['alfa'], 'expectedValue' => ['alfa']], + 'no value, no default, selected multiple' => ['value' => null, 'default' => null, 'old' => ['alfa', 'bravo'], 'expectedValue' => ['alfa', 'bravo']], + + 'value, no default, missing' => ['value' => ['alfa'], 'default' => null, 'old' => self::MISSING, 'expectedValue' => ['alfa']], + 'value, no default, selected' => ['value' => ['alfa'], 'default' => null, 'old' => ['bravo'], 'expectedValue' => ['bravo']], + 'value, no default, selected multiple' => ['value' => ['alfa'], 'default' => null, 'old' => ['bravo', 'charlie'], 'expectedValue' => ['bravo', 'charlie']], + + 'no value, default, missing' => ['value' => null, 'default' => ['alfa'], 'old' => self::MISSING, 'expectedValue' => ['alfa']], + 'no value, default, selected' => ['value' => null, 'default' => ['alfa'], 'old' => ['bravo'], 'expectedValue' => ['bravo']], + 'no value, default, selected multiple' => ['value' => null, 'default' => ['alfa'], 'old' => ['bravo', 'charlie'], 'expectedValue' => ['bravo', 'charlie']], + + 'value, default, missing' => ['value' => ['alfa'], 'default' => ['bravo'], 'old' => self::MISSING, 'expectedValue' => ['alfa']], + 'value, default, selected' => ['value' => ['alfa'], 'default' => ['bravo'], 'old' => ['charlie'], 'expectedValue' => ['charlie']], + 'value, default, selected multiple' => ['value' => ['alfa'], 'default' => ['bravo'], 'old' => ['charlie', 'delta'], 'expectedValue' => ['charlie', 'delta']], + ]; + } } class FakeTagWithRendersForms extends Tags