diff --git a/.gitignore b/.gitignore index c253173..eb49355 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ composer.lock vendor/ test.php + +.phpunit.result.cache + +.php-cs-fixer.cache diff --git a/composer.json b/composer.json index d1020e7..f760e57 100644 --- a/composer.json +++ b/composer.json @@ -1,31 +1,66 @@ { - "name": "kgrzelak/laravel-form", - "license": "MIT", - "authors": [ - { - "name": "Krzysztof Grzelak", - "email": "krzysztof@grzelak.dev", - "homepage": "https://grzelak.dev" + "name": "kgrzelak/laravel-form", + "license": "MIT", + "authors": [ + { + "name": "Krzysztof Grzelak", + "email": "krzysztof@grzelak.dev", + "homepage": "https://grzelak.dev" + } + ], + "require": { + "php": "^8.2", + "spatie/laravel-package-tools": "^1.16.1", + "laravel/framework": "^10.41|^11.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.2", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.1", + "friendsofphp/php-cs-fixer": "^3.59", + "phpstan/phpstan": "^1.11" + }, + "autoload": { + "psr-4": { + "Kgrzelak\\LaravelForm\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Kgrzelak\\LaravelForm\\Tests\\": "tests", + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/", + "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Kgrzelak\\LaravelForm\\LaravelFormServiceProvider" + ], + "aliases": { + "Form": "Kgrzelak\\LaravelForm\\LaravelForm" + } + } + }, + "scripts": { + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "build": "@php vendor/bin/testbench workbench:build --ansi", + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@build", + "@php vendor/bin/testbench serve" + ], + "lint": [ + "@php vendor/bin/phpstan analyse" + ], + "test": [ + "@php vendor/bin/phpunit" + ] } - ], - "require": { - "php": "^8.2", - "spatie/laravel-package-tools": "^1.16.1", - "laravel/framework": "^10.41|^11.0" - }, - "autoload": { - "psr-4": { - "Kgrzelak\\LaravelForm\\": "src" - } - }, - "extra": { - "laravel": { - "providers": [ - "Kgrzelak\\LaravelForm\\LaravelFormServiceProvider" - ], - "aliases": { - "Form": "Kgrzelak\\LaravelForm\\LaravelForm" - } - } - } } diff --git a/config/laravel-form.php b/config/laravel-form.php index 8ac3ea8..8099212 100644 --- a/config/laravel-form.php +++ b/config/laravel-form.php @@ -1,13 +1,21 @@ [ 'class' => 'form-control' ], 'select' => [ 'class' => 'form-select' - ] + ], -]; \ No newline at end of file + 'textarea' => [ + 'class' => 'form-control' + ], + + 'errors' => [ + 'enabled' => true, + 'element-class' => 'is-invalid', + 'html' => ':message:' + ] +]; diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..6b3835b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,12 @@ + + + + + ./tests + + + diff --git a/resources/views/_attributes.blade.php b/resources/views/_attributes.blade.php deleted file mode 100644 index b681226..0000000 --- a/resources/views/_attributes.blade.php +++ /dev/null @@ -1,3 +0,0 @@ -@foreach($attributes as $key => $value) -{{ $key }}="{{ $value }}" -@endforeach \ No newline at end of file diff --git a/resources/views/input.blade.php b/resources/views/input.blade.php deleted file mode 100644 index ed2e2aa..0000000 --- a/resources/views/input.blade.php +++ /dev/null @@ -1,9 +0,0 @@ - -@error($name) - - {{ $message }} - -@enderror diff --git a/resources/views/radio.blade.php b/resources/views/radio.blade.php deleted file mode 100644 index 2049bd3..0000000 --- a/resources/views/radio.blade.php +++ /dev/null @@ -1,9 +0,0 @@ - -@error($name) - - {{ $message }} - -@enderror \ No newline at end of file diff --git a/resources/views/select.blade.php b/resources/views/select.blade.php deleted file mode 100644 index b7b93d0..0000000 --- a/resources/views/select.blade.php +++ /dev/null @@ -1,10 +0,0 @@ - -@error($name) - - {{ $message }} - -@enderror \ No newline at end of file diff --git a/resources/views/textarea.blade.php b/resources/views/textarea.blade.php deleted file mode 100644 index 3b686e1..0000000 --- a/resources/views/textarea.blade.php +++ /dev/null @@ -1,6 +0,0 @@ - -@error($name) - - {{ $message }} - -@enderror \ No newline at end of file diff --git a/src/Attributes.php b/src/Attributes.php new file mode 100644 index 0000000..e253ced --- /dev/null +++ b/src/Attributes.php @@ -0,0 +1,104 @@ +attributes[$name]); + return $this; + } + + $this->attributes[$name] = $value; + + return $this; + } + + /** + * @description Get all attributes + * @return array + */ + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * @description Get attribute by name + * @param string $name + * @return string|null + */ + public function getAttribute(string $name): ?string + { + return $this->attributes[$name] ?? null; + } + + /** + * @description Add class to the attributes + * @param string $class + * @return Attributes + */ + public function addClass(string $class): static + { + $this->classes[] = $class; + + return $this; + } + + /** + * @description Set class to the attributes + * @param string $class + * @return static + */ + public function setClass(string $class): static + { + $this->classes = [$class]; + + return $this; + } + + /** + * @description Get all classes + * @return array + */ + public function getClass(): array + { + return $this->classes; + } + + /** + * @return string + */ + public function __toString(): string + { + $html = []; + + if ($this->classes) { + $this->attributes['class'] = implode(' ', $this->classes); + } + + foreach ($this->attributes as $name => $value) { + $html[$name] = sprintf('%s="%s"', $name, e($value)); + } + + return implode(' ', $html); + } +} diff --git a/src/BaseItem.php b/src/BaseItem.php new file mode 100644 index 0000000..90895be --- /dev/null +++ b/src/BaseItem.php @@ -0,0 +1,250 @@ +attributes = new Attributes(); + + $this->attributes->addClass(config('laravel-form.' . $this->viewName . '.class', 'form-control')); + $this->attributes->setAttribute('type', $this->type); + } + + /** + * @description Set the id attribute of the input + * @param string|int $id + * @return static + */ + public function id(string|int $id): static + { + $this->attributes->setAttribute('id', $id); + + return $this; + } + + /** + * @deprecated Use id() instead + * @param string $id + * @return static + */ + public function setId(string $id): static + { + $this->attributes->setAttribute('id', $id); + + return $this; + } + + /** + * @description Set the name attribute of the input + * @param string $name - Name of the input + * @return static + */ + public function name(string $name): static + { + $this->attributes->setAttribute('name', $name); + + return $this; + } + + /** + * @deprecated Use name() instead + * @param string $name - Name of the input + * @return $this + */ + public function setName(string $name): static + { + $this->attributes->setAttribute('name', $name); + + return $this; + } + + public function type(string $type): static + { + $this->attributes->setAttribute('type', $type); + + return $this; + } + + /** + * @description Set the value of the input + * @param mixed|null $value - Value of the input + * @return static + */ + public function value(mixed $value = null): static + { + if ($value instanceof BackedEnum) { + $value = $value->value; + } + + if (app('request')->old($this->attributes->getAttribute('name'))) { + $value = app('request')->old($this->attributes->getAttribute('name')); + } + + $this->attributes->setAttribute('value', $value); + + return $this; + } + + /** + * @deprecated Use value() instead + * @param mixed|null $value + * @return $this + */ + public function setValue(mixed $value = null): static + { + if ($value instanceof BackedEnum) { + $value = $value->value; + } + + if (app('request')->old($this->attributes->getAttribute('name'))) { + $value = app('request')->old($this->attributes->getAttribute('name')); + } + + $this->attributes->setAttribute('value', $value); + + return $this; + } + + /** + * @description Set the placeholder of the input + * @param string $placeholder + * @return static + */ + public function placeholder(string $placeholder): static + { + $this->attributes->setAttribute('placeholder', $placeholder); + + return $this; + } + + /** + * @description Set the required attribute of the input + * @param bool $required + * @return static + */ + public function required(bool $required): static + { + $this->attributes->setAttribute('required', $required); + + return $this; + } + + /** + * @description Set the class of the input + * @param string $class - Name of the class + * @return static + */ + public function setClass(string $class): static + { + $this->attributes->setClass($class); + + return $this; + } + + /** + * @description Add a class to the input + * @param string $class - Name of the class + * @return static + */ + public function addClass(string $class): static + { + $this->attributes->addClass($class); + + return $this; + } + + /** + * @description Set the attribute of the input + * @param string|int $name - Name of the attribute + * @param string|null $value - Value of the attribute + * @return static + */ + public function attribute(string|int $name, ?string $value = null): static + { + $this->attributes->setAttribute($name, $value); + + return $this; + } + + public function toHtml(): string + { + if ($this->hasError() && config('laravel-form.errors.enabled', false)) { + $this->attributes->addClass(config('laravel-form.errors.element-class', 'is-invalid')); + } + + return new HtmlString( + match ($this->viewName) { + 'input' => (new InputRender())->render($this->attributes), + 'textarea' => (new TextareaRender())->render($this->attributes), + 'select' => (new SelectRender($this->options))->render($this->attributes), + } . $this->getErrors() + ); + } + + public function __toString(): string + { + return $this->toHtml(); + } + + private function getError(): ?string + { + if (!$this->hasError()) { + return null; + } + + $errors = app('session')->get('errors', app(ViewErrorBag::class)); + + return $errors->getBag('default')->first($this->attributes->getAttribute('name')); + } + + private function hasError() + { + if (!$name = $this->attributes->getAttribute('name')) { + return null; + } + + $errors = app('session')->get('errors', app(ViewErrorBag::class)); + if (!$errors->getBag('default')->has($name)) { + return null; + } + + return $errors->getBag('default')->has($name); + } + + private function getErrors(): string + { + $errors = ''; + if (config('laravel-form.errors.enabled', false)) { + if ($error = $this->getError()) { + $errors = + Str::replace( + search: ':message:', + replace: $error, + subject: config('laravel-form.errors.html', ':message:') + ); + } + } + + return $errors; + } +} diff --git a/src/Items/Attributes.php b/src/Items/Attributes.php deleted file mode 100644 index 6f00aa7..0000000 --- a/src/Items/Attributes.php +++ /dev/null @@ -1,26 +0,0 @@ - $this->attributes - ])->render(); - } - - public function __toString() - { - return $this->toHtml(); - } -} \ No newline at end of file diff --git a/src/Items/BaseItem.php b/src/Items/BaseItem.php deleted file mode 100644 index ec11665..0000000 --- a/src/Items/BaseItem.php +++ /dev/null @@ -1,109 +0,0 @@ -attributes['id'] = $id; - - return $this; - } - - public function setName(string $name): static - { - $this->name = $name; - - return $this; - } - - public function setType(string $type): static - { - $this->type = $type; - - return $this; - } - - public function setValue(mixed $value = null): static - { - if ($value instanceof BackedEnum) { - $value = $value->value; - } - - $this->value = $value; - - return $this; - } - - public function setPlaceholder(string $placeholder): static - { - $this->placeholder = $placeholder; - - return $this; - } - - public function setAttributes(array $attributes): static - { - $this->attributes = $attributes; - - return $this; - } - - public function setRequired(bool $required): static - { - $this->required = $required; - - return $this; - } - - public function setClass(string $class): static - { - $this->class = $class; - - return $this; - } - - public function toHtml(): string - { - return view('laravel-form::' . $this->viewName, [ - 'name' => $this->name, - 'class' => $this->class, - 'type' => $this->type, - 'value' => $this->value, - 'placeholder' => $this->placeholder, - 'attributes' => new Attributes($this->attributes), - 'required' => $this->required, - 'options' => $this->options - ])->render(); - } - - public function __toString(): string - { - return $this->toHtml(); - } -} diff --git a/src/Items/FormInput.php b/src/Items/FormInput.php index 2bab771..91aa1d3 100644 --- a/src/Items/FormInput.php +++ b/src/Items/FormInput.php @@ -2,10 +2,16 @@ namespace Kgrzelak\LaravelForm\Items; +use Kgrzelak\LaravelForm\BaseItem; + class FormInput extends BaseItem { - public function __construct() + public string $viewName = 'input'; + + public ?string $type = 'text'; + + public function render(): string { - $this->class = config('laravel-form.input.class', 'form-input'); + return 'attributes . '>'; } -} \ No newline at end of file +} diff --git a/src/Items/FormSelect.php b/src/Items/FormSelect.php index 88456a3..49a2dee 100644 --- a/src/Items/FormSelect.php +++ b/src/Items/FormSelect.php @@ -2,13 +2,13 @@ namespace Kgrzelak\LaravelForm\Items; +use Kgrzelak\LaravelForm\BaseItem; + class FormSelect extends BaseItem { - public function __construct() - { - $this->class = config('laravel-form.select.class', 'form-select'); - $this->viewName = 'select'; - } + public string $viewName = 'select'; + + protected array $options = []; public function setOptions(array $options): static { @@ -23,4 +23,4 @@ public function addOption(string $value, string $label): static return $this; } -} \ No newline at end of file +} diff --git a/src/Items/FormTextarea.php b/src/Items/FormTextarea.php index 59081a1..6d726ac 100644 --- a/src/Items/FormTextarea.php +++ b/src/Items/FormTextarea.php @@ -2,11 +2,9 @@ namespace Kgrzelak\LaravelForm\Items; +use Kgrzelak\LaravelForm\BaseItem; + class FormTextarea extends BaseItem { - public function __construct () - { - $this->class = config('laravel-form.textarea.class', 'form-control'); - $this->viewName = 'textarea'; - } -} \ No newline at end of file + public string $viewName = 'textarea'; +} diff --git a/src/LaravelForm.php b/src/LaravelForm.php index ce2098b..73408ea 100644 --- a/src/LaravelForm.php +++ b/src/LaravelForm.php @@ -22,4 +22,4 @@ public static function textarea(): FormTextarea { return new FormTextarea(); } -} \ No newline at end of file +} diff --git a/src/LaravelFormServiceProvider.php b/src/LaravelFormServiceProvider.php index 91d63df..2839da8 100644 --- a/src/LaravelFormServiceProvider.php +++ b/src/LaravelFormServiceProvider.php @@ -13,4 +13,4 @@ public function configurePackage(Package $package): void ->hasConfigFile('laravel-form') ->hasViews('laravel-form'); } -} \ No newline at end of file +} diff --git a/src/Renders/InputRender.php b/src/Renders/InputRender.php new file mode 100644 index 0000000..8ecc1db --- /dev/null +++ b/src/Renders/InputRender.php @@ -0,0 +1,13 @@ +'; + } +} diff --git a/src/Renders/Render.php b/src/Renders/Render.php new file mode 100644 index 0000000..9236778 --- /dev/null +++ b/src/Renders/Render.php @@ -0,0 +1,10 @@ +getAttribute('value'); + $attributes->setAttribute('value'); + + $options = ''; + + return $options; + } +} diff --git a/src/Renders/TextareaRender.php b/src/Renders/TextareaRender.php new file mode 100644 index 0000000..ae53ae6 --- /dev/null +++ b/src/Renders/TextareaRender.php @@ -0,0 +1,16 @@ +getAttribute('value'); + $attributes->setAttribute('value', null); + + return ''; + } +} diff --git a/tests/AttributesTest.php b/tests/AttributesTest.php new file mode 100644 index 0000000..9f5a0ce --- /dev/null +++ b/tests/AttributesTest.php @@ -0,0 +1,50 @@ +setClass('test-class'); + + $this->assertEquals(['test-class'], $attributes->getClass()); + } + + public function testCanAddClassAttribute(): void + { + $attributes = new Attributes(); + $attributes->setClass('test-class'); + $attributes->addClass('another-class'); + + $this->assertEquals(['test-class', 'another-class'], $attributes->getClass()); + } + + public function testCanAddAttribute(): void + { + $attributes = new Attributes(); + $attributes->setAttribute('data-test', 'test'); + + $this->assertEquals(['data-test' => 'test'], $attributes->getAttributes()); + } + + public function testCanGetAttribute(): void + { + $attributes = new Attributes(); + $attributes->setAttribute('data-test', 'test'); + + $this->assertEquals('test', $attributes->getAttribute('data-test')); + } + + public function testCanRemoveAttribute(): void + { + $attributes = new Attributes(); + $attributes->setAttribute('data-test', 'test'); + $attributes->setAttribute('data-test'); + + $this->assertEquals([], $attributes->getAttributes()); + } +} diff --git a/tests/FormInputTest.php b/tests/FormInputTest.php new file mode 100644 index 0000000..cb9d455 --- /dev/null +++ b/tests/FormInputTest.php @@ -0,0 +1,85 @@ +assertEquals('', $item); + } + + public function testCanCreateInputWithName(): void + { + $item = LaravelForm::input()->name('test'); + + $this->assertEquals('', $item); + } + + public function testCanCreateInputWithId(): void + { + $item = LaravelForm::input()->id('test'); + + $this->assertEquals('', $item); + } + + public function testCanCreateInputWithErrors(): void + { + $this->setErrors(); + + $item = LaravelForm::input()->name('test'); + + $this->assertEquals( + expected: 'error', + actual: $item + ); + } + + public function testCanCreateInputWithNameAndId(): void + { + $item = LaravelForm::input()->name('test')->id('test'); + + $this->assertEquals('', $item); + } + + public function testCanCreateInputWithCustomAttribute(): void + { + $item = LaravelForm::input()->name('test')->attribute('data-test', 'test'); + + $this->assertEquals('', $item); + } + + public function testCanReplaceClassAttribute(): void + { + $item = LaravelForm::input()->name('test')->setClass('test'); + + $this->assertEquals('', $item); + } + + public function testErrorWorksWithCustomClassName(): void + { + $this->setErrors(); + + $item = LaravelForm::input()->name('test')->setClass('test'); + + $this->assertEquals( + expected: 'error', + actual: $item + ); + } + + public function testDontShowErrorsWhenDisabledInConfiguration(): void + { + $this->app['config']->set('laravel-form.errors.enabled', false); + + $this->setErrors(); + + $item = LaravelForm::input()->name('test'); + + $this->assertEquals('', $item); + } +} diff --git a/tests/FormSelectTest.php b/tests/FormSelectTest.php new file mode 100644 index 0000000..3060883 --- /dev/null +++ b/tests/FormSelectTest.php @@ -0,0 +1,37 @@ +name('test'); + + $this->assertEquals('', $select); + } + + public function testCanAddOptionToSelect(): void + { + $select = LaravelForm::select()->name('test')->addOption('test', 'Test'); + + $this->assertEquals('', $select); + } + + public function testCanAddMultipleOptionsToSelect(): void + { + $select = LaravelForm::select()->name('test')->addOption('test', 'Test')->addOption('test2', 'Test 2'); + + $this->assertEquals('', $select); + } + + public function testSelectedOptionWorks() + { + $select = LaravelForm::select()->name('test')->addOption('test', 'Test') + ->addOption('test2', 'Test 2')->value('test2')->toHtml(); + + $this->assertEquals('', $select); + } +} diff --git a/tests/FormTextareaTest.php b/tests/FormTextareaTest.php new file mode 100644 index 0000000..bfdd8df --- /dev/null +++ b/tests/FormTextareaTest.php @@ -0,0 +1,62 @@ +assertEquals('', $item); + } + + public function testCanCreateTextareaWithName(): void + { + $item = LaravelForm::textarea()->name('test'); + + $this->assertEquals('', $item); + } + + public function testCanCreateTextareaWithId(): void + { + $item = LaravelForm::textarea()->id('test'); + + $this->assertEquals('', $item); + } + + public function testCanCreateTextareaWithNameAndId(): void + { + $item = LaravelForm::textarea()->name('test')->id('test'); + + $this->assertEquals('', $item); + } + + public function testCanCreateTextareaWithCustomAttribute(): void + { + $item = LaravelForm::textarea()->name('test')->attribute('data-test', 'test'); + + $this->assertEquals('', $item); + } + + public function testCanCreateTextareaWithErrors(): void + { + $this->setErrors(); + + $item = LaravelForm::textarea()->name('test'); + + $this->assertEquals( + expected: 'error', + actual: $item + ); + } + + public function testCanCreateTextareaWithValue(): void + { + $item = LaravelForm::textarea()->value('test'); + + $this->assertEquals('', $item); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..70ee8bc --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,27 @@ +startSession(); + $this->withSession(['errors' => (new ViewErrorBag)->put('default', new MessageBag(['test' => 'error']))]); + } +}