defaultItems(1); $this->afterStateHydrated(static function (Repeater $component, ?array $state): void { $items = []; foreach ($state ?? [] as $itemData) { $items[(string) Str::uuid()] = $itemData; } $component->state($items); }); $this->registerListeners([ 'repeater::createItem' => [ function (Repeater $component, string $statePath): void { if ($statePath !== $component->getStatePath()) { return; } $newUuid = (string) Str::uuid(); $livewire = $component->getLivewire(); data_set($livewire, "{$statePath}.{$newUuid}", []); $component->getChildComponentContainers()[$newUuid]->fill(); $component->collapsed(false, shouldMakeComponentCollapsible: false); }, ], 'repeater::deleteItem' => [ function (Repeater $component, string $statePath, string $uuidToDelete): void { if ($statePath !== $component->getStatePath()) { return; } $items = $component->getState(); unset($items[$uuidToDelete]); $livewire = $component->getLivewire(); data_set($livewire, $statePath, $items); }, ], 'repeater::cloneItem' => [ function (Repeater $component, string $statePath, string $uuidToDuplicate): void { if ($statePath !== $component->getStatePath()) { return; } $newUuid = (string) Str::uuid(); $livewire = $component->getLivewire(); data_set( $livewire, "{$statePath}.{$newUuid}", data_get($livewire, "{$statePath}.{$uuidToDuplicate}"), ); $component->collapsed(false, shouldMakeComponentCollapsible: false); }, ], 'repeater::moveItemDown' => [ function (Repeater $component, string $statePath, string $uuidToMoveDown): void { if ($component->isItemMovementDisabled()) { return; } if ($statePath !== $component->getStatePath()) { return; } $items = array_move_after($component->getState(), $uuidToMoveDown); $livewire = $component->getLivewire(); data_set($livewire, $statePath, $items); }, ], 'repeater::moveItemUp' => [ function (Repeater $component, string $statePath, string $uuidToMoveUp): void { if ($component->isItemMovementDisabled()) { return; } if ($statePath !== $component->getStatePath()) { return; } $items = array_move_before($component->getState(), $uuidToMoveUp); $livewire = $component->getLivewire(); data_set($livewire, $statePath, $items); }, ], 'repeater::moveItems' => [ function (Repeater $component, string $statePath, array $uuids): void { if ($component->isItemMovementDisabled()) { return; } if ($statePath !== $component->getStatePath()) { return; } $items = array_merge(array_flip($uuids), $component->getState()); $livewire = $component->getLivewire(); data_set($livewire, $statePath, $items); }, ], ]); $this->createItemButtonLabel(static function (Repeater $component) { return __('forms::components.repeater.buttons.create_item.label', [ 'label' => lcfirst($component->getLabel()), ]); }); $this->mutateDehydratedStateUsing(static function (?array $state): array { return array_values($state ?? []); }); } public function createItemButtonLabel(string | Closure | null $label): static { $this->createItemButtonLabel = $label; return $this; } public function defaultItems(int | Closure $count): static { $this->default(static function (Repeater $component) use ($count): array { $items = []; $count = $component->evaluate($count); if (! $count) { return $items; } foreach (range(1, $count) as $index) { $items[(string) Str::uuid()] = []; } return $items; }); return $this; } public function disableItemCreation(bool | Closure $condition = true): static { $this->isItemCreationDisabled = $condition; return $this; } public function disableItemDeletion(bool | Closure $condition = true): static { $this->isItemDeletionDisabled = $condition; return $this; } public function disableItemMovement(bool | Closure $condition = true): static { $this->isItemMovementDisabled = $condition; return $this; } public function reorderableWithButtons(bool | Closure $condition = true): static { $this->isReorderableWithButtons = $condition; return $this; } public function inset(bool | Closure $condition = true): static { $this->isInset = $condition; return $this; } public function getChildComponentContainers(bool $withHidden = false): array { $relationship = $this->getRelationship(); $records = $relationship ? $this->getCachedExistingRecords() : null; $containers = []; foreach ($this->getState() ?? [] as $itemKey => $itemData) { $containers[$itemKey] = $this ->getChildComponentContainer() ->statePath($itemKey) ->model($relationship ? $records[$itemKey] ?? $this->getRelatedModel() : null) ->inlineLabel(false) ->getClone(); } return $containers; } public function getCreateItemButtonLabel(): string { return $this->evaluate($this->createItemButtonLabel); } public function isReorderableWithButtons(): bool { return $this->evaluate($this->isReorderableWithButtons) && (! $this->isItemMovementDisabled()); } public function isItemMovementDisabled(): bool { return $this->evaluate($this->isItemMovementDisabled) || $this->isDisabled(); } public function isItemCreationDisabled(): bool { return $this->evaluate($this->isItemCreationDisabled) || $this->isDisabled() || (filled($this->getMaxItems()) && ($this->getMaxItems() <= $this->getItemsCount())); } public function isItemDeletionDisabled(): bool { return $this->evaluate($this->isItemDeletionDisabled) || $this->isDisabled(); } public function isInset(): bool { return (bool) $this->evaluate($this->isInset); } public function orderable(string | Closure | null $column = 'sort'): static { $this->orderColumn = $column; $this->disableItemMovement(static fn (Repeater $component): bool => ! $component->evaluate($column)); return $this; } public function relationship(string | Closure | null $name = null, ?Closure $callback = null): static { $this->relationship = $name ?? $this->getName(); $this->modifyRelationshipQueryUsing = $callback; $this->afterStateHydrated(null); $this->loadStateFromRelationshipsUsing(static function (Repeater $component) { $component->clearCachedExistingRecords(); $component->fillFromRelationship(); }); $this->saveRelationshipsUsing(static function (Repeater $component, HasForms $livewire, ?array $state) { if (! is_array($state)) { $state = []; } $relationship = $component->getRelationship(); $existingRecords = $component->getCachedExistingRecords(); $recordsToDelete = []; foreach ($existingRecords->pluck($relationship->getRelated()->getKeyName()) as $keyToCheckForDeletion) { if (array_key_exists("record-{$keyToCheckForDeletion}", $state)) { continue; } $recordsToDelete[] = $keyToCheckForDeletion; } $relationship ->whereIn($relationship->getRelated()->getQualifiedKeyName(), $recordsToDelete) ->get() ->each(static fn (Model $record) => $record->delete()); $childComponentContainers = $component->getChildComponentContainers(); $itemOrder = 1; $orderColumn = $component->getOrderColumn(); $activeLocale = $livewire->getActiveFormLocale(); foreach ($childComponentContainers as $itemKey => $item) { $itemData = $item->getState(shouldCallHooksBefore: false); if ($orderColumn) { $itemData[$orderColumn] = $itemOrder; $itemOrder++; } if ($record = ($existingRecords[$itemKey] ?? null)) { $activeLocale && method_exists($record, 'setLocale') && $record->setLocale($activeLocale); $itemData = $component->mutateRelationshipDataBeforeSave($itemData, record: $record); $record->fill($itemData)->save(); continue; } $relatedModel = $component->getRelatedModel(); $record = new $relatedModel(); if ($activeLocale && method_exists($record, 'setLocale')) { $record->setLocale($activeLocale); } $itemData = $component->mutateRelationshipDataBeforeCreate($itemData); $record->fill($itemData); $record = $relationship->save($record); $item->model($record)->saveRelationships(); } }); $this->dehydrated(false); $this->disableItemMovement(); return $this; } public function itemLabel(string | Closure | null $label): static { $this->itemLabel = $label; return $this; } public function fillFromRelationship(): void { $this->state( $this->getStateFromRelatedRecords($this->getCachedExistingRecords()), ); } protected function getStateFromRelatedRecords(Collection $records): array { if (! $records->count()) { return []; } $activeLocale = $this->getLivewire()->getActiveFormLocale(); return $records ->map(function (Model $record) use ($activeLocale): array { $state = $record->attributesToArray(); if ($activeLocale && method_exists($record, 'getTranslatableAttributes') && method_exists($record, 'getTranslation')) { foreach ($record->getTranslatableAttributes() as $attribute) { $state[$attribute] = $record->getTranslation($attribute, $activeLocale); } } return $this->mutateRelationshipDataBeforeFill($state); }) ->toArray(); } public function getLabel(): string | Htmlable | null { if ($this->label === null && $this->hasRelationship()) { $label = (string) Str::of($this->getRelationshipName()) ->before('.') ->kebab() ->replace(['-', '_'], ' ') ->ucfirst(); return ($this->shouldTranslateLabel) ? __($label) : $label; } return parent::getLabel(); } public function getOrderColumn(): ?string { return $this->evaluate($this->orderColumn); } public function getRelationship(): HasOneOrMany | BelongsToMany | null { if (! $this->hasRelationship()) { return null; } return $this->getModelInstance()->{$this->getRelationshipName()}(); } public function getRelationshipName(): ?string { return $this->evaluate($this->relationship); } public function getCachedExistingRecords(): Collection { if ($this->cachedExistingRecords) { return $this->cachedExistingRecords; } $relationship = $this->getRelationship(); $relationshipQuery = $relationship->getQuery(); if ($relationship instanceof BelongsToMany) { $relationshipQuery->select([ $relationship->getTable() . '.*', $relationshipQuery->getModel()->getTable() . '.*', ]); } if ($this->modifyRelationshipQueryUsing) { $relationshipQuery = $this->evaluate($this->modifyRelationshipQueryUsing, [ 'query' => $relationshipQuery, ]) ?? $relationshipQuery; } if ($orderColumn = $this->getOrderColumn()) { $relationshipQuery->orderBy($orderColumn); } $relatedKeyName = $relationship->getRelated()->getKeyName(); return $this->cachedExistingRecords = $relationshipQuery->get()->mapWithKeys( fn (Model $item): array => ["record-{$item[$relatedKeyName]}" => $item], ); } public function getItemLabel(string $uuid): string | Htmlable | null { return $this->evaluate($this->itemLabel, [ 'state' => $this->getChildComponentContainer($uuid)->getRawState(), 'uuid' => $uuid, ]); } public function hasItemLabels(): bool { return $this->itemLabel !== null; } public function clearCachedExistingRecords(): void { $this->cachedExistingRecords = null; } protected function getRelatedModel(): string { return $this->getRelationship()->getModel()::class; } public function hasRelationship(): bool { return filled($this->getRelationshipName()); } public function mutateRelationshipDataBeforeCreateUsing(?Closure $callback): static { $this->mutateRelationshipDataBeforeCreateUsing = $callback; return $this; } public function mutateRelationshipDataBeforeCreate(array $data): array { if ($this->mutateRelationshipDataBeforeCreateUsing instanceof Closure) { $data = $this->evaluate($this->mutateRelationshipDataBeforeCreateUsing, [ 'data' => $data, ]); } return $data; } public function mutateRelationshipDataBeforeSaveUsing(?Closure $callback): static { $this->mutateRelationshipDataBeforeSaveUsing = $callback; return $this; } public function mutateRelationshipDataBeforeFill(array $data): array { if ($this->mutateRelationshipDataBeforeFillUsing instanceof Closure) { $data = $this->evaluate($this->mutateRelationshipDataBeforeFillUsing, [ 'data' => $data, ]); } return $data; } public function mutateRelationshipDataBeforeFillUsing(?Closure $callback): static { $this->mutateRelationshipDataBeforeFillUsing = $callback; return $this; } public function mutateRelationshipDataBeforeSave(array $data, Model $record): array { if ($this->mutateRelationshipDataBeforeSaveUsing instanceof Closure) { $data = $this->evaluate($this->mutateRelationshipDataBeforeSaveUsing, [ 'data' => $data, 'record' => $record, ]); } return $data; } public function canConcealComponents(): bool { return $this->isCollapsible(); } }