I like how your call API streamlines the bridging between JS and Elm. However, I think one of the greatest advantages of Elm is constrained Foreign Function Interface through ports. The extra steps it takes to
declare a port in both Elm and JS really asks people to reflect on their FFI design and if ports are necessary or the best approach in the first place. This is especially crucial when the foreign function does a lot of uncertain side effects. Considering that the Elm Core library is adding more capabilities to interact with the web api through Tasks, many things that are done in JS will eventually migrate to Elm native approaches. Keeping everything in standard ports make this transition easier.
My API uses also Elm-ports to sending messages to an other process (webworker) and is not syncronized. As a webworker is used, some bad side-effects are not possible, like manipulating the DOM or taking to much processing time and hanging the UI. Running without a webworker of course, (possible, but not with the uploaded code) could slow down the UI or change the DOM.
Due to the fact, that Elm is running as single process (also a Javascript limit), in my opinion a FFI with a limitation allowing only async JS functions (and messages), to hold some promises ELM gives, is fine, but making it just difficult to get the programmers thinking is not “polite”. Inter-operation by sending messsages is a very old concept, not so common in the development in the “front end”, but appearing now with websockets, notifications and in the communication with webworkers. Just bringing bad architectures from JS to Elm does not improve anything. Last time I checked even rendering with Elm’s own Markdown could slow down a UI, so it’s good to have processes in the web now, and such calculation could run in a webworker. But that’s maybe a bad example, as JS is called synchronous by the kernel, as far as I know.
I made the API a little bit simpler, as I had to use a lot of JS functions (external, not only the browser-API, as in the given examples) and want not to patch my code handling the port on JS side for each time I add a function. On the other hand I added some difficulties to the building process. Linking (compressing and bundling) for the browser is really hard for me.
Can you share some examples where you use a lot of JS calls? I have built several Elm apps using external JS editors and calls WebAssembly functions but the number of ports are usually kept to between 10 and 15.
I just want to create a really simple CRUD app (in my spare time) as CMS for a blog. For JS I used before only 2 ports, as suggested in some youtube video, but I experienced the mentioned problems as I had always to change the “dispatcher” on the JS side. As I decided to use PouchDB, there are called to only 5 JS-functions so far, but is still incomplete. Additional there are Asciidoctor.js and the API for the localization of date and time.
I see. I don’t have experience in databases but I had used external JS markdown parser. My suggestion for both is to separate the logic of Elm and JS with well-defined ports. Here are some advantages with that approach:
JS bugs stay in JS
Ports make debugging especially simple because you can unit test each port.
Refactor with confidence
As your projects grow, you may find a better implementation or library. Ports allow you to keep your elm code exactly the same while you refactor your JS code.
Clear semantics and native feeling
Ports are strongly-typed just like other Elm code
Ports feel like native Elm functions
If you want more guarantees on the JS side, checkout elm-typescript-interop which automatically generate type declarations for Elm ports.
As the port itself is as type save as JS, because the param is just a Value. JS can send a String, while the decoder expects an Int. The decoder will build the border. Same in my API:
SimpleJs.callJs
(\val ->
GotRoot
(Result.withDefault
0
(Json.Decode.decodeValue
Json.Decode.float
val
)
)
)
-- JS-function
[ "Math", "sqrt" ]
--JS params
[ Json.Encode.float n
]
--model
model
In case the Json.Decode.decodeValue fails, you can do what every you need.
Testing is still possible, not on port-level, but on function-level.
For refactoring, I prefer doing it in Elm and not in JS.