Window.confirm without native code?

In our application we currently have a native shim to allow calls to window.confirm.

It looks like this:

window._xtian$someApp$Native_Utils_Window = {
  confirm: window.confirm
};

If we wanted to migrate away from this solution to be compatible with 0.19, what would be the best approach?

2 Likes

If I were using window.confirm, my personal preference would be to migrate to a nicer alterative for my users, since more user-friendly confirmation dialogs can be implemented using plain old DOM nodes.

Is there a reason that approach wouldn’t be possible?

2 Likes

Actually all of the user-hostile behavior that window.alert and window.confirm used to have has been improved in modern browsers. A StackOverflow post from 2010 is perhaps not the best resource for this information.

For instance: https://developers.google.com/web/updates/2018/01/nic64#window-alert

For simple use-cases, window.confirm is a cheap and easy solution that is guaranteed to be accessible and consistent with the browser UI.

So is there a good way to use window.confirm with Elm?

1 Like

Is this one of those things that can only be called if the event loop began with a user input? If so, I believe the reason ports do not work at the moment is that the current releases uses requestAnimationFrame before showing things, thereby cutting the event provenance needed for this sort of thing. Is that correct?

I modified virtual-dom to allow people to respond directly to user input events. That means:

  1. The issue where changing <input value="blah"> too quickly can get lost will be gone. It will be synchronous now. The idea being that user inputs like typing are slow enough that it’s fine if it is not synced with animation frames.
  2. If people need to do “only on user event” actions, they can do it through ports.

That required a breaking change in virtual-dom to on that will come out with 0.19. It is just a bit more flexible (to cover “passive events” as well) so any existing uses should need only tiny tweaks.

Anyway, I think that handles your other question as well, but I’m not 100% certain I diagnosed the root problem correctly in your specific cases. If it does handle the other question as well, can someone make a note over there?

4 Likes

Here’s an implementation using ports. I don’t think it triggers the issues discussed by @evancz above.

I’m not sure about the event loop question because we didn’t get to that point.

The difficulty we had with ports was that there was no way to associate the message coming back into the Elm app with the user input that triggered the window.confirm.

We could have tried generating a unique ID to send back with the boolean result of the dialog, but that felt like a lot of complex harness for a problem that could be safely solved with three lines of native code.

This is a good demonstration of the problem I describe above. If you have triggered multiple calls to window.confirm, how do you disambiguate the subscription messages coming back in from JS?

You could pass some kind of id (an Int or String) to your port, which identifies the particular confirm. Then later, when the user has confirmed or canceled, your JS code can pass back that id into your Sub port. From there, your Elm code should be able to identify it.

I think this is the main pain point people have when being forced into ports coming from native code, however there IS still a solution going the safe route with ports albeit at the expense of terseness.

Using ports would be more type-safe, but it would be much more complex overall and so I would say it’s actually less safe in general.

1 Like

Yeah this would work. What do you think would be the best way to generate this ID?

What are the different cases that can produce a confirmation popup? The structure of some applications make IDs available quite naturally, so I think we need to know more about the specifics of your case to give the best recommendation for you.

1 Like

You could do something like this! (Fork of example Ellie above):

https://ellie-app.com/bXKyKgJgBa1/1
(edit: simplified)

1 Like

You could take a look at http://package.elm-lang.org/packages/peterszerzo/elm-porter/latest … it has a nice process for associating requests and responses for ports, at the cost of a bit of setup.

1 Like

Well, the IDs in our application are opaque types currently which allows us to guarantee that they will only ever be constructed by the JSON decoders or URL parsers that are exported by our data modules. We would have to compromise on that guarantee in order to use those values to disambiguate different subscription messages.

We also have an interface where users can create different shapes by placing vertices on a graph. These shapes are children of a parent resource and multiple shapes can be in an edited or unsaved (meaning they are newly created) state at once. In order to provide a deletion confirmation dialog for these shapes via ports we would need some way of determining which unsaved shape the dialog response was relevant for.

There’s no way to trigger more than one window.confirm dialog at a time since window.confirm is a blocking function and the only way to dismiss it is through user interaction. I’m also assuming that initiating the dialog is in response to a user interaction as well. The workflow gives you this organic guarantee that you’ll always be dealing with one dialog at a time, so you don’t really need to identify messages at all. As long as you can store which resource is up for deletion you can just wait for a message UserDeletionResponded Bool from your port and then proceed.

2 Likes

I thought message handling and ports were async, though. Seems like there could be a situation where multiple port messages from window.confirm were queued at once. Is this incorrect?

Perhaps, in theory. But an app that queues multiple window.confirms at the same time seems both unlikely and pretty user-hostile. AFAIK to trigger that, you would need to have multiple requests in a single update loop iteration.

1 Like

Sure, it just feels like at this point we would be depending on convention and side-effects to produce correct behavior rather than having some technical guarantee that messages are being processed correctly. That’s an uncomfortable place to be, especially when debugging.

In any case, native code will not be available in 0.19 and a binding to window.confirm will not be included in any packages, so using a port is the path forward. You can:

  • rely on the behavior of window.confirm and your program’s UX to ensure that a dialog can only be triggered after the previous one is resolved. This is the choice that I would make personally, but I don’t work on your team and so I don’t know all of your constraints.
  • use something like a Lamport timestamp as an ID for your port messages such that you can track which message ID is the latest and discard messages that don’t match.
  • identify your window.confirm-related port messages using the ID of the related resource. If the ID type is opaque, you might include something like the following in the module that defines it:
type alias PortId = String

toPortId : Id -> PortId
toPortId (Id value) = "port:" ++ value
6 Likes