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())
-
-
+
+ @if($collection && $collection->isNotEmpty())
@foreach($collection as $item)
-
@if($selected && $selected->getKey() === $item->getKey())
{{ $item->$labelColumn }}
@@ -28,8 +27,15 @@
@endif
@endforeach
+ @endif
-
- @endif
+ @if($canCreate && $search)
+ -
+ Create {{ $search }}
+
+ @endif
+
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');
+ }
+};