Skip to content

Commit

Permalink
Allow creation of a new option on-the-fly (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
sertxudev authored May 9, 2023
1 parent d53f2a9 commit 39e52f8
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 58 deletions.
20 changes: 13 additions & 7 deletions resources/views/livewire/combobox.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -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())
<ul class="absolute z-21 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
x-show="open" x-cloak role="listbox">

<ul class="absolute z-21 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
x-show="open" x-cloak role="listbox">
@if($collection && $collection->isNotEmpty())
@foreach($collection as $item)
<li class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:bg-blue-50"
wire:click="select('{{ $item->getKey() }}')"
wire:click="select('{{ $item->getKey() }}')" @click="open = false"
role="option" tabindex="-1">
@if($selected && $selected->getKey() === $item->getKey())
<span class="block truncate font-medium">{{ $item->$labelColumn }}</span>
Expand All @@ -28,8 +27,15 @@
@endif
</li>
@endforeach
@endif

</ul>
@endif
@if($canCreate && $search)
<li class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:bg-blue-50"
wire:click="create" @click="open = false"
role="option" tabindex="-1">
<span class="block truncate">Create <b>{{ $search }}</b></span>
</li>
@endif
</ul>
</div>
</div>
76 changes: 31 additions & 45 deletions src/Components/Combobox.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@

abstract class Combobox extends Component
{
/** @var class-string<Model> */
/** @var class-string<Model> 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 */
Expand All @@ -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);
}
}
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
41 changes: 41 additions & 0 deletions tests/AllowCreationComboboxTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace SertxuDeveloper\LivewireCombobox\Tests;

use Livewire\Livewire;
use SertxuDeveloper\LivewireCombobox\Tests\Components\AllowCreationCombobox;
use SertxuDeveloper\LivewireCombobox\Tests\Models\Post;

class AllowCreationComboboxTest extends TestCase
{
public function test_can_create_new_models(): void {
Post::factory()->count(3)->create();

$component = Livewire::test(AllowCreationCombobox::class, [
'name' => 'posts',
'label' => 'Posts',
])->set('search', 'Another Post');

$component->assertSuccessful()
->assertSee('Posts')
->assertSee('Create <b>Another Post</b>', 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',
]);
}
}
76 changes: 71 additions & 5 deletions tests/BasicComboboxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
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
{
/**
* 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, [
Expand All @@ -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, [
Expand All @@ -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, [
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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');
}
}
19 changes: 19 additions & 0 deletions tests/Components/AllowCreationCombobox.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace SertxuDeveloper\LivewireCombobox\Tests\Components;

use Illuminate\Database\Eloquent\Model;
use SertxuDeveloper\LivewireCombobox\Components\Combobox;
use SertxuDeveloper\LivewireCombobox\Tests\Models\Post;

class AllowCreationCombobox extends Combobox
{
/** @var class-string<Model> 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';
}
2 changes: 1 addition & 1 deletion tests/Components/BasicCombobox.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@

class BasicCombobox extends Combobox
{
/** @var class-string<Model> */
/** @var class-string<Model> The model to be used. */
public string $model = User::class;
}
16 changes: 16 additions & 0 deletions tests/Components/DontKeepSelectionCombobox.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace SertxuDeveloper\LivewireCombobox\Tests\Components;

use Illuminate\Database\Eloquent\Model;
use SertxuDeveloper\LivewireCombobox\Components\Combobox;
use SertxuDeveloper\LivewireCombobox\Tests\Models\User;

class DontKeepSelectionCombobox extends Combobox
{
/** @var class-string<Model> 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;
}
Loading

0 comments on commit 39e52f8

Please sign in to comment.