aboutsummaryrefslogtreecommitdiff
path: root/src/lustre.ffi.mjs
blob: 47137508dc52ff8e770034a7616a85b4547167a3 (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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import * as Cmd from "./lustre/cmd.mjs";
import * as React from "react";
import * as ReactDOM from "react-dom/client";

const Dispatcher = React.createContext(null);

export const mount = (app, selector) => {
  const el = document.querySelector(selector);

  if (!el) {
    console.warn(
      [
        "[lustre] Oops, it looks like I couldnt find an element on the ",
        'page matching the selector "' + selector + '".',
        "",
        "Hint: make sure you arent 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 cant happen.",
      ].join("\n")
    );

    return Promise.reject();
  }

  let dispatchRef = null;
  let dispatchPromise = new Promise((resolve) => (dispatchRef = resolve));

  ReactDOM.createRoot(el).render(
    React.createElement(
      React.StrictMode,
      null,
      React.createElement(
        React.forwardRef((_, ref) => {
          // When wrapped in `<React.StrictMode />` and when in development
          // mode, React will run effects (and some other hooks) twice to
          // help us debug potential issues that arise from impurity.
          //
          // This is a problem for our cmds because they are intentionally
          // impure. We can/should expect user code to be pure, but we want
          // to allow top-level impurity in the form of cmds.
          //
          // So we can keep the benefits of strict mode, we add an additional
          // bit of state to track whether we need to run the cmds we have or
          // not.
          const [shouldRunCmds, setShouldRunCmds] = React.useState(true);
          const [[state, cmds], dispatch] = React.useReducer((state, msg) => {
            // Every time we call the user's update function we'll get back a
            // new lot of cmds to run, so we need to set this flag to true to
            // let our `useEffect` know it can run them!
            setShouldRunCmds(true);
            return app.update(state, msg);
          }, app.init);

          React.useImperativeHandle(ref, () => dispatch, [dispatch]);
          React.useEffect(() => {
            if (shouldRunCmds && cmds) {
              for (const cmd of Cmd.to_list(cmds)) {
                cmd(dispatch);
              }

              // Once we've performed the side effects, we'll toggle this flag
              // back to false so we don't run them again on subsequent renders
              // or during development.
              setShouldRunCmds(false);
            }
          }, [cmds, shouldRunCmds]);

          return React.createElement(
            Dispatcher.Provider,
            { value: dispatch },
            React.createElement(({ state }) => app.render(state), { state })
          );
        }),
        { ref: dispatchRef }
      )
    )
  );

  return dispatchPromise;
};

// ELEMENTS --------------------------------------------------------------------

//
export const node = (tag, attributes, children) => {
  const dispatch = React.useContext(Dispatcher);
  const props = to_props(attributes, dispatch);

  try {
    return React.createElement(tag, props, ...children.toArray());
  } catch (_) {
    console.warn([
      "[lustre] Something went wrong while trying to render a node with the ",
      'tag "' + tag + "\". To prevent a runtime crash, I'm going to render an ",
      "empty text node instead.",
      "",
      "Hint: make sure you arent trying to render a node with a tag that ",
      "is compatible with the renderer you are using. For example, you can't ",
      'render a "div" node with the terminal renderer.',
      "",
      "If you think this might be a bug, please open an issue at ",
      "https://github.com/hayleigh-dot-dev/gleam-lustre/issues",
    ]);
    return "";
  }
};

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

  return React.createElement(() => render(state, setState));
};

//
export const text = (content) => content;

//
export const fragment = (children) => {
  return React.createElement(React.Fragment, {}, ...children.toArray());
};

//
export const map = (element, f) =>
  React.createElement(() => {
    const dispatch = React.useContext(Dispatcher);
    const mappedDispatch = React.useCallback(
      (msg) => dispatch(f(msg)),
      [dispatch]
    );

    return React.createElement(
      Dispatcher.Provider,
      { value: mappedDispatch },
      React.createElement(element)
    );
  });

// HOOKS -----------------------------------------------------------------------

export const useLustreInternalDispatch = () => {
  return React.useContext(Dispatcher);
};

// UTILS -----------------------------------------------------------------------

// This function takes a Gleam `List` of key/value pairs (in the form of a Gleam
// tuple, which is in turn a JavaScript array) and converts it into a normal
// JavaScript object.
//
export const to_object = (entries) => Object.fromEntries(entries.toArray());

const capitalise = (s = "") => s[0].toUpperCase() + s.slice(1);
const to_props = (attributes, dispatch) => {
  return attributes.toArray().reduce((props, attr) => {
    // The constructors for the `Attribute` type are not public in the
    // gleam source to prevent users from constructing them directly.
    // This has the unfortunate side effect of not letting us `instanceof`
    // the constructors to pattern match on them and instead we have to
    // rely on the structure to work out what kind of attribute it is.
    //
    if ("name" in attr && "value" in attr) {
      const prop =
        attr.name in props && typeof props[attr.name] === "string"
          ? props[attr.name] + " " + attr.value
          : attr.value;

      return { ...props, [attr.name]: prop };
    }

    // This case handles `Event` variants.
    else if ("name" in attr && "handler" in attr) {
      const name = "on" + capitalise(attr.name);
      const handler = (e) => attr.handler(e, dispatch);

      return { ...props, [name]: handler };
    }

    // 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.
    //
    else {
      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 props;
    }
  }, {});
};