From 65e16d20fbe64e3f59eda9f525566b6b397b76b3 Mon Sep 17 00:00:00 2001 From: Chris Page Date: Fri, 18 Oct 2024 15:12:05 +0100 Subject: [PATCH] Added the ability for TagsInput to utilise a BelongsToMany relationship --- .../forms/docs/03-fields/14-tags-input.md | 31 ++++++ packages/forms/src/Components/TagsInput.php | 99 +++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/packages/forms/docs/03-fields/14-tags-input.md b/packages/forms/docs/03-fields/14-tags-input.md index fe7e1aa92f..e685a4df05 100644 --- a/packages/forms/docs/03-fields/14-tags-input.md +++ b/packages/forms/docs/03-fields/14-tags-input.md @@ -34,6 +34,37 @@ class Post extends Model > Filament also supports [`spatie/laravel-tags`](https://github.com/spatie/laravel-tags). See our [plugin documentation](/plugins/filament-spatie-tags) for more information. +## Using a relationship + +If you're using a `BelongsToMany` relationship to store your tags, you can call the `relationship()` method to define the relationship: + +```php +use Filament\Forms\Components\TagsInput; + +TagsInput::make('tags') + ->relationship(), +``` + +By default, this will look for a method on the model that matches a snake cased version of the field name. +If your method name is different, you can pass the method name as an argument: + +```php +use Filament\Forms\Components\TagsInput; + +TagsInput::make('tags') + ->relationship(relationship: 'myRelationshipName'), +``` + +Likewise, by default it'll use the column `name` to determine the tag label to use. If you need to use a different +column, you can pass the column name as an argument: + +```php +use Filament\Forms\Components\TagsInput; + +TagsInput::make('tags') + ->relationship(column: 'a_different_column'), +``` + ## Comma-separated tags You may allow the tags to be stored in a separated string, instead of JSON. To set this up, pass the separating character to the `separator()` method: diff --git a/packages/forms/src/Components/TagsInput.php b/packages/forms/src/Components/TagsInput.php index 3919ad4361..5f293a83c2 100644 --- a/packages/forms/src/Components/TagsInput.php +++ b/packages/forms/src/Components/TagsInput.php @@ -7,6 +7,9 @@ use Filament\Support\Concerns\HasExtraAlpineAttributes; use Filament\Support\Concerns\HasReorderAnimationDuration; use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Support\Str; class TagsInput extends Field implements Contracts\HasAffixActions, Contracts\HasNestedRecursiveValidationRules { @@ -41,6 +44,10 @@ class TagsInput extends Field implements Contracts\HasAffixActions, Contracts\Ha protected string | Closure | null $tagSuffix = null; + protected string | Closure | null $relationship = null; + + protected string | Closure $relationshipColumn = 'name'; + protected function setUp(): void { parent::setUp(); @@ -78,6 +85,47 @@ protected function setUp(): void $this->placeholder(__('filament-forms::components.tags_input.placeholder')); $this->reorderAnimationDuration(100); + + $this->configureRelationships(); + } + + protected function configureRelationships(): void + { + $this->loadStateFromRelationshipsUsing(static function (TagsInput $component, ?Model $record): void { + if (! $component->checkRelationPresence($record, $component)) { + return; + } + + $relationship = $component->getRelationship(); + $relationshipColumn = $component->getRelationshipColumn(); + + $record->loadMissing($relationship); + + $component->state( + $record->{$relationship}()->select($relationshipColumn)->pluck($relationshipColumn)->all() + ); + }); + + $this->saveRelationshipsUsing(static function (TagsInput $component, ?Model $record, array $state) { + if (! $component->checkRelationPresence($record, $component)) { + return; + } + + $relationship = $component->getRelationship(); + $relationshipColumn = $component->getRelationshipColumn(); + + /** @var Model $related */ + $related = $record->{$relationship}()->getRelated(); + + $tagIds = collect($state) + ->map(fn ($tag) => $related->newQuery()->firstOrCreate([$relationshipColumn => $tag])) + ->pluck($related->getKeyName()) + ->all(); + + $record->{$relationship}()->sync($tagIds); + }); + + $this->dehydrated(fn (TagsInput $component) => $component->getRelationship() === null); } public function tagPrefix(string | Closure | null $prefix): static @@ -108,6 +156,15 @@ public function separator(string | Closure | null $separator = ','): static return $this; } + public function relationship(string | Closure $column = 'name', string | Closure | null $relationship = null): static + { + $this->relationshipColumn = $column; + + $this->relationship = $relationship ?? Str::camel($this->name); + + return $this; + } + /** * @param array | Closure $keys */ @@ -156,6 +213,16 @@ public function getSplitKeys(): array */ public function getSuggestions(): array { + if ($this->checkRelationPresence($model = new ($this->getModel()), $this)) { + return $model->{$this->getRelationship()}() + ->getRelated() + ->newQuery() + ->select('name') + ->orderBy('name') + ->pluck('name') + ->all(); + } + $suggestions = $this->evaluate($this->suggestions ?? []); if ($suggestions instanceof Arrayable) { @@ -165,8 +232,40 @@ public function getSuggestions(): array return $suggestions; } + public function getRelationship(): ?string + { + return $this->evaluate($this->relationship); + } + + public function getRelationshipColumn(): string + { + return $this->evaluate($this->relationshipColumn); + } + public function isReorderable(): bool { return (bool) $this->evaluate($this->isReorderable); } + + protected function checkRelationPresence(Model $record, TagsInput $component): bool + { + $relationship = $component->getRelationship(); + + // should we even be handling relationship? + if ($relationship === null) { + return false; + } + + // make sure we have a relationship method + if (! method_exists($record, $relationship)) { + return false; + } + + // and make sure it's a belongsToMany relationship + if (! ($record->{$relationship}() instanceof BelongsToMany)) { + return false; + } + + return true; + } }