reflex-frp / reflex-dom

Web applications without callbacks or side-effects. Reflex-DOM brings the power of functional reactive programming (FRP) to the web. Build HTML and other Document Object Model (DOM) data with a pure functional interface.
https://reflex-frp.org
BSD 3-Clause "New" or "Revised" License
357 stars 145 forks source link

dyn at other position in the DOM, popup handling #94

Closed emmanueltouzery closed 7 years ago

emmanueltouzery commented 8 years ago

this was covered a bit on the side in bug #35, but I think this probably deserves its own bug. I'm not sure how to handle popups in reflex-dom, actually my feeling is that it's not well-catered for use case -- I don't understand how the existing commercial projects can make it properly with the primitives in place... I must be missing something, or possibly I have a little more specific requirements?

I make popups using the bootstrap system. I have a div with a certain ID, render the HTML I want in the div, then I call a JS function that displays that div as a popup. In normal HTML/JS, I would have a DIV at the top or bottom of the page, and reuse it -- because typically can display only one popup at a time.

Now in reflex I don't see how to from point X in the DOM (where I'm rendering the 'edit' button that'll open the popup) modify point Y (where the popup contents live). So my first approach was to generate the popup div just next to my code. But that was bloated. For instance if I display a list of 10 items, I would have in my DOM 10 popup divs, each next to its 'edit' button. The next step was to move the popup div a little higher up in the DOM, then communicating, passing up and down events, but that message-passing was really messy. In addition sometimes an action for the popup would cause re-generation of the part of the DOM where the popup HTML lived... That required careful timing to make sure the popup was fully closed before the DOM update would occur (otherwise the popup would be wiped from the DOM before it had fully closed, causing funky effects), which was messy and forced me to disable animations when closing popups so that the user wouldn't have to wait too much.

Then I came up with the solution that I currently use: https://github.com/emmanueltouzery/cigale-timesheet/blob/97de81a01b5bac2a44d238df2861a82a8b0e1d90/src/WebGui/Common.hs#L220-L247

At the time I copy-pasted dyn from reflex-dom and modified it so that it may render not at the current location within the DOM, but at any element ID. I'm super happy with this solution, although it's stringly typed. I guess I could put the element in a reader monad transformer and pass it, and so on.

Anyway now I have a problem because I'm trying to update my app to the latest reflex & reflex-dom develop versions, and the code changed in such a way that I don't manage to port my dynAtEltId.

I wonder about two things:

  1. how come I seem to be the only one with such requirements? I wonder how others handle popups?
  2. if my requirements are indeed slightly different, is there a more sound way to handle this in reflex?
cgibbard commented 8 years ago

We've used bootstrap's CSS, but never its JS stuff.

Usually we'd just make a div using elDynAttr that changes its CSS class according to a leftmost of an Event coming in as a parameter telling it to display itself, and an Event which usually is just internal to the definition telling it to close again. The open Event might contain data used to determine what we display inside the popup at the same time (e.g. by having that div contain a widgetHold based on it).

The event which opens the popup can be a leftmost/mergeWith of a collection of events coming off of your list of items.

There are a lot of variations on this depending on exactly what you're after.

emmanueltouzery commented 8 years ago

But where do you have the div for the popup in the DOM? And when do you destroy it?

cgibbard commented 8 years ago

The div for the popup would typically be somewhere toward the top level -- wherever makes sense in the structure of your program. It doesn't matter if it's at the beginning or toward the end if you're going to absolute position it with CSS. In fact, it doesn't matter where it is in the DOM tree at all really, just so long as it's somewhere, once.

You could effectively destroy it if you really want, by making all the DOM for the popup occur inside a widgetHold, and have that set the widget to blank (return ()) whenever it's closed. But widgetHold/dyn are relatively expensive, so depending on the situation if you can get away with just hiding the thing using CSS attributes, that may be better. Of course, you may have a good reason to want a widgetHold in the first place, if the various things you're popping up are sufficiently different from one another.

emmanueltouzery commented 8 years ago

Maybe I can get away with redesigning how add/edit/delete of individual items is handled in my app. currently I have the list of items in the display, I use simpleList.

displaying an item returns an event containing a payload like that:

data ConfigChange = ChangeAdd ConfigItem
                  | ChangeUpdate ConfigUpdate
                  | ChangeDelete ConfigItem
                  deriving Show

in the function to display an individual item, I have the edit button; when it's clicked, I prepare the edit popup, which sends to the server, and if the server agrees, then I trigger that ConfigChange event.

Instead, the function to display each individual item should probably return an event with a payload like:

data ConfigChange = ChangeAddRequest
                  | ChangeUpdateRequest ConfigItem
                  | ChangeDeleteRequest ConfigItem
                  deriving Show

so the item display does not handle changing, just reports to the caller the request to update, the update is done at the toplevel. If the data does change then simpleList will call the individual items again. I don't know how come I didn't do it like that from the beginning. Old bad habits I guess. Maybe that solves my issue, but maybe it does not cover all of my use cases. I'll try that, and if it does work, I'll close this bug, otherwise I'll explain more in details what is the additionnal issue.

cgibbard commented 8 years ago

Note also that you don't necessarily have to change how the requests are being made to the server in order to have the elements in the list provide events that will tell a popup to appear. That's sort of orthogonal -- the list items themselves can still make the requests to the server if you like, and maybe pass back the response (I'm guessing that the popup is supposed to appear based on the server's response?). You can transform and merge together all of those response events into one which will be used to open the popup.

Note that while it might seem a bit of an annoyance sometimes to pass events around explicitly to the widgets in your application controlling the parts of the DOM affected by the events, at the same time, it's responsible for a lot of the predictability of using the system -- you can tell which events are going to affect the display of any given widget, because they're showing up as function arguments.

mulderr commented 8 years ago

how come I seem to be the only one with such requirements? I wonder how others handle popups

I've been wondering myself. I currently do this. I took and I don't want to say improved because when I look at the code I wonder the opposite, so let's say modified reflex-dom-contrib's modal - which is also a nice minimal implementation if you want to start somewhere.

My requirements were:

There's a uiModal that always stays in the DOM and uiRemovingModal that widgetHolds internally. The signatures are slightly different - I think @mightybyte explains that in reflex-dom-contrib.

Some more remarks:

ali-abrar commented 7 years ago

This might be what you're looking for: #159

emmanueltouzery commented 7 years ago

@ali-abrar oh nice! for my issue here in the end i've modified my code to bring the events to the top of the DOM, and change the model there too, as I described there: https://github.com/reflex-frp/reflex-dom/issues/94#issuecomment-244541080 it was a significant refactor of my app though.

but now if there is something builtin, even better. the fact the CSS & details are hardcoded could be detrimental if someone wishes to use for instance bootstrap modals or such, but it's all workable I guess.

anyway, since my specific issue was sorted out, and this topic has been handled elsewhere, I'm closing this bug now.