├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .prettierrc ├── README.md ├── example ├── .eslintrc.cjs ├── .yarnrc.yml ├── README.md ├── gleam.toml ├── index.html ├── manifest.toml ├── package.json ├── public │ ├── lucy.svg │ ├── lucyhappy.svg │ ├── react.svg │ └── vite.svg ├── src │ ├── example.gleam │ ├── example.main.mjs │ └── stylesheets │ │ ├── App.css │ │ └── index.css ├── test │ └── example_test.gleam ├── vite.config.js └── yarn.lock ├── redraw ├── CHANGELOG.md ├── LICENCE ├── gleam.toml ├── manifest.toml ├── src │ ├── context.ffi.mjs │ ├── events.ffi.mjs │ ├── external.ffi.mjs │ ├── props.ffi.mjs │ ├── redraw.ffi.mjs │ ├── redraw.gleam │ └── redraw │ │ ├── error.gleam │ │ ├── event.gleam │ │ └── internals │ │ └── coerce.gleam └── test │ └── redraw_test.gleam ├── redraw_dom ├── .gitignore ├── CHANGELOG.md ├── LICENCE ├── README.md ├── gleam.toml ├── manifest.toml ├── src │ ├── attribute.ffi.mjs │ ├── client.ffi.mjs │ ├── dom.ffi.mjs │ ├── events.ffi.mjs │ └── redraw │ │ ├── dom.gleam │ │ └── dom │ │ ├── attribute.gleam │ │ ├── client.gleam │ │ ├── event │ │ ├── animation.gleam │ │ ├── clipboard.gleam │ │ ├── composition.gleam │ │ ├── drag.gleam │ │ ├── focus.gleam │ │ ├── input.gleam │ │ ├── keyboard.gleam │ │ ├── mouse.gleam │ │ ├── pointer.gleam │ │ ├── touch.gleam │ │ ├── transition.gleam │ │ ├── ui.gleam │ │ └── wheel.gleam │ │ ├── events.gleam │ │ ├── html.gleam │ │ └── svg.gleam └── test │ └── redraw_dom_test.gleam └── scripts └── publish.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ghivert] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: erlef/setup-beam@v1 16 | with: 17 | otp-version: "26.0.2" 18 | gleam-version: "1.3.2" 19 | rebar3-version: "3" 20 | # elixir-version: "1.15.4" 21 | - run: gleam deps download 22 | working-directory: redraw 23 | - run: gleam test 24 | working-directory: redraw 25 | - run: gleam format --check src test 26 | working-directory: redraw 27 | - run: gleam deps download 28 | working-directory: redraw_dom 29 | - run: gleam test 30 | working-directory: redraw_dom 31 | - run: gleam format --check src test 32 | working-directory: redraw_dom 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | build/ 4 | erl_crash.dump 5 | node_modules/ 6 | dist/ 7 | .yarn/ 8 | redraw/README.md 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": false, 4 | "proseWrap": "always", 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redraw 2 | 3 | React opinionated bindings for Gleam. Use React directly from Gleam, with a 4 | friendly API that will never go in your path. Redraw tries to stick to React 5 | conventions, while providing idiomatic Gleam code. Write interoperable code 6 | between React and Gleam code, reuse existing components, and leverage type-safe 7 | components & immutable data structure. Forget runtime errors, and write React 8 | components that just works. 9 | 10 | > [!TIP] 11 | > 12 | > Have you tried [Lustre](https://lustre.build)? Lustre requires almost no 13 | > knowledge of JavaScript, no complicated runtimes to get like Node.js, few 14 | > knowledge of HTML/CSS and other web technologies, less code interfaces to 15 | > write than Redraw, and is supported by the entire Gleam community! Before 16 | > launching into Redraw, you should take a look at Lustre, it provides a 17 | > user-friendly, awesome experience right out-of-the-box for every gleamlins! 18 | > 19 | > As a bonus, Lustre is isomorphic, i.e. it can be used as well on client and on 20 | > server with the same codebase! 21 | > 22 | > Redraw assumes you have minimal knowledge on frontend development, and will 23 | > neither try to ease your learning curve nor simplify and hide the frontend 24 | > technology stack and complexities. In case you're not sure what you really 25 | > need, pick Lustre. 26 | 27 | ## Overview 28 | 29 | Redraw is a package that let you use React in a frontend-only Gleam project. By 30 | leveraging on the entire JS ecosystem, Redraw help you interop with existing 31 | current React codebases, or allows you to build your custom codebase and 32 | cherry-picking the existing components you know and love! Redraw tries to keep 33 | everything at the lowest level possible, turning all the React niceties into 34 | Gleam niceties. Wherever possible, Redraw tries to stick with Lustre API, to 35 | help you create a codebase mixing the two frameworks. For instance, you could 36 | build libraries targeting both Lustre and Redraw, and using the same design 37 | system for all your products! 38 | 39 | ## Prerequisites 40 | 41 | Redraw assumes that you're a fluent frontend developer and already understand 42 | how React works. If you don't, it's best to first learn React and the frontend 43 | ecosystem, and come back here later. Meanwhile, you could also take a look at 44 | [Lustre](https://lustre.build) to create your own application. You'll find some 45 | good tutorials on [react.dev](https://react.dev/), more specifically on 46 | ["Get Started" page](https://react.dev/learn). 47 | 48 | Redraw assumes you have [`node.js`](https://nodejs.org/en) or equivalent as well 49 | as a modern package manager, i.e. `npm`, `yarn`, `pnpm`, or even `bun`. Redraw 50 | also assumes you're using [`Vite`](https://vitejs.dev/) or an equivalent as 51 | build tool, and will not provide any interface to build your application. In the 52 | rest of that README, `Vite` will be used as example. It's up to you to use 53 | another bundler if you prefer. Redraw sticks with the modern, up-to-date 54 | frontend stack. 55 | 56 | ## Getting started 57 | 58 | Create the project, and add everything needed to make it work. Choose your 59 | prefered bundler to start. Create a Vite application, and choose to use 60 | JavaScript and React. Vite should bundle everything for you directly. 61 | 62 | ```sh 63 | npm create vite@latest 64 | ``` 65 | 66 | ```sh 67 | yarn create vite 68 | ``` 69 | 70 | ```sh 71 | pnpm create vite 72 | ``` 73 | 74 | ```sh 75 | bun create vite 76 | ``` 77 | 78 | From the follow-on, `yarn` will be used to illustrate the commands, it's up to 79 | you to see how to use your desired package manager. Then, it's time to setup the 80 | project correctly. 81 | 82 | ```sh 83 | cd [project-name] 84 | yarn install 85 | 86 | # Install the Vite Gleam plugin. That plugin is required to tell Vite how to 87 | # read Gleam files. 88 | yarn add -D vite-gleam 89 | 90 | # If you want to build the project on Vercel or Netlify. 91 | # @chouqueth/gleam provides a local version of the Gleam compiler installed in 92 | # your node_modules. You can freely skip that step if you don't need to build 93 | # your application remotely or if you're in control of the environment. 94 | yarn add -D @chouqueth/gleam 95 | 96 | # Remove the files needed for `gleam new` to work. 97 | mv README.md README.md.old 98 | mv .gitignore .gitignore.old 99 | 100 | # Setup the project. 101 | gleam new . 102 | gleam add redraw redraw_dom 103 | ``` 104 | 105 | Now that everyting is setup, you have to add the `vite-gleam` plugin in 106 | `vite.config.js`. An example of a `vite.config.js` should look like this. 107 | 108 | ```javascript 109 | import { defineConfig } from "vite" 110 | import react from "@vitejs/plugin-react" 111 | import gleam from "vite-gleam" 112 | 113 | // https://vitejs.dev/config/ 114 | export default defineConfig({ 115 | plugins: [react(), gleam()], 116 | }) 117 | ``` 118 | 119 | You're good to go! 120 | 121 | ## Writing Redraw components 122 | 123 | Writing Redraw components is the same as writing React component, with one small 124 | difference: wrap the component in `use <- redraw.component()`! 125 | 126 | ```gleam 127 | import redraw 128 | import redraw/dom/attribute 129 | import redraw/dom/html 130 | 131 | pub fn gleam_is_awesome() { 132 | use <- redraw.component__("GleamIsAwesome") 133 | html.div([attribute.class("oh-yeah")], [ 134 | html.text("Yeah, for sure") 135 | ]) 136 | } 137 | ``` 138 | 139 | While this could feels strange at first, you'll get used to it quickly. To call 140 | the component, you'll need to call the function first, _before definining a new 141 | component_. 142 | 143 | ```gleam 144 | import redraw 145 | import redraw/dom/attribute 146 | import redraw/dom/client 147 | import redraw/dom/html 148 | 149 | pub fn main() { 150 | let root = root() 151 | let assert Ok(root) = client.create_root("root") 152 | client.render(root, redraw.strict_mode([root()])) 153 | } 154 | 155 | fn root() { 156 | // Call `gleam_is_awesome` here, before component creation, otherwise a new 157 | // component will be created at each refresh. 158 | let gleam_is_awesome = gleam_is_awesome() 159 | use <- redraw.component__("Root") 160 | html.div([], [ 161 | gleam_is_awesome() 162 | ]) 163 | } 164 | 165 | fn gleam_is_awesome() { 166 | use <- redraw.component__("GleamIsAwesome") 167 | html.div([attribute.class("oh-yeah")], [ 168 | html.text("Yeah, for sure") 169 | ]) 170 | } 171 | ``` 172 | 173 | And you know everything to create Redraw components! 174 | 175 | ## What is the kind of components? 176 | 177 | Components can have different kinds, accepting props, children, ref, or other 178 | things. To provide a simpler, usable API in Gleam, props components in Gleam can 179 | be List, Tuple, CustomTypes or Nil. It means you can totally define your 180 | component like this: 181 | 182 | ```gleam 183 | pub type CounterProps { 184 | CounterProps( 185 | count: Int, 186 | set_count: fn(fn(Int) -> Int) -> Nil, 187 | ) 188 | } 189 | 190 | pub fn counter() { 191 | use props: CounterProps <- redraw.component_("Counter") 192 | html.button( 193 | [events.on_click(fn(_) { props.set_count(fn(count) { count + 1 }) })], 194 | [html.text("count is " <> int.to_string(props.count))], 195 | ) 196 | } 197 | ``` 198 | 199 | but also like this: 200 | 201 | ```gleam 202 | pub type CounterProps = #(Int, fn(fn(Int) -> Int) -> Nil) 203 | 204 | pub fn counter() { 205 | use #(count, set_count): CounterProps <- redraw.component_("Counter") 206 | html.button( 207 | [events.on_click(fn(_) { set_count(fn(count) { count + 1 }) })], 208 | [html.text("count is " <> int.to_string(count))], 209 | ) 210 | } 211 | ``` 212 | 213 | or even props-less components: 214 | 215 | ```gleam 216 | pub fn counter() { 217 | use Nil <- redraw.component_("Counter") 218 | let #(count, set_count) = redraw.use_state(0) 219 | html.button( 220 | [events.on_click(fn(_) { set_count(fn(count) { count + 1 }) })], 221 | [html.text("count is " <> int.to_string(count))], 222 | ) 223 | } 224 | ``` 225 | 226 | Don't worry about the translation of data from and to React, Redraw handles the 227 | hard task for you! 228 | 229 | ### `component`-family functions 230 | 231 | To define components, you should use `component`, `component_` or `component__`. 232 | The difference between the three is the signature of the resulting component. 233 | `component` accepts props and children, `component_` accepts only props, and 234 | `component__` do not accept anything. See it as a way to create an empty 235 | component, used with contexts or internal state for instance. You cannot create 236 | a component that accept children but no props. While it can feel boilerplaty at 237 | first, that is a design decision. Most of the time, components that accept 238 | children also accept props, so it's not worth creating another API and add 239 | overhead for a function that will almost not be used. 240 | 241 | ### `forward_ref`-family functions 242 | 243 | Defining components sometimes involves to forward a ref to internal component. 244 | React uses the mechanism of `forwardRef` to push a ref, from the parent to a 245 | nested child. Redraw fully implements forwarded ref components! Use 246 | `forward_ref` or `forward_ref_` to create a component with props, ref and 247 | children, or only with props and children! 248 | 249 | ## Some reminders on hooks 250 | 251 | Never use hooks outside of custom hooks (functions named `use_[something]`) or 252 | in components! It means you should never use something like `use_effect` or 253 | `use_state` outside of the body of `component`-related functions. If you break 254 | that rule, while it could seem to work, it's actually breaking the runtime, and 255 | it can explode at any time. So keep that rule anytime: no hooks outside of 256 | custom hooks or component body. 257 | 258 | ## Type-checking of hooks 259 | 260 | You could see that hooks often use dependencies array, to determine if they have 261 | to rerun or not. This is totally supported by Redraw, and leverages on Gleam 262 | abilities! Always pass a tuple of dependencies to hooks. No type-checking are 263 | done at this stage, and probably none will be implemented later, exactly like 264 | it's done with React currently. Be careful to provide the correct dependencies. 265 | 266 | ```gleam 267 | import gleam/io 268 | import redraw 269 | import redraw/attribute 270 | import redraw/html 271 | 272 | fn gleam_is_awesome() { 273 | use <- redraw.component__("GleamIsAwesome") 274 | redraw.use_effect(fn() { 275 | io.println("Hello from component!") 276 | }, #()) // Passing an empty tuple here is like passing [] in JavaScript. 277 | html.div([attribute.class("oh-yeah")], [ 278 | html.text("Yeah, for sure") 279 | ]) 280 | } 281 | ``` 282 | 283 | ## Using external components 284 | 285 | React is greatly used everywhere. It means a lot of components are already 286 | usable out-of-the-box. Happily, Redraw provides a way to interop directly with 287 | them! Use `to_component` and `to_component_` to integrate a foreign function 288 | directly. Define the correct props, and your work is done! Don't worry about 289 | snake_case and camelCase of props name, Redraw take care of the translation for 290 | you. Everytime you put an `Option(a)`, Redraw will also translate it to 291 | `a | null`, because React use the convention to pass `null` everywhere instead 292 | of optionals. 293 | 294 | ```gleam 295 | import gleam/option.{type Option} 296 | import redraw 297 | import redraw/dom/html 298 | 299 | // This type will be converted to correct JS props. 300 | pub type ExternalComponentProps { 301 | ExternalComponentProps( 302 | first_field: Bool, // firstField: bool 303 | second_field: Bool, // secondField: bool 304 | optional_field: Option(String) // optionalField: string | null 305 | ) 306 | } 307 | 308 | @external(javascript, "external_module", "ExternalComponent") 309 | fn external_component_ffi(props: a) -> redraw.Component 310 | 311 | fn external_component() -> fn(ExternalComponentProps) -> redraw.Component { 312 | redraw.to_component_("ExternalComponent", external_component_ffi) 313 | } 314 | 315 | pub fn my_other_component() { 316 | let external_component = external_component() 317 | use <- redraw.component__("OtherComponent") 318 | html.div([], [ 319 | external_component( 320 | ExternalComponentProps( 321 | first_field: True, 322 | second_field: False, 323 | optional_field: option.None, 324 | ) 325 | ) 326 | ]) 327 | } 328 | ``` 329 | 330 | ## Miscellaneous 331 | 332 | Some questions, answers, and various informations. 333 | 334 | ### Is there no linter for Redraw? 335 | 336 | At the moment, Redraw leverages on the Gleam compiler and does not offer linter 337 | support for critical parts like hooks dependencies. A future, complementary 338 | linter is planned, and should bridge that gap between Gleam and React. While 339 | Gleam compiler provides all useful information about Gleam code, Redraw linter 340 | will focus on specific Redraw requirements. 341 | 342 | ### What is the state of support for React Native, or any other React flavor? 343 | 344 | React is an isolated packages, and renderers can be various. Redraw has 345 | successfully been used with Raycast. You can also easily add a `redraw_native` 346 | package, and provide an interface for native components. Everything can be done 347 | quickly and easily, because the entire package is written with as few JS as 348 | possible. Everything should work almost out-of-the box, because React is already 349 | working there. You should take inspiration at how is working `redraw/html`, and 350 | it could work in the exact same way. It's only a matter of providing the correct 351 | Component to `jsx`. 352 | 353 | ### Contributing 354 | 355 | Do you love the package? You can contribute! Feel free to open a PR, or open an 356 | issue! 357 | 358 | ### Why wrapping every components in `use props <- component(name)`? 359 | 360 | If you're used to React, you know a component is no more than a function, 361 | returning a `ReactNode`. Something like this: 362 | 363 | ```javascript 364 | function GleamIsAwesome(props) { 365 | return
Yeah, for sure
366 | } 367 | ``` 368 | 369 | Actually, because of JSX, React is a bit lying to all of us, and compiles it to: 370 | 371 | ```javascript 372 | import { jsx } from "react/runtime-jsx" 373 | 374 | function GleamIsAwesome(props) { 375 | return jsx("div", { className: "oh-yeah", children: "Yeah, for sure" }) 376 | } 377 | ``` 378 | 379 | React injects a call to `jsx` before every JSX render. This allows for the 380 | runtime to determine if the function should be called once again with the new 381 | provided props. It's also true for functions. 382 | 383 | ```javascript 384 | // That code 385 | function ILoveBeam(props) { 386 | return
BEAM 💜
387 | } 388 | 389 | function GleamIsAwesome(props) { 390 | return ( 391 |
392 | 393 | Yeah, for sure 394 |
395 | ) 396 | } 397 | 398 | // Turns into 399 | import { jsx, jsxs } from "react/runtime-jsx" 400 | 401 | function ILoveBeam(props) { 402 | return jsx("div", { children: "BEAM 💜" }) 403 | } 404 | 405 | function GleamIsAwesome(props) { 406 | return jsxs("div", { 407 | className: "oh-yeah", 408 | children: [jsx(ILoveBeam, {}), "Yeah, for sure"], 409 | }) 410 | } 411 | ``` 412 | 413 | Here, we got a problem: we cannot inject the `jsx` call before `ILoveBeam` in 414 | Gleam. What we could do is write a function that generate the `jsx` call with an 415 | other function with `use`, but anonymous functions cannot be used with React: 416 | React is doing referential equality for Functional Components. Every component 417 | should be defined once and for all. 418 | 419 | To inject the `jsx` call properly, it would requires an additional 420 | compilation-step. Because we cannot do this, we pass by a generator function. 421 | That's what `use props <- component(name)` is doing. To get the correct result, 422 | we go to a _Component creator_, a function that create a component once and for 423 | all from a `render` function. Everytime we create a component, we use a render 424 | function and turns it into a proper component to be used with React. 425 | -------------------------------------------------------------------------------- /example/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /example/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | [![Package Version](https://img.shields.io/hexpm/v/example)](https://hex.pm/packages/example) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/example/) 5 | 6 | ```sh 7 | gleam add example@1 8 | ``` 9 | ```gleam 10 | import example 11 | 12 | pub fn main() { 13 | // TODO: An example of the project in use 14 | } 15 | ``` 16 | 17 | Further documentation can be found at . 18 | 19 | ## Development 20 | 21 | ```sh 22 | gleam run # Run the project 23 | gleam test # Run the tests 24 | ``` 25 | -------------------------------------------------------------------------------- /example/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "example" 2 | version = "1.0.0" 3 | target = "javascript" 4 | 5 | # Fill out these fields if you intend to generate HTML documentation or publish 6 | # your project to the Hex package manager. 7 | # 8 | # description = "" 9 | # licences = ["Apache-2.0"] 10 | # repository = { type = "github", user = "", repo = "" } 11 | # links = [{ title = "Website", href = "" }] 12 | # 13 | # For a full reference of all the available options, you can have a look at 14 | # https://gleam.run/writing-gleam/gleam-toml/. 15 | 16 | [dependencies] 17 | gleam_stdlib = ">= 0.34.0 and < 2.0.0" 18 | redraw = { path = "../redraw" } 19 | redraw_dom = { path = "../redraw_dom" } 20 | 21 | [dev-dependencies] 22 | gleeunit = ">= 1.0.0 and < 2.0.0" 23 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Vite + React 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" }, 6 | { name = "gleam_stdlib", version = "0.57.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86EFACDF6460B8681E82752C5490F9630EC0F138F88A037DDCB241799AA8811F" }, 7 | { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" }, 8 | { name = "redraw", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], source = "local", path = "../redraw" }, 9 | { name = "redraw_dom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "redraw"], source = "local", path = "../redraw_dom" }, 10 | ] 11 | 12 | [requirements] 13 | gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 14 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 15 | redraw = { path = "../redraw" } 16 | redraw_dom = { path = "../redraw_dom" } 17 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "greact", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.3.1", 14 | "react-dom": "^18.3.1", 15 | "vite-gleam": "^0.4.3" 16 | }, 17 | "devDependencies": { 18 | "@chouqueth/gleam": "^1.9.1", 19 | "@types/react": "^18.3.3", 20 | "@types/react-dom": "^18.3.0", 21 | "@vitejs/plugin-react": "^4.3.1", 22 | "eslint": "^8.57.0", 23 | "eslint-plugin-react": "^7.34.3", 24 | "eslint-plugin-react-hooks": "^4.6.2", 25 | "eslint-plugin-react-refresh": "^0.4.7", 26 | "vite": "^5.3.4" 27 | }, 28 | "packageManager": "yarn@4.3.1" 29 | } 30 | -------------------------------------------------------------------------------- /example/public/lucy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/public/lucyhappy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/public/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/src/example.gleam: -------------------------------------------------------------------------------- 1 | import gleam/int 2 | import gleam/list 3 | import redraw as react 4 | import redraw/dom/attribute as a 5 | import redraw/dom/client 6 | import redraw/dom/events 7 | import redraw/dom/html 8 | 9 | pub type Root 10 | 11 | pub type Node(props) 12 | 13 | pub type Children 14 | 15 | pub fn main() { 16 | let root = root() 17 | let assert Ok(node) = client.create_root("root") 18 | client.render(node, react.strict_mode([root()])) 19 | } 20 | 21 | pub fn root() { 22 | let app = app() 23 | use <- react.component__("Root") 24 | app() 25 | } 26 | 27 | pub type CounterProps { 28 | CounterProps(count: Int, set_count: fn(fn(Int) -> Int) -> Nil) 29 | } 30 | 31 | fn counter() { 32 | use props: CounterProps <- react.component_("Counter") 33 | html.button( 34 | [events.on_click(fn(_) { props.set_count(fn(count) { count + 1 }) })], 35 | list.map([props.count], fn(count) { 36 | html.text("count is " <> int.to_string(count)) 37 | }), 38 | ) 39 | } 40 | 41 | fn nav_links() { 42 | html.div([], [ 43 | html.a([a.href("https://vitejs.dev"), a.target("_blank")], [ 44 | html.img([a.src("/vite.svg"), a.class("logo"), a.alt("Vite logo")]), 45 | ]), 46 | html.a([a.href("https://gleam.run"), a.target("_blank")], [ 47 | html.img([a.src("/lucy.svg"), a.class("logo lucy"), a.alt("Gleam logo")]), 48 | ]), 49 | html.a([a.href("https://react.dev"), a.target("_blank")], [ 50 | html.img([a.src("/react.svg"), a.class("logo react"), a.alt("React logo")]), 51 | ]), 52 | ]) 53 | } 54 | 55 | pub fn app() { 56 | let counter = counter() 57 | use <- react.component__("App") 58 | let #(count, set_count) = react.use_state_(0) 59 | react.fragment([ 60 | nav_links(), 61 | html.h1([], [html.text("Vite + Gleam + React")]), 62 | html.div([a.class("card")], [ 63 | counter(CounterProps(count, set_count)), 64 | html.p([], [ 65 | html.text("Edit "), 66 | html.code([], [html.text("src/main.gleam")]), 67 | html.text(" and save to test HMR"), 68 | ]), 69 | ]), 70 | html.p([a.class("read-the-docs")], [ 71 | html.text("Click on the Vite, Gleam and React logos to learn more"), 72 | ]), 73 | ]) 74 | } 75 | -------------------------------------------------------------------------------- /example/src/example.main.mjs: -------------------------------------------------------------------------------- 1 | import { main } from "./example.gleam" 2 | import "./stylesheets/index.css" 3 | import "./stylesheets/App.css" 4 | 5 | // Start the Lustre app. 6 | document.addEventListener("DOMContentLoaded", main) 7 | -------------------------------------------------------------------------------- /example/src/stylesheets/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | 15 | .logo:hover { 16 | filter: drop-shadow(0 0 2em #646cffaa); 17 | } 18 | 19 | .logo.react:hover { 20 | filter: drop-shadow(0 0 2em #61dafbaa); 21 | } 22 | 23 | .logo.lucy { 24 | transition: all 0.3s ease; 25 | } 26 | 27 | .logo.lucy:hover { 28 | filter: drop-shadow(0 0 2em #ffaff3aa); 29 | content: url(/lucyhappy.svg); 30 | transform: rotate(23deg); 31 | } 32 | 33 | @keyframes logo-spin { 34 | from { 35 | transform: rotate(0deg); 36 | } 37 | 38 | to { 39 | transform: rotate(360deg); 40 | } 41 | } 42 | 43 | @media (prefers-reduced-motion: no-preference) { 44 | a:nth-of-type(3) .logo { 45 | animation: logo-spin infinite 20s linear; 46 | } 47 | } 48 | 49 | .card { 50 | padding: 2em; 51 | } 52 | 53 | .read-the-docs { 54 | color: #888; 55 | } 56 | -------------------------------------------------------------------------------- /example/src/stylesheets/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | @media (prefers-color-scheme: light) { 58 | :root { 59 | color: #213547; 60 | background-color: #ffffff; 61 | } 62 | a:hover { 63 | color: #747bff; 64 | } 65 | button { 66 | background-color: #f9f9f9; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/test/example_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit 2 | import gleeunit/should 3 | 4 | pub fn main() { 5 | gleeunit.main() 6 | } 7 | 8 | // gleeunit test functions end in `_test` 9 | pub fn hello_world_test() { 10 | 1 11 | |> should.equal(1) 12 | } 13 | -------------------------------------------------------------------------------- /example/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import react from "@vitejs/plugin-react" 3 | import gleam from "vite-gleam" 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), gleam()], 8 | }) 9 | -------------------------------------------------------------------------------- /redraw/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.0.2 - 2025-04-16 2 | 3 | - Fix FFI `suspense` external. 4 | 5 | ## v2.0.1 - 2025-03-14 6 | 7 | - Add support for `Nil` as props, instead of panicking. 8 | - Adapt to latest stdlib version (remove `function.flip`). 9 | 10 | ## v2.0.0 - 2024-12-24 11 | 12 | - Remove DOM attributes and elements in favour of `redraw_dom`. Keep only React 13 | core to simplify support for all flavours of React (DOM, Native, etc.). This 14 | decrease package size when DOM is unused. 15 | 16 | ## v1.1.1 - 2024-12-23 17 | 18 | - Add documentation for every functions. 19 | - Bump minimal required versions. 20 | - Add `offset_x`, `offset_y`, `altitude_angle` & `azimuth_angle`. 21 | 22 | ## v1.1.0 - 2024-21-08 23 | 24 | - Documentation improvements. 25 | - `html.none` added to `redraw/element/html`. 26 | - `Context` usage has been improved. In JavaScript, contexts are usually 27 | instantiated as top level side-effects. Because Gleam does not accepts any 28 | top-level code execution, contexts now have proper helpers to mitigate the 29 | issue. 30 | - `use_callback` and `use_effect_` now have proper types. 31 | 32 | ## v1.0.0 - 2024-07-21 33 | 34 | - First release of Redraw! 🎉 35 | - Redraw implements a subset of React, and starts at React 18. Previous versions 36 | are not guaranteed to work. Under-the-hood, Redraw uses `react/jsx-runtime`, 37 | and as such, requires a modern toolchain installed, whether it's Next.js or 38 | Vite. 39 | -------------------------------------------------------------------------------- /redraw/LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Guillaume Hivert 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /redraw/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "redraw" 2 | target = "javascript" 3 | version = "2.0.2" 4 | 5 | # Fill out these fields if you intend to generate HTML documentation or publish 6 | # your project to the Hex package manager. 7 | 8 | description = "React bindings for Gleam! Supports everything modern React provides, with full Gleam Type-Checking system!" 9 | internal_modules = ["redraw/internals", "redraw/internals/*"] 10 | licences = ["MIT"] 11 | links = [{title = "Sponsor", href = "https://github.com/sponsors/ghivert"}] 12 | 13 | [repository] 14 | type = "github" 15 | user = "ghivert" 16 | repo = "redraw" 17 | path = "redraw" 18 | 19 | # For a full reference of all the available options, you can have a look at 20 | # https://gleam.run/writing-gleam/gleam-toml/. 21 | 22 | [dependencies] 23 | gleam_javascript = ">= 0.13.0 and < 1.0.0" 24 | gleam_stdlib = ">= 0.51.0 and < 2.0.0" 25 | 26 | [dev-dependencies] 27 | gleeunit = ">= 1.0.0 and < 2.0.0" 28 | -------------------------------------------------------------------------------- /redraw/manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" }, 6 | { name = "gleam_stdlib", version = "0.51.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "14AFA8D3DDD7045203D422715DBB822D1725992A31DF35A08D97389014B74B68" }, 7 | { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 8 | ] 9 | 10 | [requirements] 11 | gleam_javascript = { version = ">= 0.13.0 and < 1.0.0" } 12 | gleam_stdlib = { version = ">= 0.51.0 and < 2.0.0" } 13 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 14 | -------------------------------------------------------------------------------- /redraw/src/context.ffi.mjs: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { jsx } from "./redraw.ffi.mjs" 3 | import * as gleam from "./gleam.mjs" 4 | import * as error from "./redraw/error.mjs" 5 | 6 | const contexts = {} 7 | 8 | export function contextProvider(context, value, children) { 9 | return jsx(context.Provider, { value }, children) 10 | } 11 | 12 | export function createContext(name, defaultValue) { 13 | if (contexts[name]) return new gleam.Error(new error.ExistingContext(name)) 14 | contexts[name] = React.createContext(defaultValue) 15 | return new gleam.Ok(contexts[name]) 16 | } 17 | 18 | export function getContext(name) { 19 | if (!contexts[name]) return new gleam.Error(new error.UnknownContext(name)) 20 | return new gleam.Ok(contexts[name]) 21 | } 22 | -------------------------------------------------------------------------------- /redraw/src/events.ffi.mjs: -------------------------------------------------------------------------------- 1 | export function bubbles(event) { 2 | return event.bubbles 3 | } 4 | 5 | export function cancelable(event) { 6 | return event.cancelable 7 | } 8 | 9 | export function currentTarget(event) { 10 | return event.currentTarget 11 | } 12 | 13 | export function defaultPrevented(event) { 14 | return event.defaultPrevented 15 | } 16 | 17 | export function eventPhase(event) { 18 | return event.eventPhase 19 | } 20 | 21 | export function isTrusted(event) { 22 | return event.isTrusted 23 | } 24 | 25 | export function target(event) { 26 | return event.target 27 | } 28 | 29 | export function timeStamp(event) { 30 | return event.timeStamp 31 | } 32 | 33 | export function nativeEvent(event) { 34 | return event.nativeEvent 35 | } 36 | 37 | export function isDefaultPrevented(event) { 38 | return event.isDefaultPrevented() 39 | } 40 | 41 | export function isPropagationStopped(event) { 42 | return event.isPropagationStopped() 43 | } 44 | 45 | export function isPersistent(event) { 46 | return event.isPersistent() 47 | } 48 | 49 | export function preventDefault(event) { 50 | event.preventDefault() 51 | return event 52 | } 53 | 54 | export function stopPropagation(event) { 55 | event.stopPropagation() 56 | return event 57 | } 58 | 59 | export function persist(event) { 60 | event.persist() 61 | return event 62 | } 63 | -------------------------------------------------------------------------------- /redraw/src/external.ffi.mjs: -------------------------------------------------------------------------------- 1 | import * as option from "../gleam_stdlib/gleam/option.mjs" 2 | 3 | function camelize(key) { 4 | return key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()) 5 | } 6 | 7 | function convertOption(value) { 8 | if (value instanceof option.None) return undefined 9 | if (value instanceof option.Some) return value[0] 10 | return value 11 | } 12 | 13 | export function convertProps(props) { 14 | return Object.fromEntries( 15 | Object.entries(props).map(([key, value]) => [ 16 | camelize(key), 17 | convertOption(value), 18 | ]), 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /redraw/src/props.ffi.mjs: -------------------------------------------------------------------------------- 1 | import * as gleam from "./gleam.mjs" 2 | 3 | export function propsToGleamProps(props, originalProps) { 4 | switch (props.__propsType) { 5 | case "List": { 6 | let list = new gleam.Empty() 7 | for (let i = props.__length; i > 0; i--) 8 | list = new gleam.NonEmpty(props[i - 1], list) 9 | return list 10 | } 11 | case "Tuple": { 12 | const tuple = new Array(props.__length) 13 | for (let i = 0; i < props.__length; i++) tuple[i] = props[i] 14 | return tuple 15 | } 16 | case "Nil": { 17 | return undefined 18 | } 19 | default: { 20 | const [Prototype, firstProps] = originalProps.current[props.__propsType] 21 | const values = Object.keys(firstProps).map((key) => props[key]) 22 | return new Prototype.constructor(...values) 23 | } 24 | } 25 | } 26 | 27 | export function gleamPropsToProps(props_, originalProps, ref) { 28 | if (props_ instanceof gleam.CustomType) { 29 | const prototype = Object.getPrototypeOf(props_) 30 | const name = prototype.constructor.name 31 | originalProps.current[name] ??= [prototype, props_] 32 | const props = { ...props_, __propsType: name } 33 | if (ref) props.ref = ref 34 | return props 35 | } else if (props_ instanceof gleam.List) { 36 | const props = { __propsType: "List" } 37 | let index = 0 38 | for (const item of props_) props[index++] = item 39 | props.__length = index 40 | if (ref) props.ref = ref 41 | return props 42 | } else if (Array.isArray(props_)) { 43 | const props = { __propsType: "Tuple" } 44 | let index = 0 45 | for (const item of props_) props[index++] = item 46 | props.__length = index 47 | if (ref) props.ref = ref 48 | return props 49 | } else if (props_ === undefined) { 50 | const props = { __propsType: "Nil" } 51 | if (ref) props.ref = ref 52 | return props 53 | } else { 54 | console.warn( 55 | `redraw only support custom types, list, tuples or Nil as props. 56 | ${Component.displayName} received ${props_} as props.`.replace( 57 | /\n( )*/g, 58 | "\n", 59 | ), 60 | ) 61 | return null 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /redraw/src/redraw.ffi.mjs: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import runtime from "react/jsx-runtime" 3 | import * as gleam from "./gleam.mjs" 4 | import { propsToGleamProps, gleamPropsToProps } from "./props.ffi.mjs" 5 | 6 | /** Keep display name on Wrapper, to correctly display Components 7 | * in React devtools. */ 8 | function withDisplayName(Component, Wrapper) { 9 | if (Component.displayName) Wrapper.displayName = Component.displayName 10 | return Wrapper 11 | } 12 | 13 | function withComputedProps(Component, originalProps) { 14 | return withDisplayName(Component, (props, ...rest) => { 15 | const newProps = propsToGleamProps(props, originalProps) 16 | return Component(newProps, ...rest) 17 | }) 18 | } 19 | 20 | /** Wrap the Component in a `forwardRef`, and inject the `ref` from the function 21 | * arguments to the props. In Gleam, when called, a `forwardRef` component will 22 | * have shape`fn (props, ref) -> Component`. `addForwardRef` turns it into 23 | * `fn (props) -> jsx(Component)`.*/ 24 | export function addForwardRef(Component) { 25 | const originalProps = { current: {} } 26 | const added = React.forwardRef(withComputedProps(Component, originalProps)) 27 | return withDisplayName(Component, (props_, ref) => { 28 | const props = gleamPropsToProps(props_, originalProps, ref) 29 | if (props) return jsx(added, props) 30 | const render = withDisplayName(Component, () => null) 31 | return jsx(render, {}) 32 | }) 33 | } 34 | 35 | /** Wrap the Component in a `forwardRef`, and inject the `ref` from the function 36 | * arguments to the props. In Gleam, a `forwardRef` component will have shape 37 | * `fn (props, ref, children) -> Component`. `addChildrenForwardRef` turns it 38 | * into `fn (props) -> jsx(Component)`. It will then be transformed once again 39 | * to `fn (props, ref, children) -> jsx(Component)` by extracting the children 40 | * from the props. */ 41 | export function addChildrenForwardRef(Component) { 42 | const originalProps = { current: {} } 43 | const added = React.forwardRef( 44 | withRefChildren(withComputedProps(Component, originalProps)), 45 | ) 46 | return withDisplayName(Component, (props_, ref, children) => { 47 | const props = gleamPropsToProps(props_, originalProps, ref) 48 | if (props) return jsx(added, props, children) 49 | const render = withDisplayName(Component, () => null) 50 | return jsx(render, {}) 51 | }) 52 | } 53 | 54 | /** Extract the Component's children from the props to feed it to the function 55 | * directly. */ 56 | export function withChildren(Component) { 57 | return withDisplayName(Component, (props) => { 58 | const children = gleam.List.fromArray(props.children) 59 | return Component(props, children) 60 | }) 61 | } 62 | 63 | /** Extract the Forwarded Ref Component's children from the props to feed it to 64 | * the function directly. */ 65 | export function withRefChildren(Component) { 66 | return withDisplayName(Component, (props, ref) => { 67 | const children = gleam.List.fromArray(props.children) 68 | return Component(props, ref, children) 69 | }) 70 | } 71 | 72 | /** In Gleam, a `component` will have shape 73 | * `fn (props, children) -> Component`. `addChildrenProxy` turns it 74 | * into `fn (props) -> jsx(Component)`. It will then be transformed once again 75 | * to `fn (props, children) -> jsx(Component)` by extracting the children 76 | * from the props. */ 77 | export function addChildrenProxy(Component) { 78 | const originalProps = { current: {} } 79 | const childrenAdded = withChildren( 80 | withComputedProps(Component), 81 | originalProps, 82 | ) 83 | return withDisplayName(Component, (props_, children) => { 84 | const props = gleamPropsToProps(props_, originalProps) 85 | if (props) return jsx(childrenAdded, props, children) 86 | const render = withDisplayName(Component, () => null) 87 | return jsx(render, {}) 88 | }) 89 | } 90 | 91 | /** In Gleam, a `component__` will have shape 92 | * `fn () -> Component`. `addEmptyProxy` turns it 93 | * into `fn (props) -> jsx(Component)`. It will then be transformed once again 94 | * to `fn () -> jsx(Component)` by extracting the children 95 | * from the props. */ 96 | export function addEmptyProxy(Component) { 97 | return withDisplayName(Component, () => { 98 | return jsx(Component, {}) 99 | }) 100 | } 101 | 102 | /** In Gleam, a `component_` will have shape 103 | * `fn (props) -> Component`. `addEmptyProxy` turns it 104 | * into `fn (props) -> jsx(Component)`. It will then be transformed once again 105 | * to `fn (props) -> jsx(Component)` by extracting the children 106 | * from the props. */ 107 | export function addProxy(Component) { 108 | const originalProps = { current: {} } 109 | const added = withComputedProps(Component, originalProps) 110 | return withDisplayName(Component, (props_) => { 111 | const props = gleamPropsToProps(props_, originalProps) 112 | if (props) return jsx(added, props) 113 | const render = withDisplayName(Component, () => null) 114 | return jsx(render, {}) 115 | }) 116 | } 117 | 118 | /** Generate JSX using the JSX factory. 119 | * `jsx` is for dynamic components, while `jsxs` is for static components. */ 120 | export function jsx(value, props_, children_) { 121 | if (value === "none_") return null 122 | if (value === "text_") return children_ 123 | let children = children_?.toArray?.() 124 | let isStatic = true 125 | 126 | // Handle keyed elements like lustre does. 127 | // This allow to have a similar interface between Lustre and Redraw. 128 | if (Array.isArray(children?.[0])) { 129 | children = children.map((c) => { 130 | const [key, node] = c 131 | if ("key" in node) return React.cloneElement(node, { key }) 132 | return node 133 | }) 134 | isStatic = false 135 | } 136 | 137 | // Props creation. 138 | // Uses the existing props, and add children if needed. 139 | const props = {} 140 | Object.assign(props, props_) 141 | if (children?.length > 0) props.children = children 142 | 143 | if (isStatic) { 144 | return runtime.jsxs(value, props) 145 | } else { 146 | return runtime.jsx(value, props) 147 | } 148 | } 149 | 150 | /** Set the display name function. Essential to display the correct name in 151 | * the devtools. */ 152 | export function setFunctionName(component, name) { 153 | component.displayName = name 154 | return component 155 | } 156 | 157 | export function strictMode(children) { 158 | return jsx(React.StrictMode, {}, children) 159 | } 160 | 161 | export function fragment(children) { 162 | return jsx(React.Fragment, {}, children) 163 | } 164 | 165 | export function profiler(children) { 166 | return jsx(React.Profiler, {}, children) 167 | } 168 | 169 | export function suspense(props, children) { 170 | return jsx(React.Suspense, props, children) 171 | } 172 | 173 | export function coerce(value) { 174 | return value 175 | } 176 | 177 | export function setCurrent(ref, value) { 178 | ref.current = value 179 | } 180 | 181 | export function getCurrent(ref) { 182 | return ref.current 183 | } 184 | -------------------------------------------------------------------------------- /redraw/src/redraw.gleam: -------------------------------------------------------------------------------- 1 | import gleam/javascript/promise.{type Promise} 2 | import gleam/option.{type Option} 3 | import gleam/string 4 | import redraw/error.{type Error} 5 | import redraw/internals/coerce.{coerce} 6 | 7 | // Component creation 8 | 9 | /// Default Node in Redraw. Use `component`-family functions to create components. 10 | /// Forwarded ref can be constructed using `forward_ref`-family functions, while 11 | /// external components can be used with `to_component`-family functions. 12 | pub type Component 13 | 14 | /// Create a Redraw component, with a `name`, and a `render` function. `render` 15 | /// will accept props, and a list of children. 16 | pub fn component( 17 | name name: String, 18 | render render: fn(props, List(Component)) -> Component, 19 | ) -> fn(props, List(Component)) -> Component { 20 | render 21 | |> set_function_name(name) 22 | |> add_children_proxy 23 | } 24 | 25 | /// Create a Redraw component, with a `name`, and a `render` function. This 26 | /// component does not accept children. 27 | pub fn component_( 28 | name name: String, 29 | render render: fn(props) -> Component, 30 | ) -> fn(props) -> Component { 31 | render 32 | |> set_function_name(name) 33 | |> add_proxy 34 | } 35 | 36 | /// Create a Redraw component, with a `name` and a `render` function. This 37 | /// component does not accept children nor props. 38 | pub fn component__( 39 | name name: String, 40 | render render: fn() -> Component, 41 | ) -> fn() -> Component { 42 | render 43 | |> set_function_name(name) 44 | |> add_empty_proxy 45 | } 46 | 47 | @external(javascript, "./external.ffi.mjs", "convertProps") 48 | fn convert_props(gleam_props: gleam_props) -> props 49 | 50 | /// Convert a React component to a React-redraw component with children. Give it a 51 | /// name, and send directly the FFI. Don't worry about the snake_case over 52 | /// camelCase, redraw take care of it for you. 53 | /// 54 | /// ```gleam 55 | /// import redraw 56 | /// 57 | /// pub type MyComponentProps { 58 | /// MyComponentProps( 59 | /// first_prop: Bool, 60 | /// second_prop: String, 61 | /// ) 62 | /// } 63 | /// 64 | /// @external(javascript, "my_library", "MyComponent") 65 | /// fn do_my_component(props: MyComponentProps) -> redraw.Component 66 | /// 67 | /// pub fn my_component() -> fn(MyComponentProps, List(Component)) -> redraw.Component { 68 | /// redraw.to_component("MyComponent", do_my_component) 69 | /// } 70 | /// ``` 71 | pub fn to_component( 72 | name name: String, 73 | component render: fn(props) -> Component, 74 | ) -> fn(props, List(Component)) -> Component { 75 | fn(props, children) { jsx(render, convert_props(props), children) } 76 | |> set_function_name(name) 77 | } 78 | 79 | /// Convert a React Component to a redraw Component without children. Give it a 80 | /// name, and send directly the FFI. Don't worry about the snake_case over 81 | /// camelCase, redraw take care of it for you. 82 | /// 83 | /// ```gleam 84 | /// import redraw 85 | /// 86 | /// pub type MyComponentProps { 87 | /// MyComponentProps( 88 | /// first_prop: Bool, 89 | /// second_prop: String, 90 | /// ) 91 | /// } 92 | /// 93 | /// @external(javascript, "my_library", "MyComponent") 94 | /// fn do_my_component(props: MyComponentProps) -> redraw.Component 95 | /// 96 | /// pub fn my_component() -> fn(MyComponentProps) -> redraw.Component { 97 | /// redraw.to_component_("MyComponent", do_my_component) 98 | /// } 99 | /// ``` 100 | pub fn to_component_( 101 | name name: String, 102 | component render: fn(props) -> Component, 103 | ) -> fn(props) -> Component { 104 | fn(props) { jsx(render, convert_props(props), Nil) } 105 | |> set_function_name(name) 106 | } 107 | 108 | /// Create a Redraw component with children with forwarded ref. \ 109 | /// [Documentation](https://fr.react.dev/reference/react/forwardRef) 110 | pub fn forward_ref( 111 | name name: String, 112 | render render: fn(props, Ref(ref), List(Component)) -> Component, 113 | ) -> fn(props, Ref(ref), List(Component)) -> Component { 114 | render 115 | |> set_function_name(name) 116 | |> add_children_forward_ref 117 | } 118 | 119 | /// Create a Redraw component without children with forwarded ref. \ 120 | /// [Documentation](https://react.dev/reference/react/forwardRef) 121 | pub fn forward_ref_( 122 | name name: String, 123 | render render: fn(props, Ref(ref)) -> Component, 124 | ) -> fn(props, Ref(ref)) -> Component { 125 | render 126 | |> set_function_name(name) 127 | |> add_forward_ref 128 | } 129 | 130 | /// Memoizes a Redraw component with children. \ 131 | /// [Documentation](https://react.dev/reference/react/memo) 132 | @external(javascript, "react", "memo") 133 | pub fn memo( 134 | component: fn(props, List(Component)) -> Component, 135 | ) -> fn(props, List(Component)) -> Component 136 | 137 | /// Memoizes a Redraw component without children. \ 138 | /// [Documentation](https://react.dev/reference/react/memo) 139 | @external(javascript, "react", "memo") 140 | pub fn memo_(component: fn(props) -> Component) -> fn(props) -> Component 141 | 142 | // Components 143 | 144 | /// Strict Mode should be enabled during development. \ 145 | /// [Documentation](https://react.dev/reference/react/StrictMode) 146 | @external(javascript, "./redraw.ffi.mjs", "strictMode") 147 | pub fn strict_mode(children: List(Component)) -> Component 148 | 149 | /// Fragment allow to group children, without creating a node in the DOM. \ 150 | /// [Documentation](https://react.dev/reference/react/Fragment) 151 | @external(javascript, "./redraw.ffi.mjs", "fragment") 152 | pub fn fragment(children: List(Component)) -> Component 153 | 154 | /// Profile allows to measure code performance for a component tree. \ 155 | /// [Documentation](https://react.dev/reference/react/Profiler) 156 | @external(javascript, "./redraw.ffi.mjs", "strictMode") 157 | pub fn profiler(children: List(Component)) -> Component 158 | 159 | pub type Suspense { 160 | Suspense(fallback: Component) 161 | } 162 | 163 | /// Suspense allow to display a fallback content while waiting for children to 164 | /// finish loading. \ 165 | /// [Documentation](https://fr.react.dev/reference/react/Suspense) 166 | @external(javascript, "./redraw.ffi.mjs", "suspense") 167 | pub fn suspense(props: Suspense, children: List(Component)) -> Component 168 | 169 | // Hooks 170 | 171 | /// Let you cache a function definition between re-renders. 172 | /// `dependencies` should be a tuple. \ 173 | /// [Documentation](https://react.dev/reference/react/useCallback) 174 | @external(javascript, "react", "useCallback") 175 | pub fn use_callback(fun: function, dependencies: dependencies) -> function 176 | 177 | /// Let you add a label to a custom Hook in React DevTools. \ 178 | /// [Documentation](https://react.dev/reference/react/useDebugValue) 179 | @external(javascript, "react", "useDebugValue") 180 | pub fn use_debug_value(value: a) -> Nil 181 | 182 | /// Let you add a label to a custom Hook in React DevTools, but allow to format 183 | /// it before. \ 184 | /// [Documentation](https://react.dev/reference/react/useDebugValue) 185 | @external(javascript, "react", "useDebugValue") 186 | pub fn use_debug_value_(value: a, formatter: fn(a) -> String) -> Nil 187 | 188 | /// Let you defer updating a part of the UI. \ 189 | /// [Documentation](https://react.dev/reference/react/useDeferredValue) 190 | @external(javascript, "react", "useDeferredValue") 191 | pub fn use_deferred_value(value: a) -> a 192 | 193 | /// Let you synchronize a component with an external system. \ 194 | /// [Documentation](https://react.dev/reference/react/useEffect) 195 | @external(javascript, "react", "useEffect") 196 | pub fn use_effect(value: fn() -> Nil, dependencies: a) -> Nil 197 | 198 | /// Let you synchronize a component with an external system. Allow to return 199 | /// a cleanup function. \ 200 | /// [Documentation](https://react.dev/reference/react/useEffect) 201 | @external(javascript, "react", "useEffect") 202 | pub fn use_effect_(value: fn() -> fn() -> Nil, dependencies: a) -> Nil 203 | 204 | /// Version of useEffect that fires before the browser repaints the screen. \ 205 | /// [Documentation](https://react.dev/reference/react/useLayoutEffect) 206 | @external(javascript, "react", "useLayoutEffect") 207 | pub fn use_layout_effect(value: fn() -> Nil, dependencies: a) -> Nil 208 | 209 | /// Generate unique IDs that can be passed to accessibility attributes. \ 210 | /// [Documentation](https://react.dev/reference/react/useId) 211 | @external(javascript, "react", "useId") 212 | pub fn use_id() -> String 213 | 214 | /// Let you cache the result of a calculation between re-renders. \ 215 | /// [Documentation](https://react.dev/reference/react/useMemo) 216 | @external(javascript, "react", "useMemo") 217 | pub fn use_memo(calculate_value: fn() -> a, dependencies: b) -> a 218 | 219 | /// Let you add a [reducer](https://react.dev/learn/extracting-state-logic-into-a-reducer) to your component. \ 220 | /// [Documentation](https://react.dev/reference/react/useReducer) 221 | @external(javascript, "react", "useReducer") 222 | pub fn use_reducer( 223 | reducer: fn(state, action) -> state, 224 | initial_state: state, 225 | ) -> #(state, fn(action) -> Nil) 226 | 227 | /// Let you add a [reducer](https://react.dev/learn/extracting-state-logic-into-a-reducer) to your component. 228 | /// Allow to initialize the store in a custom way. \ 229 | /// [Documentation](https://react.dev/reference/react/useReducer) 230 | @external(javascript, "react", "useReducer") 231 | pub fn use_reducer_( 232 | reducer: fn(state, action) -> state, 233 | initializer: initializer, 234 | init: fn(initializer) -> state, 235 | ) -> #(state, fn(action) -> Nil) 236 | 237 | /// Let you add a [state variable](https://react.dev/learn/state-a-components-memory) to your component. \ 238 | /// [Documentation](https://react.dev/reference/react/useState) 239 | @external(javascript, "react", "useState") 240 | pub fn use_state(initial_value: a) -> #(a, fn(a) -> Nil) 241 | 242 | /// Let you add a [state variable](https://react.dev/learn/state-a-components-memory) to your component. 243 | /// Give an `updater` function instead of a state setter. \ 244 | /// [Documentation](https://react.dev/reference/react/useState) 245 | @external(javascript, "react", "useState") 246 | pub fn use_state_(initial_value: a) -> #(a, fn(fn(a) -> a) -> Nil) 247 | 248 | /// Let you add a [state variable](https://react.dev/learn/state-a-components-memory) to your component. 249 | /// Allow to create the initial value in a lazy way. \ 250 | /// [Documentation](https://react.dev/reference/react/useState) 251 | @external(javascript, "react", "useState") 252 | pub fn use_lazy_state(initial_value: fn() -> a) -> #(a, fn(a) -> Nil) 253 | 254 | /// Let you add a [state variable](https://react.dev/learn/state-a-components-memory) to your component. 255 | /// Allow to create the initial value in a lazy way. 256 | /// Give an `updater` function instead of a state setter. \ 257 | /// [Documentation](https://react.dev/reference/react/useState) 258 | @external(javascript, "react", "useState") 259 | pub fn use_lazy_state_(initial_value: fn() -> a) -> #(a, fn(fn(a) -> a) -> Nil) 260 | 261 | /// Let you update the state without blocking the UI. \ 262 | /// [Documentation](https://react.dev/reference/react/useTransition) 263 | @external(javascript, "react", "useTransition") 264 | pub fn use_transition() -> #(Bool, fn() -> Nil) 265 | 266 | // Refs 267 | 268 | /// A Ref is a mutable data stored in React, persisted across renders. 269 | /// They allow to keep track of a DOM node, a component data, or to store a 270 | /// mutable variable in the component, outside of every component lifecycle. \ 271 | /// [Documentation](https://react.dev/learn/referencing-values-with-refs) 272 | pub type Ref(a) 273 | 274 | /// Set the current value of a ref, overriding its existing content. 275 | @external(javascript, "./redraw.ffi.mjs", "setCurrent") 276 | pub fn set_current(of ref: Ref(a), with value: a) -> Nil 277 | 278 | /// Get the current value of a ref. 279 | @external(javascript, "./redraw.ffi.mjs", "getCurrent") 280 | pub fn get_current(from ref: Ref(a)) -> a 281 | 282 | /// Let you reference a value that’s not needed for rendering. 283 | /// Most used ref you'll want to create. They're automatically created to `None`, 284 | /// and can be passed to `ref` prop or `use_imperative_handle`. 285 | /// You probably don't want the ref value to be anything than `Option(a)`, unless 286 | /// you have really strong reasons. \ 287 | /// [Documentation](https://react.dev/reference/react/useRef) 288 | pub fn use_ref() -> Ref(Option(a)) { 289 | use_ref_(option.None) 290 | } 291 | 292 | /// Let you reference a value that’s not needed for rendering. 293 | /// Use `use_ref` if you're trying to acquire a reference to a child or to a 294 | /// component. Use `use_ref_` when you want to keep track of a data, like if 295 | /// you're doing some side-effects, in conjuction with `get_current` and 296 | /// `set_current`. \ 297 | /// [Documentation](https://react.dev/reference/react/useRef) 298 | @external(javascript, "react", "useRef") 299 | pub fn use_ref_(initial_value: a) -> Ref(a) 300 | 301 | /// Let you customize the handle exposed as a [ref](https://react.dev/learn/manipulating-the-dom-with-refs). 302 | /// Use `use_imperative_handle` when you want to customize the data stored in 303 | /// a ref. It's mostly used in conjuction with `forward_ref`. \ 304 | /// [Documentation](https://react.dev/reference/react/useImperativeHandle) 305 | pub fn use_imperative_handle( 306 | ref: Ref(Option(a)), 307 | handler: fn() -> a, 308 | dependencies: b, 309 | ) -> Nil { 310 | use_imperative_handle_(ref, fn() { option.Some(handler()) }, dependencies) 311 | } 312 | 313 | /// Let you customize the handle exposed as a [ref](https://react.dev/learn/manipulating-the-dom-with-refs). 314 | /// Use `use_imperative_handle` by default, unless you really know what you're 315 | /// doing. \ 316 | /// [Documentation](https://react.dev/reference/react/useImperativeHandle) 317 | @external(javascript, "react", "useImperativeHandle") 318 | pub fn use_imperative_handle_( 319 | ref: Ref(a), 320 | handler: fn() -> a, 321 | dependencies: b, 322 | ) -> Nil 323 | 324 | // Contexts 325 | 326 | /// Pass data without props drilling. \ 327 | /// [Documentation](https://react.dev/learn/passing-data-deeply-with-context) 328 | pub type Context(a) 329 | 330 | /// Let you read and subscribe to [context](https://react.dev/learn/passing-data-deeply-with-context) from your component. \ 331 | /// [Documentation](https://react.dev/reference/react/useContext) 332 | @external(javascript, "react", "useContext") 333 | pub fn use_context(context: Context(a)) -> a 334 | 335 | /// Let you create a [context](https://react.dev/learn/passing-data-deeply-with-context) that components can provide or read. \ 336 | /// [Documentation](https://react.dev/reference/react/createContext) 337 | @deprecated("Use redraw/create_context_ instead. redraw/create_context will be removed in 2.0.0. Unusable right now, due to how React handles Context.") 338 | @external(javascript, "react", "createContext") 339 | pub fn create_context(default_value default_value: Option(a)) -> Context(a) 340 | 341 | /// Wrap your components into a context provider to specify the value of this context for all components inside. \ 342 | /// [Documentation](https://react.dev/reference/react/createContext#provider) 343 | @external(javascript, "./context.ffi.mjs", "contextProvider") 344 | pub fn provider( 345 | context context: Context(a), 346 | value value: a, 347 | children children: List(Component), 348 | ) -> Component 349 | 350 | /// Create a [context](https://react.dev/learn/passing-data-deeply-with-context) 351 | /// that components can provide or read. 352 | /// Each context is referenced by its name, a little bit like actors in OTP 353 | /// (if you're familiar with Erlang). Because Gleam cannot execute code outside of 354 | /// `main` function, creating a context should do some side-effect at startup. 355 | /// 356 | /// In traditional React code, Context usage is usually written like this. 357 | /// 358 | /// ```javascript 359 | /// import * as react from 'react' 360 | /// 361 | /// // Create your Context in a side-effectful way. 362 | /// const MyContext = react.createContext(defaultValue) 363 | /// 364 | /// // Create your own provider, wrapping your context. 365 | /// export function MyProvider(props) { 366 | /// return {props.children} 367 | /// } 368 | /// 369 | /// // Create your own hook, to simplify usage of your context. 370 | /// export function useMyContext() { 371 | /// return react.useContext(MyContext) 372 | /// } 373 | /// ``` 374 | /// 375 | /// To simplify and mimic that usage, Redraw wraps Context creation with some 376 | /// caching, to emulate a similar behaviour. 377 | /// 378 | /// ```gleam 379 | /// import redraw 380 | /// 381 | /// const context_name = "MyContextName" 382 | /// 383 | /// pub fn my_provider(children) { 384 | /// let assert Ok(context) = redraw.create_context_(context_name, default_value) 385 | /// redraw.provider(context, value, children) 386 | /// } 387 | /// 388 | /// pub fn use_my_context() { 389 | /// let assert Ok(context) = redraw.get_context(context_name) 390 | /// redraw.use_context(context) 391 | /// } 392 | /// ``` 393 | /// 394 | /// Be careful, `create_context_` fails if the Context is already defined. 395 | /// Choose a full qualified name, hard to overlap with inattention. If 396 | /// you want to get a Context in an idempotent way, take a look at [`context()`](#context). 397 | /// 398 | /// [Documentation](https://react.dev/reference/react/createContext) 399 | @external(javascript, "./context.ffi.mjs", "createContext") 400 | pub fn create_context_( 401 | name: String, 402 | default_value: a, 403 | ) -> Result(Context(a), Error) 404 | 405 | /// Get a context. Because of FFI, `get_context` breaks the type-checker. It 406 | /// should be considered as unsafe code. As a library author, never exposes 407 | /// your context and expect users will call `get_context` themselves, but rather 408 | /// exposes a `use_my_context()` function, handling the type-checking for the 409 | /// user. 410 | /// 411 | /// ```gleam 412 | /// import redraw 413 | /// 414 | /// pub type MyContext { 415 | /// MyContext(value: Int) 416 | /// } 417 | /// 418 | /// /// `use_context` returns `Context(a)`, should it can be safely returned as 419 | /// /// `Context(MyContext)`. 420 | /// pub fn use_my_context() -> redraw.Context(MyContext) { 421 | /// let context = case redraw.get_context("MyContextName") { 422 | /// // Context has been found in the context cache, use it as desired. 423 | /// Ok(context) -> context 424 | /// // Context has not been found. It means the user did not initialised it. 425 | /// Error(_) -> panic as "Unitialised context." 426 | /// } 427 | /// redraw.use_context(context) 428 | /// } 429 | /// ``` 430 | @external(javascript, "./context.ffi.mjs", "getContext") 431 | pub fn get_context(name: String) -> Result(Context(a), Error) 432 | 433 | /// `context` emulates classic Context usage in React. Instead of calling 434 | /// `create_context_` and `get_context`, it's possible to simply call `context`, 435 | /// which will get or create the context directly, and allows to write code as 436 | /// if Context is globally available. `context` also tries to preserve 437 | /// type-checking at most. `context.default_value` is lazily evaluated, meaning 438 | /// no additional computations will ever be run. 439 | /// 440 | /// ```gleam 441 | /// import redraw 442 | /// 443 | /// const context_name = "MyContextName" 444 | /// 445 | /// pub type MyContext { 446 | /// MyContext(count: Int, set_count: fn (Int) -> Nil) 447 | /// } 448 | /// 449 | /// fn default_value() { 450 | /// let count = 0 451 | /// les set_count = fn (_) { Nil } 452 | /// MyContext(count:) 453 | /// } 454 | /// 455 | /// pub fn provider() { 456 | /// use _, children <- redraw.component() 457 | /// let context = redraw.context(context_name, default_value) 458 | /// let #(count, set_count) = redraw.use_state(0) 459 | /// redraw.provider(context, MyContext(count:, set_count:), children) 460 | /// } 461 | /// 462 | /// pub fn use_my_context() { 463 | /// let context = redraw.context(context_name, default_value) 464 | /// redraw.use_context(context) 465 | /// } 466 | /// ``` 467 | /// 468 | /// `context` should never fail, but it can be wrong if you use an already used 469 | /// name. 470 | pub fn context(name: String, default_value: fn() -> a) -> Context(a) { 471 | case get_context(name) { 472 | Ok(context) -> context 473 | Error(get) -> 474 | case create_context_(name, default_value()) { 475 | Ok(context) -> context 476 | Error(create) -> { 477 | let get = " get_context: " <> string.inspect(get) 478 | let create = " create_context_: " <> string.inspect(create) 479 | let head = "[Redraw Internal Error] Unable to find or create context." 480 | let body = 481 | string.join(_, with: " ")([ 482 | "context should never panic.", 483 | "Please, open an issue on https://github.com/ghivert/redraw,", 484 | "and join the error details.\n", 485 | ]) 486 | let details = "Error details:" 487 | let msg = string.join([head, body, details, get, create], "\n") 488 | panic as msg 489 | } 490 | } 491 | } 492 | } 493 | 494 | // API 495 | 496 | /// Test helper to apply pending React updates before making assertions. \ 497 | /// [Documentation](https://react.dev/reference/react/act) 498 | @external(javascript, "react", "act") 499 | pub fn act(act_fn: fn() -> Promise(Nil)) -> Promise(Nil) 500 | 501 | /// Let you update the state without blocking the UI. \ 502 | /// [Documentation](https://react.dev/reference/react/startTransition) 503 | @external(javascript, "react", "startTransition") 504 | pub fn start_transition(scope scope: fn() -> Nil) -> Nil 505 | 506 | // Helpers 507 | 508 | /// Redraw does not support passing key element to components in an easy way like 509 | /// React does. To simplify this, it uses the same API than [Lustre](lustre.build) 510 | /// to put keys on children. 511 | /// ```gleam 512 | /// fn my_component(props, children) { 513 | /// redraw.keyed(my_other_component(props, _), { 514 | /// use item <- list.map(children) 515 | /// #("my-key", item) 516 | /// }) 517 | /// } 518 | /// ``` 519 | pub fn keyed( 520 | element: fn(List(Component)) -> Component, 521 | content: List(#(String, Component)), 522 | ) { 523 | content 524 | |> coerce 525 | |> element 526 | } 527 | 528 | // FFI 529 | // Those functions are used internally by Redraw, to setup things correctly. 530 | // They should not be accessible from the outside world. 531 | 532 | @external(javascript, "./redraw.ffi.mjs", "jsx") 533 | @internal 534 | pub fn jsx(value: a, props: props, children: b) -> Component 535 | 536 | @external(javascript, "./redraw.ffi.mjs", "setFunctionName") 537 | fn set_function_name(a: a, name: String) -> a 538 | 539 | @external(javascript, "./redraw.ffi.mjs", "addProxy") 540 | fn add_proxy(a: fn(props) -> Component) -> fn(props) -> Component 541 | 542 | @external(javascript, "./redraw.ffi.mjs", "addEmptyProxy") 543 | fn add_empty_proxy(a: fn() -> Component) -> fn() -> Component 544 | 545 | @external(javascript, "./redraw.ffi.mjs", "addChildrenForwardRef") 546 | fn add_children_forward_ref( 547 | a: fn(props, Ref(ref), List(Component)) -> Component, 548 | ) -> fn(props, Ref(ref), List(Component)) -> Component 549 | 550 | @external(javascript, "./redraw.ffi.mjs", "addForwardRef") 551 | fn add_forward_ref( 552 | a: fn(props, Ref(ref)) -> Component, 553 | ) -> fn(props, Ref(ref)) -> Component 554 | 555 | @external(javascript, "./redraw.ffi.mjs", "addChildrenProxy") 556 | fn add_children_proxy( 557 | a: fn(props, List(Component)) -> Component, 558 | ) -> fn(props, List(Component)) -> Component 559 | -------------------------------------------------------------------------------- /redraw/src/redraw/error.gleam: -------------------------------------------------------------------------------- 1 | /// Main error type. Currently only used in conjuction with `Context` related 2 | /// functions. 3 | pub type Error { 4 | /// Error returned from `create_context_`. 5 | /// Context with the corresponding `name` already exists. 6 | ExistingContext(name: String) 7 | /// Error returned from `get_context`. 8 | /// Context with the corresponding `name` does not exists. 9 | UnknownContext(name: String) 10 | } 11 | -------------------------------------------------------------------------------- /redraw/src/redraw/event.gleam: -------------------------------------------------------------------------------- 1 | //// Event handlers will receive a React event object. It is also sometimes 2 | //// known as a “synthetic event”. 3 | //// 4 | //// ```gleam 5 | //// import redraw/dom/events 6 | //// import redraw/dom/html 7 | //// 8 | //// pub fn render() { 9 | //// html.button( 10 | //// [ 11 | //// events.on_click(fn (event) { 12 | //// io.debug(event) // React Event 13 | //// }) 14 | //// ], 15 | //// [html.text("Exampe")] 16 | //// ) 17 | //// } 18 | //// ``` 19 | //// 20 | //// It conforms to the same standard as the underlying DOM events, but fixes 21 | //// some browser inconsistencies. 22 | //// 23 | //// Some React events do not map directly to the browser’s native events. 24 | //// For example in `on_mouse_leave`, `event.native_event(event)` will point to a 25 | //// `MouseEvent` event. 26 | //// The specific mapping is not part of the public API and may change in the 27 | //// future. If you need the underlying browser event for some reason, read it 28 | //// from `event.native_event(event)`. 29 | //// 30 | //// Every events have their implementation as `redraw/dom/event/[event_name]`, and 31 | //// their corresponding functions are implemented in those. Every time you need 32 | //// to use one of the functions defined here, use `to_event` from the 33 | //// corresponding module. 34 | //// 35 | //// [Documentation](https://react.dev/reference/react-dom/components/common#react-event-object) 36 | 37 | import gleam/dynamic/decode 38 | 39 | /// Synthetic Event sent by React. 40 | /// It conforms to the same standard as the underlying DOM events, but 41 | /// fixes some browser inconsistencies. \ 42 | /// [Documentation](https://react.dev/reference/react-dom/components/common#react-event-object) 43 | pub type Event 44 | 45 | /// Returns whether the event bubbles through the DOM. \ 46 | /// [Documetation](https://developer.mozilla.org/docs/Web/API/Event/bubbles) 47 | @external(javascript, "../events.ffi.mjs", "bubbles") 48 | pub fn bubbles(event: Event) -> Bool 49 | 50 | /// Returns whether the event can be canceled. \ 51 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/Event/cancelable) 52 | @external(javascript, "../events.ffi.mjs", "cancelable") 53 | pub fn cancelable(event: Event) -> Bool 54 | 55 | /// Returns the node to which the current handler is attached in the React tree. \ 56 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/Event/currentTarget) 57 | @external(javascript, "../events.ffi.mjs", "currentTarget") 58 | pub fn current_target(event: Event) -> decode.Dynamic 59 | 60 | /// Returns whether `prevent_default` was called. \ 61 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/Event/defaultPrevented) 62 | @external(javascript, "../events.ffi.mjs", "defaultPrevented") 63 | pub fn default_prevented(event: Event) -> Bool 64 | 65 | /// Returns which phase the event is currently in. \ 66 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/Event/eventPhase) 67 | @external(javascript, "../events.ffi.mjs", "eventPhase") 68 | pub fn event_phase(event: Event) -> Int 69 | 70 | /// Returns whether the event was initiated by user. \ 71 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/Event/isTrusted) 72 | @external(javascript, "../events.ffi.mjs", "isTrusted") 73 | pub fn is_trusted(event: Event) -> Bool 74 | 75 | /// Returns the node on which the event has occurred (which could be a distant child). \ 76 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/Event/target) 77 | @external(javascript, "../events.ffi.mjs", "target") 78 | pub fn target(event: Event) -> decode.Dynamic 79 | 80 | /// Returns the time when the event occurred. \ 81 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/Event/timeStamp) 82 | @external(javascript, "../events.ffi.mjs", "timeStamp") 83 | pub fn time_stamp(event: Event) -> Int 84 | 85 | /// The original browser event object. \ 86 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/Event) 87 | @external(javascript, "../events.ffi.mjs", "nativeEvent") 88 | pub fn native_event(event: Event) -> decode.Dynamic 89 | 90 | /// Prevents the default browser action for the event. \ 91 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/Event/preventDefault) 92 | @external(javascript, "../events.ffi.mjs", "preventDefault") 93 | pub fn prevent_default(event: Event) -> Event 94 | 95 | /// Stops the event propagation through the React tree. \ 96 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/Event/stopPropagation) 97 | @external(javascript, "../events.ffi.mjs", "stopPropagation") 98 | pub fn stop_propagation(event: Event) -> Event 99 | 100 | /// Returns a boolean value indicating whether `prevent_default` was called. \ 101 | /// React addition, does not exist in standard browsers. 102 | @external(javascript, "../events.ffi.mjs", "isDefaultPrevented") 103 | pub fn is_default_prevented(event: Event) -> Bool 104 | 105 | /// Returns a boolean value indicating whether `stop_propagation` was called. 106 | /// React addition, does not exist in standard browsers. 107 | @external(javascript, "../events.ffi.mjs", "isPropagationStopped") 108 | pub fn is_propagation_stopped(event: Event) -> Bool 109 | 110 | /// Not used with React DOM. With React Native, call this to read event’s properties after the event. \ 111 | /// React addition, does not exist in standard browsers. 112 | @external(javascript, "../events.ffi.mjs", "persist") 113 | pub fn persist(event: Event) -> Event 114 | 115 | /// Not used with React DOM. With React Native, returns whether persist has been called. \ 116 | /// React addition, does not exist in standard browsers. 117 | @external(javascript, "../events.ffi.mjs", "isPersistent") 118 | pub fn is_persistent(event: Event) -> Bool 119 | -------------------------------------------------------------------------------- /redraw/src/redraw/internals/coerce.gleam: -------------------------------------------------------------------------------- 1 | @external(javascript, "../../redraw.ffi.mjs", "coerce") 2 | pub fn coerce(a: a) -> b 3 | -------------------------------------------------------------------------------- /redraw/test/redraw_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit 2 | import gleeunit/should 3 | 4 | pub fn main() { 5 | gleeunit.main() 6 | } 7 | 8 | // gleeunit test functions end in `_test` 9 | pub fn hello_world_test() { 10 | 1 11 | |> should.equal(1) 12 | } 13 | -------------------------------------------------------------------------------- /redraw_dom/.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | /build 4 | erl_crash.dump 5 | -------------------------------------------------------------------------------- /redraw_dom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.0.0 - 2024-12-24 2 | 3 | - Bump dependencies versions requirements. 4 | - Change `create_root`, `hydrate_root` & `create_portal` to return 5 | `Result(a, Nil)` instead of `a`. Those functions are now nicer to use, and can 6 | easily be overriden using `let assert`. 7 | - Rename modules to improve namespacing. `create_root`, `hydrate_root` & 8 | `render` now belongs to `redraw/dom/client`. `create_portal` & `flush_sync` 9 | belongs to `redraw/dom`. 10 | - Add DOM elements, attributes and events handling in `react/dom` namespace from 11 | `redraw`. 12 | 13 | ## v1.1.0 - 2024-08-12 14 | 15 | - Improve documentation. 16 | - Stay in sync with `redraw` core package. 17 | 18 | ## v1.0.0 - 2024-07-21 19 | 20 | - First release of `redraw_dom`! 🎉 21 | - Redraw DOM allows a simple access to browsers DOM, and provides 22 | browser-relative components. 23 | -------------------------------------------------------------------------------- /redraw_dom/LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Guillaume Hivert 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /redraw_dom/README.md: -------------------------------------------------------------------------------- 1 | # Redraw DOM 2 | 3 | Redraw DOM is the renderer for Redraw, for browser. Take a look at 4 | [Redraw](https://hexdocs.pm/redraw) to get the whole package. 5 | -------------------------------------------------------------------------------- /redraw_dom/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "redraw_dom" 2 | target = "javascript" 3 | version = "2.0.0" 4 | 5 | # Fill out these fields if you intend to generate HTML documentation or publish 6 | # your project to the Hex package manager. 7 | 8 | description = "Redraw DOM renderer. Take a look at Redraw to use it." 9 | licences = ["MIT"] 10 | links = [{title = "Sponsor", href = "https://github.com/sponsors/ghivert"}] 11 | 12 | [repository] 13 | type = "github" 14 | user = "ghivert" 15 | repo = "redraw" 16 | path = "redraw_dom" 17 | 18 | # For a full reference of all the available options, you can have a look at 19 | # https://gleam.run/writing-gleam/gleam-toml/. 20 | 21 | [dependencies] 22 | gleam_stdlib = ">= 0.51.0 and < 2.0.0" 23 | redraw = ">= 2.0.0 and < 3.0.0" 24 | 25 | [dev-dependencies] 26 | gleeunit = ">= 1.0.0 and < 2.0.0" 27 | -------------------------------------------------------------------------------- /redraw_dom/manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" }, 6 | { name = "gleam_stdlib", version = "0.51.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "14AFA8D3DDD7045203D422715DBB822D1725992A31DF35A08D97389014B74B68" }, 7 | { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 8 | { name = "redraw", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "redraw", source = "hex", outer_checksum = "FF52D8626E1E6DC92EB8BC9DC8C70BC6F0E25824524A7C0658222EA406B5BE23" }, 9 | ] 10 | 11 | [requirements] 12 | gleam_stdlib = { version = ">= 0.51.0 and < 2.0.0" } 13 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 14 | redraw = { version = ">= 2.0.0 and < 3.0.0" } 15 | -------------------------------------------------------------------------------- /redraw_dom/src/attribute.ffi.mjs: -------------------------------------------------------------------------------- 1 | export function toProps(attributes) { 2 | const props = {} 3 | for (const item of attributes) { 4 | props[item.key] = item.content 5 | } 6 | return props 7 | } 8 | -------------------------------------------------------------------------------- /redraw_dom/src/client.ffi.mjs: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client" 2 | import * as gleam from "./gleam.mjs" 3 | 4 | export function createRoot(value) { 5 | const node = document.getElementById(value) 6 | if (!node) return new gleam.Error() 7 | return new gleam.Ok(ReactDOM.createRoot(node)) 8 | } 9 | 10 | export function hydrateRoot(value, content) { 11 | const node = document.getElementById(value) 12 | if (!node) return new gleam.Error() 13 | return new gleam.Ok(ReactDOM.hydrateRoot(node, content)) 14 | } 15 | 16 | export function createPortal(children, root) { 17 | const node = document.getElementById(root) 18 | if (!node) return new gleam.Error() 19 | return new gleam.Ok(ReactDOM.createPortal(children, node)) 20 | } 21 | 22 | export function render(root, children) { 23 | return root.render(children) 24 | } 25 | -------------------------------------------------------------------------------- /redraw_dom/src/dom.ffi.mjs: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client" 2 | import * as gleam from "./gleam.mjs" 3 | 4 | export function createPortal(children, root) { 5 | const node = document.getElementById(root) 6 | if (!node) return new gleam.Error() 7 | return new gleam.Ok(ReactDOM.createPortal(children, node)) 8 | } 9 | 10 | /** Turns a `List(#(String, String))` into an object `{ [key: string]: string }` 11 | * to conform with the React `style` API. */ 12 | export function convertStyle(styles) { 13 | const styles_ = {} 14 | for (const style of styles) { 15 | styles_[camelize(style[0])] = style[1] 16 | } 17 | return styles_ 18 | } 19 | 20 | /** Used to camelize CSS property names. */ 21 | function camelize(key) { 22 | return key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) 23 | } 24 | 25 | export function innerHTML(html) { 26 | return { __html: html } 27 | } 28 | -------------------------------------------------------------------------------- /redraw_dom/src/events.ffi.mjs: -------------------------------------------------------------------------------- 1 | export function animationName(event) { 2 | return event.animationName 3 | } 4 | 5 | export function elapsedTime(event) { 6 | return event.elapsedTime 7 | } 8 | 9 | export function pseudoElement(event) { 10 | return event.pseudoElement 11 | } 12 | 13 | export function clipboardData(event) { 14 | return event.clipboardData 15 | } 16 | 17 | export function detail(event) { 18 | return event.detail 19 | } 20 | 21 | export function view(event) { 22 | return event.view 23 | } 24 | 25 | export function dataTransfer(event) { 26 | return event.dataTransfer 27 | } 28 | 29 | export function relatedTarget(event) { 30 | return event.relatedTarget 31 | } 32 | 33 | export function data(event) { 34 | return event.data 35 | } 36 | 37 | export function altKey(event) { 38 | return event.altKey 39 | } 40 | 41 | export function code(event) { 42 | return event.code 43 | } 44 | 45 | export function button(event) { 46 | return event.button 47 | } 48 | 49 | export function buttons(event) { 50 | return event.buttons 51 | } 52 | 53 | export function ctrlKey(event) { 54 | return event.ctrlKey 55 | } 56 | 57 | export function clientX(event) { 58 | return event.clientX 59 | } 60 | 61 | export function clientY(event) { 62 | return event.clientY 63 | } 64 | 65 | export function offsetX(event) { 66 | return event.offsetX 67 | } 68 | 69 | export function offsetY(event) { 70 | return event.offsetY 71 | } 72 | 73 | export function metaKey(event) { 74 | return event.metaKey 75 | } 76 | 77 | export function key(event) { 78 | return event.key 79 | } 80 | 81 | export function locale(event) { 82 | return event.locale 83 | } 84 | 85 | export function location(event) { 86 | return event.location 87 | } 88 | 89 | export function repeat(event) { 90 | return event.repeat 91 | } 92 | 93 | export function movementX(event) { 94 | return event.movementX 95 | } 96 | 97 | export function movementY(event) { 98 | return event.movementY 99 | } 100 | 101 | export function pageX(event) { 102 | return event.pageX 103 | } 104 | 105 | export function pageY(event) { 106 | return event.pageY 107 | } 108 | 109 | export function screenX(event) { 110 | return event.screenX 111 | } 112 | 113 | export function screenY(event) { 114 | return event.screenY 115 | } 116 | 117 | export function shiftKey(event) { 118 | return event.shiftKey 119 | } 120 | 121 | export function height(event) { 122 | return event.height 123 | } 124 | 125 | export function width(event) { 126 | return event.width 127 | } 128 | 129 | export function isPrimary(event) { 130 | return event.isPrimary 131 | } 132 | 133 | export function altitudeAngle(event) { 134 | return event.altitudeAngle 135 | } 136 | 137 | export function azimuthAngle(event) { 138 | return event.azimuthAngle 139 | } 140 | 141 | export function pointerId(event) { 142 | return event.pointerId 143 | } 144 | 145 | export function pointerType(event) { 146 | return event.pointerType 147 | } 148 | 149 | export function pressure(event) { 150 | return event.pressure 151 | } 152 | 153 | export function tangentialPressure(event) { 154 | return event.tangentialPressure 155 | } 156 | 157 | export function tiltX(event) { 158 | return event.tiltX 159 | } 160 | 161 | export function tiltY(event) { 162 | return event.tiltY 163 | } 164 | 165 | export function twist(event) { 166 | return event.twist 167 | } 168 | 169 | export function changedTouches(event) { 170 | return event.changedTouches 171 | } 172 | 173 | export function touches(event) { 174 | return event.touches 175 | } 176 | 177 | export function targetTouches(event) { 178 | return event.targetTouches 179 | } 180 | 181 | export function propertyName(event) { 182 | return event.propertyName 183 | } 184 | 185 | export function deltaMode(event) { 186 | return event.deltaMode 187 | } 188 | 189 | export function deltaX(event) { 190 | return event.deltaX 191 | } 192 | 193 | export function deltaY(event) { 194 | return event.deltaY 195 | } 196 | 197 | export function deltaZ(event) { 198 | return event.deltaZ 199 | } 200 | 201 | export function getModifierState(event, key) { 202 | return event.getModifierState(key) 203 | } 204 | -------------------------------------------------------------------------------- /redraw_dom/src/redraw/dom.gleam: -------------------------------------------------------------------------------- 1 | import redraw.{type Component} 2 | 3 | /// Let you render some children into a different part of the DOM. 4 | /// Contrarily to JavaScript, `create_portal` returns a `Result` to avoid runtime 5 | /// error. Indeed, when the provided root does not exist in your HTML, `create_portal` 6 | /// fails. You should never assume `create_portal` will work out-of-the-box when 7 | /// you're building a library. Otherwise, you could assert the resulting 8 | /// value in your application. 9 | /// 10 | /// ```gleam 11 | /// import redraw 12 | /// import redraw/dom/client 13 | /// import redraw/dom/html 14 | /// 15 | /// pub fn main() { 16 | /// let assert Ok(root) = client.create_root("app") 17 | /// client.render(app) 18 | /// } 19 | /// 20 | /// fn app() { 21 | /// let modal = modal() 22 | /// use <- redraw.component__("App") 23 | /// let assert Ok(modal) = client.create_portal(modal, "modal") 24 | /// html.div([], [html.text("Hello World!"), modal]) 25 | /// } 26 | /// 27 | /// fn modal() { 28 | /// use <- redraw.component__("Modal") 29 | /// html.div([], [html.text("Inside the modal!")]) 30 | /// } 31 | /// ``` 32 | /// 33 | /// [Documentation](https://react.dev/reference/react-dom/createPortal) 34 | @external(javascript, "../dom.ffi.mjs", "createPortal") 35 | pub fn create_portal( 36 | children: Component, 37 | root: String, 38 | ) -> Result(Component, Nil) 39 | 40 | /// Call `flushSync` to force React to flush any pending work and update the DOM 41 | /// synchronously. \ 42 | /// 43 | /// ```gleam 44 | /// import redraw 45 | /// import redraw/dom 46 | /// import redraw/dom/events 47 | /// 48 | /// type Action { 49 | /// Increment 50 | /// Decrement 51 | /// } 52 | /// 53 | /// fn counter() { 54 | /// use <- redraw.component__() 55 | /// let #(state, set_state) = redraw.set_state(0) 56 | /// let on_click = fn(type_: Action) { 57 | /// events.on_click(fn (event) { 58 | /// // Calling flush_sync forces the DOM to refresh. 59 | /// dom.flush_sync(fn () { 60 | /// set_state(case type_ { 61 | /// Increment -> state + 1 62 | /// Decrement -> state - 1 63 | /// }) 64 | /// }) 65 | /// }) 66 | /// } 67 | /// html.div([], [ 68 | /// html.div([], [html.text("-")]), 69 | /// html.div([], [html.text(int.to_string(state))]), 70 | /// html.div([], [html.text("+")]), 71 | /// ]) 72 | /// } 73 | /// ``` 74 | /// 75 | /// Most of the time, flushSync can be avoided. Use flushSync as last resort. 76 | /// 77 | /// [Documentation](https://react.dev/reference/react-dom/flushSync) 78 | @external(javascript, "react-dom/client", "flushSync") 79 | pub fn flush_sync(callback: fn() -> Nil) -> Nil 80 | -------------------------------------------------------------------------------- /redraw_dom/src/redraw/dom/attribute.gleam: -------------------------------------------------------------------------------- 1 | //// Implementation for HTML and SVG attributes usable in React and in browser. 2 | //// Contrarily to Lustre, every attributes can take arbitrary data, because 3 | //// they're directly bind in the underlying component's props. 4 | //// 5 | //// All available attributes can be found in the 6 | //// [React.dev](https://react.dev/reference/react-dom/components/common#common) 7 | //// documentation for detailed information, as well as on 8 | //// [MDN](https://developer.mozilla.org/docs/Web/API/Element). 9 | 10 | import gleam/dynamic.{type Dynamic} 11 | import gleam/option 12 | import gleam/string 13 | import redraw 14 | 15 | /// Attribute linked on an HTML or SVG node. Think about like a `prop` in React. 16 | pub opaque type Attribute { 17 | Attribute(key: String, content: Dynamic) 18 | } 19 | 20 | pub fn attribute(key: String, content: a) -> Attribute { 21 | Attribute(key: key, content: dynamic.from(content)) 22 | } 23 | 24 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/HTMLAnchorElement/href) 25 | pub fn href(url: String) -> Attribute { 26 | attribute("href", url) 27 | } 28 | 29 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/HTMLAnchorElement/target) 30 | pub fn target(value: String) -> Attribute { 31 | attribute("target", value) 32 | } 33 | 34 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/src) 35 | pub fn src(value: String) -> Attribute { 36 | attribute("src", value) 37 | } 38 | 39 | /// [Documentation](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/class) 40 | pub fn class(value: String) -> Attribute { 41 | attribute("className", value) 42 | } 43 | 44 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/alt) 45 | pub fn alt(value: String) -> Attribute { 46 | attribute("alt", value) 47 | } 48 | 49 | /// Inner HTML data are HTML that will not be verified sanitized. 50 | /// Be careful when using it. You should almost always prefer to use 51 | /// `redraw/element/html`. 52 | pub type InnerHTML 53 | 54 | /// The `inner_html` data should be created as close to where the HTML is 55 | /// generated as possible. This ensures that all raw HTML being used in your 56 | /// code is explicitly marked as such, and that only variables that you expect 57 | /// to contain HTML are passed to `dangerously_set_inner_html`. It is not 58 | /// recommended to create the object inline like 59 | /// `html.div([attribute.dangerously_set_inner_html(attribute.inner_html(markup))], [])` \ 60 | /// [Documentation](https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html) 61 | @external(javascript, "../../dom.ffi.mjs", "innerHTML") 62 | pub fn inner_html(html: String) -> InnerHTML 63 | 64 | /// Overrides the innerHTML property of the DOM node and displays the passed 65 | /// HTML inside. This should be used with extreme caution! If the HTML inside 66 | /// isn’t trusted (for example, if it’s based on user data), you risk 67 | /// introducing an XSS vulnerability. \ 68 | /// [Documentation](https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html) 69 | pub fn dangerously_set_inner_html(inner_html: InnerHTML) -> Attribute { 70 | attribute("dangerouslySetInnerHTML", inner_html) 71 | } 72 | 73 | /// A ref object from `redraw.use_ref`. Your ref will be filled with the DOM 74 | /// element for this node. Contrarily to JS React, when using a Ref, you're 75 | /// forced to use an optional type here. Because when using this function, you 76 | /// want to get a reference from a real DOM node, meaning at the initialization 77 | /// of the reference, you won't have any data. Use `ref_` when you want full 78 | /// control over the ref you send to the Component. \ 79 | /// [Documentation](https://react.dev/reference/react-dom/components/common#manipulating-a-dom-node-with-a-ref) 80 | pub fn ref(ref: redraw.Ref(option.Option(a))) -> Attribute { 81 | attribute("ref", fn(dom_ref) -> Nil { 82 | redraw.set_current(ref, option.Some(dom_ref)) 83 | }) 84 | } 85 | 86 | /// A ref callback function. The callback will be provided with the DOM element 87 | /// for this node. Use this function to get control on the ref provided by the 88 | /// DOM node or the component. \ 89 | /// [Documentation](https://react.dev/reference/react-dom/components/common#manipulating-a-dom-node-with-a-ref) 90 | pub fn ref_(ref: fn(a) -> Nil) -> Attribute { 91 | attribute("ref", ref) 92 | } 93 | 94 | /// If `True`, suppresses the warning that React shows for elements that both 95 | /// have `children` and `content_editable(True)` (which normally do not work 96 | /// together). Use this if you’re building a text input library that manages 97 | /// the `contentEditable` content manually. 98 | pub fn suppress_content_editable_warning(value: Bool) -> Attribute { 99 | attribute("suppressContentEditableWarning", value) 100 | } 101 | 102 | /// If you use [server rendering](https://react.dev/reference/react-dom/server), 103 | /// normally there is a warning when the server and the client render different 104 | /// content. In some rare cases (like timestamps), it is very hard or impossible 105 | /// to guarantee an exact match. If you set suppressHydrationWarning to true, 106 | /// React will not warn you about mismatches in the attributes and the content 107 | /// of that element. It only works one level deep, and is intended to be used 108 | /// as an escape hatch. Don’t overuse it. 109 | /// [Read about suppressing hydration errors.](https://react.dev/reference/react-dom/client/hydrateRoot#suppressing-unavoidable-hydration-mismatch-errors) 110 | pub fn suppress_hydration_warning(value: Bool) -> Attribute { 111 | attribute("suppressHydrationWarning", value) 112 | } 113 | 114 | @external(javascript, "../../dom.ffi.mjs", "convertStyle") 115 | fn convert_style(styles: List(#(String, String))) -> a 116 | 117 | /// [Documentation](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/style) 118 | pub fn style(styles: List(#(String, String))) -> Attribute { 119 | attribute("style", convert_style(styles)) 120 | } 121 | 122 | /// Set aria attribute on the node. Should be used like `aria("valuenow", "75")`. 123 | pub fn aria(key: String, value: String) -> Attribute { 124 | attribute("aria-" <> key, value) 125 | } 126 | 127 | /// [Documentation](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/accesskey) 128 | pub fn access_key(value: String) -> Attribute { 129 | attribute("accessKey", value) 130 | } 131 | 132 | /// [Documentation](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/autocapitalize) 133 | pub fn auto_capitalize(value: String) -> Attribute { 134 | attribute("autoCapitalize", value) 135 | } 136 | 137 | /// Alias of `class`. \ 138 | /// [Documentation](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/class) 139 | pub fn class_name(value: String) -> Attribute { 140 | attribute("className", value) 141 | } 142 | 143 | /// If true, the browser lets the user edit the rendered element directly. 144 | /// This is used to implement rich text input libraries like Lexical. React 145 | /// warns if you try to pass React children to an element with 146 | /// `content_editable(True)` because React will not be able to update its content 147 | /// after user edits. \ 148 | /// [Documentation](https://developer.mozilla.org/docs/Web/API/HTML/Global_attributes/contenteditable) 149 | pub fn content_editable(value: Bool) -> Attribute { 150 | attribute("contentEditable", value) 151 | } 152 | 153 | /// Data attributes let you attach some string data to the element, for example 154 | /// `data("fruit", "banana")`. In React, they are not commonly used because you 155 | /// would usually read data from props or state instead. \ 156 | /// [Documentation](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/data-*) 157 | pub fn data(key: String, value: String) -> Attribute { 158 | attribute("data-" <> key, value) 159 | } 160 | 161 | /// Directionality of an element text. 162 | pub type Dir { 163 | Ltr 164 | Rtl 165 | } 166 | 167 | /// Specifies the text direction of the element. \ 168 | /// [Documentation](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/dir) 169 | pub fn dir(value: Dir) -> Attribute { 170 | let value = case value { 171 | Ltr -> "ltr" 172 | Rtl -> "rtl" 173 | } 174 | attribute("dir", value) 175 | } 176 | 177 | /// Specifies whether the element is draggable. Part of HTML Drag and Drop API. \ 178 | /// [Documentation](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/draggable) 179 | pub fn draggable(value: Bool) -> Attribute { 180 | attribute("draggable", value) 181 | } 182 | 183 | /// Specifies which action to present for the enter key on virtual keyboards.\ 184 | /// [Documentation](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/enterkeyhint) 185 | pub fn enter_key_hint(value: String) -> Attribute { 186 | attribute("enterKeyHint", value) 187 | } 188 | 189 | /// For