How to model FFI of code that needs part of the DOM and let it work with halogen?

Hello,

I’m trying to write a FFI for twitter bootstrap modal javascript code.

Documentation: https://getbootstrap.com/docs/4.4/components/modal/
Code: https://github.com/thednp/bootstrap.native/blob/master/src/components/modal-native.js

I’m not using the original code which is a jQuery module, but javascript native code.

So far i wrote this (didn’t test it yet)

module Bootstrap.Modal where

import Web.HTML.HTMLElement (HTMLElement)
import Data.Set (Set)
import Effect (Effect)
import Data.Unit (Unit)

foreign import data Modal :: Type

data Option
  = OBackdrop Backdrop
  | Keyboard Boolean
  | Focus Boolean
  | Show Boolean

data Backdrop
  = True
  | False
  | Static

foreign import new :: HTMLElement -> Set Option -> Effect Modal

foreign import show :: Modal -> Effect Unit

and

"use strict";

var bsn = require('bootstrap.native/dist/bootstrap-native-v4');

exports.new = (element, options) => {
  console.log(element, options);

  return () => {
    return new bsn.Modal(element,options);
  }
};

exports.show = modal => {
  return (modal) => {
    modal.Modal('show');
  }
}

Any comment on this so far would already be appreciated. Now onto the question …

I’m using halogen and i have the modal as halogen types for the view. However the new() method i made requires an HTMLElement (because the underlying javascript code requires an element which is the html of the model). To be able to reference this HTMLElement between view renders (because i need it to call actions on it), i need to put it in my Application state. Right now the new function returns type Modal which is a placeholder for the moment, but i need access to HTMLElement somehow it seems. Following a straight forwarded approach leads me to this design.

This feels very strange to me having an HTMLElement in my application State. I feel like maybe i should wrap it in some special type and then let the HTMLElement be hidden somehow. Perhaps it shouldn’t even be stored in the purescript side but be hidden somehow in the javascript code.

What’s the best way to model this modal? And keep good separation between state and view?

You can attach a HP.ref property to the element and then acquire the element in HalogenM with getHTMLElementRef. Sometime it does make sense to have it in the State, but otherwise you could just re-acquire it with getHTMLElementRef each time you need to use it. :slightly_smiling_face:

1 Like

Thanks garry!

What is a good place in the code to initialize (execute the modal javascript code with the new function) onto the HTMLElement?

I tried doing it in the intial state but it seems that i can’t quite work there with the HalogenM monad.

You can set up an initialize action in the mkEval options record - this action will be raised inside the component immediately after it has been rendered with the initialState (since rendering is strict we can’t wait for an action to complete here, so often this means you’ll end up with a Maybe in your state for the element or other-effectful-thing you need to set up in component init).

Take a look at the Ace example, this is quite similar to what you’re trying to do, as the component just renders a div with a ref that it then uses to initialize the Ace editor within: https://github.com/purescript-halogen/purescript-halogen/blob/7bb46c742c51b9cb32a6e798bf3e1336e4f3b463/examples/ace/src/AceComponent.purs

Hi @garyb i understood the first part that rendering needs to be done once before we can call an action on the DOM elements.

I looked at the ACE example and implemented the initialize action successfully. I noticed that in the Ace example the editor is stored in the application state. At the moment i am not doing this and instead try to grab the reference again.

getModalElem :: forall surface action slots output m. H.HalogenM surface action slots output m HTMLElement
getModalElem = (unsafePartial $ fromJust) <$> (getHTMLElementRef $ RefLabel "modal")

handleAction ∷ forall o m. MonadEffect m => Action → H.HalogenM State Action () o m Unit
handleAction = case _ of
  Initialize -> do
    modal_elem <- getModalElem
    H.liftEffect $ BSM.new modal_elem Set.empty
    pure unit

  OpenModal -> trace "open modal" \_ -> do
    modal_elem <- getModalElem
    H.liftEffect $ BSM.show2 modal_elem
    pure unit

And then in the FFI code

exports.new = (element) => {
  return (options) => {
    return () => {
      let modal = new bsn.Modal(element, options);
      console.log(modal)
      modal.show()
      return {}
    }
  }
};

exports.show2 = modal_elem => {
  console.log(modal_elem); // ok got an HTMLElement here
  return () => {
    console.log(modal_elem); // undefined
    modal.show();
  }
}

This code opens the modal on page load (as a test). But then when clicking a button to show the modal the modal element is undefined at the point when the effect is executed.

I think that like in the ACE example i should store the javascript object that is constructed from the HTMLElement. So return modal; instead of return {};, otherwise i have to construct this again later and possibly have a memory leak.

However i’m still puzzled why the HTMLElement is no longer available in the inner function of show2(). I feel like this could also pose a problem later when i would store the modal into the application state and the underlying HTMLElement disappears.

At the moment i don’t understand why the HTMLElement is no longer there as a i attached the reference to it. Does the reference not guarantee that the HTMLElement is not destroyed? Or should i grab the reference again from the javascript side of things?

I now store the modal into the application state

handleAction ∷ forall o m. MonadEffect m => Action → H.HalogenM State Action () o m Unit
handleAction = case _ of
  Initialize -> do
    modal_elem <- getModalElem
    modal <- H.liftEffect $ BSM.new modal_elem (Set.fromFoldable [BSM.Keyboard true, BSM.Backdrop BSM.BD_True])
    H.modify_ (_ { modal = Just modal })
    pure unit

  OpenModal -> trace "open modal" \_ -> do
    -- No longer grabbing the reference:
    -- modal_elem <- getModalElem
    -- H.liftEffect $ BSM.show2 modal_elem
    -- Instead using the application state:
    modal <- (unsafePartial $ fromJust) <$> (H.gets _.modal)
    H.liftEffect $ BSM.show2 modal
    pure unit

And JS

exports.new = element => {
  return options => {
    return () => {
      let modal = new bsn.Modal(element, options);
      modal.show()
      return modal
    }
  }
};

exports.show2 = modal => {
  console.log(modal); // Object { keyboard: true, backdrop: true, animation: true, content: undefined, toggle: toggle(), show: show(), hide: hide(), setContent: setContent(content), update: update() }
  return modal => {
    console.log(modal); // modal is undefined here
    modal.show();
  }
}

This didn’t improve the situation, the effect no longer has access to the modal. @garyb i would really appreciate any hints you could give me. I’m stuck on this issue for continuing the project.

It looks like you have a mistake in your JS, show2 should be:

exports.show2 = modal => {
  console.log(modal);
  return () => {
    console.log(modal);
    modal.show();
  }
}

In the code you pasted the effect thunk is rebinding modal.

1 Like

Sweet, that works, thanks a lot gary !

1 Like