├── roadmap ├── smart-keys.md ├── this-wire.md ├── install-command.md ├── dev-modal.md ├── streaming.md ├── docs-best-practices.md ├── array-update-hook.md ├── wire-ref.md ├── tag-syntax.md ├── islands.md ├── configuration.md ├── single-file-components.md └── overview.md ├── dist └── manifest.json ├── src ├── Features │ ├── SupportConsoleCommands │ │ ├── Commands │ │ │ ├── livewire-mfc-js.stub │ │ │ ├── livewire.view.stub │ │ │ ├── livewire-mfc-view.stub │ │ │ ├── livewire-mfc-class.stub │ │ │ ├── livewire-sfc.stub │ │ │ ├── livewire-mfc-test.stub │ │ │ ├── livewire.form.stub │ │ │ ├── livewire.attribute.stub │ │ │ ├── livewire.pest.stub │ │ │ ├── livewire.stub │ │ │ ├── livewire.inline.stub │ │ │ ├── LivewireMakeCommand.php │ │ │ ├── Upgrade │ │ │ │ ├── ClearViewCache.php │ │ │ │ ├── RemoveDeferModifierFromWireModelDirectives.php │ │ │ │ ├── AddLiveModifierToWireModelDirectives.php │ │ │ │ ├── RemovePrefetchModifierFromWireClickDirective.php │ │ │ │ ├── ChangeLazyToBlurModifierOnWireModelDirectives.php │ │ │ │ ├── RemovePreventModifierFromWireSubmitDirective.php │ │ │ │ ├── ChangeWireLoadDirectiveToWireInit.php │ │ │ │ ├── RemoveDeferModifierFromEntangleDirectives.php │ │ │ │ ├── ThirdPartyUpgradeNotice.php │ │ │ │ ├── ReplaceTemporaryUploadedFileNamespace.php │ │ │ │ ├── RepublishNavigation.php │ │ │ │ ├── UpgradeConfigInstructions.php │ │ │ │ ├── UpgradeIntroduction.php │ │ │ │ ├── AddLiveModifierToEntangleDirectives.php │ │ │ │ ├── ChangeForgetComputedToUnset.php │ │ │ │ ├── UpgradeAlpineInstructions.php │ │ │ │ ├── ChangeDefaultLayoutView.php │ │ │ │ └── ChangeTestAssertionMethods.php │ │ │ ├── livewire.test.stub │ │ │ ├── livewire.layout.stub │ │ │ ├── the-tao.php │ │ │ ├── FormCommand.php │ │ │ ├── AttributeCommand.php │ │ │ ├── PublishCommand.php │ │ │ ├── FileManipulationCommand.php │ │ │ └── StubsCommand.php │ │ └── SupportConsoleCommands.php │ ├── SupportScriptsAndAssets │ │ ├── test.js │ │ └── non-livewire-asset.js │ ├── SupportFileUploads │ │ ├── browser_test_image.png │ │ ├── browser_test_image2.png │ │ ├── browser_test_image_big.jpg │ │ ├── FileNotPreviewableException.php │ │ ├── MissingFileUploadsTraitException.php │ │ ├── S3DoesntSupportMultipleFileUploads.php │ │ ├── FilePreviewController.php │ │ ├── SupportFileUploads.php │ │ ├── FileUploadController.php │ │ ├── FileUploadSynth.php │ │ └── GenerateSignedUploadUrl.php │ ├── SupportWireLoading │ │ └── browser_test_image.png │ ├── SupportPagination │ │ ├── WithoutUrlPagination.php │ │ ├── PaginationUrl.php │ │ └── HandlesPagination.php │ ├── SupportJsEvaluation │ │ ├── fixtures │ │ │ ├── mfc-component-with-dollar-js-magic │ │ │ │ ├── mfc-component-with-dollar-js-magic.js │ │ │ │ ├── mfc-component-with-dollar-js-magic.php │ │ │ │ └── mfc-component-with-dollar-js-magic.blade.php │ │ │ └── sfc-component-with-dollar-js-magic.blade.php │ │ ├── HandlesJsEvaluation.php │ │ ├── SupportJsEvaluation.php │ │ └── BaseJs.php │ ├── SupportSingleAndMultiFileComponents │ │ ├── fixtures │ │ │ ├── mfc-counter │ │ │ │ ├── mfc-counter.blade.php │ │ │ │ └── mfc-counter.php │ │ │ ├── sfc-counter.blade.php │ │ │ ├── sfc-component-with-render-and-data.blade.php │ │ │ └── sfc-scripts.blade.php │ │ └── SupportSingleAndMultiFileComponents.php │ ├── SupportNavigate │ │ ├── test-views │ │ │ ├── test-navigate-asset.js │ │ │ ├── layout.blade.php │ │ │ ├── changed-layout.blade.php │ │ │ ├── tracked-layout.blade.php │ │ │ ├── changed-tracked-layout.blade.php │ │ │ ├── html-attributes1.blade.php │ │ │ ├── html-attributes2.blade.php │ │ │ ├── layout-with-noscript.blade.php │ │ │ └── layout-with-navigate-outside.blade.php │ │ └── SupportNavigate.php │ ├── SupportAttributes │ │ ├── AttributeLevel.php │ │ ├── HandlesAttributes.php │ │ └── AttributeCollection.php │ ├── SupportIsolating │ │ ├── BaseIsolate.php │ │ └── SupportIsolating.php │ ├── SupportValidation │ │ ├── BaseRule.php │ │ └── SupportValidation.php │ ├── SupportTesting │ │ ├── ShowDuskComponent.php │ │ ├── Render.php │ │ ├── RequestBroker.php │ │ ├── ComponentState.php │ │ └── SubsequentRender.php │ ├── SupportWireables │ │ ├── SupportWireables.php │ │ └── WireableSynth.php │ ├── SupportPageComponents │ │ ├── BaseTitle.php │ │ ├── BaseLayout.php │ │ ├── MissingLayoutException.php │ │ ├── HandlesPageComponents.php │ │ └── PageComponentConfig.php │ ├── SupportRouting │ │ ├── fixtures │ │ │ └── sfc-counter.blade.php │ │ └── SupportRouting.php │ ├── SupportModels │ │ ├── SupportModels.php │ │ └── IsLazy.php │ ├── SupportLockedProperties │ │ ├── BaseLocked.php │ │ └── CannotUpdateLockedPropertyException.php │ ├── SupportLazyLoading │ │ ├── BaseDefer.php │ │ └── BaseLazy.php │ ├── SupportAsync │ │ └── BaseAsync.php │ ├── SupportReactiveProps │ │ ├── CannotMutateReactivePropException.php │ │ ├── SupportReactiveProps.php │ │ └── BaseReactive.php │ ├── SupportLocales │ │ └── SupportLocales.php │ ├── SupportFormObjects │ │ ├── HandlesFormObjects.php │ │ └── FormObjectSynth.php │ ├── SupportComputed │ │ └── CannotCallComputedDirectlyException.php │ ├── SupportDisablingBackButtonCache │ │ ├── HandlesDisablingBackButtonCache.php │ │ ├── SupportDisablingBackButtonCache.php │ │ └── DisableBackButtonCacheMiddleware.php │ ├── SupportReleaseTokens │ │ ├── HandlesReleaseTokens.php │ │ ├── SupportReleaseTokens.php │ │ └── ReleaseToken.php │ ├── SupportEvents │ │ ├── HandlesEvents.php │ │ ├── BaseOn.php │ │ ├── fake-echo.js │ │ └── Event.php │ ├── SupportHtmlAttributeForwarding │ │ ├── HandlesHtmlAttributeForwarding.php │ │ └── SupportHtmlAttributeForwarding.php │ ├── SupportLifecycleHooks │ │ └── DirectlyCallingLifecycleHooksNotAllowedException.php │ ├── SupportMultipleRootElementDetection │ │ ├── MultipleRootElementsDetectedException.php │ │ └── SupportMultipleRootElementDetection.php │ ├── SupportLegacyModels │ │ └── CannotBindToModelDataWithoutValidationRuleException.php │ ├── SupportTeleporting │ │ └── SupportTeleporting.php │ ├── SupportBladeAttributes │ │ └── SupportBladeAttributes.php │ ├── SupportMagicActions │ │ └── SupportMagicActions.php │ ├── SupportEntangle │ │ └── SupportEntangle.php │ ├── SupportStreaming │ │ ├── HandlesStreaming.php │ │ ├── StreamManager.php │ │ └── SupportStreaming.php │ ├── SupportRedirects │ │ ├── HandlesRedirects.php │ │ ├── Redirector.php │ │ ├── TestsRedirects.php │ │ └── SupportRedirects.php │ ├── SupportWireRef │ │ └── SupportWireRef.php │ ├── SupportSlots │ │ ├── SlotProxy.php │ │ ├── HandlesSlots.php │ │ ├── PlaceholderSlot.php │ │ ├── Slot.php │ │ └── SupportSlots.php │ ├── SupportFileDownloads │ │ └── TestsFileDownloads.php │ ├── SupportWithMethod │ │ └── SupportWithMethod.php │ ├── SupportSession │ │ └── BaseSession.php │ ├── SupportJsModules │ │ └── SupportJsModules.php │ ├── SupportWireModelingNestedComponents │ │ └── BaseModelable.php │ ├── SupportQueryString │ │ └── SupportQueryString.php │ ├── SupportNestedComponentListeners │ │ └── SupportNestedComponentListeners.php │ └── SupportIslands │ │ └── SupportIslands.php ├── Exceptions │ ├── BypassViewHandler.php │ ├── ComponentNotFoundException.php │ ├── EventHandlerDoesNotExist.php │ ├── NonPublicComponentMethodCall.php │ ├── ComponentAttributeMissingOnDynamicComponentException.php │ ├── MethodNotFoundException.php │ ├── PropertyNotFoundException.php │ ├── MissingRulesException.php │ ├── PublicPropertyNotFoundException.php │ ├── RootTagMissingFromViewException.php │ └── LivewireReleaseTokenMismatchException.php ├── Form.php ├── Wireable.php ├── WithPagination.php ├── Attributes │ ├── Js.php │ ├── Async.php │ ├── Lazy.php │ ├── Url.php │ ├── Defer.php │ ├── Isolate.php │ ├── Title.php │ ├── Computed.php │ ├── Layout.php │ ├── Locked.php │ ├── Reactive.php │ ├── Renderless.php │ ├── Modelable.php │ ├── Session.php │ ├── Rule.php │ ├── On.php │ └── Validate.php ├── Attribute.php ├── WithFileUploads.php ├── WithoutUrlPagination.php ├── Mechanisms │ ├── Mechanism.php │ ├── CompileLivewireTags │ │ └── CompileLivewireTags.php │ ├── HandleComponents │ │ ├── BaseRenderless.php │ │ ├── Synthesizers │ │ │ ├── StringableSynth.php │ │ │ ├── IntSynth.php │ │ │ ├── FloatSynth.php │ │ │ ├── StdClassSynth.php │ │ │ ├── EnumSynth.php │ │ │ ├── ArraySynth.php │ │ │ ├── CollectionSynth.php │ │ │ ├── CarbonSynth.php │ │ │ └── Synth.php │ │ ├── CorruptComponentPayloadException.php │ │ ├── ViewContext.php │ │ ├── Checksum.php │ │ └── ComponentContext.php │ ├── ClearCachedFiles.php │ ├── ExtendBlade │ │ └── DeterministicBladeKeys.php │ └── RenderComponent.php ├── Pipe.php ├── Wrapped.php ├── Transparency.php ├── WireDirective.php ├── Livewire.php └── Compiler │ └── Compiler.php ├── vitest.config.js ├── LICENSE.md ├── composer.json └── README.md /roadmap/smart-keys.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roadmap/this-wire.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roadmap/install-command.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roadmap/dev-modal.md: -------------------------------------------------------------------------------- 1 | * multiple root elements warning -------------------------------------------------------------------------------- /dist/manifest.json: -------------------------------------------------------------------------------- 1 | 2 | {"/livewire.js":"9a221de9"} 3 | -------------------------------------------------------------------------------- /roadmap/streaming.md: -------------------------------------------------------------------------------- 1 | 2 | * Add $this->stream()->update/html/island 3 | * Make canellable -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/livewire-mfc-js.stub: -------------------------------------------------------------------------------- 1 | // Add your JavaScript here -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/livewire.view.stub: -------------------------------------------------------------------------------- 1 |
2 | {{-- [quote] --}} 3 |
4 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/livewire-mfc-view.stub: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{-- [quote] --}} 4 |
-------------------------------------------------------------------------------- /src/Features/SupportScriptsAndAssets/test.js: -------------------------------------------------------------------------------- 1 | 2 | document.querySelector('[dusk="foo"]').textContent = 'evaluated' 3 | -------------------------------------------------------------------------------- /src/Exceptions/BypassViewHandler.php: -------------------------------------------------------------------------------- 1 | { 2 | window.test = 'through dollar js' 3 | }) 4 | -------------------------------------------------------------------------------- /src/Form.php: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/Wireable.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Features/SupportSingleAndMultiFileComponents/fixtures/mfc-counter/mfc-counter.blade.php: -------------------------------------------------------------------------------- 1 |
2 | Count: {{ $count }} 3 | 4 |
-------------------------------------------------------------------------------- /src/WithPagination.php: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | {{-- [quote] --}} 13 |
-------------------------------------------------------------------------------- /src/Attributes/Computed.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 8 | }); 9 | -------------------------------------------------------------------------------- /src/Attributes/Renderless.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 9 | }); 10 | -------------------------------------------------------------------------------- /src/Features/SupportSingleAndMultiFileComponents/fixtures/mfc-counter/mfc-counter.php: -------------------------------------------------------------------------------- 1 | count++; 9 | } 10 | }; 11 | ?> -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/livewire.stub: -------------------------------------------------------------------------------- 1 | instance(static::class, $this); 10 | } 11 | 12 | function boot() 13 | { 14 | // 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Attributes/Validate.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import path from 'path' 3 | 4 | export default defineConfig({ 5 | test: { 6 | environment: 'jsdom', 7 | globals: true, 8 | }, 9 | resolve: { 10 | alias: { 11 | '@': path.resolve(__dirname, './js'), 12 | }, 13 | }, 14 | }) -------------------------------------------------------------------------------- /src/Features/SupportNavigate/test-views/changed-layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Features/SupportValidation/BaseRule.php: -------------------------------------------------------------------------------- 1 | call(app('livewire')->new($class)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportNavigate/test-views/tracked-layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Exceptions/EventHandlerDoesNotExist.php: -------------------------------------------------------------------------------- 1 | propertySynthesizer(WireableSynth::class); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportJsEvaluation/HandlesJsEvaluation.php: -------------------------------------------------------------------------------- 1 | push('js', compact('expression', 'params')); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Features/SupportNavigate/test-views/changed-tracked-layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Exceptions/NonPublicComponentMethodCall.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Features/SupportNavigate/test-views/html-attributes2.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $slot }} 8 | 9 | @stack('scripts') 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Features/SupportSingleAndMultiFileComponents/SupportSingleAndMultiFileComponents.php: -------------------------------------------------------------------------------- 1 | precompiler(new LivewireTagPrecompiler); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /roadmap/docs-best-practices.md: -------------------------------------------------------------------------------- 1 | * Messaging: 2 | * 101 should be practical livewire case not counter 3 | * Encourage comuted properties instead of naked public methods 4 | * Recipes: 5 | * Shared modal 6 | * Integrate with third-party library 7 | * Convention: show component structure conventions 8 | * Education: livewire components aren't vue/react components 9 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/livewire.inline.stub: -------------------------------------------------------------------------------- 1 | 13 | {{-- [quote] --}} 14 | 15 | HTML; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Features/SupportRouting/fixtures/sfc-counter.blade.php: -------------------------------------------------------------------------------- 1 | count++; 9 | } 10 | }; 11 | ?> 12 | 13 |
14 | Count: {{ $count }} 15 | 16 |
-------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/BaseRenderless.php: -------------------------------------------------------------------------------- 1 | component->skipRender(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Features/SupportJsEvaluation/fixtures/sfc-component-with-dollar-js-magic.blade.php: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 |
12 | 13 | 18 | -------------------------------------------------------------------------------- /src/Features/SupportPageComponents/BaseLayout.php: -------------------------------------------------------------------------------- 1 | count++; 9 | } 10 | }; 11 | ?> 12 | 13 |
14 | Count: {{ $count }} 15 | 16 |
-------------------------------------------------------------------------------- /src/Features/SupportSingleAndMultiFileComponents/fixtures/sfc-component-with-render-and-data.blade.php: -------------------------------------------------------------------------------- 1 | view([ 10 | 'message' => 'Hello World', 11 | ]); 12 | } 13 | }; 14 | ?> 15 | 16 |
Message: {{ $message }}
17 | -------------------------------------------------------------------------------- /src/Exceptions/ComponentAttributeMissingOnDynamicComponentException.php: -------------------------------------------------------------------------------- 1 | propertySynthesizer([ 12 | ModelSynth::class, 13 | EloquentCollectionSynth::class, 14 | ]); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Features/SupportLockedProperties/BaseLocked.php: -------------------------------------------------------------------------------- 1 | getName()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Features/SupportLazyLoading/BaseDefer.php: -------------------------------------------------------------------------------- 1 | property.']' 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Exceptions/MissingRulesException.php: -------------------------------------------------------------------------------- 1 | getName(); 13 | 14 | $context->pushMemo('async', $methodName); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Features/SupportNavigate/test-views/layout-with-noscript.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ $slot }} 9 | 10 | @stack('scripts') 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Features/SupportReactiveProps/CannotMutateReactivePropException.php: -------------------------------------------------------------------------------- 1 | count++; 9 | } 10 | }; 11 | ?> 12 | 13 |
14 |
bar
15 |
16 | 17 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/LivewireMakeCommand.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Go to second page (outside) 8 | 9 | {{ $slot }} 10 | 11 | @stack('scripts') 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Exceptions/PublicPropertyNotFoundException.php: -------------------------------------------------------------------------------- 1 | setLocale($locale); 12 | } 13 | 14 | function dehydrate($context) 15 | { 16 | $context->addMemo('locale', app()->getLocale()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Features/SupportFormObjects/HandlesFormObjects.php: -------------------------------------------------------------------------------- 1 | all() as $key => $value) { 12 | if ($value instanceof Form) { 13 | $forms[] = $value; 14 | } 15 | } 16 | 17 | return $forms; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Features/SupportJsEvaluation/SupportJsEvaluation.php: -------------------------------------------------------------------------------- 1 | component)->has('js')) return; 14 | 15 | $context->addEffect('xjs', store($this->component)->get('js')); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Features/SupportComputed/CannotCallComputedDirectlyException.php: -------------------------------------------------------------------------------- 1 | call('view:clear'); 12 | 13 | return $next($console); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/RootTagMissingFromViewException.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Features/SupportReleaseTokens/HandlesReleaseTokens.php: -------------------------------------------------------------------------------- 1 | getName(); 13 | 14 | $stringifiedMethod = $this->component->$name(); 15 | 16 | $context->pushEffect('js', $stringifiedMethod, $name); 17 | } 18 | } 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Features/SupportEvents/HandlesEvents.php: -------------------------------------------------------------------------------- 1 | listeners; 13 | } 14 | 15 | public function dispatch($event, ...$params) 16 | { 17 | $event = new Event($event, $params); 18 | 19 | store($this)->push('dispatched', $event); 20 | 21 | return $event; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Features/SupportHtmlAttributeForwarding/HandlesHtmlAttributeForwarding.php: -------------------------------------------------------------------------------- 1 | htmlAttributes = $attributes; 12 | 13 | return $this; 14 | } 15 | 16 | public function getHtmlAttributes(): array 17 | { 18 | return $this->htmlAttributes; 19 | } 20 | } -------------------------------------------------------------------------------- /src/Exceptions/LivewireReleaseTokenMismatchException.php: -------------------------------------------------------------------------------- 1 | __toString(), []]; 16 | } 17 | 18 | function hydrate($value) { 19 | return str($value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/livewire.layout.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $title ?? config('app.name') }} 8 | 9 | @vite(['resources/css/app.css', 'resources/js/app.js']) 10 | 11 | @livewireStyles 12 | 13 | 14 | {{ $slot }} 15 | 16 | @livewireScripts 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/FileNotPreviewableException.php: -------------------------------------------------------------------------------- 1 | guessExtension()}\" is not previewable. See the livewire.temporary_file_upload.preview_mimes config." 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/MissingFileUploadsTraitException.php: -------------------------------------------------------------------------------- 1 | getName()}] component class." 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/S3DoesntSupportMultipleFileUploads.php: -------------------------------------------------------------------------------- 1 | getName() . ']'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /roadmap/array-update-hook.md: -------------------------------------------------------------------------------- 1 | Currently when a value in an array is updated, the updated hook only shows the delta of the changes. 2 | 3 | This is unexpected and at time the value can actually be `__rm__`. 4 | 5 | This should be changed to match v2 where by the updated hook returns the full value of the array instead. 6 | 7 | See my failing test PR for full details of the issue and possible options https://github.com/livewire/livewire/pull/8235 8 | 9 | Other issues: 10 | - https://github.com/livewire/livewire/discussions/7101 11 | - https://github.com/livewire/livewire/discussions/6665 12 | 13 | -------------------------------------------------------------------------------- /src/Features/SupportLegacyModels/CannotBindToModelDataWithoutValidationRuleException.php: -------------------------------------------------------------------------------- 1 | ">'; 14 | }); 15 | 16 | Blade::directive('endteleport', function () { 17 | return ''; 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /roadmap/wire-ref.md: -------------------------------------------------------------------------------- 1 | @php 2 | new class extends Livewire\Component { 3 | public function save() 4 | { 5 | // 6 | 7 | $this->dispatch('close')->to(ref: 'modal'); 8 | } 9 | } 10 | @endphp 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /src/Features/SupportIsolating/SupportIsolating.php: -------------------------------------------------------------------------------- 1 | shouldIsolate()) { 12 | $context->addMemo('isolate', true); 13 | } 14 | } 15 | 16 | public function shouldIsolate() 17 | { 18 | return $this->component->getAttributes() 19 | ->filter(fn ($i) => is_subclass_of($i, BaseIsolate::class)) 20 | ->count() > 0; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportPagination/PaginationUrl.php: -------------------------------------------------------------------------------- 1 | pushQueryStringEffect($context); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/CorruptComponentPayloadException.php: -------------------------------------------------------------------------------- 1 | runningInConsole()) { 11 | app('events')->listen(\Illuminate\Console\Events\CommandFinished::class, function ($event) { 12 | if ($event->command === 'view:clear' && $event->exitCode === 0) { 13 | app('livewire.compiler')->clearCompiled($event->output); 14 | } 15 | }); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/IntSynth.php: -------------------------------------------------------------------------------- 1 | whereStartsWith('wire:'.$name)); 15 | 16 | $directive = head(array_keys($entries)); 17 | $value = head(array_values($entries)); 18 | 19 | return new WireDirective($name, $directive, $value); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportMagicActions/SupportMagicActions.php: -------------------------------------------------------------------------------- 1 | new Middleware($middleware), static::$middleware); 16 | } 17 | 18 | public function handle($filename) 19 | { 20 | abort_unless(request()->hasValidSignature(), 401); 21 | 22 | return Utils::pretendPreviewResponseIsPreviewFile($filename); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Features/SupportRouting/SupportRouting.php: -------------------------------------------------------------------------------- 1 | addComponent($component); 15 | } 16 | 17 | return Route::get($uri, function () use ($component) { 18 | return app()->call([ 19 | app('livewire')->new($component), 20 | '__invoke', 21 | ]); 22 | }); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Features/SupportAttributes/HandlesAttributes.php: -------------------------------------------------------------------------------- 1 | attributes ??= AttributeCollection::fromComponent($this); 12 | } 13 | 14 | function setPropertyAttribute($property, $attribute) 15 | { 16 | $attribute->__boot($this, AttributeLevel::PROPERTY, $property); 17 | 18 | $this->mergeOutsideAttributes(new AttributeCollection([$attribute])); 19 | } 20 | 21 | function mergeOutsideAttributes(AttributeCollection $attributes) 22 | { 23 | $this->attributes = $this->getAttributes()->concat($attributes); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /roadmap/tag-syntax.md: -------------------------------------------------------------------------------- 1 | ## Tag syntax 2 | 3 | Blade components are currently rendered using the following syntax: 4 | 5 | ```php 6 | 7 | 8 | 9 | ``` 10 | 11 | Livewire v3 components are rendered using the following syntax: 12 | 13 | ```php 14 | 15 | 16 | 17 | ``` 18 | 19 | I think I want Livewire v4 components to be rendered using the following syntax: 20 | 21 | ```php 22 | 23 | 24 | 25 | ``` 26 | 27 | It's so much cleaner to me. 28 | 29 | These `wire:` components now should map to the `resources/views/components` directory... 30 | -------------------------------------------------------------------------------- /src/Features/SupportEntangle/SupportEntangle.php: -------------------------------------------------------------------------------- 1 | window.Livewire.find('{{ \$__livewire->getId() }}').entangle('{{ {$expression}->value() }}'){{ {$expression}->hasModifier('live') ? '.live' : '' }}window.Livewire.find('{{ \$__livewire->getId() }}').entangle('{{ {$expression} }}') 15 | EOT; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Features/SupportEvents/BaseOn.php: -------------------------------------------------------------------------------- 1 | event) as $event) { 19 | store($this->component)->push( 20 | 'listenersFromAttributes', 21 | $this->getName() ?? '$refresh', 22 | $event, 23 | ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Features/SupportReleaseTokens/SupportReleaseTokens.php: -------------------------------------------------------------------------------- 1 | addMemo('release', ReleaseToken::generate($component)); 15 | }); 16 | 17 | // Use `snapshot-verified` to run the check before any component properties are hydrated 18 | // but after the snapshot has been verified to ensure it hasn't been tampered with... 19 | on('snapshot-verified', function ($snapshot) { 20 | ReleaseToken::verify($snapshot); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/RemoveDeferModifierFromWireModelDirectives.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The wire:model directive is now deferred by default.', 14 | before: 'wire:model.defer', 15 | after: 'wire:model', 16 | pattern: '/wire:model\.defer/', 17 | replacement: 'wire:model', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/AddLiveModifierToWireModelDirectives.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The wire:model directive is now deferred by default.', 14 | before: 'wire:model', 15 | after: 'wire:model.live', 16 | pattern: '/wire:model(?!\.(?:defer|lazy|live))/', 17 | replacement: 'wire:model.live', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/RemovePrefetchModifierFromWireClickDirective.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The wire:click.prefetch modifier has been removed.', 14 | before: 'wire:click.prefetch', 15 | after: 'wire:click', 16 | pattern: '/wire:click\.prefetch/', 17 | replacement: 'wire:click', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/ChangeLazyToBlurModifierOnWireModelDirectives.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The wire:model.lazy modifier is now wire:model.blur.', 14 | before: 'wire:model.lazy', 15 | after: 'wire:model.blur', 16 | pattern: '/wire:model.lazy/', 17 | replacement: 'wire:model.blur', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportTesting/Render.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The wire:submit directive now prevents submission by default.', 14 | before: 'wire:submit.prevent', 15 | after: 'wire:submit', 16 | pattern: '/wire:submit\.prevent/', 17 | replacement: 'wire:submit', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/ChangeWireLoadDirectiveToWireInit.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The livewire:load is now livewire:init.', 14 | before: 'livewire:load', 15 | after: 'livewire:init', 16 | pattern: '/livewire:load/', 17 | replacement: 'livewire:init', 18 | directories: ['resources/views', 'resources/js'] 19 | ); 20 | 21 | return $next($console); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Features/SupportStreaming/HandlesStreaming.php: -------------------------------------------------------------------------------- 1 | content($content, $replace); 24 | } 25 | 26 | return $stream->content($content, $replace)->to($name, $el, $ref); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/RemoveDeferModifierFromEntangleDirectives.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The @entangle(...) directive is now deferred by default.', 14 | before: '@entangle(...).defer', 15 | after: '@entangle(...)', 16 | pattern: '/@entangle\(((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*)\)\.defer/', 17 | replacement: '@entangle($1)', 18 | ); 19 | 20 | return $next($console); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Features/SupportReactiveProps/SupportReactiveProps.php: -------------------------------------------------------------------------------- 1 | static::$pendingChildParams = []); 15 | 16 | on('mount.stub', function ($tag, $id, $params, $parent, $key) { 17 | static::$pendingChildParams[$id] = $params; 18 | }); 19 | } 20 | 21 | static function hasPassedInProps($id) { 22 | return isset(static::$pendingChildParams[$id]); 23 | } 24 | 25 | static function getPassedInProp($id, $name) { 26 | $params = static::$pendingChildParams[$id] ?? []; 27 | 28 | return $params[$name] ?? null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/ThirdPartyUpgradeNotice.php: -------------------------------------------------------------------------------- 1 | line(' Third-party package upgrade 🚀 '); 12 | $console->newLine(); 13 | $console->comment('!! Please be aware that the following upgrade steps are registered by third-parties !!'); 14 | $console->newLine(); 15 | $console->newLine(); 16 | $console->line('You can abort this command at any time by pressing ctrl+c.'); 17 | 18 | if($console->confirm('Ready to continue?', true)) 19 | { 20 | return $next($console); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Pipe.php: -------------------------------------------------------------------------------- 1 | target = $target; 12 | } 13 | 14 | function __invoke(...$params) { 15 | if (empty($params)) return $this->target; 16 | 17 | [ $before, $through, $after ] = [ [], null, [] ]; 18 | 19 | foreach ($params as $key => $param) { 20 | if (! $through) { 21 | if (is_callable($param)) $through = $param; 22 | 23 | else $before[$key] = $param; 24 | } else { 25 | $after[$key] = $param; 26 | } 27 | } 28 | 29 | $params = [ ...$before, $this->target, ...$after ]; 30 | 31 | $this->target = $through(...$params); 32 | 33 | return $this; 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/Features/SupportDisablingBackButtonCache/SupportDisablingBackButtonCache.php: -------------------------------------------------------------------------------- 1 | make(\Illuminate\Contracts\Http\Kernel::class); 20 | 21 | if ($kernel->hasMiddleware(DisableBackButtonCacheMiddleware::class)) { 22 | return; 23 | } 24 | 25 | $kernel->pushMiddleware(DisableBackButtonCacheMiddleware::class); 26 | } 27 | 28 | public function boot() 29 | { 30 | static::$disableBackButtonCache = true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Wrapped.php: -------------------------------------------------------------------------------- 1 | fallback = $fallback; 14 | 15 | return $this; 16 | } 17 | 18 | function __call($method, $params) 19 | { 20 | if (! method_exists($this->target, $method)) return value($this->fallback); 21 | 22 | try { 23 | return ImplicitlyBoundMethod::call(app(), [$this->target, $method], $params); 24 | } catch (\Throwable $e) { 25 | $shouldPropagate = true; 26 | 27 | $stopPropagation = function () use (&$shouldPropagate) { 28 | $shouldPropagate = false; 29 | }; 30 | 31 | trigger('exception', $this->target, $e, $stopPropagation); 32 | 33 | $shouldPropagate && throw $e; 34 | } 35 | } 36 | } 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /roadmap/islands.md: -------------------------------------------------------------------------------- 1 | ## Ialands 2 | 3 | Take a look at the following example of using @islands for a basic "load more" feature. 4 | 5 | ```php 6 | @php 7 | 8 | use Livewire\Attributes\Computed; 9 | use Livewire\WithPagination; 10 | use App\Models\Post; 11 | 12 | new class extends Livewire\Component { 13 | use WithPagination; 14 | 15 | #[Computed] 16 | public function posts() 17 | { 18 | return Post::paginate(10); 19 | } 20 | 21 | public function loadMore() 22 | { 23 | $this->nextPage(); 24 | } 25 | } 26 | @endphp 27 | 28 |
29 | @island(mode: 'append') 30 | @foreach ($posts as $post) 31 |
32 |

