svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Allow bind:open with <dialog>

Open tvanc opened this issue 5 years ago • 25 comments

Attempting to use bind:open with <dialog> results in an error:

<!-- Dialog.svelte -->
<script>
  export let open = false
</script>

<dialog bind:open />
<!--
Error: Module build failed (from ./node_modules/svelte-loader/index.js):
Error: ValidationError: 'open' binding can only be used with <details> (17:46)
-->

To Reproduce View REPL

Expected behavior I expected bind to work with <dialog> as well as <details>.

Information about your Svelte project: Svelte v3.21.0

Severity Annoying.

tvanc avatar Apr 26 '20 00:04 tvanc

I'd love to make a PR but I'm not sure how to write the test.

If anyone could look at my attempt and tell me what I'm doing wrong, that'd be swell.

https://github.com/sveltejs/svelte/compare/master...tvanc:openBindingForDialog

tvanc avatar Apr 26 '20 01:04 tvanc

interestingly, based on the MDN docs, https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement, there's no event we can listen to determine when a dialog is open.

there's close to know when it is closed. but no way we can tell if it is opened.

tanhauhau avatar Apr 26 '20 03:04 tanhauhau

Is having an event necessary for the directive and the test? Maybe we can use MutationObserver as a workaround

tvanc avatar Apr 26 '20 19:04 tvanc

I don't have a strong opinion on if Svelte should support this, but here's an example I found of using a MutationObserver to fire an event when the dialog opens: https://jsfiddle.net/vmj8d79q/. Looks like it works fine.

dimfeld avatar Apr 28 '20 21:04 dimfeld

I really would like to have the ability to bind open property for dialogs in svelte

ZerdoX-x avatar Aug 27 '20 06:08 ZerdoX-x

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jun 27 '21 00:06 stale[bot]

Why details has this option but dialog does not? I don't think it's that hard. I'll try to implement this later

ZerdoX-x avatar Jun 27 '21 08:06 ZerdoX-x

@ZerdoX-x I'd love to see how you do it. I don't know svelte internals very well. I couldn't figure it out.

tvanc avatar Jul 04 '21 03:07 tvanc

The lack of a close event could be problematic. I'm also running into this issue, but I wonder if it's actually solveable.

antony avatar Dec 07 '21 18:12 antony

I'm not sure if I should make a separate issue for this but I noticed something in the same area.

I'm using a dialog like in the below code, which works fine, but if I try to use the <form method='dialog'> technique for closing the dialog based on button clicks, it makes it so the dialog cannot be reopened. Clicking the close button will close the dialog, but subsequent calls to .$set({ open: true}) will not reopen it. I'm guessing because the way the browser implements the form close is destroying the attribute in some way so svelte can't use it.

If I remove the form and just use svelte reactivity, it works fine and allows the dialog to be reopened.

<script>
    export let open = true;
</script>

<dialog {open}>
   <form method='dialog'>
        <button value="cancel">Cancel</button>  //doesn't allow reopening
  </form>
</dialog>

<dialog {open}>
        <button value="cancel" on:click={() => open = false}>Cancel</button> //allows reopening
</dialog>

joepetrakovich avatar Oct 01 '22 17:10 joepetrakovich

idk how bindings internally work but this could be implemented by

  • externally set? if you need to make it open, use dialog.showModal(); if you need to make it close use dialog.close()
  • changed by browser? listen for the cancel event (fired on esc)

KTibow avatar Jan 21 '23 17:01 KTibow

The lack of a close event could be problematic. I'm also running into this issue, but I wonder if it's actually solveable.

HTMLDialogElement has a close and cancel event (although that may not have been true at the time you wrote your comment)

danielniccoli avatar Aug 28 '23 22:08 danielniccoli

I'm not sure if I should make a separate issue for this but I noticed something in the same area.

I'm using a dialog like in the below code, which works fine, but if I try to use the <form method='dialog'> technique for closing the dialog based on button clicks, it makes it so the dialog cannot be reopened. Clicking the close button will close the dialog, but subsequent calls to .$set({ open: true}) will not reopen it. I'm guessing because the way the browser implements the form close is destroying the attribute in some way so svelte can't use it.

