aboutsummaryrefslogtreecommitdiff
path: root/src/ffi.mjs
blob: 0fed3b131a875e8c511aeaabaf9afcb10af67642 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import * as React from 'react'
import * as ReactDOM from 'react-dom'

import * as Gleam from './gleam.mjs'
import * as Cmd from './lustre/cmd.mjs'

// -----------------------------------------------------------------------------

export const mount = ({ init, update, render }, selector) => {
    const root = document.querySelector(selector)

    if (!root) {
        console.warn([
            '[lustre] Oops, it looks like I couldn\'t find an element on the ',
            'page matching the selector "' + selector + '".',
            '',
            'Hint: make sure you aren\'t running your script before the rest of ',
            'the HTML document has been parsed! you can add the `defer` attribute ',
            'to your script tag to make sure that can\'t happen.'
        ].join('\n'))

        return new Gleam.Error()
    }

    // OK this looks sus, what's going on here? We want to be able to return the
    // dispatch function given to us from `useReducer` so that the rest of our
    // Gleam code *outside* the application and dispatch commands.
    //
    // That's great but because `useReducer` is part of the callback passed to
    // `createElement` we can't just save it to a variable and return it. We
    // immediately render the app, so this all happens synchronously, so there's
    // no chance of accidentally returning `null` and our Gleam code consuming it
    // as if it was a function.
    let dispatch = null

    const App = React.createElement(() => {
        const [[state, cmds], $dispatch] = React.useReducer(([state, _], action) => update(state, action), init)
        const el = render(state)

        if (dispatch === null) dispatch = $dispatch

        React.useEffect(() => {
            for (const cmd of Cmd.to_list(cmds)) {
                cmd($dispatch)
            }
        })

        return typeof el == 'string'
            ? el
            : el($dispatch)
    })

    ReactDOM.render(App, root)

    return new Gleam.Ok(dispatch)
}

// -----------------------------------------------------------------------------

export const node = (tag, attributes, children) => (dispatch) => {
    return React.createElement(tag, toProps(attributes, dispatch),
        // Recursively pass down the dispatch function to all children. Text nodes
        // – constructed below – aren't functions
        ...children.toArray().map(child => typeof child === 'function'
            ? child(dispatch)
            : child
        )
    )
}

export const stateful = (init, render) => (dispatch) => {
    const [state, setState] = React.useState(init)

    return render(state, setState)(dispatch)
}

export const fragment = (children) => (dispatch) => {
    return React.createElement(React.Fragment, null,
        ...children.toArray().map(child => typeof child === 'function'
            ? child(dispatch)
            : child
        )
    )
}

// This is just an identity function! We need it because we want to trick Gleam
// into converting a `String` into an `Element(action)` .
export const text = (content) => content

// -----------------------------------------------------------------------------

export const map = (element, f) => (dispatch) => {
    return element(action => dispatch(f(action)))
}

// React expects an object of "props" where the keys are attribute names
// like "class" or "onClick" and their corresponding values. Lustre's API
// works with lists, though.
//
// The snippet below converts our Gleam list of attributes to a JavaScript
// array of key/value pairs, and then a plain ol' object.
export const toProps = (attributes, dispatch) => {
    const capitalise = s => s && s[0].toUpperCase() + s.slice(1)

    return Object.fromEntries(
        attributes.toArray().map(attr => {
            switch (attr.constructor.name) {
                case "Attribute":
                case "Property":
                    return [attr.name, attr.value]

                case "Event":
                    return ['on' + capitalise(attr.name), (e) => attr.handler(e, dispatch)]

                // This should Never Happen™️ but if it does we don't want everything
                // to explode, so we'll print a friendly error, ignore the attribute
                // and carry on as normal.
                default: {
                    console.warn([
                        '[lustre] Oops, I\'m not sure how to handle attributes with ',
                        'the type "' + attr.constructor.name + '". Did you try calling ',
                        'this function from JavaScript by mistake?',
                        '',
                        'If not, it might be an error in lustre itself. Please open ',
                        'an issue at https://github.com/hayleigh-dot-dev/gleam-lustre/issues'
                    ].join('\n'))

                    return []
                }
            }
        })
    )
}

// -----------------------------------------------------------------------------

export const object = (entries) => Object.fromEntries(entries)