Untitled

 avatar
unknown
plain_text
2 years ago
172 kB
57
Indexable
Skip to content
Product
Solutions
Open Source
Pricing

Search or jump to...
Sign in
Sign up
livewire
/
livewire
Public
Code
Issues
Pull requests
44
Discussions
Actions
Projects
2
Security
Insights
Comparing changes
Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also  or learn more about diff comparisons.
 
...
 
 33 contributors
 Commits 51
 Files changed 89
Showing  with 2,426 additions and 450 deletions.
  6 changes: 3 additions & 3 deletions6  
composer.json
@@ -10,7 +10,7 @@
  ],
  "require": {
    "php": "^8.1",
    "symfony/http-kernel": "^5.0|^6.0",
    "symfony/http-kernel": "^6.2",
    "illuminate/support": "^10.0",
    "illuminate/database": "^10.0",
    "illuminate/validation": "^10.0",
@@ -21,8 +21,8 @@
    "mockery/mockery": "^1.3.1",
    "phpunit/phpunit": "^9.0",
    "laravel/framework": "^10.0",
    "orchestra/testbench": "^7.0|^8.0",
    "orchestra/testbench-dusk": "^7.0|^8.0",
    "orchestra/testbench": "^8.0",
    "orchestra/testbench-dusk": "^8.0",
    "calebporzio/sushi": "^2.1",
    "laravel/prompts": "^0.1.6"
  },
  1 change: 1 addition & 0 deletions1  