{{ $post->title }}

33 |

{{ $post->content }}

34 |
35 | @endforeach 36 | @enislandd 37 | 38 | @if ($posts->hasMorePages()) 39 | 40 | @endif 41 |
42 | ``` 43 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/ViewContext.php: -------------------------------------------------------------------------------- 1 | slots = $factory->slots; 21 | $this->pushes = $factory->pushes; 22 | $this->prepends = $factory->prepends; 23 | $this->sections = $factory->sections; 24 | } 25 | 26 | function mergeIntoNewEnvironment($__env) 27 | { 28 | $factory = invade($__env); 29 | 30 | $factory->slots = $this->slots; 31 | $factory->pushes = $this->pushes; 32 | $factory->prepends = $this->prepends; 33 | $factory->sections = $this->sections; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/StdClassSynth.php: -------------------------------------------------------------------------------- 1 | $child) { 18 | $data[$key] = $dehydrateChild($key, $child); 19 | } 20 | 21 | return [$data, []]; 22 | } 23 | 24 | function hydrate($value, $meta, $hydrateChild) { 25 | $obj = new stdClass; 26 | 27 | foreach ($value as $key => $child) { 28 | $obj->$key = $hydrateChild($key, $child); 29 | } 30 | 31 | return $obj; 32 | } 33 | 34 | function set(&$target, $key, $value) { 35 | $target->$key = $value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Transparency.php: -------------------------------------------------------------------------------- 1 | target; 14 | } 15 | 16 | function offsetExists(mixed $offset): bool 17 | { 18 | return isset($this->target[$offset]); 19 | } 20 | 21 | function offsetGet(mixed $offset): mixed 22 | { 23 | return $this->target[$offset]; 24 | } 25 | 26 | function offsetSet(mixed $offset, mixed $value): void 27 | { 28 | $this->target[$offset] = $value; 29 | } 30 | 31 | function offsetUnset(mixed $offset): void 32 | { 33 | unset($this->target[$offset]); 34 | } 35 | 36 | function getIterator(): Traversable 37 | { 38 | return (function () { 39 | foreach ($this->target as $key => $value) { 40 | yield $key => $value; 41 | } 42 | })(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/EnumSynth.php: -------------------------------------------------------------------------------- 1 | value, 25 | ['class' => get_class($target)] 26 | ]; 27 | } 28 | 29 | function hydrate($value, $meta) { 30 | if ($value === null || $value === '') return null; 31 | 32 | $class = $meta['class']; 33 | 34 | return $class::from($value); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Features/SupportNavigate/SupportNavigate.php: -------------------------------------------------------------------------------- 1 | forceAssetInjection(); ?>
'; 15 | }); 16 | 17 | Blade::directive('endpersist', function ($expression) { 18 | return '
'; 19 | }); 20 | 21 | app('livewire')->useScriptTagAttributes([ 22 | 'data-navigate-once' => true, 23 | ]); 24 | 25 | Vite::useScriptTagAttributes([ 26 | 'data-navigate-track' => 'reload', 27 | ]); 28 | 29 | Vite::useStyleTagAttributes([ 30 | 'data-navigate-track' => 'reload', 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Features/SupportStreaming/StreamManager.php: -------------------------------------------------------------------------------- 1 | component = $component; 24 | $this->hook = $hook; 25 | } 26 | 27 | public function content($content, $replace = false) 28 | { 29 | $this->content = $content; 30 | $this->replace = $replace; 31 | 32 | return $this; 33 | } 34 | 35 | public function to($name = null, $el = null, $ref = null) 36 | { 37 | $this->name = $name; 38 | $this->el = $el; 39 | $this->ref = $ref; 40 | 41 | $this->hook->stream($this->content, $this->replace, $this->name, $this->el, $this->ref); 42 | 43 | return $this; 44 | } 45 | } -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/ReplaceTemporaryUploadedFileNamespace.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'Livewire\TemporaryUploadedFile is now Livewire\Features\SupportFileUploads\TemporaryUploadedFile.', 14 | before: 'Livewire\TemporaryUploadedFile', 15 | after: 'Livewire\Features\SupportFileUploads\TemporaryUploadedFile', 16 | pattern: '/Livewire\\\\TemporaryUploadedFile/', 17 | replacement: "Livewire\\Features\\SupportFileUploads\\TemporaryUploadedFile", 18 | directories: ['app', 'tests', 'resources/views'] 19 | ); 20 | if ($console->confirm('Continue?', true)) { 21 | return $next($console); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | 3 | Copyright © Caleb Porzio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /roadmap/configuration.md: -------------------------------------------------------------------------------- 1 | 2 | * Ensure compatibility with route caching 3 | 4 | layout('layouts::app') 12 | ->withComponents(function (Components $components) { 13 | $components->directory(__DIR__.'/../app/Livewire'); 14 | 15 | $components->namespace('pages', __DIR__.'/../resources/views/pages'); 16 | }) 17 | ->withRoutes(function (Routes $routes) { 18 | $routes->update(function ($handle) { 19 | return Route::get('/livewire/update', $handle) 20 | ->middleware('web'); 21 | }); 22 | 23 | $routes->upload(function ($handle) { 24 | return Route::post('/livewire/upload', $handle) 25 | ->middleware('web'); 26 | }); 27 | 28 | $routes->preview(function ($handle) { 29 | return Route::get('/livewire/preview/{filename}', $handle) 30 | ->middleware('web'); 31 | }); 32 | })->create(); 33 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/the-tao.php: -------------------------------------------------------------------------------- 1 | filesystem()->directoryExists('resources/views/vendor/livewire')) { 12 | $console->line(' The Livewire pagination views have changed. '); 13 | $console->newLine(); 14 | 15 | $console->line('Republishing of the pagination views is required.'); 16 | 17 | $confirm = $console->confirm('Do you want to republish the pagination views?', true); 18 | 19 | if($confirm) { 20 | $console->call('vendor:publish', [ 21 | '--tag' => 'livewire:pagination', 22 | '--force' => true, 23 | ]); 24 | } 25 | } 26 | 27 | return $next($console); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeConfigInstructions.php: -------------------------------------------------------------------------------- 1 | line(' Manual Upgrade: New configuration '); 12 | $console->newLine(); 13 | $console->line('Livewire V3 has both added and removed certain configuration items.'); 14 | $console->line('If your application has a published configuration file `config/livewire.php`, you will need to update it to account for the following changes.'); 15 | $console->line('Added options: legacy_model_binding, inject_assets, inject_morph_markers, and navigate'); 16 | $console->line('Removed options: app_url, middleware_group, manifest_path, back_button_cache'); 17 | $console->newLine(); 18 | 19 | if($console->confirm('Continue?', true)) 20 | { 21 | return $next($console); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Features/SupportRedirects/HandlesRedirects.php: -------------------------------------------------------------------------------- 1 | set('redirect', $url); 12 | 13 | if ($navigate) store($this)->set('redirectUsingNavigate', true); 14 | 15 | $shouldSkipRender = ! config('livewire.render_on_redirect', false); 16 | 17 | $shouldSkipRender && $this->skipRender(); 18 | } 19 | 20 | public function redirectRoute($name, $parameters = [], $absolute = true, $navigate = false) 21 | { 22 | $this->redirect(route($name, $parameters, $absolute), $navigate); 23 | } 24 | 25 | public function redirectIntended($default = '/', $navigate = false) 26 | { 27 | $url = session()->pull('url.intended', $default); 28 | 29 | $this->redirect($url, $navigate); 30 | } 31 | 32 | public function redirectAction($name, $parameters = [], $absolute = true, $navigate = false) 33 | { 34 | $this->redirect(action($name, $parameters, $absolute), $navigate); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Features/SupportWireRef/SupportWireRef.php: -------------------------------------------------------------------------------- 1 | storeSet('ref', $params['wire:ref']); 14 | } 15 | } 16 | 17 | public function dehydrate($context) 18 | { 19 | if ($this->storeHas('ref')) { 20 | $context->addMemo('ref', $this->storeGet('ref')); 21 | } 22 | } 23 | 24 | public function hydrate($memo) 25 | { 26 | $ref = $memo['ref'] ?? null; 27 | 28 | if (! $ref) return; 29 | 30 | $this->storeSet('ref', $ref); 31 | } 32 | 33 | public function render($view, $data) 34 | { 35 | return function ($html, $replaceHtml) { 36 | $ref = $this->storeGet('ref'); 37 | 38 | if (! $ref) return; 39 | 40 | $replaceHtml(Utils::insertAttributesIntoHtmlRoot($html, [ 41 | 'wire:ref' => $ref, 42 | ])); 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeIntroduction.php: -------------------------------------------------------------------------------- 1 | line(' LIVEWIRE v2 to v3 UPGRADE 🚀 '); 12 | $console->newLine(); 13 | $console->comment('!! Running this command multiple times may result in incorrect replacements !!'); 14 | $console->newLine(); 15 | $console->line('This command will help you upgrade from Livewire v2 to v3.'); 16 | $console->newLine(); 17 | $console->line('Files will be modified in-place, so make sure you have a backup of your project before continuing.'); 18 | $console->newLine(); 19 | $console->newLine(); 20 | $console->line('You can abort this command at any time by pressing ctrl+c.'); 21 | 22 | if($console->confirm('Ready to continue?', true)) 23 | { 24 | return $next($console); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Features/SupportRedirects/Redirector.php: -------------------------------------------------------------------------------- 1 | component->redirect($this->generator->to($path, [], $secure)); 15 | 16 | return $this; 17 | } 18 | 19 | public function away($path, $status = 302, $headers = []) 20 | { 21 | return $this->to($path, $status, $headers); 22 | } 23 | 24 | public function with($key, $value = null) 25 | { 26 | $key = is_array($key) ? $key : [$key => $value]; 27 | 28 | foreach ($key as $k => $v) { 29 | $this->session->flash($k, $v); 30 | } 31 | 32 | return $this; 33 | } 34 | 35 | public function component(Component $component) 36 | { 37 | $this->component = $component; 38 | 39 | return $this; 40 | } 41 | 42 | public function response($to) 43 | { 44 | return $this->createRedirect($to, 302, []); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Checksum.php: -------------------------------------------------------------------------------- 1 | getKey(); 24 | 25 | // Remove the children from the memo in the snapshot, as it is actually Ok 26 | // if the "children" tracking is tampered with. This way JavaScript can 27 | // modify children as it needs to for dom-diffing purposes... 28 | unset($snapshot['memo']['children']); 29 | 30 | $checksum = hash_hmac('sha256', json_encode($snapshot), $hashKey); 31 | 32 | trigger('checksum.generate', $checksum, $snapshot); 33 | 34 | return $checksum; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Features/SupportHtmlAttributeForwarding/SupportHtmlAttributeForwarding.php: -------------------------------------------------------------------------------- 1 | component->getHtmlAttributes(); 13 | 14 | $view->with(['attributes' => new ComponentAttributeBag($attributes)]); 15 | } 16 | 17 | public function renderIsland($name, $view, $properties) 18 | { 19 | $attributes = $this->component->getHtmlAttributes(); 20 | 21 | $view->with(['attributes' => new ComponentAttributeBag($attributes)]); 22 | } 23 | 24 | function hydrate($memo) 25 | { 26 | $attributes = $memo['attributes'] ?? []; 27 | 28 | if (! empty($attributes)) { 29 | $this->component->withHtmlAttributes($attributes); 30 | } 31 | } 32 | 33 | public function dehydrate($context) 34 | { 35 | $attributes = $this->component->getHtmlAttributes(); 36 | 37 | if (! empty($attributes)) { 38 | $context->addMemo('attributes', $attributes); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Features/SupportWireables/WireableSynth.php: -------------------------------------------------------------------------------- 1 | toLivewire(); 20 | } 21 | 22 | function dehydrate($target, $dehydrateChild) 23 | { 24 | $data = $target->toLivewire(); 25 | 26 | foreach ($data as $key => $child) { 27 | $data[$key] = $dehydrateChild($key, $child); 28 | } 29 | 30 | return [ 31 | $data, 32 | ['class' => get_class($target)], 33 | ]; 34 | } 35 | 36 | function hydrate($value, $meta, $hydrateChild) { 37 | foreach ($value as $key => $child) { 38 | $value[$key] = $hydrateChild($key, $child); 39 | } 40 | 41 | return $meta['class']::fromLivewire($value); 42 | } 43 | 44 | function set(&$target, $key, $value) { 45 | $target->{$key} = $value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Features/SupportDisablingBackButtonCache/DisableBackButtonCacheMiddleware.php: -------------------------------------------------------------------------------- 1 | headers->add([ 23 | 'Pragma' => 'no-cache', 24 | 'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT', 25 | 'Cache-Control' => 'no-cache, must-revalidate, no-store, max-age=0, private', 26 | ]); 27 | 28 | // We do flush this in the `SupportDisablingBackButtonCache` hook, but we 29 | // need to do it here as well to ensure that unit tests still work... 30 | SupportDisablingBackButtonCache::$disableBackButtonCache = false; 31 | } 32 | 33 | return $response; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Features/SupportSlots/SlotProxy.php: -------------------------------------------------------------------------------- 1 | get($name); 19 | } 20 | 21 | public function find($name) 22 | { 23 | foreach ($this->slots as $slot) { 24 | if ($slot->getName() === $name) { 25 | return $slot; 26 | } 27 | } 28 | 29 | return null; 30 | } 31 | 32 | public function get($name = 'default') 33 | { 34 | return $this->find($name) ?? new Slot($name, '', $this->component->getId()); 35 | } 36 | 37 | public function has($name): bool 38 | { 39 | return $this->find($name) !== null; 40 | } 41 | 42 | public function toHtml(): string 43 | { 44 | return $this->__toString(); 45 | } 46 | 47 | public function __toString(): string 48 | { 49 | return $this->get('default')->toHtml(); 50 | } 51 | } -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/ArraySynth.php: -------------------------------------------------------------------------------- 1 | $child) { 14 | $target[$key] = $dehydrateChild($key, $child); 15 | } 16 | 17 | return [$target, []]; 18 | } 19 | 20 | function hydrate($value, $meta, $hydrateChild) { 21 | // If we are "hydrating" a value about to be used in an update, 22 | // Let's make sure it's actually an array before try to set it. 23 | // This is most common in the case of "__rm__" values, but also 24 | // applies to any non-array value... 25 | if (! is_array($value)) { 26 | return $value; 27 | } 28 | 29 | foreach ($value as $key => $child) { 30 | $value[$key] = $hydrateChild($key, $child); 31 | } 32 | 33 | return $value; 34 | } 35 | 36 | function set(&$target, $key, $value) { 37 | $target[$key] = $value; 38 | } 39 | 40 | function unset(&$target, $key) { 41 | unset($target[$key]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/WireDirective.php: -------------------------------------------------------------------------------- 1 | name; 20 | } 21 | 22 | public function directive() 23 | { 24 | return $this->directive; 25 | } 26 | 27 | public function value() 28 | { 29 | return $this->value; 30 | } 31 | 32 | public function modifiers() 33 | { 34 | return str($this->directive) 35 | ->replace("wire:{$this->name}", '') 36 | ->explode('.') 37 | ->filter()->values(); 38 | } 39 | 40 | public function hasModifier($modifier) 41 | { 42 | return $this->modifiers()->contains($modifier); 43 | } 44 | 45 | public function toHtml() 46 | { 47 | return (new ComponentAttributeBag([$this->directive => $this->value]))->toHtml(); 48 | } 49 | 50 | public function toString() 51 | { 52 | return (string) $this; 53 | } 54 | 55 | public function __toString() 56 | { 57 | return (string) $this->value; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Features/SupportPageComponents/HandlesPageComponents.php: -------------------------------------------------------------------------------- 1 | getName() !== null ? $this->getName() : $this::class; 18 | 19 | $html = app('livewire')->mount($name, $params); 20 | }); 21 | 22 | $layoutConfig = $layoutConfig ?: new PageComponentConfig; 23 | 24 | $layoutConfig->normalizeViewNameAndParamsForBladeComponents(); 25 | 26 | $response = response(SupportPageComponents::renderContentsIntoLayout($html, $layoutConfig)); 27 | 28 | if (is_callable($layoutConfig->response)) { 29 | call_user_func($layoutConfig->response, $response); 30 | } 31 | 32 | return $response; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Features/SupportReleaseTokens/ReleaseToken.php: -------------------------------------------------------------------------------- 1 | resolveComponentClass($snapshot['memo']['name']); 16 | 17 | if (!isset($snapshot['memo']['release']) || $snapshot['memo']['release'] !== static::generate($componentClass)) { 18 | throw new LivewireReleaseTokenMismatchException; 19 | } 20 | } 21 | 22 | static function generate($componentOrComponentClass): string 23 | { 24 | $livewireReleaseToken = static::$LIVEWIRE_RELEASE_TOKEN; 25 | $appReleaseToken = app('config')->get('livewire.release_token', ''); 26 | $componentReleaseToken = method_exists($componentOrComponentClass, 'releaseToken') ? $componentOrComponentClass::releaseToken() : ''; 27 | 28 | return $livewireReleaseToken . '-' . $appReleaseToken . '-' . $componentReleaseToken; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Features/SupportModels/IsLazy.php: -------------------------------------------------------------------------------- 1 | isUninitializedLazyObject($target); 14 | } 15 | 16 | public function getLazyMeta($target) { 17 | if (! static::$lazyMetas) { 18 | static::$lazyMetas = new \WeakMap(); 19 | } 20 | 21 | if (! static::$lazyMetas->offsetExists($target)) { 22 | throw new \Exception('Lazy model not found'); 23 | } 24 | 25 | return static::$lazyMetas[$target]; 26 | } 27 | 28 | public function setLazyMeta($target, $meta) { 29 | if (! static::$lazyMetas) { 30 | static::$lazyMetas = new \WeakMap(); 31 | } 32 | 33 | static::$lazyMetas[$target] = $meta; 34 | } 35 | 36 | public function makeLazyProxy($class, $meta, $callback) { 37 | if (PHP_VERSION_ID < 80400) { 38 | return $callback(); 39 | } 40 | 41 | $reflector = new \ReflectionClass($class); 42 | 43 | $lazyModel = $reflector->newLazyProxy($callback); 44 | 45 | $this->setLazyMeta($lazyModel, $meta); 46 | 47 | return $lazyModel; 48 | } 49 | } -------------------------------------------------------------------------------- /src/Features/SupportFileDownloads/TestsFileDownloads.php: -------------------------------------------------------------------------------- 1 | effects, 'download'); 12 | 13 | if ($filename) { 14 | PHPUnit::assertEquals( 15 | $filename, 16 | data_get($downloadEffect, 'name') 17 | ); 18 | } else { 19 | PHPUnit::assertNotNull($downloadEffect); 20 | } 21 | 22 | if ($content) { 23 | $downloadedContent = data_get($this->effects, 'download.content'); 24 | 25 | PHPUnit::assertEquals( 26 | $content, 27 | base64_decode($downloadedContent) 28 | ); 29 | } 30 | 31 | if ($contentType) { 32 | PHPUnit::assertEquals( 33 | $contentType, 34 | data_get($this->effects, 'download.contentType') 35 | ); 36 | } 37 | 38 | return $this; 39 | } 40 | 41 | public function assertNoFileDownloaded() 42 | { 43 | $downloadEffect = data_get($this->effects, 'download'); 44 | 45 | PHPUnit::assertNull($downloadEffect); 46 | 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Features/SupportReactiveProps/BaseReactive.php: -------------------------------------------------------------------------------- 1 | getName(); 18 | 19 | store($this->component)->push('reactiveProps', $property); 20 | 21 | $this->originalValueHash = crc32(json_encode($this->getValue())); 22 | } 23 | 24 | public function hydrate() 25 | { 26 | if (SupportReactiveProps::hasPassedInProps($this->component->getId())) { 27 | $updatedValue = SupportReactiveProps::getPassedInProp( 28 | $this->component->getId(), $this->getName() 29 | ); 30 | 31 | $this->setValue($updatedValue); 32 | } 33 | 34 | $this->originalValueHash = crc32(json_encode($this->getValue())); 35 | } 36 | 37 | public function dehydrate($context) 38 | { 39 | if ($this->originalValueHash !== crc32(json_encode($this->getValue()))) { 40 | throw new CannotMutateReactivePropException($this->component->getName(), $this->getName()); 41 | } 42 | 43 | $context->pushMemo('props', $this->getName()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/AddLiveModifierToEntangleDirectives.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The @entangle(...) directive is now deferred by default.', 14 | before: '@entangle(...)', 15 | after: '@entangle(...).live', 16 | pattern: '/@entangle\(((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*)\)(?!\.(?:defer|live))/', 17 | replacement: '@entangle($1).live', 18 | ); 19 | 20 | $this->interactiveReplacement( 21 | console: $console, 22 | title: 'The $wire.entangle function is now deferred by default and has been changed to $wire.$entangle.', 23 | before: '$wire.entangle(...)', 24 | after: '$wire.$entangle(..., true)', 25 | pattern: '/\$wire\.entangle\(((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*)\)(?!\.(?:defer))/', 26 | replacement: '$wire.$entangle($1, true)', 27 | directories: ['resources/views', 'resources/js'] 28 | ); 29 | 30 | return $next($console); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Features/SupportMultipleRootElementDetection/SupportMultipleRootElementDetection.php: -------------------------------------------------------------------------------- 1 | warnAgainstMoreThanOneRootElement($component, $html); 17 | }; 18 | }); 19 | } 20 | 21 | function warnAgainstMoreThanOneRootElement($component, $html) 22 | { 23 | $count = $this->getRootElementCount($html); 24 | 25 | if ($count > 1) { 26 | throw new MultipleRootElementsDetectedException($component); 27 | } 28 | } 29 | 30 | function getRootElementCount($html) 31 | { 32 | $dom = new \DOMDocument(); 33 | 34 | @$dom->loadHTML($html); 35 | 36 | $body = $dom->getElementsByTagName('body')->item(0); 37 | 38 | $count = 0; 39 | 40 | foreach ($body->childNodes as $child) { 41 | if ($child->nodeType == XML_ELEMENT_NODE) { 42 | if ($child->tagName === 'script') continue; 43 | 44 | $count++; 45 | } 46 | } 47 | 48 | return $count; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/SupportConsoleCommands.php: -------------------------------------------------------------------------------- 1 | runningInConsole()) return; 13 | 14 | static::commands([ 15 | Commands\MakeCommand::class, // make:livewire 16 | Commands\LivewireMakeCommand::class, // livewire:make (alias) 17 | Commands\ConvertCommand::class, // livewire:convert 18 | Commands\FormCommand::class, // livewire:form 19 | Commands\AttributeCommand::class, // livewire:attribute 20 | Commands\LayoutCommand::class, // livewire:layout 21 | Commands\StubsCommand::class, // livewire:stubs 22 | Commands\S3CleanupCommand::class, // livewire:configure-s3-upload-cleanup 23 | Commands\PublishCommand::class, // livewire:publish 24 | Commands\UpgradeCommand::class, // livewire:upgrade 25 | ]); 26 | } 27 | 28 | static function commands($commands) 29 | { 30 | $commands = is_array($commands) ? $commands : func_get_args(); 31 | 32 | // Filter out null values 33 | $commands = array_filter($commands); 34 | 35 | Artisan::starting(fn(Artisan $artisan) => $artisan->resolveCommands($commands)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/SupportFileUploads.php: -------------------------------------------------------------------------------- 1 | runningUnitTests()) { 15 | // Don't actually generate S3 signedUrls during testing. 16 | GenerateSignedUploadUrlFacade::swap(new class extends GenerateSignedUploadUrl { 17 | public function forS3($file, $visibility = '') { return []; } 18 | }); 19 | } 20 | 21 | app('livewire')->propertySynthesizer([ 22 | FileUploadSynth::class, 23 | ]); 24 | 25 | on('call', function ($component, $method, $params, $componentContext, $earlyReturn) { 26 | if ($method === '_startUpload') { 27 | if (! method_exists($component, $method)) { 28 | throw new MissingFileUploadsTraitException($component); 29 | } 30 | } 31 | }); 32 | 33 | Route::post('/livewire/upload-file', [FileUploadController::class, 'handle']) 34 | ->name('livewire.upload-file'); 35 | 36 | Route::get('/livewire/preview-file/{filename}', [FilePreviewController::class, 'handle']) 37 | ->name('livewire.preview-file'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Mechanisms/ExtendBlade/DeterministicBladeKeys.php: -------------------------------------------------------------------------------- 1 | currentPathHash) { 16 | throw new \Exception('Latest compiled component path not found.'); 17 | } 18 | 19 | $path = $this->currentPathHash; 20 | $count = $this->counter(); 21 | 22 | // $key = "lw-[hash of Blade view path]-[current @livewire directive count]" 23 | return 'lw-' . $this->currentPathHash . '-' . $count; 24 | } 25 | 26 | public function counter() 27 | { 28 | if (! isset($this->countersByPath[$this->currentPathHash])) { 29 | $this->countersByPath[$this->currentPathHash] = 0; 30 | } 31 | 32 | return $this->countersByPath[$this->currentPathHash]++; 33 | } 34 | 35 | public function hookIntoCompile(BladeCompiler $compiler, $viewContent) 36 | { 37 | $path = $compiler->getPath(); 38 | 39 | // If there is no path this means this Blade is being compiled 40 | // with ->compileString(...) directly instead of ->compile() 41 | // therefore we'll generate a hash of the contents instead 42 | if ($path === null) { 43 | $path = $viewContent; 44 | } 45 | 46 | $this->currentPathHash = crc32($path); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/ComponentContext.php: -------------------------------------------------------------------------------- 1 | mounting; 21 | } 22 | 23 | public function addEffect($key, $value) 24 | { 25 | if (is_array($key)) { 26 | foreach ($key as $iKey => $iValue) $this->addEffect($iKey, $iValue); 27 | 28 | return; 29 | } 30 | 31 | $this->effects[$key] = $value; 32 | } 33 | 34 | public function pushEffect($key, $value, $iKey = null) 35 | { 36 | if (! isset($this->effects[$key])) $this->effects[$key] = []; 37 | 38 | if ($iKey) { 39 | $this->effects[$key][$iKey] = $value; 40 | } else { 41 | $this->effects[$key][] = $value; 42 | } 43 | } 44 | 45 | public function addMemo($key, $value) 46 | { 47 | $this->memo[$key] = $value; 48 | } 49 | 50 | public function pushMemo($key, $value, $iKey = null) 51 | { 52 | if (! isset($this->memo[$key])) $this->memo[$key] = []; 53 | 54 | if ($iKey) { 55 | $this->memo[$key][$iKey] = $value; 56 | } else { 57 | $this->memo[$key][] = $value; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/FormCommand.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'The forgetComputed component method is replaced by PHP\'s unset function', 14 | before: '$this->forgetComputed(\'title\')', 15 | after: 'unset($this->title)', 16 | pattern: '/\$this->forgetComputed\((.*?)\);/', 17 | replacement: function($matches) { 18 | $replacement = ''; 19 | 20 | if(isset($matches[1])) { 21 | preg_match_all('/(?:\'|")(.*?)(?:\'|")/', $matches[1], $keys); 22 | 23 | $replacement .= 'unset('; 24 | foreach($keys[1] ?? [] as $key) { 25 | $replacement .= '$this->' . $key . ', '; 26 | } 27 | $replacement = rtrim($replacement, ', '); 28 | $replacement .= ');'; 29 | } 30 | 31 | // Leave unchanged if no replacement was possible. 32 | return $replacement ?: $matches[0]; 33 | }, 34 | directories: 'app', 35 | ); 36 | 37 | return $next($console); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/CollectionSynth.php: -------------------------------------------------------------------------------- 1 | all(); 16 | 17 | foreach ($data as $key => $child) { 18 | $data[$key] = $dehydrateChild($key, $child); 19 | } 20 | 21 | return [ 22 | $data, 23 | ['class' => get_class($target)] 24 | ]; 25 | } 26 | 27 | function hydrate($value, $meta, $hydrateChild) { 28 | foreach ($value as $key => $child) { 29 | $value[$key] = $hydrateChild($key, $child); 30 | } 31 | 32 | return new $meta['class']($value); 33 | } 34 | 35 | function &get(&$target, $key) { 36 | // We need this "$reader" callback to get a reference to 37 | // the items property inside collections. Otherwise, 38 | // we'd receive a copy instead of the reference. 39 | $reader = function & ($object, $property) { 40 | $value = & \Closure::bind(function & () use ($property) { 41 | return $this->$property; 42 | }, $object, $object)->__invoke(); 43 | 44 | return $value; 45 | }; 46 | 47 | $items =& $reader($target, 'items'); 48 | 49 | return $items[$key]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/CarbonSynth.php: -------------------------------------------------------------------------------- 1 | DateTime::class, 14 | 'nativeImmutable' => DateTimeImmutable::class, 15 | 'carbon' => Carbon::class, 16 | 'carbonImmutable' => CarbonImmutable::class, 17 | 'illuminate' => \Illuminate\Support\Carbon::class, 18 | ]; 19 | 20 | public static $key = 'cbn'; 21 | 22 | static function match($target) { 23 | foreach (static::$types as $type => $class) { 24 | if ($target instanceof $class) return true; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | static function matchByType($type) { 31 | return is_subclass_of($type, DateTimeInterface::class); 32 | } 33 | 34 | function dehydrate($target) { 35 | return [ 36 | $target->format(DateTimeInterface::ATOM), 37 | ['type' => array_search(get_class($target), static::$types)], 38 | ]; 39 | } 40 | 41 | static function hydrateFromType($type, $value) { 42 | if ($value === '' || $value === null) return null; 43 | 44 | return new $type($value); 45 | } 46 | 47 | function hydrate($value, $meta) { 48 | if ($value === '' || $value === null) return null; 49 | 50 | return new static::$types[$meta['type']]($value); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Features/SupportSlots/HandlesSlots.php: -------------------------------------------------------------------------------- 1 | slotsForSkippedChildRenders; 15 | } 16 | 17 | public function withChildSlots(array $slots, $childId) 18 | { 19 | foreach ($slots as $name => $content) { 20 | $this->slotsForSkippedChildRenders[] = (new Slot($name, $content, $childId, $this->getId()))->toHtml(); 21 | } 22 | } 23 | 24 | /** 25 | * Child concerns... 26 | */ 27 | protected array $slots = []; 28 | 29 | public function getSlots() 30 | { 31 | return $this->slots; 32 | } 33 | 34 | public function withSlots(array $slots, $parent = null): self 35 | { 36 | $parentId = $parent && method_exists($parent, 'getId') ? $parent->getId() : null; 37 | 38 | foreach ($slots as $name => $content) { 39 | $this->slots[] = new Slot($name, $content, $this->getId(), $parentId); 40 | } 41 | 42 | return $this; 43 | } 44 | 45 | public function withPlaceholderSlots(array $slots): self 46 | { 47 | foreach ($slots as $slot) { 48 | $this->slots[] = new PlaceholderSlot( 49 | $slot['name'], 50 | $slot['componentId'], 51 | $slot['parentId'], 52 | ); 53 | } 54 | 55 | return $this; 56 | } 57 | 58 | 59 | } -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/AttributeCommand.php: -------------------------------------------------------------------------------- 1 | app = $app; 20 | } 21 | 22 | function temporarilyDisableExceptionHandlingAndMiddleware($callback) 23 | { 24 | $cachedHandler = app(ExceptionHandler::class); 25 | 26 | $cachedShouldSkipMiddleware = $this->app->shouldSkipMiddleware(); 27 | 28 | $this->withoutExceptionHandling([HttpException::class, AuthorizationException::class])->withoutMiddleware(); 29 | 30 | $result = $callback($this); 31 | 32 | $this->app->instance(ExceptionHandler::class, $cachedHandler); 33 | 34 | if (! $cachedShouldSkipMiddleware) { 35 | unset($this->app['middleware.disable']); 36 | } 37 | 38 | return $result; 39 | } 40 | 41 | function withoutHandling($except = []) 42 | { 43 | return $this->withoutExceptionHandling($except); 44 | } 45 | 46 | function addHeaders(array $headers) 47 | { 48 | $this->serverVariables = $this->transformHeadersToServerVars($headers); 49 | 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/PublishCommand.php: -------------------------------------------------------------------------------- 1 | option('assets')) { 21 | $this->publishAssets(); 22 | } elseif ($this->option('config')) { 23 | $this->publishConfig(); 24 | } elseif ($this->option('pagination')) { 25 | $this->publishPagination(); 26 | } else { 27 | $this->publishConfig(); 28 | $this->publishPagination(); 29 | } 30 | } 31 | 32 | public function publishAssets() 33 | { 34 | $this->call('vendor:publish', ['--tag' => 'livewire:assets', '--force' => true]); 35 | } 36 | 37 | public function publishConfig() 38 | { 39 | $this->call('vendor:publish', ['--tag' => 'livewire:config', '--force' => true]); 40 | } 41 | 42 | public function publishPagination() 43 | { 44 | $this->call('vendor:publish', ['--tag' => 'livewire:pagination', '--force' => true]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livewire/livewire", 3 | "description": "A front-end framework for Laravel.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Caleb Porzio", 8 | "email": "calebporzio@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^8.1", 13 | "illuminate/database": "^10.0|^11.0|^12.0", 14 | "illuminate/routing": "^10.0|^11.0|^12.0", 15 | "illuminate/support": "^10.0|^11.0|^12.0", 16 | "illuminate/validation": "^10.0|^11.0|^12.0", 17 | "league/mime-type-detection": "^1.9", 18 | "symfony/console": "^6.0|^7.0", 19 | "symfony/http-kernel": "^6.2|^7.0", 20 | "laravel/prompts": "^0.1.24|^0.2|^0.3" 21 | }, 22 | "require-dev": { 23 | "psy/psysh": "^0.11.22|^0.12", 24 | "mockery/mockery": "^1.3.1", 25 | "phpunit/phpunit": "^10.4|^11.5", 26 | "laravel/framework": "^10.15.0|^11.0|^12.0", 27 | "orchestra/testbench": "^8.21.0|^9.0|^10.0", 28 | "orchestra/testbench-dusk": "^8.24|^9.1|^10.0", 29 | "calebporzio/sushi": "^2.1" 30 | }, 31 | "autoload": { 32 | "files": [ 33 | "src/helpers.php" 34 | ], 35 | "psr-4": { 36 | "Livewire\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "App\\": "vendor/orchestra/testbench-core/laravel/app", 42 | "Tests\\": "tests/", 43 | "LegacyTests\\": "legacy_tests/" 44 | } 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Livewire\\LivewireServiceProvider" 50 | ], 51 | "aliases": { 52 | "Livewire": "Livewire\\Livewire" 53 | } 54 | } 55 | }, 56 | "minimum-stability": "dev", 57 | "prefer-stable": true 58 | } 59 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/FileManipulationCommand.php: -------------------------------------------------------------------------------- 1 | replaceFirst(app()->getNamespace(), ''); 24 | 25 | $livewireFolder = app_path($namespace->explode('\\')->implode(DIRECTORY_SEPARATOR)); 26 | 27 | return ! File::isDirectory($livewireFolder); 28 | } 29 | 30 | public function writeWelcomeMessage() 31 | { 32 | $asciiLogo = << _._ 34 | / /o\ \ || () () __ 35 | |_\ /_| || || \\\// /_\ \\\ // || |~~ /_\ 36 | |`|`| || || \/ \\\_ \^/ || || \\\_ 37 | EOT; 38 | // _._ 39 | // / /o\ \ || () () __ 40 | // |_\ /_| || || \\\// /_\ \\\ // || |~~ /_\ 41 | // |`|`| || || \/ \\\_ \^/ || || \\\_ 42 | $this->line("\n".$asciiLogo."\n"); 43 | $this->line("\nCongratulations, you've created your first Livewire component! 🎉🎉🎉\n"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/UpgradeAlpineInstructions.php: -------------------------------------------------------------------------------- 1 | line(' Manual Upgrade: Remove Alpine references '); 12 | $console->newLine(); 13 | $console->line('Livewire version 3 ships with AlpineJS by default.'); 14 | $console->line('If you use Alpine in your Livewire application, you will need to remove it and any of the plugins listed below so that Livewire\'s built-in version doesn\'t conflict with it.'); 15 | $console->line('Livewire version 3 now also ships with the following Alpine plugins:'); 16 | $console->line('- Intersect'); 17 | $console->line('- Collapse'); 18 | $console->line('- Persist'); 19 | $console->line('- Morph'); 20 | $console->line('- Focus'); 21 | $console->line('- Mask'); 22 | $console->newLine(); 23 | $console->line('If you were accessing Alpine via JS bundle you can now import Livewire\'s ESM module instead and call Livewire.start() when ready, for example:'); 24 | $console->newLine(); 25 | $console->line('import { Livewire, Alpine } from \'../../vendor/livewire/livewire/dist/livewire.esm\';'); 26 | $console->line('Alpine.plugin(yourCustomPlugin);'); 27 | $console->line('Livewire.start();'); 28 | 29 | if($console->confirm('Continue?', true)) 30 | { 31 | return $next($console); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Livewire Logo

2 | 3 |

4 | 5 | Total Downloads 6 | 7 | 8 | Latest Stable Version 9 | 10 | 11 | License 12 | 13 |

14 | 15 | ## Introduction 16 | 17 | Livewire is a full-stack framework for Laravel that allows you to build dynamic UI components without leaving PHP. 18 | 19 | ## Official Documentation 20 | 21 | You can read the official documentation on the [Livewire website](https://livewire.laravel.com/docs). 22 | 23 | ## Contributing 24 | 25 | 26 | Thank you for considering contributing to Livewire! You can read the contribution guide [here](.github/CONTRIBUTING.md). 27 | 28 | ## Code of Conduct 29 | 30 | 31 | In order to ensure that the Laravel community is welcoming to all, please review and abide by Laravel's [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 32 | 33 | ## Security Vulnerabilities 34 | 35 | 36 | Please review [our security policy](https://github.com/livewire/livewire/security/policy) on how to report security vulnerabilities. 37 | 38 | ## License 39 | 40 | 41 | Livewire is open-sourced software licensed under the [MIT license](LICENSE.md). 42 | -------------------------------------------------------------------------------- /src/Features/SupportWithMethod/SupportWithMethod.php: -------------------------------------------------------------------------------- 1 | component, 'with')) { 14 | return; 15 | } 16 | 17 | // Call the with() method and get the additional data 18 | $withData = wrap($this->component)->with(); 19 | 20 | // Ensure the with() method returns an array 21 | if (! is_array($withData)) { 22 | return; 23 | } 24 | 25 | // Merge the with() data with the existing view data, giving precedence to with() data 26 | $mergedData = array_merge($data, $withData); 27 | 28 | // Update the view's data with the merged data 29 | $view->with($withData); 30 | } 31 | 32 | public function renderIsland($name, $view, $data) 33 | { 34 | // Check if the component has a with() method 35 | if (! method_exists($this->component, 'with')) { 36 | return; 37 | } 38 | 39 | // Call the with() method and get the additional data 40 | $withData = wrap($this->component)->with(); 41 | 42 | // Ensure the with() method returns an array 43 | if (! is_array($withData)) { 44 | return; 45 | } 46 | 47 | // Merge the with() data with the existing view data, giving precedence to with() data 48 | $mergedData = array_merge($data, $withData); 49 | 50 | // Update the view's data with the merged data 51 | $view->with($withData); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Features/SupportStreaming/SupportStreaming.php: -------------------------------------------------------------------------------- 1 | 'directive', 21 | $hasEl => 'element', 22 | $hasRef => 'ref', 23 | }; 24 | 25 | static::streamContent([ 26 | 'id' => $this->component->getId(), 27 | 'type' => $type, 28 | 'content' => $content, 29 | 'mode' => $replace ? 'replace' : 'default', 30 | 'name' => $name, 31 | 'el' => $el, 32 | 'ref' => $ref, 33 | ]); 34 | } 35 | 36 | public static function ensureStreamResponseStarted() 37 | { 38 | if (static::$response) return; 39 | 40 | static::$response = response()->stream(fn () => null , 200, [ 41 | 'Cache-Control' => 'no-cache', 42 | 'Content-Type' => 'text/event-stream', 43 | 'X-Accel-Buffering' => 'no', 44 | 'X-Livewire-Stream' => true, 45 | ]); 46 | 47 | static::$response->sendHeaders(); 48 | } 49 | 50 | public static function streamContent($body) 51 | { 52 | echo json_encode(['stream' => true, 'body' => $body, 'endStream' => true]); 53 | 54 | if (ob_get_level() > 0) { 55 | ob_flush(); 56 | } 57 | 58 | flush(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Features/SupportSession/BaseSession.php: -------------------------------------------------------------------------------- 1 | exists()) return; 19 | 20 | $fromSession = $this->read(); 21 | 22 | $this->setValue($fromSession); 23 | } 24 | 25 | public function dehydrate($context) 26 | { 27 | $this->write(); 28 | } 29 | 30 | protected function exists() 31 | { 32 | return Session::exists($this->key()); 33 | } 34 | 35 | protected function read() 36 | { 37 | return Session::get($this->key()); 38 | } 39 | 40 | protected function write() 41 | { 42 | Session::put($this->key(), $this->getValue()); 43 | } 44 | 45 | protected function key() 46 | { 47 | if (! $this->key) { 48 | return (string) 'lw' . crc32($this->component->getName() . $this->getName()); 49 | } 50 | 51 | return self::replaceDynamicPlaceholders($this->key, $this->component); 52 | } 53 | 54 | static function replaceDynamicPlaceholders($key, $component) 55 | { 56 | return preg_replace_callback('/\{(.*)\}/U', function ($matches) use ($component) { 57 | return data_get($component, $matches[1], function () use ($matches) { 58 | throw new \Exception('Unable to evaluate dynamic session key placeholder: '.$matches[0]); 59 | }); 60 | }, $key); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Features/SupportJsModules/SupportJsModules.php: -------------------------------------------------------------------------------- 1 | new($component); 19 | 20 | if (! method_exists($instance, 'scriptModuleSrc')) { 21 | throw new \Exception('Component '.$component.' does not have a script source.'); 22 | } 23 | 24 | $path = $instance->scriptModuleSrc(); 25 | 26 | if (! file_exists($path)) { 27 | throw new \Exception('Script file not found: '.$path); 28 | } 29 | 30 | $source = file_get_contents($path); 31 | 32 | $filemtime = filemtime($path); 33 | 34 | return Utils::pretendResponseIsFileFromString( 35 | $source, 36 | $filemtime, 37 | $component.'.js', 38 | ); 39 | }); 40 | } 41 | 42 | public function dehydrate($context) 43 | { 44 | if (! $context->isMounting()) return; 45 | 46 | if (method_exists($this->component, 'scriptModuleSrc')) { 47 | $path = $this->component->scriptModuleSrc(); 48 | 49 | $filemtime = filemtime($path); 50 | 51 | $hash = crc32($filemtime); 52 | 53 | $context->addEffect('scriptModule', $hash); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Livewire.php: -------------------------------------------------------------------------------- 1 | hasOldLayout()) 12 | { 13 | $console->line(' The Livewire default layout has changed. '); 14 | $console->newLine(); 15 | 16 | $console->line('When rendering full-page components Livewire would use the "layouts.app" view as the default layout. This has been changed to "components.layouts.app".'); 17 | 18 | $choice = $console->choice('Would you like to migrate or keep the old layout?', [ 19 | 'migrate', 20 | 'keep', 21 | ], 'migrate'); 22 | 23 | if($choice == 'keep') { 24 | $console->line('Keeping the old default layout...'); 25 | 26 | $this->publishConfigIfMissing($console); 27 | 28 | $console->line('Setting the default layout to "layouts.app"...'); 29 | 30 | $this->patternReplacement('/components\.layouts\.app/', 'layouts.app', 'config'); 31 | 32 | return $next($console); 33 | } 34 | 35 | $console->line('Setting the default layout to "components.layouts.app"...'); 36 | 37 | $this->patternReplacement('/layouts\.app/', 'components.layouts.app', 'config'); 38 | } 39 | 40 | return $next($console); 41 | } 42 | 43 | protected function hasOldLayout() 44 | { 45 | return config('livewire.class_namespace') === 'layouts.app' || $this->filesystem()->exists('resources/views/layouts/app.blade.php'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/FileUploadController.php: -------------------------------------------------------------------------------- 1 | new Middleware($middleware), $middleware); 25 | } 26 | 27 | public function handle() 28 | { 29 | abort_unless(request()->hasValidSignature(), 401); 30 | 31 | $disk = FileUploadConfiguration::disk(); 32 | 33 | $filePaths = $this->validateAndStore(request('files'), $disk); 34 | 35 | return ['paths' => $filePaths]; 36 | } 37 | 38 | public function validateAndStore($files, $disk) 39 | { 40 | Validator::make(['files' => $files], [ 41 | 'files.*' => FileUploadConfiguration::rules() 42 | ])->validate(); 43 | 44 | $fileHashPaths = collect($files)->map(function ($file) use ($disk) { 45 | return FileUploadConfiguration::storeTemporaryFile($file, $disk); 46 | }); 47 | 48 | // Strip out the temporary upload directory from the paths. 49 | return $fileHashPaths->map(function ($path) { return str_replace(FileUploadConfiguration::path('/'), '', $path); }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Features/SupportTesting/ComponentState.php: -------------------------------------------------------------------------------- 1 | component; 20 | } 21 | 22 | function getSnapshot() 23 | { 24 | return $this->snapshot; 25 | } 26 | 27 | function getSnapshotData() 28 | { 29 | return $this->untupleify($this->snapshot['data']); 30 | } 31 | 32 | function getEffects() 33 | { 34 | return $this->effects; 35 | } 36 | 37 | function getView() 38 | { 39 | return $this->view; 40 | } 41 | 42 | function getResponse() 43 | { 44 | return $this->response; 45 | } 46 | 47 | function untupleify($payload) { 48 | $value = Utils::isSyntheticTuple($payload) ? $payload[0] : $payload; 49 | 50 | if (is_array($value)) { 51 | foreach ($value as $key => $child) { 52 | $value[$key] = $this->untupleify($child); 53 | } 54 | } 55 | 56 | return $value; 57 | } 58 | 59 | function getHtml($stripInitialData = false) 60 | { 61 | $html = $this->html; 62 | 63 | if ($stripInitialData) { 64 | $removeMe = (string) str($html)->betweenFirst( 65 | 'wire:snapshot="', '"' 66 | ); 67 | 68 | $html = str_replace($removeMe, '', $html); 69 | 70 | $removeMe = (string) str($html)->betweenFirst( 71 | 'wire:effects="', '"' 72 | ); 73 | 74 | $html = str_replace($removeMe, '', $html); 75 | } 76 | 77 | return $html; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/StubsCommand.php: -------------------------------------------------------------------------------- 1 | makeDirectory($stubsPath); 22 | } 23 | 24 | file_put_contents( 25 | $stubsPath.'/livewire.stub', 26 | file_get_contents(__DIR__.'/livewire.stub') 27 | ); 28 | 29 | file_put_contents( 30 | $stubsPath.'/livewire.inline.stub', 31 | file_get_contents(__DIR__.'/livewire.inline.stub') 32 | ); 33 | 34 | file_put_contents( 35 | $stubsPath.'/livewire.view.stub', 36 | file_get_contents(__DIR__.'/livewire.view.stub') 37 | ); 38 | 39 | file_put_contents( 40 | $stubsPath.'/livewire.test.stub', 41 | file_get_contents(__DIR__.'/livewire.test.stub') 42 | ); 43 | 44 | file_put_contents( 45 | $stubsPath.'/livewire.pest.stub', 46 | file_get_contents(__DIR__.'/livewire.pest.stub') 47 | ); 48 | 49 | file_put_contents( 50 | $stubsPath.'/livewire.form.stub', 51 | file_get_contents(__DIR__.'/livewire.form.stub') 52 | ); 53 | 54 | file_put_contents( 55 | $stubsPath.'/livewire.attribute.stub', 56 | file_get_contents(__DIR__.'/livewire.attribute.stub') 57 | ); 58 | 59 | $this->info('Stubs published successfully.'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Features/SupportSlots/PlaceholderSlot.php: -------------------------------------------------------------------------------- 1 | name; 18 | } 19 | 20 | public function getComponentId(): string 21 | { 22 | return $this->componentId; 23 | } 24 | 25 | public function getParentId(): ?string 26 | { 27 | return $this->parentComponentId; 28 | } 29 | 30 | public function toHtml(): string 31 | { 32 | $parentPart = $this->parentComponentId ? ['parent' => $this->parentComponentId] : []; 33 | 34 | return $this->wrapWithFragmentMarkers('', [ 35 | 'name' => $this->name, 36 | 'type' => 'slot', 37 | 'id' => $this->componentId, 38 | // This is here for JS to match opening and close markers... 39 | 'token' => crc32($this->name . $this->componentId . $this->parentComponentId ?? ''), 40 | 'mode' => 'skip', 41 | ...$parentPart, 42 | ]); 43 | } 44 | 45 | protected function wrapWithFragmentMarkers($output, $metadata) 46 | { 47 | $startFragment = ""; 48 | 49 | $endFragment = ""; 50 | 51 | return $startFragment . $output . $endFragment; 52 | } 53 | 54 | protected function encodeFragmentMetadata($metadata) 55 | { 56 | $output = ''; 57 | 58 | foreach ($metadata as $key => $value) { 59 | $output .= "{$key}={$value}|"; 60 | } 61 | 62 | return rtrim($output, '|'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Features/SupportConsoleCommands/Commands/Upgrade/ChangeTestAssertionMethods.php: -------------------------------------------------------------------------------- 1 | interactiveReplacement( 12 | console: $console, 13 | title: 'assertEmitted is now assertDispatched.', 14 | before: 'assertEmitted', 15 | after: 'assertDispatched', 16 | pattern: '/assertEmitted\((.*)\)/', 17 | replacement: 'assertDispatched($1)', 18 | directories: 'tests' 19 | ); 20 | 21 | $this->interactiveReplacement( 22 | console: $console, 23 | title: 'assertEmittedTo is now assertDispatchedTo.', 24 | before: 'assertEmittedTo', 25 | after: 'assertDispatchedTo', 26 | pattern: '/assertEmittedTo\((.*)\)/', 27 | replacement: 'assertDispatchedTo($1)', 28 | directories: 'tests' 29 | ); 30 | 31 | $this->interactiveReplacement( 32 | console: $console, 33 | title: 'assertNotEmitted is now assertNotDispatched.', 34 | before: 'assertNotEmitted', 35 | after: 'assertNotDispatched', 36 | pattern: '/assertNotEmitted\((.*)\)/', 37 | replacement: 'assertNotDispatched($1)', 38 | directories: 'tests' 39 | ); 40 | 41 | $this->interactiveReplacement( 42 | console: $console, 43 | title: 'assertEmittedUp is no longer available.', 44 | before: 'assertEmittedUp', 45 | after: '', 46 | pattern: '/->assertEmittedUp\(.*\)/', 47 | replacement: '', 48 | directories: 'tests' 49 | ); 50 | 51 | return $next($console); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Features/SupportSlots/Slot.php: -------------------------------------------------------------------------------- 1 | name; 19 | } 20 | 21 | public function getComponentId(): string 22 | { 23 | return $this->componentId; 24 | } 25 | 26 | public function getParentId(): ?string 27 | { 28 | return $this->parentComponentId; 29 | } 30 | 31 | public function toHtml(): string 32 | { 33 | $parentPart = $this->parentComponentId ? ['parent' => $this->parentComponentId] : []; 34 | 35 | return $this->wrapWithFragmentMarkers($this->content, [ 36 | 'name' => $this->name, 37 | 'type' => 'slot', 38 | 'id' => $this->componentId, 39 | // This is here for JS to match opening and close markers... 40 | 'token' => crc32($this->name . $this->componentId . $this->parentComponentId ?? ''), 41 | 'mode' => 'morph', 42 | ...$parentPart, 43 | ]); 44 | } 45 | 46 | protected function wrapWithFragmentMarkers($output, $metadata) 47 | { 48 | $startFragment = ""; 49 | 50 | $endFragment = ""; 51 | 52 | return $startFragment . $output . $endFragment; 53 | } 54 | 55 | protected function encodeFragmentMetadata($metadata) 56 | { 57 | $output = ''; 58 | 59 | foreach ($metadata as $key => $value) { 60 | $output .= "{$key}={$value}|"; 61 | } 62 | 63 | return rtrim($output, '|'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Features/SupportPageComponents/PageComponentConfig.php: -------------------------------------------------------------------------------- 1 | view = $view ?: config('livewire.component_layout'); 21 | $this->viewContext = new ViewContext; 22 | } 23 | 24 | function mergeParams($toMerge) 25 | { 26 | $this->params = array_merge($toMerge, $this->params); 27 | } 28 | 29 | function normalizeViewNameAndParamsForBladeComponents() 30 | { 31 | // If a user passes the class name of a Blade component to the 32 | // layout macro (or uses inside their config), we need to 33 | // convert it to it's "view" name so Blade doesn't break. 34 | $view = $this->view; 35 | $params = $this->params; 36 | 37 | $attributes = $params['attributes'] ?? []; 38 | unset($params['attributes']); 39 | 40 | if (is_subclass_of($view, \Illuminate\View\Component::class)) { 41 | $layout = app()->makeWith($view, $params); 42 | $view = $layout->resolveView()->name(); 43 | $params = array_merge($params, $layout->resolveView()->getData()); 44 | } else { 45 | $layout = new AnonymousComponent($view, $params); 46 | } 47 | 48 | $layout->withAttributes($attributes); 49 | 50 | $params = array_merge($params, $layout->data()); 51 | 52 | $this->view = $view; 53 | $this->params = $params; 54 | 55 | // Remove default slot if present... 56 | if (isset($this->slots['default'])) unset($this->slots['default']); 57 | 58 | return $this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Features/SupportValidation/SupportValidation.php: -------------------------------------------------------------------------------- 1 | component->setErrorBag( 15 | $memo['errors'] ?? [] 16 | ); 17 | } 18 | 19 | function render($view, $data) 20 | { 21 | $errors = (new ViewErrorBag)->put('default', $this->component->getErrorBag()); 22 | 23 | $revert = Utils::shareWithViews('errors', $errors); 24 | 25 | return function () use ($revert) { 26 | // After the component has rendered, let's revert our global 27 | // sharing of the "errors" variable with blade views... 28 | $revert(); 29 | }; 30 | } 31 | 32 | function renderIsland($name, $view, $data) 33 | { 34 | $errors = (new ViewErrorBag)->put('default', $this->component->getErrorBag()); 35 | 36 | $revert = Utils::shareWithViews('errors', $errors); 37 | 38 | return function () use ($revert) { 39 | $revert(); 40 | }; 41 | } 42 | 43 | function dehydrate($context) 44 | { 45 | $errors = $this->component->getErrorBag()->toArray(); 46 | 47 | // Only persist errors that were born from properties on the component 48 | // and not from custom validators (Validator::make) that were run. 49 | $context->addMemo('errors', collect($errors) 50 | ->filter(function ($value, $key) { 51 | return Utils::hasProperty($this->component, $key); 52 | }) 53 | ->toArray() 54 | ); 55 | } 56 | 57 | function exception($e, $stopPropagation) 58 | { 59 | if (! $e instanceof ValidationException) return; 60 | 61 | $this->component->setErrorBag($e->validator->errors()); 62 | 63 | $stopPropagation(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Features/SupportWireModelingNestedComponents/BaseModelable.php: -------------------------------------------------------------------------------- 1 | $value) { 18 | if (str($key)->startsWith('wire:model')) { 19 | $outer = $value; 20 | store($this->component)->push('bindings-directives', $key, $value); 21 | break; 22 | } 23 | } 24 | 25 | if ($outer === null) return; 26 | 27 | $inner = $this->getName(); 28 | 29 | store($this->component)->push('bindings', $inner, $outer); 30 | 31 | $this->setValue(data_get($parent, $outer)); 32 | } 33 | 34 | // This update hook is for the following scenario: 35 | // An modelable value has changed in the browser. 36 | // A network request is triggered from the parent. 37 | // The request contains both parent and child component updates. 38 | // The parent finishes it's request and the "updated" value is 39 | // overridden in the parent's lifecycle (ex. a form field being reset). 40 | // Without this hook, the child's value will not honor that change 41 | // and will instead still be updated to the old value, even though 42 | // the parent changed the bound value. This hook detects if the parent 43 | // has provided a value during this request and ensures that it is the 44 | // final value for the child's request... 45 | function update($fullPath, $newValue) 46 | { 47 | if (store($this->component)->get('hasBeenSeeded', false)) { 48 | $oldValue = $this->getValue(); 49 | 50 | return function () use ($oldValue) { 51 | $this->setValue($oldValue); 52 | }; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Features/SupportFormObjects/FormObjectSynth.php: -------------------------------------------------------------------------------- 1 | toArray(); 22 | 23 | foreach ($data as $key => $child) { 24 | $data[$key] = $dehydrateChild($key, $child); 25 | } 26 | 27 | return [$data, ['class' => get_class($target)]]; 28 | } 29 | 30 | function hydrate($data, $meta, $hydrateChild) 31 | { 32 | $form = new $meta['class']($this->context->component, $this->path); 33 | 34 | $callBootMethod = static::bootFormObject($this->context->component, $form, $this->path); 35 | 36 | foreach ($data as $key => $child) { 37 | if ($child === null && Utils::propertyIsTypedAndUninitialized($form, $key)) { 38 | continue; 39 | } 40 | 41 | $form->$key = $hydrateChild($key, $child); 42 | } 43 | 44 | $callBootMethod(); 45 | 46 | return $form; 47 | } 48 | 49 | function set(&$target, $key, $value) 50 | { 51 | if ($value === null && Utils::propertyIsTyped($target, $key) && ! Utils::getProperty($target, $key)->getType()->allowsNull()) { 52 | unset($target->$key); 53 | } else { 54 | $target->$key = $value; 55 | } 56 | } 57 | 58 | public static function bootFormObject($component, $form, $path) 59 | { 60 | $component->mergeOutsideAttributes( 61 | AttributeCollection::fromComponent($component, $form, $path . '.') 62 | ); 63 | 64 | return function () use ($form) { 65 | wrap($form)->boot(); 66 | }; 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/Mechanisms/HandleComponents/Synthesizers/Synth.php: -------------------------------------------------------------------------------- 1 | $key; 35 | } 36 | 37 | function __call($method, $params) { 38 | if ($method === 'dehydrate') { 39 | throw new \Exception('You must define a "dehydrate" method'); 40 | } 41 | 42 | if ($method === 'hydrate') { 43 | throw new \Exception('You must define a "hydrate" method'); 44 | } 45 | 46 | if ($method === 'hydrateFromType') { 47 | throw new \Exception('You must define a "hydrateFromType" method'); 48 | } 49 | 50 | if ($method === 'get') { 51 | throw new \Exception('This synth doesn\'t support getting properties: '.get_class($this)); 52 | } 53 | 54 | if ($method === 'set') { 55 | throw new \Exception('This synth doesn\'t support setting properties: '.get_class($this)); 56 | } 57 | 58 | if ($method === 'unset') { 59 | throw new \Exception('This synth doesn\'t support unsetting properties: '.get_class($this)); 60 | } 61 | 62 | if ($method === 'call') { 63 | throw new \Exception('This synth doesn\'t support calling methods: '.get_class($this)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Features/SupportRedirects/TestsRedirects.php: -------------------------------------------------------------------------------- 1 | action($uri); 15 | } 16 | 17 | if (! app('livewire')->isLivewireRequest()) { 18 | $this->lastState->getResponse()->assertRedirect($uri); 19 | 20 | return $this; 21 | } 22 | 23 | PHPUnit::assertArrayHasKey( 24 | 'redirect', 25 | $this->effects, 26 | 'Component did not perform a redirect.' 27 | ); 28 | 29 | if (! is_null($uri)) { 30 | PHPUnit::assertSame(url($uri), url($this->effects['redirect'])); 31 | } 32 | 33 | return $this; 34 | } 35 | 36 | public function assertRedirectContains($uri) 37 | { 38 | if (is_subclass_of($uri, Component::class)) { 39 | $uri = url()->action($uri); 40 | } 41 | 42 | if (! app('livewire')->isLivewireRequest()) { 43 | $this->lastState->getResponse()->assertRedirectContains($uri); 44 | 45 | return $this; 46 | } 47 | 48 | PHPUnit::assertArrayHasKey( 49 | 'redirect', 50 | $this->effects, 51 | 'Component did not perform a redirect.' 52 | ); 53 | 54 | PHPUnit::assertTrue( 55 | Str::contains($this->effects['redirect'], $uri), 'Redirect location ['.$this->effects['redirect'].'] does not contain ['.$uri.'].' 56 | ); 57 | 58 | return $this; 59 | } 60 | 61 | public function assertRedirectToRoute($name, $parameters = []) 62 | { 63 | $uri = route($name, $parameters); 64 | 65 | return $this->assertRedirect($uri); 66 | } 67 | 68 | public function assertNoRedirect() 69 | { 70 | PHPUnit::assertTrue(! isset($this->effects['redirect'])); 71 | 72 | return $this; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Mechanisms/RenderComponent.php: -------------------------------------------------------------------------------- 1 | generate(); 42 | $deterministicBladeKey = "'{$deterministicBladeKey}'"; 43 | 44 | return <<mount(\$__name, \$__params, \$key, \$__componentSlots); 57 | 58 | echo \$__html; 59 | 60 | unset(\$__html); 61 | unset(\$__name); 62 | unset(\$__params); 63 | unset(\$__componentSlots); 64 | unset(\$__split); 65 | if (isset(\$__slots)) unset(\$__slots); 66 | ?> 67 | EOT; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /roadmap/single-file-components.md: -------------------------------------------------------------------------------- 1 | 2 | Currently in Livewire v3 all components are normal PHP classes that extend \Liveiwre\Component like so: 3 | 4 | ## V3 components 5 | 6 | ```php 7 | count++; 17 | } 18 | 19 | public function render() 20 | { 21 | return view('livewire.counter'); 22 | } 23 | } 24 | ``` 25 | 26 | Then the associated view looks like this: 27 | 28 | ```php 29 | // Path resources/views/livewire/counter.blade.php 30 | 31 |
32 | Count: {{ $count }} 33 | 34 | 35 |
36 | ``` 37 | 38 | In V4, we will be transitioning to a new single-file, view-first, system to unify this experience. 39 | 40 | Here is the new, V4 way: 41 | 42 | ## V4 components 43 | 44 | Note: Components will be resolved similar to anonymous Blade components. Instead of only being inside the resources/views/livewire directory, they will co-exist among other blade files in resources/views/components. 45 | 46 | Here is the single file version of the above counter component. Would likely be a file called: 47 | 48 | - resources/views/components/counter.wire.php 49 | 50 | (notice the new file extension for these single file components (wire.php)) 51 | 52 | ```php 53 | @php 54 | new class extends Livewire\Component { 55 | public $count = 0; 56 | 57 | public function increment() 58 | { 59 | $this->count++; 60 | } 61 | } 62 | @endphp 63 | 64 |
65 | Count: {{ $count }} 66 | 67 | 68 |
69 | ``` 70 | 71 | ### External components 72 | 73 | Alternatively, if folks still want seperate files they would reference the class from this view like so: 74 | 75 | ```php 76 | @php(new \App\Livewire\Components\Counter) 77 | 78 |
79 | Count: {{ $count }} 80 | 81 | 82 |
83 | ``` 84 | 85 | This way we can still maintain a view-first approach but allow for seperate of concerns for those that value that. 86 | -------------------------------------------------------------------------------- /src/Features/SupportQueryString/SupportQueryString.php: -------------------------------------------------------------------------------- 1 | getQueryString()) return; 18 | 19 | foreach ($queryString as $key => $value) { 20 | $key = is_string($key) ? $key : $value; 21 | $alias = $value['as'] ?? $key; 22 | $history = $value['history'] ?? true; 23 | $keep = $value['alwaysShow'] ?? $value['keep'] ?? false; 24 | $except = $value['except'] ?? null; 25 | 26 | $this->component->setPropertyAttribute($key, new BaseUrl(as: $alias, history: $history, keep: $keep, except: $except)); 27 | } 28 | } 29 | 30 | public function getQueryString() 31 | { 32 | if (isset($this->queryString)) return $this->queryString; 33 | 34 | $component = $this->component; 35 | 36 | $componentQueryString = []; 37 | 38 | if (method_exists($component, 'queryString')) $componentQueryString = invade($component)->queryString(); 39 | elseif (property_exists($component, 'queryString')) $componentQueryString = invade($component)->queryString; 40 | 41 | return $this->queryString = collect(class_uses_recursive($class = $component::class)) 42 | ->map(function ($trait) use ($class, $component) { 43 | $member = 'queryString' . class_basename($trait); 44 | 45 | if (method_exists($class, $member)) { 46 | return invade($component)->{$member}(); 47 | } 48 | 49 | if (property_exists($class, $member)) { 50 | return invade($component)->{$member}; 51 | } 52 | 53 | return []; 54 | }) 55 | ->values() 56 | ->mapWithKeys(function ($value) { 57 | return $value; 58 | }) 59 | ->merge($componentQueryString) 60 | ->toArray(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/FileUploadSynth.php: -------------------------------------------------------------------------------- 1 | dehydratePropertyFromWithFileUploads($target), []]; 17 | } 18 | 19 | public function dehydratePropertyFromWithFileUploads($value) 20 | { 21 | if (TemporaryUploadedFile::canUnserialize($value)) { 22 | return TemporaryUploadedFile::unserializeFromLivewireRequest($value); 23 | } 24 | 25 | if ($value instanceof TemporaryUploadedFile) { 26 | return $value->serializeForLivewireResponse(); 27 | } 28 | 29 | if (is_array($value) && isset(array_values($value)[0])) { 30 | $isValid = true; 31 | 32 | foreach ($value as $key => $arrayValue) { 33 | if (!($arrayValue instanceof TemporaryUploadedFile) || !is_numeric($key)) { 34 | $isValid = false; 35 | break; 36 | } 37 | } 38 | 39 | if ($isValid) { 40 | return array_values($value)[0]::serializeMultipleForLivewireResponse($value); 41 | } 42 | } 43 | 44 | if (is_array($value)) { 45 | foreach ($value as $key => $item) { 46 | $value[$key] = $this->dehydratePropertyFromWithFileUploads($item); 47 | } 48 | } 49 | 50 | if ($value instanceof \Livewire\Wireable) { 51 | $keys = array_keys(get_object_vars($value)); 52 | 53 | foreach ($keys as $key) { 54 | $value->{$key} = $this->dehydratePropertyFromWithFileUploads($value->{$key}); 55 | } 56 | } 57 | 58 | return $value; 59 | } 60 | 61 | function hydrate($value) { 62 | if (TemporaryUploadedFile::canUnserialize($value)) { 63 | return TemporaryUploadedFile::unserializeFromLivewireRequest($value); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /roadmap/overview.md: -------------------------------------------------------------------------------- 1 | 2 | Livewire 4 is a new version of Livewire focused on making livewire faster, more stable, and more intuitive. 3 | 4 | ## V4 Checklist 5 | - [x] Blaze 6 | - [x] Route::livewire() 7 | - [x] wire:ref 8 | - [x] CSP-safe 9 | - [ ] Support wire:model on dialog and popover elements 10 | - [x] Interceptors 11 | - [x] data-loading 12 | - [x] Slots 13 | - [x] Attributes 14 | - [ ] Getters/setter properties 15 | - [x] Multi-file components 16 | - [x] Single file components 17 | - [x] Islands 18 | - [x] Streaming 19 | - [x] Make command 20 | - [x] Convert commands to Prompts 21 | 22 | ## Potential extras 23 | - [ ] [Add `wire:submit.clear`](wire-submit-dot-clear.md) 24 | - [ ] [Add `bootstrap/livewire.php` configuration file](configuration.md) 25 | - [ ] [Add `artisan livewire:install` command](install-command.md) 26 | - [ ] [Support multiple file uploads in S3](multiple-file-uploads-s3.md) 27 | - [ ] [Remove $this-> demand for computed properties](remove-this-arrow-for-computeds.md) 28 | - [ ] [Dispatch from mount()](dispatch-from-mount.md) 29 | - [ ] [Fill empty X-Livewire request headers](fill-request-headers.md) 30 | - [ ] [Missing closing divs warning](warn-closing-elements.md) 31 | - [ ] [Add dev modal](dev-modal.md) 32 | - [ ] [Docs rewrite](docs-rewrite.md) 33 | - [ ] [Docs recipe section](docs-recipes.md) 34 | - [ ] [Docs best practices](docs-best-practices.md) 35 | - [ ] [Missing model 404 problems](missing-models.md) 36 | - [ ] [$wire should be actual Alpine component data](actual-alpine-component-data.md) 37 | - [ ] [Named wire:model](wire-model-named.md) 38 | - [ ] [Hydratable query string](hydratable-query-string-hook.md) 39 | - [ ] [Mutable updating hook](mutable-update-hook.md) 40 | - [ ] [Explicit hydrate/dehydrate methods](hydration-control.md) 41 | - [ ] [wire:submit with sending form data](wire-submit-form-data.md) 42 | - [ ] [Fix array update hook calls and values](array-update-hook.md) 43 | - [ ] [Plan for Volt messaging now that it's kinda baked in] 44 | * [Story: some story for repeating fields] 45 | * [Story: property assignment type-error] 46 | * [Add server-sent events support some way somehow](sse.md) 47 | 48 | ## Breaking changes 49 | * /livewire/upload-file -> livewire/upload 50 | * /livewire/prefiew-file -> livewire/preview 51 | * Make #[On] not global by default 52 | * `this` in script is now $wire instead of window -------------------------------------------------------------------------------- /src/Features/SupportPagination/HandlesPagination.php: -------------------------------------------------------------------------------- 1 | paginators)->mapWithKeys(function ($page, $pageName) { 15 | return ['paginators.'.$pageName => ['history' => true, 'as' => $pageName, 'keep' => false]]; 16 | })->toArray(); 17 | } 18 | 19 | public function getPage($pageName = 'page') 20 | { 21 | return $this->paginators[$pageName] ?? Paginator::resolveCurrentPage($pageName); 22 | } 23 | 24 | public function previousPage($pageName = 'page') 25 | { 26 | $this->setPage(max(($this->paginators[$pageName] ?? 1) - 1, 1), $pageName); 27 | } 28 | 29 | public function nextPage($pageName = 'page') 30 | { 31 | $this->setPage(($this->paginators[$pageName] ?? 1) + 1, $pageName); 32 | } 33 | 34 | public function gotoPage($page, $pageName = 'page') 35 | { 36 | $this->setPage($page, $pageName); 37 | } 38 | 39 | public function resetPage($pageName = 'page') 40 | { 41 | $this->setPage(1, $pageName); 42 | } 43 | 44 | public function setPage($page, $pageName = 'page') 45 | { 46 | if (is_numeric($page)) { 47 | $page = (int) ($page <= 0 ? 1 : $page); 48 | } 49 | 50 | $beforePaginatorMethod = 'updatingPaginators'; 51 | $afterPaginatorMethod = 'updatedPaginators'; 52 | 53 | $beforeMethod = 'updating' . ucfirst(Str::camel($pageName)); 54 | $afterMethod = 'updated' . ucfirst(Str::camel($pageName)); 55 | 56 | if (method_exists($this, $beforePaginatorMethod)) { 57 | $this->{$beforePaginatorMethod}($page, $pageName); 58 | } 59 | 60 | if (method_exists($this, $beforeMethod)) { 61 | $this->{$beforeMethod}($page, null); 62 | } 63 | 64 | $this->paginators[$pageName] = $page; 65 | 66 | if (method_exists($this, $afterPaginatorMethod)) { 67 | $this->{$afterPaginatorMethod}($page, $pageName); 68 | } 69 | 70 | if (method_exists($this, $afterMethod)) { 71 | $this->{$afterMethod}($page, null); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Features/SupportAttributes/AttributeCollection.php: -------------------------------------------------------------------------------- 1 | push(tap($attribute->newInstance(), function ($attribute) use ($component, $subTarget) { 19 | $attribute->__boot($component, AttributeLevel::ROOT, null, null, $subTarget); 20 | })); 21 | } 22 | 23 | foreach ($reflected->getMethods() as $method) { 24 | foreach ($method->getAttributes(Attribute::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { 25 | $instance->push(tap($attribute->newInstance(), function ($attribute) use ($component, $method, $propertyNamePrefix, $subTarget) { 26 | $attribute->__boot($component, AttributeLevel::METHOD, $propertyNamePrefix . $method->getName(), $method->getName(), $subTarget); 27 | })); 28 | } 29 | } 30 | 31 | foreach ($reflected->getProperties() as $property) { 32 | foreach ($property->getAttributes(Attribute::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { 33 | $instance->push(tap($attribute->newInstance(), function ($attribute) use ($component, $property, $propertyNamePrefix, $subTarget) { 34 | $attribute->__boot($component, AttributeLevel::PROPERTY, $propertyNamePrefix . $property->getName(), $property->getName(), $subTarget); 35 | })); 36 | } 37 | } 38 | 39 | return $instance; 40 | } 41 | 42 | protected static function getClassAttributesRecursively($reflected) { 43 | $attributes = []; 44 | 45 | while ($reflected) { 46 | foreach ($reflected->getAttributes(Attribute::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { 47 | $attributes[] = $attribute; 48 | } 49 | 50 | $reflected = $reflected->getParentClass(); 51 | } 52 | 53 | return $attributes; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Features/SupportNestedComponentListeners/SupportNestedComponentListeners.php: -------------------------------------------------------------------------------- 1 | $value) { 21 | if (str($key)->startsWith('@')) { 22 | // any kebab-cased parameters passed in will have been converted to camelCase 23 | // so we need to convert back to kebab to ensure events are valid in html 24 | $fullEvent = str($key)->after('@')->kebab(); 25 | $attributeKey = 'x-on:'.$fullEvent; 26 | $attributeValue = "\$wire.\$parent.".$value; 27 | 28 | store($this->component)->push('generatedAttributes', $attributeValue, $attributeKey); 29 | } 30 | } 31 | } 32 | 33 | public function render($view, $data) 34 | { 35 | return function ($html, $replaceHtml) { 36 | $attributes = store($this->component)->get('generatedAttributes', false); 37 | 38 | if (! $attributes) return; 39 | 40 | $replaceHtml(Utils::insertAttributesIntoHtmlRoot($html, $attributes)); 41 | }; 42 | } 43 | 44 | public function dehydrate($context) 45 | { 46 | $attributes = store($this->component)->get('generatedAttributes', false); 47 | 48 | if (! $attributes) return; 49 | 50 | $attributes && $context->addMemo('generatedAttributes', $attributes); 51 | } 52 | 53 | public function hydrate($memo) 54 | { 55 | if (! isset($memo['generatedAttributes'])) return; 56 | 57 | $attributes = $memo['generatedAttributes']; 58 | 59 | // Store the attributes for later dehydration... 60 | store($this->component)->set('generatedAttributes', $attributes); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Features/SupportRedirects/SupportRedirects.php: -------------------------------------------------------------------------------- 1 | has('session.store')) { 21 | session()->forget(session()->get('_flash.new')); 22 | } 23 | }); 24 | 25 | on('flush-state', function () { 26 | static::$atLeastOneMountedComponentHasRedirected = false; 27 | }); 28 | } 29 | 30 | public function boot() 31 | { 32 | // Put Laravel's redirector aside and replace it with our own custom one. 33 | static::$redirectorCacheStack[] = app('redirect'); 34 | 35 | app()->bind('redirect', function () { 36 | $redirector = app(Redirector::class)->component($this->component); 37 | 38 | if (app()->has('session.store')) { 39 | $redirector->setSession(app('session.store')); 40 | } 41 | 42 | return $redirector; 43 | }); 44 | } 45 | 46 | public function dehydrate($context) 47 | { 48 | // Put the old redirector back into the container. 49 | app()->instance('redirect', array_pop(static::$redirectorCacheStack)); 50 | 51 | $to = $this->storeGet('redirect'); 52 | $usingNavigate = $this->storeGet('redirectUsingNavigate'); 53 | 54 | if (is_subclass_of($to, Component::class)) { 55 | $to = url()->action($to); 56 | } 57 | 58 | if ($to && ! app(HandleRequests::class)->isLivewireRequest()) { 59 | abort(redirect($to)); 60 | } 61 | 62 | if (! $to) return; 63 | 64 | $context->addEffect('redirect', $to); 65 | $usingNavigate && $context->addEffect('redirectUsingNavigate', true); 66 | 67 | if (! $context->isMounting()) { 68 | static::$atLeastOneMountedComponentHasRedirected = true; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Features/SupportFileUploads/GenerateSignedUploadUrl.php: -------------------------------------------------------------------------------- 1 | addMinutes(FileUploadConfiguration::maxUploadTime()) 15 | ); 16 | } 17 | 18 | public function forS3($file, $visibility = 'private') 19 | { 20 | $storage = FileUploadConfiguration::storage(); 21 | 22 | $driver = $storage->getDriver(); 23 | 24 | // Flysystem V2+ doesn't allow direct access to adapter, so we need to invade instead. 25 | $adapter = invade($driver)->adapter; 26 | 27 | // Flysystem V2+ doesn't allow direct access to client, so we need to invade instead. 28 | $client = invade($adapter)->client; 29 | 30 | // Flysystem V2+ doesn't allow direct access to bucket, so we need to invade instead. 31 | $bucket = invade($adapter)->bucket; 32 | 33 | $fileType = $file->getMimeType(); 34 | $fileHashName = TemporaryUploadedFile::generateHashNameWithOriginalNameEmbedded($file); 35 | $path = FileUploadConfiguration::path($fileHashName); 36 | 37 | $command = $client->getCommand('putObject', array_filter([ 38 | 'Bucket' => $bucket, 39 | 'Key' => $path, 40 | 'ACL' => $visibility, 41 | 'ContentType' => $fileType ?: 'application/octet-stream', 42 | 'CacheControl' => null, 43 | 'Expires' => null, 44 | ])); 45 | 46 | $signedRequest = $client->createPresignedRequest( 47 | $command, 48 | '+' . FileUploadConfiguration::maxUploadTime() . ' minutes' 49 | ); 50 | 51 | $uri = $signedRequest->getUri(); 52 | 53 | if (filled($url = $storage->getConfig()['temporary_url'] ?? null)) { 54 | $uri = invade($storage)->replaceBaseUrl($uri, $url); 55 | } 56 | 57 | return [ 58 | 'path' => $fileHashName, 59 | 'url' => (string) $uri, 60 | 'headers' => $this->headers($signedRequest, $fileType), 61 | ]; 62 | } 63 | 64 | protected function headers($signedRequest, $fileType) 65 | { 66 | return array_merge( 67 | $signedRequest->getHeaders(), 68 | [ 69 | 'Content-Type' => $fileType ?: 'application/octet-stream' 70 | ] 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Features/SupportEvents/fake-echo.js: -------------------------------------------------------------------------------- 1 | 2 | window.fakeEchoListeners = [] 3 | 4 | class FakeChannel { 5 | constructor(channel) { 6 | this.channel = channel 7 | 8 | this.type = 'public' 9 | } 10 | 11 | listen(eventName, callback) { 12 | window.fakeEchoListeners.push({ 13 | channel: this.channel, 14 | event: eventName, 15 | type: this.type, 16 | callback, 17 | }) 18 | 19 | return this 20 | } 21 | 22 | stopListening(eventName, callback) { 23 | window.fakeEchoListeners = window.fakeEchoListeners.filter(i => { 24 | if (callback) { 25 | return ! (i.event === eventName && i.callback === callback) 26 | } 27 | 28 | return ! (i.event === eventName) 29 | }) 30 | 31 | 32 | return this 33 | } 34 | } 35 | 36 | class FakePrivateChannel extends FakeChannel { 37 | constructor(channel) { 38 | super(channel) 39 | 40 | this.type = 'private' 41 | } 42 | 43 | whisper(eventName, data) { 44 | return this 45 | } 46 | } 47 | 48 | class FakePresenceChannel extends FakeChannel { 49 | constructor(channel) { 50 | super(channel) 51 | 52 | this.type = 'presence' 53 | } 54 | 55 | here(callback) { 56 | return this 57 | } 58 | 59 | joining(callback) { 60 | return this 61 | } 62 | 63 | whisper(eventName, data) { 64 | return this 65 | } 66 | 67 | leaving(callback) { 68 | return this 69 | } 70 | } 71 | 72 | class FakeEcho { 73 | join(channel) { 74 | return new FakePresenceChannel(channel); 75 | } 76 | 77 | channel(channel) { 78 | return new FakeChannel(channel); 79 | } 80 | 81 | private(channel) { 82 | return new FakePrivateChannel(channel); 83 | } 84 | 85 | encryptedPrivate(channel) { 86 | return new FakePrivateChannel(channel); 87 | } 88 | 89 | socketId() { 90 | return 'fake-socked-id' 91 | } 92 | 93 | // For dusk to trigger listeners... 94 | 95 | fakeTrigger({ channel, event, type, payload = {} }) { 96 | window.fakeEchoListeners.filter(listener => { 97 | if (event !== listener.event) return false 98 | if (channel !== listener.channel) return false 99 | if (type !== undefined && type !== listener.type) return false 100 | 101 | return true 102 | }).forEach(({ callback }) => { 103 | callback(payload) 104 | }) 105 | } 106 | } 107 | 108 | window.Echo = new FakeEcho 109 | -------------------------------------------------------------------------------- /src/Compiler/Compiler.php: -------------------------------------------------------------------------------- 1 | cacheManager->hasBeenCompiled($path)) 24 | || $this->cacheManager->isExpired($path) 25 | ) { 26 | $this->compilePath($path); 27 | } 28 | 29 | return $this->cacheManager->getClassName($path); 30 | } 31 | 32 | public function compilePath(string $path): void 33 | { 34 | $parser = is_file($path) 35 | ? SingleFileParser::parse($this, $path) 36 | : MultiFileParser::parse($this, $path); 37 | 38 | $viewFileName = $this->cacheManager->getViewPath($path); 39 | 40 | $placeholderFileName = null; 41 | $scriptFileName = null; 42 | 43 | $placeholderContents = $parser->generatePlaceholderContents(); 44 | $scriptContents = $parser->generateScriptContents(); 45 | 46 | if ($placeholderContents !== null) { 47 | $placeholderFileName = $this->cacheManager->getPlaceholderPath($path); 48 | 49 | $this->cacheManager->writePlaceholderFile($path, $placeholderContents); 50 | } 51 | 52 | if ($scriptContents !== null) { 53 | $scriptFileName = $this->cacheManager->getScriptPath($path); 54 | 55 | $this->cacheManager->writeScriptFile($path, $scriptContents); 56 | } 57 | 58 | $this->cacheManager->writeClassFile($path, $parser->generateClassContents( 59 | $viewFileName, 60 | $placeholderFileName, 61 | $scriptFileName, 62 | )); 63 | 64 | $this->cacheManager->writeViewFile($path, $parser->generateViewContents()); 65 | } 66 | 67 | public function clearCompiled($output = null) 68 | { 69 | $this->cacheManager->clearCompiledFiles($output); 70 | } 71 | 72 | public function prepareViewsForCompilationUsing($callback) 73 | { 74 | $this->prepareViewsForCompilationUsing[] = $callback; 75 | } 76 | 77 | public function prepareViewForCompilation($contents, $path) 78 | { 79 | foreach ($this->prepareViewsForCompilationUsing as $callback) { 80 | $contents = $callback($contents, $path); 81 | } 82 | 83 | return $contents; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Features/SupportTesting/SubsequentRender.php: -------------------------------------------------------------------------------- 1 | makeSubsequentRequest($calls, $updates, $cookies); 17 | } 18 | 19 | function makeSubsequentRequest($calls = [], $updates = [], $cookies = []) { 20 | $uri = app('livewire')->getUpdateUri(); 21 | 22 | $encodedSnapshot = json_encode($this->lastState->getSnapshot()); 23 | 24 | $payload = [ 25 | 'components' => [ 26 | [ 27 | 'snapshot' => $encodedSnapshot, 28 | 'calls' => $calls, 29 | 'updates' => $updates, 30 | ], 31 | ], 32 | ]; 33 | 34 | [$response, $componentInstance, $componentView] = $this->extractComponentAndBladeView(function () use ($uri, $payload, $cookies) { 35 | return $this->requestBroker->temporarilyDisableExceptionHandlingAndMiddleware(function ($requestBroker) use ($uri, $payload, $cookies) { 36 | return $requestBroker->addHeaders(['X-Livewire' => true])->call('POST', $uri, $payload, $cookies); 37 | }); 38 | }); 39 | 40 | app('livewire')->flushState(); 41 | 42 | if (! $response->isOk()) { 43 | return new ComponentState( 44 | $componentInstance, 45 | $response, 46 | null, 47 | '', 48 | [], 49 | [], 50 | ); 51 | } 52 | 53 | $json = $response->json(); 54 | 55 | // Set "original" to Blade view for assertions like "assertViewIs()"... 56 | $response->original = $componentView; 57 | 58 | $componentResponsePayload = $json['components'][0]; 59 | 60 | $snapshot = json_decode($componentResponsePayload['snapshot'], true); 61 | 62 | $effects = $componentResponsePayload['effects']; 63 | 64 | // If no new HTML has been rendered, let's forward the last known HTML... 65 | $html = $effects['html'] ?? $this->lastState->getHtml(stripInitialData: true); 66 | $view = $componentView ?? $this->lastState->getView(); 67 | 68 | return new ComponentState( 69 | $componentInstance, 70 | $response, 71 | $view, 72 | $html, 73 | $snapshot, 74 | $effects, 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Features/SupportEvents/Event.php: -------------------------------------------------------------------------------- 1 | name = $name; 17 | 18 | if (isset($params['ref'])) { 19 | $this->ref($params['ref']); 20 | unset($params['ref']); 21 | } 22 | 23 | if (isset($params['component'])) { 24 | $this->component($params['component']); 25 | unset($params['component']); 26 | } 27 | 28 | if (isset($params['el'])) { 29 | $this->el($params['el']); 30 | unset($params['el']); 31 | } 32 | 33 | if (isset($params['self'])) { 34 | $this->self(); 35 | unset($params['self']); 36 | } 37 | 38 | // Handle legacy 'to' parameter for backward compatibility 39 | if (isset($params['to'])) { 40 | $this->component($params['to']); 41 | unset($params['to']); 42 | } 43 | 44 | $this->params = $params; 45 | } 46 | 47 | public function self() 48 | { 49 | $this->self = true; 50 | 51 | return $this; 52 | } 53 | 54 | public function component($name) 55 | { 56 | $this->component = $name; 57 | 58 | return $this; 59 | } 60 | 61 | public function ref($ref) 62 | { 63 | $this->ref = $ref; 64 | 65 | return $this; 66 | } 67 | 68 | public function el($selector) 69 | { 70 | $this->el = $selector; 71 | 72 | return $this; 73 | } 74 | 75 | public function to($component = null, $ref = null, $el = null, $self = null) 76 | { 77 | if ($self) { 78 | return $this->self(); 79 | } 80 | 81 | if ($ref) { 82 | return $this->ref($ref); 83 | } 84 | 85 | if ($el) { 86 | return $this->el($el); 87 | } 88 | 89 | return $this->component($component); 90 | } 91 | 92 | public function serialize() 93 | { 94 | $output = [ 95 | 'name' => $this->name, 96 | 'params' => $this->params, 97 | ]; 98 | 99 | if ($this->self) $output['self'] = true; 100 | if ($this->component) $output['component'] = app('livewire.factory')->resolveComponentName($this->component); 101 | if ($this->ref) $output['ref'] = $this->ref; 102 | if ($this->el) $output['el'] = $this->el; 103 | 104 | return $output; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Features/SupportSlots/SupportSlots.php: -------------------------------------------------------------------------------- 1 | withChildSlots($slots, $id); 17 | }); 18 | } 19 | 20 | public function render($view, $properties) 21 | { 22 | // Ensure that the slot proxy is available for child views... 23 | $slots = $this->component->getSlots(); 24 | 25 | $view->with(['slot' => new SlotProxy($this->component, $slots)]); 26 | } 27 | 28 | public function renderIsland($name, $view, $properties) 29 | { 30 | $slots = $this->component->getSlots(); 31 | 32 | $view->with(['slot' => new SlotProxy($this->component, $slots)]); 33 | } 34 | 35 | function hydrate($memo) 36 | { 37 | // When a child component re-renders, we will need to stub out the known slots 38 | // with placeholders so that they can be rendered and morph'd correctly... 39 | $slots = $memo['slots'] ?? []; 40 | 41 | if (! empty($slots)) { 42 | $this->component->withPlaceholderSlots($slots); 43 | } 44 | } 45 | 46 | public function dehydrate($context) 47 | { 48 | $this->dehydrateSlotsThatWereRenderedIntoMorphEffects($context); 49 | $this->dehydrateSlotsThatWerePassedToTheComponentForSubsequentRenders($context); 50 | } 51 | 52 | protected function dehydrateSlotsThatWereRenderedIntoMorphEffects($context) 53 | { 54 | // When a parent renders, capture the slots and include them in the response... 55 | $slots = $this->component->getSlotsForSkippedChildRenders(); 56 | 57 | if (! empty($slots)) { 58 | $context->addEffect('slotFragments', $slots); 59 | } 60 | } 61 | 62 | protected function dehydrateSlotsThatWerePassedToTheComponentForSubsequentRenders($context) 63 | { 64 | // Ensure a child component is aware of what slots belong to it... 65 | $slots = $this->component->getSlots(); 66 | 67 | $slotMemo = []; 68 | 69 | foreach ($slots as $slot) { 70 | $slotMemo[] = [ 71 | 'name' => $slot->getName(), 72 | 'componentId' => $slot->getComponentId(), 73 | 'parentId' => $slot->getParentId(), 74 | ]; 75 | } 76 | 77 | if (! empty($slotMemo)) { 78 | $context->addMemo('slots', $slotMemo); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Features/SupportIslands/SupportIslands.php: -------------------------------------------------------------------------------- 1 | renderIslandDirective({$expression}); ?>"; 21 | }); 22 | } 23 | 24 | public static function registerInlineIslandPrecompiler() 25 | { 26 | Blade::precompiler(function ($content) { 27 | // Shortcut out if there are no islands in the content... 28 | if (! str_contains($content, '@endisland')) return $content; 29 | 30 | $pathSignature = Blade::getPath() ?: crc32($content); 31 | 32 | return IslandCompiler::compile($pathSignature, $content); 33 | }); 34 | } 35 | 36 | function call($method, $params, $returnEarly, $metadata, $componentContext) 37 | { 38 | if (! isset($metadata['island'])) return; 39 | 40 | $island = $metadata['island']; 41 | 42 | $mount = false; 43 | 44 | if ($method === '__lazyLoadIsland') { 45 | $mount = true; 46 | $returnEarly(); 47 | } 48 | 49 | // if metadata contains an island, then we should render it... 50 | return function (...$params) use ($island, $componentContext, $mount) { 51 | ['name' => $name, 'mode' => $mode] = $island; 52 | 53 | $islands = $this->component->getIslands(); 54 | 55 | $islands = array_filter($islands, fn ($island) => $island['name'] === $name); 56 | 57 | if (empty($islands)) return; 58 | 59 | $this->component->skipRender(); 60 | 61 | $this->component->renderIsland( 62 | name: $name, 63 | mode: $mode, 64 | mount: $mount, 65 | ); 66 | }; 67 | } 68 | 69 | public function dehydrate($context) 70 | { 71 | $context->addMemo('islands', $this->component->getIslands()); 72 | 73 | if ($this->component->hasRenderedIslandFragments()) { 74 | $context->addEffect('islandFragments', $this->component->getRenderedIslandFragments()); 75 | } 76 | } 77 | 78 | public function hydrate($memo) 79 | { 80 | $this->component->markIslandsAsMounted(); 81 | 82 | $islands = $memo['islands'] ?? null; 83 | 84 | if (! $islands) return; 85 | 86 | $this->component->setIslands($islands ?? []); 87 | } 88 | } 89 | --------------------------------------------------------------------------------