purescripthalogen

how to componentize purescript-halogen application


I would like to write specific components of my frontend using Purescript's Halogen.

For example, I would like to create a registration form using Halogen. It would look something like below:

module RegistrationForm where

import Prelude

...

-- | The state of the application
newtype State = State { email :: String, password :: String }

derive instance genericState :: Generic State
instance showState :: Show State where show = gShow
instance eqState :: Eq State where eq = gEq

initialState :: State
initialState = State { email: "", password: "" }

-- | Inputs to the state machine
data Input a = FormSubmit a
             | UpdateEmail String a
             | UpdatePassword String a
             | NoAction a

type AppEffects eff = HalogenEffects (ajax :: AJAX, console :: CONSOLE | eff)

ui :: forall eff . Component State Input (Aff (AppEffects eff))
ui = component render eval
  where
    render :: Render State Input
    render (State state) = H.div_
        [ H.h1_ [ H.text "Register" ]
        , ...
        ]

    eval :: Eval Input State Input (Aff (AppEffects eff))
    eval (NoAction next) = pure next
    eval (FormSubmit next) = do
        ...
    eval (UpdateEmail email next) = do
        ...
    eval (UpdatePassword password next) = do
        ...

runRegistrationForm :: Eff (AppEffects ()) Unit
runRegistrationForm = runAff throwException (const (pure unit)) $ do
    { node: node, driver: driver } <- runUI ui initialState
    appendTo ".registration" node

Similarly, I have a LoginForm module that handles logging a user into the application.

I'm wondering how to go about organizing my source code, building my source code, and calling my Purescript code from Javascript?

Currently, my source code is organized like the following:

$ cd my-application/
$ tree
.
├── bower.json
├── README.md
├── site/
│   └── index.html
├── src/
│   ├── LoginForm.purs
│   └── RegistrationForm.purs
└── test/
    └── Test.purs

However, since I don't have a Main.purs, I can't do any of the following to build my source code:

$ pulp build --to site/app.js
$ pulp browserify --to site/app.js
$ pulp server

It would be nice to be able to build my purescript code into logical javascript files. For instance, src/LoginForm.purs could be built as site/loginForm.js, and src/RegistrationForm.purs could be built as site/registrationForm.js.

Then, I could include loginForm.js and registrationForm.js in my actual html pages as need-be.


Solution

  • Pulp doesn't really cover this use case, it's only intended for apps where there is a single Main.

    I'd suggest using a gulp setup to achieve this, using a gulpfile something like this:

    "use strict";
    
    var gulp = require("gulp"),
        purescript = require("gulp-purescript"),
        webpack = require("webpack-stream");
    
    var sources = [
      "src/**/*.purs",
      "bower_components/purescript-*/src/**/*.purs",
    ];
    
    var foreigns = [
      "src/**/*.js",
      "bower_components/purescript-*/src/**/*.js"
    ];
    
    gulp.task("make", function() {
      return purescript.psc({
        src: sources,
        ffi: foreigns
      });
    });
    
    var mkBundleTask = function (name, main) {
    
      gulp.task("prebundle-" + name, ["make"], function() {
        return purescript.pscBundle({
          src: "output/**/*.js",
          output: "tmp/js/" + name + ".js",
          module: main,
          main: main
        });
      });
    
      gulp.task("bundle-" + name, ["prebundle-" + name], function () {
        return gulp.src("tmp/js/" + name + ".js")
          .pipe(webpack({
            resolve: { modulesDirectories: ["node_modules"] },
            output: { filename: name + ".js" }
          }))
          .pipe(gulp.dest("site/js"));
      });
    
      return "bundle-" + name;
    };
    
    gulp.task("bundle", [
      mkBundleTask("loginForm", "LoginForm"),
      mkBundleTask("registrationForm", "RegistrationForm")
    ]);
    
    gulp.task("default", ["bundle"]);
    

    That might not be quite right, but I extracted it from how we do things with SlamData so it's definitely along the right lines.