SimpleJs - Calling JS from Elm

Hello!

As there are from time to time discussions about calling JS from Elm, I uploaded my approach to Github: https://github.com/hans-helmut/SimpleJs

This is my API:

callJs :
    (Json.Decode.Value -> msg)
    -> List String
    -> List Json.Encode.Value
    -> { a | simplejs : Model msg }
    -> ( { a | simplejs : Model msg }, Cmd msg )
  • A (An anonymous) function, taking the returned value of the JS function and creating a message
  • The name of your JS function as list of strings. In case your JS function has a dot-separated name, like a.b.c, use ["a","b","c"]
  • A list containing a Json.Encode.Value for each of the expected function parameters
  • And your global model.

So calling this JavaScript

console.log("One", 2, 3.4)

is in Elm:

SimpleJs.callJs
        (\val -> NoOp)
        -- JS-function
        [ "console", "log" ]
        --JS params
        [ Json.Encode.string "One"
        , Json.Encode.int 2
        , Json.Encode.float 3.4
        ]
       model

I am also interested to have a better process for building. Currently it uses a shell-script.

8 Likes

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.

2 Likes

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.

3 Likes

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:

  1. JS bugs stay in JS
    • Ports make debugging especially simple because you can unit test each port.
  2. 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.
  3. 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.

Well, after seeing “The Importance of Ports” by Murphy Randle I decided to use only two ports. Later I simplified the API.

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.

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.