config/livewire.php
@@ -128,6 +128,7 @@

    'navigate' => [
        'show_progress_bar' => true,
        'progress_bar_color' => '#2299dd',
    ],

    /*
  81 changes: 42 additions & 39 deletions81  
dist/livewire.esm.js
@@ -1837,7 +1837,11 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
      let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression.trim()) || /^(let|const)\s/.test(expression.trim()) ? `(async()=>{ ${expression} })()` : expression;
      const safeAsyncFunction = () => {
        try {
          return new AsyncFunction(["__self", "scope"], `with (scope) { __self.result = ${rightSideSafeExpression} }; __self.finished = true; return __self.result;`);
          let func2 = new AsyncFunction(["__self", "scope"], `with (scope) { __self.result = ${rightSideSafeExpression} }; __self.finished = true; return __self.result;`);
          Object.defineProperty(func2, "name", {
            value: `[Alpine] ${expression}`
          });
          return func2;
        } catch (error2) {
          handleError(error2, el, expression);
          return Promise.resolve();
@@ -2808,7 +2812,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
      get raw() {
        return raw;
      },
      version: "3.13.0",
      version: "3.13.1",
      flushAndStopDeferringMutations,
      dontAutoEvaluateFunctions,
      disableEffectScheduling,
@@ -3003,6 +3007,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
          placeInDom(el._x_teleport, target2, modifiers);
        });
      };
      cleanup2(() => clone2.remove());
    });
    var teleportContainerDuringClone = document.createElement("div");
    function getTarget(expression) {
@@ -4895,7 +4900,7 @@ var require_module_cjs5 = __commonJS({
        let evaluate = evaluateLater(expression);
        let options = {
          rootMargin: getRootMargin(modifiers),
          threshold: getThreshhold(modifiers)
          threshold: getThreshold(modifiers)
        };
        let observer = new IntersectionObserver((entries) => {
          entries.forEach((entry) => {
@@ -4911,7 +4916,7 @@ var require_module_cjs5 = __commonJS({
        });
      });
    }
    function getThreshhold(modifiers) {
    function getThreshold(modifiers) {
      if (modifiers.includes("full"))
        return 0.99;
      if (modifiers.includes("half"))
@@ -5355,8 +5360,8 @@ var require_module_cjs6 = __commonJS({
              continue;
            }
          }
          let isIf = (node) => node && node.nodeType === 8 && node.textContent === " __BLOCK__ ";
          let isEnd = (node) => node && node.nodeType === 8 && node.textContent === " __ENDBLOCK__ ";
          let isIf = (node) => node && node.nodeType === 8 && node.textContent === "[if BLOCK]><![endif]";
          let isEnd = (node) => node && node.nodeType === 8 && node.textContent === "[if ENDBLOCK]><![endif]";
          if (isIf(currentTo) && isIf(currentFrom)) {
            let nestedIfCount = 0;
            let fromBlockStart = currentFrom;
@@ -5863,6 +5868,9 @@ function isSynthetic(subject) {
  return Array.isArray(subject) && subject.length === 2 && typeof subject[1] === "object" && Object.keys(subject[1]).includes("s");
}
function getCsrfToken() {
  if (document.querySelector('meta[name="csrf-token"]')) {
    return document.querySelector('meta[name="csrf-token"]').getAttribute("content");
  }
  if (document.querySelector("[data-csrf]")) {
    return document.querySelector("[data-csrf]").getAttribute("data-csrf");
  }
@@ -6205,7 +6213,7 @@ function generateEntangleFunction(component, cleanup2) {
  if (!cleanup2)
    cleanup2 = () => {
    };
  return (name, live) => {
  return (name, live = false) => {
    let isLive = live;
    let livewireProperty = name;
    let livewireComponent = component.$wire;
@@ -6215,24 +6223,22 @@ function generateEntangleFunction(component, cleanup2) {
        console.error(`Livewire Entangle Error: Livewire property ['${livewireProperty}'] cannot be found on component: ['${component.name}']`);
        return;
      }
      queueMicrotask(() => {
        let release = import_alpinejs.default.entangle({
          get() {
            return livewireComponent.get(name);
          },
          set(value) {
            livewireComponent.set(name, value, isLive);
          }
        }, {
          get() {
            return getter();
          },
          set(value) {
            setter(value);
          }
        });
        cleanup2(() => release());
      let release = import_alpinejs.default.entangle({
        get() {
          return livewireComponent.get(name);
        },
        set(value) {
          livewireComponent.set(name, value, isLive);
        }
      }, {
        get() {
          return getter();
        },
        set(value) {
          setter(value);
        }
      });
      cleanup2(() => release());
      return livewireComponent.get(name);
    }, (obj) => {
      Object.defineProperty(obj, "live", {
@@ -7146,13 +7152,13 @@ function finishAndHideProgressBar() {
function injectStyles() {
  let style = document.createElement("style");
  style.innerHTML = `/* Make clicks pass-through */
    #nprogress {
      pointer-events: none;
    }
    #nprogress .bar {
    //   background: #FC70A9;
      background: #29d;
      background: var(--livewire-progress-bar-color, #29d);
      position: fixed;
      z-index: 1031;
@@ -7193,8 +7199,8 @@ function injectStyles() {
      box-sizing: border-box;
      border: solid 2px transparent;
      border-top-color: #29d;
      border-left-color: #29d;
      border-top-color: var(--livewire-progress-bar-color, #29d);
      border-left-color: var(--livewire-progress-bar-color, #29d);
      border-radius: 50%;
      -webkit-animation: nprogress-spinner 400ms linear infinite;
@@ -7434,7 +7440,7 @@ function navigate_default(Alpine21) {
    });
  });
  setTimeout(() => {
    fireEventForOtherLibariesToHookInto("alpine:navigated", true);
    fireEventForOtherLibariesToHookInto("alpine:navigated");
  });
}
function fetchHtmlOrUsePrefetchedHtml(fromDestination, callback) {
@@ -7451,8 +7457,8 @@ function preventAlpineFromPickingUpDomChanges(Alpine21, callback) {
    });
  });
}
function fireEventForOtherLibariesToHookInto(eventName, init = false) {
  document.dispatchEvent(new CustomEvent(eventName, { bubbles: true, detail: { init } }));
function fireEventForOtherLibariesToHookInto(eventName) {
  document.dispatchEvent(new CustomEvent(eventName, { bubbles: true }));
}
function nowInitializeAlpineOnTheNewPage(Alpine21) {
  Alpine21.initTree(document.body, void 0, (el, skip) => {
@@ -7915,20 +7921,16 @@ on("effects", (component, effects) => {
});

// js/features/supportNavigate.js
var isNavigating = false;
shouldHideProgressBar() && Alpine.navigate.disableProgressBar();
document.addEventListener("alpine:navigated", (e) => {
  if (e.detail && e.detail.init)
    return;
  isNavigating = true;
  document.dispatchEvent(new CustomEvent("livewire:navigated", { bubbles: true }));
});
document.addEventListener("alpine:navigating", (e) => {
  document.dispatchEvent(new CustomEvent("livewire:navigating", { bubbles: true }));
});
function shouldRedirectUsingNavigateOr(effects, url, or) {
  let forceNavigate = effects.redirectUsingNavigate;
  if (forceNavigate || isNavigating) {
  if (forceNavigate) {
    Alpine.navigate(url);
  } else {
    or();
@@ -8064,7 +8066,7 @@ on("morph.added", ({ el }) => {
  el.__addedByMorph = true;
});
directive("transition", ({ el, directive: directive2, component, cleanup: cleanup2 }) => {
  let visibility = import_alpinejs13.default.reactive({ state: false });
  let visibility = import_alpinejs13.default.reactive({ state: el.__addedByMorph ? false : true });
  import_alpinejs13.default.bind(el, {
    [directive2.rawName.replace("wire:", "x-")]: "",
    "x-show"() {
@@ -8190,13 +8192,14 @@ directive("loading", ({ el, directive: directive2, component }) => {
  ]);
});
function applyDelay(directive2) {
  if (!directive2.modifiers.includes("delay"))
  if (!directive2.modifiers.includes("delay") || directive2.modifiers.includes("none"))
    return [(i) => i(), (i) => i()];
  let duration = 200;
  let delayModifiers = {
    "shortest": 50,
    "shorter": 100,
    "short": 150,
    "default": 200,
    "long": 300,
    "longer": 500,
    "longest": 1e3
@@ -8452,7 +8455,7 @@ directive("model", ({ el, directive: directive2, component, cleanup: cleanup2 })
    return handleFileUpload(el, expression, component, cleanup2);
  }
  let isLive = modifiers.includes("live");
  let isLazy = modifiers.includes("lazy");
  let isLazy = modifiers.includes("lazy") || modifiers.includes("change");
  let onBlur = modifiers.includes("blur");
  let isDebounced = modifiers.includes("debounce");
  let update = () => component.$wire.$commit();
  81 changes: 42 additions & 39 deletions81  
dist/livewire.js
@@ -394,6 +394,9 @@
    return Array.isArray(subject) && subject.length === 2 && typeof subject[1] === "object" && Object.keys(subject[1]).includes("s");
  }
  function getCsrfToken() {
    if (document.querySelector('meta[name="csrf-token"]')) {
      return document.querySelector('meta[name="csrf-token"]').getAttribute("content");
    }
    if (document.querySelector("[data-csrf]")) {
      return document.querySelector("[data-csrf]").getAttribute("data-csrf");
    }
@@ -1275,7 +1278,11 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
    let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression.trim()) || /^(let|const)\s/.test(expression.trim()) ? `(async()=>{ ${expression} })()` : expression;
    const safeAsyncFunction = () => {
      try {
        return new AsyncFunction(["__self", "scope"], `with (scope) { __self.result = ${rightSideSafeExpression} }; __self.finished = true; return __self.result;`);
        let func2 = new AsyncFunction(["__self", "scope"], `with (scope) { __self.result = ${rightSideSafeExpression} }; __self.finished = true; return __self.result;`);
        Object.defineProperty(func2, "name", {
          value: `[Alpine] ${expression}`
        });
        return func2;
      } catch (error2) {
        handleError(error2, el, expression);
        return Promise.resolve();
@@ -2246,7 +2253,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
    get raw() {
      return raw;
    },
    version: "3.13.0",
    version: "3.13.1",
    flushAndStopDeferringMutations,
    dontAutoEvaluateFunctions,
    disableEffectScheduling,
@@ -3088,6 +3095,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
        placeInDom(el._x_teleport, target2, modifiers);
      });
    };
    cleanup22(() => clone2.remove());
  });
  var teleportContainerDuringClone = document.createElement("div");
  function getTarget(expression) {
@@ -3762,7 +3770,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
    if (!cleanup3)
      cleanup3 = () => {
      };
    return (name, live) => {
    return (name, live = false) => {
      let isLive = live;
      let livewireProperty = name;
      let livewireComponent = component.$wire;
@@ -3772,24 +3780,22 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
          console.error(`Livewire Entangle Error: Livewire property ['${livewireProperty}'] cannot be found on component: ['${component.name}']`);
          return;
        }
        queueMicrotask(() => {
          let release2 = module_default.entangle({
            get() {
              return livewireComponent.get(name);
            },
            set(value) {
              livewireComponent.set(name, value, isLive);
            }
          }, {
            get() {
              return getter();
            },
            set(value) {
              setter(value);
            }
          });
          cleanup3(() => release2());
        let release2 = module_default.entangle({
          get() {
            return livewireComponent.get(name);
          },
          set(value) {
            livewireComponent.set(name, value, isLive);
          }
        }, {
          get() {
            return getter();
          },
          set(value) {
            setter(value);
          }
        });
        cleanup3(() => release2());
        return livewireComponent.get(name);
      }, (obj) => {
        Object.defineProperty(obj, "live", {
@@ -5536,7 +5542,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
      let evaluate2 = evaluateLater2(expression);
      let options = {
        rootMargin: getRootMargin(modifiers),
        threshold: getThreshhold(modifiers)
        threshold: getThreshold(modifiers)
      };
      let observer2 = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
@@ -5552,7 +5558,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
      });
    });
  }
  function getThreshhold(modifiers) {
  function getThreshold(modifiers) {
    if (modifiers.includes("full"))
      return 0.99;
    if (modifiers.includes("half"))
@@ -5832,13 +5838,13 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
  function injectStyles() {
    let style = document.createElement("style");
    style.innerHTML = `/* Make clicks pass-through */
    #nprogress {
      pointer-events: none;
    }
    #nprogress .bar {
    //   background: #FC70A9;
      background: #29d;
      background: var(--livewire-progress-bar-color, #29d);
      position: fixed;
      z-index: 1031;
@@ -5879,8 +5885,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
      box-sizing: border-box;
      border: solid 2px transparent;
      border-top-color: #29d;
      border-left-color: #29d;
      border-top-color: var(--livewire-progress-bar-color, #29d);
      border-left-color: var(--livewire-progress-bar-color, #29d);
      border-radius: 50%;
      -webkit-animation: nprogress-spinner 400ms linear infinite;
@@ -6119,7 +6125,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
      });
    });
    setTimeout(() => {
      fireEventForOtherLibariesToHookInto("alpine:navigated", true);
      fireEventForOtherLibariesToHookInto("alpine:navigated");
    });
  }
  function fetchHtmlOrUsePrefetchedHtml(fromDestination, callback) {
@@ -6136,8 +6142,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
      });
    });
  }
  function fireEventForOtherLibariesToHookInto(eventName, init = false) {
    document.dispatchEvent(new CustomEvent(eventName, { bubbles: true, detail: { init } }));
  function fireEventForOtherLibariesToHookInto(eventName) {
    document.dispatchEvent(new CustomEvent(eventName, { bubbles: true }));
  }
  function nowInitializeAlpineOnTheNewPage(Alpine3) {
    Alpine3.initTree(document.body, void 0, (el, skip) => {
@@ -6437,8 +6443,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
            continue;
          }
        }
        let isIf = (node) => node && node.nodeType === 8 && node.textContent === " __BLOCK__ ";
        let isEnd = (node) => node && node.nodeType === 8 && node.textContent === " __ENDBLOCK__ ";
        let isIf = (node) => node && node.nodeType === 8 && node.textContent === "[if BLOCK]><![endif]";
        let isEnd = (node) => node && node.nodeType === 8 && node.textContent === "[if ENDBLOCK]><![endif]";
        if (isIf(currentTo) && isIf(currentFrom)) {
          let nestedIfCount = 0;
          let fromBlockStart = currentFrom;
@@ -7078,20 +7084,16 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
  });

  // js/features/supportNavigate.js
  var isNavigating = false;
  shouldHideProgressBar() && Alpine.navigate.disableProgressBar();
  document.addEventListener("alpine:navigated", (e) => {
    if (e.detail && e.detail.init)
      return;
    isNavigating = true;
    document.dispatchEvent(new CustomEvent("livewire:navigated", { bubbles: true }));
  });
  document.addEventListener("alpine:navigating", (e) => {
    document.dispatchEvent(new CustomEvent("livewire:navigating", { bubbles: true }));
  });
  function shouldRedirectUsingNavigateOr(effects, url, or) {
    let forceNavigate = effects.redirectUsingNavigate;
    if (forceNavigate || isNavigating) {
    if (forceNavigate) {
      Alpine.navigate(url);
    } else {
      or();
@@ -7225,7 +7227,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
    el.__addedByMorph = true;
  });
  directive2("transition", ({ el, directive: directive3, component, cleanup: cleanup3 }) => {
    let visibility = module_default.reactive({ state: false });
    let visibility = module_default.reactive({ state: el.__addedByMorph ? false : true });
    module_default.bind(el, {
      [directive3.rawName.replace("wire:", "x-")]: "",
      "x-show"() {
@@ -7349,13 +7351,14 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
    ]);
  });
  function applyDelay(directive3) {
    if (!directive3.modifiers.includes("delay"))
    if (!directive3.modifiers.includes("delay") || directive3.modifiers.includes("none"))
      return [(i) => i(), (i) => i()];
    let duration = 200;
    let delayModifiers = {
      "shortest": 50,
      "shorter": 100,
      "short": 150,
      "default": 200,
      "long": 300,
      "longer": 500,
      "longest": 1e3
@@ -7610,7 +7613,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
      return handleFileUpload(el, expression, component, cleanup3);
    }
    let isLive = modifiers.includes("live");
    let isLazy = modifiers.includes("lazy");
    let isLazy = modifiers.includes("lazy") || modifiers.includes("change");
    let onBlur = modifiers.includes("blur");
    let isDebounced = modifiers.includes("debounce");
    let update = () => component.$wire.$commit();
 18 changes: 9 additions & 9 deletions18  
dist/livewire.min.js
Large diffs are not rendered by default.

 2 changes: 1 addition & 1 deletion2  
dist/manifest.json
@@ -1,2 +1,2 @@

{"/livewire.js":"f41737f6"}
{"/livewire.js":"5d3e67e0"}
  16 changes: 13 additions & 3 deletions16  
docs/__nav.md
@@ -17,17 +17,27 @@ Features:
    Lazy Loading: { uri: /docs/lazy, file: /lazy.md }
    Validation: { uri: /docs/validation, file: /validation.md }
    File Uploads: { uri: /docs/uploads, file: /uploads.md }
    Loading States: { uri: /docs/loading, file: /loading.md }
    Pagination: { uri: /docs/pagination, file: /pagination.md }
    URL Query Parameters: { uri: /docs/url, file: /url.md }
    Computed Properties: { uri: /docs/computed-properties, file: /computed-properties.md }
    Redirecting: { uri: /docs/redirecting, file: /redirecting.md }
    Polling: { uri: /docs/polling, file: /polling.md }
    File Downloads: { uri: /docs/downloads, file: /downloads.md }
    Locked Properties: { uri: /docs/locked, file: /locked.md }
    Dirty States: { uri: /docs/dirty, file: /dirty.md }
    Offline States: { uri: /docs/offline, file: /offline.md }
    Teleport: { uri: /docs/teleport, file: /teleport.md }
HTML Directives:
    wire:click: { uri: /docs/wire-click, file: /wire-click.md }
    wire:submit: { uri: /docs/wire-submit, file: /wire-submit.md }
    wire:model: { uri: /docs/wire-model, file: /wire-model.md }
    wire:loading: { uri: /docs/wire-loading, file: /wire-loading.md }
    wire:dirty: { uri: /docs/wire-dirty, file: /wire-dirty.md }
    wire:navigate: { uri: /docs/wire-navigate, file: /wire-navigate.md }
    wire:transition: { uri: /docs/wire-transition, file: /wire-transition.md }
    wire:init: { uri: /docs/wire-init, file: /wire-init.md }
    wire:poll: { uri: /docs/wire-poll, file: /wire-poll.md }
    wire:offline: { uri: /docs/wire-offline, file: /wire-offline.md }
    wire:ignore: { uri: /docs/wire-ignore, file: /wire-ignore.md }
    wire:stream: { uri: /docs/wire-stream, file: /wire-stream.md }
Concepts:
    Morphing: { uri: /docs/morphing, file: /morph.md }
    Hydration: { uri: /docs/hydration, file: /hydration.md }
  3 changes: 2 additions & 1 deletion3  
docs/actions.md
@@ -56,6 +56,7 @@ Livewire supports a variety of event listeners, allowing you to respond to vario
| `wire:click`    | Triggered when an element is clicked      |
| `wire:submit`   | Triggered when a form is submitted        |
| `wire:keydown`  | Triggered when a key is pressed down      |
| `wire:keyup`  | Triggered when a key is released
| `wire:mouseenter`| Triggered when the mouse enters an element |
| `wire:*`| Whatever text follows `wire:` will be used as the event name of the listener |

@@ -226,7 +227,7 @@ Livewire provides a `wire:loading` directive that makes it trivial to show and h
</form>
```

`wire:loading` is a powerful feature with a variety of more powerful features. [Check out the full loading documentation for more information](/docs/loading).
`wire:loading` is a powerful feature with a variety of more powerful features. [Check out the full loading documentation for more information](/docs/wire-loading).

## Passing parameters

 76 changes: 0 additions & 76 deletions76  
docs/attributes.md
This file was deleted.

  24 changes: 24 additions & 0 deletions24  
docs/components.md
@@ -634,3 +634,27 @@ class ShowPost extends Component
```

The `$post` property will automatically be assigned to the model bound via the route's `{post}` parameter.

### Modifying the response

In some scenarios you might want to modify the response and set a custom response header. You can hook into the response object by calling the `response()` method on the view and use a closure to modify the response object:

```php
<?php

namespace App\Livewire;

use Livewire\Component;
use Illuminate\Http\Response;

class ShowPost extends Component
{
    public function render()
    {
        return view('livewire.show-post')
            ->response(function(Response $response) {
                $response->header('X-Custom-Header', true);
            });
    }
}
```
  20 changes: 19 additions & 1 deletion20  
docs/computed-properties.md
@@ -183,6 +183,25 @@ public function posts()

In the above example, until the cache expires or is busted, every instance of this component in your application will share the same cached value for `$this->posts`.

If you need to manually clear the cache for a computed property, you may set a custom cache key using the `key` parameter:

```php
use Livewire\Attributes\Computed;
use App\Models\Post;

#[Computed(cache: true, key: 'homepage-posts')]
public function posts()
{
    return Post::all();
}
```

Then somewhere else in your app, you may clear the cache for that property:

```php
Cache::forget('homepage-posts');
```

## When to use computed properties?

In addition to offering performance advantages, there are a few other scenarios where computed properties are helpful.
@@ -324,4 +343,3 @@ class ShowPosts extends Component
    @endforeach
</div>
```

  131 changes: 3 additions & 128 deletions131  
docs/forms.md
@@ -169,6 +169,8 @@ class CreatePost extends Component

    public function save()
    {
        $this->validate();

        Post::create(
            $this->form->all()
        );
@@ -387,7 +389,7 @@ Here's an example of adding a small loading spinner to the "Save" button via `wi

Now, when a user presses "Save", a small, inline spinner will show up.

Livewire's `wire:loading` feature has a lot more to offer. Visit the [Loading documentation to learn more.](/docs/loading)
Livewire's `wire:loading` feature has a lot more to offer. Visit the [Loading documentation to learn more.](/docs/wire-loading)

## Live-updating fields

@@ -678,130 +680,3 @@ Because `x-modelable` works for both `wire:model` and `x-model`, you can also us
```

Creating custom input elements in your application is extremely powerful but requires a deeper understanding of the utilities Livewire and Alpine provide and how they interact with each other.

## Input fields

Livewire supports most native input elements out of the box. Meaning you should just be able to attach `wire:model` to any input element in the browser and easily bind properties to them.

Here's a comprehensive list of the different available input types and how you use them in a Livewire context.

### Text inputs

First and foremost, text inputs are the bedrock of most forms. Here's how to bind a property named "title" to one:

```blade
<input type="text" wire:model="title">
```

### Textarea inputs

Textarea elements are similarly straightforward. Simply add `wire:model` to a textarea and the value will be bound:

```blade
<textarea type="text" wire:model="content"></textarea>
```

If the "content" value is initialized with a string, Livewire will fill the textarea with that value - there's no need to do something like the following:

```blade
<!-- Warning: This snippet demonstrates what NOT to do... -->
<textarea type="text" wire:model="content">{{ $content }}</textarea>
```

### Checkboxes

Checkboxes can be used for single values, such as when toggling a boolean property. Or, checkboxes may be used to toggle a single value in a group of related values. We'll discuss both scenarios:

#### Single checkbox

At the end of a signup form, you might have a checkbox allowing the user to opt-in to email updates. You might call this property `$receiveUpdates`. You can easily bind this value to the checkbox using `wire:model`:

```blade
<input type="checkbox" wire:model="receiveUpdates">
```

Now when the `$receiveUpdates` value is `false`, the checkbox will be unchecked. Of course, when the value is `true`, the checkbox will be checked.

#### Multiple checkboxes

Now, let's say in addition to allowing the user to decide to receive updates, you have an array property in your class called `$updateTypes`, allowing the user to choose from a variety of update types:

```php
public $updateTypes = [];
```

By binding multiple checkboxes to the `$updateTypes` property, the user can select multiple update types and they will be added to the `$updateTypes` array property:

```blade
<input type="checkbox" value="email" wire:model="updateTypes">
<input type="checkbox" value="sms" wire:model="updateTypes">
<input type="checkbox" value="notificaiton" wire:model="updateTypes">
```

For example, if the user checks the first two boxes but not the third, the value of `$updateTypes` will be: `["email", "sms"]`

### Radio buttons

To toggle between two different values for a single property, you may use radio buttons:

```blade
<input type="radio" value="yes" wire:model="receiveUpdates">
<input type="radio" value="no" wire:model="receiveUpdates">
```

### Select dropdowns

Livewire makes it simple to work with `<select>` dropdowns. When adding `wire:model` to a dropdown, the currently selected value will be bound to the provided property name and vice versa.

In addition, there's no need to manually add `selected` to the option that will be selected - Livewire handles that for you automatically.

Below is an example of a select dropdown filled with a static list of states:

```blade
<select wire:model="state">
    <option value="AL">Alabama<option>
    <option value="AK">Alaska</option>
    <option value="AZ">Arizona</option>
    ...
</select>
```

When a specific state is selected, for example, "Alaska", the `$state` property on the component will be set to `AK`. If you would prefer the value to be set to "Alaska" instead of "AK", you can leave the `value=""` attribute off the `<option>` element entirely.

Often, you may build your dropdown options dynamically using Blade:

```blade
<select wire:model="state">
    @foreach (\App\Models\State::all() as $state)
        <option value="{{ $state->id }}">{{ $state->label }}</option>
    @endforeach
</select>
```

If you don't have a specific option selected by default, you may want to show a muted placeholder option by default, such as "Select a state":

```blade
<select wire:model="state">
    <option disabled>Select a state...</option>
    @foreach (\App\Models\State::all() as $state)
        <option value="{{ $option->id }}">{{ $option->label }}</option>
    @endforeach
</select>
```

As you can see, there is no "placeholder" attribute for a select menu like there is for text inputs. Instead, you have to add a `disabled` option element as the first option in the list.

### Multi-select dropdowns

If you are using a "multiple" select menu, Livewire works as expected. In this example, states will be added to the `$states` array property when they are selected and removed if they are deselected:

```blade
<select wire:model="states" multiple>
    <option value="AL">Alabama<option>
    <option value="AK">Alaska</option>
    <option value="AZ">Arizona</option>
    ...
</select>
```
  2 changes: 1 addition & 1 deletion2  
docs/javascript.md
@@ -55,7 +55,7 @@ let components = Livewire.all()
### Interacting with events

In addition to dispatching and listening for events from individual components in PHP, the global `Livewire` object allows you interact with [Livewire's event system](/docs/events) from anywhere in your application:
In addition to dispatching and listening for events from individual components in PHP, the global `Livewire` object allows you to interact with [Livewire's event system](/docs/events) from anywhere in your application:

```js
// Dispatch an event to any Livewire components listening...
  3 changes: 3 additions & 0 deletions3  
docs/lazy.md
@@ -86,6 +86,9 @@ class Revenue extends Component

Because the above component specifies a "placeholder" by returning HTML from a `placeholder()` method, the user will see an SVG loading spinner on the page until the component is fully loaded.

> [!warning] The placeholder and the component must share the same element type
> For instance, if your placeholder's root element type is a 'div,' your component must also use a 'div' element.
### Rendering a placeholder via a view

For more complex loaders (such as skeletons) you can return a `view` from the `placeholder()` similar to `render()`. 
  2 changes: 2 additions & 0 deletions2  
docs/loading.md
@@ -171,10 +171,12 @@ The above element will only appear if the request takes over 200 milliseconds. T
To customize the amount of time to delay the loading indicator, you can use one of Livewire's helpful interval aliases:

```blade
<div wire:loading.delay.none>...</div>     <!-- 0ms -->
<div wire:loading.delay.shortest>...</div> <!-- 50ms -->
<div wire:loading.delay.shorter>...</div>  <!-- 100ms -->
<div wire:loading.delay.short>...</div>    <!-- 150ms -->
<div wire:loading.delay>...</div>          <!-- 200ms -->
<div wire:loading.delay.default>...</div>  <!-- 200ms -->
<div wire:loading.delay.long>...</div>     <!-- 300ms -->
<div wire:loading.delay.longer>...</div>   <!-- 500ms -->
<div wire:loading.delay.longest>...</div>  <!-- 1000ms -->
  7 changes: 3 additions & 4 deletions7  
docs/morph.md
@@ -133,7 +133,7 @@ Here is a visualization of the "look-ahead" algorithm in action:

### Injecting morph markers

On the backend, Livewire automatically detects conditionals inside Blade templates and wraps them in markers that Livewire's JavaScript can use as a guide when morphing.
On the backend, Livewire automatically detects conditionals inside Blade templates and wraps them in HTML comment markers that Livewire's JavaScript can use as a guide when morphing.

Here's an example of the previous Blade template but with Livewire's injected markers:

@@ -143,11 +143,11 @@ Here's an example of the previous Blade template but with Livewire's injected ma
        <input wire:model="title">
    </div>
    <!-- __BLOCK__ --> <!-- [tl! highlight] -->
    <!--[if BLOCK]><![endif]--> <!-- [tl! highlight] -->
    @if ($errors->has('title'))
        <div>Error: {{ $errors->first('title') }}</div>
    @endif
    <!-- __ENDBLOCK__ --> <!-- [tl! highlight] -->
    <!--[if ENDBLOCK]><![endif]--> <!-- [tl! highlight] -->
    <div>
        <button>Save</button>
@@ -188,4 +188,3 @@ For example, here's the above Blade template rewritten with wrapping `<div>` ele
```

Now that the conditional has been wrapped in a persistent element, Livewire will morph the two different HTML trees properly.

  13 changes: 13 additions & 0 deletions13  
docs/navigate.md
@@ -226,3 +226,16 @@ If you have a `<script>` tag in the body that you only want to be run once, you
    console.log('Runs only on page one')
</script>
```
## Customizing the progress bar
When a page takes longer than 150ms to load, Livewirew will show a progress bar at the top of the page.
You can customize the color of this bar or disable it all together inside Livewire's config file (`config/livewire.php`):
```php
'navigate' => [
    'show_progress_bar' => false,
    'progress_bar_color' => '#2299dd',
],
```
  2 changes: 1 addition & 1 deletion2  
docs/nesting.md
@@ -498,7 +498,7 @@ class Steps extends Component

    public function next()
    {
        $currentIndex = array_search($this->steps, $this->current);
        $currentIndex = array_search($this->current, $this->steps);

        $this->current = $this->steps[$currentIndex + 1];
    }
  16 changes: 16 additions & 0 deletions16  
docs/pagination.md
@@ -49,6 +49,22 @@ As you can see, in addition to limiting the number of posts shown via the `Post:

For more information on pagination using Laravel, check out [Laravel's comprehensive pagination documentation](https://laravel.com/docs/pagination).

## Customizing scroll behavior

By default, Livewire's paginator scrolls to the top of the page after every page change.

You disable this behavior by passing `false` to the `scrollTo` parameter of the `links()` method like so:

```blade
{{ $posts->links(data: ['scrollTo' => false]) }}
```

Alternatively, you can provide any CSS selector to the `scrollTo` parameter, and Livewire will find the nearest element matching that selector and scroll to it after each navigation:

```blade
{{ $posts->links(data: ['scrollTo' => '#paginated-posts']) }}
```

## Resetting the page

When sorting or filtering results, it is common to want to reset the page number back to `1`.
  5 changes: 5 additions & 0 deletions5  
docs/properties.md
@@ -195,6 +195,11 @@ Supported PHP types:
| Carbon | `Carbon\Carbon` |
| Stringable | `Illuminate\Support\Stringable` |

> [!warning] Eloquent Collections and Models
> When storing Eloquent Collections and Models in Livewire properties, additional query constraints like select(...) will not be re-applied on subsequent requests.
>
> See [Eloquent constraints aren't preserved between requests](#eloquent-constraints-arent-preserved-between-requests) for more details
Here's a quick example of setting properties as these various types:

```php
  56 changes: 55 additions & 1 deletion56  
docs/testing.md
@@ -319,6 +319,57 @@ class SearchPostsTest extends TestCase
}
```

### Setting cookies

If your Livewire component depends on cookies, you can use the `withCookie()` or `withCookies()` methods to set the cookies manually for your test.

Below is a basic `Cart` component that loads a discount token from a cookie on mount:

```php
<?php

namespace App\Livewire;

use Livewire\Component;
use Livewire\With\Url;
use App\Models\Post;

class Cart extends Component
{
    public $discountToken;

    public mount()
    {
        $this->discountToken = request()->cookie('discountToken');
    }
}
```

As you can see, the `$discountToken` property above gets its value from a cookie in the request.

Below is an example of how you would simulate the scenario of loading this component on a page with cookies:

```php
<?php

namespace Tests\Feature\Livewire;

use App\Livewire\Cart;
use Livewire\Livewire;
use Tests\TestCase;

class CartTest extends TestCase
{
    /** @test */
    public function can_load_discount_token_from_a_cookie()
    {
        Livewire::withCookies(['discountToken' => 'CALEB2023'])
            ->test(Cart::class)
            ->assertSet('discountToken', 'CALEB2023');
    }
}
```

## Calling actions

Livewire actions are typically called from the frontend using something like `wire:click`.
@@ -391,7 +442,7 @@ class CreatePostTest extends TestCase
If you want to test that a specific validation rule has failed, you can pass an array of rules:

```php
$this->assertHasErrors(['title', ['required']]);
$this->assertHasErrors(['title' => ['required']]);
```

### Authorization
@@ -587,6 +638,9 @@ Livewire provides many more testing utilities. Below is a comprehensive list of
| `Livewire::test(UpdatePost::class, ['post' => $post])`                      | Test the `UpdatePost` component with the `post` parameter (To be received through the `mount()` method) |
| `Livewire::actingAs($user)`                      | Set the provided user as the session's authenticated user |
| `Livewire::withQueryParams(['search' => '...'])`                      | Set the test's `search` URL query parameter to the provided value (ex. `?search=...`). Typically in the context of a property using Livewire's [`#[Url]` attribute](/docs/url) |
| `Livewire::withCookie('color', 'blue')`                      | Set the test's `color` cookie to the provided value (`blue`). |
| `Livewire::withCookies(['color' => 'blue', 'name' => 'Taylor])`                      | Set the test's `color` and `name` cookies to the provided values (`blue`, `Taylor`). |


### Interacting with components
| Method                                                  | Description                                                                                                      |
  2 changes: 1 addition & 1 deletion2  
docs/upgrading.md
@@ -349,7 +349,7 @@ class Dashboard extends Component

The three main changes from Livewire 2 are:

1. `emit()` has been renamed to `dispatch()`
1. `emit()` has been renamed to `dispatch()` (Likewise `emitTo()` and `emitSelf()` are now `dispatchTo()` and `dispatchSelf()`)
1. `dispatchBrowserEvent()` has been renamed to `dispatch()`
2. All event parameters must be named

  2 changes: 1 addition & 1 deletion2  
docs/uploads.md
@@ -291,7 +291,7 @@ You can display a loading indicator scoped to the file upload like so:

Now, while the file is uploading, the "Uploading..." message will be shown and then hidden when the upload is finished.

For more information on loading states, check out our comprehensive [loading state documentation](/docs/loading).
For more information on loading states, check out our comprehensive [loading state documentation](/docs/wire-loading).

## Progress indicators

  41 changes: 41 additions & 0 deletions41  
docs/url.md
@@ -158,3 +158,44 @@ class ShowUsers extends Component
```

In the example above, when a user changes the search value from "bob" to "frank" and then clicks the browser's back button, the search value (and the URL) will be set back to "bob" instead of navigating to the previously visited page.

## Using the queryString method

The query string can also be defined as a method on the component. This can be useful if some properties have dynamic options.

```php
use Livewire\Component;

class ShowUsers extends Component
{
    // ...

    protected function queryString()
    {
        return [
            'search' => [
                'as' => 'q',
            ],
        ];
    }
}
```

## Trait hooks

Livewire offers [hooks](/docs/lifecycle-hooks) for query strings as well.

```php
trait WithSorting
{
    // ...

    protected function queryStringWithSorting()
    {
        return [
            'sortBy' => ['as' => 'sort'],
            'sortDirection' => ['as' => 'direction'],
        ];
    }
}
```
  4 changes: 2 additions & 2 deletions4  
docs/validation.md
@@ -376,10 +376,10 @@ class CreatePost extends Component
        return [
            'content.required' => 'The :attribute are missing.',
            'content.min' => 'The :attribute is too short.',
        ]
        ];
    }

    public function attributes() // [tl! highlight:6]
    public function validationAttributes() // [tl! highlight:6]
    {
        return [
            'content' => 'description',
  7 changes: 7 additions & 0 deletions7  
docs/volt.md
@@ -52,6 +52,13 @@ By adding the `--test` directive when generating a component, a corresponding te
php artisan make:volt counter --test --pest
```


By adding the `--class` directive it will generate a class-based volt component.

```bash
php artisan make:volt counter --class
```

## API style

By utilizing Volt's functional API, we can define a Livewire component's logic through imported `Livewire\Volt` functions. Volt then transforms and compiles the functional code into a conventional Livewire class, enabling us to leverage the extensive capabilities of Livewire with reduced boilerplate.
 45 changes: 45 additions & 0 deletions45  
docs/wire-click.md
@@ -0,0 +1,45 @@

Livewire provides a simple `wire:click` directive for calling component methods (aka actions) when a user clicks a specific element on the page.

For example, given the `ShowInvoice` component below:

```php
<?php

namespace App\Livewire;

use Livewire\Component;
use App\Models\Invoice;

class ShowInvoice extends Component
{
    public Invoice $invoice;

    public function download()
    {
        return response()->download(
            $this->invoice->file_path, 'invoice.pdf'
        );
    }
}
```

You can trigger the `download()` method from the class above when a user clicks a "Download Invoice" button by adding `wire:click="download"`:

```html
<button type="button" wire:click="download"> <!-- [tl! highlight] -->
    Download Invoice
</button>
```

## Using `wire:click` on links

When using `wire:click` on `<a>` tags, you must append `.prevent` to prevent the default handling of a link in the browser. Otherwise, the browser will visit the provided link and update the page's URL.

```html
<a href="#" wire:click.prevent="...">
```

## Going deeper

The `wire:click` directive is just one of many different avilable event listeners in Livewire. For full documentation on its (and other event listeners) capabilities, visit [the Livewire actions documentation page](/docs/actions).
 67 changes: 67 additions & 0 deletions67  
docs/wire-dirty.md
@@ -0,0 +1,67 @@

In a traditional HTML page containing a form, the form is only ever submitted when the user presses the "Submit" button.

However, Livewire is capable of much more than traditional form submissions. You can validate form inputs in real-time or even save the form as a user types.

In these "real-time" update scenarios, it can be helpful to signal to your users when a form or subset of a form has been changed, but hasn't been saved to the database.

When a form contains un-saved input, that form is considered "dirty". It only becomes "clean" when a network request has been triggered to synchronize the server state with the client-side state.

## Basic usage

Livewire allows you to easily toggle visual elements on the page using the `wire:dirty` directive.

By adding `wire:dirty` to an element, you are instructing Livewire to only show the element when the client-side state diverges from the server-side state.

To demonstrate, here is an example of an `UpdatePost` form containing a visual "Unsaved changes..." indication that signals to the user that the form contains input that has not been saved:

```blade
<form wire:submit="update">
    <input type="text" wire:model="title">
    <!-- ... -->
    <button type="submit">Update</button>
    <div wire:dirty>Unsaved changes...</div> <!-- [tl! highlight] -->
</form>
```

Because `wire:dirty` has been added to the "Unsaved changes..." message, the message will be hidden by default. Livewire will automatically display the message when the user starts modifying the form inputs.

When the user submits the form, the message will disappear again, since the server / client data is back in sync.

### Removing elements

By adding the `.remove` modifier to `wire:dirty`, you can instead show an element by default and only hide it when the component has "dirty" state:

```blade
<div wire:dirty.remove>The data is in-sync...</div>
```

## Targeting property updates

Imagine you are using `wire:model.blur` to update a property on the server immediately after a user leaves an input field. In this scenario, you can provide a "dirty" indication for only that property by adding `wire:target` to the element that contains the `wire:dirty` directive.

Here is an example of only showing a dirty indication when the title property has been changed:

```blade
<form wire:submit="update">
    <input wire:model.blur="title">
    <div wire:dirty wire:target="title">Unsaved title...</div> <!-- [tl! highlight] -->
    <button type="submit">Update</button>
</form>
```

## Toggling classes

Often, instead of toggling entire elements, you may want to toggle individual CSS classes on an input when its state is "dirty".

Below is an example where a user types into an input field and the border becomes yellow, indicating an "unsaved" state. Then, when the user tabs away from the field, the border is removed, indicating that the state has been saved on the server:

```blade
<input wire:model.blur="title" wire:dirty.class="border-yellow-500">
```

 30 changes: 30 additions & 0 deletions30  
docs/wire-ignore.md
@@ -0,0 +1,30 @@

Livewire's ability to make updates to the page is what makes it "live", however, there are times where you might want to prevent Livewire from updating a portion of the page.

In these cases, you can use the `wire:ignore` directive to instruct Livewire to ignore the contents of a particular element, even if they change between requests.

This is most useful in the context of working with third-party javascript libraries for custom form inputs and such.

Below is an example of wrapping an element used by a third-party library in `wire:ignore` so that Livewire doesn't tamper with the HTML generated by the library:

```blade
<form>
    <!-- ... -->
    <div wire:ignore>
        <!-- This element would be reference by a -->
        <!-- third-party library for initialization... -->
        <input id="id-for-date-picker-library">
    </div>
    <!-- ... -->
</form>
```

You can also instruct Livewire to only ignore changes to attributes of the root element rather than observing changes to it's contents using `wire:ignore.self`.

```blade
<div wire:ignore.self>
    <!-- ... -->
</div>
```
 12 changes: 12 additions & 0 deletions12  
docs/wire-init.md
@@ -0,0 +1,12 @@

Livewire offers a `wire:init` directive to run an action as soon as the component is rendered. This can be helpful in cases where you don't want to hold up the entire page load, but want to load some data immediately after the page load.

```blade
<div wire:init="loadPosts">
    <!-- ... -->
</div>
```

The `loadPosts` action will be run immediately after the Livewire component renders on the page.

In most cases however, [Livewire's lazy loading feature](/docs/lazy) is preferable to using `wire:init`.
 182 changes: 182 additions & 0 deletions182  
docs/wire-loading.md
@@ -0,0 +1,182 @@

Loading indicators are an important part of crafting good user interfaces. They give users visual feedback when a request is being made to the server so they know they are waiting for a process to complete.

## Basic usage

Livewire provides a simple yet extremely powerful syntax for controlling loading indicators: `wire:loading`. Adding `wire:loading` to any element will hide it by default (using `display: none` in CSS) and show it when a request is sent to the server.

Below is a basic example of a `CreatePost` component's form with `wire:loading` being used to toggle a loading message:

```blade
<form wire:submit="save">
    <!-- ... -->
    <button type="submit">Save</button>
    <div wire:loading> <!-- [tl! highlight:2] -->
        Saving post...
    </div>
</form>
```

When a user presses "Save", the "Saving post..." message will appear below the button while the "save" action is being executed. The message will disappear when the response is received from the server and processed by Livewire.

### Removing elements

Alternatively, you can append `.remove` for the inverse effect, showing an element by default and hiding it during requests to the server:

```blade
<div wire:loading.remove>...</div>
```

## Toggling classes

In addition to toggling the visibility of entire elements, it's often useful to change the styling of an existing element by toggling CSS classes on and off during requests to the server. This technique can be used for things like changing background colors, lowering opacity, triggering spinning animations, and more.

Below is a simple example of using the [Tailwind](https://tailwindcss.com/) class `opacity-50` to make the "Save" button fainter while the form is being submitted:

```blade
<button wire:loading.class="opacity-50">Save</button>
```

Like toggling an element, you can perform the inverse class operation by appending `.remove` to the `wire:loading` directive. In the example below, the button's `bg-blue-500` class will be removed when the "Save" button is pressed:

```blade
<button class="bg-blue-500" wire:loading.class.remove="bg-blue-500">
    Save
</button>
```

## Toggling attributes

By default, when a form is submitted, Livewire will automatically disable the submit button and add the `readonly` attribute to each input element while the form is being processed.

However, in addition to this default behavior, Livewire offers the `.attr` modifier to allow you to toggle other attributes on an element or toggle attributes on elements that are outside of forms:

```blade
<button
    type="button"
    wire:click="remove"
    wire:loading.attr="disabled"
>
    Remove
</button>
```

Because the button above isn't a submit button, it won't be disabled by Livewire's default form handling behavior when pressed. Instead, we manually added `wire:loading.attr="disabled"` to achieve this behavior.

## Targeting specific actions

By default, `wire:loading` will be triggered whenever a component makes a request to the server.

However, in components with multiple elements that can trigger server requests, you should scope your loading indicators down to individual actions.

For example, consider the following "Save post" form. In addition to a "Save" button that submits the form, there might also be a "Remove" button that executes a "remove" action on the component.

By adding `wire:target` to the following `wire:loading` element, you can instruct Livewire to only show the loading message when the "Remove" button is clicked:

```blade
<form wire:submit="save">
    <!-- ... -->
    <button type="submit">Save</button>
    <button type="button" wire:click="remove">Remove</button>
    <div wire:loading wire:target="remove">  <!-- [tl! highlight:2] -->
        Removing post...
    </div>
</form>
```

When the above "Remove" button is pressed, the "Removing post..." message will be displayed to the user. However, the message will not be displayed when the "Save" button is pressed.

### Targeting action parameters

In situations where the same action is triggered with different parameters from multiple places on a page, you can further scope `wire:target` to a specific action by passing in additional parameters. For example, consider the following scenario where a "Remove" button exists for each post on the page:

```blade
<div>
    @foreach ($posts as $post)
        <div wire:key="{{ $post->id }}">
            <h2>{{ $post->title }}</h2>
            <button wire:click="remove({{ $post->id }})">Remove</button>
            <div wire:loading wire:target="remove({{ $post->id }})">  <!-- [tl! highlight:2] -->
                Removing post...
            </div>
        </div>
    @endforeach
</div>
```

Without passing `{{ $post->id }}` to `wire:target="remove"`, the "Removing post..." message would show when any of the buttons on the page are clicked.

However, because we are passing in unique parameters to each instance of `wire:target`, Livewire will only show the loading message when the matching parameters are passed to the "remove" action.

### Targeting property updates

Livewire also allows you to target specific component property updates by passing the property's name to the `wire:target` directive.

Consider the following example where a form input named `username` uses `wire:model.live` for real-time validation as a user types:

```blade
<form wire:submit="save">
    <input type="text" wire:model.live="username">
    @error('username') <span>{{ $message }}</span> @enderror
    <div wire:loading wire:target="username"> <!-- [tl! highlight:2] -->
        Checking availability of username...
    </div>
    <!-- ... -->
</form>
```

The "Checking availability..." message will show when the server is updated with the new username as the user types into the input field.

## Customizing CSS display property

When `wire:loading` is added to an element, Livewire updates the CSS `display` property of the element to show and hide the element. By default, Livewire uses `none` to hide and `inline-block` to show.

If you are toggling an element that uses a display value other than `inline-block`, like `flex` in the following example, you can append `.flex` to `wire:loading`:

```blade
<div class="flex" wire:loading.flex>...</div>
```

Below is the complete list of available display values:

```blade
<div wire:loading.inline-flex>...</div>
<div wire:loading.inline>...</div>
<div wire:loading.block>...</div>
<div wire:loading.table>...</div>
<div wire:loading.flex>...</div>
<div wire:loading.grid>...</div>
```

## Delaying a loading indicator

On fast connections, updates often happen so quickly that loading indicators only flash briefly on the screen before being removed. In these cases, the indicator is more of a distraction than a helpful affordance.

For this reason, Livewire provides a `.delay` modifier to delay the showing of an indicator. For example, if you add `wire:loading.delay` to an element like so:

```blade
<div wire:loading.delay>...</div>
```

The above element will only appear if the request takes over 200 milliseconds. The user will never see the indicator if the request completes before then.

To customize the amount of time to delay the loading indicator, you can use one of Livewire's helpful interval aliases:

```blade
<div wire:loading.delay.shortest>...</div> <!-- 50ms -->
<div wire:loading.delay.shorter>...</div>  <!-- 100ms -->
<div wire:loading.delay.short>...</div>    <!-- 150ms -->
<div wire:loading.delay>...</div>          <!-- 200ms -->
<div wire:loading.delay.long>...</div>     <!-- 300ms -->
<div wire:loading.delay.longer>...</div>   <!-- 500ms -->
<div wire:loading.delay.longest>...</div>  <!-- 1000ms -->
```
 243 changes: 243 additions & 0 deletions243  
docs/wire-model.md
@@ -0,0 +1,243 @@

Livewire makes it easy to bind a component property's value with form inputs using `wire:model`.

Here is a simple example of using `wire:model` to bind the `$title` and `$content` properties with form inputs in a "Create Post" component:

```php
use Livewire\Component;
use App\Models\Post;

class CreatePost extends Component
{
    public $title = '';

    public $content = '';

    public function save()
    {
		$post = Post::create([
			'title' => $this->title
			'content' => $this->content
		]);

        // ...
    }
}
```

```blade
<form wire:submit="save">
    <label>
        <span>Title</span>
        <input type="text" wire:model="title"> <!-- [tl! highlight] -->
    </label>
    <label>
        <span>Content</span>
        <textarea wire:model="content"></textarea> <!-- [tl! highlight] -->
    </label>
	<button type="submit">Save</button>
</form>
```

Because both inputs use `wire:model`, their values will be synchronized with the server's properties when the "Save" button is pressed.

> [!warning] "Why isn't my component live updating as I type?"
> If you tried this in your browser and are confused why the title isn't automatically updating, it's because Livewire only updates a component when an "action" is submitted—like pressing a submit button—not when a user types into a field. This cuts down on network requests and improves performance. To enable "live" updating as a user types, you can use `wire:model.live` instead. [Learn more about data binding](/docs/properties#data-binding).
## Customizing update timing

By default, Livewire will only send a network request when an action is performed (like `wire:click` or `wire:submit`), NOT when a `wire:model` input is updated.

This drastically improves the performance of Livewire by reducing network requests and providing a smoother experience for your users.

However, there are occasions where you may want to update the server more frequently for things like real-time validation.

### Live updating

To send property updates to the server as a user types into an input-field, you can append the `.live` modifier to `wire:model`:

```html
<input type="text" wire:model.live="title">
```

#### Customizing the debounce

By default, when using `wire:model.live`, Livewire adds a 150 millisecond debounce to server updates. This means if a user is continually typing, Livewire will wait untill the user stops typing for 150 milliseconds before sending a request.

You can customize this timing by appending `.debounce.Xms` to the input. Here is an example of changing the debounce to 250 milliseconds:

```html
<input type="text" wire:model.live.debounce.250ms="title">
```

### Updating on "blur" event

By appending the `.blur` modifier, Livewire will only send network requests with property updates when a user clicks away from an input, or presses the tab key to move to the next input.

Adding `.blur` is helpful for scenarios where you want to update the server more frequently, but not as a user types. For example, real-time validation is a common instance where `.blur` is helpful.

```html
<input type="text" wire:model.blur="title">
```

### Updating on "change" event

There are times where the behavior of `.blur` isn't exactly what you want and instead `.change` is.

For example, if you want to run validation every time a select input is changed, by adding `.change`, Livewire will send a network request and validate the property as soon as a user selects a new option. As opposed to `.blur` which will only update the server after the user tabs away from the select input.

```html
<select wire:model.change="title">
    <!-- ... -->
</select>
```

Any changes made to the text input will be automatically synchronized with the `$title` property in your Livewire component.

## All available modifiers

Modifier | Description
--- | ---
`.live` | Send updates as a user types
`.blur` | Only send updates on the `blur` event
`.change` | Only send updates on the the `change` event
`.lazy` | An alias for `.change`
`.debounce.[?]ms` | Debounce the sending of updates by specified millisecond delay
`.throttle.[?]ms` | Throttle network request updates by the specified millisecond interval
`.number` | Cast the text value of an input to `int` on the server
`.fill` | Use the initial value provided by a "value" HTML attribute on page-load

## Input fields

Livewire supports most native input elements out of the box. Meaning you should just be able to attach `wire:model` to any input element in the browser and easily bind properties to them.

Here's a comprehensive list of the different available input types and how you use them in a Livewire context.

### Text inputs

First and foremost, text inputs are the bedrock of most forms. Here's how to bind a property named "title" to one:

```blade
<input type="text" wire:model="title">
```

### Textarea inputs

Textarea elements are similarly straightforward. Simply add `wire:model` to a textarea and the value will be bound:

```blade
<textarea type="text" wire:model="content"></textarea>
```

If the "content" value is initialized with a string, Livewire will fill the textarea with that value - there's no need to do something like the following:

```blade
<!-- Warning: This snippet demonstrates what NOT to do... -->
<textarea type="text" wire:model="content">{{ $content }}</textarea>
```

### Checkboxes

Checkboxes can be used for single values, such as when toggling a boolean property. Or, checkboxes may be used to toggle a single value in a group of related values. We'll discuss both scenarios:

#### Single checkbox

At the end of a signup form, you might have a checkbox allowing the user to opt-in to email updates. You might call this property `$receiveUpdates`. You can easily bind this value to the checkbox using `wire:model`:

```blade
<input type="checkbox" wire:model="receiveUpdates">
```

Now when the `$receiveUpdates` value is `false`, the checkbox will be unchecked. Of course, when the value is `true`, the checkbox will be checked.

#### Multiple checkboxes

Now, let's say in addition to allowing the user to decide to receive updates, you have an array property in your class called `$updateTypes`, allowing the user to choose from a variety of update types:

```php
public $updateTypes = [];
```

By binding multiple checkboxes to the `$updateTypes` property, the user can select multiple update types and they will be added to the `$updateTypes` array property:

```blade
<input type="checkbox" value="email" wire:model="updateTypes">
<input type="checkbox" value="sms" wire:model="updateTypes">
<input type="checkbox" value="notificaiton" wire:model="updateTypes">
```

For example, if the user checks the first two boxes but not the third, the value of `$updateTypes` will be: `["email", "sms"]`

### Radio buttons

To toggle between two different values for a single property, you may use radio buttons:

```blade
<input type="radio" value="yes" wire:model="receiveUpdates">
<input type="radio" value="no" wire:model="receiveUpdates">
```

### Select dropdowns

Livewire makes it simple to work with `<select>` dropdowns. When adding `wire:model` to a dropdown, the currently selected value will be bound to the provided property name and vice versa.

In addition, there's no need to manually add `selected` to the option that will be selected - Livewire handles that for you automatically.

Below is an example of a select dropdown filled with a static list of states:

```blade
<select wire:model="state">
    <option value="AL">Alabama<option>
    <option value="AK">Alaska</option>
    <option value="AZ">Arizona</option>
    ...
</select>
```

When a specific state is selected, for example, "Alaska", the `$state` property on the component will be set to `AK`. If you would prefer the value to be set to "Alaska" instead of "AK", you can leave the `value=""` attribute off the `<option>` element entirely.

Often, you may build your dropdown options dynamically using Blade:

```blade
<select wire:model="state">
    @foreach (\App\Models\State::all() as $state)
        <option value="{{ $state->id }}">{{ $state->label }}</option>
    @endforeach
</select>
```

If you don't have a specific option selected by default, you may want to show a muted placeholder option by default, such as "Select a state":

```blade
<select wire:model="state">
    <option disabled>Select a state...</option>
    @foreach (\App\Models\State::all() as $state)
        <option value="{{ $option->id }}">{{ $option->label }}</option>
    @endforeach
</select>
```

As you can see, there is no "placeholder" attribute for a select menu like there is for text inputs. Instead, you have to add a `disabled` option element as the first option in the list.

### Multi-select dropdowns

If you are using a "multiple" select menu, Livewire works as expected. In this example, states will be added to the `$states` array property when they are selected and removed if they are deselected:

```blade
<select wire:model="states" multiple>
    <option value="AL">Alabama<option>
    <option value="AK">Alaska</option>
    <option value="AZ">Arizona</option>
    ...
</select>
```

## Going deeper

For a more complete documentation on using `wire:model` in the context of HTML forms, visit the [Livewire forms documentation page](/docs/forms).
 28 changes: 28 additions & 0 deletions28  
docs/wire-navigate.md
@@ -0,0 +1,28 @@

Livewire's `wire:navigate` feature makes page navigate much faster, providing an SPA-like experience for your users.

This page is a simple reference for the `wire:navigate` directive. Be sure to read the [page on Livewire's Navigate feature](/docs/navigate) for more complete documentation.

Below is a simple example of adding `wire:navigate` to links in a nav bar:

```blade
<nav>
    <a href="/" wire:navigate>Dashboard</a>
    <a href="/posts" wire:navigate>Posts</a>
    <a href="/users" wire:navigate>Users</a>
</nav>
```

When any of these links are clicked, Livewire will intercept the click and instead of allowing the browser to perform a full page visit, Livewire will fetch the page in the background and swap it with the current page (resulting in much faster and smoother page navigation).

## Prefetching pages on hover

By adding the `.hover` modifier, Livewire will pre-fetch a page when a user hovers over a link. This way, the page will have already been downloaded from the server when the user clicks on the link.

```blade
<a href="/" wire:navigate.hover>Dashboard</a>
```

## Going deeper

For more complete documentation on this feature, visit [Livewire's navigate documentation page](/docs/navigate).
 14 changes: 14 additions & 0 deletions14  
docs/wire-offline.md
@@ -0,0 +1,14 @@

In certain circumstances it can be helpful for your users to know if they are currently connected to the internet.

If for example, you have built a blogging platform on Livewire, you may want to notify your users in some way if they are offline so that they don't draft an entire blog post without the ability for Livewire to save it to the database.

Livewire make this trivial by providing the `wire:offline` directive. By attaching `wire:offline` to an element in your Livewire component, it will be hidden by default and only be displayed when Livewire detects the network connection has been interupted and is unavailable. It will then dissapear again when the network has regained connection.

For example:

```blade
<p class="alert alert-warning" wire:offline>
    Whoops, your device has lost connection. The web page you are viewing is offline.
</p>
```
 78 changes: 78 additions & 0 deletions78  
docs/wire-poll.md
@@ -0,0 +1,78 @@
Polling is a technique used in web applications to "poll" the server (send requests on a regular interval) for updates. It's a simple way to keep a page up-to-date without the need for a more sophisticated technology like [WebSockets](/docs/events#real-time-events-using-laravel-echo).

## Basic usage

Using polling inside Livewire is as simple as adding `wire:poll` to an element.

Below is an example of a `SubscriberCount` component that shows a user's subscriber count:

```php
<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;

class SubscriberCount extends Component
{
    public function render()
    {
        return view('livewire.subscriber-count', [
            'count' => Auth::user()->subscribers->count(),
        ]);
    }
}
```

```blade
<div wire:poll> <!-- [tl! highlight] -->
    Subscribers: {{ $count }}
</div>
```

Normally, this component would show the subscriber count for the user and never update until the page was refreshed. However, because of `wire:poll` on the component's template, this component will now refresh itself every `2.5` seconds, keeping the subscriber count up-to-date.

You can also specify an action to fire on the polling interval by passing a value to `wire:poll`:

```blade
<div wire:poll="refreshSubscribers">
    Subscribers: {{ $count }}
</div>
```

Now, the `refreshSubscribers()` method on the component will be called every `2.5` seconds.

## Timing control

The primary drawback of polling is that it can be resource intensive. If you have a thousand visitors on a page that uses polling, one thousand network requests will be triggered every `2.5` seconds.

The best way to reduce requests in this scenario is simply to make the polling interval longer.

You can manually control how often the component will poll by appending the desired duration to `wire:poll` like so:

```blade
<div wire:poll.15s> <!-- In seconds... -->
<div wire:poll.15000ms> <!-- In milliseconds... -->
```

## Background throttling

To further cut down on server requests, Livewire automatically throttles polling when a page is in the background. For example, if a user keeps a page open in a different browser tab, Livewire will reduce the number of polling requests by 95% until the user revisits the tab.

If you want to opt-out of this behavior and keep polling continuously, even when a tab is in the background, you can add the `.keep-alive` modifier to `wire:poll`:

```blade
<div wire:poll.keep-alive>
```

##  Viewport throttling

Another measure you can take to only poll when necessary, is to add the `.visible` modifier to `wire:poll`. The `.visible` modifier instructs Livewire to only poll the component when it is visible on the page:

```blade
<div wire:poll.visible>
```

If a component using `wire:visible` is at the bottom of a long page, it won't start polling until the user scrolls it into the viewport. When the user scrolls away, it will stop polling again.
 158 changes: 158 additions & 0 deletions158  
docs/wire-stream.md
@@ -0,0 +1,158 @@

Livewire allows you to stream content to a web page before a request is complete via the `wire:stream` API. This is an extremely useful feature for things like AI chat-bots which stream responses as they are generated.

To demonstrate the most basic functionality of `wire:stream`, below is a simple CountDown component that when a button is pressed displays a count-down to the user from "3" to "0":

```php
use Livewire\Component;

class CountDown extends Component
{
    public $start = 3;

    public function begin()
    {
        while ($start >= 0) {
            // Stream the current count to the browser...
            $this->stream(  // [tl! highlight:4]
                to: 'count',
                content: $this->start,
                replace: true,
            );

            // Pause for 1 second between numbers...
            sleep(1);

            // Decrement the counter...
            $this->start = $this->start - 1;
        };
    }

    public function render()
    {
        return <<<HTML
        <div>
            <button wire:click="begin">Start count-down</button>

            <h1>Count: <span wire:stream="count">{{ $start }}</span></h1> <!-- [tl! highlight] -->
        </div>
        HTML;
    }
}
```

Here's what's happening from the user's perspective when they press "Start count-down":
* "Count: 3" is shown on the page
* They press the "Start count-down" button
* One second elapses and "Count: 2" is shown
* This process continues until "Count: 0" is shown

All of the above happens while a single network request is out to the server.

Here's what's happening from the system's perspective when the button is pressed:
* A request is sent to Livewire to call the `begin()` method
* The `begin()` method is called and the `while` loop begins
* `$this->stream()` is called and immediately starts a "streamed response" to the browser
* The browser receives a streamed response with instructions to find the element in the component with `wire:stream="count"`, and replace its contents with the received payload ("3" in the case of the first streamed number)
* The `sleep(1)` method causes the server to hang for one second
* The `while` loop is repeated and the process of streaming a new number ever second continues until the `while` condition is falsy
* When `begin()` has finished running and all the counts have beeen streamed to the browser, Livewire finishes it's request lifecycle, rendering the component and sending the final response to the browser

## Streaming chat-bot responses

A common use-case for `wire:stream` is streaming chat-bot responses as they are received from an API that supports streamed responses (like [OpenAI's ChatGPT](https://chat.openai.com/)).

Below is an example of using `wire:stream` to accomplish a ChatGPT-like interface:

```php
use Livewire\Component;

class ChatBot extends Component
{
    public $prompt = '';

    public $question = '';

    public $answer = '';

    function submitPrompt()
    {
        $this->question = $this->prompt;

        $this->prompt = '';

        $this->js('$wire.ask()');
    }

    function ask()
    {
        $this->answer = OpenAI::ask($this->question, function ($partial) {
            $this->stream(to: 'answer', content: $partial); // [tl! highlight]
        });
    }

    public function render()
    {
        return <<<'HTML'
        <div>
            <section>
                <div>ChatBot</div>

                @if ($question)
                    <article>
                        <hgroup>
                            <h3>User</h3>
                            <p>{{ $question }}</p>
                        </hgroup>

                        <hgroup>
                            <h3>ChatBot</h3>
                            <p wire:stream="answer">{{ $answer }}</p> <!-- [tl! highlight] -->
                        </hgroup>
                    </article>
                @endif
            </section>

            <form wire:submit="submitPrompt">
                <input wire:model="prompt" type="text" placeholder="Send a message" autofocus>
            </form>
        </div>
        HTML;
    }
}
```

Here's what's going on in the above example:
* A user types into a text field labelled "Send a message" to ask the chat-bot a question
* They press the [Enter] key
* A network request is sent to the server, sets the message to the `$question` property, and clears the `$prompt` property.
* The response is sent back to the browser and the input is cleared. Because `$this->js('...')` was called, a new request is triggered to the server calling the `ask()` method.
* The `ask()` method calls on the ChatBot API and receives streamed response partials via the `$partial` parameter in the callback.
* Each `$partial` get's streamed to the browser into the `wire:stream="answer"` element on the page, showing the answer progressively reveal itself to the user
* When the entire response is received, the Livewire request finishes and the user receives the full response.

## Replace vs. append

When streaming content to an element using `$this->stream()`, you can tell Livewire to either replace the contents of the target element with the streamed contents or append them to the existing contents.

Replacing or appending can both be desireable depending on the scenario. For example, when streaming a response from a chatbot, typically appending is desired (and is therefore the default). However, when showing something like a count-down, replacing is more fitting.

You can configure either by passing the `replace:` parameter to `$this->stream` with a boolean value:

```php
// Append contents...
$this->stream(to: 'target', content: '...');

// Replace contents...
$this->stream(to: 'target', content: '...', replace: true);
```

Append/replace can also be specified at the target element level by appending or removing the `.replace` modifier:

```blade
// Append contents...
<div wire:stream="target">
// Replace contents...
<div wire:stream.replace="target">
```
 60 changes: 60 additions & 0 deletions60  
docs/wire-submit.md
@@ -0,0 +1,60 @@

Livewire makes it easy to handle form submissions via the `wire:submit` directive. By adding `wire:submit` to a `<form>` element, Livewire will intercept the form submission, prevent the default browser handling, and call any Livewire component method.

Here's a basic example of using `wire:submit` to handle a "Create Post" form submission:

```php
<?php

namespace App\Livewire;

use Livewire\Component;
use App\Models\Post;

class CreatePost extends Component
{
    public $title = '';

    public $content = '';

    public function save()
    {
        Post::create([
            'title' => $this->title,
            'content' => $this->content,
        ]);

        return redirect()->to('/posts');
    }

    public function render()
    {
        return view('livewire.create-post');
    }
}
```

```blade
<form wire:submit="save"> <!-- [tl! highlight] -->
    <input type="text" wire:model="title">
    <textarea wire:model="content"></textarea>
    <button type="submit">Save</button>
</form>
```

In the above example, when a user submits the form by clicking "Save", `wire:submit` intercepts the `submit` event and calls the `save()` action on the server.

> [!info] Livewire automatically calls `preventDefault()`
> `wire:submit` is different than other Livewire event handlers in that it internally calls `event.preventDefault()` without the need for the `.prevent` modifier. This is because there are very few instances you would be listening for the `submit` event and NOT want to prevent it's default browser handling (performing a full form submission to an endpoint).
> [!info] Livewire automatically disables forms while submitting
> By default, when Livewire is sending a form submission to the server, it will disable form submit buttons and mark all form inputs as `readonly`. This way a user cannot submit the same form again until the initial submission is complete.
## Going deeper

`wire:submit` is just one of many event listeners that Livewire provides. The following two pages provide much more complete documentation on using `wire:submit` in your application:

* [Responding to browser events with Livewire](/docs/actions)
* [Creating forms in Livewire](/docs/forms)
 168 changes: 168 additions & 0 deletions168  
docs/wire-transition.md
@@ -0,0 +1,168 @@

## Basic usage

Showing or hiding content in Livewire is as simple as using one of Blade's conditional directives like `@if`. To enhance this experience for your users, Livewire provides a `wire:transition` directive that allows you to transition conditional elements smoothly in and out of the page.

For example, below is a `ShowPost` component with the ability to toggle viewing comments on and off:

```php
use App\Models\Post;

class ShowPost extends Component
{
    public Post $post;

    public $showComments = false;
}
```

```blade
<div>
    <!-- ... -->
    <button wire:click="$set('showComments', true)">Show comments</button>
    @if ($showComments)
        <div wire:transition> <!-- [tl! highlight] -->
            @foreach ($post->comments as $comment)
                <!-- ... -->
            @endforeach
        </div>
    @endif
</div>
```
Because `wire:transition` has been added to the `<div>` containing the post's comments, when the "Show comments" button is pressed, `$showComments` will be set to `true` and the comments will "fade" onto the page instead of abruptly appearing.

## Default transition style

By default, Livewire applies both an opacity and a scale CSS transition to elements with `wire:transtion`. Here's a visual preview:

<div x-data="{ show: false }" x-cloak class="border border-gray-700 rounded-xl p-6 w-full flex justify-between">
    <a href="#" x-on:click.prevent="show = ! show" class="py-2.5 outline-none">
        Preview transition <span x-text="show ? 'out' : 'in →'">in</span>
    </a>
    <div class="hey">
        <div
            x-show="show"
            x-transition
            class="inline-flex px-16 py-2.5 rounded-[10px] bg-pink-400 text-white uppercase font-medium transition focus-visible:outline-none focus-visible:!ring-1 focus-visible:!ring-white"
            style="
                background: linear-gradient(109.48deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.1) 100%), #EE5D99;
                box-shadow: inset 0px -1px 0px rgba(0, 0, 0, 0.5), inset 0px 1px 0px rgba(255, 255, 255, 0.1);
            "
        >
            &nbsp;
        </div>
    </div>
</div>

The above transition uses the following values for transitioning by default:

Transition in | Transition out
--- | ---
`duration: 150ms` | `duration: 75ms`
`opacity: [0 - 100]` | `opacity: [100 - 0]`
`transform: scale([0.95 - 1])` | `transform: scale([1 - 0.95])`

## Customizing transitions

To customize the CSS Livewire internally uses when transitioning, you can use any combination of the available modifiers:

Modifier | Description
--- | ---
`.in` | Only transition the element "in"
`.out` | Only transition the element "out"
`.duration.[?]ms` | Customize the transtion duration in milliseconds
`.duration.[?]s` | Customize the transtion duration in seconds
`.delay.[?]ms` | Customize the transtion delay in milliseconds
`.delay.[?]s` | Customize the transtion delay in seconds
`.opacity` | Only apply the opacity transition
`.scale` | Only apply the scale transition
`.origin.[top\|bottom\|left\|right]` | Customize the scale "origin" used

Below is a list of various transition combinations that may help to better visualize these customizations:

**Fade-only transition**

By default Livewire both fades and scales the element when transitioning. You can disable scaling and only fade by adding the `.opacity` modifier. This is useful for things like transitioning a full-page overlay, where adding a scale doesn't make sense.

```html
<div wire:transition.opacity>
```

<div x-data="{ show: false }" x-cloak class="border border-gray-700 rounded-xl p-6 w-full flex justify-between">
    <a href="#" x-on:click.prevent="show = ! show" class="py-2.5 outline-none">
        Preview transition <span x-text="show ? 'out' : 'in →'">in</span>
    </a>
    <div class="hey">
        <div
            x-show="show"
            x-transition.opacity
            class="inline-flex px-16 py-2.5 rounded-[10px] bg-pink-400 text-white uppercase font-medium transition focus-visible:outline-none focus-visible:!ring-1 focus-visible:!ring-white"
            style="
                background: linear-gradient(109.48deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.1) 100%), #EE5D99;
                box-shadow: inset 0px -1px 0px rgba(0, 0, 0, 0.5), inset 0px 1px 0px rgba(255, 255, 255, 0.1);
            "
        >
            ...
        </div>
    </div>
</div>

**Fade-out transition**

A common transition technique is to show an element immediately when transitioning in, and fade its opacity when transitioning out. You'll notice this effect on most native MacOS dropdowns and menus. Therefore it's commonly applied on the web to dropdowns, popovers, and menus.

```html
<div wire:transition.out.opacity.duration.200ms>
```

<div x-data="{ show: false }" x-cloak class="border border-gray-700 rounded-xl p-6 w-full flex justify-between">
    <a href="#" x-on:click.prevent="show = ! show" class="py-2.5 outline-none">
        Preview transition <span x-text="show ? 'out' : 'in →'">in</span>
    </a>
    <div class="hey">
        <div
            x-show="show"
            x-transition.out.opacity.duration.200ms
            class="inline-flex px-16 py-2.5 rounded-[10px] bg-pink-400 text-white uppercase font-medium transition focus-visible:outline-none focus-visible:!ring-1 focus-visible:!ring-white"
            style="
                background: linear-gradient(109.48deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.1) 100%), #EE5D99;
                box-shadow: inset 0px -1px 0px rgba(0, 0, 0, 0.5), inset 0px 1px 0px rgba(255, 255, 255, 0.1);
            "
        >
            ...
        </div>
    </div>
</div>

**Origin-top transition**

When using Livewire to transition an element such as a dropdown menu, it makes sense to scale in from the top of the menu as the origin, rather than center (Livewire's default). This way the menu feels visually anchored to the element that triggered it.

```html
<div wire:transition.scale.origin.top>
```

<div x-data="{ show: false }" x-cloak class="border border-gray-700 rounded-xl p-6 w-full flex justify-between">
    <a href="#" x-on:click.prevent="show = ! show" class="py-2.5 outline-none">
        Preview transition <span x-text="show ? 'out' : 'in →'">in</span>
    </a>
    <div class="hey">
        <div
            x-show="show"
            x-transition.origin.top
            class="inline-flex px-16 py-2.5 rounded-[10px] bg-pink-400 text-white uppercase font-medium transition focus-visible:outline-none focus-visible:!ring-1 focus-visible:!ring-white"
            style="
                background: linear-gradient(109.48deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.1) 100%), #EE5D99;
                box-shadow: inset 0px -1px 0px rgba(0, 0, 0, 0.5), inset 0px 1px 0px rgba(255, 255, 255, 0.1);
            "
        >
            ...
        </div>
    </div>
</div>

> [!tip] Livewire uses Alpine transitions behind the scenes
> When using `wire:transition` on an element, Livewire is internally applying Alpine's `x-transition` directive. Therefore you can use most if not all syntaxes you would normally use with `x-transition`. Check out [Alpine's transition documentation](https://alpinejs.dev/directives/transition) for all its capabilities.
  3 changes: 2 additions & 1 deletion3  
js/directives/wire-loading.js
@@ -19,14 +19,15 @@ directive('loading', ({ el, directive, component }) => {
})

function applyDelay(directive) {
    if (! directive.modifiers.includes('delay')) return [i => i(), i => i()]
    if (! directive.modifiers.includes('delay') || directive.modifiers.includes('none')) return [i => i(), i => i()]

    let duration = 200

    let delayModifiers = {
        'shortest': 50,
        'shorter': 100,
        'short': 150,
        'default': 200,
        'long': 300,
        'longer': 500,
        'longest': 1000,
  2 changes: 1 addition & 1 deletion2  
js/directives/wire-model.js
@@ -36,7 +36,7 @@ directive('model', ({ el, directive, component, cleanup }) => {
    }

    let isLive = modifiers.includes('live')
    let isLazy = modifiers.includes('lazy')
    let isLazy = modifiers.includes('lazy') || modifiers.includes('change')
    let onBlur = modifiers.includes('blur')
    let isDebounced = modifiers.includes('debounce')

  2 changes: 0 additions & 2 deletions2  
js/directives/wire-stream.js
@@ -2,8 +2,6 @@ import { contentIsFromDump } from '@/utils'
import { directive } from '@/directives'
import { on, trigger } from '@/events'

// Warning: this API is still in development and is subject to change...

directive('stream', ({el, directive, component, cleanup }) => {
    let { expression, modifiers } = directive

  2 changes: 1 addition & 1 deletion2  
js/directives/wire-transition.js
@@ -7,7 +7,7 @@ on('morph.added', ({ el }) => {
})

directive('transition', ({ el, directive, component, cleanup }) => {
    let visibility = Alpine.reactive({ state: false })
    let visibility = Alpine.reactive({ state: el.__addedByMorph ? false : true })

    // We're going to control the element's transition with Alpine transitions...
    Alpine.bind(el, {
  4 changes: 1 addition & 3 deletions4  
js/features/supportEntangle.js
@@ -5,7 +5,7 @@ import Alpine from 'alpinejs'
export function generateEntangleFunction(component, cleanup) {
    if (! cleanup) cleanup = () => {}

    return (name, live) => {
    return (name, live = false) => {
        let isLive = live
        let livewireProperty = name
        let livewireComponent = component.$wire
@@ -19,7 +19,6 @@ export function generateEntangleFunction(component, cleanup) {
                return
            }

            queueMicrotask(() => {
                let release = Alpine.entangle({
                    // Outer scope...
                    get() {
@@ -39,7 +38,6 @@ export function generateEntangleFunction(component, cleanup) {
                })

                cleanup(() => release())
            })

            return livewireComponent.get(name)
        }, obj => {
  8 changes: 1 addition & 7 deletions8  
js/features/supportNavigate.js
@@ -1,14 +1,8 @@
import { on, trigger } from "@/events"

let isNavigating = false

shouldHideProgressBar() && Alpine.navigate.disableProgressBar()

document.addEventListener('alpine:navigated', e => {
    if (e.detail && e.detail.init) return

    isNavigating = true

    // Forward a "livewire" version of the Alpine event...
    document.dispatchEvent(new CustomEvent('livewire:navigated', { bubbles: true }))
})
@@ -21,7 +15,7 @@ document.addEventListener('alpine:navigating', e => {
export function shouldRedirectUsingNavigateOr(effects, url, or) {
    let forceNavigate = effects.redirectUsingNavigate

    if (forceNavigate || isNavigating) {
    if (forceNavigate) {
        Alpine.navigate(url)
    } else {
        or()
  8 changes: 4 additions & 4 deletions8  
js/plugins/navigate/bar.js
@@ -78,13 +78,13 @@ function destroyBar() {
function injectStyles() {
    let style = document.createElement('style')
    style.innerHTML = `/* Make clicks pass-through */
    #nprogress {
      pointer-events: none;
    }
    #nprogress .bar {
    //   background: #FC70A9;
      background: #29d;
      background: var(--livewire-progress-bar-color, #29d);
      position: fixed;
      z-index: 1031;
@@ -125,8 +125,8 @@ function injectStyles() {
      box-sizing: border-box;
      border: solid 2px transparent;
      border-top-color: #29d;
      border-left-color: #29d;
      border-top-color: var(--livewire-progress-bar-color, #29d);
      border-left-color: var(--livewire-progress-bar-color, #29d);
      border-radius: 50%;
      -webkit-animation: nprogress-spinner 400ms linear infinite;
  6 changes: 3 additions & 3 deletions6  
js/plugins/navigate/index.js
@@ -126,7 +126,7 @@ export default function (Alpine) {
    // Because DOMContentLoaded is fired on first load,
    // we should fire alpine:navigated as a replacement as well...
    setTimeout(() => {
        fireEventForOtherLibariesToHookInto('alpine:navigated', true)
        fireEventForOtherLibariesToHookInto('alpine:navigated')
    })
}

@@ -148,8 +148,8 @@ function preventAlpineFromPickingUpDomChanges(Alpine, callback) {
    })
}

function fireEventForOtherLibariesToHookInto(eventName, init = false) {
    document.dispatchEvent(new CustomEvent(eventName, { bubbles: true, detail: { init } }))
function fireEventForOtherLibariesToHookInto(eventName) {
    document.dispatchEvent(new CustomEvent(eventName, { bubbles: true }))
}

function nowInitializeAlpineOnTheNewPage(Alpine) {
  5 changes: 5 additions & 0 deletions5  
js/utils.js
@@ -168,6 +168,11 @@ export function isSynthetic(subject) {
export function getCsrfToken() {
    // Purposely not caching. Fetching it fresh every time ensures we're
    // not depending on a stale session's CSRF token...

    if (document.querySelector('meta[name="csrf-token"]')) {
        return document.querySelector('meta[name="csrf-token"]').getAttribute('content')
    }

    if (document.querySelector('[data-csrf]')) {
        return document.querySelector('[data-csrf]').getAttribute('data-csrf')
    }
  2 changes: 2 additions & 0 deletions2  
legacy_tests/Browser/Loading/ComponentWithLoadingDelays.php
@@ -20,9 +20,11 @@ public function render()
<div>
    <button wire:click="$refresh" dusk="load">Load</button>
    <h1 wire:loading.delay.none dusk="delay-none">Loading delay none</h1>    
    <h1 wire:loading.delay.shortest dusk="delay-shortest">Loading delay shortest</h1>
    <h1 wire:loading.delay.shorter dusk="delay-shorter">Loading delay shorter</h1>
    <h1 wire:loading.delay.short dusk="delay-short">Loading delay short</h1>
    <h1 wire:loading.delay.default dusk="delay-default">Loading delay default</h1>    
    <h1 wire:loading.delay dusk="delay">Loading delay</h1>
    <h1 wire:loading.delay.long dusk="delay-long">Loading delay long</h1>
    <h1 wire:loading.delay.longer dusk="delay-longer">Loading delay longer</h1>
  9 changes: 8 additions & 1 deletion9  
legacy_tests/Browser/Loading/Test.php
@@ -158,8 +158,13 @@ public function test_different_delay_durations()
    {
        $this->browse(function ($browser) {
            $this->visitLivewireComponent($browser, ComponentWithLoadingDelays::class)
                ->assertNotVisible('@delay-none')
                ->assertNotVisible('@delay-shortest')
                ->waitForLivewire(function (Browser $browser) {
                    $browser->click('@load')
                        ->assertNotVisible('@delay-shortest')
                        ->assertVisible('@delay-none');
                })->waitForLivewire(function (Browser $browser) {
                    $browser->click('@load')
                            ->pause(51)
                            ->assertNotVisible('@delay-shorter')
@@ -173,12 +178,14 @@ public function test_different_delay_durations()
                    $browser->click('@load')
                            ->pause(151)
                            ->assertNotVisible('@delay')
                            ->assertNotVisible('@delay-default')
                            ->assertVisible('@delay-short');
                })->waitForLivewire(function (Browser $browser) {
                    $browser->click('@load')
                            ->pause(201)
                            ->assertNotVisible('@delay-long')
                            ->assertVisible('@delay');
                            ->assertVisible('@delay')
                            ->assertVisible('@delay-default');
                })->waitForLivewire(function (Browser $browser) {
                    $browser->click('@load')
                            ->pause(301)
 84 changes: 42 additions & 42 deletions84  
package-lock.json
  7 changes: 6 additions & 1 deletion7  
src/Features/SupportAttributes/SupportAttributes.php
@@ -51,7 +51,12 @@ function update($propertyName, $fullPath, $newValue)
            ->whereInstanceOf(LivewireAttribute::class)
            ->filter(fn ($attr) => $attr->getLevel() === AttributeLevel::PROPERTY)
            // Call "update" on the root property attribute even if it's a deep update...
            ->filter(fn ($attr) => str($fullPath)->startsWith($attr->getName()))
            ->filter(function ($attr) use ($fullPath) {
                $attributeRoot = (string) str($attr->getName())->before('.');
                $updatePathRoot = (string) str($fullPath)->before('.');

                return $attributeRoot === $updatePathRoot;
            })
            ->map(function ($attribute) use ($fullPath, $newValue) {
                if (method_exists($attribute, 'update')) {
                    return $attribute->update($fullPath, $newValue);
  3 changes: 3 additions & 0 deletions3  
src/Features/SupportComputed/BaseComputed.php
@@ -18,6 +18,7 @@ function __construct(
        public $persist = false,
        public $seconds = 3600, // 1 hour...
        public $cache = false,
        public $key = null,
    ) {}

    function boot()
@@ -118,6 +119,8 @@ protected function generatePersistedKey()

    protected function generateCachedKey()
    {
        if ($this->key) return $this->key;

        return 'lw_computed.'.$this->component->getName().'.'.$this->getName();
    }

  221 changes: 221 additions & 0 deletions221  
src/Features/SupportEntangle/BrowserTest.php
@@ -10,6 +10,120 @@

class BrowserTest extends BrowserTestCase
{
    /** @test */
    public function can_persist_entangled_data()
    {
        Livewire::visit(new class extends Component {
            public $input;

            public function render()
            {
                return <<<'BLADE'
                    <div>
                        <div x-data="{ value: $persist(@entangle('input')) }">
                            <input dusk="input" x-model="value" />
                        </div>
                    </div>
                BLADE;
            }
        })
            ->type('@input', 'Hello World')
            ->assertScript('localStorage.getItem("_x_value") == \'"Hello World"\'')
            ->tap(fn ($b) => $b->refresh())
            ->assertScript("localStorage.getItem('_x_value')", '"Hello World"')
            ;
    }

    /** @test */
    public function is_not_live_by_default()
    {
        Livewire::visit(new class extends Component {
            public $foo = 'foo';

            function render()
            {
                return <<<'HTML'
                <div>
                    <div x-data="{ state: $wire.$entangle('foo') }">
                        <button dusk="set" x-on:click="state = 'bar'" type="button">
                            Set to bar
                        </button>
                    </div>
                    <div dusk="state">{{ $foo }}</div>
                    <button dusk="refresh" x-on:click="$wire.$refresh()" type="button">
                        Refresh
                    </button>
                </div>
                HTML;
            }
        })
            ->assertSeeIn('@state', 'foo')
            ->waitForNoLivewire()->click('@set')
            ->assertSeeIn('@state', 'foo')
            ->waitForLivewire()->click('@refresh')
            ->assertSeeIn('@state', 'bar');
    }

    /** @test */
    public function can_be_forced_to_not_be_live()
    {
        Livewire::visit(new class extends Component {
            public $foo = 'foo';

            function render()
            {
                return <<<'HTML'
                <div>
                    <div x-data="{ state: $wire.$entangle('foo', false) }">
                        <button dusk="set" x-on:click="state = 'bar'" type="button">
                            Set to bar
                        </button>
                    </div>
                    <div dusk="state">{{ $foo }}</div>
                    <button dusk="refresh" x-on:click="$wire.$refresh()" type="button">
                        Refresh
                    </button>
                </div>
                HTML;
            }
        })
            ->assertSeeIn('@state', 'foo')
            ->waitForNoLivewire()->click('@set')
            ->assertSeeIn('@state', 'foo')
            ->waitForLivewire()->click('@refresh')
            ->assertSeeIn('@state', 'bar');
    }

    /** @test */
    public function can_be_live()
    {
        Livewire::visit(new class extends Component {
            public $foo = 'foo';

            function render()
            {
                return <<<'HTML'
                <div>
                    <div x-data="{ state: $wire.$entangle('foo', true) }">
                        <button dusk="set" x-on:click="state = 'bar'" type="button">
                            Set to bar
                        </button>
                    </div>
                    <div dusk="state">{{ $foo }}</div>
                </div>
                HTML;
            }
        })
            ->assertSeeIn('@state', 'foo')
            ->waitForLivewire()->click('@set')
            ->assertSeeIn('@state', 'bar');
    }

    /** @test */
    public function can_remove_entangled_components_from_dom_without_side_effects()
    {
@@ -165,4 +279,111 @@ function render()
            ->assertMissing('@item2')
            ->assertMissing('@item3');
    }

    /** @test */
    public function can_removed_nested_items_without_multiple_requests_when_entangled_items_are_present()
    {
        Livewire::visit(new class extends Component {
            public $components = [];

            public $filters = [
                'name' => 'bob',
                'phone' => '123',
                'address' => 'street',
            ];

            public $counter = 0;

            public function boot()
            {
                $this->counter++;
            }

            public function removeFilter($filter)
            {
                $this->filters[$filter] = null;
            }

            public function addBackFiltersWithEntangled()
            {
                $this->filters = [
                    'name' => 'bob',
                    'phone' => '123',
                    'address' => 'street',
                    'entangled' => 'hello world', // This filter will be entangled,
                ];
            }

            public function addBackFiltersWithoutEntangled()
            {
                // Add back the same non entangled filters to show that
                // removing/adding non entangled items is not the issue.
                $this->filters = [
                    'name' => 'bob',
                    'phone' => '123',
                    'address' => 'street',
                ];
            }

            function render()
            {
                return <<<'HTML'
                <div>
                    <div>Page</div>
                    <div dusk="counter">Boot counter: {{ $counter }}</div>
                    @foreach ($filters as $filter => $value)
                        @if ($filter === 'entangled')
                            <div x-data="{ value: $wire.entangle('filters.{{ $filter }}') }">
                                <span
                                    x-text="'Entangled: ' + value"
                                ></span>
                            </div>
                            <div>
                                <button dusk="remove-{{ $filter }}" wire:click="removeFilter('{{ $filter }}')">Remove {{ $filter }}</button>
                            </div>
                        @else
                            <div>
                                Normal: {{ $value }}
                            </div>
                            <div>
                                <button dusk="remove-{{ $filter }}" wire:click="removeFilter('{{ $filter }}')">Remove {{ $filter }}</button>
                            </div>
                        @endif
                    @endforeach
                    <button dusk="add-entangled-filter" wire:click="addEntangledFilter()">Add entangled filter</button>
                    <button dusk="add-back-filters-without-entangled" wire:click="addBackFiltersWithoutEntangled()">Add back filters without entangled</button>
                    <button dusk="add-back-filters-with-entangled" wire:click="addBackFiltersWithEntangled()">Add back filters with entangled</button>
                </div>
                HTML;
            }
        })
        ->assertSeeIn('@counter', '1')
        ->waitForLivewire()->click('@remove-name')
        ->assertSeeIn('@counter', '2')
        ->waitForLivewire()->click('@remove-phone')
        ->assertSeeIn('@counter', '3')
        ->waitForLivewire()->click('@remove-address')
        ->assertSeeIn('@counter', '4')
        ->waitForLivewire()->click('@add-back-filters-without-entangled')
        ->assertSeeIn('@counter', '5')
        ->waitForLivewire()->click('@remove-name')
        ->assertSeeIn('@counter', '6')
        ->waitForLivewire()->click('@remove-phone')
        ->assertSeeIn('@counter', '7')
        ->waitForLivewire()->click('@remove-address')
        ->assertSeeIn('@counter', '8')
        ->waitForLivewire()->click('@add-back-filters-with-entangled')
        ->assertSeeIn('@counter', '9')
        ->waitForLivewire()->click('@remove-name')
        ->assertSeeIn('@counter', '10') // This test will fail here since there will be duplicate requests
        ->waitForLivewire()->click('@remove-phone')
        ->assertSeeIn('@counter', '11')
        ->waitForLivewire()->click('@remove-address')
        ->assertSeeIn('@counter', '12')
        ->waitForLivewire()->click('@remove-entangled')
        ->assertSeeIn('@counter', '13');
    }
}
  16 changes: 12 additions & 4 deletions16  
src/Features/SupportFileUploads/FileUploadController.php
@@ -8,10 +8,18 @@ class FileUploadController
{
    public function getMiddleware()
    {
        return [[
            'middleware' => FileUploadConfiguration::middleware(),
            'options' => [],
        ]];
        /**
         * Laravel requires the returned array to contain an array for each
         * middleware with `middleware` and `options` keys. So we'll map
         * through the file upload config middleware and format them.
         */
        return array_map(
            fn($middleware) => [
                'middleware' => $middleware,
                'options' => [],
            ],
            (array) FileUploadConfiguration::middleware()
        );
    }

    public function handle()
  2 changes: 1 addition & 1 deletion2  
src/Features/SupportFileUploads/TemporaryUploadedFile.php
@@ -87,7 +87,7 @@ public function temporaryUrl()
            return $this->storage->temporaryUrl(
                $this->path,
                now()->addDay()->endOfHour(),
                ['ResponseContentDisposition' => 'filename="' . $this->getClientOriginalName() . '"']
                ['ResponseContentDisposition' => 'attachment; filename="' . $this->getClientOriginalName() . '"']
            );
        }

  19 changes: 17 additions & 2 deletions19  
src/Features/SupportFileUploads/UnitTest.php
@@ -419,7 +419,22 @@ public function the_global_upload_route_middleware_is_configurable()
        try {
            $this->withoutExceptionHandling()->post($url);
        } catch (\Throwable $th) {
            $this->assertEquals('Middleware was hit!', $th->getMessage());
            $this->assertStringContainsString(DummyMiddleware::class, $th->getMessage());
        }
    }

    /** @test */
    public function the_global_upload_route_middleware_supports_multiple_middleware()
    {
        config()->set('livewire.temporary_file_upload.middleware', ['throttle:60,1', DummyMiddleware::class]);

        $url = GenerateSignedUploadUrl::forLocal();

        try {
            $this->withoutExceptionHandling()->post($url);
        } catch (\Throwable $th) {
            $this->assertStringContainsString('throttle:60,1', $th->getMessage());
            $this->assertStringContainsString(DummyMiddleware::class, $th->getMessage());
        }
    }

@@ -688,7 +703,7 @@ class DummyMiddleware
{
    public function handle($request, $next)
    {
        throw new \Exception('Middleware was hit!');
        throw new \Exception(implode(',', $request->route()->computedMiddleware));
    }
}

  17 changes: 17 additions & 0 deletions17  
src/Features/SupportLockedProperties/UnitTest.php
@@ -48,4 +48,21 @@ public function render() {
        ->assertSet('foo.count', 1)
        ->set('foo.count', 2);
    }

    /** @test */
    function can_update_locked_property_with_similar_name()
    {
        Livewire::test(new class extends Component {
            #[BaseLocked]
            public $count = 1;

            public $count2 = 1;

            public function render() {
                return '<div></div>';
            }
        })
        ->assertSet('count2', 1)
        ->set('count2', 2);
    }
}
 89 changes: 89 additions & 0 deletions89  
src/Features/SupportModels/BrowserTest.php
@@ -0,0 +1,89 @@
<?php

namespace Livewire\Features\SupportModels;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Component;
use Livewire\Features\SupportEvents\BaseOn;
use Livewire\Livewire;
use Sushi\Sushi;

class BrowserTest extends \Tests\BrowserTestCase
{
    use RefreshDatabase;

    /** @test */
    public function parent_component_with_eloquent_collection_property_does_not_error_when_child_deletes_a_model_contained_within_it()
    {
        Livewire::visit([
            new class extends Component {
                public $posts;

                public function mount()
                {
                    $this->posts = Post::all();
                }

                #[BaseOn('postDeleted')]
                public function setPosts() {
                    $this->posts = Post::all();
                }

                public function render()
                {
                    return <<<'HTML'
                    <div>
                        @foreach($posts as $post)
                            <div wire:key="parent-post-{{ $post->id }}">
                                <livewire:child wire:key="{{ $post->id }}" :post="$post" />
                            </div>
                        @endforeach
                    </div>
                    HTML;
                }
            },
            'child' => new class extends Component {
                public $post;

                public function delete($id)
                {
                    Post::find($id)->delete();
                    $this->dispatch('postDeleted');
                }

                public function render()
                {
                    return <<<'HTML'
                    <div dusk="post-{{ $post->id }}">
                        {{ $post->title }}
                        <button dusk="delete-{{ $post->id }}" wire:click="delete({{ $post->id }})">Delete</button>
                    </div>
                    HTML;
                }
            },
        ])
            ->waitForLivewireToLoad()
            ->assertPresent('@post-1')
            ->assertSeeIn('@post-1', 'Post #1')
            ->waitForLivewire()->click('@delete-1')
            ->assertNotPresent('@parent-post-1')
            ;
    }
}

class Post extends Model
{
    use Sushi;

    protected $guarded = [];

    public function getRows() {
        return [
            ['id' => 1, 'title' => 'Post #1'],
            ['id' => 2, 'title' => 'Post #2'],
            ['id' => 3, 'title' => 'Post #3'],
        ];
    }
}
 86 changes: 86 additions & 0 deletions86  
src/Features/SupportModels/EloquentCollectionSynth.php
@@ -0,0 +1,86 @@
<?php

namespace Livewire\Features\SupportModels;

use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Queue\SerializesAndRestoresModelIdentifiers;
use Livewire\Mechanisms\HandleComponents\Synthesizers\Synth;

class EloquentCollectionSynth extends Synth {
    use SerializesAndRestoresModelIdentifiers;

    public static $key = 'elcln';

    static function match($target)
    {
        return $target instanceof EloquentCollection;
    }

    function dehydrate(EloquentCollection $target, $dehydrateChild)
    {
        $class = $target::class;
        $modelClass = $target->getQueueableClass();

        /**
         * `getQueueableClass` above checks all models are the same and
         * then returns the class. We then instantiate a model object
         * so we can call `getMorphClass()` on it.
         *
         * If no alias is found, this just returns the class name
         */
        $modelAlias = (new $modelClass)->getMorphClass();

        $meta = [];

        $serializedCollection = (array) $this->getSerializedPropertyValue($target);

        $meta['keys'] = $serializedCollection['id'];
        $meta['class'] = $class;
        $meta['modelClass'] = $modelAlias;

        return [
            null,
            $meta
        ];
    }

    function hydrate($data, $meta, $hydrateChild)
    {
        $class = $meta['class'];

        $modelClass = $meta['modelClass'];

        // If no alias found, this returns `null`
        $modelAlias = Relation::getMorphedModel($modelClass);

        if (! is_null($modelAlias)) {
            $modelClass = $modelAlias;
        }

        $keys = $meta['keys'] ?? [];

        if (count($keys) === 0) {
            return new $class();
        }

        // We are using Laravel's method here for restoring the collection, which ensures
        // that all models in the collection are restored in one query, preventing n+1
        // issues and also only restores models that exist.
        $collection = (new $modelClass)->newQueryForRestoration($keys)->useWritePdo()->get();

        return $collection;
    }

    function get(&$target, $key) {
        throw new \Exception('Can\'t access model properties directly');
    }

    function set(&$target, $key, $value, $pathThusFar, $fullPath) {
        throw new \Exception('Can\'t set model properties directly');
    }

    function call($target, $method, $params, $addEffect) {
        throw new \Exception('Can\'t call model methods directly');
    }
}
  25 changes: 18 additions & 7 deletions25  
src/Features/SupportModels/ModelSynth.php
@@ -17,23 +17,27 @@ static function match($target) {
    }

    function dehydrate($target) {
        if (! $target->exists) {
            throw new \Exception('Can\'t set model as property if it hasn\'t been persisted yet');
        }

        // If no alias is found, this just returns the class name
        $alias = $target->getMorphClass();

        $serializedModel = (array) $this->getSerializedPropertyValue($target);
        $serializedModel = $target->exists
            ? (array) $this->getSerializedPropertyValue($target)
            : null;

        $meta = ['class' => $alias];

        // If the model doesn't exist as it's an empty model or has been
        // recently deleted, then we don't want to include any key.
        if ($serializedModel) $meta['key'] = $serializedModel['id'];


        return [
            null,
            ['class' => $alias, 'key' => $serializedModel['id']],
            $meta,
        ];
    }

    function hydrate($data, $meta) {
        $key = $meta['key'];
        $class = $meta['class'];

        // If no alias found, this returns `null`
@@ -43,6 +47,13 @@ function hydrate($data, $meta) {
            $class = $aliasClass;
        }

        // If no key is provided then an empty model is returned
        if (! array_key_exists('key', $meta)) {
            return new $class;
        }

        $key = $meta['key'];

        $model = (new $class)->newQueryForRestoration($key)->useWritePdo()->firstOrFail();

        return $model;
  1 change: 1 addition & 0 deletions1  
src/Features/SupportModels/SupportModels.php
@@ -10,6 +10,7 @@ static function provide()
    {
        app('livewire')->propertySynthesizer([
            ModelSynth::class,
            EloquentCollectionSynth::class,
        ]);
    }
}
  23 changes: 23 additions & 0 deletions23  
src/Features/SupportModels/UnitTest.php
@@ -70,6 +70,29 @@ public function render() { return <<<'HTML'
        $this->assertNull($data['post']);
    }

    /** @test */
    public function unpersisted_models_can_be_assigned_but_no_data_is_persisted_between_requests()
    {
        $component = Livewire::test(new class extends \Livewire\Component {
            public Post $post;

            public function mount() {
                $this->post = new Post();
            }

            public function render() { return <<<'HTML'
                <div>{{ $post->title }}</div>
            HTML; }
        })
        ->call('$refresh')
        ->assertSet('post', new Post())
        ;

        $data = $component->getData();

        $this->assertNull($data['post']);
    }

    /** @test */
    public function model_properties_are_lazy_loaded()
    {
  4 changes: 2 additions & 2 deletions4  
src/Features/SupportMorphAwareIfStatement/SupportMorphAwareIfStatement.php
@@ -71,13 +71,13 @@ static function registerPrecompilers()
            $entire = preg_replace_callback($generatePattern($openings), function ($matches) {
                $original = $matches[0];

                return '<!-- __BLOCK__ -->'.$original;
                return '<!--[if BLOCK]><![endif]-->'.$original;
            }, $entire) ?? $entire;

            $entire = preg_replace_callback($generatePattern($closings), function ($matches) {
                $original = $matches[0];

                return $original.' <!-- __ENDBLOCK__ -->';
                return $original.' <!--[if ENDBLOCK]><![endif]-->';
            }, $entire) ?? $entire;

            return $entire;
  12 changes: 6 additions & 6 deletions12  
src/Features/SupportMorphAwareIfStatement/UnitTest.php
@@ -22,8 +22,8 @@ public function render() {
            <livewire:foo />
        ');

        $this->assertCount(2, explode('__BLOCK__', $output));
        $this->assertCount(2, explode('__ENDBLOCK__', $output));
        $this->assertCount(2, explode('<!--[if BLOCK]><![endif]-->', $output));
        $this->assertCount(2, explode('<!--[if ENDBLOCK]><![endif]-->', $output));
    }

    /** @test */
@@ -41,8 +41,8 @@ public function handles_custom_blade_conditional_directives()
        </div>
        HTML);

        $this->assertOccurrences(1, '__BLOCK__', $output);
        $this->assertOccurrences(1, '__ENDBLOCK__', $output);
        $this->assertOccurrences(1, '<!--[if BLOCK]><![endif]-->', $output);
        $this->assertOccurrences(1, '<!--[if ENDBLOCK]><![endif]-->', $output);
    }

    /**
@@ -53,8 +53,8 @@ function foo($occurances, $template, $expectedCompiled = null)
    {
        $compiled = $this->compile($template);

        $this->assertOccurrences($occurances, '__BLOCK__', $compiled);
        $this->assertOccurrences($occurances, '__ENDBLOCK__', $compiled);
        $this->assertOccurrences($occurances, '<!--[if BLOCK]><![endif]-->', $compiled);
        $this->assertOccurrences($occurances, '<!--[if ENDBLOCK]><![endif]-->', $compiled);

        $expectedCompiled && $this->assertEquals($expectedCompiled, $compiled);
    }
  37 changes: 35 additions & 2 deletions37  
src/Features/SupportNavigate/BrowserTest.php
@@ -299,7 +299,7 @@ public function can_navigate_to_page_from_child_via_parent_component_without_rel
    }

    /** @test */
    public function can_redirect_without_reloading_from_a_page_that_was_loaded_by_wire_navigate()
    public function can_redirect_with_reloading_from_a_page_that_was_loaded_by_wire_navigate()
    {
        $this->browse(function ($browser) {
            $browser
@@ -313,7 +313,7 @@ public function can_redirect_without_reloading_from_a_page_that_was_loaded_by_wi
                ->assertScript('return window._lw_dusk_test')
                ->click('@redirect.to.first')
                ->waitFor('@link.to.second')
                ->assertScript('return window._lw_dusk_test')
                ->assertScript('return window._lw_dusk_test', false)
                ->assertSee('On first');
        });
    }
@@ -517,6 +517,33 @@ public function navigate_is_only_triggered_on_left_click()
        });
    }

    /** @test */
    public function livewire_navigated_event_is_fired_on_first_page_load()
    {
        $this->browse(function ($browser) {
            $browser
                ->visit('/second')
                ->assertSee('On second')
                ->assertScript('window.foo_navigated', 'bar');
        });
    }

    /** @test */
    public function livewire_navigated_event_is_fired_after_redirect_without_reloading()
    {
        $this->browse(function ($browser) {
            $browser
                ->visit('/first')
                ->tap(fn ($b) => $b->script('window._lw_dusk_test = true'))
                ->assertScript('return window._lw_dusk_test')
                ->assertSee('On first')
                ->click('@link.to.second')
                ->waitFor('@link.to.first')
                ->assertSee('On second')
                ->assertScript('window.foo_navigated', 'bar');
        });
    }

    /** @test */
    public function navigate_is_not_triggered_on_cmd_click()
    {
@@ -703,6 +730,12 @@ public function render()
            @endpersist
            <script data-navigate-once>window.foo = 'bar';</script>
            
            <script>
                document.addEventListener('livewire:navigated', () => { 
                    window.foo_navigated = 'bar'
                })
            </script>
        </div>
        HTML;
    }
  10 changes: 8 additions & 2 deletions10  
src/Features/SupportPageComponents/HandlesPageComponents.php
@@ -17,10 +17,16 @@ function __invoke()
            $html = app('livewire')->mount($this::class, $params);
        });

        $layoutConfig = $layoutConfig ?: new LayoutConfig;
        $layoutConfig = $layoutConfig ?: new PageComponentConfig;

        $layoutConfig->normalizeViewNameAndParamsForBladeComponents();

        return SupportPageComponents::renderContentsIntoLayout($html, $layoutConfig);
        $response = response(SupportPageComponents::renderContentsIntoLayout($html, $layoutConfig));

        if (is_callable($layoutConfig->response)) {
            call_user_func($layoutConfig->response, $response);
        }

        return $response;
    }
}
  3 changes: 2 additions & 1 deletion3  
...es/SupportPageComponents/LayoutConfig.php → ...ortPageComponents/PageComponentConfig.php
@@ -5,10 +5,11 @@
use Illuminate\View\AnonymousComponent;
use Livewire\Mechanisms\HandleComponents\ViewContext;

class LayoutConfig
class PageComponentConfig
{
    public $slots = [];
    public $viewContext = null;
    public $response;

    function __construct(
        public $type = 'component',
  22 changes: 15 additions & 7 deletions22  
src/Features/SupportPageComponents/SupportPageComponents.php
@@ -20,39 +20,39 @@ static function provide()
    static function registerLayoutViewMacros()
    {
        View::macro('layoutData', function ($data = []) {
            if (! isset($this->layoutConfig)) $this->layoutConfig = new LayoutConfig;
            if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;

            $this->layoutConfig->mergeParams($data);

            return $this;
        });

        View::macro('section', function ($section) {
            if (! isset($this->layoutConfig)) $this->layoutConfig = new LayoutConfig;
            if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;

            $this->layoutConfig->slotOrSection = $section;

            return $this;
        });

        View::macro('title', function ($title) {
            if (! isset($this->layoutConfig)) $this->layoutConfig = new LayoutConfig;
            if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;

            $this->layoutConfig->mergeParams(['title' => $title]);

            return $this;
        });

        View::macro('slot', function ($slot) {
            if (! isset($this->layoutConfig)) $this->layoutConfig = new LayoutConfig;
            if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;

            $this->layoutConfig->slotOrSection = $slot;

            return $this;
        });

        View::macro('extends', function ($view, $params = []) {
            if (! isset($this->layoutConfig)) $this->layoutConfig = new LayoutConfig;
            if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;

            $this->layoutConfig->type = 'extends';
            $this->layoutConfig->slotOrSection = 'content';
@@ -63,7 +63,7 @@ static function registerLayoutViewMacros()
        });

        View::macro('layout', function ($view, $params = []) {
            if (! isset($this->layoutConfig)) $this->layoutConfig = new LayoutConfig;
            if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;

            $this->layoutConfig->type = 'component';
            $this->layoutConfig->slotOrSection = 'slot';
@@ -72,6 +72,14 @@ static function registerLayoutViewMacros()

            return $this;
        });

        View::macro('response', function (callable $callback) {
            if (! isset($this->layoutConfig)) $this->layoutConfig = new PageComponentConfig;

            $this->layoutConfig->response = $callback;

            return $this;
        });
    }

    static function interceptTheRenderOfTheComponentAndRetreiveTheLayoutConfiguration($callback)
@@ -93,7 +101,7 @@ static function interceptTheRenderOfTheComponentAndRetreiveTheLayoutConfiguratio
                $view->title($titleAttr->content);
            }

            $layoutConfig = $view->layoutConfig ?? new LayoutConfig;
            $layoutConfig = $view->layoutConfig ?? new PageComponentConfig;

            return function ($html, $replace, $viewContext) use ($view, $layoutConfig) {
                // Gather up any slots and sections declared in the component template and store them
  20 changes: 19 additions & 1 deletion20  
src/Features/SupportPageComponents/UnitTest.php
@@ -5,6 +5,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
use Livewire\Livewire;
@@ -393,6 +394,16 @@ public function can_use_layout_slots_in_full_page_components()
            ->assertSee('I am a footer - foo');
    }

    /** @test */
    public function can_modify_response()
    {
        Route::get('/configurable-layout', ComponentWithCustomResponseHeaders::class);

        $this
            ->get('/configurable-layout')
            ->assertHeader('x-livewire', 'awesome');
    }

    /** @test */
    public function can_configure_title_in_render_method_and_layout_using_layout_attribute()
    {
@@ -535,7 +546,6 @@ public function render()
    }
}


class ComponentWithExtendsLayout extends Component
{
    public function render()
@@ -650,6 +660,14 @@ public function render()
    }
}

class ComponentWithCustomResponseHeaders extends Component
{
    public function render()
    {
        return view('null-view')->response(fn(Response $response) => $response->header('x-livewire', 'awesome'));
    }
}

class FrameworkModel extends Model
{
    public function resolveRouteBinding($value, $field = null)
  36 changes: 36 additions & 0 deletions36  
src/Features/SupportPagination/BrowserTest.php
@@ -1012,6 +1012,42 @@ public function render()
            ->assertSeeIn('@child', 'Child')
        ;
    }

    public function test_pagination_links_scroll_to_top_by_default()
    {
        Livewire::visit(new class extends Component {
            use WithPagination;

            public function render()
            {
                return Blade::render(
                    <<< 'HTML'
                    <div>
                        <div id="top">Top...</div>
                        @foreach ($posts as $post)
                            <h1 wire:key="post-{{ $post->id }}">{{ $post->title }}</h1>
                        @endforeach
                        <div style="min-height: 100vh">&nbsp;</div>
                        {{ $posts->links() }}
                        <div id="bottom">Bottom...</div>
                    </div>
                    HTML,
                    [
                        'posts' => Post::paginate(),
                    ]
                );
            }
        })
        ->scrollTo('#bottom')
        ->assertNotInViewPort('#top')
        ->waitForLivewire()->click('@nextPage.before')
        ->assertInViewPort('#top')
        ;
    }
}

class Post extends Model
  3 changes: 2 additions & 1 deletion3  
src/Features/SupportPagination/HandlesPagination.php
@@ -2,6 +2,7 @@

namespace Livewire\Features\SupportPagination;

use Illuminate\Pagination\Paginator;
use Illuminate\Support\Str;

trait HandlesPagination
@@ -17,7 +18,7 @@ public function queryStringHandlesPagination()

    public function getPage($pageName = 'page')
    {
        return $this->paginators[$pageName] ?? 1;
        return $this->paginators[$pageName] ?? Paginator::resolveCurrentPage($pageName);
    }

    public function previousPage($pageName = 'page')
  33 changes: 33 additions & 0 deletions33  
src/Features/SupportPagination/UnitTest.php
@@ -82,6 +82,39 @@ function render()
            }
        })->assertSee('Custom pagination theme');
    }

    public function test_calling_pagination_getPage_before_paginate_method_resolve_the_correct_page_number_in_first_visit_or_after_reload()
    {
        Livewire::withQueryParams(['page' => 5])->test(new class extends Component {
            use WithPagination;

            public int $page = 1;

            #[Computed]
            function posts()
            {
                $this->page = $this->getPage();
                return PaginatorPostTestModel::paginate();
            }

            function render()
            {
                return <<<'HTML'
                <div>
                    @foreach ($this->posts as $post)
                    @endforeach
                    {{ $this->posts->links() }}
                </div>
                HTML;
            }
        })
            ->assertSet('page', 5)
            ->assertSet('paginators.page', 5)
            ->call('gotoPage', 3)
            ->assertSet('page', 3)
            ->assertSet('paginators.page', 3);
    }
}

class ComponentWithPaginationStub extends Component
  18 changes: 15 additions & 3 deletions18  
src/Features/SupportPagination/views/bootstrap.blade.php
@@ -1,3 +1,15 @@
@php
if (! isset($scrollTo)) {
    $scrollTo = 'body';
}
$scrollIntoViewJsSnippet = ($scrollTo !== false)
    ? <<<JS
       (\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
    JS
    : '';
@endphp
<div>
    @if ($paginator->hasPages())
        <nav>
@@ -9,7 +21,7 @@
                    </li>
                @else
                    <li class="page-item">
                        <button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" wire:loading.attr="disabled" rel="prev" aria-label="@lang('pagination.previous')">&lsaquo;</button>
                        <button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="prev" aria-label="@lang('pagination.previous')">&lsaquo;</button>
                    </li>
                @endif
@@ -26,7 +38,7 @@
                            @if ($page == $paginator->currentPage())
                                <li class="page-item active" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}" aria-current="page"><span class="page-link">{{ $page }}</span></li>
                            @else
                                <li class="page-item" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}"><button type="button" class="page-link" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')">{{ $page }}</button></li>
                                <li class="page-item" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}"><button type="button" class="page-link" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}">{{ $page }}</button></li>
                            @endif
                        @endforeach
                    @endif
@@ -35,7 +47,7 @@
                {{-- Next Page Link --}}
                @if ($paginator->hasMorePages())
                    <li class="page-item">
                        <button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" wire:loading.attr="disabled" rel="next" aria-label="@lang('pagination.next')">&rsaquo;</button>
                        <button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="next" aria-label="@lang('pagination.next')">&rsaquo;</button>
                    </li>
                @else
                    <li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
  22 changes: 16 additions & 6 deletions22  
src/Features/SupportPagination/views/simple-bootstrap.blade.php
@@ -1,3 +1,15 @@
@php
if (! isset($scrollTo)) {
    $scrollTo = 'body';
}
$scrollIntoViewJsSnippet = ($scrollTo !== false)
    ? <<<JS
       (\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
    JS
    : '';
@endphp
<div>
    @if ($paginator->hasPages())
        <nav>
@@ -10,12 +22,11 @@
                @else
                    @if(method_exists($paginator,'getCursorName'))
                        <li class="page-item">
                            {{-- // @todo: Remove `wire:key` once mutation observer has been fixed to detect parameter change for the `setPage()` method call --}}
                            <button dusk="previousPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" wire:loading.attr="disabled" rel="prev">@lang('pagination.previous')</button>
                            <button dusk="previousPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="prev">@lang('pagination.previous')</button>
                        </li>
                    @else
                        <li class="page-item">
                            <button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" wire:loading.attr="disabled" rel="prev">@lang('pagination.previous')</button>
                            <button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="prev">@lang('pagination.previous')</button>
                        </li>
                    @endif
                @endif
@@ -24,12 +35,11 @@
                @if ($paginator->hasMorePages())
                    @if(method_exists($paginator,'getCursorName'))
                        <li class="page-item">
                            {{-- // @todo: Remove `wire:key` once mutation observer has been fixed to detect parameter change for the `setPage()` method call --}}
                            <button dusk="nextPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" wire:loading.attr="disabled" rel="next">@lang('pagination.next')</button>
                            <button dusk="nextPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="next">@lang('pagination.next')</button>
                        </li>
                    @else
                        <li class="page-item">
                            <button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" wire:loading.attr="disabled" rel="next">@lang('pagination.next')</button>
                            <button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="next">@lang('pagination.next')</button>
                        </li>
                    @endif
                @else
  23 changes: 17 additions & 6 deletions23  
src/Features/SupportPagination/views/simple-tailwind.blade.php
@@ -1,3 +1,15 @@
@php
if (! isset($scrollTo)) {
    $scrollTo = 'body';
}
$scrollIntoViewJsSnippet = ($scrollTo !== false)
    ? <<<JS
       (\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
    JS
    : '';
@endphp
<div>
    @if ($paginator->hasPages())
        <nav role="navigation" aria-label="Pagination Navigation" class="flex justify-between">
@@ -9,12 +21,12 @@
                    </span>
                @else
                    @if(method_exists($paginator,'getCursorName'))
                        {{-- // @todo: Remove `wire:key` once mutation observer has been fixed to detect parameter change for the `setPage()` method call --}}
                        <button type="button" dusk="previousPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                        <button type="button" dusk="previousPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                                {!! __('pagination.previous') !!}
                        </button>
                    @else
                        <button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                        <button
                            type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                                {!! __('pagination.previous') !!}
                        </button>
                    @endif
@@ -25,12 +37,11 @@
                {{-- Next Page Link --}}
                @if ($paginator->hasMorePages())
                    @if(method_exists($paginator,'getCursorName'))
                        {{-- // @todo: Remove `wire:key` once mutation observer has been fixed to detect parameter change for the `setPage()` method call --}}
                        <button type="button" dusk="nextPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                        <button type="button" dusk="nextPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                                {!! __('pagination.next') !!}
                        </button>
                    @else
                        <button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                        <button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                                {!! __('pagination.next') !!}
                        </button>
                    @endif
  22 changes: 17 additions & 5 deletions22  
src/Features/SupportPagination/views/tailwind.blade.php
@@ -1,3 +1,15 @@
@php
if (! isset($scrollTo)) {
    $scrollTo = 'body';
}
$scrollIntoViewJsSnippet = ($scrollTo !== false)
    ? <<<JS
       (\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
    JS
    : '';
@endphp
<div>
    @if ($paginator->hasPages())
        <nav role="navigation" aria-label="Pagination Navigation" class="flex items-center justify-between">
@@ -8,15 +20,15 @@
                            {!! __('pagination.previous') !!}
                        </span>
                    @else
                        <button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                        <button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                            {!! __('pagination.previous') !!}
                        </button>
                    @endif
                </span>
                <span>
                    @if ($paginator->hasMorePages())
                        <button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                        <button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
                            {!! __('pagination.next') !!}
                        </button>
                    @else
@@ -53,7 +65,7 @@
                                    </span>
                                </span>
                            @else
                                <button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after" rel="prev" class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150" aria-label="{{ __('pagination.previous') }}">
                                <button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after" rel="prev" class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150" aria-label="{{ __('pagination.previous') }}">
                                    <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
                                        <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
                                    </svg>
@@ -79,7 +91,7 @@
                                                <span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 select-none">{{ $page }}</span>
                                            </span>
                                        @else
                                            <button type="button" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150" aria-label="{{ __('Go to page :page', ['page' => $page]) }}">
                                            <button type="button" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150" aria-label="{{ __('Go to page :page', ['page' => $page]) }}">
                                                {{ $page }}
                                            </button>
                                        @endif
@@ -91,7 +103,7 @@
                        <span>
                            {{-- Next Page Link --}}
                            @if ($paginator->hasMorePages())
                                <button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after" rel="next" class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150" aria-label="{{ __('pagination.next') }}">
                                <button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after" rel="next" class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150" aria-label="{{ __('pagination.next') }}">
                                    <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
                                        <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
                                    </svg>
  5 changes: 2 additions & 3 deletions5  
src/Features/SupportStreaming/HandlesStreaming.php
@@ -6,10 +6,9 @@

trait HandlesStreaming
{
    function stream($name, $content, $replace = false)
    function stream($to, $content, $replace = false)
    {
        $hook = ComponentHookRegistry::getHook($this, SupportStreaming::class);

        $hook->stream($name, $content, $replace);
    }
        $hook->stream($to, $content, $replace); }
}
  46 changes: 46 additions & 0 deletions46  
src/Features/SupportTesting/DuskBrowserMacros.php
@@ -205,6 +205,52 @@ public function __call($method, $params)
        };
    }

    public function waitForNoLivewire()
    {
        return function ($callback = null) {
            /** @var \Laravel\Dusk\Browser $this */
            $id = str()->random();

            $this->script([
                "window.duskIsWaitingForLivewireRequest{$id} = true",
                "window.Livewire.hook('request', ({ respond }) => {
                    window.duskIsWaitingForLivewireRequest{$id} = true
                    respond(() => {
                        queueMicrotask(() => {
                            delete window.duskIsWaitingForLivewireRequest{$id}
                        })
                    })
                })",
            ]);

            if ($callback) {
                $callback($this);

                return $this->waitUsing(6, 25, function () use ($id) {
                    return $this->driver->executeScript("return window.duskIsWaitingForLivewireRequest{$id}");
                }, 'Livewire request was triggered');
            }

            // If no callback is passed, make ->waitForNoLivewire a higher-order method.
            return new class($this, $id) {
                protected $browser;
                protected $id;

                public function __construct($browser, $id) { $this->browser = $browser; $this->id = $id; }

                public function __call($method, $params)
                {
                    return tap($this->browser->{$method}(...$params), function ($browser) {
                        $browser->waitUsing(6, 25, function () use ($browser) {
                            return $browser->driver->executeScript("return window.duskIsWaitingForLivewireRequest{$this->id}");
                        }, 'Livewire request was triggered');
                    });
                }
            };
        };
    }

    public function waitForNavigate()
    {
        return function ($callback = null) {
  12 changes: 6 additions & 6 deletions12  
src/Features/SupportTesting/InitialRender.php
@@ -10,14 +10,14 @@ function __construct(
        protected RequestBroker $requestBroker,
    ) {}

    static function make($requestBroker, $name, $params = [], $fromQueryString = [])
    static function make($requestBroker, $name, $params = [], $fromQueryString = [], $cookies = [])
    {
        $instance = new static($requestBroker);

        return $instance->makeInitialRequest($name, $params, $fromQueryString);
        return $instance->makeInitialRequest($name, $params, $fromQueryString, $cookies);
    }

    function makeInitialRequest($name, $params, $fromQueryString = []) {
    function makeInitialRequest($name, $params, $fromQueryString = [], $cookies = []) {
        $uri = '/livewire-unit-test-endpoint/'.str()->random(20);

        $this->registerRouteBeforeExistingRoutes($uri, function () use ($name, $params) {
@@ -27,9 +27,9 @@ function makeInitialRequest($name, $params, $fromQueryString = []) {
            ]);
        });

        [$response, $componentInstance, $componentView] = $this->extractComponentAndBladeView(function () use ($uri, $fromQueryString) {
            return $this->requestBroker->temporarilyDisableExceptionHandlingAndMiddleware(function ($requestBroker) use ($uri, $fromQueryString) {
                return $requestBroker->call('GET', $uri, $fromQueryString);
        [$response, $componentInstance, $componentView] = $this->extractComponentAndBladeView(function () use ($uri, $fromQueryString, $cookies) {
            return $this->requestBroker->temporarilyDisableExceptionHandlingAndMiddleware(function ($requestBroker) use ($uri, $fromQueryString, $cookies) {
                return $requestBroker->call('GET', $uri, $fromQueryString, $cookies);
            });
        });

  3 changes: 2 additions & 1 deletion3  
src/Features/SupportTesting/Testable.php
@@ -25,7 +25,7 @@ protected function __construct(
        protected ComponentState $lastState,
    ) {}

    static function create($name, $params = [], $fromQueryString = [])
    static function create($name, $params = [], $fromQueryString = [], $cookies = [])
    {
        $name = static::normalizeAndRegisterComponentName($name);

@@ -36,6 +36,7 @@ static function create($name, $params = [], $fromQueryString = [])
            $name,
            $params,
            $fromQueryString,
            $cookies,
        );

        return new static($requestBroker, $initialState);
  24 changes: 24 additions & 0 deletions24  
src/Features/SupportTesting/UnitTest.php
@@ -501,6 +501,30 @@ function assert_response_of_calling_method()
            ->assertReturned('bar')
            ->assertReturned(fn ($data) => $data === 'bar');
    }
    /** @test */
    public function can_set_cookies_for_use_with_testing()
    {
        // Test both the `withCookies` and `withCookie` methods that Laravel normally provides
        Livewire::withCookies(['colour' => 'blue'])
            ->withCookie('name', 'Taylor')
            ->test(new class extends Component {
                public $colourCookie = '';
                public $nameCookie = '';
                public function mount()
                {
                    $this->colourCookie = request()->cookie('colour');
                    $this->nameCookie = request()->cookie('name');
                }

                public function render()
                {
                    return '<div></div>';
                }
            })
            ->assertSet('colourCookie', 'blue')
            ->assertSet('nameCookie', 'Taylor')
            ;
    }
}

class HasMountArguments extends Component
  40 changes: 40 additions & 0 deletions40  
src/Features/SupportTransitions/BrowserTest.php
@@ -48,4 +48,44 @@ public function render() { return <<<'HTML'
        ->assertMissing('@target')
        ;
    }

    /** @test */
    public function elements_the_contain_transition_are_displayed_on_page_load()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public $messages = [
                    'message 1',
                    'message 2',
                    'message 3',
                    'message 4',
                ];

                public function addMessage()
                {
                    $this->messages[] = 'message ' . count($this->messages) + 1;
                }

                public function render()
                {
                    return <<< 'HTML'
                    <div>
                        <ul class="text-xs">
                            @foreach($messages as $index => $message)
                                <li wire:transition.fade.duration.1000ms dusk="message-{{ $index + 1 }}">{{$message}}</li>
                            @endforeach
                        </ul>
                        <button type="button" wire:click="addMessage" dusk="add-message">Add message</button>
                    </div>
                    HTML;
                }
            }
        )
        ->assertVisible('@message-1')
        ->assertVisible('@message-2')
        ->assertVisible('@message-3')
        ->assertVisible('@message-4')
        ;
    }
}
  10 changes: 8 additions & 2 deletions10  
src/Features/SupportValidation/BaseRule.php
@@ -10,7 +10,6 @@
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class BaseRule extends LivewireAttribute
{
    // @todo: support custom messages...
    function __construct(
        public $rule,
        protected $attribute = null,
@@ -68,7 +67,14 @@ function boot()

                $this->component->addMessagesFromOutside($messages);
            } else {
                $this->component->addMessagesFromOutside([$this->getName() => $this->translate ? trans($this->message) : $this->message]);
                // If a single message was provided, apply it to the first given rule.
                // There should have only been one rule provided in this case anyways...
                $rule = head(array_values($rules));

                // In the case of "min:5" or something, we only want "min"...
                $rule = (string) str($rule)->before(':');

                $this->component->addMessagesFromOutside([$this->getName().'.'.$rule => $this->translate ? trans($this->message) : $this->message]);
            }
        }

  17 changes: 17 additions & 0 deletions17  
src/Features/SupportValidation/UnitTest.php
@@ -186,6 +186,23 @@ function save() { $this->validate(); }
            ;
    }

    /** @test */
    public function rule_attribute_supports_custom_messages_when_using_repeated_attributes()
    {
        Livewire::test(new class extends TestComponent {
            #[BaseRule('required', message: 'Please provide a post title')]
            #[BaseRule('min:3', message: 'This title is too short')]
            public $title = '';
        })
            ->set('title', '')
            ->assertHasErrors(['title' => 'required'])
            ->tap(function ($component) {
                $messages = $component->errors()->getMessages();
                $this->assertEquals('Please provide a post title', $messages['title'][0]);
            })
        ;
    }

    /** @test */
    public function rule_attribute_message_is_translatable()
    {
  1 change: 0 additions & 1 deletion1  
src/Livewire.php
@@ -15,7 +15,6 @@
 * @method static array update($snapshot, $diff, $calls)
 * @method static bool isLivewireRequest()
 * @method static void setUpdateRoute($callback)
 * @method static void setUpdateUri()
 * @method static string getUpdateUri($callback)
 * @method static void setScriptRoute($callback)
 * @method static void useScriptTagAttributes($attributes)
  18 changes: 17 additions & 1 deletion18  
src/LivewireManager.php
@@ -140,6 +140,8 @@ function useScriptTagAttributes($attributes)

    protected $queryParamsForTesting = [];

    protected $cookiesForTesting = [];

    function withUrlParams($params)
    {
        return $this->withQueryParams($params);
@@ -152,9 +154,23 @@ function withQueryParams($params)
        return $this;
    }

    function withCookie($name, $value)
    {
        $this->cookiesForTesting[$name] = $value;

        return $this;
    }

    function withCookies($cookies)
    {
        $this->cookiesForTesting = array_merge($this->cookiesForTesting, $cookies);

        return $this;
    }

    function test($name, $params = [])
    {
        return Testable::create($name, $params, $this->queryParamsForTesting);
        return Testable::create($name, $params, $this->queryParamsForTesting, $this->cookiesForTesting);
    }

    function visit($name)
  8 changes: 7 additions & 1 deletion8  
src/Mechanisms/FrontendAssets/FrontendAssets.php
@@ -82,14 +82,16 @@ public static function styles($options = [])

        $nonce = isset($options['nonce']) ? "nonce=\"{$options['nonce']}\"" : '';

        $progressBarColor = config('livewire.navigate.progress_bar_color', '#2299dd');

        $html = <<<HTML
        <!-- Livewire Styles -->
        <style {$nonce}>
            [wire\:loading], [wire\:loading\.delay], [wire\:loading\.inline-block], [wire\:loading\.inline], [wire\:loading\.block], [wire\:loading\.flex], [wire\:loading\.table], [wire\:loading\.grid], [wire\:loading\.inline-flex] {
                display: none;
            }
            [wire\:loading\.delay\.shortest], [wire\:loading\.delay\.shorter], [wire\:loading\.delay\.short], [wire\:loading\.delay\.long], [wire\:loading\.delay\.longer], [wire\:loading\.delay\.longest] {
            [wire\:loading\.delay\.none], [wire\:loading\.delay\.shortest], [wire\:loading\.delay\.shorter], [wire\:loading\.delay\.short], [wire\:loading\.delay\.default], [wire\:loading\.delay\.long], [wire\:loading\.delay\.longer], [wire\:loading\.delay\.longest] {
                display:none;
            }
@@ -101,6 +103,10 @@ public static function styles($options = [])
                display: none;
            }
            :root {
                --livewire-progress-bar-color: {$progressBarColor};
            }
            [x-cloak] {
                display: none;
            }
  8 changes: 6 additions & 2 deletions8  
src/Mechanisms/HandleRequests/HandleRequests.php
@@ -26,7 +26,9 @@ function boot()

    function getUpdateUri()
    {
        return (string) str($this->updateRoute->uri)->start('/');
        return (string) str(
            route($this->updateRoute->getName(), [], false)
        )->start('/');
    }

    function skipRequestPayloadTamperingMiddleware()
@@ -45,7 +47,9 @@ function setUpdateRoute($callback)
        $route = $callback([self::class, 'handleUpdate']);

        // Append `livewire.update` to the existing name, if any.
        $route->name('livewire.update');
        if (! str($route->getName())->endsWith('livewire.update')) {
            $route->name('livewire.update');
        }

        $this->updateRoute = $route;
    }
 69 changes: 69 additions & 0 deletions69  
src/Mechanisms/Tests/CustomUpdateRouteBrowserTest.php
@@ -0,0 +1,69 @@
<?php

namespace Livewire\Mechanisms\Tests;

use Livewire\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\URL;

class CustomUpdateRouteBrowserTest extends \Tests\BrowserTestCase
{
    public static function tweakApplicationHook()
    {
        return function () {
            // This would normally be done in something like middleware
            URL::defaults(['tenant' => 'custom-tenant']);

            Livewire::setUpdateRoute(function ($handle) {
                return Route::post('/{tenant}/livewire/update', $handle)->name('tenant.livewire.update');
            });

            // Doesn't seem to be needed in real applications, but is needed in tests
            app('router')->getRoutes()->refreshNameLookups();

            Route::prefix('/{tenant}')->group(function () {
                Route::get('/page', function ($tenant) {
                    return (app('livewire')->new('test'))();
                })->name('tenant.page');
            });

            Livewire::component('test', new class extends Component {
                public $count = 0;

                function increment()
                {
                    $this->count++;
                }

                public function render() {
                    return <<<'HTML'
                        <div>
                            <h1 dusk="count">Count: {{ $count }}</h1>
                            <button wire:click="increment" dusk="button">Increment</button>
                            <p>Tenant: {{ request()->route()->parameter('tenant') }}</p>
                            <p>Route: {{ request()->route()->getName() }}</p>
                        </div>
                    HTML;
                }
            });
        };
    }

    /** @test */
    public function can_use_a_custom_update_route_with_a_uri_segment()
    {
        $this->browse(function (\Laravel\Dusk\Browser $browser) {
            $browser
                ->visit('/custom-tenant/page')
                ->assertSee('Count: 0')
                ->assertSee('Tenant: custom-tenant')
                ->assertSee('Route: tenant.page')
                ->waitForLivewire()
                ->click('button')
                ->assertSee('Count: 1')
                ->assertSee('Tenant: custom-tenant')
                ->assertSee('Route: tenant.livewire.update');
        });
    }
}
Footer
© 2023 GitHub, Inc.
Footer navigation
Terms
Privacy
Security
Status
Docs
Contact GitHub
Pricing
API
Training
Blog
About
Editor is loading...