If I remove the form and just use svelte reactivity, it works fine and allows the dialog to be reopened.

<script>
    export let open = true;
</script>

<dialog {open}>
   <form method='dialog'>
        <button value="cancel">Cancel</button>  //doesn't allow reopening
  </form>
</dialog>

<dialog {open}>
        <button value="cancel" on:click={() => open = false}>Cancel</button> //allows reopening
</dialog>

try this out: <dialog {open} on:close={() => open = false}> this is how I am currently working around dialog's missing bind for open

sprataa avatar Dec 01 '23 13:12 sprataa

The problem with that is that it doesn't enable the backdrop

KTibow avatar Dec 01 '23 14:12 KTibow

That is besides the point of this issue and also not the default behavior.

If in plain HTML adding open opens the dialog non-modal, then it would be highly inconsistent if Svelte just decides to do something else. Personally, I find it much easier to interact with dialog components imperatively anyway, you can just throw them away after being closed and keep everything clean.

Manually calling showModal() really should not be a problem.

brunnerh avatar Dec 01 '23 16:12 brunnerh

That is besides the point of this issue and also not the default behavior.

If in plain HTML adding open opens the dialog non-modal, then it would be highly inconsistent if Svelte just decides to do something else. Personally, I find it much easier to interact with dialog components imperatively anyway, you can just throw them away after being closed and keep everything clean.

Manually calling showModal() really should not be a problem.

I disagree. The backdrop is an integral part. If you open the modal without the backdrop showing, you did not follow the recommendations from the specs.

Which is why the on:bind needs to do more than just modify the open property.

danielniccoli avatar Dec 01 '23 16:12 danielniccoli

What 😅 It's not a modal, it's a dialog. It can be used in any context for whatever the given use case requires.

Just because modals may be the most common use case for dialogs, does not mean that Svelte should introduce opinionated, non-standard behavior.

brunnerh avatar Dec 01 '23 16:12 brunnerh

The problem with that is that it doesn't enable the backdrop

oh yes, for that you need to call showModal. this is how we do it:

<script>
let modal
$: if (open) modal.showModal()
</script>
<dialog bind:this={modal} {open} on:close={() => open = false}>
...
</dialog>  

not pretty, but it works

sprataa avatar Dec 01 '23 18:12 sprataa

@danielniccoli I think your confusion stems from the fact that <dialog> can actually do both behaviours. this blogpost made the difference clear to me: https://blog.webdevsimplified.com/2023-04/html-dialog/

specifically this:

dialog.show() // Opens a non-modal dialog
dialog.showModal() // Opens a modal

cheers

andreasnuesslein avatar Dec 13 '23 20:12 andreasnuesslein

I didn't know it can do both. I read in the MDN about it, and the docs were either updated, or I just remember it wrong.

danielniccoli avatar Dec 13 '23 20:12 danielniccoli

Please distinguish the two types of <dialog> usage. These are from MDN.

  1. HTML-only dialog - This example demonstrates the create a non-modal dialog by using only HTML.
  2. Creating a modal dialog - This example demonstrates a modal dialog with a gradient backdrop.

For modal usage - with the backdrop and the focus-trap - reference the official Svelte example.

It already binds the showModal boolean value to showModal() and close() the modal.

<script>
  export let showModal; // boolean
  let dialog; // HTMLDialogElement
  $: if (dialog && showModal) dialog.showModal();
</script>

<dialog
  bind:this={dialog}
  on:close={() => (showModal = false)}
  on:click|self={() => dialog.close()}
>

For a wrapper library, consider my svelte-html-modal.


Allowing bind:open in <dialog> will be intuitive since it is supported in details,

<script>
  let open = false;
  $: console.log(open);
</script>

<details bind:open>
  <summary>Details</summary>
  Something small enough to escape casual notice.
</details>

