George
George

Reputation: 141

Elm + Masonry.js

I'm trying to get an Elm app integrated with Masonry.js via ports but I'm having trouble trying to figure out how to get a Signal Html to trigger the port that tells Masonry.js to redraw the view.

I'm using StartApp and I'm not sure how to get a Signal that the view has finished re-rendering from a update call.

Alternate libraries that may work better with Elm or a fully Elm solutions would be appreciated too.

More detail about the overall problem that I'm trying to solve: I have a series of images and I want to tile in in a masonry format (http://masonry.desandro.com/). They're represented by a list of objects in Elm which are converted to a list of divs in the view (with background-image set appropriately), but the images are of different sizes, hence the desire to tile them nicely. I'm using StartApp (http://package.elm-lang.org/packages/evancz/start-app/2.0.2/) to abstract the actual rendering of the html.

Upvotes: 2

Views: 449

Answers (1)

Chad Gilbert
Chad Gilbert

Reputation: 36375

You can use ports in Elm to communicate with javascript in order to publish and subscribe to events in both directions. Let's build an example where a list of images is displayed with masonry layout, and clicking an image will remove it and trigger masonry to layout the remaining images.

Since the Elm app will send multiple types of events to javascript, we can create a single port that sends strings that javascript can then act upon. These strings will be commands that we can interpret in javascript in order to tell masonry to do certain things, like "initialize" and "imageRemoved". This port will also need a mailbox to which we can send messages from inside Elm.

masonryMailbox : Signal.Mailbox String
masonryMailbox =
  Signal.mailbox ""

port masonryCommands : Signal String
port masonryCommands =
  masonryMailbox.signal

Since you're using StartApp, you can return Effects in your update function, so let's create a function that will send messages to this mailbox from both the StartApp initializer and from inside the update function.

sendMasonryCommand : String -> Effects.Effects Action
sendMasonryCommand cmd =
  let
    task =
      Signal.send masonryMailbox.address cmd
        `Task.andThen` \_ -> Task.succeed NoOp
  in
    Effects.task task

You can send the "initialize" command during the StartApp init function like this:

init =
  (initialModel, sendMasonryCommand "initialize")

In the update function, if we have a RemoveImage String action, we can send an "imageRemove" command to javascript:

update action model =
  case action of
    NoOp ->
      (model, Effects.none)
    RemoveImage url ->
      let model' =
        { model
        | images = List.filter ((/=) url) model.images
        , message = "Removing image " ++ url
        }
      in (model', sendMasonryCommand "imageRemoved")

Now we need to wire up the javascript side of things to listen for these events. If javascript gets the command "initialize", then we can wire up masonry. If we get the "imageRemoved" command, we can just tell masonry to trigger the layout command again.

var app = Elm.fullscreen(Elm.Main);
app.ports.masonryCommands.subscribe(function(cmd) {
  var $grid = $('.grid')

  if (cmd === "initialize") {
    $grid.masonry({
      itemSelector: '.grid-item',
      percentPosition: true,
      columnWidth: '.grid-sizer'
    });

    $grid.imagesLoaded().progress( function() {
      $grid.masonry();
    });  
  } else if (cmd === "imageRemoved") {
    $grid.masonry();
  }
});

We can also wire up ports to send events back into Elm. Let's add onto the example by sending a message to Elm every time masonry completes its rendering. First we'll create a port called setMessage.

port setMessage : Signal String

Since this is a port that javascript will be publishing to, we only define the function signature in Elm, not the function itself. Instead, we have to give the signal an initial value from the javascript side when we call Elm.fullscreen(). The javascript changes to this:

var app = Elm.fullscreen(Elm.Main, { setMessage: "" });

Now, inside the javascript block that handles the "initialize" command, you can wire up masonry's layoutComplete function to send a message to this new port.

$grid.on("layoutComplete", function() {
  app.ports.setMessage.send("Masonry layout complete!");
});

In order to consume these messages from the setMessage port in Elm, you'll need a SetMessage String action and you'll need to map the signal inside the StartApp inputs list. Your StartApp initializer code will look like this:

app =
  StartApp.start
    { init = init
    , view = view
    , update = update
    , inputs = [ Signal.map SetMessage setMessage ]
    }

I've provided a complete working example of all of this in a few gists. Here is a gist containing the Elm code and here is a gist containing the html and javascript.

Upvotes: 7

Related Questions