diff --git a/config/config.php b/config/config.php index 455df96..58bb87e 100644 --- a/config/config.php +++ b/config/config.php @@ -15,7 +15,7 @@ 'field' => 'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm block mt-1 w-full', 'form_error' => 'mb-4 font-bold text-white bg-red-800 p-3 rounded-md', 'group' => 'mb-4', - 'label' => 'block font-medium text-sm text-gray-700', + 'label' => 'block font-medium text-sm text-gray-700 dark:text-white', 'required' => 'text-red-800', ], ]; diff --git a/resources/views/components/field-errors.blade.php b/resources/views/components/field-errors.blade.php index 12bb417..82e86bc 100644 --- a/resources/views/components/field-errors.blade.php +++ b/resources/views/components/field-errors.blade.php @@ -1,4 +1,4 @@ -
+
@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
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); + } +}