but without a proper DOM event in <dialog>, it seems undesirable.

Maybe provide a better lint result to explain why bind:open does not work?

hyunbinseo avatar Dec 14 '23 03:12 hyunbinseo

I'm using Svelte 5 RC, and the problem is still here.

Maze-fr avatar Oct 13 '24 04:10 Maze-fr

Also using the 5 release and you can get around it with the form events

<script>
  let { open = false} = $props();
</script>
 <dialog open={open}>
  <form method="dialog" onsubmit={() => {open = false}}>
    Alert Stuff!! 
    <button>Ok</button>
  </form>
</dialog>

still annoying though would love to see this pattern formalized in a library

RyanGuild avatar Oct 24 '24 22:10 RyanGuild

@RyanGuild looks like an antipattern (I believe state will desync if it's closed with ESC)

KTibow avatar Oct 24 '24 22:10 KTibow

Actually, you can't bind open on a dialog or details, as well as disabled on an input, because this is not a value by itself (like the "value" attribute of an input) and so it doesn't have a state as it's changing (I don't know if you see what I mean), because it's there or it's not there. It's value is changed programmatically only, and never by a user input which is the meaning of using bind. The only way to deal with it is to do something like open={isOpen} having let isOpen: boolean = $state(false) and change it whatever the way you want.

Also, like Sprataa said, you can do :

<script>
  let open: boolean = $stat(false);
</script>
<dialog {open}>
  ...
</dialog>

Because it's a shortcut to open={isOpen}. And it works the same for / with disabled on an input.

Maze-fr avatar Oct 25 '24 13:10 Maze-fr

For modal usage - with the backdrop and the focus-trap - reference the official Svelte example.

the link for the official Svelte example should updated https://svelte.dev/playground/modal

Benbinbin avatar Jan 16 '25 06:01 Benbinbin

@Benbinbin I have updated the link in the original comment as well. Thank you.

As for why the redirection isn't working, it seems to be intentional:

  • https://github.com/sveltejs/svelte.dev/issues/306#issue-2576465366

hyunbinseo avatar Jan 16 '25 14:01 hyunbinseo

I did it this way with svelte 5 in a reusable component that I'm building.

The required onclose handler is just to update the associated parent state.

If you spot anything wrong please tell me:

<script>
  /** @typedef {import("svelte/elements").HTMLDialogAttributes} DialogProps*/

  import { onMount } from "svelte";

  /** @type {HTMLDialogElement|null}*/
  let ref = $state(null);

  onMount(() => {
    if (typeof HTMLDialogElement === 'function') {
      console.log("The browser supports the <dialog> element and its methods.");
    } else {
      console.error("The browser does NOT support the <dialog> element.");
    }
  });

  /** 
   * @type {{
   * title:string,open?:boolean,
   * onclose:NonNullable<DialogProps["onclose"]>
   * } & DialogProps}
   */
  let { title, open = $bindable(true), children, onclose, ...rest } = $props();

  $effect(() => {
    if (open) {
      ref?.showModal();
    }
  });
</script>

<dialog
  onclose={(e) => {
    onclose(e);
  }}
  bind:this={ref}
  {...rest}
>
  {#if open}
    <h1 >{title}</h1>
    <button autofocus onclick={() => ref?.close()}>Close</button>
    {@render children?.()}
  {/if}
</dialog>

daviareias avatar Jan 26 '25 10:01 daviareias

@daviareias why require the parent to handle onclose instead of doing something like oncancel={() => open = false}

KTibow avatar Jan 26 '25 17:01 KTibow

@daviareias why require the parent to handle onclose instead of doing something like oncancel={() => open = false}

I'm still trying to figure out, but in my app I have more than one dialog that can be opened by different buttons, so I'm doing something like this on the parent:

<DialogComponent open={globalState.openedModal=="contact"}  onClose={()=>globalState.openedModal=null} />

I haven't test the cancel event, but it seems that only gets called in case the user presses esc?

daviareias avatar Jan 28 '25 10:01 daviareias