diff --git a/resources/views/livewire/combobox.blade.php b/resources/views/livewire/combobox.blade.php index 69830f7..42305cd 100644 --- a/resources/views/livewire/combobox.blade.php +++ b/resources/views/livewire/combobox.blade.php @@ -8,13 +8,12 @@ @focus="open = true" @click="open = true" wire:model.debounce.800ms="search" wire:clear role="combobox" aria-controls="options" aria-expanded="false"> - @if($collection && $collection->isNotEmpty()) - diff --git a/src/Components/Combobox.php b/src/Components/Combobox.php index b9b0f8a..168f3b2 100644 --- a/src/Components/Combobox.php +++ b/src/Components/Combobox.php @@ -11,15 +11,19 @@ abstract class Combobox extends Component { - /** @var class-string */ + /** @var class-string The model to be used. */ public string $model; + /** The selected model. */ public ?Model $selected = null; + /** The name of the combobox. */ public string $name = 'combobox'; + /** The label of the combobox. */ public string $label = 'Combobox'; + /** The placeholder of the combobox. */ public string $placeholder = 'Select an option'; /** The search query */ @@ -28,68 +32,43 @@ abstract class Combobox extends Component /** If set to false, will not keep the selection, useful if you want to make a list. */ public bool $keepSelection = true; - /** - * The initial selection of the combobox. - */ + /** The initial selection of the combobox. */ public ?Model $init = null; - /** - * The columns that should be obtained. - * - * @var string[] - */ + /** The columns that should be obtained. */ public array $columns = ['*']; - /** - * The column to be shown as the label. - */ + /** The column to be shown as the label. */ public string $labelColumn = 'name'; - /** - * The columns that should be searched on. - * - * @var string[] - */ + /** The columns that should be searched on.*/ public array $searchColumns = ['name']; - /** - * The columns and the order that should be obtained. - * - * @var string[] - */ + /** If the combobox should be able to create new models. */ + public bool $canCreate = false; + + /** The columns and the order that should be obtained. */ public array $sortColumns = [ 'id' => 'asc', ]; - /** - * The quantity of results to be shown. - */ + /** The quantity of results to be shown. */ public int $limit = 10; - /** - * If the result has only one result, it will be automatically selected. - */ + /** If the result has only one result, it will be automatically selected. */ public bool $selectOnlyResult = true; - /** - * The properties that should be reset once a selection is made. - */ - protected array $resets = [ - // - ]; - /** * Mount the component. */ public function mount(): void { - if (!$this->label) { - $this->label = Str::headline($this->name); - } + $this->label ??= Str::headline($this->name); if ($this->init && !$this->selected && !$this->search && $this->keepSelection) { if (!$this->init instanceof $this->model) { return; } + $this->selectModel($this->init, true); } } @@ -107,12 +86,22 @@ public function render(): View { * Select the given model. */ public function select(mixed $id, bool $silent = false): void { - $model = $this->model::query()->find($id, $this->columns); - if ($model) { + if ($model = $this->model::query()->find($id, $this->columns)) { $this->selectModel($model, $silent); } } + /** + * Create a brand-new model with the given label. + */ + public function create(): void { + $model = $this->model::create([ + $this->labelColumn => $this->search, + ]); + + $this->selectModel(model: $model->fresh()); + } + /** * Get the collection of results. */ @@ -163,15 +152,12 @@ protected function selectModel(mixed $model, bool $silent = false): void { if ($this->keepSelection) { $this->selected = $model; $this->search = $model->{$this->labelColumn}; + } else { + $this->reset(['selected', 'search']); } if (!$silent) { $this->emitUp("selected-$this->name", $model); - - // Prevent the component from resetting all the properties if array is empty - if (!empty($this->resets)) { - $this->reset($this->resets); - } } } diff --git a/tests/AllowCreationComboboxTest.php b/tests/AllowCreationComboboxTest.php new file mode 100644 index 0000000..44e2aa2 --- /dev/null +++ b/tests/AllowCreationComboboxTest.php @@ -0,0 +1,41 @@ +count(3)->create(); + + $component = Livewire::test(AllowCreationCombobox::class, [ + 'name' => 'posts', + 'label' => 'Posts', + ])->set('search', 'Another Post'); + + $component->assertSuccessful() + ->assertSee('Posts') + ->assertSee('Create Another Post', false) + ->assertNotEmitted('selected-posts') + ->assertNotEmitted('cleared-posts'); + + $this->assertDatabaseCount('posts', 3); + + $component->call('create'); + + $post = Post::query()->where('title', 'Another Post')->first(); + + $component->assertEmitted('selected-posts') + ->assertNotEmitted('cleared-posts') + ->assertSet('selected', $post); + + $this->assertDatabaseCount('posts', 4); + + $this->assertDatabaseHas('posts', [ + 'title' => 'Another Post', + ]); + } +} diff --git a/tests/BasicComboboxTest.php b/tests/BasicComboboxTest.php index 096b837..f8f77f4 100644 --- a/tests/BasicComboboxTest.php +++ b/tests/BasicComboboxTest.php @@ -5,6 +5,7 @@ use Livewire\Livewire; use SertxuDeveloper\LivewireCombobox\Tests\Components\BasicCombobox; use SertxuDeveloper\LivewireCombobox\Tests\Database\Seeders\UserSeeder; +use SertxuDeveloper\LivewireCombobox\Tests\Models\Post; use SertxuDeveloper\LivewireCombobox\Tests\Models\User; class BasicComboboxTest extends TestCase @@ -12,7 +13,7 @@ class BasicComboboxTest extends TestCase /** * Check if the component is rendered correctly. */ - public function test_the_component_can_render(): void { + public function test_can_render(): void { $this->seed(UserSeeder::class); $component = Livewire::test(BasicCombobox::class, [ @@ -32,7 +33,7 @@ public function test_the_component_can_render(): void { /** * Check if the component is rendered the available options correctly. */ - public function test_the_component_can_render_available_results(): void { + public function test_can_render_available_results(): void { $this->seed(UserSeeder::class); $component = Livewire::test(BasicCombobox::class, [ @@ -56,7 +57,7 @@ public function test_the_component_can_render_available_results(): void { /** * Check if the component can select automatically the only option. */ - public function test_the_component_can_select_automatically_the_only_option(): void { + public function test_can_select_automatically_the_only_option(): void { $this->seed(UserSeeder::class); $component = Livewire::test(BasicCombobox::class, [ @@ -107,7 +108,7 @@ public function test_an_option_can_be_selected(): void { /** * Check the selected option is cleared when the search has changed. */ - public function test_the_selected_option_is_cleared_when_the_search_has_changed(): void { + public function test_selected_option_is_cleared_when_the_search_has_changed(): void { $this->seed(UserSeeder::class); $user = User::query()->where('email', 'user_a@example.com')->first(); @@ -146,7 +147,7 @@ public function test_the_selected_option_is_cleared_when_the_search_has_changed( /** * Check the selected option is cleared when the search has been cleared. */ - public function test_the_selected_option_is_cleared_when_the_search_has_been_cleared(): void { + public function test_selected_option_is_cleared_when_the_search_has_been_cleared(): void { $this->seed(UserSeeder::class); $user = User::query()->where('email', 'user_a@example.com')->first(); @@ -181,4 +182,69 @@ public function test_the_selected_option_is_cleared_when_the_search_has_been_cle $component->assertNotEmitted('selected-users'); $component->assertEmittedUp('cleared-users'); } + + public function test_can_render_with_a_custom_search_placeholder(): void { + $this->seed(UserSeeder::class); + + $component = Livewire::test(BasicCombobox::class, [ + 'name' => 'users', + 'label' => 'Users', + 'placeholder' => 'Search users...', + ]); + + $component->assertSuccessful(); + $component->assertSee('Users'); + $component->assertSee('Search users...'); + } + + public function test_can_be_mounted_with_an_initial_model(): void { + $this->seed(UserSeeder::class); + $user = User::query()->where('email', 'user_b@example.com')->first(); + + $component = Livewire::test(BasicCombobox::class, [ + 'name' => 'users', + 'label' => 'Users', + 'init' => $user, + ]); + + $component->assertSuccessful(); + $component->assertSee('Users'); + $component->assertSee('User B'); + $component->assertDontSee('User A'); + $component->assertDontSee('User C'); + $component->assertDontSee('Other'); + + $component->assertEmittedUp('selected-users'); + $component->assertNotEmitted('cleared-users'); + + $component = $component->set('search', 'User'); + + $component->assertSuccessful(); + $component->assertSee('Users'); + $component->assertSee('User A'); + $component->assertSee('User B'); + $component->assertSee('User C'); + $component->assertDontSee('Other'); + + $component->assertNotEmitted('selected-users'); + $component->assertEmittedUp('cleared-users'); + } + + public function test_initial_value_ignored_if_not_an_instance_of_search_model(): void { + $this->seed(UserSeeder::class); + $post = Post::factory()->create(); + + $component = Livewire::test(BasicCombobox::class, [ + 'name' => 'users', + 'label' => 'Users', + 'init' => $post, + ]); + + $component->assertSuccessful(); + $component->assertSee('Users'); + $component->assertSee('Select an option'); + + $component->assertNotEmitted('selected-users'); + $component->assertNotEmitted('cleared-users'); + } } diff --git a/tests/Components/AllowCreationCombobox.php b/tests/Components/AllowCreationCombobox.php new file mode 100644 index 0000000..4024b2a --- /dev/null +++ b/tests/Components/AllowCreationCombobox.php @@ -0,0 +1,19 @@ + The model to be used. */ + public string $model = Post::class; + + /** If the combobox should be able to create new models. */ + public bool $canCreate = true; + + /** The column to be shown as the label. */ + public string $labelColumn = 'title'; +} diff --git a/tests/Components/BasicCombobox.php b/tests/Components/BasicCombobox.php index be17df2..c2e5be4 100644 --- a/tests/Components/BasicCombobox.php +++ b/tests/Components/BasicCombobox.php @@ -8,6 +8,6 @@ class BasicCombobox extends Combobox { - /** @var class-string */ + /** @var class-string The model to be used. */ public string $model = User::class; } diff --git a/tests/Components/DontKeepSelectionCombobox.php b/tests/Components/DontKeepSelectionCombobox.php new file mode 100644 index 0000000..b45e62f --- /dev/null +++ b/tests/Components/DontKeepSelectionCombobox.php @@ -0,0 +1,16 @@ + The model to be used. */ + public string $model = User::class; + + /** If set to false, will not keep the selection, useful if you want to make a list. */ + public bool $keepSelection = false; +} diff --git a/tests/DontKeepSelectionComboboxTest.php b/tests/DontKeepSelectionComboboxTest.php new file mode 100644 index 0000000..ff77743 --- /dev/null +++ b/tests/DontKeepSelectionComboboxTest.php @@ -0,0 +1,35 @@ +seed(UserSeeder::class); + + $component = Livewire::test(DontKeepSelectionCombobox::class, [ + 'name' => 'users', + 'label' => 'Users', + 'placeholder' => 'Select a user', + ])->set('search', 'User'); + + $component->assertSuccessful() + ->assertSee('Users') + ->assertSee('Select a user') + ->assertSee('User A') + ->assertSee('User B') + ->assertSee('User C') + ->assertDontSee('Other') + ->assertNotEmitted('selected-users') + ->assertNotEmitted('cleared-users'); + + $component->call('select', 1) + ->assertEmittedUp('selected-users') + ->assertNotEmitted('cleared-users') + ->assertSet('selected', null); + } +} diff --git a/tests/Models/Post.php b/tests/Models/Post.php new file mode 100644 index 0000000..b5ba8c5 --- /dev/null +++ b/tests/Models/Post.php @@ -0,0 +1,27 @@ +|bool + */ + protected $guarded = []; + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): Factory { + return new PostFactory; + } +} diff --git a/tests/database/factories/PostFactory.php b/tests/database/factories/PostFactory.php new file mode 100644 index 0000000..84bea23 --- /dev/null +++ b/tests/database/factories/PostFactory.php @@ -0,0 +1,29 @@ + + */ + protected $model = Post::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array { + return [ + 'title' => $this->faker->sentence, + 'content' => $this->faker->paragraph, + ]; + } +} diff --git a/tests/database/migrations/2023_05_08_1515400_create_posts_table.php b/tests/database/migrations/2023_05_08_1515400_create_posts_table.php new file mode 100644 index 0000000..b3a97d8 --- /dev/null +++ b/tests/database/migrations/2023_05_08_1515400_create_posts_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('title'); + $table->text('content')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + Schema::dropIfExists('posts'); + } +};