Problem
I need some way to enable some JavaScript process to consume the stream of messages output by a Halogen component. In other words, the ‘parent’ of the component isn’t yet another Halogen component, it’s an executing script running in the browser, which might be a React component or something similar.
A WIP solution is here:
thomashoneyman/purescript-halogen-leaf
Background
At my company we’ve largely replaced a large Angular application with Halogen.
At first, my team mounted Halogen components within otherwise Angular-controlled pages. The components didn’t communicate with Angular except through shared resources (local storage, the API, etc). As far as the Javascript was concerned there was just some chunk of code executed and left to its own devices.
Now, though, other teams within our parent company would like to use the various Halogen components we’ve built. Since they won’t be inside a larger Halogen application, we really do have to communicate from the Halogen component to some framework-agnostic consumer in JavaScript.
Attempted Solution
A Halogen component, once run with something like runUI
, produces the HalogenIO
type:
type HalogenIO f o m =
{ query :: f ~> m
, subscribe :: Consumer o m Unit -> m Unit
}
query
enables some external source to trigger queries in the root component, and subscribe
allows some external consumer to receive messages from the root component.
Exactly what we want! All I have to do is construct some Consumer o m Unit
, which can be done by providing the consumer
function from purescript-coroutines
with a function (o -> m (Maybe Unit))
. Then, every time some output is produced by the root, the handler will run, and we’re good to go.
Our app main function might look like this:
main
:: ∀ f i o
. H.Component HH.HTML f i o Aff
-> i
-> String
-> (o -> Aff (Maybe Unit))
-> Effect Unit
main component input querySelector = HA.runHalogenAff do
element <- HA.selectElement (QuerySelector querySelector)
io <- traverse (runUI component input) element
traverse_ (\i -> i.subscribe $ consumer handler) io
pure unit
Next, I turned to the JavaScript to see what that might look like. While I couldn’t quite figure out how to provide that o -> Aff (Maybe Unit)
function, this seems like a nice minimal bit of vanilla JS to play around with for testing:
import { main } from './halogen/output/Main/index.js';
import { component } from './halogen/output/Component/index.js';
// Create a div to hold the Halogen component and mount the
// component with its arguments.
function mkComponent(args) {
let element = document.createElement('div');
let querySelector = "#" + args.id;
element.setAttribute("id", args.id);
document.body.appendChild(element);
// Mount the Halogen component
main(args.component)(args.input)(querySelector)();
}
mkComponent({component, input: {initialText: "hello"}, id: "component"});
Problems
Unfortunately, this attempt has some problems already:
-
I don’t actually know how to produce the function
o -> Aff (Maybe Unit)
and pass it in. It’s the responsibility of the folks using the component to write it, so we can’t write it on their behalf. And with #2, perhaps this isn’t even a reasonable approach. -
We’ve lost the ability to query the component, which I’m sure will later become important. It seems like a better approach would be just to return the
{ subscribe, query }
record and allow the caller to leverage either function as they see fit (callsubscribe
with some JavaScript promise or something to consume values, and callquery
to trigger queries in the component).
So far I haven’t been able to get either #1 or #2 to work, but I’m continuing to push on #2 as the more promising approach.
If this sparks any ideas among y’all, please share! Otherwise, I’ll update when I have more to say about this.
API Design
[excerpted from Slack]
The simplest API design that I think would be usable as a starting point would be a single function, mount
, which takes a component, the component’s input, a query selector at which to mount the component, and some sort of handler which can process the outputs. Then, the work to actually call this handler on every output is done within Halogen and it’s a bit simpler to implement.
A more complex but perhaps nicer one might be to instantiate an object representing the component, where you can call the query()
method to query the component and the subscribe()
method to subscribe to updates (still not sure how to do this).