Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Add recursive form:fields tag with support for group fieldtype #10976

Draft
wants to merge 13 commits into
base: 5.x
Choose a base branch
from
2 changes: 1 addition & 1 deletion resources/views/extend/forms/fields/assets.antlers.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<input
id="{{ id }}"
type="file"
name="{{ handle }}{{ if max_files !== 1 }}[]{{ /if }}"
name="{{ name }}{{ if max_files !== 1 }}[]{{ /if }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if max_files !== 1 }}multiple{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
Expand Down
4 changes: 2 additions & 2 deletions resources/views/extend/forms/fields/checkboxes.antlers.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<input type="hidden" name="{{ handle }}[]">
<input type="hidden" name="{{ name }}[]">
{{ foreach:options as="option|label" }}
<label>
<input
id="{{ id }}-{{ option | slugify }}-option"
type="checkbox"
name="{{ handle }}[]"
name="{{ name }}[]"
value="{{ option }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if value|in_array:option }}checked{{ /if }}
Expand Down
2 changes: 1 addition & 1 deletion resources/views/extend/forms/fields/default.antlers.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<input
id="{{ id }}"
type="{{ input_type ?? 'text' }}"
name="{{ handle }}"
name="{{ name }}"
value="{{ value }}"
{{ if placeholder }}placeholder="{{ placeholder }}"{{ /if }}
{{ if character_limit }}maxlength="{{ character_limit }}"{{ /if }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<select
id="{{ id }}"
name="{{ handle }}{{ multiple ?= "[]" }}"
name="{{ name }}{{ multiple ?= "[]" }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if multiple }}multiple{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
Expand Down
2 changes: 1 addition & 1 deletion resources/views/extend/forms/fields/files.antlers.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<input
id="{{ id }}"
type="file"
name="{{ handle }}{{ if max_files !== 1 }}[]{{ /if }}"
name="{{ name }}{{ if max_files !== 1 }}[]{{ /if }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if max_files !== 1 }}multiple{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
Expand Down
7 changes: 7 additions & 0 deletions resources/views/extend/forms/fields/group.antlers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div {{ if js_driver }}{{ js_attributes }}{{ /if }}>
{{ fields scope="__field" }}
<div class="pb-2">
{{ slot:addContext(__field) }}
</div>
{{ /fields }}
</div>
2 changes: 1 addition & 1 deletion resources/views/extend/forms/fields/integer.antlers.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<input
type="number"
name="{{ handle }}"
name="{{ name }}"
value="{{ value }}"
{{ if placeholder }}placeholder="{{ placeholder }}"{{ /if }}
{{ if character_limit }}maxlength="{{ character_limit }}"{{ /if }}
Expand Down
2 changes: 1 addition & 1 deletion resources/views/extend/forms/fields/radio.antlers.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<input
id="{{ id }}-{{ option | slugify }}-option"
type="radio"
name="{{ handle }}"
name="{{ name }}"
value="{{ option }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if value == option }}checked{{ /if }}
Expand Down
2 changes: 1 addition & 1 deletion resources/views/extend/forms/fields/select.antlers.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<select
id="{{ id }}"
name="{{ handle }}{{ multiple ?= "[]" }}"
name="{{ name }}{{ multiple ?= "[]" }}"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if multiple }}multiple{{ /if }}
{{ if validate|contains:required }}required{{ /if }}
Expand Down
2 changes: 1 addition & 1 deletion resources/views/extend/forms/fields/text.antlers.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<input
id="{{ id }}"
type="{{ input_type ?? 'text' }}"
name="{{ handle }}"
name="{{ name }}"
value="{{ value }}"
{{ if placeholder }}placeholder="{{ placeholder }}"{{ /if }}
{{ if character_limit }}maxlength="{{ character_limit }}"{{ /if }}
Expand Down
2 changes: 1 addition & 1 deletion resources/views/extend/forms/fields/textarea.antlers.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<textarea
id="{{ id }}"
name="{{ handle }}"
name="{{ name }}"
rows="5"
{{ if placeholder }}placeholder="{{ placeholder }}"{{ /if }}
{{ if character_limit }}maxlength="{{ character_limit }}"{{ /if }}
Expand Down
4 changes: 2 additions & 2 deletions resources/views/extend/forms/fields/toggle.antlers.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<label>
<input type="hidden" name="{{ handle }}" value="0">
<input type="hidden" name="{{ name }}" value="0">
<input
id="{{ id }}"
type="checkbox"
name="{{ handle }}"
name="{{ name }}"
value="1"
{{ if js_driver }}{{ js_attributes }}{{ /if }}
{{ if value && value !== '0' }}checked{{ /if }}
Expand Down
1 change: 1 addition & 0 deletions src/Fieldtypes/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Group extends Fieldtype
{
protected $categories = ['structured'];
protected $defaultable = false;
protected $selectableInForms = true;

protected function configFieldItems(): array
{
Expand Down
63 changes: 63 additions & 0 deletions src/Forms/RenderableField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace Statamic\Forms;

use Illuminate\Contracts\Support\Htmlable;

class RenderableField implements Htmlable
{
protected $slot;
protected $isBlade = false;

public function __construct(protected $field, protected $data)
{
//
}

public function slot(RenderableFieldSlot $slot): self
{
$this->slot = $slot;

collect($this->data['fields'] ?? [])
->each(fn ($field) => $field['field']->slot($slot));

return $this;
}

public function isBlade(bool $isBlade): self
{
$this->isBlade = $isBlade;

collect($this->data['fields'] ?? [])
->each(fn ($field) => $field['field']->isBlade($isBlade));

return $this;
}

public function toHtml(): string
{
$data = array_merge($this->data, [
'slot' => $this->slot,
]);

return $this->minifyFieldHtml(
view($this->field->fieldtype()->view(), $data)->render(),
);
}

public function __toString(): string
{
return $this->toHtml();
}

protected function minifyFieldHtml(string $html): string
{
// Leave whitespace around these html elements.
$ignoredHtmlElements = collect(['a', 'span'])->implode('|');

// Trim whitespace between all other html elements.
$html = preg_replace('/\s*(<(?!\/*('.$ignoredHtmlElements.'))[^>]+>)\s*/', '$1', $html);

return $html;
}
}
32 changes: 32 additions & 0 deletions src/Forms/RenderableFieldSlot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Statamic\Forms;

use Illuminate\Support\Facades\Blade;
use Statamic\Facades\Antlers;

class RenderableFieldSlot
{
protected $context;

public function __construct(protected $html, protected $isBlade)
{
//
}

public function addContext(array $context): self
{
$this->context = $context;

return $this;
}

public function __toString(): string
{
if ($this->isBlade) {
return Blade::render($this->html, ['field' => $this->context]);
}

return (string) Antlers::parse($this->html, $this->context);
}
}
22 changes: 22 additions & 0 deletions src/Forms/Tags.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use DebugBar\DataCollector\ConfigCollector;
use DebugBar\DebugBarException;
use Statamic\Contracts\Forms\Form as FormContract;
use Statamic\Facades\Antlers;
use Statamic\Facades\Blink;
use Statamic\Facades\Blueprint;
use Statamic\Facades\Form;
Expand Down Expand Up @@ -138,6 +139,27 @@ public function create()
return $html;
}

/**
* Maps to {{ form:fields }}.
*
* @return string
*/
public function fields()
{
$isBlade = $this->isAntlersBladeComponent();

$slot = new RenderableFieldSlot($this->content, $isBlade);

collect($this->context['fields'])
->each(fn ($field) => $field['field']->slot($slot)->isBlade($isBlade));

if ($isBlade) {
return $this->tagRenderer->render('@foreach($fields as $field)'.$this->content.'@endforeach', $this->context->all());
}

return Antlers::parse('{{ fields }}'.$this->content.'{{ /fields }}', $this->context->all());
}

/**
* Maps to {{ form:errors }}.
*
Expand Down
43 changes: 26 additions & 17 deletions src/Tags/Concerns/RendersForms.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Closure;
use Illuminate\Support\MessageBag;
use Statamic\Fields\Field;
use Statamic\Forms\RenderableField;
use Statamic\Support\Str;

trait RendersForms
Expand Down Expand Up @@ -142,46 +143,54 @@ protected function getRenderableField($field, $errorBag = 'default', $manipulate
->filter()->all();

$formHandle = $field->form()?->handle() ?? Str::slug($errorBag);

$data = array_merge($configDefaults, $field->toArray(), [
'handle' => $field->handle(),
'name' => $this->convertDottedHandleToInputName($field->handle()),
'id' => $this->generateFieldId($field->handle(), $formHandle),
'instructions' => $field->instructions(),
'error' => $errors->first($field->handle()) ?: null,
'default' => $field->value() ?? $field->defaultValue(),
'old' => old($field->handle()),
'old' => old($field->handle()), // TODO: Ensure dotted path for old input works here.
'value' => $value,
], $field->fieldtype()->extraRenderableFieldData());

if ($field->fieldtype()->handle() === 'group') {
$data['fields'] = collect($field->fieldtype()->fields()->all())
->map(fn ($child) => $child->setHandle($field->handle().'.'.$child->handle()))
->map(fn ($child) => $this->getRenderableField($child, $errorBag, $manipulateDataCallback))
->values()
->all();
}

if ($manipulateDataCallback instanceof Closure) {
$data = $manipulateDataCallback($data, $field);
}

$data['field'] = $this->minifyFieldHtml(view($field->fieldtype()->view(), $data)->render());
$data['field'] = new RenderableField($field, $data);

return $data;
}

/**
* Minify field html.
*
* @param string $html
* @return string
* Generate a field id to associate input with label.
*/
protected function minifyFieldHtml($html)
private function generateFieldId(string $fieldHandle, ?string $formName = null): string
{
// Leave whitespace around these html elements.
$ignoredHtmlElements = collect(['a', 'span'])->implode('|');

// Trim whitespace between all other html elements.
$html = preg_replace('/\s*(<(?!\/*('.$ignoredHtmlElements.'))[^>]+>)\s*/', '$1', $html);

return $html;
return ($formName ?? 'default').'-form-'.$fieldHandle.'-field';
}

/**
* Generate a field id to associate input with label.
* Convert dotted handle to input name that can be submitted as array value in form html.
*/
private function generateFieldId(string $fieldHandle, ?string $formName = null): string
protected function convertDottedHandleToInputName(string $handle): string
{
return ($formName ?? 'default').'-form-'.$fieldHandle.'-field';
$parts = collect(explode('.', $handle));

$first = $parts->pull(0);

return $first.$parts
->map(fn ($part) => '['.$part.']')
->join('');
}
}
5 changes: 5 additions & 0 deletions src/View/Antlers/Language/Runtime/RuntimeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ public function __construct(DocumentParser $documentParser, NodeProcessor $nodeP
$this->antlersParser = $antlersParser;
}

public function getCascade()
{
return $this->cascade;
}

/**
* Sets the RuntimeConfiguration instance.
*
Expand Down
Loading