flekschas / svelte-simple-modal

A simple, small, and content-agnostic modal for Svelte v3 and v4
https://svelte.dev/repl/b95ce66b0ef34064a34afc5c0249f313
MIT License
422 stars 30 forks source link

Support multiple modals opening on top of each other #89

Closed josdejong closed 1 year ago

josdejong commented 1 year ago

Right now, when I open a second modal from an open modal, the first modal disappears. Ideally, the second modal should be rendered on top of the original one, showing both.

flekschas commented 1 year ago

Apologies for my belated reply. By design, what you're trying to achieve is not possible with a single <Modal /> instance. You can only ever have as many modals as you have <Modal /> instances. So in order to open two modals you need two <Modal /> instances.

Beyond that it's considered a bad practice to have multiple superimposing/nested modal dialogs, which is why I am not planning to add direct support for it to this library. But again, if you need to have multiple modals open at the same time, simply create two global <Modal /> components.

josdejong commented 1 year ago

No need to apologize, thanks for your reply.

I agree in general that nested modals are not a very good idea. I have a bit of a special case here though. I'll see how I can go about it. I can probably model a stack holding the state and opening the latest in the stack in a single popup or so.

flekschas commented 1 year ago

If you know ahead of time that you'll only ever have to deal with two modals a very simple solution could be the following

<!-- App.svelte -->
<script>
  import Modal, { bind } from 'svelte-simple-modal';
  import Popup from './Popup.svelte';
  import { modal1, modal2 } from './stores'
  const showModal = () => modal1.set(bind(Popup, { message: 'Surprise' }));
</script>

<Modal show={$modal1}>
  <Modal show={$modal2}>
    <button on:click={showModal}>Show modal</button>
  </Modal>
</Modal>

<!-- Popup.svelte -->
<script>
  import { bind } from 'svelte-simple-modal';
  import Popup2 from './Popup2.svelte';
  import { modal2 } from './stores'
  const showModal = () => modal2.set(bind(Popup2, { message: 'Double Surprise' }));

  export let message = 'Hi';
</script>

<p>πŸŽ‰ {message} 🍾</p>
<button on:click={showModal}>Open another modal</button>

<!-- Popup2.svelte -->
<script>
  export let message = 'Hi';
</script>

<p>πŸŽ‰πŸŽ‰ {message} 🍾🍾</p>

<!-- stores.js -->
import { writable } from 'svelte/store';

export const modal1 = writable(null);
export const modal2 = writable(null);

Live demo: https://svelte.dev/repl/3ed9fa66fdc04664b9a9d93b88aef310?version=3.53.1

This triggers two nested components. There are only two downsides really. First, the number of nested components needs to be known upfront. Second, both modals are relative to the HTML body element. So one cannot really create a modal that's relative to another DOM element (like the first modal).

I hope this helps.

josdejong commented 1 year ago

O wow, that is a really helpful example! Thanks Fritz. So it is possible to have multiple modals, though they must know their "level" in advance.

I have a fun, recursive case: a popup in a JSON Editor to edit a small section of a JSON document. Since a JSON structure can be deeply nested, you could open another popup again from the first popup, and again. These are edge cases though, normally you open 1 popup, and in exceptional cases 2. After your feedback I've been thinking about alternative solutions. I think I can neatly solve this by opening 1 modal, and inside this single modal, keep a stack of the nested levels+state, showing only the last one, with a navigation path so you do not lose track. I'll figure something out.

flekschas commented 1 year ago

So it is possible to have multiple modals, though they must know their "level" in advance.

Yes, that's correct.

I think I can neatly solve this by opening 1 modal, and inside this single modal, keep a stack of the nested levels+state, showing only the last one, with a navigation path so you do not lose track.

This sounds like a neat solution!

