├── .envrc ├── .ghci ├── .ghcjsi ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md └── workflows │ └── main.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── Setup.hs ├── bun.lock ├── bunfig.toml ├── cabal.config ├── cabal.project ├── default.nix ├── docker ├── Dockerfile ├── README.md └── docker-compose.yml ├── docs └── Internals.md ├── eslint.config.js ├── examples ├── .envrc ├── .ghci ├── LICENSE ├── canvas2d │ └── Main.hs ├── components │ └── Main.hs ├── fetch │ └── Main.hs ├── file-reader │ └── Main.hs ├── mario │ ├── Main.hs │ └── imgs │ │ └── mario.png ├── mathml │ └── Main.hs ├── miso-examples.cabal ├── router │ └── Main.hs ├── shell.nix ├── simple │ └── Main.hs ├── sse │ ├── .envrc │ ├── .ghci │ ├── LICENSE │ ├── client │ │ └── Main.hs │ ├── default.nix │ ├── nix │ │ ├── machine.nix │ │ └── module.nix │ ├── server │ │ └── Main.hs │ ├── shared │ │ └── Common.hs │ ├── shell.nix │ └── sse.cabal ├── svg │ └── Main.hs ├── three │ ├── Main.hs │ └── index.html ├── todo-mvc │ └── Main.hs ├── wasm-hello-world │ └── Main.hs └── websocket │ └── Main.hs ├── haskell-miso.org ├── .ghci ├── ChangeLog.md ├── LICENSE ├── Setup.hs ├── client │ ├── .envrc │ ├── DevelMain.hs │ ├── Main.hs │ └── shell.nix ├── default.nix ├── haskell-miso.cabal ├── nix │ ├── aws.nix │ ├── machine.nix │ ├── module.nix │ └── nginx.nix ├── server │ ├── .envrc │ ├── Main.hs │ └── shell.nix └── shared │ └── Common.hs ├── js ├── miso.js └── miso.prod.js ├── jsstring-src └── Miso │ └── String.hs ├── logo ├── favicon.ico └── miso.png ├── miso.cabal ├── nix ├── default.nix ├── haskell │ └── packages │ │ ├── ghc │ │ └── default.nix │ │ └── ghcjs │ │ └── default.nix ├── legacy │ ├── default.nix │ ├── haskell │ │ └── packages │ │ │ ├── ghc │ │ │ └── default.nix │ │ │ └── ghcjs │ │ │ └── default.nix │ ├── nixpkgs.json │ └── overlay.nix ├── nixpkgs.json ├── overlay.nix ├── source.nix └── wasm │ └── default.nix ├── package.json ├── sample-app ├── .gitignore ├── Main.hs ├── app.cabal └── default.nix ├── shell.nix ├── src ├── Miso.hs └── Miso │ ├── Canvas.hs │ ├── Concurrent.hs │ ├── Delegate.hs │ ├── Diff.hs │ ├── Effect.hs │ ├── Event.hs │ ├── Event │ ├── Decoder.hs │ └── Types.hs │ ├── Exception.hs │ ├── FFI.hs │ ├── FFI │ └── Internal.hs │ ├── Fetch.hs │ ├── Html.hs │ ├── Html │ ├── Element.hs │ ├── Event.hs │ ├── Property.hs │ └── Types.hs │ ├── Internal.hs │ ├── Lens.hs │ ├── Lens │ └── TH.hs │ ├── Mathml.hs │ ├── Mathml │ ├── Element.hs │ └── Property.hs │ ├── Media.hs │ ├── Property.hs │ ├── Render.hs │ ├── Router.hs │ ├── Run.hs │ ├── Storage.hs │ ├── Style.hs │ ├── Style │ └── Color.hs │ ├── Subscription.hs │ ├── Subscription │ ├── History.hs │ ├── Keyboard.hs │ ├── Mouse.hs │ ├── SSE.hs │ ├── WebSocket.hs │ └── Window.hs │ ├── Svg.hs │ ├── Svg │ ├── Element.hs │ ├── Event.hs │ └── Property.hs │ ├── Types.hs │ └── Util.hs ├── text-src └── Miso │ └── String.hs ├── ts ├── README.md ├── happydom.ts ├── index.ts ├── miso.spec.ts ├── miso.ts ├── miso │ ├── dom.ts │ ├── event.ts │ ├── hydrate.ts │ ├── smart.ts │ ├── types.ts │ └── util.ts └── spec │ ├── component.spec.ts │ ├── dom.spec.ts │ ├── event.spec.ts │ ├── hydrate.spec.ts │ └── util.spec.ts └── tsconfig.json /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.ghci: -------------------------------------------------------------------------------- 1 | :set prompt ">>> " 2 | :set prompt2 "... " 3 | :set -isrc 4 | :set -Wall 5 | -------------------------------------------------------------------------------- /.ghcjsi: -------------------------------------------------------------------------------- 1 | :set prompt ">>> " 2 | :set prompt2 "... " 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: miso 4 | github: dmjio 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Miso CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | env: 9 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | if: github.ref != 'refs/heads/master' 15 | steps: 16 | - uses: DeterminateSystems/nix-installer-action@main 17 | - uses: actions/checkout@v3.5.3 18 | - uses: cachix/install-nix-action@v25 19 | with: 20 | nix_path: nixpkgs=channel:nixpkgs-unstable 21 | - name: Cancel Previous Runs 22 | uses: styfle/cancel-workflow-action@0.9.1 23 | with: 24 | access_token: ${{ github.token }} 25 | - uses: cachix/cachix-action@v16 26 | with: 27 | name: haskell-miso-cachix 28 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 29 | 30 | - name: Nix channel update 31 | run: nix-channel --update 32 | 33 | - name: Bun install 34 | run: nix-env -iA bun -f . && bun install 35 | 36 | - name: Miso.ts tests 37 | run: bun run test 38 | 39 | - name: Bun build prod 40 | run: bun run build && bun run prod 41 | 42 | - name: (JS) Miso GHCJS (GHCJS 8.6) 43 | run: nix-build -A miso-ghcjs 44 | 45 | - name: (JS) Miso GHCJS Production (GHCJS 8.6) 46 | run: nix-build -A miso-ghcjs-prod 47 | 48 | - name: (x86) Miso GHC (GHC 8.6.5) 49 | run: nix-build -A miso-ghc 50 | 51 | - name: (JS) Miso GHCJS (GHC 9.12.2) 52 | run: nix-build -A miso-ghcjs-9122 53 | 54 | - name: (x86) Miso GHC (GHC 9.12.2) 55 | run: nix-build -A miso-ghc-9122 56 | 57 | - name: (x86) Haskell-miso.org server (GHC 8.6.5) 58 | run: nix-build -A haskell-miso-server 59 | 60 | - name: (JS) Haskell-miso.org client (GHCJS 8.6) 61 | run: nix-build -A haskell-miso-client 62 | 63 | - name: (x86) sse.haskell-miso.org server (GHC 8.6.5) 64 | run: nix-build -A sse-server 65 | 66 | - name: (JS) sse.haskell-miso.org client (GHCJS 8.6) 67 | run: nix-build -A sse-client 68 | 69 | - name: (x86) Miso examples (GHC 8.6.5) 70 | run: nix-build -A miso-examples-ghc 71 | 72 | - name: (x86) Miso examples (GHC 9.12.2) 73 | run: nix-build -A miso-examples-ghc-9122 74 | 75 | - name: (JS) Miso examples (GHCJS 9.12.2) 76 | run: nix-build -A miso-examples-ghcjs-9122 77 | 78 | - name: (JS) Miso third-party examples (2048, flatris, etc.) (GHCJS 8.6.5) 79 | run: nix-build -A more-examples -j1 80 | 81 | - name: (JS) Miso sample app (GHCJS 8.6) 82 | run: nix-build -A sample-app-js 83 | 84 | - name: (x86) Miso sample app jsaddle (GHC 8.6.5) 85 | run: nix-build -A sample-app 86 | 87 | - name: (x86) Nix garbage collect 88 | run: nix-collect-garbage -d 89 | 90 | - name: (WASM) Miso examples (GHC 9.12.2) 91 | run: nix-build -A wasmExamples && ./result/bin/build.sh 92 | 93 | - name: (x86) NixOS test runner for haskell-miso.org (GHC 8.6.5) 94 | run: nix-build -A haskell-miso-org-test 95 | 96 | - name: (x86) Nix garbage collect 97 | run: nix-collect-garbage -d 98 | 99 | - name: Miso Haddocks 100 | run: nix-shell --run 'cabal update && cabal haddock-project' 101 | 102 | - name: (x86) Nginx example hosting check 103 | run: nix-build -A nginx-nixos-test --dry-run 104 | 105 | deploy: 106 | runs-on: ubuntu-latest 107 | if: github.ref == 'refs/heads/master' 108 | steps: 109 | - uses: DeterminateSystems/nix-installer-action@v12 110 | - uses: actions/checkout@v3.5.3 111 | - uses: cachix/install-nix-action@v25 112 | with: 113 | nix_path: nixpkgs=channel:nixpkgs-unstable 114 | - uses: cachix/cachix-action@v16 115 | with: 116 | name: haskell-miso-cachix 117 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 118 | 119 | - name: Nix channel update 120 | run: nix-channel --update 121 | 122 | - name: Bun install 123 | run: nix-env -iA bun -f . && bun install 124 | 125 | - name: Bun build 126 | run: bun run build 127 | 128 | - name: Build 129 | run: nix-build -A miso-ghcjs 130 | 131 | - name: Miso Haddocks 132 | run: nix-shell --run 'cabal update && cabal haddock-project' 133 | 134 | - name: Miso.ts test coverage 135 | run: bun run test 136 | 137 | - name: Deploy 138 | run: nix-build -A deploy -j1 && ./result 139 | env: 140 | AWS_SECRET_ACCESS_KEY: '${{ secrets.AWS_SECRET_ACCESS_KEY }}' 141 | AWS_ACCESS_KEY_ID: '${{ secrets.AWS_ACCESS_KEY_ID }}' 142 | DEPLOY: '${{ secrets.DEPLOY }}' 143 | EMAIL: '${{ secrets.EMAIL }}' 144 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # haskell 2 | .stack-work/ 3 | dist-newstyle/ 4 | cabal.project.local 5 | .ghc.environment.* 6 | dist 7 | *.o 8 | *.hi 9 | *.jsexe 10 | 11 | # nix 12 | result* 13 | 14 | # emacs 15 | *~ 16 | 17 | # darwin 18 | .DS_Store 19 | 20 | # misc 21 | TAGS 22 | tags 23 | .direnv 24 | 25 | # js 26 | package-lock.json 27 | node_modules/ 28 | coverage/ 29 | 30 | # typescript 31 | *.js 32 | 33 | # haddocks 34 | haddocks/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at code@dmj.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ================================= 3 | All contributions are appreciated and welcome. 4 | 5 | ## Guidelines 6 | Please note we have a [Code of Conduct](https://github.com/dmjio/miso/blob/master/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 7 | 8 | A good guideline for going about contributing to a Haskell project is [Neil Mitchell's](https://github.com/ndmitchell) "Drive-by-Haskell Contributions" presentation. 9 | 10 | [video](https://www.youtube.com/watch?v=6kGLHXsUQD4) / [slides](http://ndmitchell.com/downloads/slides-drive-by_haskell_contributions-09_jun_2017.pdf) 11 | 12 | ## When making PR's 13 | - Do fork `master` 14 | - Do rebase `squash` / `fixup` as you see fit (so as to keep changes atomic) 15 | - Do include documentation (where applicable) 16 | - Do include tests (where applicable) 17 | - Don't bump the package version, that will be handled when a release is made 18 | - Don't just make one-off formatting or stylistic changes (i.e. rearrange the deck chairs) 19 | - Do have fun, use emojis :ramen: 20 | 21 | ## Call for participation 22 | #### Things `miso` really needs help with 23 | - Better Documentation 24 | - More [Haddocks](https://haddocks.haskell-miso.org) 25 | - Better "Quick Start" guides 26 | - Getting started with `nix` 27 | - `client` / `server` setup walkthrough guide (for both `nix`, `stack`, `cabal`) 28 | - Introductory materials 29 | - Blog posts 30 | - User experiences, comparisons 31 | - JS minification, cabal file setup, configuration walk-throughs 32 | - Press 33 | - Get the word out, blog, etc. 34 | - Testimonials 35 | - Are you using `miso` in any capacity? Please let us know! 36 | - Anything listed as `low-hanging-fruit` / `help-wanted` in the [issues](https://github.com/dmjio/miso/issues) 37 | - Anything you think this project would benefit greatly from 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2025, David M. Johnson 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [test] 2 | preload = "./ts/happydom.ts" 3 | coverage = true 4 | coverageReporter = ["text", "lcov"] 5 | coverageSkipTestFiles = true 6 | -------------------------------------------------------------------------------- /cabal.config: -------------------------------------------------------------------------------- 1 | compiler: ghcjs 2 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: 2 | . 3 | examples/ 4 | examples/sse/ 5 | haskell-miso.org/ 6 | sample-app/ 7 | 8 | index-state: 2025-05-01T20:28:51Z 9 | 10 | allow-newer: 11 | all:base 12 | 13 | if arch(javascript) 14 | -- https://github.com/haskellari/splitmix/pull/73 15 | source-repository-package 16 | type: git 17 | location: https://github.com/amesgen/splitmix 18 | tag: cea9e31bdd849eb0c17611bb99e33d590e126164 19 | 20 | if arch(wasm32) 21 | -- Required for TemplateHaskell. When using wasm32-wasi-cabal from 22 | -- ghc-wasm-meta, this is superseded by the global cabal.config. 23 | shared: True 24 | 25 | -- https://github.com/haskellari/splitmix/pull/73 26 | source-repository-package 27 | type: git 28 | location: https://github.com/amesgen/splitmix 29 | tag: cea9e31bdd849eb0c17611bb99e33d590e126164 30 | 31 | package aeson 32 | flags: -ordered-keymap 33 | 34 | source-repository-package 35 | type: git 36 | location: https://github.com/haskell-wasm/foundation.git 37 | tag: 8e6dd48527fb429c1922083a5030ef88e3d58dd3 38 | subdir: basement 39 | 40 | -- for the fetch example 41 | source-repository-package 42 | type: git 43 | location: https://github.com/amesgen/servant-client-js 44 | tag: 2853fb4f26175f51ae7b9aaf0ec683c45070d06e 45 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { overlays ? [] 2 | }: 3 | with (import ./nix { inherit overlays; }); 4 | 5 | with pkgs.haskell.lib; 6 | { 7 | inherit pkgs legacyPkgs; 8 | 9 | # hackage release 10 | release = 11 | with pkgs.haskell.packages.ghc9122; 12 | sdistTarball (buildStrictly miso); 13 | 14 | # hackage release examples 15 | release-examples = 16 | with pkgs.haskell.packages.ghc9122; 17 | sdistTarball (buildStrictly miso-examples); 18 | 19 | # ghcjs9122 20 | miso-ghcjs-9122 = pkgs.pkgsCross.ghcjs.haskell.packages.ghc9122.miso; 21 | miso-examples-ghcjs-9122 = pkgs.pkgsCross.ghcjs.haskell.packages.ghc9122.miso-examples; 22 | sample-app-js-9122 = pkgs.pkgsCross.ghcjs.haskell.packages.ghc9122.sample-app-js; 23 | 24 | # ghcjs86 25 | miso-ghcjs = legacyPkgs.haskell.packages.ghcjs.miso; 26 | miso-ghcjs-prod = legacyPkgs.haskell.packages.ghcjs86.miso-prod; 27 | inherit (legacyPkgs.haskell.packages.ghcjs) miso-examples sample-app-js; 28 | 29 | # miso x86 30 | miso-ghc = legacyPkgs.haskell.packages.ghc865.miso; 31 | miso-ghc-9122 = pkgs.haskell.packages.ghc9122.miso; 32 | 33 | # miso-examples x86 34 | miso-examples-ghc = legacyPkgs.haskell.packages.ghc865.miso-examples; 35 | miso-examples-ghc-9122 = pkgs.haskell.packages.ghc9122.miso-examples; 36 | 37 | # sample app legacy build 38 | inherit (legacyPkgs.haskell.packages.ghc865) 39 | sample-app; 40 | 41 | # sample app 42 | sample-app-ghc9122 = 43 | pkgs.haskell.packages.ghc9122.sample-app; 44 | 45 | # Miso wasm examples 46 | inherit (pkgs) 47 | wasmExamples 48 | svgWasm 49 | componentsWasm 50 | threejsWasm 51 | canvas2DWasm 52 | todoWasm; 53 | 54 | # wasm utils 55 | inherit (pkgs) 56 | wasm-ghc 57 | ghc-wasm-meta 58 | hello-world-web-wasm; 59 | 60 | # sse 61 | inherit (import ./examples/sse {}) 62 | sse-runner 63 | sse-client 64 | sse-server; 65 | 66 | # website 67 | inherit (import ./haskell-miso.org {}) 68 | haskell-miso-dev 69 | haskell-miso-client 70 | haskell-miso-server 71 | haskell-miso-runner; 72 | 73 | # code coverage 74 | inherit (pkgs) 75 | coverage; 76 | 77 | # haddocks 78 | inherit (pkgs) 79 | haddocks; 80 | 81 | # ci 82 | inherit (legacyPkgs) 83 | deploy 84 | nixops 85 | haskell-miso-org-test; 86 | 87 | # ghciwatch 88 | inherit (pkgs) 89 | ghciwatch; 90 | 91 | # utils 92 | inherit (pkgs.haskell.packages.ghc9122) 93 | miso-from-html; 94 | 95 | # misc. examples 96 | inherit (legacyPkgs) 97 | more-examples; 98 | 99 | # dmj: make a NixOS test to ensure examples can be hosted 100 | # dry-running this ensures we catch the failure before deploy 101 | inherit (legacyPkgs) 102 | nginx-nixos-test; 103 | 104 | # bun 105 | inherit (pkgs) 106 | bun; 107 | 108 | # nurl 109 | # $ nurl https://github.com/nix-community/nurl 110 | # 111 | # fetchFromGitHub { 112 | # owner = "nix-community"; 113 | # repo = "nurl"; 114 | # rev = "3a3ba7f0d14d92e1266395d826c6e229797d0044"; 115 | # hash = "sha256-WAFqmlsShuQngk6LMFlgz7Oyc41TAQeTa/49phhRizY="; 116 | # } 117 | # 118 | inherit (pkgs) 119 | nurl; 120 | 121 | # favicon.ico and miso.png 122 | miso-logos = pkgs.stdenv.mkDerivation { 123 | name = "miso-logos"; 124 | src = ./logo; 125 | buildCommand = '' 126 | mkdir -p $out 127 | cp -v $src/* $out/ 128 | ''; 129 | }; 130 | 131 | } 132 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lnl7/nix:2.3.3 2 | 3 | RUN nix-env -iA \ 4 | nixpkgs.curl \ 5 | nixpkgs.jq \ 6 | nixpkgs.git \ 7 | nixpkgs.gnutar \ 8 | nixpkgs.gzip \ 9 | # get ca certificates for connecting to cachix 10 | nixpkgs.libressl \ 11 | # install ag and entr for auto-rebuild 12 | nixpkgs.silver-searcher \ 13 | nixpkgs.entr \ 14 | nixpkgs.cabal-install 15 | 16 | RUN nix-env -iA cachix -f https://cachix.org/api/v1/install 17 | RUN SYSTEM_CERTIFICATE_PATH=$NIX_SSL_CERT_FILE USER=miso cachix use haskell-miso-cachix 18 | 19 | COPY ./sample-app /miso/sample-app 20 | 21 | WORKDIR /miso/sample-app 22 | RUN nix-build -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | ## Docker Based Workflow 2 | 3 | It is possible to edit and run miso applications with docker and docker-compose as the only dependencies on the host system. 4 | 5 | 0. On Windows and Mac open your docker settings and increase the Memory and Swap (I use Memory=6GB and Swap=2GB), otherwise the container will be killed when exceeding the limit. 6 | 1. Run `git clone https://github.com/dmjio/miso` and `cd` into `miso/docker` 7 | 1. Run `docker-compose build`, this will take a long time 8 | 1. Run `docker-compose up` 9 | 1. `miso/sample-app` has now the build in `\dist-newstyle\build\x86_64-linux\ghcjs-8.6.0.1\app-0.1.0.0\x\app\build\app\app.jsexe` 10 | 1. Open `index.html` on your host-machine and edit any file in `miso/sample-app` and docker will automatically rebuild the app 11 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | app: 4 | build: 5 | context: .. 6 | dockerfile: docker/Dockerfile 7 | working_dir: /miso/sample-app 8 | command: [nix-shell, -A, env, default.nix, --run, "cabal configure --ghcjs; ag -l | (ENTR_INOTIFY_WORKAROUND=1 entr sh -c 'cabal build')"] 9 | volumes: 10 | - ../sample-app:/miso/sample-app/ -------------------------------------------------------------------------------- /docs/Internals.md: -------------------------------------------------------------------------------- 1 | Internals 2 | =========================== 3 | 4 | ## Overview 5 | 6 | Miso’s external API has three main parts. The `model`, `view` function, and `update` function. 7 | 8 | ## Concurrency 9 | 10 | Under the hood miso’s concurrency model centers around an atomically updated `IORef (Seq action)` (known henceforth as event queue). The queue is used to capture events and update the user-defined `model` [^1]. Each captured browser event appends an `action` to the event queue. 11 | 12 | ## Event Loop 13 | 14 | `miso` operates in an event [loop](https://github.com/dmjio/miso/blob/master/src/Miso.hs#L124) that blocks on [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame). The `model` lives on the stack of the event loop function. Inside this loop we drain the queue into `[action]`, and fold against the `model` (this is known as “event batching”). We do this to minimize calls to `diff`. 15 | 16 | Miso then diffs the new `model` against the old `model`. If the `model` has been updated (dirty), the `view` function is invoked on the updated `model` produce a `VTree` [^3]. This `VTree` gets converted to a `JS` `vtree` and is diffed + patched against the existing tree (that lives on the JS heap) and the DOM updates. The loop recurses on the new `model`. 17 | 18 | ## Diffing and patching (simultaneously) 19 | 20 | During [diffing](https://github.com/dmjio/miso/blob/master/js/miso.js#L3) we mutually recurse over the parent and child nodes of both the old and new virtual DOM structures. During this process we diff the old tree against the new tree (while simultaneously updating the DOM to reflect the new structure) [^4]. We apply various new optimizations not seen in other frameworks (like react) on the child lists (see [syncChildren](https://github.com/dmjio/miso/blob/master/js/miso.js#L187)). During diffing we also invoke node creation / destruction life cycle hooks. 21 | 22 | ## Events 23 | 24 | While the event loop is executing, browser events are raised asynchronously and delegated into Haskell callbacks that live on the virtual DOM in the JS heap [^6]. The event body is parsed into a Haskell structure via JSON (`FromJSON`), the update function is invoked to produce an `Action` that gets written to the queue atomically. Event delegation and DOM diffing occur simultaneously. 25 | 26 | ## Misc. concurrency 27 | 28 | `Sub` and `Sink` are ways to write into the actions queue externally (useful for integration with third party components as well). Each `Component` has its own `Sink`, and list of `Sub`. There are some predefined `Sub` in `Miso.Subscription` for conveniently working with the History, Websocket, Keyboard and Mouse APIs. All `Sub` are forked, and all `Sink` writes are executed synchronously. A `Sink` write appends to the event queue, its `action` is evaluated asynchronously in the event loop thread. 29 | 30 | ## Pre-rendering 31 | 32 | Pre-rendering (using the `miso` function) on application startup will traverse the DOM and copy pointers into miso’s virtual DOM structure (this process is known as [hydration](https://en.wikipedia.org/wiki/Hydration_(web_development))). This is necessary for events to work, since event delegation works by DOM pointer traversal on the virtual DOM to find the correct node to dispatch the event [^6]. 33 | 34 | ## View 35 | 36 | The `view` function is how templating works in `miso`. The `View` is a [Rose Tree](https://en.wikipedia.org/wiki/Rose_tree) that represents the DOM. This function is used in the event loop to construct new virtual DOMs in response to browser events. 37 | 38 | [^1]: `Seq` is used to aid event ordering and avoid excessive redraws. 39 | 40 | [^2]: Green threads are very cheap in Haskell. The GHCJS and GHC RTS (w/ WASM backend) should have equivalent operating semantics for threads. 41 | 42 | [^3]: Since events can be no-ops we want to avoid generating a tree if the model hasn’t changed. Miso uses both `Eq` and `StablePtr` equality to determine if a draw is necessary. The `StablePtr` equality is an optimization that avoids expensive calls to `(==)` on large models. 43 | 44 | [^4]: This diff + patch approach is responsible for a lot of performance gains. We don't generate a list of patches and apply them in a separate phase like some other frameworks. 45 | 46 | [^5]: `VTree` is the Haskell AST version of a JS virtual DOM. The `view` function constructs terms in this AST, it is then lowered into a `JSVal`. The `JSVal` is a virtual DOM tree structure that lives in the JS heap that is used for diffing. Once lowered, we diff against the existing virtual DOM that already lives in the JS heap. 47 | 48 | [^6]: The event handlers that live on the JS virtual DOM are how miso calls back into the Haskell heap from the JS heap to write to the `Action` queue. Events are defined on the `View` using the `onWithOptions` function. Lifecycle are also defined using the View DSL. 49 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import globals from "globals"; 3 | import js from "@eslint/js"; 4 | 5 | export default defineConfig([ 6 | { files: ["**/*.{js,mjs,cjs,ts}"] }, 7 | { files: ["**/*.js","**/*.ts"], languageOptions: { sourceType: "script" } }, 8 | { 9 | files: ["**/*.{js,mjs,cjs,ts}"], 10 | languageOptions: { 11 | globals: { ...globals.browser, ...globals.node }, 12 | }, 13 | }, 14 | { 15 | files: ["**/*.{js,mjs,cjs,ts}"], 16 | plugins: { js }, 17 | extends: ["js/recommended"], 18 | }, 19 | { 20 | rules: { 21 | "no-global-assign": ["error", { exceptions: ["miso"] }], 22 | "arrow-body-style": ["error", "always"], 23 | }, 24 | }, 25 | ]); 26 | -------------------------------------------------------------------------------- /examples/.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /examples/.ghci: -------------------------------------------------------------------------------- 1 | :set prompt ">>> " 2 | :set prompt2 "... " 3 | :set -Wall 4 | -------------------------------------------------------------------------------- /examples/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2020, David M. Johnson 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /examples/canvas2d/Main.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE CPP #-} 3 | {-# LANGUAGE TypeApplications #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | ----------------------------------------------------------------------------- 6 | module Main where 7 | ----------------------------------------------------------------------------- 8 | import Control.Monad (replicateM_) 9 | import Language.Javascript.JSaddle (JSM, (#), fromJSVal, new, jsg) 10 | ----------------------------------------------------------------------------- 11 | import Miso 12 | import Miso.Canvas 13 | import qualified Miso.Canvas as Canvas 14 | import Miso.String 15 | import Miso.Style 16 | ----------------------------------------------------------------------------- 17 | #ifdef WASM 18 | foreign export javascript "hs_start" main :: IO () 19 | #endif 20 | ----------------------------------------------------------------------------- 21 | type Model = (Double, Double) 22 | ----------------------------------------------------------------------------- 23 | data Action 24 | = GetTime 25 | | SetTime Model 26 | ----------------------------------------------------------------------------- 27 | baseUrl :: MisoString 28 | baseUrl = "https://7b40c187-5088-4a99-9118-37d20a2f875e.mdnplay.dev/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations/" 29 | ----------------------------------------------------------------------------- 30 | main :: IO () 31 | main = 32 | run $ do 33 | sun <- newImage (baseUrl <> "canvas_sun.png") 34 | moon <- newImage (baseUrl <> "canvas_moon.png") 35 | earth <- newImage (baseUrl <> "canvas_earth.png") 36 | startComponent (app sun moon earth) { initialAction = Just GetTime } 37 | where 38 | app sun moon earth = defaultComponent (0.0, 0.0) updateModel (view_ sun moon earth) 39 | view_ sun moon earth m = 40 | div_ 41 | [ id_ "canvas grid" ] 42 | [ Canvas.canvas_ 43 | [ id_ "canvas" 44 | , width_ "300" 45 | , height_ "300" 46 | ] (canvasDraw sun moon earth m n) 47 | | n <- [ 1 :: Int .. 4 ] 48 | ] 49 | ----------------------------------------------------------------------------- 50 | canvasDraw :: Image -> Image -> Image -> (Double, Double) -> Int -> Canvas () 51 | canvasDraw sun moon earth (millis', secs') n = do 52 | let 53 | secs = secs' + fromIntegral n 54 | millis = millis' + fromIntegral n 55 | globalCompositeOperation DestinationOver 56 | clearRect (0,0,300,300) 57 | fillStyle $ Canvas.color (rgba 0 0 0 0.6) 58 | strokeStyle $ Canvas.color (rgba 0 153 255 0.4) 59 | save () 60 | translate (150, 150) 61 | rotate ((((2 * pi) / 60) * secs) + (((2 * pi) / 60000) * millis)) 62 | translate (105,0) 63 | fillRect (0 ,-12, 50, 24) 64 | drawImage (earth, -12, -12) 65 | save () 66 | rotate ((((2 * pi) / 6) * secs) + (((2 * pi) / 6000) * millis)) 67 | translate (0,28.5) 68 | drawImage (moon, -3.5, -3.5) 69 | replicateM_ 2 (restore ()) 70 | beginPath () 71 | arc (150, 150, 105, 0, pi * 2) 72 | stroke () 73 | drawImage' (sun, 0, 0, 300, 300) 74 | ----------------------------------------------------------------------------- 75 | newTime :: JSM (Double, Double) 76 | newTime = do 77 | date <- new (jsg @MisoString "Date") ([] :: [MisoString]) 78 | millis' <- date # ("getMilliseconds" :: MisoString) $ ([] :: [MisoString]) 79 | seconds' <- date # ("getSeconds" :: MisoString) $ ([] :: [MisoString]) 80 | Just millis <- fromJSVal millis' 81 | Just seconds <- fromJSVal seconds' 82 | pure (millis, seconds) 83 | ----------------------------------------------------------------------------- 84 | updateModel 85 | :: Action 86 | -> Effect Model Action 87 | updateModel GetTime = 88 | io (SetTime <$> newTime) 89 | updateModel (SetTime m) = 90 | m <# pure GetTime 91 | ----------------------------------------------------------------------------- 92 | -------------------------------------------------------------------------------- /examples/file-reader/Main.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE CPP #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | ----------------------------------------------------------------------------- 6 | -- | 7 | -- Module : Main 8 | -- Copyright : (C) 2016-2025 David M. Johnson 9 | -- License : BSD3-style (see the file LICENSE) 10 | -- Maintainer : David M. Johnson 11 | -- Stability : experimental 12 | -- Portability : non-portable 13 | ---------------------------------------------------------------------------- 14 | module Main where 15 | ---------------------------------------------------------------------------- 16 | import Control.Concurrent.MVar (newEmptyMVar, putMVar, readMVar) 17 | import Control.Monad (void) 18 | import Control.Monad.IO.Class (liftIO) 19 | import Language.Javascript.JSaddle ((!), (!!), (#), JSVal, (<#)) 20 | import qualified Language.Javascript.JSaddle as J 21 | import Prelude hiding ((!!), null, unlines) 22 | ---------------------------------------------------------------------------- 23 | import Miso (Component(styles), View,Effect, defaultComponent, run, CSS(..), startComponent, io, io_) 24 | import qualified Miso as M 25 | import Miso.Lens ((.=), Lens, lens) 26 | import Miso.String (MisoString, unlines, null) 27 | import qualified Miso.Style as CSS 28 | ---------------------------------------------------------------------------- 29 | -- | Model 30 | newtype Model 31 | = Model 32 | { _info :: MisoString 33 | } deriving (Eq, Show) 34 | ---------------------------------------------------------------------------- 35 | -- | info Lens 36 | info :: Lens Model MisoString 37 | info = lens _info $ \r x -> r { _info = x } 38 | ---------------------------------------------------------------------------- 39 | -- | Action 40 | data Action 41 | = ReadFile JSVal 42 | | SetContent MisoString 43 | | ClickInput JSVal 44 | ---------------------------------------------------------------------------- 45 | -- | WASM support 46 | #ifdef WASM 47 | foreign export javascript "hs_start" main :: IO () 48 | #endif 49 | ---------------------------------------------------------------------------- 50 | -- | Main entry point 51 | main :: IO () 52 | main = run $ startComponent app 53 | { styles = 54 | [ Href "https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css" 55 | , Style css 56 | ] 57 | } 58 | ---------------------------------------------------------------------------- 59 | -- | Custom styling 60 | css :: MisoString 61 | css = unlines 62 | [ ".content-container {" 63 | , " min-height: 300px;" 64 | , " display: flex;" 65 | , " flex-direction: column;" 66 | , " justify-content: center;" 67 | , "}" 68 | , "" 69 | , "#codeDisplay {" 70 | , " min-height: 200px;" 71 | , " background-color: #f5f5f5;" 72 | , " border-radius: 4px;" 73 | , " padding: 1rem;" 74 | , " white-space: pre-wrap;" 75 | , " font-family: monospace;" 76 | , "}" 77 | ] 78 | ---------------------------------------------------------------------------- 79 | -- | Miso application 80 | app :: Component name Model Action 81 | app = defaultComponent (Model mempty) updateModel viewModel 82 | ---------------------------------------------------------------------------- 83 | -- | Update function 84 | updateModel :: Action -> Effect Model Action 85 | updateModel (ReadFile fileReaderInput) = io $ do 86 | file <- fileReaderInput ! ("files" :: String) !! 0 87 | reader <- J.new (J.jsg ("FileReader" :: String)) ([] :: [JSVal]) 88 | mvar <- liftIO newEmptyMVar 89 | (reader <# ("onload" :: String)) =<< do 90 | M.asyncCallback $ do 91 | result <- J.fromJSValUnchecked =<< reader ! ("result" :: String) 92 | liftIO (putMVar mvar result) 93 | void $ reader # ("readAsText" :: String) $ [file] 94 | SetContent <$> liftIO (readMVar mvar) 95 | updateModel (SetContent c) = info .= c 96 | updateModel (ClickInput button) = io_ $ do 97 | fileReader <- button ! ("nextSibling" :: MisoString) -- dmj: gets hidden input 98 | void $ fileReader # ("click" :: String) $ ([] :: [JSVal]) 99 | ---------------------------------------------------------------------------- 100 | -- | View function 101 | viewModel :: Model -> View Action 102 | viewModel Model{..} = 103 | M.section_ 104 | [ M.class_ "section" 105 | ] 106 | [ M.div_ 107 | [ M.class_ "container" 108 | ] 109 | [ M.h1_ 110 | [ M.class_ "title has-text-centered" 111 | ] 112 | [ "🍜 Miso File Reader example" 113 | ] 114 | , M.div_ 115 | [ M.class_ "columns is-centered mt-5" 116 | ] 117 | [ M.div_ 118 | [ M.class_ "column is-narrow content-container" 119 | ] 120 | [ M.div_ 121 | [ M.class_ "field" 122 | ] 123 | [ M.div_ 124 | [ M.class_ "control" 125 | ] 126 | [ M.button_ 127 | [ M.class_ "button is-primary is-large" 128 | , M.onClickWith ClickInput 129 | ] 130 | [ "Select File" ] 131 | , M.input_ 132 | [ CSS.style_ [ CSS.display "none" ] 133 | , M.id_ "fileReader" 134 | , M.type_ "file" 135 | , M.class_ "button is-large" 136 | , M.onChangeWith (const ReadFile) 137 | ] 138 | ] 139 | ] 140 | ] 141 | ] 142 | , M.div_ 143 | [ M.class_ "column content-container" 144 | ] 145 | [ M.div_ 146 | [ M.id_ "codeDisplay" 147 | , M.class_ "box" 148 | ] 149 | [ M.p_ 150 | [ M.class_ "has-text-grey-light" 151 | ] 152 | [ M.pre_ 153 | [] 154 | [ M.text _info 155 | ] 156 | | not (null _info) 157 | ] 158 | ] 159 | ] 160 | ] 161 | ] 162 | -------------------------------------------------------------------------------- /examples/mario/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE MultiWayIf #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | 6 | module Main where 7 | 8 | import Data.Bool 9 | import Data.Function 10 | 11 | import Miso 12 | import Miso.String 13 | import qualified Miso.Style as CSS 14 | 15 | data Action 16 | = GetArrows !Arrows 17 | | Time !Double 18 | | WindowCoords !(Int, Int) 19 | | Start 20 | 21 | spriteFrames :: [MisoString] 22 | spriteFrames = ["0 0", "-74px 0", "-111px 0", "-148px 0", "-185px 0", "-222px 0", "-259px 0", "-296px 0"] 23 | 24 | #ifdef WASM 25 | foreign export javascript "hs_start" main :: IO () 26 | #endif 27 | 28 | main :: IO () 29 | main = run $ do 30 | time <- now 31 | let m = mario{time = time} 32 | startComponent (defaultComponent m updateMario display) 33 | { subs = 34 | [ arrowsSub GetArrows 35 | , windowCoordsSub WindowCoords 36 | ] 37 | , initialAction = Just Start 38 | } 39 | 40 | data Model = Model 41 | { x :: !Double 42 | , y :: !Double 43 | , vx :: !Double 44 | , vy :: !Double 45 | , dir :: !Direction 46 | , time :: !Double 47 | , delta :: !Double 48 | , arrows :: !Arrows 49 | , window :: !(Int, Int) 50 | } 51 | deriving (Show, Eq) 52 | 53 | data Direction 54 | = L 55 | | R 56 | deriving (Show, Eq) 57 | 58 | mario :: Model 59 | mario = 60 | Model 61 | { x = 0 62 | , y = 0 63 | , vx = 0 64 | , vy = 0 65 | , dir = R 66 | , time = 0 67 | , delta = 0 68 | , arrows = Arrows 0 0 69 | , window = (0, 0) 70 | } 71 | 72 | updateMario :: Action -> Effect Model Action 73 | updateMario Start = get >>= step 74 | updateMario (GetArrows arrs) = do 75 | modify newModel 76 | step =<< get 77 | where 78 | newModel m = m { arrows = arrs } 79 | updateMario (Time newTime) = do 80 | modify newModel 81 | step =<< get 82 | where 83 | newModel m = m 84 | { delta = (newTime - time m) / 20 85 | , time = newTime 86 | } 87 | updateMario (WindowCoords coords) = do 88 | modify newModel 89 | step =<< get 90 | where 91 | newModel m = m { window = coords } 92 | 93 | step :: Model -> Effect Model Action 94 | step m@Model{..} = k <# Time <$> now 95 | where 96 | k = 97 | m 98 | & gravity delta 99 | & jump arrows 100 | & walk arrows 101 | & physics delta 102 | 103 | jump :: Arrows -> Model -> Model 104 | jump Arrows{..} m@Model{..} = 105 | if arrowY > 0 && vy == 0 106 | then m{vy = 6} 107 | else m 108 | 109 | gravity :: Double -> Model -> Model 110 | gravity dt m@Model{..} = 111 | m{vy = if y > 0 then vy - (dt / 4) else 0} 112 | 113 | physics :: Double -> Model -> Model 114 | physics dt m@Model{..} = 115 | m 116 | { x = x + dt * vx 117 | , y = max 0 (y + dt * vy) 118 | } 119 | 120 | walk :: Arrows -> Model -> Model 121 | walk Arrows{..} m@Model{..} = 122 | m 123 | { vx = fromIntegral arrowX 124 | , dir = 125 | if 126 | | arrowX < 0 -> L 127 | | arrowX > 0 -> R 128 | | otherwise -> dir 129 | } 130 | 131 | display :: Model -> View action 132 | display m@Model{..} = marioImage 133 | where 134 | (h, w) = window 135 | groundY = 62 - (fromIntegral (fst window) / 2) 136 | marioImage = 137 | div_ 138 | [ height_ $ ms h 139 | , width_ $ ms w 140 | ] 141 | [ nodeHtml "style" [] ["@keyframes play { 100% { background-position: -296px; } }"] 142 | , div_ [CSS.style_ (marioStyle m groundY)] [] 143 | ] 144 | 145 | marioStyle :: Model -> Double -> [CSS.Style] 146 | marioStyle Model{..} gy = 147 | [ ("transform", matrix dir x $ abs (y + gy)) 148 | , ("display", "block") 149 | , ("width", "37px") 150 | , ("height", "37px") 151 | , ("background-color", "transparent") 152 | , ("background-image", "url(imgs/mario.png)") 153 | , ("background-repeat", "no-repeat") 154 | , ("background-position", spriteFrames !! frame) 155 | , bool mempty ("animation", "play 0.8s steps(8) infinite") (y == 0 && vx /= 0) 156 | ] 157 | where 158 | frame 159 | | y > 0 = 1 160 | | otherwise = 0 161 | 162 | matrix :: Direction -> Double -> Double -> MisoString 163 | matrix dir x y = 164 | "matrix(" 165 | <> (if dir == L then "-1" else "1") 166 | <> ",0,0,1," 167 | <> ms x 168 | <> "," 169 | <> ms y 170 | <> ")" 171 | -------------------------------------------------------------------------------- /examples/mario/imgs/mario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmjio/miso/db43c3e902906f751157c3fe9eb7389952941c36/examples/mario/imgs/mario.png -------------------------------------------------------------------------------- /examples/mathml/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | module Main where 4 | 5 | import Miso 6 | import Miso.Mathml 7 | 8 | #if defined(wasm32_HOST_ARCH) 9 | foreign export javascript "hs_start" main :: IO () 10 | #endif 11 | 12 | -- | Entry point for a miso application 13 | main :: IO () 14 | main = run $ startComponent (defaultComponent Main.Empty updateModel viewModel) 15 | 16 | data Model = Empty 17 | deriving (Eq) 18 | 19 | updateModel :: Applicative f => p -> f () 20 | updateModel _ = pure () 21 | 22 | -- | Constructs a virtual DOM from a model 23 | viewModel :: Model -> View () 24 | viewModel _ = 25 | math_ 26 | [display_ "block"] 27 | [ mrow_ [] 28 | [ msub_ [] 29 | [ mi_ [] [text "x"] 30 | , mtext_ [] [text "1,2"] 31 | ] 32 | , mo_ [] [text "="] 33 | , mfrac_ [] 34 | [ mrow_ [] 35 | [ mo_ [] [text "−"] 36 | , mi_ [] [text "b"] 37 | , mo_ [] [text "±"] 38 | , msqrt_ 39 | [] 40 | [ mrow_ [] 41 | [ msup_ [] 42 | [ mi_ [] [text "b"] 43 | , mn_ [] [text "2"] 44 | ] 45 | , mo_ [] [text "−"] 46 | , mrow_ [] 47 | [ mn_ [] [text "4"] 48 | , mi_ [] [text "a"] 49 | , mi_ [] [text "c"] 50 | ] 51 | ] 52 | ] 53 | ] 54 | , mrow_ [] 55 | [ mn_ [] [text "2"] 56 | , mi_ [] [text "a"] 57 | ] 58 | ] 59 | ] 60 | ] 61 | -------------------------------------------------------------------------------- /examples/miso-examples.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: miso-examples 3 | version: 1.9.0.0 4 | category: Web, Miso, Data Structures 5 | author: David M. Johnson 6 | maintainer: David M. Johnson 7 | homepage: http://github.com/dmjio/miso 8 | copyright: Copyright (c) 2016-2025 David M. Johnson 9 | bug-reports: https://github.com/dmjio/miso/issues 10 | build-type: Simple 11 | synopsis: A tasty Haskell front-end web framework 12 | description: Examples for miso 13 | license: BSD-3-Clause 14 | license-file: LICENSE 15 | 16 | extra-source-files: 17 | mario/imgs/mario.png 18 | 19 | common common-options 20 | if arch(wasm32) 21 | ghc-options: 22 | -no-hs-main -optl-mexec-model=reactor "-optl-Wl,--export=hs_start" 23 | ghc-options: 24 | -funbox-strict-fields -O2 -ferror-spans -fspecialise-aggressively -Wall 25 | if impl(ghcjs) || arch(javascript) 26 | cpp-options: 27 | -DGHCJS_BOTH 28 | if impl(ghcjs) 29 | cpp-options: 30 | -DGHCJS_OLD 31 | elif arch(javascript) 32 | cpp-options: 33 | -DGHCJS_NEW 34 | elif arch(wasm32) 35 | cpp-options: 36 | -DWASM 37 | else 38 | cpp-options: 39 | -DNATIVE 40 | 41 | executable components 42 | import: 43 | common-options 44 | default-language: 45 | Haskell2010 46 | main-is: 47 | Main.hs 48 | ghcjs-options: 49 | -dedupe 50 | cpp-options: 51 | -DGHCJS_BROWSER 52 | hs-source-dirs: 53 | components 54 | build-depends: 55 | aeson, 56 | base < 5, 57 | containers, 58 | miso, 59 | mtl, 60 | transformers 61 | 62 | executable simple 63 | import: 64 | common-options 65 | default-language: 66 | Haskell2010 67 | main-is: 68 | Main.hs 69 | ghcjs-options: 70 | -dedupe 71 | cpp-options: 72 | -DGHCJS_BROWSER 73 | hs-source-dirs: 74 | simple 75 | build-depends: 76 | aeson, 77 | base < 5, 78 | containers, 79 | miso, 80 | mtl, 81 | transformers 82 | 83 | executable todo-mvc 84 | import: 85 | common-options 86 | default-language: 87 | Haskell2010 88 | main-is: 89 | Main.hs 90 | ghcjs-options: 91 | -dedupe 92 | cpp-options: 93 | -DGHCJS_BROWSER 94 | hs-source-dirs: 95 | todo-mvc 96 | build-depends: 97 | aeson, 98 | base < 5, 99 | containers, 100 | miso, 101 | mtl, 102 | transformers 103 | 104 | executable threejs 105 | import: 106 | common-options 107 | default-language: 108 | Haskell2010 109 | main-is: 110 | Main.hs 111 | ghcjs-options: 112 | -dedupe 113 | cpp-options: 114 | -DGHCJS_BROWSER 115 | hs-source-dirs: 116 | three 117 | build-depends: 118 | aeson, 119 | base < 5, 120 | containers, 121 | jsaddle, 122 | miso 123 | 124 | executable file-reader 125 | import: 126 | common-options 127 | default-language: 128 | Haskell2010 129 | main-is: 130 | Main.hs 131 | ghcjs-options: 132 | -dedupe 133 | cpp-options: 134 | -DGHCJS_BROWSER 135 | hs-source-dirs: 136 | file-reader 137 | build-depends: 138 | aeson, 139 | base < 5, 140 | containers, 141 | jsaddle, 142 | miso, 143 | mtl 144 | 145 | executable fetch 146 | import: 147 | common-options 148 | default-language: 149 | Haskell2010 150 | main-is: 151 | Main.hs 152 | ghcjs-options: 153 | -dedupe 154 | cpp-options: 155 | -DGHCJS_BROWSER 156 | hs-source-dirs: 157 | fetch 158 | build-depends: 159 | aeson, 160 | base < 5, 161 | containers, 162 | jsaddle, 163 | miso, 164 | mtl, 165 | servant, 166 | servant-client-js 167 | 168 | executable canvas2d 169 | import: 170 | common-options 171 | default-language: 172 | Haskell2010 173 | main-is: 174 | Main.hs 175 | ghcjs-options: 176 | -dedupe 177 | cpp-options: 178 | -DGHCJS_BROWSER 179 | hs-source-dirs: 180 | canvas2d 181 | build-depends: 182 | aeson, 183 | base < 5, 184 | jsaddle, 185 | miso, 186 | mtl 187 | 188 | executable router 189 | import: 190 | common-options 191 | default-language: 192 | Haskell2010 193 | main-is: 194 | Main.hs 195 | ghcjs-options: 196 | -dedupe 197 | cpp-options: 198 | -DGHCJS_BROWSER 199 | hs-source-dirs: 200 | router 201 | build-depends: 202 | aeson, 203 | base < 5, 204 | containers, 205 | miso, 206 | mtl, 207 | servant, 208 | transformers 209 | 210 | executable websocket 211 | import: 212 | common-options 213 | default-language: 214 | Haskell2010 215 | main-is: 216 | Main.hs 217 | ghcjs-options: 218 | -dedupe 219 | cpp-options: 220 | -DGHCJS_BROWSER 221 | hs-source-dirs: 222 | websocket 223 | build-depends: 224 | aeson, 225 | base < 5, 226 | containers, 227 | miso, 228 | mtl, 229 | transformers 230 | 231 | executable mario 232 | import: 233 | common-options 234 | default-language: 235 | Haskell2010 236 | main-is: 237 | Main.hs 238 | ghcjs-options: 239 | -dedupe 240 | cpp-options: 241 | -DGHCJS_BROWSER 242 | hs-source-dirs: 243 | mario 244 | build-depends: 245 | base < 5, 246 | containers, 247 | miso, 248 | mtl 249 | 250 | executable mathml 251 | import: 252 | common-options 253 | default-language: 254 | Haskell2010 255 | main-is: 256 | Main.hs 257 | ghcjs-options: 258 | -dedupe 259 | cpp-options: 260 | -DGHCJS_BROWSER 261 | hs-source-dirs: 262 | mathml 263 | build-depends: 264 | base < 5, 265 | miso 266 | 267 | executable svg 268 | import: 269 | common-options 270 | default-language: 271 | Haskell2010 272 | main-is: 273 | Main.hs 274 | ghcjs-options: 275 | -dedupe 276 | cpp-options: 277 | -DGHCJS_BROWSER 278 | hs-source-dirs: 279 | svg 280 | build-depends: 281 | base < 5, 282 | containers, 283 | aeson, 284 | miso, 285 | mtl 286 | -------------------------------------------------------------------------------- /examples/router/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE TypeFamilies #-} 7 | {-# LANGUAGE TypeOperators #-} 8 | module Main where 9 | 10 | import Data.Proxy 11 | import Miso 12 | import Servant.API 13 | import Servant.Links 14 | 15 | #if defined(wasm32_HOST_ARCH) 16 | foreign export javascript "hs_start" main :: IO () 17 | #endif 18 | 19 | -- | Model 20 | data Model = Model 21 | { uri :: URI 22 | -- ^ current URI of application 23 | } 24 | deriving (Eq, Show) 25 | 26 | -- | Action 27 | data Action 28 | = HandleURI URI 29 | | ChangeURI URI 30 | deriving (Show, Eq) 31 | 32 | -- | Main entry point 33 | main :: IO () 34 | main = run $ 35 | miso $ \u -> 36 | (defaultComponent (Model u) updateModel viewModel) 37 | { subs = [uriSub HandleURI] 38 | } 39 | 40 | -- | Update your model 41 | updateModel :: Action -> Effect Model Action 42 | updateModel (HandleURI u) = modify $ \m -> m { uri = u } 43 | updateModel (ChangeURI u) = io_ (pushURI u) 44 | 45 | -- | View function, with routing 46 | viewModel :: Model -> View Action 47 | viewModel m = view_ 48 | where 49 | view_ = 50 | either (const the404) id $ 51 | route (Proxy :: Proxy API) handlers uri m 52 | handlers = about :<|> home 53 | home (_ :: Model) = 54 | div_ 55 | [] 56 | [ div_ [] [text "home"] 57 | , button_ [onClick goAbout] [text "go about"] 58 | ] 59 | about (_ :: Model) = 60 | div_ 61 | [] 62 | [ div_ [] [text "about"] 63 | , button_ [onClick goHome] [text "go home"] 64 | ] 65 | the404 = 66 | div_ 67 | [] 68 | [ text "the 404 :(" 69 | , button_ [onClick goHome] [text "go home"] 70 | ] 71 | 72 | -- | Type-level routes 73 | type API = About :<|> Home 74 | type Home = View Action 75 | type About = "about" :> View Action 76 | 77 | -- | Type-safe links used in `onClick` event handlers to route the application 78 | aboutUri, homeUri :: URI 79 | aboutUri :<|> homeUri = allLinks' linkURI (Proxy @API) 80 | 81 | goHome, goAbout :: Action 82 | goHome = ChangeURI homeUri 83 | goAbout = ChangeURI aboutUri 84 | -------------------------------------------------------------------------------- /examples/shell.nix: -------------------------------------------------------------------------------- 1 | { pkg ? "ghc" }: 2 | 3 | with (import ../default.nix {}); 4 | 5 | if pkg == "ghc" 6 | then miso-examples-ghc.env.overrideAttrs (d: { 7 | shellHook = '' 8 | export PATH=$PATH:${pkgs.ghciwatch}/bin 9 | function watch () { 10 | ghciwatch \ 11 | --command "cabal repl $1" \ 12 | --watch $1 \ 13 | --test-ghci=Main.main 14 | } 15 | ''; 16 | }) 17 | else 18 | miso-examples.env 19 | -------------------------------------------------------------------------------- /examples/simple/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | -- | Haskell module declaration 5 | module Main where 6 | 7 | -- Miso framework import 8 | import Prelude hiding (unlines) 9 | 10 | import Miso 11 | import Miso.Lens 12 | import Miso.String 13 | 14 | -- | Type synonym for an application model 15 | newtype Model = Model { _value :: Int } 16 | deriving (Show, Eq) 17 | 18 | instance ToMisoString Model where 19 | toMisoString (Model v) = toMisoString v 20 | 21 | value :: Lens Model Int 22 | value = lens _value $ \m v -> m { _value = v } 23 | 24 | -- | Sum type for application events 25 | data Action 26 | = AddOne PointerEvent 27 | | SubtractOne PointerEvent 28 | | SayHelloWorld 29 | deriving (Show, Eq) 30 | 31 | #ifdef WASM 32 | foreign export javascript "hs_start" main :: IO () 33 | #endif 34 | 35 | -- | Entry point for a miso application 36 | main :: IO () 37 | main = run $ startComponent app 38 | { events = pointerEvents 39 | , styles = [ Style css ] 40 | } 41 | 42 | -- | Component definition (uses 'defaultComponent' smart constructor) 43 | app :: Component name Model Action 44 | app = defaultComponent (Model 0) updateModel viewModel 45 | 46 | -- | UpdateModels model, optionally introduces side effects 47 | updateModel :: Action -> Effect Model Action 48 | updateModel (AddOne event) = do 49 | value += 1 50 | io_ $ consoleLog (ms (show event)) 51 | updateModel (SubtractOne event) = do 52 | value -= 1 53 | io_ $ consoleLog (ms (show event)) 54 | updateModel SayHelloWorld = 55 | io_ (consoleLog "Hello World!") 56 | 57 | -- | Constructs a virtual DOM from a model 58 | viewModel :: Model -> View Action 59 | viewModel x = div_ 60 | [ class_ "counter-container" ] 61 | [ h1_ 62 | [ class_ "counter-title" 63 | ] 64 | [ "🍜 Miso counter" 65 | ] 66 | , div_ 67 | [ class_ "counter-display" 68 | ] 69 | [ text (ms x) 70 | ] 71 | , div_ 72 | [ class_ "buttons-container" 73 | ] 74 | [ button_ 75 | [ onPointerDown AddOne 76 | , class_ "decrement-btn" 77 | ] [text "+"] 78 | , button_ 79 | [ onPointerDown SubtractOne 80 | , class_ "increment-btn" 81 | ] [text "-"] 82 | ] 83 | ] 84 | 85 | css :: MisoString 86 | css = unlines 87 | [ ":root {" 88 | , "--primary-color: #4a6bff;" 89 | , "--primary-hover: #3451d1;" 90 | , "--secondary-color: #ff4a6b;" 91 | , "--secondary-hover: #d13451;" 92 | , "--background: #f7f9fc;" 93 | , "--text-color: #333;" 94 | , "--shadow: 0 4px 10px rgba(0, 0, 0, 0.1);" 95 | , "--transition: all 0.3s ease;" 96 | , "}" 97 | , "body {" 98 | , " font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;" 99 | , " display: flex;" 100 | , " justify-content: center;" 101 | , " align-items: center;" 102 | , " height: 100vh;" 103 | , " margin: 0;" 104 | , " background-color: var(--background);" 105 | , " color: var(--text-color);" 106 | , "}" 107 | , ".counter-container {" 108 | , " background-color: white;" 109 | , " padding: 2rem;" 110 | , " border-radius: 12px;" 111 | , " box-shadow: var(--shadow);" 112 | , " text-align: center;" 113 | , "}" 114 | , ".counter-display {" 115 | , " font-size: 5rem;" 116 | , " font-weight: bold;" 117 | , " margin: 1rem 0;" 118 | , " transition: var(--transition);" 119 | , "}" 120 | , ".buttons-container {" 121 | , " display: flex;" 122 | , " gap: 1rem;" 123 | , " justify-content: center;" 124 | , " margin-top: 1.5rem;" 125 | , "}" 126 | , "button {" 127 | , " font-size: 1.5rem;" 128 | , " width: 3rem;" 129 | , " height: 3rem;" 130 | , " border: none;" 131 | , " border-radius: 50%;" 132 | , " cursor: pointer;" 133 | , " transition: var(--transition);" 134 | , " color: white;" 135 | , " display: flex;" 136 | , " align-items: center;" 137 | , " justify-content: center;" 138 | , "}" 139 | , ".increment-btn {" 140 | , " background-color: var(--primary-color);" 141 | , "}" 142 | , ".increment-btn:hover {" 143 | , " background-color: var(--primary-hover);" 144 | , " transform: translateY(-2px);" 145 | , "}" 146 | , ".decrement-btn {" 147 | , " background-color: var(--secondary-color);" 148 | , "}" 149 | , ".decrement-btn:hover {" 150 | , " background-color: var(--secondary-hover);" 151 | , " transform: translateY(-2px);" 152 | , "}" 153 | , "@keyframes pulse {" 154 | , " 0% { transform: scale(1); }" 155 | , " 50% { transform: scale(1.1); }" 156 | , " 100% { transform: scale(1); }" 157 | , "}" 158 | , ".counter-display.animate {" 159 | , " animation: pulse 0.3s ease;" 160 | , "}" 161 | , "@media (max-width: 480px) {" 162 | , " .counter-container {" 163 | , " padding: 1.5rem;" 164 | , " }" 165 | , " .counter-display {" 166 | , " font-size: 3rem;" 167 | , " }" 168 | , " button {" 169 | , " font-size: 1.2rem;" 170 | , " width: 2.5rem;" 171 | , " height: 2.5rem;" 172 | , " }" 173 | , "}" 174 | ] 175 | 176 | -------------------------------------------------------------------------------- /examples/sse/.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /examples/sse/.ghci: -------------------------------------------------------------------------------- 1 | :set prompt ">>> " 2 | :set prompt2 "... " 3 | :set -isrc -ishared 4 | :set -Wall 5 | -------------------------------------------------------------------------------- /examples/sse/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2018, David Johnson 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of David Johnson nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /examples/sse/client/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Common (sse) 4 | 5 | import Miso (miso, run) 6 | 7 | main :: IO () 8 | main = run (miso sse) 9 | -------------------------------------------------------------------------------- /examples/sse/default.nix: -------------------------------------------------------------------------------- 1 | {}: 2 | with (import ../.. {}); 3 | let 4 | sources = import ../../nix/source.nix pkgs; 5 | inherit (pkgs) runCommand closurecompiler; 6 | ghc = legacyPkgs.haskell.packages.ghc865; 7 | ghcjs = legacyPkgs.haskell.packages.ghcjs; 8 | client = ghcjs.callCabal2nix "sse" (sources.sse) {}; 9 | server = ghc.callCabal2nix "sse" (sources.sse) {}; 10 | in 11 | { 12 | sse-runner = runCommand "sse.haskell-miso.org" { inherit client server; } '' 13 | mkdir -p $out/{bin,static} 14 | cp ${server}/bin/* $out/bin 15 | ${closurecompiler}/bin/closure-compiler --compilation_level ADVANCED_OPTIMIZATIONS \ 16 | --jscomp_off=checkVars \ 17 | --externs=${client}/bin/client.jsexe/all.js.externs \ 18 | ${client}/bin/client.jsexe/all.js > temp.js 19 | mv temp.js $out/static/all.js 20 | ''; 21 | sse-client = client; 22 | sse-server = server; 23 | inherit pkgs; 24 | } 25 | -------------------------------------------------------------------------------- /examples/sse/nix/machine.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, ... }: 2 | { 3 | imports = [ ./module.nix ]; 4 | services = { 5 | sse-haskell-miso.enable = true; 6 | nginx = { 7 | enable = true; 8 | virtualHosts = { 9 | "sse.haskell-miso.org" = { 10 | extraConfig = " 11 | proxy_set_header Connection ''; 12 | proxy_http_version 1.1; 13 | chunked_transfer_encoding off; 14 | proxy_buffering off; 15 | proxy_cache off; 16 | "; 17 | forceSSL = true; 18 | enableACME = true; 19 | locations = { 20 | "/" = { 21 | proxyPass = "http://localhost:3003"; 22 | }; 23 | }; 24 | }; 25 | }; 26 | }; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /examples/sse/nix/module.nix: -------------------------------------------------------------------------------- 1 | with (import ../default.nix {}); 2 | 3 | { options, lib, config, pkgs, modulesPath }: 4 | let 5 | cfg = config.services.sse-haskell-miso; 6 | in { 7 | options.services.sse-haskell-miso.enable = lib.mkEnableOption 8 | "Enable the sse.haskell-miso.org service"; 9 | config = lib.mkIf cfg.enable { 10 | systemd.services.sse-haskell-miso = { 11 | path = with pkgs; [ bash ]; 12 | wantedBy = [ "multi-user.target" ]; 13 | script = ''./bin/server +RTS -N -A4M -RTS''; 14 | description = ''https://sse.haskell-miso.org''; 15 | serviceConfig = { 16 | WorkingDirectory=sse-runner; 17 | KillSignal="INT"; 18 | Type = "simple"; 19 | Restart = "on-abort"; 20 | RestartSec = "10"; 21 | }; 22 | }; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /examples/sse/server/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE PolyKinds #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | {-# LANGUAGE TypeOperators #-} 6 | 7 | module Main where 8 | 9 | import Common 10 | 11 | import Control.Concurrent 12 | import Control.Monad 13 | import Data.Binary.Builder 14 | import Data.Proxy 15 | import Data.Time.Clock 16 | import Network.HTTP.Types 17 | import Network.Wai 18 | import Network.Wai.EventSource 19 | import Network.Wai.Handler.Warp 20 | import Network.Wai.Middleware.Gzip 21 | import Network.Wai.Middleware.RequestLogger 22 | import Servant 23 | import qualified System.IO as IO 24 | 25 | import Miso hiding (SSE, run) 26 | 27 | type Website = StaticFiles :<|> SSE :<|> ServerRoutes :<|> The404 28 | type ServerRoutes = Get '[HTML] Page 29 | type StaticFiles = "static" :> Raw 30 | type SSE = "sse" :> Raw 31 | type The404 = Raw 32 | 33 | app :: Chan ServerEvent -> Application 34 | app chan = serve (Proxy @Website) website 35 | where 36 | website = 37 | serveDirectoryFileServer "static" 38 | :<|> Tagged (eventSourceAppChan chan) 39 | :<|> pure (Page (sse goHome)) 40 | :<|> Tagged handle404 41 | 42 | port :: Int 43 | port = 3003 44 | 45 | main :: IO () 46 | main = do 47 | IO.hPutStrLn IO.stderr ("Running on port " <> show port <> "...") 48 | chan <- newChan 49 | _ <- forkIO (sendEvents chan) 50 | run port $ logStdout (compress (app chan)) 51 | where 52 | compress = gzip def{gzipFiles = GzipCompress} 53 | 54 | -- Send 1 event/s containing the current server time 55 | sendEvents :: Chan ServerEvent -> IO () 56 | sendEvents chan = 57 | forever $ do 58 | time <- getCurrentTime 59 | writeChan 60 | chan 61 | (ServerEvent Nothing Nothing [putStringUtf8 (show (show time))]) 62 | threadDelay (10 ^ (6 :: Int)) 63 | 64 | -- | Page for setting HTML doctype and header 65 | newtype Page = Page (Component "app" Model Action) 66 | 67 | instance ToHtml Page where 68 | toHtml (Page x) = 69 | toHtml 70 | [ doctype_ 71 | , head_ 72 | [] 73 | [ meta_ [charset_ "utf-8"] 74 | , jsRef "static/all.js" -- Include the frontend 75 | ] 76 | , body_ [] [toView x] 77 | ] 78 | where 79 | jsRef href = 80 | script_ 81 | [ src_ href 82 | , async_ "true" 83 | , defer_ "true" 84 | ] 85 | "" 86 | 87 | handle404 :: Application 88 | handle404 _ respond = 89 | respond $ 90 | responseLBS status404 [("Content-Type", "text/html")] $ 91 | toHtml the404 92 | -------------------------------------------------------------------------------- /examples/sse/shared/Common.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Common ( 5 | -- * Component 6 | sse, 7 | -- * Types 8 | Model, 9 | Action, 10 | -- * Exported links 11 | goHome, 12 | the404, 13 | ) where 14 | 15 | import Data.Proxy 16 | import Servant.API 17 | import Servant.Links 18 | 19 | import Miso 20 | import Miso.String 21 | 22 | data Model = Model 23 | { modelUri :: URI 24 | , modelMsg :: String 25 | } deriving (Show, Eq) 26 | 27 | data Action 28 | = ServerMsg String 29 | | ChangeURI URI 30 | | HandleURI URI 31 | deriving (Show, Eq) 32 | 33 | home :: Model -> View Action 34 | home (Model _ msg) = 35 | div_ 36 | [] 37 | [ div_ 38 | [] 39 | [ h3_ 40 | [] 41 | [ text "SSE (Server-sent events) Example" 42 | ] 43 | ] 44 | , text (ms msg) 45 | ] 46 | 47 | -- There is only a single route in this example 48 | type ClientRoutes = View Action 49 | 50 | the404 :: View Action 51 | the404 = 52 | div_ 53 | [] 54 | [ text "404: Page not found" 55 | , a_ [onClick $ ChangeURI goHome] [text " - Go Home"] 56 | ] 57 | 58 | goHome :: URI 59 | goHome = allLinks' linkURI (Proxy :: Proxy ClientRoutes) 60 | 61 | sse :: URI -> Component name Model Action 62 | sse currentURI 63 | = app { subs = 64 | [ sseSub "/sse" handleSseMsg 65 | , uriSub HandleURI 66 | ] 67 | } 68 | where 69 | app = defaultComponent (Model currentURI "No event received") updateModel viewModel 70 | viewModel m 71 | | Right r <- route (Proxy :: Proxy ClientRoutes) home modelUri m = 72 | r 73 | | otherwise = the404 74 | 75 | handleSseMsg :: SSE String -> Action 76 | handleSseMsg (SSEMessage msg) = ServerMsg msg 77 | handleSseMsg SSEClose = ServerMsg "SSE connection closed" 78 | handleSseMsg SSEError = ServerMsg "SSE error" 79 | 80 | updateModel :: Action -> Effect Model Action 81 | updateModel (ServerMsg msg) = 82 | modify $ \m -> m { modelMsg = "Event received: " ++ msg } 83 | updateModel (HandleURI u) = 84 | modify $ \m -> m { modelUri = u } 85 | updateModel (ChangeURI u) = 86 | io_ (pushURI u) 87 | -------------------------------------------------------------------------------- /examples/sse/shell.nix: -------------------------------------------------------------------------------- 1 | { pkg ? "ghc" }: 2 | with (import ./default.nix {}); 3 | 4 | if pkg == "ghc" 5 | then sse-server.env.overrideAttrs (d: { 6 | shellHook = '' 7 | export PATH=$PATH:${pkgs.ghciwatch}/bin 8 | function watch () { 9 | ghciwatch \ 10 | --command "cabal repl $1" \ 11 | --watch $1 \ 12 | --test-ghci=Main.main 13 | } 14 | ''; 15 | }) 16 | else sse-client.env 17 | -------------------------------------------------------------------------------- /examples/sse/sse.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: sse 3 | version: 0.1.0.0 4 | synopsis: Server sent events example 5 | homepage: https://sse.haskell-miso.org 6 | license: BSD-3-Clause 7 | license-file: LICENSE 8 | author: David Johnson 9 | maintainer: code@dmj.io 10 | copyright: David Johnson (c) 2016-2025 11 | category: Web 12 | build-type: Simple 13 | 14 | common server-only 15 | if impl(ghcjs) || arch(javascript) || arch(wasm32) 16 | buildable: False 17 | 18 | common client-only 19 | if impl(ghcjs) || arch(javascript) || arch(wasm32) 20 | buildable: True 21 | else 22 | buildable: False 23 | 24 | common common-modules 25 | other-modules: 26 | Common 27 | 28 | executable server 29 | import: 30 | server-only, common-modules 31 | main-is: 32 | Main.hs 33 | ghc-options: 34 | -O2 -threaded -Wall -rtsopts 35 | hs-source-dirs: 36 | server, shared 37 | build-depends: 38 | aeson, 39 | base < 5, 40 | binary, 41 | containers, 42 | http-types, 43 | miso, 44 | mtl, 45 | network-uri, 46 | servant, 47 | servant-server, 48 | time, 49 | wai, 50 | wai-extra, 51 | warp 52 | default-language: 53 | Haskell2010 54 | 55 | executable client 56 | import: 57 | client-only, common-modules 58 | main-is: 59 | Main.hs 60 | ghcjs-options: 61 | -dedupe 62 | cpp-options: 63 | -DGHCJS_BROWSER 64 | hs-source-dirs: 65 | client, shared 66 | build-depends: 67 | aeson, 68 | base < 5, 69 | containers, 70 | network-uri, 71 | miso, 72 | mtl, 73 | servant 74 | default-language: 75 | Haskell2010 76 | -------------------------------------------------------------------------------- /examples/svg/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE TypeFamilies #-} 4 | 5 | module Main where 6 | 7 | import Control.Monad.State 8 | import qualified Data.Map as M 9 | 10 | import Miso hiding (update) 11 | import Miso.String (ms) 12 | import Miso.Svg hiding (height_, id_, style_, width_) 13 | import qualified Miso.Style as CSS 14 | import Miso.Style ((=:)) 15 | 16 | #if WASM 17 | foreign export javascript "hs_start" main :: IO () 18 | #endif 19 | 20 | main :: IO () 21 | main = run $ startComponent app 22 | { events = M.insert "pointermove" False pointerEvents 23 | , subs = [ mouseSub HandlePointer ] 24 | } 25 | 26 | -- | Component definition (uses 'defaultComponent' smart constructor) 27 | app :: Component name Model Action 28 | app = defaultComponent emptyModel updateModel viewModel 29 | 30 | emptyModel :: Model 31 | emptyModel = Model (0, 0) 32 | 33 | updateModel :: Action -> Effect Model Action 34 | updateModel (HandlePointer pointer) = modify update 35 | where 36 | update m = m { mouseCoords = client pointer } 37 | 38 | data Action 39 | = HandlePointer PointerEvent 40 | 41 | newtype Model 42 | = Model 43 | { mouseCoords :: (Double, Double) 44 | } deriving (Show, Eq) 45 | 46 | viewModel :: Model -> View Action 47 | viewModel (Model (x, y)) = 48 | div_ 49 | [] 50 | [ svg_ 51 | [ CSS.style_ 52 | [ CSS.borderStyle "solid" 53 | , CSS.height "700px" 54 | , CSS.width "100%" 55 | ] 56 | , onPointerMove HandlePointer 57 | ] 58 | [ g_ 59 | [] 60 | [ ellipse_ 61 | [ cx_ $ ms x 62 | , cy_ $ ms y 63 | , CSS.style_ 64 | [ "fill" =: "yellow" 65 | , "stroke" =: "purple" 66 | , "stroke-width" =: "2" 67 | ] 68 | , rx_ "100" 69 | , ry_ "100" 70 | ] 71 | [] 72 | ] 73 | , text_ 74 | [ x_ $ ms x 75 | , y_ $ ms y 76 | ] 77 | [text $ ms $ show (x, y)] 78 | ] 79 | ] 80 | -------------------------------------------------------------------------------- /examples/three/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/wasm-hello-world/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | foreign export javascript "hs_start" main :: IO () 4 | 5 | main :: IO () 6 | main = putStrLn "hello world" 7 | -------------------------------------------------------------------------------- /examples/websocket/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE ExtendedDefaultRules #-} 5 | {-# LANGUAGE FlexibleInstances #-} 6 | {-# LANGUAGE MultiParamTypeClasses #-} 7 | {-# LANGUAGE OverloadedStrings #-} 8 | {-# LANGUAGE RecordWildCards #-} 9 | {-# LANGUAGE ScopedTypeVariables #-} 10 | {-# LANGUAGE TypeFamilies #-} 11 | 12 | module Main where 13 | 14 | import Control.Monad.State 15 | import Data.Aeson 16 | import Data.Bool 17 | import GHC.Generics 18 | 19 | import Miso 20 | import Miso.String (MisoString) 21 | import qualified Miso.String as S 22 | 23 | import qualified Miso.Style as CSS 24 | 25 | #if WASM 26 | foreign export javascript "hs_start" main :: IO () 27 | #endif 28 | 29 | main :: IO () 30 | main = run $ startComponent app 31 | { events = defaultEvents <> keyboardEvents 32 | , subs = 33 | [ websocketSub url protocols HandleWebSocket 34 | ] 35 | } where 36 | url = URL "wss://echo.websocket.org" 37 | protocols = Protocols [] 38 | 39 | app :: Component name Model Action 40 | app = defaultComponent emptyModel updateModel appView 41 | 42 | emptyModel :: Model 43 | emptyModel = Model (Message "") mempty 44 | 45 | updateModel :: Action -> Effect Model Action 46 | updateModel (HandleWebSocket (WebSocketMessage (Message m))) = 47 | modify $ \model -> model { received = m } 48 | updateModel (SendMessage msg) = 49 | io_ (send msg) 50 | updateModel (UpdateMessage m) = do 51 | modify $ \model -> model { msg = Message m } 52 | updateModel _ = pure () 53 | 54 | instance ToJSON Message 55 | instance FromJSON Message 56 | 57 | newtype Message = Message MisoString 58 | deriving (Eq, Show, Generic) 59 | 60 | data Action 61 | = HandleWebSocket (WebSocket Message) 62 | | SendMessage Message 63 | | UpdateMessage MisoString 64 | | Id 65 | 66 | data Model = Model 67 | { msg :: Message 68 | , received :: MisoString 69 | } deriving (Show, Eq) 70 | 71 | appView :: Model -> View Action 72 | appView Model{..} = 73 | div_ 74 | [ CSS.style_ [ CSS.textAlign "center" ] ] 75 | [ link_ [rel_ "stylesheet", href_ "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.4.3/css/bulma.min.css"] 76 | , h1_ [ CSS.style_ [CSS.fontWeight "bold"] 77 | ] 78 | [ a_ 79 | [ href_ "https://github.com/dmjio/miso"] 80 | [ text $ S.pack "Miso Websocket Example"] 81 | ] 82 | , h3_ [] [text $ S.pack "wss://echo.websocket.org"] 83 | , input_ 84 | [ type_ "text" 85 | , onInput UpdateMessage 86 | , onEnter (SendMessage msg) 87 | ] 88 | , button_ 89 | [ onClick (SendMessage msg) 90 | ] 91 | [text (S.pack "Send to echo server")] 92 | , div_ [] [p_ [] [text received | not . S.null $ received]] 93 | ] 94 | 95 | onEnter :: Action -> Attribute Action 96 | onEnter action = onKeyDown $ bool Id action . (== KeyCode 13) 97 | -------------------------------------------------------------------------------- /haskell-miso.org/.ghci: -------------------------------------------------------------------------------- 1 | :set prompt ">>> " 2 | :set prompt2 "... " 3 | :set -isrc -ishared 4 | :set -Wall 5 | -------------------------------------------------------------------------------- /haskell-miso.org/ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Revision history for haskell-miso 2 | 3 | ## 0.1.0.0 -- YYYY-mm-dd 4 | 5 | * First version. Released on an unsuspecting world. 6 | -------------------------------------------------------------------------------- /haskell-miso.org/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2018, David Johnson 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of David Johnson nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /haskell-miso.org/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /haskell-miso.org/client/.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /haskell-miso.org/client/DevelMain.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module DevelMain (update) where 3 | 4 | import Common (haskellMisoComponent, uriHome) 5 | import Miso (startComponent, run) 6 | import Miso.String (MisoString) 7 | 8 | import Rapid 9 | 10 | update :: IO () 11 | update = 12 | rapid 0 $ \r -> 13 | restart r ("miso-client" :: MisoString) $ 14 | run (startComponent (haskellMisoComponent uriHome)) 15 | -------------------------------------------------------------------------------- /haskell-miso.org/client/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | 3 | module Main where 4 | 5 | import Common (haskellMisoComponent) 6 | import Miso (miso, run) 7 | import Miso.String 8 | 9 | #ifdef WASM 10 | foreign export javascript "hs_start" main :: IO () 11 | #endif 12 | 13 | main :: IO () 14 | main = run (miso haskellMisoComponent) 15 | -------------------------------------------------------------------------------- /haskell-miso.org/client/shell.nix: -------------------------------------------------------------------------------- 1 | with (import ../../default.nix {}); 2 | haskell-miso-dev.env 3 | -------------------------------------------------------------------------------- /haskell-miso.org/default.nix: -------------------------------------------------------------------------------- 1 | {}: 2 | with (import ../default.nix {}); 3 | let 4 | sources = import ../nix/source.nix pkgs; 5 | inherit (pkgs) runCommand closurecompiler; 6 | ghc = legacyPkgs.haskell.packages.ghc865; 7 | ghcjs = legacyPkgs.haskell.packages.ghcjs; 8 | client = ghcjs.callCabal2nix "haskell-miso" (sources.haskell-miso) {}; 9 | server = ghc.callCabal2nix "haskell-miso" (sources.haskell-miso) {}; 10 | dev = ghc.callCabal2nixWithOptions "haskell-miso" (sources.haskell-miso) "-fdev" {}; 11 | in 12 | { haskell-miso-client = client; 13 | haskell-miso-server = server; 14 | haskell-miso-dev = dev; 15 | haskell-miso-runner = 16 | runCommand "haskell-miso.org" { inherit client server; } '' 17 | mkdir -p $out/{bin,static} 18 | cp ${server}/bin/* $out/bin 19 | ${closurecompiler}/bin/closure-compiler --compilation_level ADVANCED_OPTIMIZATIONS \ 20 | --jscomp_off=checkVars \ 21 | --externs=${client}/bin/client.jsexe/all.js.externs \ 22 | ${client}/bin/client.jsexe/all.js > temp.js 23 | mv temp.js $out/static/all.js 24 | cp -v ${miso-logos}/* $out/static/ 25 | ''; 26 | } 27 | -------------------------------------------------------------------------------- /haskell-miso.org/haskell-miso.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: haskell-miso 3 | version: 0.1.0.1 4 | synopsis: https://haskell-miso.org 5 | description: Website for the Miso web framework 6 | homepage: https://haskell-miso.org 7 | license: BSD-3-Clause 8 | license-file: LICENSE 9 | author: David Johnson 10 | maintainer: code@dmj.io 11 | copyright: David Johnson (c) 2016-2025 12 | category: Web 13 | build-type: Simple 14 | extra-source-files: ChangeLog.md 15 | 16 | flag dev 17 | manual: 18 | True 19 | default: 20 | False 21 | description: 22 | Run haskell-miso.org client w/ jsaddle and rapid 23 | 24 | common server-only 25 | if impl(ghcjs) || arch(javascript) || flag(dev) 26 | buildable: False 27 | 28 | common client-only 29 | if impl(ghcjs) || arch(javascript) || arch(wasm32) || flag(dev) 30 | buildable: True 31 | else 32 | buildable: False 33 | 34 | if flag(dev) 35 | build-depends: 36 | rapid 37 | 38 | common common-options 39 | if arch(wasm32) 40 | ghc-options: 41 | -no-hs-main -optl-mexec-model=reactor "-optl-Wl,--export=hs_start" 42 | cpp-options: 43 | -DWASM 44 | ghc-options: 45 | -funbox-strict-fields -O2 -ferror-spans -fspecialise-aggressively 46 | 47 | common common-modules 48 | other-modules: 49 | Common 50 | 51 | executable server 52 | import: 53 | server-only, common-modules, common-options 54 | main-is: 55 | Main.hs 56 | ghc-options: 57 | -threaded -Wall -rtsopts 58 | hs-source-dirs: 59 | server, shared 60 | build-depends: 61 | aeson, 62 | base < 5, 63 | containers, 64 | http-types, 65 | miso, 66 | mtl, 67 | network-uri, 68 | servant, 69 | servant-server, 70 | text, 71 | wai, 72 | wai-app-static, 73 | wai-extra, 74 | warp 75 | default-language: 76 | Haskell2010 77 | 78 | executable client 79 | import: 80 | client-only, common-modules, common-options 81 | main-is: 82 | Main.hs 83 | ghcjs-options: 84 | -dedupe 85 | cpp-options: 86 | -DGHCJS_BROWSER 87 | hs-source-dirs: 88 | client, shared 89 | other-modules: 90 | DevelMain 91 | build-depends: 92 | aeson, 93 | base < 5, 94 | containers, 95 | miso, 96 | mtl, 97 | servant, 98 | default-language: 99 | Haskell2010 100 | -------------------------------------------------------------------------------- /haskell-miso.org/nix/aws.nix: -------------------------------------------------------------------------------- 1 | { email ? "" 2 | , token ? "" 3 | }: 4 | let 5 | accessKeyId = "dev"; 6 | region = "us-east-2"; 7 | awsBox = 8 | { pkgs, config, lib, resources, ...}: { 9 | imports = 10 | [ ./machine.nix 11 | ./nginx.nix 12 | ../../examples/sse/nix/machine.nix 13 | ]; 14 | security.acme = { 15 | inherit email; 16 | acceptTerms = true; 17 | }; 18 | nix.gc.automatic = true; 19 | boot.loader.grub.device = pkgs.lib.mkForce "/dev/nvme0n1"; 20 | networking.firewall = { 21 | allowedTCPPorts = [ 80 22 443 ]; 22 | enable = true; 23 | allowPing = true; 24 | }; 25 | services.openssh.enable = true; 26 | deployment = { 27 | targetEnv = "ec2"; 28 | ec2 = { 29 | ebsBoot = true; 30 | ebsInitialRootDiskSize = 40; 31 | securityGroups = [ resources.ec2SecurityGroups.miso-firewall ]; 32 | elasticIPv4 = resources.elasticIPs.miso-ip; 33 | associatePublicIpAddress = true; 34 | inherit region accessKeyId; 35 | keyPair = resources.ec2KeyPairs.misoKeyPair; 36 | instanceType = "t3.small"; 37 | }; 38 | }; 39 | }; 40 | in 41 | { 42 | resources.ec2SecurityGroups.miso-firewall = { 43 | inherit accessKeyId region; 44 | rules = [ 45 | { fromPort = 80; toPort = 80; sourceIp = "0.0.0.0/0"; } 46 | { fromPort = 22; toPort = 22; sourceIp = "0.0.0.0/0"; } 47 | { fromPort = 443; toPort = 443; sourceIp = "0.0.0.0/0"; } 48 | ]; 49 | }; 50 | 51 | resources.elasticIPs.miso-ip = { 52 | inherit region accessKeyId; 53 | vpc = true; 54 | }; 55 | 56 | resources.ec2KeyPairs.misoKeyPair = { 57 | inherit region accessKeyId; 58 | }; 59 | 60 | inherit awsBox; 61 | 62 | network.enableRollback = false; 63 | network.description = "Miso network"; 64 | } 65 | -------------------------------------------------------------------------------- /haskell-miso.org/nix/machine.nix: -------------------------------------------------------------------------------- 1 | { pkgs 2 | , config 3 | , lib 4 | , ... 5 | }: 6 | { 7 | imports = [ ./module.nix ]; 8 | services.haskell-miso.enable = true; 9 | } 10 | -------------------------------------------------------------------------------- /haskell-miso.org/nix/module.nix: -------------------------------------------------------------------------------- 1 | with (import ../../default.nix {}); 2 | { options, lib, config, pkgs, ... }: 3 | let 4 | cfg = config.services.haskell-miso; 5 | in { 6 | options.services.haskell-miso.enable = 7 | lib.mkEnableOption "Enable the haskell-miso.org service"; 8 | config = lib.mkIf cfg.enable { 9 | systemd.services.haskell-miso = { 10 | path = 11 | [ pkgs.bash 12 | ]; 13 | wantedBy = [ "multi-user.target" ]; 14 | script = '' 15 | ./bin/server +RTS -N -A4M -RTS 16 | ''; 17 | description = '' 18 | https://haskell-miso.org 19 | ''; 20 | serviceConfig = { 21 | WorkingDirectory=haskell-miso-runner; 22 | KillSignal="INT"; 23 | Type = "simple"; 24 | Restart = "on-abort"; 25 | RestartSec = "10"; 26 | }; 27 | }; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /haskell-miso.org/nix/nginx.nix: -------------------------------------------------------------------------------- 1 | with (import ../../default.nix {}); 2 | { config 3 | , lib 4 | , ... 5 | }: 6 | { 7 | services.nginx = { 8 | enable = true; 9 | recommendedGzipSettings = true; 10 | recommendedOptimisation = true; 11 | virtualHosts = { 12 | "haskell-miso.org" = { 13 | forceSSL = true; 14 | enableACME = true; 15 | locations = { 16 | "/" = { 17 | proxyPass = "http://127.0.0.1:3002"; 18 | }; 19 | }; 20 | }; 21 | "components.haskell-miso.org" = { 22 | forceSSL = true; 23 | enableACME = true; 24 | locations = { 25 | "/" = { 26 | root = "${miso-examples}/bin/components.jsexe"; 27 | }; 28 | }; 29 | }; 30 | "todo-mvc.haskell-miso.org" = { 31 | forceSSL = true; 32 | enableACME = true; 33 | locations = { 34 | "/" = { 35 | root = "${miso-examples}/bin/todo-mvc.jsexe"; 36 | }; 37 | }; 38 | }; 39 | "coverage.haskell-miso.org" = { 40 | forceSSL = true; 41 | enableACME = true; 42 | locations = { 43 | "/" = { 44 | root = coverage; 45 | }; 46 | }; 47 | }; 48 | "haddocks.haskell-miso.org" = { 49 | forceSSL = true; 50 | enableACME = true; 51 | locations = { 52 | "/" = { 53 | root = haddocks; 54 | }; 55 | }; 56 | }; 57 | "flatris.haskell-miso.org" = { 58 | forceSSL = true; 59 | enableACME = true; 60 | locations = { 61 | "/" = { 62 | root = "${more-examples.flatris}/bin/app.jsexe"; 63 | }; 64 | }; 65 | }; 66 | "miso-plane.haskell-miso.org" = { 67 | forceSSL = true; 68 | enableACME = true; 69 | locations = { 70 | "/" = { 71 | root = more-examples.miso-plane; 72 | }; 73 | }; 74 | }; 75 | "2048.haskell-miso.org" = { 76 | forceSSL = true; 77 | enableACME = true; 78 | locations = { 79 | "/" = { 80 | root = more-examples.hs2048; 81 | }; 82 | }; 83 | }; 84 | "threejs.haskell-miso.org" = { 85 | forceSSL = true; 86 | enableACME = true; 87 | locations = { 88 | "/" = { 89 | root = "${miso-examples}/bin/threejs.jsexe"; 90 | }; 91 | }; 92 | }; 93 | "snake.haskell-miso.org" = { 94 | forceSSL = true; 95 | enableACME = true; 96 | locations = { 97 | "/" = { 98 | root = "${more-examples.snake}/bin/app.jsexe"; 99 | }; 100 | }; 101 | }; 102 | "router.haskell-miso.org" = { 103 | forceSSL = true; 104 | enableACME = true; 105 | locations = { 106 | "/" = { 107 | root = "${miso-examples}/bin/router.jsexe"; 108 | }; 109 | }; 110 | }; 111 | "mario.haskell-miso.org" = { 112 | forceSSL = true; 113 | enableACME = true; 114 | locations = { 115 | "/" = { 116 | root = "${miso-examples}/bin/mario.jsexe"; 117 | }; 118 | }; 119 | }; 120 | "simple.haskell-miso.org" = { 121 | forceSSL = true; 122 | enableACME = true; 123 | locations = { 124 | "/" = { 125 | root = "${miso-examples}/bin/simple.jsexe"; 126 | }; 127 | }; 128 | }; 129 | "canvas.haskell-miso.org" = { 130 | forceSSL = true; 131 | enableACME = true; 132 | locations = { 133 | "/" = { 134 | root = "${miso-examples}/bin/canvas2d.jsexe"; 135 | }; 136 | }; 137 | }; 138 | "svg.haskell-miso.org" = { 139 | forceSSL = true; 140 | enableACME = true; 141 | locations = { 142 | "/" = { 143 | root = "${miso-examples}/bin/svg.jsexe"; 144 | }; 145 | }; 146 | }; 147 | "file-reader.haskell-miso.org" = { 148 | forceSSL = true; 149 | enableACME = true; 150 | locations = { 151 | "/" = { 152 | root = "${miso-examples}/bin/file-reader.jsexe"; 153 | }; 154 | }; 155 | }; 156 | "fetch.haskell-miso.org" = { 157 | forceSSL = true; 158 | enableACME = true; 159 | locations = { 160 | "/" = { 161 | root = "${miso-examples}/bin/fetch.jsexe"; 162 | }; 163 | }; 164 | }; 165 | "websocket.haskell-miso.org" = { 166 | forceSSL = true; 167 | enableACME = true; 168 | locations = { 169 | "/" = { 170 | root = "${miso-examples}/bin/websocket.jsexe"; 171 | }; 172 | }; 173 | }; 174 | }; 175 | }; 176 | } 177 | -------------------------------------------------------------------------------- /haskell-miso.org/server/.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /haskell-miso.org/server/shell.nix: -------------------------------------------------------------------------------- 1 | with (import ../default.nix {}); 2 | haskell-miso-server.env 3 | -------------------------------------------------------------------------------- /logo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmjio/miso/db43c3e902906f751157c3fe9eb7389952941c36/logo/favicon.ico -------------------------------------------------------------------------------- /logo/miso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmjio/miso/db43c3e902906f751157c3fe9eb7389952941c36/logo/miso.png -------------------------------------------------------------------------------- /miso.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: miso 3 | version: 1.9.0.0 4 | category: Web, Miso, Data Structures 5 | license: BSD-3-Clause 6 | license-file: LICENSE 7 | author: David M. Johnson 8 | maintainer: David M. Johnson 9 | homepage: http://github.com/dmjio/miso 10 | copyright: Copyright (c) 2016-2025 David M. Johnson 11 | bug-reports: https://github.com/dmjio/miso/issues 12 | build-type: Simple 13 | extra-source-files: README.md 14 | synopsis: A tasty Haskell front-end web framework 15 | description: 16 | Miso is a small, production-ready, component-oriented, isomorphic Haskell front-end framework featuring a virtual-dom, recursive diffing / patching algorithm, event delegation, event batching, SVG, Server-sent events, Websockets, type-safe servant-style routing and an extensible Subscription-based subsystem. Inspired by Elm and React. Miso is pure by default, but side effects can be introduced into the system via the Effect data type. Miso makes heavy use of the GHC FFI and therefore has minimal dependencies. 17 | 18 | extra-source-files: 19 | README.md 20 | 21 | source-repository head 22 | type: git 23 | location: https://github.com/dmjio/miso.git 24 | 25 | common cpp 26 | if impl(ghcjs) || arch(javascript) 27 | cpp-options: 28 | -DGHCJS_BOTH 29 | if impl(ghcjs) 30 | cpp-options: 31 | -DGHCJS_OLD 32 | elif arch(javascript) 33 | cpp-options: 34 | -DGHCJS_NEW 35 | elif arch(wasm32) 36 | cpp-options: 37 | -DWASM 38 | else 39 | cpp-options: 40 | -DNATIVE 41 | if flag(production) 42 | cpp-options: 43 | -DPRODUCTION 44 | 45 | common string-selector 46 | if impl(ghcjs) || arch(javascript) || flag (jsstring-only) || arch(wasm32) 47 | hs-source-dirs: 48 | jsstring-src 49 | else 50 | hs-source-dirs: 51 | text-src 52 | 53 | common jsaddle 54 | if !(impl(ghcjs) || arch(javascript) || arch(wasm32)) 55 | build-depends: 56 | jsaddle-warp < 0.10 57 | 58 | if !(impl(ghcjs) || arch(javascript)) 59 | build-depends: 60 | file-embed < 0.1 61 | 62 | if arch(wasm32) 63 | build-depends: 64 | jsaddle-wasm >= 0.1.1 && < 0.2 65 | 66 | common client 67 | if impl(ghcjs) || arch(javascript) 68 | build-depends: 69 | ghcjs-base -any 70 | 71 | if impl(ghcjs) || arch(javascript) || arch(wasm32) 72 | if flag(production) 73 | js-sources: 74 | js/miso.prod.js 75 | else 76 | js-sources: 77 | js/miso.js 78 | 79 | flag production 80 | manual: 81 | True 82 | default: 83 | False 84 | description: 85 | Uses miso's production quality JS (miso.prod.js). 86 | This is built from calling "bun build --production" 87 | 88 | flag template-haskell 89 | manual: 90 | True 91 | default: 92 | False 93 | description: 94 | Checks if template-haskell is enabled. If so, allows Miso.Lens.TH 95 | 96 | flag tests 97 | manual: 98 | True 99 | default: 100 | False 101 | description: 102 | Builds Miso's tests 103 | 104 | flag jsstring-only 105 | manual: 106 | True 107 | default: 108 | False 109 | description: 110 | Always set MisoString = JSString 111 | 112 | library 113 | import: 114 | string-selector, 115 | jsaddle, 116 | client, 117 | cpp 118 | default-language: 119 | Haskell2010 120 | other-modules: 121 | Miso.Concurrent 122 | Miso.Delegate 123 | Miso.Diff 124 | Miso.FFI.Internal 125 | Miso.Internal 126 | exposed-modules: 127 | Miso 128 | Miso.Canvas 129 | Miso.Effect 130 | Miso.Event 131 | Miso.Event.Decoder 132 | Miso.Event.Types 133 | Miso.Exception 134 | Miso.Fetch 135 | Miso.FFI 136 | Miso.Html 137 | Miso.Html.Element 138 | Miso.Html.Event 139 | Miso.Html.Property 140 | Miso.Html.Types 141 | Miso.Lens 142 | Miso.Mathml 143 | Miso.Mathml.Element 144 | Miso.Mathml.Property 145 | Miso.Media 146 | Miso.Property 147 | Miso.Render 148 | Miso.Router 149 | Miso.Run 150 | Miso.Subscription 151 | Miso.Subscription.History 152 | Miso.Subscription.Keyboard 153 | Miso.Subscription.Mouse 154 | Miso.Subscription.WebSocket 155 | Miso.Subscription.Window 156 | Miso.Subscription.SSE 157 | Miso.Svg 158 | Miso.Svg.Property 159 | Miso.Svg.Element 160 | Miso.Svg.Event 161 | Miso.Storage 162 | Miso.String 163 | Miso.Style 164 | Miso.Style.Color 165 | Miso.Types 166 | Miso.Util 167 | 168 | if flag (template-haskell) 169 | exposed-modules: 170 | Miso.Lens.TH 171 | build-depends: 172 | template-haskell < 2.24 173 | 174 | ghc-options: 175 | -Wall 176 | hs-source-dirs: 177 | src 178 | build-depends: 179 | aeson < 2.3, 180 | base < 5, 181 | bytestring < 0.13, 182 | containers < 0.9, 183 | http-api-data < 0.9, 184 | http-media < 0.9, 185 | http-types < 0.13, 186 | jsaddle < 0.10, 187 | mtl < 2.4, 188 | network-uri < 2.7, 189 | servant < 0.21, 190 | tagsoup < 0.15, 191 | text < 2.2, 192 | transformers < 0.7, 193 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | options: 2 | with (builtins.fromJSON (builtins.readFile ./nixpkgs.json)); 3 | let 4 | nixpkgs = builtins.fetchTarball { 5 | url = "https://github.com/alexfmpe/nixpkgs/archive/${rev}.tar.gz"; 6 | inherit sha256; 7 | }; 8 | config.allowUnfree = true; 9 | config.allowBroken = false; 10 | overlays = [ (import ./wasm) 11 | (import ./overlay.nix) 12 | ] ++ options.overlays; 13 | legacyPkgs = import ./legacy options; 14 | pkgs = import nixpkgs { inherit overlays config; }; 15 | in 16 | { 17 | inherit pkgs legacyPkgs; 18 | } 19 | -------------------------------------------------------------------------------- /nix/haskell/packages/ghc/default.nix: -------------------------------------------------------------------------------- 1 | pkgs: 2 | let 3 | source = import ../../../source.nix pkgs; 4 | in 5 | with pkgs.haskell.lib; 6 | self: super: 7 | { 8 | /* miso */ 9 | miso = self.callCabal2nixWithOptions "miso" source.miso "-ftemplate-haskell" {}; 10 | 11 | /* miso utils */ 12 | miso-from-html = self.callCabal2nix "miso-from-html" source.miso-from-html {}; 13 | 14 | /* examples */ 15 | miso-examples = self.callCabal2nix "miso-examples" source.examples {}; 16 | sample-app = self.callCabal2nix "app" source.sample-app {}; 17 | jsaddle = self.callCabal2nix "jsaddle" "${source.jsaddle}/jsaddle" {}; 18 | jsaddle-warp = 19 | dontCheck (self.callCabal2nix "jsaddle-warp" "${source.jsaddle}/jsaddle-warp" {}); 20 | servant-client-core = doJailbreak super.servant-client-core; 21 | servant-client-js = self.callCabal2nix "servant-client-js" source.servant-client-js {}; 22 | 23 | /* cruft */ 24 | crypton = dontCheck super.crypton; 25 | cryptonite = dontCheck super.cryptonite; 26 | monad-logger = doJailbreak super.monad-logger; 27 | string-interpolate = doJailbreak super.string-interpolate; 28 | servant-server = doJailbreak super.servant-server; 29 | } 30 | -------------------------------------------------------------------------------- /nix/haskell/packages/ghcjs/default.nix: -------------------------------------------------------------------------------- 1 | pkgs: 2 | let 3 | source = import ../../../source.nix pkgs; 4 | in 5 | with pkgs.haskell.lib; 6 | with pkgs.lib; 7 | self: super: 8 | { 9 | /* miso */ 10 | miso = self.callCabal2nixWithOptions "miso" source.miso "-ftemplate-haskell" {}; 11 | 12 | /* examples */ 13 | sample-app-js = self.callCabal2nix "app" source.sample-app {}; 14 | jsaddle = self.callCabal2nix "jsaddle" "${source.jsaddle}/jsaddle" {}; 15 | servant-client-core = doJailbreak super.servant-client-core; 16 | servant-client-js = self.callCabal2nix "servant-client-js" source.servant-client-js {}; 17 | flatris = self.callCabal2nix "flatris" source.flatris {}; 18 | miso-plane-core = self.callCabal2nix "miso-plane" source.miso-plane {}; 19 | miso-plane = pkgs.runCommand "miso-plane" {} '' 20 | mkdir -p $out 21 | cp -rv ${source.miso-plane}/public/images $out 22 | cp -v ${self.miso-plane-core}/bin/client.jsexe/* $out 23 | chmod +w $out/index.html 24 | cp -v ${source.miso-plane}/public/index.html $out 25 | ''; 26 | hs2048-core = self.callCabal2nix "hs2048" source.hs2048 {}; 27 | hs2048 = pkgs.runCommand "hs2048" {} '' 28 | mkdir -p $out/bin 29 | cp -rv ${self.hs2048-core}/bin/*.jsexe $out/* 30 | chmod +w $out/bin/*.jsexe 31 | chmod +w $out/bin/*.jsexe/index.html 32 | cp -v ${source.hs2048}/static/main.css $out/bin/app.jsexe/main.css 33 | cp -v ${source.hs2048}/static/index.html $out/bin/app.jsexe/index.html 34 | ''; 35 | snake = self.callCabal2nix "miso-snake" source.snake {}; 36 | miso-examples-core = self.callCabal2nix "miso-examples" source.examples {}; 37 | miso-examples = pkgs.runCommand "miso-examples" {} '' 38 | mkdir -p $out/bin/mario.jsexe/imgs 39 | cp -fr ${self.miso-examples-core}/bin/*.jsexe $out/* 40 | cp -frv ${source.examples}/mario/imgs $out/bin/mario.jsexe/ 41 | chmod +w $out/bin/threejs.jsexe/index.html 42 | cp -fv ${source.examples}/three/index.html $out/bin/threejs.jsexe/index.html 43 | chmod +w $out/bin/todo-mvc.jsexe 44 | ''; 45 | } 46 | -------------------------------------------------------------------------------- /nix/legacy/default.nix: -------------------------------------------------------------------------------- 1 | options: 2 | with (builtins.fromJSON (builtins.readFile ./nixpkgs.json)); 3 | let 4 | nixpkgs = builtins.fetchTarball { 5 | url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz"; 6 | inherit sha256; 7 | }; 8 | config = { 9 | allowUnfree = true; 10 | allowBroken = false; 11 | }; 12 | overlays = [ (import ./overlay.nix options) ]; 13 | in 14 | import nixpkgs 15 | { inherit overlays config; 16 | } 17 | -------------------------------------------------------------------------------- /nix/legacy/haskell/packages/ghc/default.nix: -------------------------------------------------------------------------------- 1 | pkgs: 2 | let 3 | source = import ../../../../source.nix pkgs; 4 | in 5 | with pkgs.haskell.lib; 6 | self: super: 7 | { 8 | miso = self.callCabal2nix "miso" source.miso {}; 9 | miso-examples = self.callCabal2nix "miso-examples" source.examples {}; 10 | miso-from-html = self.callCabal2nix "miso-from-html" source.miso-from-html {}; 11 | sample-app = self.callCabal2nix "app" source.sample-app {}; 12 | 13 | jsaddle = self.callCabal2nix "jsaddle" "${source.jsaddle}/jsaddle" {}; 14 | jsaddle-warp = dontCheck (self.callCabal2nix "jsaddle-warp" "${source.jsaddle}/jsaddle-warp" {}); 15 | servant-client-js = self.callCabal2nix "servant-client-js" source.servant-client-js {}; 16 | } 17 | -------------------------------------------------------------------------------- /nix/legacy/haskell/packages/ghcjs/default.nix: -------------------------------------------------------------------------------- 1 | options: pkgs: 2 | let 3 | source = import ../../../../source.nix pkgs; 4 | in 5 | with pkgs.haskell.lib; 6 | with pkgs.lib; 7 | self: super: 8 | { 9 | inherit (pkgs.haskell.packages.ghc865) hpack; 10 | sample-app-js = self.callCabal2nix "app" source.sample-app {}; 11 | jsaddle = self.callCabal2nix "jsaddle" "${source.jsaddle}/jsaddle" {}; 12 | jsaddle-warp = dontCheck (self.callCabal2nix "jsaddle-warp" "${source.jsaddle}/jsaddle-warp" {}); 13 | servant-client-js = self.callCabal2nix "servant-client-js" source.servant-client-js {}; 14 | flatris = self.callCabal2nix "flatris" source.flatris {}; 15 | miso-plane = 16 | let 17 | miso-plane = self.callCabal2nix "miso-plane" source.miso-plane {}; 18 | in 19 | pkgs.runCommand "miso-plane" {} '' 20 | mkdir $out 21 | cp -rv ${source.miso-plane}/public/images $out 22 | cp ${miso-plane}/bin/client.jsexe/* $out 23 | rm $out/index.html 24 | cp -v ${source.miso-plane}/public/index.html $out 25 | ''; 26 | hs2048 = import source.hs2048 { inherit pkgs; inherit (self) miso; }; 27 | snake = self.callCabal2nix "miso-snake" source.snake {}; 28 | mkDerivation = args: super.mkDerivation (args // { doCheck = false; }); 29 | doctest = null; 30 | miso-examples = (self.callCabal2nix "miso-examples" source.examples {}).overrideDerivation (drv: { 31 | postInstall = '' 32 | mkdir -p $out/bin/mario.jsexe/imgs 33 | mkdir -p $out/bin/threejs.jsexe 34 | cp -r ${drv.src}/mario/imgs $out/bin/mario.jsexe/ 35 | cp -fv ${drv.src}/three/index.html $out/bin/threejs.jsexe/ 36 | ${pkgs.closurecompiler}/bin/closure-compiler --compilation_level ADVANCED_OPTIMIZATIONS \ 37 | --jscomp_off=checkVars \ 38 | --externs=$out/bin/todo-mvc.jsexe/all.js.externs \ 39 | $out/bin/todo-mvc.jsexe/all.js > temp.js 40 | mv temp.js $out/bin/todo-mvc.jsexe/all.js 41 | ''; 42 | }); 43 | miso-prod = self.callCabal2nixWithOptions "miso" source.miso "-fproduction" {}; 44 | miso = self.callCabal2nix "miso" source.miso {}; 45 | } 46 | -------------------------------------------------------------------------------- /nix/legacy/nixpkgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "rev" : "868282b4aa56a01ef9306d2c7cfdddf14a4d89f7", 3 | "sha256" : "1712zds1gns0zpkc2apw2fy60fdyfp84gz6qh2m9l7np5dcw6m5h" 4 | } 5 | -------------------------------------------------------------------------------- /nix/legacy/overlay.nix: -------------------------------------------------------------------------------- 1 | options: self: super: { 2 | 3 | nixosPkgsSrc = 4 | "https://github.com/nixos/nixpkgs/archive/6d1a044fc9ff3cc96fca5fa3ba9c158522bbf2a5.tar.gz"; 5 | 6 | haskell = super.haskell // { 7 | packages = super.haskell.packages // { 8 | ghc865 = super.haskell.packages.ghc865.override { 9 | overrides = import ./haskell/packages/ghc self; 10 | }; 11 | ghc864 = super.haskell.packages.ghc864.override { 12 | overrides = selfGhc864: superGhc864: with super.haskell.lib; { 13 | happy = dontCheck (selfGhc864.callHackage "happy" "1.19.9" {}); 14 | mkDerivation = args: superGhc864.mkDerivation (args // { 15 | enableLibraryProfiling = false; 16 | doCheck = false; 17 | doHaddock = false; 18 | }); 19 | }; 20 | }; 21 | ghcjs86 = super.haskell.packages.ghcjs86.override { 22 | overrides = import ./haskell/packages/ghcjs options self; 23 | }; 24 | }; 25 | }; 26 | 27 | nixops = super.nixops.overrideAttrs (drv: { 28 | src = builtins.fetchTarball { 29 | url = "https://nixos.org/releases/nixops/nixops-1.7/nixops-1.7.tar.bz2"; 30 | sha256 = "sha256:1iax9hz16ry1pm9yw2wab0np7140d7pv4rnk1bw63kq4gnxnr93c"; 31 | }; 32 | }); 33 | 34 | haskell-miso-org-test = self.nixosTest { 35 | nodes.machine = { config, pkgs, ... }: { 36 | imports = [ ../../haskell-miso.org/nix/machine.nix 37 | ]; 38 | }; 39 | testScript = {nodes, ...}: 40 | '' 41 | startAll; 42 | $machine->waitForUnit("haskell-miso.service"); 43 | $machine->succeed("curl localhost:3002"); 44 | ''; 45 | }; 46 | 47 | nginx-nixos-test = self.nixosTest { 48 | nodes.machine = { config, pkgs, ... }: { 49 | imports = [ ../../haskell-miso.org/nix/nginx.nix 50 | ]; 51 | }; 52 | testScript = {nodes, ...}: 53 | '' 54 | startAll; 55 | $machine->waitForUnit("nginx.service"); 56 | $machine->succeed("curl localhost:80"); 57 | ''; 58 | }; 59 | 60 | deploy = super.writeScript "deploy" '' 61 | export PATH=$PATH:${self.nixops}/bin 62 | export PATH=$PATH:${self.jq}/bin 63 | rm -rf ~/.nixops 64 | mkdir -p ~/.aws 65 | echo "[dev]" >> ~/.aws/credentials 66 | echo "aws_access_key_id = $AWS_ACCESS_KEY_ID" >> ~/.aws/credentials 67 | echo "aws_secret_access_key = $AWS_SECRET_ACCESS_KEY" >> ~/.aws/credentials 68 | mkdir -p ~/.ssh 69 | echo "Host *" > ~/.ssh/config 70 | echo " StrictHostKeyChecking=no" >> ~/.ssh/config 71 | chmod 600 ~/.ssh/config 72 | chown $USER ~/.ssh/config 73 | echo $DEPLOY | jq > deploy.json 74 | nixops import < deploy.json 75 | rm deploy.json 76 | nixops set-args --argstr email $EMAIL -d haskell-miso 77 | nixops modify haskell-miso.org/nix/aws.nix -d haskell-miso -Inixpkgs=${self.nixosPkgsSrc} 78 | nix --version 79 | # https://github.com/NixOS/nixops/issues/1557 80 | nix shell github:nixos/nixpkgs/8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8#nix -c \ 81 | nixops ssh -d haskell-miso awsBox nix-collect-garbage -d 82 | nix shell github:nixos/nixpkgs/8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8#nix -c \ 83 | nixops deploy -j1 -d haskell-miso --option substituters "https://cache.nixos.org/" 84 | ''; 85 | more-examples = with super.haskell.lib; { 86 | inherit (self.haskell.packages.ghcjs) flatris hs2048 snake miso-plane; 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /nix/nixpkgs.json: -------------------------------------------------------------------------------- 1 | { 2 | "rev" : "b594b289740a2bc917ed9c66fef5d905f389cb96", 3 | "sha256" : "sha256:1p25582cbrjlqv4kr64481m776vgppgxghinrdjpfjglkjj3aw2w" 4 | } 5 | -------------------------------------------------------------------------------- /nix/overlay.nix: -------------------------------------------------------------------------------- 1 | self: super: { 2 | 3 | # ghci watcher 4 | ghciwatch = 5 | (builtins.getFlake "github:MercuryTechnologies/ghciwatch") 6 | .outputs.packages."${super.system}".ghciwatch; 7 | 8 | # dmj: ensure you call 'bun run test' first 9 | # js nix packaging is more trouble than its worth right now 10 | coverage = self.stdenv.mkDerivation { 11 | name = "coverage"; 12 | src = ../coverage; 13 | buildCommand = '' 14 | mkdir -p $out 15 | cp -v $src/* $out 16 | ''; 17 | }; 18 | 19 | # dmj: Ensure you call 'nix-shell --run 'cabal haddock-project'' first 20 | # this happens in CI 21 | haddocks = self.stdenv.mkDerivation { 22 | name = "haddocks"; 23 | src = ../haddocks; 24 | buildCommand = '' 25 | mkdir -p $out 26 | cp -rv $src/* $out 27 | ''; 28 | }; 29 | 30 | # haskell stuff 31 | haskell = super.haskell // { 32 | packages = super.haskell.packages // { 33 | ghc9122 = super.haskell.packages.ghc9122.override { 34 | overrides = if super.stdenv.targetPlatform.isGhcjs 35 | then import ./haskell/packages/ghcjs self 36 | else import ./haskell/packages/ghc self; 37 | }; 38 | }; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /nix/source.nix: -------------------------------------------------------------------------------- 1 | { lib, fetchFromGitHub, fetchgit, fetchzip, ... }: 2 | with lib; 3 | let 4 | make-src-filter = src: with lib; 5 | cleanSourceWith { 6 | inherit src; 7 | filter = 8 | name: type: let baseName = baseNameOf (toString name); in 9 | ((type == "regular" && hasSuffix ".hs" baseName) || 10 | (hasSuffix ".yaml" baseName) || 11 | (hasSuffix ".cabal" baseName) || 12 | (hasSuffix ".css" baseName) || 13 | (hasSuffix ".html" baseName) || 14 | (hasSuffix ".png" baseName) || 15 | (hasSuffix ".js" baseName) || 16 | (baseName == "README.md") || 17 | (baseName == "LICENSE") || 18 | (type == "directory" && baseName != "examples") || 19 | (type == "directory" && baseName != "dist")); 20 | }; 21 | in 22 | { 23 | sse = make-src-filter ../examples/sse; 24 | miso = make-src-filter ../.; 25 | examples = make-src-filter ../examples; 26 | sample-app = make-src-filter ../sample-app; 27 | haskell-miso = make-src-filter ../haskell-miso.org; 28 | miso-from-html = fetchFromGitHub { 29 | owner = "dmjio"; 30 | repo = "miso-from-html"; 31 | rev = "8c7635889ca0a5aaac36a8b21db7f5e5ec0ae4c9"; 32 | sha256 = "0s6kzqxbshsnqbqfj7rblqkrr5mzkjxknb6k8m8z4h10mcv1zh7j"; 33 | }; 34 | jsaddle = fetchFromGitHub { 35 | owner = "ghcjs"; 36 | repo = "jsaddle"; 37 | rev = "0d5e427cb99391179b143dc93dfbac9c1019237b"; 38 | sha256 = "sha256-jyJ7bdz0gNLOSzRxOWcv7eWGIwo3N/O4PcY7HyNF8Fo="; 39 | }; 40 | servant-client-js = fetchFromGitHub { 41 | owner = "amesgen"; 42 | repo = "servant-client-js"; 43 | rev = "3ff9ad6906ebeeae52a7eaa31f7026790a59769a"; 44 | hash = "sha256-7x2bxbm2cyuzhotXtdQ0jwfc0aMzjQ/fxDfHjmVvivQ="; 45 | }; 46 | flatris = fetchFromGitHub { 47 | owner = "dmjio"; 48 | repo = "hs-flatris"; 49 | rev = "aa7a2e00cf87832de660718c15f1d85093ded103"; 50 | hash = "sha256-RLBfjIGeoSTsAuxh8Pa8kcQkupVtFZEYewDE783lZFg="; 51 | }; 52 | miso-plane = fetchFromGitHub { 53 | owner = "dmjio"; 54 | repo = "miso-plane"; 55 | rev = "f143eb9"; 56 | hash = "sha256-S5urxw4eHrxsrZ9ivHeW5Nwec5eqpeat6GusNrxS+08="; 57 | }; 58 | hs2048 = fetchFromGitHub { 59 | owner = "dmjio"; 60 | repo = "hs2048"; 61 | rev = "65d7b3d"; 62 | hash = "sha256-46dhEMVutlPheLyw3/11WxF0STr9O3kR5w8xXMs9mCw="; 63 | }; 64 | snake = fetchFromGitHub { 65 | owner = "dmjio"; 66 | repo = "miso-snake"; 67 | rev = "1da7afa"; 68 | hash = "sha256-bAfIlnd3PRn9wqGn38R3+6ok1dxRR3Jeb+UUIYJZ7/M="; 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /nix/wasm/default.nix: -------------------------------------------------------------------------------- 1 | self: super: 2 | { 3 | 4 | wasm-flake = "gitlab:haskell-wasm/ghc-wasm-meta?host=gitlab.haskell.org"; 5 | 6 | ghc-wasm-meta = 7 | let 8 | src = self.wasm-flake; 9 | in 10 | (builtins.getFlake src).outputs.packages."${super.system}"; 11 | 12 | wasm-cabal = 13 | self.ghc-wasm-meta.wasm32-wasi-cabal-9_12; 14 | 15 | wasm-ghc = 16 | self.ghc-wasm-meta.wasm32-wasi-ghc-9_12; 17 | 18 | wasmWebBuilder = args: with self; 19 | # dmj: note, only works for single files (no cabal / hackage support) 20 | super.stdenv.mkDerivation { 21 | inherit (args) src name; 22 | buildInputs = [ ghc-wasm-meta.all_9_12 ]; 23 | buildCommand = with args; '' 24 | mkdir -p $out/${name}.jsexe/ 25 | 26 | wasm32-wasi-ghc \ 27 | -no-hs-main -optl-mexec-model=reactor "-optl-Wl,--export=hs_start" \ 28 | ${src} -o ${name}.wasm 29 | 30 | $(wasm32-wasi-ghc --print-libdir)/post-link.mjs \ 31 | --input ${name}.wasm \ 32 | --output ghc_wasm_jsffi.js 33 | 34 | cp -v ghc_wasm_jsffi.js $out/${name}.jsexe/ 35 | mv -v ${name}.wasm $out/${name}.jsexe/ 36 | cat ${wasmIndexJs name} > $out/${name}.jsexe/index.js 37 | cat ${wasmIndexHtml title name scripts} > $out/${name}.jsexe/index.html 38 | ''; 39 | }; 40 | 41 | hello-world-web-wasm = 42 | self.wasmWebBuilder 43 | { name = "hello-world"; 44 | title = "Hello world Example"; 45 | src = self.wasmHelloWorld; 46 | scripts = ""; 47 | }; 48 | 49 | # nix-build -A wasmExamples 50 | # && ./result/bin/build.sh 51 | # && nix-build -A svgWasm 52 | # && http-server ./result/svg.wasmexe 53 | svgWasm = 54 | self.wasmPkgExample { 55 | name = "svg"; 56 | title = "SVG WASM Example"; 57 | scripts = ""; 58 | }; 59 | 60 | componentsWasm = 61 | self.wasmPkgExample { 62 | name = "components"; 63 | title = "Components WASM Example"; 64 | scripts = ""; 65 | }; 66 | 67 | threejsWasm = 68 | self.wasmPkgExample { 69 | name = "threejs"; 70 | title = "Three.js WASM Example"; 71 | scripts = '' 72 | 73 | 74 | ''; 75 | }; 76 | 77 | canvas2DWasm = 78 | self.wasmPkgExample { 79 | name = "canvas2d"; 80 | title = "Canvas 2D WASM Example"; 81 | scripts = ""; 82 | }; 83 | 84 | todoWasm = 85 | self.wasmPkgExample { 86 | name = "todo-mvc"; 87 | title = "Todo-mvc WASM Example"; 88 | scripts = ""; 89 | }; 90 | 91 | # call nix-build -A wasmExamples && ./result/bin/build.sh 92 | # to populate examples 93 | wasmExamples = self.writeScriptBin "build.sh" '' 94 | nix shell '${self.wasm-flake}' --command wasm32-wasi-cabal update 95 | nix shell '${self.wasm-flake}' --command wasm32-wasi-cabal clean 96 | nix shell '${self.wasm-flake}' --command wasm32-wasi-cabal build miso examples 97 | ''; 98 | 99 | # Used for packaging up cabal-built wasm packages 100 | # since GHC WASM isn't in nixpkgs yet (pending LLVM patches) 101 | # we must build the executables first w/ cabal and then package them up w/nix 102 | # Call "nix shell 'gitlab:haskell-wasm/ghc-wasm-meta?host=gitlab.haskell.org' \ 103 | # --command wasm32-wasi-cabal build miso-examples --allow-newer" 104 | wasmPkgExample = args: with args; 105 | super.stdenv.mkDerivation { 106 | inherit (args) name; 107 | src = ../../dist-newstyle/build/wasm32-wasi; 108 | buildInputs = [ self.ghc-wasm-meta.all_9_12 ]; 109 | buildCommand = '' 110 | 111 | export WASMPATH=$src/ghc-*/*/x/${name}/build/${name}/ 112 | 113 | mkdir -p $out/${name}.jsexe 114 | $(wasm32-wasi-ghc --print-libdir)/post-link.mjs \ 115 | --input $WASMPATH/${name}.wasm \ 116 | --output $out/${name}.jsexe/ghc_wasm_jsffi.js 117 | 118 | cp -v $WASMPATH/${name}.wasm $out/${name}.jsexe/ 119 | cat ${self.wasmIndexJs name} > $out/${name}.jsexe/index.js 120 | cat ${self.wasmIndexHtml title name scripts} > $out/${name}.jsexe/index.html 121 | ''; 122 | }; 123 | 124 | wasmHelloWorld = super.writeTextFile { 125 | name = "Main.hs"; 126 | text = builtins.readFile ../../examples/wasm-hello-world/Main.hs; 127 | }; 128 | 129 | wasmIndexHtml = title: name: scripts: 130 | super.writeTextFile { 131 | name = "index.html"; 132 | text = '' 133 | 134 | 135 | 136 | 137 | ${title} 138 | ${scripts} 139 | 140 | 141 | 142 | 143 | 144 | 145 | ''; 146 | }; 147 | 148 | wasmIndexJs = name: 149 | super.writeTextFile { 150 | name = "index.js"; 151 | text = 152 | '' 153 | import { WASI, OpenFile, File, ConsoleStdout } from "https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.3.0/dist/index.js"; 154 | import ghc_wasm_jsffi from "./ghc_wasm_jsffi.js"; 155 | 156 | const args = []; 157 | const env = ["GHCRTS=-H64m"]; 158 | const fds = [ 159 | new OpenFile(new File([])), // stdin 160 | ConsoleStdout.lineBuffered((msg) => console.log(`[WASI stdout] ''${msg}`)), 161 | ConsoleStdout.lineBuffered((msg) => console.warn(`[WASI stderr] ''${msg}`)), 162 | ]; 163 | const options = { debug: false }; 164 | const wasi = new WASI(args, env, fds, options); 165 | 166 | const instance_exports = {}; 167 | const { instance } = await WebAssembly.instantiateStreaming(fetch("${name}.wasm"), { 168 | wasi_snapshot_preview1: wasi.wasiImport, 169 | ghc_wasm_jsffi: ghc_wasm_jsffi(instance_exports), 170 | }); 171 | Object.assign(instance_exports, instance.exports); 172 | 173 | wasi.initialize(instance); 174 | await instance.exports.hs_start(globalThis.example); 175 | ''; 176 | }; 177 | } 178 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haskell-miso", 3 | "version": "1.9.0", 4 | "description": "miso: A tasty Haskell front-end web framework", 5 | "scripts": { 6 | "clean": "tsc --build --clean && find ts -name '*~' -or -name '*.js' -delete", 7 | "lint": "eslint --fix ts/*.ts", 8 | "test": "bun test && bun run lcov", 9 | "lcov": "lcov-viewer lcov -o ./coverage ./coverage/lcov.info", 10 | "watch": "tsc ts/miso.ts --watch", 11 | "watch-test": "tsc ts/spec/*.ts --watch", 12 | "test-runner": "bun test --watch", 13 | "pretty": "prettier --write ts/miso/*.ts ts/*.ts", 14 | "build": "bun build --outfile=js/miso.js ./ts/index.ts --target=browser", 15 | "prod": "bun build --outfile=js/miso.prod.js ./ts/index.ts --target=browser --minify" 16 | }, 17 | "type": "module", 18 | "module": "ts/miso.ts", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/dmjio/miso.git" 22 | }, 23 | "keywords": [ 24 | "miso", 25 | "virtual-dom", 26 | "haskell" 27 | ], 28 | "author": "dmijo", 29 | "license": "BSD-3-Clause", 30 | "bugs": { 31 | "url": "https://github.com/dmjio/miso/issues" 32 | }, 33 | "prettier": { 34 | "singleQuote": true, 35 | "printWidth": 100, 36 | "quoteProps": "preserve" 37 | }, 38 | "homepage": "https://haskell-miso.org", 39 | "devDependencies": { 40 | "@happy-dom/global-registrator": "^17.5.6", 41 | "prettier": "3.5.3", 42 | "@types/bun": "latest" 43 | }, 44 | "files": [ 45 | "ts/miso.ts", 46 | "ts/happydom.ts", 47 | "ts/index.ts", 48 | "ts/miso/dom.ts", 49 | "ts/miso/event.ts", 50 | "ts/miso/hydrate.ts", 51 | "ts/miso/smart.ts", 52 | "ts/miso/types.ts", 53 | "ts/miso/util.ts", 54 | "ts/miso.spec.ts", 55 | "ts/spec/dom.ts", 56 | "ts/spec/event.ts", 57 | "ts/spec/hydrate.ts", 58 | "ts/spec/smart.ts", 59 | "ts/spec/types.ts", 60 | "ts/spec/util.ts", 61 | "package.json", 62 | "README.md" 63 | ], 64 | "dependencies": { 65 | "@lcov-viewer/cli": "^1.3.0", 66 | "eslint": "^9.27.0", 67 | "typescript": "^5.8.3" 68 | }, 69 | "peerDependencies": { 70 | "typescript": "^5.0.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /sample-app/.gitignore: -------------------------------------------------------------------------------- 1 | /dist-newstyle -------------------------------------------------------------------------------- /sample-app/Main.hs: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------------- 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE CPP #-} 5 | ---------------------------------------------------------------------------- 6 | module Main where 7 | ---------------------------------------------------------------------------- 8 | import Miso 9 | import Miso.String 10 | import Miso.Lens 11 | ---------------------------------------------------------------------------- 12 | -- | Component model state 13 | data Model 14 | = Model 15 | { _counter :: Int 16 | } deriving (Show, Eq) 17 | ---------------------------------------------------------------------------- 18 | counter :: Lens Model Int 19 | counter = lens _counter $ \record field -> record { _counter = field } 20 | ---------------------------------------------------------------------------- 21 | -- | Sum type for App events 22 | data Action 23 | = AddOne 24 | | SubtractOne 25 | | SayHelloWorld 26 | deriving (Show, Eq) 27 | ---------------------------------------------------------------------------- 28 | -- | Entry point for a miso application 29 | main :: IO () 30 | main = run (startComponent app) 31 | ---------------------------------------------------------------------------- 32 | -- | WASM export, required when compiling w/ the WASM backend. 33 | #ifdef WASM 34 | foreign export javascript "hs_start" main :: IO () 35 | #endif 36 | ---------------------------------------------------------------------------- 37 | -- | `defaultApp` takes as arguments the initial model, update function, view function 38 | app :: Component name Model Action 39 | app = defaultComponent emptyModel updateModel viewModel 40 | ---------------------------------------------------------------------------- 41 | -- | Empty application state 42 | emptyModel :: Model 43 | emptyModel = Model 0 44 | ---------------------------------------------------------------------------- 45 | -- | Updates model, optionally introduces side effects 46 | updateModel :: Action -> Effect Model Action 47 | updateModel = \case 48 | AddOne -> counter += 1 49 | SubtractOne -> counter -= 1 50 | SayHelloWorld -> io_ $ do 51 | alert "Hello World" 52 | consoleLog "Hello World" 53 | ---------------------------------------------------------------------------- 54 | -- | Constructs a virtual DOM from a model 55 | viewModel :: Model -> View Action 56 | viewModel x = div_ [] 57 | [ button_ [ onClick AddOne ] [ text "+" ] 58 | , text $ ms (x ^. counter) 59 | , button_ [ onClick SubtractOne ] [ text "-" ] 60 | , br_ [] 61 | , button_ [ onClick SayHelloWorld ] [ text "Alert Hello World!" ] 62 | ] 63 | ---------------------------------------------------------------------------- 64 | -------------------------------------------------------------------------------- /sample-app/app.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: app 3 | version: 0.1.0.0 4 | synopsis: Sample miso app 5 | category: Web 6 | 7 | common wasm 8 | if arch(wasm32) 9 | ghc-options: 10 | -no-hs-main 11 | -optl-mexec-model=reactor 12 | "-optl-Wl,--export=hs_start" 13 | cpp-options: 14 | -DWASM 15 | 16 | executable app 17 | import: 18 | wasm 19 | main-is: 20 | Main.hs 21 | build-depends: 22 | base, miso 23 | default-language: 24 | Haskell2010 25 | -------------------------------------------------------------------------------- /sample-app/default.nix: -------------------------------------------------------------------------------- 1 | with (import ../default.nix {}); 2 | { 3 | inherit pkgs; 4 | inherit sample-app-js; 5 | inherit sample-app; 6 | } 7 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkg ? "ghc" }: 2 | 3 | with (import ./default.nix {}); 4 | 5 | if pkg == "ghcjs" 6 | then miso-ghcjs.env 7 | else miso-ghc-9122.env -------------------------------------------------------------------------------- /src/Miso.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE CPP #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | {-# LANGUAGE TemplateHaskell #-} 5 | {-# OPTIONS_GHC -Wno-duplicate-exports #-} 6 | ----------------------------------------------------------------------------- 7 | -- | 8 | -- Module : Miso 9 | -- Copyright : (C) 2016-2025 David M. Johnson (@dmjio) 10 | -- License : BSD3-style (see the file LICENSE) 11 | -- Maintainer : David M. Johnson 12 | -- Stability : experimental 13 | -- Portability : non-portable 14 | ---------------------------------------------------------------------------- 15 | module Miso 16 | ( -- * API 17 | -- ** Entry 18 | miso 19 | , (🍜) 20 | , startComponent 21 | -- ** Sink 22 | , withSink 23 | , Sink 24 | -- ** Sampling 25 | , sample 26 | , sample' 27 | -- ** Message Passing 28 | , notify 29 | , notify' 30 | -- ** Subscription 31 | , startSub 32 | , stopSub 33 | , Sub 34 | -- ** Effect 35 | , issue 36 | , batch 37 | , io 38 | , io_ 39 | , for 40 | , module Miso.Types 41 | -- * Effect 42 | , module Miso.Effect 43 | -- * Event 44 | , module Miso.Event 45 | -- * Property 46 | , module Miso.Property 47 | -- * Html 48 | , module Miso.Html 49 | , module Miso.Render 50 | -- * Router 51 | , module Miso.Router 52 | -- * Run 53 | , module Miso.Run 54 | -- * Exception 55 | , module Miso.Exception 56 | -- * Subscriptions 57 | , module Miso.Subscription 58 | -- * Storage 59 | , module Miso.Storage 60 | -- * Fetch 61 | , module Miso.Fetch 62 | -- * Util 63 | , module Miso.Util 64 | -- * FFI 65 | , module Miso.FFI 66 | -- * State management 67 | , ask 68 | , modify 69 | , modify' 70 | , get 71 | , gets 72 | , put 73 | , tell 74 | ) where 75 | ----------------------------------------------------------------------------- 76 | import Control.Monad (void) 77 | import Control.Monad.IO.Class (liftIO) 78 | import Control.Monad.RWS (get, gets, modify, modify', tell, put, ask) 79 | import Data.IORef (newIORef) 80 | import Language.Javascript.JSaddle (Object(Object), JSM) 81 | #ifndef GHCJS_BOTH 82 | import Data.FileEmbed (embedStringFile) 83 | import Language.Javascript.JSaddle (eval) 84 | import Miso.String (MisoString) 85 | #endif 86 | ----------------------------------------------------------------------------- 87 | import Miso.Diff 88 | import Miso.Effect 89 | import Miso.Event 90 | import Miso.Exception 91 | import Miso.Fetch 92 | import Miso.FFI 93 | import qualified Miso.FFI.Internal as FFI 94 | import Miso.Html 95 | import Miso.Internal 96 | import Miso.Property 97 | import Miso.Render 98 | import Miso.Router 99 | import Miso.Run 100 | import Miso.Storage 101 | import Miso.Subscription 102 | import Miso.Types 103 | import Miso.Util 104 | ---------------------------------------------------------------------------- 105 | -- | Runs an isomorphic @miso@ application. 106 | -- Assumes the pre-rendered DOM is already present. 107 | -- Note: Uses 'mountPoint' as the 'Component' name. 108 | -- Always mounts to \. Copies page into the virtual DOM. 109 | miso :: Eq model => (URI -> Component name model action) -> JSM () 110 | miso f = withJS $ do 111 | app@Component {..} <- f <$> getURI 112 | initialize app $ \snk -> do 113 | renderStyles styles 114 | VTree (Object vtree) <- runView Hydrate (view model) snk logLevel events 115 | let name = getMountPoint mountPoint 116 | FFI.setBodyComponent name 117 | mount <- FFI.getBody 118 | FFI.hydrate (logLevel `elem` [DebugHydrate, DebugAll]) mount vtree 119 | viewRef <- liftIO $ newIORef $ VTree (Object vtree) 120 | pure (name, mount, viewRef) 121 | ----------------------------------------------------------------------------- 122 | -- | Alias for 'miso'. 123 | (🍜) :: Eq model => (URI -> Component name model action) -> JSM () 124 | (🍜) = miso 125 | ---------------------------------------------------------------------------- 126 | -- | Runs a miso application 127 | -- Initializes application at 'mountPoint' (defaults to \ when @Nothing@) 128 | startComponent :: Eq model => Component name model action -> JSM () 129 | startComponent app@Component {..} = withJS $ 130 | initialize app $ \snk -> do 131 | renderStyles styles 132 | vtree <- runView Draw (view model) snk logLevel events 133 | let name = getMountPoint mountPoint 134 | FFI.setBodyComponent name 135 | mount <- mountElement name 136 | diff Nothing (Just vtree) mount 137 | viewRef <- liftIO (newIORef vtree) 138 | pure (name, mount, viewRef) 139 | ----------------------------------------------------------------------------- 140 | -- | Used when compiling with jsaddle to make miso's JavaScript present in 141 | -- the execution context. 142 | withJS :: JSM a -> JSM () 143 | withJS action = void $ do 144 | #ifndef GHCJS_BOTH 145 | #ifdef PRODUCTION 146 | _ <- eval ($(embedStringFile "js/miso.prod.js") :: MisoString) 147 | #else 148 | _ <- eval ($(embedStringFile "js/miso.js") :: MisoString) 149 | #endif 150 | #endif 151 | action 152 | ----------------------------------------------------------------------------- 153 | -------------------------------------------------------------------------------- /src/Miso/Concurrent.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | ----------------------------------------------------------------------------- 4 | -- | 5 | -- Module : Miso.Concurrent 6 | -- Copyright : (C) 2016-2025 David M. Johnson 7 | -- License : BSD3-style (see the file LICENSE) 8 | -- Maintainer : David M. Johnson 9 | -- Stability : experimental 10 | -- Portability : non-portable 11 | ---------------------------------------------------------------------------- 12 | module Miso.Concurrent 13 | ( Waiter (..) 14 | , waiter 15 | ) where 16 | ----------------------------------------------------------------------------- 17 | import Control.Concurrent 18 | ----------------------------------------------------------------------------- 19 | data Waiter 20 | = Waiter 21 | { wait :: IO () 22 | -- ^ Blocks on MVar 23 | , serve :: IO () 24 | -- ^ Unblocks threads waiting on MVar 25 | } 26 | ----------------------------------------------------------------------------- 27 | -- | Creates a new 'Waiter' 28 | waiter :: IO Waiter 29 | waiter = do 30 | mvar <- newEmptyMVar 31 | pure Waiter 32 | { wait = takeMVar mvar 33 | , serve = do 34 | _ <- tryPutMVar mvar () 35 | pure () 36 | } 37 | ----------------------------------------------------------------------------- 38 | -------------------------------------------------------------------------------- /src/Miso/Delegate.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE RecordWildCards #-} 2 | ----------------------------------------------------------------------------- 3 | {-# LANGUAGE OverloadedStrings #-} 4 | ----------------------------------------------------------------------------- 5 | -- | 6 | -- Module : Miso.Delegate 7 | -- Copyright : (C) 2016-2025 David M. Johnson 8 | -- License : BSD3-style (see the file LICENSE) 9 | -- Maintainer : David M. Johnson 10 | -- Stability : experimental 11 | -- Portability : non-portable 12 | ---------------------------------------------------------------------------- 13 | module Miso.Delegate 14 | ( delegator 15 | , undelegator 16 | ) where 17 | ----------------------------------------------------------------------------- 18 | import Control.Monad.IO.Class (liftIO) 19 | import Data.IORef (IORef, readIORef) 20 | import qualified Data.Map.Strict as M 21 | import Language.Javascript.JSaddle (create, JSM, JSVal, Object(..), ToJSVal(toJSVal)) 22 | import Miso.Html.Types (VTree(..)) 23 | import Miso.String (MisoString) 24 | import qualified Miso.FFI.Internal as FFI 25 | ----------------------------------------------------------------------------- 26 | -- | Local Event type, used to create field names for a delegated event 27 | data Event 28 | = Event 29 | { name :: MisoString 30 | -- ^ Event name 31 | , capture :: Bool 32 | -- ^ Capture settings for event 33 | } deriving (Show, Eq) 34 | ----------------------------------------------------------------------------- 35 | -- | Instance used to initialize event delegation 36 | instance ToJSVal Event where 37 | toJSVal Event {..} = do 38 | o <- create 39 | flip (FFI.set "name") o =<< toJSVal name 40 | flip (FFI.set "capture") o =<< toJSVal capture 41 | toJSVal o 42 | ----------------------------------------------------------------------------- 43 | -- | Entry point for event delegation 44 | delegator 45 | :: JSVal 46 | -> IORef VTree 47 | -> M.Map MisoString Bool 48 | -> Bool 49 | -> JSM () 50 | delegator mountPointElement vtreeRef es debug = do 51 | evts <- toJSVal (uncurry Event <$> M.toList es) 52 | FFI.delegateEvent mountPointElement evts debug $ do 53 | VTree (Object vtree) <- liftIO (readIORef vtreeRef) 54 | pure vtree 55 | ----------------------------------------------------------------------------- 56 | -- | Entry point for deinitalizing event delegation 57 | undelegator 58 | :: JSVal 59 | -> IORef VTree 60 | -> M.Map MisoString Bool 61 | -> Bool 62 | -> JSM () 63 | undelegator mountPointElement vtreeRef es debug = do 64 | events <- toJSVal (M.toList es) 65 | FFI.undelegateEvent mountPointElement events debug $ do 66 | VTree (Object vtree) <- liftIO (readIORef vtreeRef) 67 | pure vtree 68 | ----------------------------------------------------------------------------- 69 | -------------------------------------------------------------------------------- /src/Miso/Diff.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE OverloadedStrings #-} 3 | ----------------------------------------------------------------------------- 4 | -- | 5 | -- Module : Miso.Diff 6 | -- Copyright : (C) 2016-2025 David M. Johnson 7 | -- License : BSD3-style (see the file LICENSE) 8 | -- Maintainer : David M. Johnson 9 | -- Stability : experimental 10 | -- Portability : non-portable 11 | ---------------------------------------------------------------------------- 12 | module Miso.Diff 13 | ( diff 14 | , mountElement 15 | ) where 16 | ----------------------------------------------------------------------------- 17 | import GHCJS.Foreign.Internal hiding (Object) 18 | import GHCJS.Types 19 | import JavaScript.Object.Internal 20 | ----------------------------------------------------------------------------- 21 | import qualified Miso.FFI.Internal as FFI 22 | import Miso.FFI.Internal (JSM) 23 | import Miso.Html.Types 24 | import Miso.String 25 | ----------------------------------------------------------------------------- 26 | -- | diffing / patching a given element 27 | diff :: Maybe VTree -> Maybe VTree -> JSVal -> JSM () 28 | diff current new mountEl = 29 | case (current, new) of 30 | (Nothing, Nothing) -> pure () 31 | (Just (VTree current'), Just (VTree new')) -> 32 | FFI.diff current' new' mountEl 33 | (Nothing, Just (VTree new')) -> do 34 | FFI.diff (Object jsNull) new' mountEl 35 | (Just (VTree current'), Nothing) -> 36 | FFI.diff current' (Object jsNull) mountEl 37 | ----------------------------------------------------------------------------- 38 | -- | return the configured mountPoint element or the body 39 | mountElement :: MisoString -> JSM JSVal 40 | mountElement "body" = FFI.getBody 41 | mountElement e = FFI.getElementById e 42 | ----------------------------------------------------------------------------- 43 | -------------------------------------------------------------------------------- /src/Miso/Event/Decoder.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE CPP #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | ----------------------------------------------------------------------------- 6 | -- | 7 | -- Module : Miso.Event.Decoder 8 | -- Copyright : (C) 2016-2025 David M. Johnson 9 | -- License : BSD3-style (see the file LICENSE) 10 | -- Maintainer : David M. Johnson 11 | -- Stability : experimental 12 | -- Portability : non-portable 13 | ---------------------------------------------------------------------------- 14 | module Miso.Event.Decoder 15 | ( -- ** Types 16 | Decoder (..) 17 | , DecodeTarget (..) 18 | -- ** Combinators 19 | , at 20 | -- ** Decoders 21 | , emptyDecoder 22 | , keycodeDecoder 23 | , keyInfoDecoder 24 | , checkedDecoder 25 | , valueDecoder 26 | , pointerDecoder 27 | ) where 28 | ----------------------------------------------------------------------------- 29 | import Control.Applicative 30 | import Data.Aeson.Types 31 | #ifdef GHCJS_OLD 32 | import GHCJS.Marshal (ToJSVal(toJSVal)) 33 | #else 34 | import Language.Javascript.JSaddle (ToJSVal(toJSVal)) 35 | #endif 36 | ----------------------------------------------------------------------------- 37 | import Miso.Event.Types 38 | import Miso.String 39 | ----------------------------------------------------------------------------- 40 | -- | Data type representing path (consisting of field names) within event object 41 | -- where a decoder should be applied. 42 | data DecodeTarget 43 | = DecodeTarget [MisoString] 44 | -- ^ Specify single path within Event object, where a decoder should be applied. 45 | | DecodeTargets [[MisoString]] 46 | -- ^ Specify multiple paths withing Event object, where decoding should be attempted. The first path where decoding suceeds is the one taken. 47 | ----------------------------------------------------------------------------- 48 | -- | `ToJSVal` instance for `Decoder` 49 | instance ToJSVal DecodeTarget where 50 | toJSVal (DecodeTarget xs) = toJSVal xs 51 | toJSVal (DecodeTargets xs) = toJSVal xs 52 | ----------------------------------------------------------------------------- 53 | -- | Decoder data type for parsing events 54 | data Decoder a 55 | = Decoder 56 | { decoder :: Value -> Parser a 57 | -- ^ FromJSON-based Event decoder 58 | , decodeAt :: DecodeTarget 59 | -- ^ Location in DOM of where to decode 60 | } 61 | ----------------------------------------------------------------------------- 62 | -- | Smart constructor for building a `Decoder`. 63 | at :: [MisoString] -> (Value -> Parser a) -> Decoder a 64 | at decodeAt decoder = Decoder {decodeAt = DecodeTarget decodeAt, ..} 65 | ----------------------------------------------------------------------------- 66 | -- | Empty decoder for use with events like "click" that do not 67 | -- return any meaningful values 68 | emptyDecoder :: Decoder () 69 | emptyDecoder = mempty `at` go 70 | where 71 | go = withObject "emptyDecoder" $ \_ -> pure () 72 | ----------------------------------------------------------------------------- 73 | -- | Retrieves either "keyCode", "which" or "charCode" field in `Decoder` 74 | keycodeDecoder :: Decoder KeyCode 75 | keycodeDecoder = Decoder {..} 76 | where 77 | decodeAt = DecodeTarget mempty 78 | decoder = withObject "event" $ \o -> 79 | KeyCode <$> (o .: "keyCode" <|> o .: "which" <|> o .: "charCode") 80 | ----------------------------------------------------------------------------- 81 | -- | Retrieves either "keyCode", "which" or "charCode" field in `Decoder`, 82 | -- along with shift, ctrl, meta and alt. 83 | keyInfoDecoder :: Decoder KeyInfo 84 | keyInfoDecoder = Decoder {..} 85 | where 86 | decodeAt = 87 | DecodeTarget mempty 88 | decoder = 89 | withObject "event" $ \o -> 90 | KeyInfo 91 | <$> (o .: "keyCode" <|> o .: "which" <|> o .: "charCode") 92 | <*> o .: "shiftKey" 93 | <*> o .: "metaKey" 94 | <*> o .: "ctrlKey" 95 | <*> o .: "altKey" 96 | ----------------------------------------------------------------------------- 97 | -- | Retrieves "value" field in `Decoder` 98 | valueDecoder :: Decoder MisoString 99 | valueDecoder = Decoder {..} 100 | where 101 | decodeAt = DecodeTarget ["target"] 102 | decoder = withObject "target" $ \o -> o .: "value" 103 | ----------------------------------------------------------------------------- 104 | -- | Retrieves "checked" field in Decoder 105 | checkedDecoder :: Decoder Checked 106 | checkedDecoder = Decoder {..} 107 | where 108 | decodeAt = DecodeTarget ["target"] 109 | decoder = withObject "target" $ \o -> 110 | Checked <$> (o .: "checked") 111 | ----------------------------------------------------------------------------- 112 | -- | Pointer decoder for use with events like "onpointerover" 113 | pointerDecoder :: Decoder PointerEvent 114 | pointerDecoder = Decoder {..} 115 | where 116 | pair o x y = liftA2 (,) (o .: x) (o .: y) 117 | decodeAt = DecodeTarget mempty 118 | decoder = withObject "pointerDecoder" $ \o -> 119 | PointerEvent 120 | <$> o .: "pointerType" 121 | <*> o .: "pointerId" 122 | <*> o .: "isPrimary" 123 | <*> pair o "clientX" "clientY" 124 | <*> pair o "screenX" "screenY" 125 | <*> pair o "pageX" "pageY" 126 | <*> pair o "tiltX" "tiltY" 127 | <*> o .: "pressure" 128 | ----------------------------------------------------------------------------- 129 | -------------------------------------------------------------------------------- /src/Miso/Exception.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE OverloadedStrings #-} 3 | ----------------------------------------------------------------------------- 4 | -- | 5 | -- Module : Miso.Exception 6 | -- Copyright : (C) 2016-2025 David M. Johnson 7 | -- License : BSD3-style (see the file LICENSE) 8 | -- Maintainer : David M. Johnson 9 | -- Stability : experimental 10 | -- Portability : non-portable 11 | ---------------------------------------------------------------------------- 12 | module Miso.Exception 13 | ( -- ** Types 14 | MisoException (..) 15 | -- ** Functions 16 | , exception 17 | ) where 18 | ---------------------------------------------------------------------------- 19 | import Control.Exception 20 | import Language.Javascript.JSaddle 21 | ---------------------------------------------------------------------------- 22 | import Miso.String (MisoString, ms) 23 | import qualified Miso.FFI as FFI 24 | ---------------------------------------------------------------------------- 25 | -- | The @MisoException@ type is used to catch @Component@-related mounting errors. 26 | -- 27 | -- The two mounting errors that can occur during the lifetime of a miso application are 28 | -- 29 | -- * Not Mounted Exception 30 | -- 31 | -- This occurs if a user tries to call @sample myComponent@ when @myComponent@ is currently 32 | -- not mounted on the DOM. 33 | -- 34 | -- * Already Mounted Exception 35 | -- 36 | -- It is a requirement that all @Component@ be named uniquely 37 | -- (this is to avoid runaway recursion during mounting). 38 | -- If we detect a @Component@ is attempting to be mounted twice 39 | -- this exception will be raised. 40 | -- 41 | -- Other exceptions can arise, but its up to the user to handle them in 42 | -- the @update@ function. All unhandled exceptions are caught in the event loop 43 | -- and logged to the console with /console.error()/ 44 | -- 45 | data MisoException 46 | = NotMountedException MisoString 47 | -- ^ Thrown when a @Component@ is sampled, yet not mounted. 48 | | AlreadyMountedException MisoString 49 | -- ^ Thrown when a @Component@ is attempted to be mounted twice. 50 | deriving (Show, Eq) 51 | ---------------------------------------------------------------------------- 52 | instance Exception MisoException 53 | ---------------------------------------------------------------------------- 54 | -- | Exception handler 55 | -- 56 | -- Used to catch @Component@ mounting exceptions 57 | -- 58 | -- > action `catch` exception 59 | exception :: SomeException -> JSM JSVal 60 | exception ex 61 | | Just (NotMountedException name) <- fromException ex = do 62 | FFI.consoleError 63 | ("NotMountedException: Could not sample model state from the Component \"" <> name <> "\"") 64 | pure jsNull 65 | | Just (AlreadyMountedException name) <- fromException ex = do 66 | FFI.consoleError ("AlreadyMountedException: Component \"" <> name <> "\" is already") 67 | pure jsNull 68 | | otherwise = do 69 | FFI.consoleError ("UnknownException: " <> ms ex) 70 | pure jsNull 71 | ---------------------------------------------------------------------------- 72 | -------------------------------------------------------------------------------- /src/Miso/FFI.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Miso.FFI 4 | -- Copyright : (C) 2016-2025 David M. Johnson 5 | -- License : BSD3-style (see the file LICENSE) 6 | -- Maintainer : David M. Johnson 7 | -- Stability : experimental 8 | -- Portability : non-portable 9 | ---------------------------------------------------------------------------- 10 | module Miso.FFI 11 | ( -- * Functions 12 | set 13 | , now 14 | , consoleLog 15 | , consoleLog' 16 | , consoleError 17 | , consoleWarn 18 | , getElementById 19 | , focus 20 | , blur 21 | , alert 22 | , reload 23 | , addStyle 24 | , addStyleSheet 25 | , syncCallback 26 | , syncCallback1 27 | , asyncCallback 28 | -- ** Image 29 | , Image (..) 30 | , newImage 31 | ) where 32 | ----------------------------------------------------------------------------- 33 | import Miso.FFI.Internal 34 | ----------------------------------------------------------------------------- 35 | -------------------------------------------------------------------------------- /src/Miso/Fetch.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE MultiParamTypeClasses #-} 4 | {-# LANGUAGE FlexibleInstances #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE TypeApplications #-} 7 | {-# LANGUAGE TypeOperators #-} 8 | {-# LANGUAGE TypeFamilies #-} 9 | {-# LANGUAGE PolyKinds #-} 10 | {-# LANGUAGE CPP #-} 11 | {-# LANGUAGE UndecidableInstances #-} 12 | {-# LANGUAGE FlexibleContexts #-} 13 | ----------------------------------------------------------------------------- 14 | -- | 15 | -- Module : Miso.Fetch 16 | -- Copyright : (C) 2016-2025 David M. Johnson 17 | -- License : BSD3-style (see the file LICENSE) 18 | -- Maintainer : David M. Johnson 19 | -- Stability : experimental 20 | -- Portability : non-portable 21 | -- 22 | -- Module for interacting with the Fetch API 23 | -- manually. 24 | -- 25 | -- Refer to the miso README if you want to automatically interact with a Servant 26 | -- API. 27 | -- 28 | ---------------------------------------------------------------------------- 29 | module Miso.Fetch 30 | ( fetchJSON 31 | ) where 32 | 33 | import Miso.FFI.Internal (fetchJSON) 34 | -------------------------------------------------------------------------------- /src/Miso/Html.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Miso.Html 4 | -- Copyright : (C) 2016-2025 David M. Johnson 5 | -- License : BSD3-style (see the file LICENSE) 6 | -- Maintainer : David M. Johnson 7 | -- Stability : experimental 8 | -- Portability : non-portable 9 | -- 10 | -- Example usage: 11 | -- 12 | -- @ 13 | -- import Miso 14 | -- 15 | -- data IntAction = Add | Subtract 16 | -- 17 | -- intView :: Int -> View IntAction 18 | -- intView n 19 | -- = div_ 20 | -- [ class_ "main" 21 | -- ] 22 | -- [ btn_ 23 | -- [ onClick Add 24 | -- ] 25 | -- [ text_ "+" 26 | -- ] 27 | -- , text_ $ pack (show n) 28 | -- , btn_ 29 | -- [ onClick Subtract 30 | -- ] 31 | -- [ text_ "-" 32 | -- ] 33 | -- ] 34 | -- @ 35 | -- 36 | -- More information on how to use miso is available on GitHub 37 | -- 38 | -- 39 | -- 40 | ---------------------------------------------------------------------------- 41 | module Miso.Html 42 | ( -- ** Elements 43 | module Miso.Html.Element 44 | -- ** Attributes 45 | , module Miso.Html.Property 46 | -- ** Events 47 | , module Miso.Html.Event 48 | -- ** Virtual DOM 49 | , module Miso.Html.Types 50 | ) where 51 | ----------------------------------------------------------------------------- 52 | import Miso.Html.Element hiding (data_, title_) -- conflicts with helpers from Miso.Html.Property 53 | import Miso.Html.Event 54 | import Miso.Html.Types 55 | import Miso.Html.Property 56 | ----------------------------------------------------------------------------- 57 | -------------------------------------------------------------------------------- /src/Miso/Html/Types.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Miso.Html.Types 4 | -- Copyright : (C) 2016-2025 David M. Johnson 5 | -- License : BSD3-style (see the file LICENSE) 6 | -- Maintainer : David M. Johnson 7 | -- Stability : experimental 8 | -- Portability : non-portable 9 | -- 10 | -- Construct custom properties on DOM elements 11 | -- 12 | -- > div_ [ prop "id" "foo" ] [ ] 13 | -- 14 | ---------------------------------------------------------------------------- 15 | module Miso.Html.Types 16 | ( -- *** Types 17 | VTree (..) 18 | , View (..) 19 | , Attribute (..) 20 | , Key (..) 21 | , NS (..) 22 | -- *** Classes 23 | , ToView (..) 24 | -- *** Combinators 25 | , node 26 | , text 27 | , textRaw 28 | , rawHtml 29 | ) where 30 | ----------------------------------------------------------------------------- 31 | import Language.Javascript.JSaddle (Object) 32 | ----------------------------------------------------------------------------- 33 | import Miso.String hiding (reverse) 34 | import Miso.Types 35 | ----------------------------------------------------------------------------- 36 | -- | Create a new @Miso.Html.Types.TextRaw@. 37 | -- 38 | -- @expandable@ 39 | -- a 'rawHtml' node takes raw HTML and attempts to convert it to a 'VTree' 40 | -- at runtime. This is a way to dynamically populate the virtual DOM from 41 | -- HTML received at runtime. If rawHtml cannot parse the HTML it will not render. 42 | rawHtml 43 | :: MisoString 44 | -> View action 45 | rawHtml = VTextRaw 46 | ----------------------------------------------------------------------------- 47 | -- | Create a new @Miso.Html.Types.VNode@. 48 | -- 49 | -- @node ns tag key attrs children@ creates a new node with tag @tag@ 50 | -- and 'Key' @key@ in the namespace @ns@. All @attrs@ are called when 51 | -- the node is created and its children are initialized to @children@. 52 | node :: NS 53 | -> MisoString 54 | -> [Attribute action] 55 | -> [View action] 56 | -> View action 57 | node = VNode 58 | ----------------------------------------------------------------------------- 59 | -- | Create a new @Text@ with the given content. 60 | text :: MisoString -> View action 61 | text = VText 62 | ----------------------------------------------------------------------------- 63 | -- | `TextRaw` creation. Don't use directly 64 | textRaw :: MisoString -> View action 65 | textRaw = VTextRaw 66 | ----------------------------------------------------------------------------- 67 | -- | Virtual DOM implemented as a JavaScript `Object`. 68 | -- Used for diffing, patching and event delegation. 69 | -- Not meant to be constructed directly, see `View` instead. 70 | newtype VTree = VTree { getTree :: Object } 71 | ----------------------------------------------------------------------------- 72 | -------------------------------------------------------------------------------- /src/Miso/Lens/TH.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE LambdaCase #-} 3 | ----------------------------------------------------------------------------- 4 | -- | 5 | -- Module : Miso.Lens.TH 6 | -- Copyright : (C) 2016-2025 David M. Johnson 7 | -- License : BSD3-style (see the file LICENSE) 8 | -- Maintainer : David M. Johnson 9 | -- Stability : experimental 10 | -- Portability : non-portable 11 | ----------------------------------------------------------------------------- 12 | module Miso.Lens.TH (makeLenses) where 13 | ----------------------------------------------------------------------------- 14 | import Data.Maybe 15 | import Language.Haskell.TH 16 | ----------------------------------------------------------------------------- 17 | makeLenses :: Name -> Q [Dec] 18 | makeLenses name = do 19 | reify name >>= \case 20 | TyConI (NewtypeD _ _ _ _ con _) -> do 21 | case con of 22 | RecC _ fieldNames -> 23 | pure (processFieldNames fieldNames) 24 | _ -> pure [] 25 | TyConI (DataD _ _ _ _ cons _) -> 26 | flip concatMapM cons $ \case 27 | RecC _ fieldNames -> do 28 | pure (processFieldNames fieldNames) 29 | _ -> pure [] 30 | _ -> pure [] 31 | where 32 | processFieldNames fieldNames = concat 33 | [ mkFields fName (ConT name) fieldType 34 | | (fieldName, _, fieldType) <- fieldNames 35 | , let fName = nameBase fieldName 36 | , listToMaybe fName == Just '_' 37 | ] 38 | mkFields fieldName conType fieldType = 39 | let -- dmj: drops '_' prefix 40 | lensName = mkName (drop 1 fieldName) 41 | in 42 | [ FunD lensName 43 | [ Clause [] (NormalB (mkLens fieldName)) [] 44 | ] 45 | , SigD lensName (mkLensType conType fieldType) 46 | ] 47 | concatMapM f xs = 48 | concat <$> mapM f xs 49 | mkLensType conType = 50 | AppT (AppT (ConT (mkName "Lens")) conType) 51 | mkLens n = 52 | AppE (AppE (VarE (mkName "lens")) (VarE (mkName n))) $ 53 | LamE 54 | [ VarP recName, VarP fieldName ] $ 55 | RecUpdE (VarE recName) 56 | [ (mkName n, VarE fieldName) ] 57 | where 58 | recName = mkName "record" 59 | fieldName = mkName "field" 60 | ----------------------------------------------------------------------------- 61 | -------------------------------------------------------------------------------- /src/Miso/Mathml.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Miso.Mathml 4 | -- Copyright : (C) 2016-2025 David M. Johnson 5 | -- License : BSD3-style (see the file LICENSE) 6 | -- Maintainer : David M. Johnson 7 | -- Stability : experimental 8 | -- Portability : non-portable 9 | -- 10 | -- Example usage: 11 | -- 12 | -- @ 13 | -- xSquared :: View action 14 | -- xSquared = 15 | -- math_ [] 16 | -- [ msup_ [] 17 | -- [ mi_ [] [text "x"] 18 | -- , mn_ [] [text "2"] 19 | -- ] 20 | -- ] 21 | -- @ 22 | -- 23 | -- More complex example in [examples/mathml](https://github.com/dmjio/miso/blob/master/examples/mathml/Main.hs). 24 | -- 25 | ---------------------------------------------------------------------------- 26 | module Miso.Mathml 27 | ( -- * Elements 28 | module Miso.Mathml.Element 29 | -- * Properties 30 | , module Miso.Mathml.Property 31 | ) where 32 | ----------------------------------------------------------------------------- 33 | import Miso.Mathml.Element 34 | import Miso.Mathml.Property hiding (align_) 35 | ----------------------------------------------------------------------------- 36 | -------------------------------------------------------------------------------- /src/Miso/Property.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE OverloadedStrings #-} 3 | ----------------------------------------------------------------------------- 4 | -- | 5 | -- Module : Miso.Property 6 | -- Copyright : (C) 2016-2025 David M. Johnson 7 | -- License : BSD3-style (see the file LICENSE) 8 | -- Maintainer : David M. Johnson 9 | -- Stability : experimental 10 | -- Portability : non-portable 11 | -- 12 | -- Construct custom properties on DOM elements 13 | -- 14 | -- > div_ [ prop "id" "foo" ] [ ] 15 | -- 16 | ---------------------------------------------------------------------------- 17 | module Miso.Property 18 | ( -- *** Smart constructors 19 | textProp 20 | , stringProp 21 | , boolProp 22 | , intProp 23 | , integerProp 24 | , doubleProp 25 | , prop 26 | , keyProp 27 | , key_ 28 | ) where 29 | ----------------------------------------------------------------------------- 30 | import Data.Aeson (ToJSON(..)) 31 | ----------------------------------------------------------------------------- 32 | import Miso.Types 33 | import Miso.String (MisoString) 34 | ----------------------------------------------------------------------------- 35 | -- | @prop k v@ is an attribute that will set the attribute @k@ of the DOM 36 | -- node associated with the vnode to @v@. 37 | prop :: ToJSON a => MisoString -> a -> Attribute action 38 | prop k v = Property k (toJSON v) 39 | ----------------------------------------------------------------------------- 40 | -- | Set field to `Bool` value 41 | boolProp :: MisoString -> Bool -> Attribute action 42 | boolProp = prop 43 | ----------------------------------------------------------------------------- 44 | -- | Set field to `String` value 45 | stringProp :: MisoString -> String -> Attribute action 46 | stringProp = prop 47 | ----------------------------------------------------------------------------- 48 | -- | Set field to `Text` value 49 | textProp :: MisoString -> MisoString -> Attribute action 50 | textProp = prop 51 | ----------------------------------------------------------------------------- 52 | -- | Set field to `Int` value 53 | intProp :: MisoString -> Int -> Attribute action 54 | intProp = prop 55 | ----------------------------------------------------------------------------- 56 | -- | Set field to `Integer` value 57 | integerProp :: MisoString -> Integer -> Attribute action 58 | integerProp = prop 59 | ----------------------------------------------------------------------------- 60 | -- | Set field to `Double` value 61 | doubleProp :: MisoString -> Double -> Attribute action 62 | doubleProp = prop 63 | ----------------------------------------------------------------------------- 64 | -- | Set `Key` on 'VNode'. 65 | keyProp :: ToKey key => key -> Attribute action 66 | keyProp key = prop "key" (toKey key) 67 | ----------------------------------------------------------------------------- 68 | -- | Synonym for 'keyProp' 69 | -- Allows a user to specify a 'Key' inside of an @[Attribute action]@ 70 | key_ :: ToKey key => key -> Attribute action 71 | key_ = keyProp 72 | ----------------------------------------------------------------------------- 73 | -------------------------------------------------------------------------------- /src/Miso/Render.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE MultiParamTypeClasses #-} 3 | {-# LANGUAGE FlexibleInstances #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | ----------------------------------------------------------------------------- 7 | -- | 8 | -- Module : Miso.Render 9 | -- Copyright : (C) 2016-2025 David M. Johnson 10 | -- License : BSD3-style (see the file LICENSE) 11 | -- Maintainer : David M. Johnson 12 | -- Stability : experimental 13 | -- Portability : non-portable 14 | ---------------------------------------------------------------------------- 15 | module Miso.Render 16 | ( -- *** Classes 17 | ToHtml (..) 18 | -- *** Combinator 19 | , HTML 20 | ) where 21 | ---------------------------------------------------------------------------- 22 | import Data.Aeson 23 | import Data.ByteString.Builder 24 | import qualified Data.ByteString.Lazy as L 25 | import qualified Data.List.NonEmpty as NE 26 | import qualified Data.Map.Strict as M 27 | import qualified Network.HTTP.Media as M 28 | import Servant.API (Accept (..), MimeRender (..)) 29 | ---------------------------------------------------------------------------- 30 | import Miso.String hiding (intercalate) 31 | import Miso.Types 32 | ---------------------------------------------------------------------------- 33 | -- | HTML MimeType used for servant APIs 34 | -- 35 | -- > type Home = "home" :> Get '[HTML] (Component model action) 36 | -- 37 | data HTML 38 | ---------------------------------------------------------------------------- 39 | -- | @text/html;charset=utf-8@ 40 | instance Accept HTML where 41 | contentTypes _ = 42 | "text" M.// "html" M./: ("charset", "utf-8") NE.:| 43 | ["text" M.// "html"] 44 | ---------------------------------------------------------------------------- 45 | -- | Class for rendering HTML 46 | class ToHtml a where 47 | toHtml :: a -> L.ByteString 48 | ---------------------------------------------------------------------------- 49 | -- | Render a @View@ to a @L.ByteString@ 50 | instance ToHtml (View a) where 51 | toHtml = renderView 52 | ---------------------------------------------------------------------------- 53 | -- | Render a @[View]@ to a @L.ByteString@ 54 | instance ToHtml [View a] where 55 | toHtml = foldMap renderView 56 | ---------------------------------------------------------------------------- 57 | -- | Render HTML from a servant API 58 | instance ToHtml a => MimeRender HTML a where 59 | mimeRender _ = toHtml 60 | ---------------------------------------------------------------------------- 61 | renderView :: View a -> L.ByteString 62 | renderView = toLazyByteString . renderBuilder 63 | ---------------------------------------------------------------------------- 64 | intercalate :: Builder -> [Builder] -> Builder 65 | intercalate _ [] = "" 66 | intercalate _ [x] = x 67 | intercalate sep (x:xs) = 68 | mconcat 69 | [ x 70 | , sep 71 | , intercalate sep xs 72 | ] 73 | ---------------------------------------------------------------------------- 74 | renderBuilder :: View a -> Builder 75 | renderBuilder (VText "") = fromMisoString " " 76 | renderBuilder (VText s) = fromMisoString s 77 | renderBuilder (VTextRaw "") = fromMisoString " " 78 | renderBuilder (VTextRaw s) = fromMisoString s 79 | renderBuilder (VNode _ "doctype" [] []) = "" 80 | renderBuilder (VNode _ tag attrs children) = 81 | mconcat 82 | [ "<" 83 | , fromMisoString tag 84 | , mconcat [ " " <> intercalate " " (renderAttrs <$> attrs) 85 | | not (Prelude.null attrs) 86 | ] 87 | , ">" 88 | , mconcat 89 | [ mconcat 90 | [ foldMap renderBuilder (collapseSiblingTextNodes children) 91 | , " fromMisoString tag <> ">" 92 | ] 93 | | tag `notElem` ["img", "input", "br", "hr", "meta", "link"] 94 | ] 95 | ] 96 | renderBuilder (VComp mount attributes (SomeComponent Component {..})) = 97 | mconcat 98 | [ stringUtf8 "
attributes) 102 | , ">" 103 | , renderBuilder (view model) 104 | , "
" 105 | ] 106 | ---------------------------------------------------------------------------- 107 | renderAttrs :: Attribute action -> Builder 108 | renderAttrs (Property key value) = 109 | mconcat 110 | [ fromMisoString key 111 | , stringUtf8 "=\"" 112 | , toHtmlFromJSON value 113 | , stringUtf8 "\"" 114 | ] 115 | renderAttrs (Event _) = mempty 116 | renderAttrs (Styles styles) = 117 | mconcat 118 | [ "style" 119 | , stringUtf8 "=\"" 120 | , mconcat 121 | [ mconcat 122 | [ fromMisoString k 123 | , charUtf8 ':' 124 | , fromMisoString v 125 | , charUtf8 ';' 126 | ] 127 | | (k,v) <- M.toList styles 128 | ] 129 | , stringUtf8 "\"" 130 | ] 131 | ---------------------------------------------------------------------------- 132 | -- | The browser can't distinguish between multiple text nodes 133 | -- and a single text node. So it will always parse a single text node 134 | -- this means we must collapse adjacent text nodes during hydration. 135 | collapseSiblingTextNodes :: [View a] -> [View a] 136 | collapseSiblingTextNodes [] = [] 137 | collapseSiblingTextNodes (VText x : VText y : xs) = 138 | collapseSiblingTextNodes (VText (x <> y) : xs) 139 | collapseSiblingTextNodes (x:xs) = 140 | x : collapseSiblingTextNodes xs 141 | ---------------------------------------------------------------------------- 142 | -- | Helper for turning JSON into Text 143 | -- Object, Array and Null are kind of non-sensical here 144 | toHtmlFromJSON :: Value -> Builder 145 | toHtmlFromJSON (String t) = fromMisoString (ms t) 146 | toHtmlFromJSON (Number t) = fromMisoString $ ms (show t) 147 | toHtmlFromJSON (Bool True) = "true" 148 | toHtmlFromJSON (Bool False) = "false" 149 | toHtmlFromJSON Null = "null" 150 | toHtmlFromJSON (Object o) = fromMisoString $ ms (show o) 151 | toHtmlFromJSON (Array a) = fromMisoString $ ms (show a) 152 | ---------------------------------------------------------------------------- 153 | -------------------------------------------------------------------------------- /src/Miso/Run.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE CPP #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | ----------------------------------------------------------------------------- 5 | -- | 6 | -- Module : Miso.Run 7 | -- Copyright : (C) 2016-2025 David M. Johnson 8 | -- License : BSD3-style (see the file LICENSE) 9 | -- Maintainer : David M. Johnson 10 | -- Stability : experimental 11 | -- Portability : non-portable 12 | ---------------------------------------------------------------------------- 13 | module Miso.Run 14 | ( -- ** Live reload 15 | run 16 | ) where 17 | ----------------------------------------------------------------------------- 18 | #ifdef WASM 19 | import qualified Language.Javascript.JSaddle.Wasm as J 20 | #elif !GHCJS_BOTH 21 | import Data.Maybe 22 | import System.Environment 23 | import Text.Read 24 | import qualified Language.Javascript.JSaddle.Warp as J 25 | #endif 26 | import Language.Javascript.JSaddle 27 | ----------------------------------------------------------------------------- 28 | -- | Entry point for a miso application 29 | -- When compiling with jsaddle on native platforms 30 | -- 'run' will start a web server for live reload 31 | -- of your miso application. 32 | -- 33 | -- When compiling to WASM use 'jsaddle-wasm'. 34 | -- When compiling to JS no special package is required (simply the 'id' function). 35 | -- JSM becomes a type synonym for IO 36 | run :: JSM () -> IO () 37 | #ifdef WASM 38 | run = J.run 39 | #elif GHCJS_BOTH 40 | run = id 41 | #else 42 | run action = do 43 | port <- fromMaybe 8008 . (readMaybe =<<) <$> lookupEnv "PORT" 44 | isGhci <- (== "") <$> getProgName 45 | putStrLn $ "Running on port " <> show port <> "..." 46 | (if isGhci then J.debug else J.run) port action 47 | #endif 48 | ----------------------------------------------------------------------------- 49 | -------------------------------------------------------------------------------- /src/Miso/Subscription.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Miso.Subscription 4 | -- Copyright : (C) 2016-2025 David M. Johnson 5 | -- License : BSD3-style (see the file LICENSE) 6 | -- Maintainer : David M. Johnson 7 | -- Stability : experimental 8 | -- Portability : non-portable 9 | ---------------------------------------------------------------------------- 10 | module Miso.Subscription 11 | ( -- ** Mouse 12 | module Miso.Subscription.Mouse 13 | -- ** Keyboard 14 | , module Miso.Subscription.Keyboard 15 | -- ** History 16 | , module Miso.Subscription.History 17 | -- ** Window 18 | , module Miso.Subscription.Window 19 | -- ** Websocket 20 | , module Miso.Subscription.WebSocket 21 | -- ** SSE 22 | , module Miso.Subscription.SSE 23 | ) where 24 | ----------------------------------------------------------------------------- 25 | import Miso.Subscription.Mouse 26 | import Miso.Subscription.Keyboard 27 | import Miso.Subscription.History 28 | import Miso.Subscription.Window 29 | import Miso.Subscription.WebSocket 30 | import Miso.Subscription.SSE 31 | ----------------------------------------------------------------------------- 32 | -------------------------------------------------------------------------------- /src/Miso/Subscription/History.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE RecordWildCards #-} 3 | ----------------------------------------------------------------------------- 4 | -- | 5 | -- Module : Miso.Subscription.History 6 | -- Copyright : (C) 2016-2025 David M. Johnson 7 | -- License : BSD3-style (see the file LICENSE) 8 | -- Maintainer : David M. Johnson 9 | -- Stability : experimental 10 | -- Portability : non-portable 11 | ---------------------------------------------------------------------------- 12 | module Miso.Subscription.History 13 | ( -- *** Subscription 14 | uriSub 15 | -- *** Functions 16 | , getURI 17 | , pushURI 18 | , replaceURI 19 | , back 20 | , forward 21 | , go 22 | -- *** Types 23 | , URI (..) 24 | ) where 25 | ----------------------------------------------------------------------------- 26 | import Control.Monad 27 | import Control.Monad.IO.Class 28 | import Language.Javascript.JSaddle 29 | import Network.URI hiding (path) 30 | import System.IO.Unsafe 31 | ----------------------------------------------------------------------------- 32 | import Miso.Concurrent 33 | import qualified Miso.FFI.Internal as FFI 34 | import Miso.String 35 | import Miso.Effect (Sub) 36 | ----------------------------------------------------------------------------- 37 | -- | Retrieves current URI of page 38 | getURI :: JSM URI 39 | {-# INLINE getURI #-} 40 | getURI = do 41 | href <- fromMisoString <$> getWindowLocationHref 42 | case parseURI href of 43 | Nothing -> fail $ "Could not parse URI from window.location: " ++ href 44 | Just uri -> 45 | pure (dropPrefix uri) 46 | where 47 | dropPrefix u@URI{..} 48 | | '/' : xs <- uriPath = u { uriPath = xs } 49 | | otherwise = u 50 | ----------------------------------------------------------------------------- 51 | -- | Pushes a new URI onto the History stack 52 | pushURI :: URI -> JSM () 53 | {-# INLINE pushURI #-} 54 | pushURI uri = pushStateNoModel uri { uriPath = toPath uri } 55 | ----------------------------------------------------------------------------- 56 | -- | Prepend '/' if necessary 57 | toPath :: URI -> String 58 | toPath uri = 59 | case uriPath uri of 60 | "" -> "/" 61 | "/" -> "/" 62 | xs@('/' : _) -> xs 63 | xs -> '/' : xs 64 | ----------------------------------------------------------------------------- 65 | -- | Replaces current URI on stack 66 | replaceURI :: URI -> JSM () 67 | {-# INLINE replaceURI #-} 68 | replaceURI uri = replaceTo' uri { uriPath = toPath uri } 69 | ----------------------------------------------------------------------------- 70 | -- | Navigates backwards 71 | back :: JSM () 72 | {-# INLINE back #-} 73 | back = void $ getHistory # "back" $ () 74 | ----------------------------------------------------------------------------- 75 | -- | Navigates forwards 76 | forward :: JSM () 77 | {-# INLINE forward #-} 78 | forward = void $ getHistory # "forward" $ () 79 | ----------------------------------------------------------------------------- 80 | -- | Jumps to a specific position in history 81 | go :: Int -> JSM () 82 | {-# INLINE go #-} 83 | go n = void $ getHistory # "go" $ [n] 84 | ----------------------------------------------------------------------------- 85 | chan :: Waiter 86 | {-# NOINLINE chan #-} 87 | chan = unsafePerformIO waiter 88 | ----------------------------------------------------------------------------- 89 | -- | Subscription for @popstate@ events, from the History API 90 | uriSub :: (URI -> action) -> Sub action 91 | uriSub = \f sink -> do 92 | void . FFI.forkJSM . forever $ do 93 | liftIO (wait chan) 94 | sink . f =<< getURI 95 | FFI.windowAddEventListener (ms "popstate") $ \_ -> 96 | sink . f =<< getURI 97 | ----------------------------------------------------------------------------- 98 | pushStateNoModel :: URI -> JSM () 99 | {-# INLINE pushStateNoModel #-} 100 | pushStateNoModel u = do 101 | pushState . pack . show $ u 102 | liftIO (serve chan) 103 | ----------------------------------------------------------------------------- 104 | replaceTo' :: URI -> JSM () 105 | {-# INLINE replaceTo' #-} 106 | replaceTo' u = do 107 | replaceState . pack . show $ u 108 | liftIO (serve chan) 109 | ----------------------------------------------------------------------------- 110 | getWindowLocationHref :: JSM MisoString 111 | getWindowLocationHref = do 112 | href <- fromJSVal =<< jsg "window" ! "location" ! "href" 113 | case join href of 114 | Nothing -> pure mempty 115 | Just uri -> pure uri 116 | ----------------------------------------------------------------------------- 117 | getHistory :: JSM JSVal 118 | getHistory = jsg "window" ! "history" 119 | ----------------------------------------------------------------------------- 120 | pushState :: MisoString -> JSM () 121 | pushState url = do 122 | _ <- getHistory # "pushState" $ (jsNull, jsNull, url) 123 | pure () 124 | ----------------------------------------------------------------------------- 125 | replaceState :: MisoString -> JSM () 126 | replaceState url = do 127 | _ <- getHistory # "replaceState" $ (jsNull, jsNull, url) 128 | pure () 129 | ----------------------------------------------------------------------------- 130 | -------------------------------------------------------------------------------- /src/Miso/Subscription/Keyboard.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE BangPatterns #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | ----------------------------------------------------------------------------- 5 | -- | 6 | -- Module : Miso.Subscription.Keyboard 7 | -- Copyright : (C) 2016-2025 David M. Johnson 8 | -- License : BSD3-style (see the file LICENSE) 9 | -- Maintainer : David M. Johnson 10 | -- Stability : experimental 11 | -- Portability : non-portable 12 | ---------------------------------------------------------------------------- 13 | module Miso.Subscription.Keyboard 14 | ( -- *** Types 15 | Arrows (..) 16 | -- *** Subscriptions 17 | , arrowsSub 18 | , directionSub 19 | , keyboardSub 20 | , wasdSub 21 | ) where 22 | ----------------------------------------------------------------------------- 23 | import Control.Monad.IO.Class 24 | import Data.IORef 25 | import Data.Set 26 | import qualified Data.Set as S 27 | import Language.Javascript.JSaddle hiding (new) 28 | ----------------------------------------------------------------------------- 29 | import Miso.Effect (Sub) 30 | import qualified Miso.FFI.Internal as FFI 31 | ----------------------------------------------------------------------------- 32 | -- | type for arrow keys currently pressed 33 | -- 34 | -- * 37 left arrow ( x = -1 ) 35 | -- * 38 up arrow ( y = 1 ) 36 | -- * 39 right arrow ( x = 1 ) 37 | -- * 40 down arrow ( y = -1 ) 38 | data Arrows 39 | = Arrows 40 | { arrowX :: !Int 41 | , arrowY :: !Int 42 | } deriving (Show, Eq) 43 | ----------------------------------------------------------------------------- 44 | -- | Helper function to convert keys currently pressed to @Arrows@, given a 45 | -- mapping for keys representing up, down, left and right respectively. 46 | toArrows :: ([Int], [Int], [Int], [Int]) -> Set Int -> Arrows 47 | toArrows (up, down, left, right) set' = Arrows 48 | { arrowX = 49 | case (check left, check right) of 50 | (True, False) -> -1 51 | (False, True) -> 1 52 | (_,_) -> 0 53 | , arrowY = 54 | case (check down, check up) of 55 | (True, False) -> -1 56 | (False, True) -> 1 57 | (_,_) -> 0 58 | } where 59 | check = any (`S.member` set') 60 | ----------------------------------------------------------------------------- 61 | -- | Maps @Arrows@ onto a Keyboard subscription 62 | arrowsSub :: (Arrows -> action) -> Sub action 63 | arrowsSub = directionSub ([38], [40], [37], [39]) 64 | ----------------------------------------------------------------------------- 65 | -- | Maps @Arrows@ onto a Keyboard subscription for directions (W+A+S+D keys) 66 | wasdSub :: (Arrows -> action) -> Sub action 67 | wasdSub = directionSub ([87], [83], [65], [68]) 68 | ----------------------------------------------------------------------------- 69 | -- | Maps a specified list of keys to directions (up, down, left, right) 70 | directionSub 71 | :: ([Int], [Int], [Int], [Int]) 72 | -> (Arrows -> action) 73 | -> Sub action 74 | directionSub dirs = keyboardSub . (. toArrows dirs) 75 | ----------------------------------------------------------------------------- 76 | -- | Returns subscription for Keyboard. 77 | -- The callback will be called with the Set of currently pressed @keyCode@s. 78 | keyboardSub :: (Set Int -> action) -> Sub action 79 | keyboardSub f sink = do 80 | keySetRef <- liftIO (newIORef mempty) 81 | FFI.windowAddEventListener "keyup" $ keyUpCallback keySetRef 82 | FFI.windowAddEventListener "keydown" $ keyDownCallback keySetRef 83 | FFI.windowAddEventListener "blur" $ blurCallback keySetRef 84 | where 85 | keyDownCallback keySetRef = \keyDownEvent -> do 86 | Just key <- fromJSVal =<< getProp "keyCode" (Object keyDownEvent) 87 | newKeys <- liftIO $ atomicModifyIORef' keySetRef $ \keys -> 88 | let !new = S.insert key keys 89 | in (new, new) 90 | sink (f newKeys) 91 | 92 | keyUpCallback keySetRef = \keyUpEvent -> do 93 | Just key <- fromJSVal =<< getProp "keyCode" (Object keyUpEvent) 94 | newKeys <- liftIO $ atomicModifyIORef' keySetRef $ \keys -> 95 | let !new = S.delete key keys 96 | in (new, new) 97 | sink (f newKeys) 98 | 99 | -- Assume keys are released the moment focus is lost. Otherwise going 100 | -- back and forth to the app can cause keys to get stuck. 101 | blurCallback keySetRef = \_ -> do 102 | newKeys <- liftIO $ atomicModifyIORef' keySetRef $ \_ -> 103 | let !new = S.empty 104 | in (new, new) 105 | sink (f newKeys) 106 | ----------------------------------------------------------------------------- 107 | -------------------------------------------------------------------------------- /src/Miso/Subscription/Mouse.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE OverloadedStrings #-} 3 | ----------------------------------------------------------------------------- 4 | -- | 5 | -- Module : Miso.Subscription.Mouse 6 | -- Copyright : (C) 2016-2025 David M. Johnson 7 | -- License : BSD3-style (see the file LICENSE) 8 | -- Maintainer : David M. Johnson 9 | -- Stability : experimental 10 | -- Portability : non-portable 11 | ---------------------------------------------------------------------------- 12 | module Miso.Subscription.Mouse 13 | ( -- *** Subscription 14 | mouseSub 15 | ) where 16 | ----------------------------------------------------------------------------- 17 | import Miso.Event (pointerDecoder, PointerEvent) 18 | import Miso.Subscription.Window (windowSub) 19 | import Miso.Effect (Sub) 20 | ----------------------------------------------------------------------------- 21 | -- | Captures mouse coordinates as they occur and writes them to 22 | -- an event sink 23 | mouseSub :: (PointerEvent -> action) -> Sub action 24 | mouseSub = windowSub "pointermove" pointerDecoder 25 | ----------------------------------------------------------------------------- 26 | -------------------------------------------------------------------------------- /src/Miso/Subscription/SSE.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Miso.Subscription.SSE 4 | -- Copyright : (C) 2016-2025 David M. Johnson 5 | -- License : BSD3-style (see the file LICENSE) 6 | -- Maintainer : David M. Johnson 7 | -- Stability : experimental 8 | -- Portability : non-portable 9 | ---------------------------------------------------------------------------- 10 | module Miso.Subscription.SSE 11 | ( -- *** Subscription 12 | sseSub 13 | -- *** Types 14 | , SSE (..) 15 | ) where 16 | ----------------------------------------------------------------------------- 17 | import Data.Aeson 18 | import qualified Language.Javascript.JSaddle as JSaddle 19 | import Language.Javascript.JSaddle hiding (new) 20 | ----------------------------------------------------------------------------- 21 | import Miso.Effect (Sub) 22 | import qualified Miso.FFI.Internal as FFI 23 | import Miso.String 24 | ----------------------------------------------------------------------------- 25 | -- | Server-sent events Subscription 26 | sseSub 27 | :: FromJSON msg 28 | => MisoString -- ^ EventSource URL 29 | -> (SSE msg -> action) 30 | -> Sub action 31 | sseSub url f sink = do 32 | es <- JSaddle.new (jsg (ms "EventSource")) [url] 33 | FFI.addEventListener es (ms "message") $ \v -> do 34 | dat <- FFI.jsonParse =<< v ! (ms "data") 35 | sink (f (SSEMessage dat)) 36 | FFI.addEventListener es (ms "error") $ \_ -> 37 | sink (f SSEError) 38 | FFI.addEventListener es (ms "close") $ \_ -> 39 | sink (f SSEClose) 40 | ----------------------------------------------------------------------------- 41 | -- | Server-sent events data 42 | data SSE message 43 | = SSEMessage message 44 | | SSEClose 45 | | SSEError 46 | deriving (Show, Eq) 47 | ----------------------------------------------------------------------------- 48 | -------------------------------------------------------------------------------- /src/Miso/Subscription/Window.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE RecordWildCards #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | ----------------------------------------------------------------------------- 5 | -- | 6 | -- Module : Miso.Subscription.Window 7 | -- Copyright : (C) 2016-2025 David M. Johnson 8 | -- License : BSD3-style (see the file LICENSE) 9 | -- Maintainer : David M. Johnson 10 | -- Stability : experimental 11 | -- Portability : non-portable 12 | ---------------------------------------------------------------------------- 13 | module Miso.Subscription.Window 14 | ( -- *** Subscription 15 | windowSub 16 | , windowCoordsSub 17 | , windowPointerMoveSub 18 | , windowSubWithOptions 19 | ) where 20 | ----------------------------------------------------------------------------- 21 | import Control.Monad 22 | import Language.Javascript.JSaddle 23 | import Data.Aeson.Types (parseEither) 24 | ----------------------------------------------------------------------------- 25 | import Miso.Event 26 | import Miso.Effect 27 | import qualified Miso.FFI.Internal as FFI 28 | import Miso.String 29 | ----------------------------------------------------------------------------- 30 | -- | Captures window coordinates changes as they occur and writes them to 31 | -- an event sink 32 | windowCoordsSub :: ((Int, Int) -> action) -> Sub action 33 | windowCoordsSub f = \write -> do 34 | write . f =<< (,) <$> FFI.windowInnerHeight <*> FFI.windowInnerWidth 35 | FFI.windowAddEventListener "resize" $ 36 | \windowEvent -> do 37 | target <- getProp "target" (Object windowEvent) 38 | Just w <- fromJSVal =<< getProp "innerWidth" (Object target) 39 | Just h <- fromJSVal =<< getProp "innerHeight" (Object target) 40 | write $ f (h, w) 41 | ----------------------------------------------------------------------------- 42 | -- | @windowSub eventName decoder toAction@ provides a subscription 43 | -- to listen to window level events. 44 | windowSub :: MisoString -> Decoder r -> (r -> action) -> Sub action 45 | windowSub = windowSubWithOptions defaultOptions 46 | ----------------------------------------------------------------------------- 47 | -- | @windowSubWithOptions options eventName decoder toAction@ provides a 48 | -- subscription to listen to window level events. 49 | windowSubWithOptions :: Options -> MisoString -> Decoder r -> (r -> action) -> Sub action 50 | windowSubWithOptions Options{..} eventName Decoder{..} toAction = \write -> 51 | FFI.windowAddEventListener eventName $ \e -> do 52 | decodeAtVal <- toJSVal decodeAt 53 | Just v <- fromJSVal =<< FFI.eventJSON decodeAtVal e 54 | case parseEither decoder v of 55 | Left s -> error $ "Parse error on " <> unpack eventName <> ": " <> s 56 | Right r -> do 57 | when stopPropagation (FFI.eventStopPropagation e) 58 | when preventDefault (FFI.eventPreventDefault e) 59 | write (toAction r) 60 | ----------------------------------------------------------------------------- 61 | -- | @window.addEventListener ("pointermove", (event) => handle(event))@ 62 | -- A 'Sub' to handle @PointerEvent@s on window 63 | windowPointerMoveSub :: (PointerEvent -> action) -> Sub action 64 | windowPointerMoveSub = windowSub "pointermove" pointerDecoder 65 | -------------------------------------------------------------------------------- /src/Miso/Svg.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Miso.Svg 4 | -- Copyright : (C) 2016-2025 David M. Johnson 5 | -- License : BSD3-style (see the file LICENSE) 6 | -- Maintainer : David M. Johnson 7 | -- Stability : experimental 8 | -- Portability : non-portable 9 | -- 10 | -- Example usage: 11 | -- 12 | -- @ 13 | -- import Miso 14 | -- import Miso.Svg 15 | -- 16 | -- intView :: Int -> View IntAction 17 | -- intView n = svg_ [ height_ "100", width "100" ] [ 18 | -- circle_ [ cx_ "50", cy_ "50", r_ "40", stroke_ "green", strokeWidth_ "4", fill_ "yellow" ] [] 19 | -- ] 20 | -- @ 21 | -- 22 | -- More information on how to use miso is available on GitHub 23 | -- 24 | -- 25 | -- 26 | ---------------------------------------------------------------------------- 27 | module Miso.Svg 28 | ( -- ** Element 29 | module Miso.Svg.Element 30 | -- ** Property 31 | , module Miso.Svg.Property 32 | -- ** Event 33 | , module Miso.Svg.Event 34 | ) where 35 | ----------------------------------------------------------------------------- 36 | import Miso.Svg.Property hiding (filter_, path_, mask_, clipPath_, cursor_) 37 | import Miso.Svg.Element 38 | import Miso.Svg.Event 39 | ----------------------------------------------------------------------------- 40 | -------------------------------------------------------------------------------- /src/Miso/Svg/Event.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE TypeFamilies #-} 4 | {-# LANGUAGE DataKinds #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE MultiParamTypeClasses #-} 7 | ----------------------------------------------------------------------------- 8 | -- | 9 | -- Module : Miso.Svg.Events 10 | -- Copyright : (C) 2016-2025 David M. Johnson 11 | -- License : BSD3-style (see the file LICENSE) 12 | -- Maintainer : David M. Johnson 13 | -- Stability : experimental 14 | -- Portability : non-portable 15 | ---------------------------------------------------------------------------- 16 | module Miso.Svg.Event 17 | ( -- *** Animation 18 | onBegin 19 | , onEnd 20 | , onRepeat 21 | -- *** Document 22 | , onAbort 23 | , onError 24 | , onResize 25 | , onScroll 26 | , onLoad 27 | , onUnload 28 | , onZoom 29 | -- *** Graphical 30 | , onActivate 31 | , onClick 32 | , onFocusIn 33 | , onFocusOut 34 | , onMouseDown 35 | , onMouseMove 36 | , onMouseOut 37 | , onMouseOver 38 | , onMouseUp 39 | ) where 40 | ----------------------------------------------------------------------------- 41 | import Miso.Event (on, emptyDecoder) 42 | import Miso.Html.Event (onClick) 43 | import Miso.Html.Types (Attribute) 44 | ----------------------------------------------------------------------------- 45 | -- | onBegin event 46 | onBegin :: action -> Attribute action 47 | onBegin action = on "begin" emptyDecoder $ \() _ -> action 48 | ----------------------------------------------------------------------------- 49 | -- | onEnd event 50 | onEnd :: action -> Attribute action 51 | onEnd action = on "end" emptyDecoder $ \() _ -> action 52 | ----------------------------------------------------------------------------- 53 | -- | onRepeat event 54 | onRepeat :: action -> Attribute action 55 | onRepeat action = on "repeat" emptyDecoder $ \() _ -> action 56 | ----------------------------------------------------------------------------- 57 | -- | onAbort event 58 | onAbort :: action -> Attribute action 59 | onAbort action = on "abort" emptyDecoder $ \() _ -> action 60 | ----------------------------------------------------------------------------- 61 | -- | onError event 62 | onError :: action -> Attribute action 63 | onError action = on "error" emptyDecoder $ \() _ -> action 64 | ----------------------------------------------------------------------------- 65 | -- | onResize event 66 | onResize :: action -> Attribute action 67 | onResize action = on "resize" emptyDecoder $ \() _ -> action 68 | ----------------------------------------------------------------------------- 69 | -- | onScroll event 70 | onScroll :: action -> Attribute action 71 | onScroll action = on "scroll" emptyDecoder $ \() _ -> action 72 | ----------------------------------------------------------------------------- 73 | -- | onLoad event 74 | onLoad :: action -> Attribute action 75 | onLoad action = on "load" emptyDecoder $ \() _ -> action 76 | ----------------------------------------------------------------------------- 77 | -- | onUnload event 78 | onUnload :: action -> Attribute action 79 | onUnload action = on "unload" emptyDecoder $ \() _ -> action 80 | ----------------------------------------------------------------------------- 81 | -- | onZoom event 82 | onZoom :: action -> Attribute action 83 | onZoom action = on "zoom" emptyDecoder $ \() _ -> action 84 | ----------------------------------------------------------------------------- 85 | -- | onActivate event 86 | onActivate :: action -> Attribute action 87 | onActivate action = on "activate" emptyDecoder $ \() _ -> action 88 | ----------------------------------------------------------------------------- 89 | -- | onFocusIn event 90 | onFocusIn :: action -> Attribute action 91 | onFocusIn action = on "focusin" emptyDecoder $ \() _ -> action 92 | ----------------------------------------------------------------------------- 93 | -- | onFocusOut event 94 | onFocusOut :: action -> Attribute action 95 | onFocusOut action = on "focusout" emptyDecoder $ \() _ -> action 96 | ----------------------------------------------------------------------------- 97 | -- | onMouseDown event 98 | onMouseDown :: action -> Attribute action 99 | onMouseDown action = on "mousedown" emptyDecoder $ \() _ -> action 100 | ----------------------------------------------------------------------------- 101 | -- | onMouseMove event 102 | onMouseMove :: action -> Attribute action 103 | onMouseMove action = on "mousemove" emptyDecoder $ \() _ -> action 104 | ----------------------------------------------------------------------------- 105 | -- | onMouseOut event 106 | onMouseOut :: action -> Attribute action 107 | onMouseOut action = on "mouseout" emptyDecoder $ \() _ -> action 108 | ----------------------------------------------------------------------------- 109 | -- | onMouseOver event 110 | onMouseOver :: action -> Attribute action 111 | onMouseOver action = on "mouseover" emptyDecoder $ \() _ -> action 112 | ----------------------------------------------------------------------------- 113 | -- | onMouseUp event 114 | onMouseUp :: action -> Attribute action 115 | onMouseUp action = on "mouseup" emptyDecoder $ \() _ -> action 116 | ----------------------------------------------------------------------------- 117 | -------------------------------------------------------------------------------- /src/Miso/Util.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Miso.Util 4 | -- Copyright : (C) 2016-2025 David M. Johnson 5 | -- License : BSD3-style (see the file LICENSE) 6 | -- Maintainer : David M. Johnson 7 | -- Stability : experimental 8 | -- Portability : non-portable 9 | ---------------------------------------------------------------------------- 10 | module Miso.Util 11 | ( withFoldable 12 | , conditionalViews 13 | ) where 14 | ----------------------------------------------------------------------------- 15 | import Data.Foldable 16 | ----------------------------------------------------------------------------- 17 | import Miso.Html (View) 18 | ----------------------------------------------------------------------------- 19 | -- | Generic @map@ function, useful for creating @View@s from the elements of 20 | -- some @Foldable@. Particularly handy for @Maybe@, as shown in the example 21 | -- below. 22 | -- 23 | -- @ 24 | -- view model = 25 | -- div_ [] $ 26 | -- withFoldable (model ^. mSomeMaybeVal) $ \\someVal -> 27 | -- p_ [] [ text $ "Hey, look at this value: " <> ms (show someVal) ] 28 | -- @ 29 | withFoldable :: Foldable t => t a -> (a -> b) -> [b] 30 | withFoldable ta f = map f (toList ta) 31 | ----------------------------------------------------------------------------- 32 | -- | Hides the @View@s if the condition is False. Shows them when the condition 33 | -- is True. 34 | conditionalViews :: Bool -> [View action] -> [View action] 35 | conditionalViews condition views = 36 | if condition 37 | then views 38 | else [] 39 | ----------------------------------------------------------------------------- 40 | 41 | -------------------------------------------------------------------------------- /text-src/Miso/String.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | {-# LANGUAGE CPP #-} 3 | {-# LANGUAGE FlexibleInstances #-} 4 | ----------------------------------------------------------------------------- 5 | -- | 6 | -- Module : Miso.String 7 | -- Copyright : (C) 2016-2025 David M. Johnson 8 | -- License : BSD3-style (see the file LICENSE) 9 | -- Maintainer : David M. Johnson 10 | -- Stability : experimental 11 | -- Portability : non-portable 12 | ---------------------------------------------------------------------------- 13 | module Miso.String 14 | ( -- ** Classes 15 | ToMisoString (..) 16 | , FromMisoString (..) 17 | -- ** Types 18 | , MisoString 19 | -- ** Functions 20 | , ms 21 | , fromMisoString 22 | -- ** Re-exports 23 | , module S 24 | ) where 25 | ---------------------------------------------------------------------------- 26 | import Control.Exception (SomeException) 27 | import qualified Data.ByteString as B 28 | import qualified Data.ByteString.Builder as B 29 | import qualified Data.ByteString.Lazy as BL 30 | import Data.JSString 31 | import Data.JSString.Text 32 | import Data.Monoid as S 33 | import Data.Text as S hiding ( 34 | #if MIN_VERSION_text(2,1,2) 35 | show, 36 | #endif 37 | #if MIN_VERSION_text(1,2,5) 38 | elem, 39 | #endif 40 | ) 41 | import qualified Data.Text as T 42 | import qualified Data.Text.Encoding as T 43 | import qualified Data.Text.Lazy as LT 44 | import qualified Data.Text.Lazy.Encoding as LT 45 | import Text.Read(readEither) 46 | import Prelude as P 47 | ---------------------------------------------------------------------------- 48 | -- | String type swappable based on compiler 49 | type MisoString = Text 50 | ---------------------------------------------------------------------------- 51 | -- | Convenience class for creating `MisoString` from other string-like types 52 | class ToMisoString str where 53 | toMisoString :: str -> MisoString 54 | ---------------------------------------------------------------------------- 55 | -- | Class from safely parsing 'MisoString' 56 | class FromMisoString t where 57 | fromMisoStringEither :: MisoString -> Either String t 58 | ---------------------------------------------------------------------------- 59 | -- | Parses a `MisoString`, throws an error when decoding 60 | -- fails. Use `fromMisoStringEither` for as a safe alternative. 61 | fromMisoString :: FromMisoString a => MisoString -> a 62 | fromMisoString s = 63 | case fromMisoStringEither s of 64 | Left err -> error err 65 | Right x -> x 66 | ---------------------------------------------------------------------------- 67 | -- | Convenience function, shorthand for `toMisoString` 68 | ms :: ToMisoString str => str -> MisoString 69 | ms = toMisoString 70 | ---------------------------------------------------------------------------- 71 | instance ToMisoString MisoString where 72 | toMisoString = id 73 | ---------------------------------------------------------------------------- 74 | instance ToMisoString SomeException where 75 | toMisoString = toMisoString . show 76 | ---------------------------------------------------------------------------- 77 | instance ToMisoString String where 78 | toMisoString = T.pack 79 | ---------------------------------------------------------------------------- 80 | instance ToMisoString LT.Text where 81 | toMisoString = LT.toStrict 82 | ---------------------------------------------------------------------------- 83 | instance ToMisoString JSString where 84 | toMisoString = textFromJSString 85 | ---------------------------------------------------------------------------- 86 | instance ToMisoString B.ByteString where 87 | toMisoString = toMisoString . T.decodeUtf8 88 | ---------------------------------------------------------------------------- 89 | instance ToMisoString BL.ByteString where 90 | toMisoString = toMisoString . LT.decodeUtf8 91 | ---------------------------------------------------------------------------- 92 | instance ToMisoString B.Builder where 93 | toMisoString = toMisoString . B.toLazyByteString 94 | ---------------------------------------------------------------------------- 95 | instance ToMisoString Float where 96 | toMisoString = T.pack . P.show 97 | ---------------------------------------------------------------------------- 98 | instance ToMisoString Double where 99 | toMisoString = T.pack . P.show 100 | ---------------------------------------------------------------------------- 101 | instance ToMisoString Int where 102 | toMisoString = T.pack . P.show 103 | ---------------------------------------------------------------------------- 104 | instance ToMisoString Word where 105 | toMisoString = T.pack . P.show 106 | ---------------------------------------------------------------------------- 107 | instance FromMisoString MisoString where 108 | fromMisoStringEither = Right 109 | ---------------------------------------------------------------------------- 110 | instance FromMisoString String where 111 | fromMisoStringEither = Right . T.unpack 112 | ---------------------------------------------------------------------------- 113 | instance FromMisoString LT.Text where 114 | fromMisoStringEither = Right . LT.fromStrict 115 | ---------------------------------------------------------------------------- 116 | instance FromMisoString JSString where 117 | fromMisoStringEither = Right . textToJSString 118 | ---------------------------------------------------------------------------- 119 | instance FromMisoString B.ByteString where 120 | fromMisoStringEither = fmap T.encodeUtf8 . fromMisoStringEither 121 | ---------------------------------------------------------------------------- 122 | instance FromMisoString BL.ByteString where 123 | fromMisoStringEither = fmap LT.encodeUtf8 . fromMisoStringEither 124 | ---------------------------------------------------------------------------- 125 | instance FromMisoString B.Builder where 126 | fromMisoStringEither = fmap B.byteString . fromMisoStringEither 127 | ---------------------------------------------------------------------------- 128 | instance FromMisoString Float where 129 | fromMisoStringEither = readEither . T.unpack 130 | ---------------------------------------------------------------------------- 131 | instance FromMisoString Double where 132 | fromMisoStringEither = readEither . T.unpack 133 | ---------------------------------------------------------------------------- 134 | instance FromMisoString Int where 135 | fromMisoStringEither = readEither . T.unpack 136 | ---------------------------------------------------------------------------- 137 | instance FromMisoString Word where 138 | fromMisoStringEither = readEither . T.unpack 139 | ---------------------------------------------------------------------------- 140 | -------------------------------------------------------------------------------- /ts/README.md: -------------------------------------------------------------------------------- 1 | # miso.js ![haskell-miso](https://img.shields.io/npm/v/haskell-miso) 2 | 3 | This package serves as a companion to the Haskell 4 | [miso](https://haskell-miso.org) project. It provides the JavaScript API for 5 | rendering all elements on the page, facilitating prerendering and handling event 6 | delegation. There are also a handful of utilities as well. 7 | -------------------------------------------------------------------------------- /ts/happydom.ts: -------------------------------------------------------------------------------- 1 | import { GlobalRegistrator } from '@happy-dom/global-registrator'; 2 | 3 | GlobalRegistrator.register(); 4 | -------------------------------------------------------------------------------- /ts/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | diff, 3 | hydrate, 4 | version, 5 | delegate, 6 | callBlur, 7 | callFocus, 8 | eventJSON, 9 | fetchJSON, 10 | undelegate, 11 | shouldSync, 12 | integrityCheck, 13 | setComponent, 14 | } from './miso'; 15 | 16 | /* export globally */ 17 | globalThis['miso'] = {}; 18 | globalThis['miso']['diff'] = diff; 19 | globalThis['miso']['hydrate'] = hydrate; 20 | globalThis['miso']['version'] = version; 21 | globalThis['miso']['delegate'] = delegate; 22 | globalThis['miso']['callBlur'] = callBlur; 23 | globalThis['miso']['callFocus'] = callFocus; 24 | globalThis['miso']['eventJSON'] = eventJSON; 25 | globalThis['miso']['fetchJSON'] = fetchJSON; 26 | globalThis['miso']['undelegate'] = undelegate; 27 | globalThis['miso']['shouldSync'] = shouldSync; 28 | globalThis['miso']['integrityCheck'] = integrityCheck; 29 | globalThis['miso']['setComponent'] = setComponent; 30 | -------------------------------------------------------------------------------- /ts/miso.spec.ts: -------------------------------------------------------------------------------- 1 | /* imports */ 2 | import { version } from './miso/util'; 3 | import { test, expect, describe, afterEach, beforeAll } from 'bun:test'; 4 | 5 | /* silence */ 6 | beforeAll(() => { 7 | console.log = () => {}; 8 | console.info = () => {}; 9 | console.warn = () => {}; 10 | console.error = () => {}; 11 | }); 12 | 13 | /* reset DOM */ 14 | afterEach(() => { 15 | document.body.innerHTML = ''; 16 | }); 17 | 18 | /* tests */ 19 | describe('Version test', () => { 20 | test('Should be latest version', () => { 21 | expect(version).toEqual('1.9.0.0'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ts/miso.ts: -------------------------------------------------------------------------------- 1 | import { diff } from './miso/dom'; 2 | import { delegate, undelegate, eventJSON } from './miso/event'; 3 | import { hydrate, integrityCheck } from './miso/hydrate'; 4 | import { shouldSync, version, callFocus, callBlur, setComponent, fetchJSON } from './miso/util'; 5 | import { VTree, VNode, VText, VComp, Props, CSS, Events, NS, DOMRef, EventCapture, EventObject, Options } from './miso/types'; 6 | import { vcomp, vnode, vtext } from './miso/smart'; 7 | 8 | /* top level re-export */ 9 | export { 10 | /* Functions */ 11 | diff, 12 | hydrate, 13 | version, 14 | delegate, 15 | callBlur, 16 | callFocus, 17 | eventJSON, 18 | fetchJSON, 19 | undelegate, 20 | integrityCheck, 21 | setComponent, 22 | shouldSync, 23 | /* Types */ 24 | VTree, 25 | VComp, 26 | VText, 27 | VNode, 28 | EventCapture, 29 | EventObject, 30 | Options, 31 | CSS, 32 | Props, 33 | Events, 34 | NS, 35 | DOMRef, 36 | /* Smart constructors */ 37 | vnode, 38 | vtext, 39 | vcomp, 40 | }; 41 | -------------------------------------------------------------------------------- /ts/miso/event.ts: -------------------------------------------------------------------------------- 1 | import { VTree, EventCapture, EventObject, Options } from './types'; 2 | 3 | /* event delegation algorithm */ 4 | export function delegate( 5 | mount: HTMLElement, 6 | events: Array, 7 | getVTree: (vtree: VTree) => void, 8 | debug: boolean, 9 | ): void { 10 | for (const event of events) { 11 | mount.addEventListener( 12 | event['name'], 13 | function (e: Event) { 14 | listener(e, mount, getVTree, debug); 15 | e.stopPropagation(); 16 | }, 17 | event['capture'], 18 | ); 19 | } 20 | } 21 | /* event undelegation */ 22 | export function undelegate( 23 | mount: HTMLElement, 24 | events: Array, 25 | getVTree: (vtree: VTree) => void, 26 | debug: boolean, 27 | ): void { 28 | for (const event of events) { 29 | mount.removeEventListener( 30 | event['name'], 31 | function (e: Event) { 32 | listener(e, mount, getVTree, debug); 33 | }, 34 | event['capture'], 35 | ); 36 | } 37 | } 38 | /* the event listener shared by both delegator and undelegator */ 39 | function listener(e: Event, mount: HTMLElement, getVTree: (VTree) => void, debug: boolean): void { 40 | getVTree(function (obj: VTree) { 41 | if (e.target) { 42 | delegateEvent(e, obj, buildTargetToElement(mount, e.target as ParentNode), [], debug); 43 | } 44 | }); 45 | } 46 | /* Create a stack of ancestors used to index into the virtual DOM */ 47 | function buildTargetToElement(element: HTMLElement, target: ParentNode): Array { 48 | var stack = []; 49 | while (element !== target) { 50 | stack.unshift(target); 51 | target = target.parentNode; 52 | } 53 | return stack; 54 | } 55 | /* Finds event in virtual dom via pointer equality 56 | Accumulate parent stack as well for propagation up the vtree 57 | */ 58 | function delegateEvent( 59 | event: Event, 60 | obj: VTree, 61 | stack: Array, 62 | parentStack: Array, 63 | debug: boolean, 64 | ): void { 65 | /* base case, not found */ 66 | if (!stack.length) { 67 | if (debug) { 68 | console.warn( 69 | 'Event "' + event.type + '" did not find an event handler to dispatch on', 70 | obj, 71 | event, 72 | ); 73 | } 74 | return; 75 | } else if (stack.length > 1) { /* stack not length 1, recurse */ 76 | parentStack.unshift(obj); 77 | for (const child of obj['children']) { 78 | if (child['type'] === 'vcomp') continue; 79 | if (child['domRef'] === stack[1]) { 80 | delegateEvent(event, child, stack.slice(1), parentStack, debug); 81 | break; 82 | } 83 | } 84 | } /* stack.length == 1 */ 85 | else { 86 | const eventObj: EventObject = obj['events'][event.type]; 87 | if (eventObj) { 88 | const options: Options = eventObj['options']; 89 | if (options['preventDefault']) { 90 | event.preventDefault(); 91 | } 92 | /* dmj: stack[0] represents the domRef that raised the event */ 93 | eventObj['runEvent'](event, stack[0]); 94 | if (!options['stopPropagation']) { 95 | propagateWhileAble(parentStack, event); 96 | } 97 | } else { 98 | /* still propagate to parent handlers even if event not defined */ 99 | propagateWhileAble(parentStack, event); 100 | } 101 | } 102 | } 103 | /* Propagate the event up the chain, invoking other event handlers as encountered */ 104 | function propagateWhileAble(parentStack: Array, event: Event): void { 105 | for (const vtree of parentStack) { 106 | if (vtree['events'][event.type]) { 107 | const eventObj = vtree['events'][event.type], 108 | options = eventObj['options']; 109 | if (options['preventDefault']) event.preventDefault(); 110 | eventObj['runEvent'](event); 111 | if (options['stopPropagation']) { 112 | event.stopPropagation(); 113 | break; 114 | } 115 | } 116 | } 117 | } 118 | /* Walks down obj following the path described by `at`, then filters primitive 119 | values (string, numbers and booleans). Sort of like JSON.stringify(), but 120 | on an Event that is stripped of impure references. 121 | */ 122 | export function eventJSON(at: string | Array, obj: Event): Object[] { 123 | /* If at is of type [[MisoString]] */ 124 | if (typeof at[0] === 'object') { 125 | var ret = []; 126 | for (var i: number = 0; i < at.length; i++) { 127 | ret.push(eventJSON(at[i], obj)); 128 | } 129 | return ret; 130 | } 131 | for (const a of at) obj = obj[a]; 132 | /* If obj is a list-like object */ 133 | var newObj; 134 | if (obj instanceof Array || ('length' in obj && obj['localName'] !== 'select')) { 135 | newObj = []; 136 | for (var j = 0; j < obj.length; j++) { 137 | newObj.push(eventJSON([], obj[j])); 138 | } 139 | return newObj; 140 | } 141 | /* If obj is a non-list-like object */ 142 | newObj = {}; 143 | for (var key in getAllPropertyNames(obj)) { 144 | /* bug in safari, throws TypeError if the following fields are referenced on a checkbox */ 145 | /* https://stackoverflow.com/a/25569117/453261 */ 146 | /* https://html.spec.whatwg.org/multipage/input.html#do-not-apply */ 147 | if ( 148 | obj['localName'] === 'input' && 149 | (key === 'selectionDirection' || key === 'selectionStart' || key === 'selectionEnd') 150 | ) { 151 | continue; 152 | } 153 | if ( 154 | typeof obj[key] == 'string' || 155 | typeof obj[key] == 'number' || 156 | typeof obj[key] == 'boolean' 157 | ) { 158 | newObj[key] = obj[key]; 159 | } 160 | } 161 | return newObj; 162 | } 163 | /* get static and dynamic properties */ 164 | function getAllPropertyNames(obj: Event): Object { 165 | var props: Object = {}, 166 | i: number = 0; 167 | do { 168 | var names = Object.getOwnPropertyNames(obj); 169 | for (i = 0; i < names.length; i++) { 170 | props[names[i]] = null; 171 | } 172 | } while ((obj = Object.getPrototypeOf(obj))); 173 | return props; 174 | } 175 | -------------------------------------------------------------------------------- /ts/miso/smart.ts: -------------------------------------------------------------------------------- 1 | /* smart constructors for VTree */ 2 | import { VText, VTree, VNode, VComp } from './types'; 3 | import { shouldSync } from './util'; 4 | 5 | /* vtext factory */ 6 | export function vtext(input: string) : VText { 7 | return { 8 | ns : 'text', 9 | text: input, 10 | type: 'vtext', 11 | domRef : null, 12 | key : null, 13 | }; 14 | } 15 | 16 | export function vtextKeyed(input: string, key: string) : VText { 17 | return union(vtext(input), { key }); 18 | } 19 | 20 | /* vtree factory */ 21 | export function vnode(props: Partial): VNode { 22 | var node = union(mkVNode(), props); 23 | /* dmj: If the property is already set the check is bypassed. 24 | By setting 'shouldSync' manually in 'vnode' you are implicitly 25 | saying all keys exist and should be synched. 26 | */ 27 | if (!node['shouldSync']) node['shouldSync'] = shouldSync(node); 28 | return node; 29 | } 30 | 31 | export function vcomp(props: Partial): VComp { 32 | return union(mkVComp(), props); 33 | } 34 | 35 | /* set union */ 36 | function union(obj: T, updates: Partial): T { 37 | return Object.assign({}, obj, updates); 38 | } 39 | 40 | /* smart constructors */ 41 | export function vnodeKeyed(tag:string, key:string): VNode { 42 | return vnode({ 43 | tag: tag, 44 | children: [vtext(key)], 45 | key: key, 46 | }); 47 | } 48 | 49 | export function vnodeKids(tag:string, kids:Array): VNode { 50 | return vnode({ 51 | tag: tag, 52 | children: kids, 53 | }); 54 | } 55 | 56 | /* "smart" helper for constructing an empty virtual DOM */ 57 | function mkVNode() : VNode { 58 | return { 59 | props: {}, 60 | css: {}, 61 | children: [], 62 | ns: 'html', 63 | domRef: null, 64 | tag: 'div', 65 | key: null, 66 | events: {}, 67 | onDestroyed: () => {}, 68 | onBeforeDestroyed: () => {}, 69 | onCreated: () => {}, 70 | onBeforeCreated: () => {}, 71 | shouldSync: false, 72 | type : 'vnode', 73 | }; 74 | } 75 | 76 | function mkVComp() : VComp { 77 | return union(mkVNode() as any, { 78 | type : 'vcomp', 79 | 'data-component-id': '', 80 | mount: () => {}, 81 | unmount: () => {}, 82 | onUnmounted: () => {}, 83 | onBeforeUnmounted: () => {}, 84 | onMounted: () => {}, 85 | onBeforeMounted: () => {}, 86 | }); 87 | } 88 | 89 | -------------------------------------------------------------------------------- /ts/miso/types.ts: -------------------------------------------------------------------------------- 1 | /* core type for virtual DOM */ 2 | type Props = Record; 3 | type CSS = Record; 4 | type Events = Record; 5 | 6 | /* element name spacing */ 7 | type NS = 'text' | 'html' | 'svg' | 'mathml'; 8 | 9 | type DOMRef = HTMLElement | SVGElement | MathMLElement; 10 | 11 | type VComp = { 12 | type: 'vcomp'; 13 | domRef: HTMLElement; 14 | ns: 'html'; 15 | tag: 'div'; 16 | key: string; 17 | props: Props; 18 | css: CSS; 19 | events: Events; 20 | 'data-component-id': string; 21 | children: Array; 22 | onBeforeMounted: () => void; 23 | onMounted: (componentId: string) => void; 24 | onBeforeUnmounted: () => void; 25 | onUnmounted: (componentId: string) => void; 26 | mount: (f: (component: VTree) => void) => void; 27 | unmount: (e: Element) => void; 28 | }; 29 | 30 | type VNode = { 31 | type: 'vnode'; 32 | ns: NS; 33 | domRef: DOMRef; 34 | tag: string; 35 | key: string; 36 | props: Props; 37 | css: CSS; 38 | events: Events; 39 | shouldSync: boolean; 40 | children: Array; 41 | onDestroyed: () => void; 42 | onBeforeDestroyed: () => void; 43 | onCreated: () => void; 44 | onBeforeCreated: () => void; 45 | draw?: (DOMRef) => void; 46 | }; 47 | 48 | type VText = { 49 | type: 'vtext'; 50 | text: string; 51 | domRef: Text; 52 | ns: NS; 53 | key: string; 54 | }; 55 | 56 | type VTree = VComp | VNode | VText; 57 | 58 | type EventObject = { 59 | options: Options; 60 | runEvent: (e: Event) => void; 61 | }; 62 | 63 | type Options = { 64 | preventDefault: boolean; 65 | stopPropagation: boolean; 66 | }; 67 | 68 | type EventCapture = { 69 | name: string; 70 | capture: boolean; 71 | }; 72 | 73 | export { 74 | VTree, 75 | VComp, 76 | VNode, 77 | VText, 78 | EventCapture, 79 | EventObject, 80 | Options, 81 | Props, 82 | CSS, 83 | Events, 84 | NS, 85 | DOMRef, 86 | }; 87 | -------------------------------------------------------------------------------- /ts/miso/util.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from './types'; 2 | 3 | /* various utilities */ 4 | export function callFocus(id: string, delay: number): void { 5 | var setFocus = function () { 6 | var e = document.getElementById(id); 7 | if (e && e.focus) e.focus(); 8 | }; 9 | delay > 0 ? setTimeout(setFocus, delay) : setFocus(); 10 | } 11 | 12 | export function callBlur(id: string, delay: number): void { 13 | var setBlur = function () { 14 | var e = document.getElementById(id); 15 | if (e && e.blur) e.blur(); 16 | }; 17 | delay > 0 ? setTimeout(setBlur, delay) : setBlur(); 18 | } 19 | 20 | export function setComponent(node: Element, componentId: string): void { 21 | node.setAttribute('data-component-id', componentId); 22 | } 23 | 24 | export function fetchJSON ( 25 | url : string, 26 | method : string, 27 | body : any, 28 | headers : Record, 29 | successful: (string) => void, 30 | errorful: (string) => void 31 | ): void 32 | { 33 | var options = { method, headers }; 34 | if (body) { 35 | options['body'] = body; 36 | } 37 | fetch (url, options) 38 | .then(response => { 39 | if (!response.ok) { 40 | throw new Error(response.statusText); 41 | } 42 | return response.json(); 43 | }) 44 | .then(successful) /* success callback */ 45 | .catch(errorful); /* error callback */ 46 | } 47 | 48 | /* 49 | 'shouldSync' 50 | dmj: Used to determine if we should enter `syncChildren` 51 | 52 | */ 53 | export function shouldSync ( 54 | node: VNode 55 | ): boolean { 56 | /* cannot sync on null children */ 57 | if (node.children.length === 0) { 58 | return false; 59 | } 60 | 61 | /* only sync if keys exist on all children */ 62 | var enterSync = true; 63 | for (const child of node.children) { 64 | if (!child.key) { 65 | enterSync = false; 66 | break; 67 | } 68 | } 69 | return enterSync; 70 | } 71 | 72 | 73 | /* current miso version */ 74 | export const version: string = '1.9.0.0'; 75 | -------------------------------------------------------------------------------- /ts/spec/event.spec.ts: -------------------------------------------------------------------------------- 1 | import { diff } from '../miso/dom'; 2 | import { delegate, undelegate, eventJSON } from '../miso/event'; 3 | import { DOMRef, EventCapture } from '../miso/types'; 4 | import { vnode } from '../miso/smart'; 5 | import { test, expect, describe, afterEach, beforeAll } from 'bun:test'; 6 | 7 | /* silence */ 8 | beforeAll(() => { 9 | console.log = () => {}; 10 | console.info = () => {}; 11 | console.warn = () => {}; 12 | console.error = () => {}; 13 | }); 14 | 15 | /* reset DOM */ 16 | afterEach(() => { 17 | document.body.innerHTML = ''; 18 | }); 19 | 20 | describe ('Event tests', () => { 21 | 22 | test('Should delegate and undelegate button click', () => { 23 | var body = document.body; 24 | var count = 0; 25 | var result = null; 26 | var events = { 27 | click: { 28 | runEvent: (e : Event) => { 29 | result = eventJSON('', e); 30 | count++; 31 | }, 32 | options: { 33 | preventDefault: true, 34 | stopPropagation: false, 35 | }, 36 | }, 37 | }; 38 | var vtreeChild = vnode({ 39 | tag: 'button', 40 | events: events, 41 | }); 42 | 43 | var vtreeParent = vnode({ 44 | children: [vtreeChild], 45 | events: events, 46 | }); 47 | 48 | /* initial page draw */ 49 | diff(null, vtreeParent, document.body); 50 | 51 | /* ensure structures match */ 52 | expect(vtreeParent.domRef).toEqual(document.body.childNodes[0] as DOMRef); 53 | expect(vtreeChild.domRef).toEqual( 54 | document.body.childNodes[0].childNodes[0] as DOMRef 55 | ); 56 | 57 | /* setup event delegation */ 58 | var getVTree = (cb : any) => { 59 | cb(vtreeParent); 60 | }; 61 | const delegatedEvents : Array = [{ name: 'click', capture: true }]; 62 | delegate(body, delegatedEvents, getVTree, true); 63 | 64 | /* initiate click event */ 65 | (vtreeChild.domRef as HTMLElement).click(); 66 | 67 | /* check results */ 68 | expect(count).toEqual(2); 69 | expect(result).not.toEqual(null); 70 | 71 | /* unmount delegation */ 72 | undelegate(document.body, delegatedEvents, getVTree, true); 73 | }); 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /ts/spec/util.spec.ts: -------------------------------------------------------------------------------- 1 | import { callFocus, callBlur, setComponent } from '../miso/util'; 2 | import { test, expect, describe, afterEach, beforeAll } from 'bun:test'; 3 | 4 | /* silence */ 5 | beforeAll(() => { 6 | console.log = () => {}; 7 | console.info = () => {}; 8 | console.warn = () => {}; 9 | console.error = () => {}; 10 | }); 11 | 12 | /* reset DOM */ 13 | afterEach(() => { 14 | document.body.innerHTML = ''; 15 | }); 16 | 17 | /* tests */ 18 | describe ('Utils tests', () => { 19 | 20 | test('Should set body[data-component-id] via setComponent()', () => { 21 | setComponent(document.body, 'component-one'); 22 | expect(document.body.getAttribute('data-component-id')).toEqual( 23 | 'component-one', 24 | ); 25 | }); 26 | 27 | test('Should call callFocus() and callBlur()', () => { 28 | var child = document.createElement('input'); 29 | child['id'] = 'foo'; 30 | document.body.appendChild(child); 31 | callFocus('blah', 0); /* missing case */ 32 | callFocus('foo', 0); /* found case */ 33 | callFocus('foo', 1); /* found case */ 34 | expect(document.activeElement).toEqual(child); 35 | callBlur('blah', 0); /* missing case */ 36 | callBlur('foo', 0); /* found case */ 37 | callBlur('foo', 1); /* found case */ 38 | expect(document.activeElement).toEqual(document.body); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | "target": "es6", 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------