I'm working on an React app, my first one, and I'm struggling with how to implement user-interactive popovers. I have a business requirement to have a popover with yes/no buttons in it. This popover is triggered by a button ("pobtn") that lives in a row in a table in a view:
+- View ---------------------+ +- View ---------------------+
| +- Table ----------------+ | click | +- Table ----------------+ |
| | +- Row --------------+ | | --> | | +- Row --------------+ | |
| | | ... | | | pobtn | | | ... | | |
| | +--------------------+ | | | | +--------------------+ | |
| | +- Row --------------+ | | ,--------. +- Row --------------+ | |
| | | ... [pobtn] | | | | Yes/No |>| ... [pobtn] | | |
| | +--------------------+ | | `--------' +--------------------+ | |
| | +- Row --------------+ | | | | +- Row --------------+ | |
| | | ... | | | | | | ... | | |
| | +--------------------+ | | | | +--------------------+ | |
| | ... | | | | ... | |
| +------------------------+ | | +------------------------+ |
+----------------------------+ +----------------------------+
The View, Table and Row are all components. Table data is fetched in View and pushed to Table via props, and that trickles down into Row, which I understand is "the right way" to do it, and makes sense. There isn't much local state there.
The trouble comes with the popover. The "right way" to do it would seem to include a Popover component along side each button and push props down for hidden/visible, I guess. This doesn't seem super great but is closer to the right way to do it in my mind.
However, the react-bootstrap popover (which I used because time) doesn't seem to work this way:
var content = <MyRowPopoverContent/>;
<OverlayTrigger trigger='click' placement='left' overlay={<Popover>{content}</Popover>
<button className="btn btn-primary small"><i className="fa fa-trash-o"></i> Delete</button>
</OverlayTrigger>
When you click the trigger button, it creates a DIV attached to the body and positions itself next to the triggering element, similar to how it works normally (non-react). It looks like I might be able to attach the DIV to another container, but that isn't documented very well. Anyway, the overlay content (i.e., MyRowPopoverContent) is something like:
this.handleNoClick: function(){
// Re-click the button to close; gross.
$('[data-row-id="'+this.props.row.key+'"]').find('.fa-trash-o').parents('button').first().click();
},
this.handleYesClick: function(){
// Do stuff...update store which would re-render the view.
// Re-click the button to close; gross.
$('[data-row-id="'+this.props.row.key+'"]').find('.fa-trash-o').parents('button').first().click();
},
this.render: function(){
<div>
<div>Are you sure?</div>
<input placeholder="Reason"/>
<button onClick={this.handleYesClick}>Yes</button> <button onClick={this.handleNoClick}>No</button>
</div>
}
I started implementing the handleYesClick
function, trying to do error handling, asynchronous events, etc., and it all seems really sketchy, or at least fragile. I guess the question is: I need an interactive popover, how do I do that? It seems like the popup would be part of the local state of the button or row, or maybe this creeps up to the top as most things seem to do.
Since react-bootstrap already provides one, it would be nice to use that, but I'm not sure how to "hook it up" with everything else.
Update: If it were a simple static popover that would be cake; showing/hiding it via props and clicking pobtn
is pretty trivial. The part I struggle with is the interactive content within the popover--having that do something (like an event to update the store), show a spinner, determine if that something worked, show an error message or disappear. Even clicking the "No" button and making the popover disappear isn't clear (I currently find it with jQuery and .click()
the button again.) Doing that all via props from the parent seems scary...
Suppose you encapsulate your popover into a
PopoverMenu
component. Here's a React-friendly pattern where a parent handles its child's events:The
PopoverMenu
component is stateless; it knows just enough to render. Its handler receives the click event and then adjustsPopoverMenu
's props as needed. The magic is in.bind
, which allows you to inject caller-specific information for the handler to react to. (You can imagine that the.bind
occurs in amap
call whereYes
and{row :2}
are dynamic variables.) Note that you can handle all popover choices with a single handler in the parent, and that you can easily track, as part of a parent's state (e.g. Row), the waiting/success/failure state of any calls. This may seem scary but said logic needs to live somewhere :) and it's better if that somewhere is not the child, since the React way is stateless children. State naturally becomes props as info flows from parent to child.That said, if your parent is doing too much book-keeping and you aren't getting clean, self-contained behavior from the child components, you can push logic down into the children, usually at the cost of child reusability. It is both reasonable and possible for the child to handle its own open/close state and waiting/error/success state. If you go this route, one pattern which you can use is to put the child in a waiting state until and unless you receive a matching value from the parent via props. For example, if the user makes a menu selection 'X' that requires a callback, the child does
setState{ selection: 'X'}
and waits for the parent to corroborate via props (which reflect the state of the store):Regarding the open/close status of the popover, it would be easier if you instead used a component like
ButtonDropdown
, which manages its own open/close state. (You may have to set an emptyonSelect
handler on the dropdown for it to close automatically upon selection.)