diff --git a/resources/views/extend/forms/fields/checkboxes.antlers.html b/resources/views/extend/forms/fields/checkboxes.antlers.html
index 5864bdb2893..909a9e76ccf 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 dc01262b2af..44c28b9ba00 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 4fa8cc2f022..b093f6a0264 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 5b7803cffb0..44c28b9ba00 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 e73417ce506..943bb307e2e 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 50a5fc470e5..345191628d5 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 fc17195ee0f..0354ee8c9cc 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