diff --git a/README.md b/README.md index f374176..3c89972 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,14 @@ By default, the modal will close when you click outside the modal. If you want t } ``` + By default, when a child modal is closed, the closed components state is still available if the same modal component is opened again. If you would like to destroy the component when its closed you can override the static `destroyOnClose` method and have it return `true`. When a destroyed modal is opened again its state will be reset. + ```php + public static function destroyOnClose(): bool + { + return true; + } + ``` + ## Skipping previous modals In some cases you might want to skip previous modals. For example: 1. Team overview modal @@ -314,6 +322,12 @@ class DeleteTeam extends ModalComponent } ``` +You can also optionally call the `destroySkippedModals()` method to destroy the skipped modals so if any are opened again their state will be reset + + + + + ## Building Tailwind CSS for production To purge the classes used by the package, add the following lines to your purge array in `tailwind.config.js`: ```js @@ -406,6 +420,8 @@ return [ 'close_modal_on_escape_is_forceful' => true, 'dispatch_close_event' => false, + + 'destroy_on_close' => false, ], ]; ``` diff --git a/config/livewire-ui-modal.php b/config/livewire-ui-modal.php index f62e473..103f7ca 100644 --- a/config/livewire-ui-modal.php +++ b/config/livewire-ui-modal.php @@ -48,5 +48,7 @@ 'close_modal_on_escape_is_forceful' => true, 'dispatch_close_event' => false, + + 'destroy_on_close' => false, ], ]; diff --git a/public/modal.js b/public/modal.js index e6ddf28..9a711b4 100644 --- a/public/modal.js +++ b/public/modal.js @@ -1 +1 @@ -(()=>{var t,e={331:()=>{function t(t){return function(t){if(Array.isArray(t))return e(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,o){if(!t)return;if("string"==typeof t)return e(t,o);var n=Object.prototype.toString.call(t).slice(8,-1);"Object"===n&&t.constructor&&(n=t.constructor.name);if("Map"===n||"Set"===n)return Array.from(t);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return e(t,o)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var o=0,n=new Array(e);o0&&void 0!==arguments[0]&&arguments[0],e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;if(!0===this.getActiveComponentModalAttribute("dispatchCloseEvent")){var o=this.$wire.get("components")[this.activeComponent].name;Livewire.emit("modalClosed",o)}if(e>0)for(var n=0;n1&&void 0!==arguments[1]&&arguments[1];if(this.show=!0,this.activeComponent!==t){!1!==this.activeComponent&&!1===o&&this.componentHistory.push(this.activeComponent);var n=50;!1===this.activeComponent?(this.activeComponent=t,this.showActiveComponent=!0,this.modalWidth="sm:max-w-"+this.getActiveComponentModalAttribute("maxWidth")):(this.showActiveComponent=!1,n=400,setTimeout((function(){e.activeComponent=t,e.showActiveComponent=!0,e.modalWidth="sm:max-w-"+e.getActiveComponentModalAttribute("maxWidth")}),300)),this.$nextTick((function(){var o=e.$refs[t].querySelector("[autofocus]");o&&setTimeout((function(){o.focus()}),n)}))}},focusables:function(){return t(this.$el.querySelectorAll("a, button, input, textarea, select, details, [tabindex]:not([tabindex='-1'])")).filter((function(t){return!t.hasAttribute("disabled")}))},firstFocusable:function(){return this.focusables()[0]},lastFocusable:function(){return this.focusables().slice(-1)[0]},nextFocusable:function(){return this.focusables()[this.nextFocusableIndex()]||this.firstFocusable()},prevFocusable:function(){return this.focusables()[this.prevFocusableIndex()]||this.lastFocusable()},nextFocusableIndex:function(){return(this.focusables().indexOf(document.activeElement)+1)%(this.focusables().length+1)},prevFocusableIndex:function(){return Math.max(0,this.focusables().indexOf(document.activeElement))-1},init:function(){var t=this;this.$watch("show",(function(e){e?document.body.classList.add("overflow-y-hidden"):(document.body.classList.remove("overflow-y-hidden"),setTimeout((function(){t.activeComponent=!1,t.$wire.resetState()}),300))})),Livewire.on("closeModal",(function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;t.closeModal(e,o)})),Livewire.on("activeModalComponentChanged",(function(e){t.setActiveModalComponent(e)}))}}}},754:()=>{}},o={};function n(t){var i=o[t];if(void 0!==i)return i.exports;var r=o[t]={exports:{}};return e[t](r,r.exports,n),r.exports}n.m=e,t=[],n.O=(e,o,i,r)=>{if(!o){var s=1/0;for(l=0;l=r)&&Object.keys(n.O).every((t=>n.O[t](o[c])))?o.splice(c--,1):(a=!1,r0&&t[l-1][2]>r;l--)t[l]=t[l-1];t[l]=[o,i,r]},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{var t={387:0,109:0};n.O.j=e=>0===t[e];var e=(e,o)=>{var i,r,[s,a,c]=o,l=0;for(i in a)n.o(a,i)&&(n.m[i]=a[i]);if(c)var u=c(n);for(e&&e(o);ln(331)));var i=n.O(void 0,[109],(()=>n(754)));i=n.O(i)})(); \ No newline at end of file +(()=>{var t,e={331:()=>{function t(t){return function(t){if(Array.isArray(t))return e(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,o){if(!t)return;if("string"==typeof t)return e(t,o);var n=Object.prototype.toString.call(t).slice(8,-1);"Object"===n&&t.constructor&&(n=t.constructor.name);if("Map"===n||"Set"===n)return Array.from(t);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return e(t,o)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var o=0,n=new Array(e);o0&&void 0!==arguments[0]&&arguments[0],e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,o=arguments.length>2&&void 0!==arguments[2]&&arguments[2];if(!0===this.getActiveComponentModalAttribute("dispatchCloseEvent")){var n=this.$wire.get("components")[this.activeComponent].name;Livewire.emit("modalClosed",n)}if(!0===this.getActiveComponentModalAttribute("destroyOnClose")&&Livewire.emit("destroyComponent",this.activeComponent),e>0)for(var i=0;i1&&void 0!==arguments[1]&&arguments[1];if(this.show=!0,this.activeComponent!==t){!1!==this.activeComponent&&!1===o&&this.componentHistory.push(this.activeComponent);var n=50;!1===this.activeComponent?(this.activeComponent=t,this.showActiveComponent=!0,this.modalWidth="sm:max-w-"+this.getActiveComponentModalAttribute("maxWidth")):(this.showActiveComponent=!1,n=400,setTimeout((function(){e.activeComponent=t,e.showActiveComponent=!0,e.modalWidth="sm:max-w-"+e.getActiveComponentModalAttribute("maxWidth")}),300)),this.$nextTick((function(){var o=e.$refs[t].querySelector("[autofocus]");o&&setTimeout((function(){o.focus()}),n)}))}},focusables:function(){return t(this.$el.querySelectorAll("a, button, input, textarea, select, details, [tabindex]:not([tabindex='-1'])")).filter((function(t){return!t.hasAttribute("disabled")}))},firstFocusable:function(){return this.focusables()[0]},lastFocusable:function(){return this.focusables().slice(-1)[0]},nextFocusable:function(){return this.focusables()[this.nextFocusableIndex()]||this.firstFocusable()},prevFocusable:function(){return this.focusables()[this.prevFocusableIndex()]||this.lastFocusable()},nextFocusableIndex:function(){return(this.focusables().indexOf(document.activeElement)+1)%(this.focusables().length+1)},prevFocusableIndex:function(){return Math.max(0,this.focusables().indexOf(document.activeElement))-1},init:function(){var t=this;this.$watch("show",(function(e){e?document.body.classList.add("overflow-y-hidden"):(document.body.classList.remove("overflow-y-hidden"),setTimeout((function(){t.activeComponent=!1,t.$wire.resetState()}),300))})),Livewire.on("closeModal",(function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];t.closeModal(e,o,n)})),Livewire.on("activeModalComponentChanged",(function(e){t.setActiveModalComponent(e)}))}}}},754:()=>{}},o={};function n(t){var i=o[t];if(void 0!==i)return i.exports;var r=o[t]={exports:{}};return e[t](r,r.exports,n),r.exports}n.m=e,t=[],n.O=(e,o,i,r)=>{if(!o){var s=1/0;for(l=0;l=r)&&Object.keys(n.O).every((t=>n.O[t](o[c])))?o.splice(c--,1):(a=!1,r0&&t[l-1][2]>r;l--)t[l]=t[l-1];t[l]=[o,i,r]},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{var t={387:0,109:0};n.O.j=e=>0===t[e];var e=(e,o)=>{var i,r,[s,a,c]=o,l=0;for(i in a)n.o(a,i)&&(n.m[i]=a[i]);if(c)var u=c(n);for(e&&e(o);ln(331)));var i=n.O(void 0,[109],(()=>n(754)));i=n.O(i)})(); \ No newline at end of file diff --git a/resources/js/modal.js b/resources/js/modal.js index c482e10..7bda255 100644 --- a/resources/js/modal.js +++ b/resources/js/modal.js @@ -25,15 +25,23 @@ window.LivewireUIModal = () => { this.closeModal(true); }, - closeModal(force = false, skipPreviousModals = 0) { + closeModal(force = false, skipPreviousModals = 0, destroySkipped = false) { if (this.getActiveComponentModalAttribute('dispatchCloseEvent') === true) { const componentName = this.$wire.get('components')[this.activeComponent].name; Livewire.emit('modalClosed', componentName); } + if (this.getActiveComponentModalAttribute('destroyOnClose') === true) { + Livewire.emit('destroyComponent', this.activeComponent); + } + if (skipPreviousModals > 0) { for (var i = 0; i < skipPreviousModals; i++) { + if (destroySkipped) { + const id = this.componentHistory[this.componentHistory.length - 1]; + Livewire.emit('destroyComponent', id); + } this.componentHistory.pop(); } } @@ -126,13 +134,15 @@ window.LivewireUIModal = () => { } }); - Livewire.on('closeModal', (force = false, skipPreviousModals = 0) => { - this.closeModal(force, skipPreviousModals); + Livewire.on('closeModal', (force = false, skipPreviousModals = 0, destroySkipped = false) => { + this.closeModal(force, skipPreviousModals, destroySkipped); }); Livewire.on('activeModalComponentChanged', (id) => { this.setActiveModalComponent(id); }); + + } }; } diff --git a/resources/views/modal.blade.php b/resources/views/modal.blade.php index ef62b2b..6d2dbee 100644 --- a/resources/views/modal.blade.php +++ b/resources/views/modal.blade.php @@ -46,7 +46,7 @@ class="fixed inset-0 transition-all transform" class="inline-block w-full align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:w-full" > @forelse($components as $id => $component) -
+
@livewire($component['name'], $component['attributes'], key($id))
@empty diff --git a/src/Modal.php b/src/Modal.php index 0ca5575..95223da 100644 --- a/src/Modal.php +++ b/src/Modal.php @@ -38,6 +38,7 @@ public function openModal($component, $componentAttributes = [], $modalAttribute 'closeOnEscape' => $componentClass::closeModalOnEscape(), 'closeOnEscapeIsForceful' => $componentClass::closeModalOnEscapeIsForceful(), 'dispatchCloseEvent' => $componentClass::dispatchCloseEvent(), + 'destroyOnClose' => $componentClass::destroyOnClose(), 'maxWidth' => $componentClass::modalMaxWidth(), ], $modalAttributes), ]; @@ -47,10 +48,16 @@ public function openModal($component, $componentAttributes = [], $modalAttribute $this->emit('activeModalComponentChanged', $id); } + public function destroyComponent($id): void + { + unset($this->components[$id]); + } + public function getListeners(): array { return [ 'openModal', + 'destroyComponent' ]; } diff --git a/src/ModalComponent.php b/src/ModalComponent.php index b5c3be8..e1643f1 100644 --- a/src/ModalComponent.php +++ b/src/ModalComponent.php @@ -11,16 +11,26 @@ abstract class ModalComponent extends Component implements Contract public int $skipModals = 0; - public function skipPreviousModals($count = 1): self + public bool $destroySkipped = false; + + public function destroySkippedModals(): self + { + $this->destroySkipped = true; + + return $this; + } + + public function skipPreviousModals($count = 1, $destroy = false): self { - $this->skipPreviousModal($count); + $this->skipPreviousModal($count, $destroy); return $this; } - public function skipPreviousModal($count = 1): self + public function skipPreviousModal($count = 1, $destroy = false): self { $this->skipModals = $count; + $this->destroySkipped = $destroy; return $this; } @@ -34,7 +44,7 @@ public function forceClose(): self public function closeModal(): void { - $this->emit('closeModal', $this->forceClose, $this->skipModals); + $this->emit('closeModal', $this->forceClose, $this->skipModals, $this->destroySkipped); } public function closeModalWithEvents(array $events): void @@ -68,6 +78,11 @@ public static function dispatchCloseEvent(): bool return config('livewire-ui-modal.component_defaults.dispatch_close_event', false); } + public static function destroyOnClose(): bool + { + return config('livewire-ui-modal.component_defaults.destroy_on_close', false); + } + private function emitModalEvents(array $events): void { foreach ($events as $component => $event) { diff --git a/tests/LivewireModalComponentTest.php b/tests/LivewireModalComponentTest.php index 4f67c29..f96b53d 100644 --- a/tests/LivewireModalComponentTest.php +++ b/tests/LivewireModalComponentTest.php @@ -11,7 +11,7 @@ public function testCloseModal(): void { Livewire::test(DemoModal::class) ->call('closeModal') - ->assertEmitted('closeModal', false, 0); + ->assertEmitted('closeModal', false, 0, false); } public function testForceCloseModal(): void @@ -19,7 +19,7 @@ public function testForceCloseModal(): void Livewire::test(DemoModal::class) ->call('forceClose') ->call('closeModal') - ->assertEmitted('closeModal', true, 0); + ->assertEmitted('closeModal', true, 0, false); } public function testModalSkipping(): void @@ -27,12 +27,18 @@ public function testModalSkipping(): void Livewire::test(DemoModal::class) ->call('skipPreviousModals', 5) ->call('closeModal') - ->assertEmitted('closeModal', false, 5); + ->assertEmitted('closeModal', false, 5, false); Livewire::test(DemoModal::class) ->call('skipPreviousModal') ->call('closeModal') - ->assertEmitted('closeModal', false, 1); + ->assertEmitted('closeModal', false, 1, false); + + Livewire::test(DemoModal::class) + ->call('skipPreviousModal') + ->call('destroySkippedModals') + ->call('closeModal') + ->assertEmitted('closeModal', false, 1, true); } public function testModalEventEmitting(): void diff --git a/tests/LivewireModalTest.php b/tests/LivewireModalTest.php index 68659b9..d55ab46 100644 --- a/tests/LivewireModalTest.php +++ b/tests/LivewireModalTest.php @@ -7,6 +7,8 @@ use LivewireUI\Modal\Tests\Components\DemoModal; use LivewireUI\Modal\Tests\Components\InvalidModal; +use function PHPUnit\Framework\assertArrayNotHasKey; + class LivewireModalTest extends TestCase { public function testOpenModalEventListener(): void @@ -17,7 +19,7 @@ public function testOpenModalEventListener(): void // Event attributes $component = 'demo-modal'; $componentAttributes = ['message' => 'Foobar']; - $modalAttributes = ['hello' => 'world', 'closeOnEscape' => true, 'maxWidth' => '2xl', 'closeOnClickAway' => true, 'closeOnEscapeIsForceful' => true, 'dispatchCloseEvent' => false]; + $modalAttributes = ['hello' => 'world', 'closeOnEscape' => true, 'maxWidth' => '2xl', 'closeOnClickAway' => true, 'closeOnEscapeIsForceful' => true, 'dispatchCloseEvent' => false, 'destroyOnClose' => false]; // Demo modal unique identifier $id = md5($component . serialize($componentAttributes)); @@ -38,6 +40,32 @@ public function testOpenModalEventListener(): void ->assertEmitted('activeModalComponentChanged', $id); } + public function testDestroyComponentEventListener(): void + { + // Demo modal component + Livewire::component('demo-modal', DemoModal::class); + + $component = 'demo-modal'; + $componentAttributes = ['message' => 'Foobar']; + $modalAttributes = ['hello' => 'world', 'closeOnEscape' => true, 'maxWidth' => '2xl', 'closeOnClickAway' => true, 'closeOnEscapeIsForceful' => true, 'dispatchCloseEvent' => false, 'destroyOnClose' => false]; + + // Demo modal unique identifier + $id = md5($component . serialize($componentAttributes)); + + Livewire::test(Modal::class) + ->emit('openModal', $component, $componentAttributes, $modalAttributes) + ->assertSet('components', [ + $id => [ + 'name' => $component, + 'attributes' => $componentAttributes, + 'modalAttributes' => $modalAttributes, + ], + ]) + ->emit('destroyComponent', $id) + ->assertSet('components', []); + + } + public function testModalReset(): void { Livewire::component('demo-modal', DemoModal::class);