Skip to content

Instantly share code, notes, and snippets.

@rupertlssmith
Last active September 7, 2022 15:33
Show Gist options
  • Save rupertlssmith/13f48be67a7d1f1fed0cd480ec1e1f66 to your computer and use it in GitHub Desktop.
Save rupertlssmith/13f48be67a7d1f1fed0cd480ec1e1f66 to your computer and use it in GitHub Desktop.
FFI in Elm
module Main exposing (main)
import Browser
import Html exposing (Html)
import Html.Events
import Json.Decode
import Json.Encode
type alias Flags =
Json.Decode.Value
type alias Model =
{ proxy : Json.Decode.Value }
type Msg
= Refresh
main : Program Flags Model Msg
main =
Browser.element
{ init = \f -> ( { proxy = f }, Cmd.none )
, update = \_ m -> ( m, Cmd.none )
, subscriptions = \_ -> Sub.none
, view = view
}
ffiCall : Json.Decode.Value -> String -> Json.Decode.Decoder a -> Maybe a
ffiCall proxy key expecting =
proxy
|> Json.Decode.decodeValue (Json.Decode.field key expecting)
|> Result.toMaybe
view : Model -> Html Msg
view model =
Html.button
[ Html.Events.onClick Refresh ]
[ Html.text
(Debug.toString
(List.repeat 5 ()
|> List.filterMap (\_ -> ffiCall model.proxy "randomInt" Json.Decode.int)
)
)
]
<html>
<head>
<style>
/* you can style your program here */
</style>
</head>
<body>
<main></main>
<script>
var proxy = new Proxy({
randomInt() {
return Math.round(Math.random() * 10000);
}
}, {
get(target, property) {
if (typeof target[property] == 'function') {
return target[property]();
} else {
return target[property];
}
}
});
var app = Elm.Main.init({
node: document.querySelector('main'),
flags: proxy,
})
// you can use ports and stuff here
</script>
</body>
</html>
module Interop exposing (..)
import Json.Decode as Decode
import Json.Encode as Encode
import Random
import Time
type alias Interop =
{ windowSize : () -> (Int, Int)
, seed : () -> Random.Seed
, querySelector : String -> List Decode.Value
, now : () -> Time.Posix
, isLocalhost : () -> Bool
}
interop : Interop
interop =
let
interface =
Encode.object []
in
{ windowSize = \_ ->
Decode.decodeValue
(Decode.at ["__elm_interop__", "__windowSize__"] (Decode.map2 Tuple.pair (Decode.index 0 Decode.int) (Decode.index 1 Decode.int)))
interface
|> Result.withDefault (0, 0)
, isLocalhost = \_ ->
Decode.decodeValue
(Decode.at ["__elm_interop__", "window", "location", "host"] Decode.string
|> Decode.map (String.contains "localhost")
)
interface
|> Result.withDefault False
, seed = \_ ->
Decode.decodeValue
(Decode.at ["__elm_interop__", "__seed__"] (Decode.int))
interface
|> Result.withDefault 0
|> Random.initialSeed
, now = \_ ->
Decode.decodeValue
(Decode.at ["__elm_interop__", "__now__"] (Decode.int))
interface
|> Result.withDefault 0
|> Time.millisToPosix
, querySelector = \selector ->
Decode.decodeValue
(querySelector selector)
interface
|> Result.withDefault []
}
querySelector selector = (Decode.at ["__elm_interop__", "__querySelectorAll__"] (Decode.field selector (Decode.list Decode.value)))
computedStyle = Decode.at ["__elm_interop__", "__getComputedStyle__"] (Decode.dict Decode.string)
const domMonkeyPatches = () => {
// prevent files from opening when dropping them in the browser
window.addEventListener("dragover", (ev) => ev.preventDefault());
window.addEventListener("drop", (ev) => ev.preventDefault());
// preven safari pinch-to-zoom default behaviors
window.addEventListener("gesturestart", (ev) => ev.preventDefault());
window.addEventListener("gesturechange", (e) => e.preventDefault());
window.addEventListener("gestureend", (e) => e.preventDefault());
// monkey-patch HTML so that elm can read properties from HTML elements
HTMLElement.prototype.__defineGetter__("boundingClientRect", function () {
return this.getBoundingClientRect();
});
};
domMonkeyPatches();
const initSyncInterop = () => {
Object.defineProperty({}.__proto__, "__elm_interop__", {
get() {
const ctx = this;
return {
get __windowSize__() {
return [window.innerWidth, window.innerHeight];
},
get __window__() {
return window;
},
get __seed__() {
return randomNumber();
},
get __now__() {
return Date.now();
},
get __querySelectorAll__() {
return querySelectorProxy(ctx);
},
get __getComputedStyle__() {
return window.getComputedStyle(ctx);
},
};
},
});
};
const querySelectorProxy = (ctx) =>
new Proxy(
{},
{
get: (obj, key) => {
return Array.from(
(ctx.querySelectorAll && ctx.querySelectorAll(key)) ||
document.querySelectorAll(key)
);
},
has: (_, key) => {
return true;
},
}
);
initSyncInterop();
export const randomNumber = () => crypto.getRandomValues(new Uint32Array(1))[0];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment