@if (isset($errors))
{{ $errors->first($field->name) }}
@endif
diff --git a/resources/views/components/field-group.blade.php b/resources/views/components/field-group.blade.php
index a160ed2..1f7b076 100644
--- a/resources/views/components/field-group.blade.php
+++ b/resources/views/components/field-group.blade.php
@@ -1,7 +1,19 @@
+ @if ($field->multiple)
+
+
+ @endif
+
{{ $slot }}
+
+ @if ($field->multiple)
+
+
+
+
+ @endif
diff --git a/resources/views/components/input.blade.php b/resources/views/components/input.blade.php
index c6f1954..ab700b9 100644
--- a/resources/views/components/input.blade.php
+++ b/resources/views/components/input.blade.php
@@ -1,3 +1,3 @@
-
+
diff --git a/src/Components/BaseComponent.php b/src/Components/BaseComponent.php
index abd88fc..9e4eb36 100644
--- a/src/Components/BaseComponent.php
+++ b/src/Components/BaseComponent.php
@@ -37,4 +37,16 @@ public function render()
return view($this->viewName(), Formulate::applyComponentMiddleware($this, $data))->render();
};
}
+
+ public function shouldRenderComponent()
+ {
+ return true;
+ }
+
+ public function shouldRender()
+ {
+ $componentName = class_basename($this);
+
+ return Formulate::applyMiddleware($this->shouldRenderComponent(), 'shouldRender' . $componentName);
+ }
}
diff --git a/src/Components/FieldErrorComponent.php b/src/Components/FieldErrorComponent.php
index d5883a5..917fe1b 100644
--- a/src/Components/FieldErrorComponent.php
+++ b/src/Components/FieldErrorComponent.php
@@ -6,7 +6,7 @@
use Illuminate\Support\ViewErrorBag;
use Illuminate\View\Component;
-class FieldErrorComponent extends Component
+class FieldErrorComponent extends BaseComponent
{
public function __construct(public InputComponent $field)
{
@@ -17,9 +17,9 @@ public function __construct(public InputComponent $field)
*
* @return \Illuminate\View\View|\Closure|string
*/
- public function render()
+ public function viewName()
{
- return view('formulate::components.field-errors');
+ return 'formulate::components.field-errors';
}
/**
@@ -27,7 +27,7 @@ public function render()
*
* @return bool
*/
- public function shouldRender()
+ public function shouldRenderComponent()
{
// get the errors that are being shared with the View
$errors = View::shared('errors', new ViewErrorBag());
diff --git a/src/Components/FormComponent.php b/src/Components/FormComponent.php
index 6047c72..4fe87a1 100644
--- a/src/Components/FormComponent.php
+++ b/src/Components/FormComponent.php
@@ -10,10 +10,13 @@
class FormComponent extends Component
{
+ public ?Route $routeDetails = null;
+
public function __construct(
public string $action = '',
public ?string $method = null,
public ?string $route = null,
+ public array $rules = [],
?array $routeParams = null,
array | Model $data = []
) {
@@ -27,15 +30,27 @@ public function __construct(
if (!empty($route)) {
// get the route details
- $routeDetails = new Route($route);
+ $this->routeDetails = new Route($route);
// we have a route, lets get the action from the url
- $this->action = $routeDetails->createRouteUrlWithPossibleDefaultBindings($routeParams, $data);
+ $this->action = $this->routeDetails->createRouteUrlWithPossibleDefaultBindings($routeParams, $data);
// if we don't already have a method
if (empty($this->method)) {
// use the first available method
- $this->method = $routeDetails->getDefaultHttpMethod();
+ $this->method = $this->routeDetails->getDefaultHttpMethod();
+ }
+
+ if (empty($rules)) {
+ $requestClassName = $this->routeDetails->getRequestClass();
+
+ if ($requestClassName) {
+ $requestClass = new $requestClassName;
+
+ if (method_exists($requestClass, 'rules')) {
+ $this->rules = $requestClass->rules();
+ }
+ }
}
}
}
diff --git a/src/Components/InputComponent.php b/src/Components/InputComponent.php
index 18988db..4d5de0f 100644
--- a/src/Components/InputComponent.php
+++ b/src/Components/InputComponent.php
@@ -35,10 +35,14 @@ public function __construct(
public string $type = 'text',
public mixed $value = null,
public bool $required = false,
+ public array $rules = [],
+ public bool $multiple = false
) {
// store an instance of this class as the field, this is passed to child components
$this->field = $this;
+ $this->form = Formulate::getForm();
+
// create instances of the necessary attribute bags
$this->groupAttributes = $this->newAttributeBag();
$this->labelAttributes = $this->newAttributeBag();
@@ -79,6 +83,14 @@ public function __construct(
// for all other fields, we just get the value from the service provider
$this->value = Formulate::getFieldValue($this->name, $value);
}
+
+ if (empty($this->rules) && !empty($this->form->rules) && array_key_exists($this->name, $this->form->rules)) {
+ $this->rules = $this->form->rules[$this->name];
+ }
+
+ if (!$this->required && in_array('required', $this->rules)) {
+ $this->required = true;
+ }
}
/**
diff --git a/src/Formulate.php b/src/Formulate.php
index 5ea5202..2ea23e2 100644
--- a/src/Formulate.php
+++ b/src/Formulate.php
@@ -35,7 +35,7 @@ class Formulate
* The current form
* @var FormComponent
*/
- protected FormComponent $form;
+ protected ?FormComponent $form = null;
/**
* A collection of fields that are used within the current form
@@ -122,6 +122,11 @@ public function populateFormData(array | Model $data)
$this->formData = $data;
}
+ public function getForm()
+ {
+ return $this->form;
+ }
+
/**
* Return the current form data
*
@@ -157,9 +162,9 @@ public function getFields()
* Get the current field
*
*/
- public function getCurrentField(): InputComponent
+ public function getCurrentField()
{
- return $this->currentField;
+ return $this->fields->where('name', $this->currentField)->first();
}
/**
@@ -280,7 +285,9 @@ public function applyMiddleware($passable, $method)
$pipeline = new Pipeline(app());
// we don't need all of the middleware to have each method, so we need to filter
- $filteredMiddleware = collect($this->middleware)->filter(function ($middleware) use ($method) {
+ $filteredMiddleware = collect($this->middleware)->filter(function ($middleware) {
+ return app($middleware)->shouldApply();
+ })->filter(function ($middleware) use ($method) {
return method_exists($middleware, $method);
})->toArray();
diff --git a/src/FormulateComponentAttributeBag.php b/src/FormulateComponentAttributeBag.php
index f4722e1..1f5a486 100644
--- a/src/FormulateComponentAttributeBag.php
+++ b/src/FormulateComponentAttributeBag.php
@@ -9,8 +9,12 @@ class FormulateComponentAttributeBag extends ComponentAttributeBag
{
public function set($attribute, $value)
{
- if (is_object($value)) {
- $value = Js::from($value, JSON_FORCE_OBJECT);
+ if (is_object($value) || is_array($value)) {
+ if (!count($value)) {
+ $value = Js::from($value, JSON_FORCE_OBJECT);
+ } else {
+ $value = Js::from($value);
+ }
}
$this->attributes[$attribute] = $value;
diff --git a/src/FormulateServiceProvider.php b/src/FormulateServiceProvider.php
index 3c3b3a2..090f4fe 100644
--- a/src/FormulateServiceProvider.php
+++ b/src/FormulateServiceProvider.php
@@ -5,6 +5,7 @@
use AppKit\Formulate\Facades\Formulate as FormulateFacade;
use AppKit\Formulate\Middleware\ApplyAlpineJsFormAttributes;
use AppKit\Formulate\Middleware\ApplyFormThemeClassesMiddleware;
+use AppKit\Formulate\Middleware\PrecognitionMiddleware;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
@@ -45,5 +46,6 @@ public function register()
FormulateFacade::registerMiddleware(ApplyFormThemeClassesMiddleware::class);
FormulateFacade::registerMiddleware(ApplyAlpineJsFormAttributes::class);
+ FormulateFacade::registerMiddleware(PrecognitionMiddleware::class);
}
}
diff --git a/src/Helpers/Routing/Route.php b/src/Helpers/Routing/Route.php
index 9c43141..54cdf26 100644
--- a/src/Helpers/Routing/Route.php
+++ b/src/Helpers/Routing/Route.php
@@ -6,6 +6,7 @@
use Illuminate\Routing\Route as RoutingRoute;
use Illuminate\Routing\Router;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Route as RouteFacade;
use ReflectionClass;
class Route
@@ -32,6 +33,13 @@ class Route
*/
protected Collection $params;
+ /**
+ * The middleware that are applied to the route
+ *
+ * @var array
+ */
+ protected array $middleware = [];
+
public function __construct(public string $routeName)
{
// create a collection to store the params
@@ -66,6 +74,9 @@ public function __construct(public string $routeName)
$parameter->isOptional(),
));
}
+
+ // gather the middleware that are applied to this route
+ $this->middleware = RouteFacade::gatherRouteMiddleware($this->route);
}
}
@@ -143,4 +154,15 @@ public function getDefaultHttpMethod()
{
return app('router')->getRoutes()->getByName($this->routeName)->methods[0];
}
+
+ public function supportPrecognition()
+ {
+ $precognitionClass = 'Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests';
+
+ if (class_exists($precognitionClass) && in_array($precognitionClass, $this->middleware)) {
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/src/Middleware/ApplyAlpineJsFormAttributes.php b/src/Middleware/ApplyAlpineJsFormAttributes.php
index 75bab6b..efbe0b7 100644
--- a/src/Middleware/ApplyAlpineJsFormAttributes.php
+++ b/src/Middleware/ApplyAlpineJsFormAttributes.php
@@ -5,8 +5,10 @@
use AppKit\Formulate\Facades\Formulate;
use AppKit\Formulate\FormulateComponentAttributeBag;
use Closure;
+use Illuminate\Support\Js;
+use Illuminate\Support\Str;
-class ApplyAlpineJsFormAttributes
+class ApplyAlpineJsFormAttributes extends BaseMiddleware
{
public function getFormComponentAttributes(FormulateComponentAttributeBag $attributes, Closure $next)
{
@@ -14,14 +16,30 @@ public function getFormComponentAttributes(FormulateComponentAttributeBag $attri
if ($attributes->has('x-data') && $attributes->get('x-data') === true) {
// generate the x-data
$data = Formulate::getFields()->mapWithKeys(function ($field) {
+ if ($field->multiple) {
+ return [$field->name => $this->field->value ?? ['']];
+ }
+
return [$field->name => $field->value ?? ''];
});
- // add in the x-data
$attributes->set('x-data', $data);
}
// pass onto the next middleware
return $next($attributes);
}
+
+ public function getInputComponentAttributes(FormulateComponentAttributeBag $attributes, Closure $next)
+ {
+ if ($this->form && $this->form->attributes->has('x-data') && !$attributes->has('x-model')) {
+ if ($this->field->multiple) {
+ $attributes->set('x-model', 'form.' . $this->field->name . '[index]');
+ } else {
+ $attributes->set('x-model', 'form.' . $this->field->name);
+ }
+ }
+
+ return $next($attributes);
+ }
}
diff --git a/src/Middleware/ApplyFormThemeClassesMiddleware.php b/src/Middleware/ApplyFormThemeClassesMiddleware.php
index 318a438..197566c 100644
--- a/src/Middleware/ApplyFormThemeClassesMiddleware.php
+++ b/src/Middleware/ApplyFormThemeClassesMiddleware.php
@@ -5,7 +5,7 @@
use AppKit\Formulate\FormulateComponentAttributeBag;
use Closure;
-class ApplyFormThemeClassesMiddleware
+class ApplyFormThemeClassesMiddleware extends BaseMiddleware
{
public function getInputComponentAttributes(FormulateComponentAttributeBag $attributes, Closure $next)
{
diff --git a/src/Middleware/BaseMiddleware.php b/src/Middleware/BaseMiddleware.php
new file mode 100644
index 0000000..61f2aa6
--- /dev/null
+++ b/src/Middleware/BaseMiddleware.php
@@ -0,0 +1,24 @@
+form = Formulate::getForm();
+ $this->field = Formulate::getCurrentField();
+ }
+
+ public function shouldApply()
+ {
+ return true;
+ }
+}
diff --git a/src/Middleware/PrecognitionMiddleware.php b/src/Middleware/PrecognitionMiddleware.php
new file mode 100644
index 0000000..8055106
--- /dev/null
+++ b/src/Middleware/PrecognitionMiddleware.php
@@ -0,0 +1,67 @@
+form && $this->form->routeDetails && $this->form->routeDetails->supportPrecognition() && $this->form->attributes->has('x-data');
+ }
+
+ public function getFormComponentAttributes(FormulateComponentAttributeBag $attributes, Closure $next)
+ {
+ $errors = View::shared('errors', new ViewErrorBag());
+
+ $precognitionXData = sprintf(
+ '{%s: $form(\'%s\', \'%s\', %s)%s}',
+ 'form',
+ $this->form->method,
+ $this->form->action,
+ $attributes->get('x-data'),
+ $errors->isEmpty() ? '' : '.setErrors(' . Js::from($errors->messages()) . ')'
+ );
+
+ $attributes->set('x-data', $precognitionXData);
+
+ // pass onto the next middleware
+ return $next($attributes);
+ }
+
+ public function getInputComponentAttributes(FormulateComponentAttributeBag $attributes, Closure $next)
+ {
+ if ($this->field->multiple) {
+ $attributes->set('@change', 'form.validate(\'' . $this->field->name . '.\' + index)');
+ $attributes->set(':aria-invalid', 'form.invalid(\'' . $this->field->name . '.\' + index)');
+ } else {
+ $attributes->set('@change', 'form.validate(\'' . $this->field->name . '\')');
+ $attributes->set(':aria-invalid', 'form.invalid(\'' . $this->field->name . '\')');
+ }
+
+ return $next($attributes);
+ }
+
+ public function shouldRenderFieldErrorComponent($value, Closure $next)
+ {
+ return $next(true);
+ }
+
+ public function getFieldErrorComponentAttributes(FormulateComponentAttributeBag $attributes, Closure $next)
+ {
+ if ($this->field->multiple) {
+ $attributes->set('x-show', 'form.invalid(\'' . $this->field->name . '.\' + index)');
+ $attributes->set('x-text', 'form.errors[\'' . $this->field->name . '.\' + index]');
+ } else {
+ $attributes->set('x-show', 'form.invalid(\'' . $this->field->name . '\')');
+ $attributes->set('x-text', 'form.errors.' . $this->field->name);
+ }
+
+ return $next($attributes);
+ }
+}