Apart from that, I think another interesting solution to get infinitely-nested Modals is to instantiate a new <Modal /> within a modal. Ideally, the nested modals would overlay only the previous modal, but I haven't gotten around to implement this (i.e., that's what https://github.com/flekschas/svelte-simple-modal/issues/32 is open for)

Here's a demo that supports infinitely nested modals: https://svelte.dev/repl/0341523e52074c92a6a3ba454270921d?version=3.53.1

<!-- App.svelte -->
<script>
    import { writable } from 'svelte/store';
    import Modal from 'svelte-simple-modal';
    import Popup from './Popup.svelte';

    const modal = writable(null);
    const showModal = () => modal.set(Popup);
</script>

<Modal show={$modal}>
    <button on:click={showModal}>Show modal</button>
</Modal>

<!-- Popup.svelte -->
<script>
    import { writable } from 'svelte/store';
    import Modal, { bind } from 'svelte-simple-modal';
    import Popup from './Popup.svelte';

    export let level = 1;

    const modal = writable(null);
    const showModal = () => modal.set(bind(Popup, { level: level + 1 }));
    const message = Array.from({ length: level }, () => 'Suprise').join(' ');
</script>

<Modal show={$modal}>
    <p>πŸŽ‰ {message} 🍾</p>
    <button on:click={showModal}>Show another modal</button>
</Modal>
josdejong commented 1 year ago

Ha, that is nifty 😎 . I couldn't have figured out that use of svelte-simple-modal myself. I have to say, the library is more flexible than I had expected :). This gives me some good inspiration to work out a solution for my recursive case, thanks.

Neptunium1129 commented 1 year ago

closeOnEsc -> all modal window terminated... how to each modal terminated?

"closeButton" option as intended. But "closeOnEsc" is all terminated.

Neptunium1129 commented 1 year ago

https://svelte.dev/repl/514f1335749a4eae9d34ad74dc277f20?version=3.37.0 this example destroy nested modal one by one as "ESC" key clicked

flekschas commented 1 year ago

@Neptunium1129 This isn't supported out of the box as the library by default assumes you're using it a global singleton. If you need to support multiple nested components you'll have to implement the closing on escape logic yourself. However, it isn't too hard. The following works for me:

<!-- App.svelte -->
<script>
  import { writable } from 'svelte/store';
  import Modal from 'svelte-simple-modal';
  import Popup from './Popup.svelte';
  import { currentModal } from './stores';

  const modal = writable(null);

  const showModal = () => modal.set(Popup);

  const handleKeydown = (event) => {
    if (event.key === 'Escape' && $currentModal === 1) {
      event.preventDefault();
      modal.update(() => null);
      currentModal.update(() => 1);
    }
  }
</script>

<svelte:window on:keydown={handleKeydown} />

<Modal show={$modal} closeOnEsc={false}>
  <button on:click={showModal}>Show modal</button>
</Modal>

<!-- Popup.svelte -->
<script>
  import Modal, { bind } from 'svelte-simple-modal';
  import Popup from './Popup.svelte';
  import { currentModal } from './stores';

  export let level = 1;
  const modal = writable(null);

  const showModal = () => modal.set(bind(Popup, { level: level + 1 }));

  const message = Array.from({ length: level }, () => 'Suprise').join(' ');

  currentModal.update(() => level);

  const handleKeydown = (event) => {
    if (event.key === 'Escape' && $currentModal === level + 1) {
      event.preventDefault();
      modal.update(() => null);
      currentModal.update(() => level);
    }
  }
</script>

<svelte:window on:keydown={handleKeydown} />

<Modal show={$modal} closeOnEsc={false}>
  <p>πŸŽ‰ {message} 🍾</p>
  <button on:click={showModal}>Show another modal</button>
</Modal>

<!-- stores.js -->
import { writable } from 'svelte/store';
export const currentModal = writable(1);

https://svelte.dev/repl/736fca8151744b4e83bf4791c1fd51ed?version=3.53.1

Neptunium1129 commented 1 year ago

@flekschas thanks !!