react-bootstrapscalajs-react

How would I build a Bootstrap React Modal using Scala Js React


How would I create a Modal component from the React Bootstrap library using the Scala.js React library?


Solution

  • The fact that a Modal has to change state (show = true/false) to show/hide the dialog made the solution non-trivial. I resolved this by wrapping it in a component that had a Boolean state that could be changed - and when it needs to be shown/hidden I change state with effects impure.

    Another issue was that if the Modal has buttons that need to change the state their event handlers need access to this state somehow. I resolved this issue by giving users of the component access to the Backend of the component on creation.

    Here is my implementation of the Modal:

    class Modal(bs: BackendScope[Unit, Boolean], onHide: => Unit, children: Modal => Seq[ChildArg]) {
    
      def render(show: Boolean) = {
        val props = (new js.Object).asInstanceOf[Modal.Props]
        props.show = show
        props.onHide = () => {
            dismiss()
            onHide
        }
    
        Modal.component(props)(children(this): _*)
      }
    
      def dismiss() = {
        bs.withEffectsImpure.setState(false)
      }
    }
    
    object Modal {
    
      @JSImport("react-bootstrap", "Modal")
      @js.native
      object RawComponent extends js.Object
    
      @js.native
      trait Props extends js.Object {
        var show: Boolean = js.native
        var onHide: js.Function0[Unit] = js.native
      }
    
      val component = JsComponent[Props, Children.Varargs, Null](RawComponent)
    
      type Unmounted = Scala.Unmounted[Unit, Boolean, Modal]
    
      def apply(onHide: => Unit = Unit)(children: Modal => Seq[ChildArg]): Unmounted =   {
        val component = ScalaComponent.builder[Unit]("Modal")
        .initialState(true)
        .backend(new Modal(_, onHide, children))
        .renderBackend
        .build
        component()
      }
    }
    

    And a Dialog object that uses it:

    object Dialog {
      object Response extends Enumeration {
        type Response = Value
        val OK, CANCEL = Value
      }
    
      import Response._
    
      def prompt(title: String, body: String, okText: String): Future[Response] = {
    
        // Add an element into which we render the dialog
        val element = dom.document.body.appendChild(div(id := "dialog").render).asInstanceOf[Element]
    
        // Create a promise of a return and a method to send it
        val p = Promise[Response]
        def respond(ret: Response) = {
            // Remove the containing element and return the response
            dom.document.body.removeChild(element)
            p.success(ret)
        }
    
        Modal(respond(Response.CANCEL)) { modal =>
    
            // Function to dismiss the dialog and respond
            def quit(ret: Response) = {
                modal.dismiss()
                respond(ret)
            }
    
            // Create the components for our Modal
            Seq(
                ModalHeader(true,
                    ModalTitle(title)
                ),
                ModalBody(body),
                ModalFooter(
                    Button(variant = "secondary", onClick = () => { quit(Response.CANCEL) })("Cancel"),
                    Button(variant = "primary", onClick = () => { quit(Response.OK) })(okText)
                )
            )
        }.renderIntoDOM(element).backend
    
        p.future
      }
    }