├── .github ├── FUNDING.yml └── workflows │ └── CI.yml ├── .gitignore ├── .tidyrc.json ├── LICENSE ├── README.md ├── benchmarks └── js-framework-benchmark │ ├── keyed │ ├── index.html │ ├── index.js │ ├── package.json │ ├── packages.dhall │ ├── spago.dhall │ ├── src │ │ ├── Main.js │ │ └── Main.purs │ └── webpack.flame.config.js │ └── non-keyed │ ├── index.html │ ├── index.js │ ├── package.json │ ├── packages.dhall │ ├── spago.dhall │ ├── src │ ├── Main.js │ └── Main.purs │ └── webpack.flame.config.js ├── bower.json ├── docs ├── CNAME ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _layouts │ └── default.html ├── assets │ ├── css │ │ └── site.css │ └── img │ │ ├── benchmark-keyed-1.png │ │ ├── benchmark-keyed-2.png │ │ ├── benchmark-non-keyed-1.png │ │ └── benchmark-non-keyed-2.png ├── benchmarks.md ├── concepts.md ├── events.md ├── favicon.ico ├── index.md ├── rendering.md └── views.md ├── examples.dhall ├── examples ├── Affjax │ ├── Affjax.purs │ ├── README.md │ ├── affjax.html │ └── affjax.js ├── Counter │ ├── Counter.purs │ ├── README.md │ ├── counter.html │ └── counter.js ├── Counters │ ├── Counters.purs │ ├── README.md │ ├── counters.html │ └── counters.js ├── Dice │ ├── Dice.purs │ ├── README.md │ ├── dice.html │ └── dice.js ├── EffectfulAffjax │ ├── Affjax.purs │ ├── README.md │ ├── affjax.html │ └── affjax.js ├── EffectfulDice │ ├── Dice.purs │ ├── README.md │ ├── dice.html │ └── dice.js ├── README.md ├── ServerSideRendering │ ├── Client │ │ ├── ServerSideRendering.purs │ │ └── server-side-rendering-client.js │ ├── README.md │ ├── Server │ │ ├── ServerSideRendering.purs │ │ └── server-side-rendering-server.js │ └── Shared │ │ └── ServerSideRendering.purs ├── SpecialElements │ ├── README.md │ ├── Special.purs │ ├── special.html │ └── special.js ├── Subscriptions │ ├── README.md │ ├── Subscriptions.purs │ ├── subscriptions.html │ └── subscriptions.js └── Todo │ ├── README.md │ ├── Todo.purs │ ├── todo.html │ └── todo.js ├── licenses ├── LOADASH-LICENSE ├── SNABBDOM-TO-HTML-LICENSE └── STAGE0-LICENSE ├── package-lock.json ├── package.json ├── packages.dhall ├── spago.dhall ├── src ├── Flame.purs └── Flame │ ├── Application │ ├── EffectList.purs │ ├── Effectful.js │ ├── Effectful.purs │ ├── Internal │ │ ├── Dom.js │ │ ├── Dom.purs │ │ ├── PreMount.js │ │ └── PreMount.purs │ └── NoEffects.purs │ ├── Html │ ├── Attribute.purs │ ├── Attribute │ │ ├── Event.js │ │ ├── Event.purs │ │ ├── Internal.js │ │ └── Internal.purs │ ├── Element.js │ └── Element.purs │ ├── Internal │ ├── Equality.js │ ├── Equality.purs │ ├── Fragment.js │ └── Fragment.purs │ ├── Renderer │ ├── Internal │ │ ├── Dom.js │ │ └── Dom.purs │ ├── String.js │ └── String.purs │ ├── Serialize.purs │ ├── Subscription.purs │ ├── Subscription │ ├── Document.purs │ ├── Internal │ │ ├── Listener.js │ │ ├── Listener.purs │ │ └── Source.purs │ ├── Unsafe │ │ └── CustomEvent.purs │ └── Window.purs │ ├── Types.js │ └── Types.purs └── test ├── Basic ├── EffectList.purs ├── Effectful.purs └── NoEffects.purs ├── Effectful └── SlowEffects.purs ├── Functor ├── Basic.purs └── Lazy.purs ├── Main.js ├── Main.purs ├── ServerSideRendering ├── Effectful.js ├── Effectful.purs ├── FragmentNode.js ├── FragmentNode.purs ├── ManagedNode.js └── ManagedNode.purs └── Subscription ├── Broadcast.purs ├── EffectList.purs ├── Effectful.purs └── NoEffects.purs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: easafe 3 | custom: ["https://asafe.dev/donate"] 4 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v2 12 | with: 13 | node-version: '15' 14 | cache: 'npm' 15 | 16 | - uses: purescript-contrib/setup-purescript@main 17 | 18 | - name: Cache PureScript dependencies 19 | uses: actions/cache@v2 20 | # This cache uses the .dhall files to know when it should reinstall 21 | # and rebuild packages. It caches both the installed packages from 22 | # the `.spago` directory and compilation artifacts from the `output` 23 | # directory. When restored the compiler will rebuild any files that 24 | # have changed. If you do not want to cache compiled output, remove 25 | # the `output` path. 26 | with: 27 | key: ${{ runner.os }}-spago-${{ hashFiles('**/*.dhall') }} 28 | path: | 29 | .spago 30 | output 31 | - run: npm install 32 | - run: spago build 33 | - run: spago test --no-install 34 | - run: spago -x examples.dhall build 35 | - run: npm run build-examples 36 | - name: keyed bench 37 | working-directory: ./benchmarks/js-framework-benchmark/keyed 38 | run: npm run build 39 | - name: non-keyed bench 40 | working-directory: ./benchmarks/js-framework-benchmark/non-keyed 41 | run: npm run build 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | .pulp-cache/ 4 | output/ 5 | generated-docs/ 6 | .psc-package/ 7 | .psc* 8 | /.purs* 9 | .psa* 10 | dist/ 11 | todo 12 | dce-output 13 | 14 | npm-debug.log 15 | examples/App/ 16 | .sass-cache/ 17 | _site/ 18 | 19 | .jekyll-metadata 20 | 21 | scratchpad.* 22 | scratchpadloader.* 23 | vendor 24 | .cache 25 | .vscode/ 26 | .bundle 27 | 28 | .spago 29 | 30 | .parcel-cache -------------------------------------------------------------------------------- /.tidyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "importWrap": "source", 3 | "indent": 6, 4 | "operatorsFile": null, 5 | "ribbon": 1, 6 | "typeArrowPlacement": "last", 7 | "unicode": "always", 8 | "width": null 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Eduardo Asafe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Flame ![build status](https://github.com/easafe/purescript-flame/actions/workflows/CI.yml/badge.svg) 2 | 3 | Flame is a fast & simple framework inspired by the Elm architecture for building web applications in PureScript 4 | 5 | ### Documentation 6 | 7 | See the [project page](https://flame.asafe.dev/) or [pursuit](https://pursuit.purescript.org/packages/purescript-flame) 8 | 9 | ### Examples 10 | 11 | See the [examples folder](/examples) 12 | 13 | ### Quick start 14 | 15 | Install: 16 | 17 | ```bash 18 | spago install flame 19 | ``` 20 | 21 | Example counter app: 22 | 23 | ```purescript 24 | module Counter.Main where 25 | 26 | import Prelude 27 | 28 | import Effect (Effect) 29 | import Flame (Html, QuerySelector(..), Subscription) 30 | -- Side effects free updating; see docs for other examples 31 | import Flame.Application.NoEffects as FAN 32 | import Flame.Html.Element as HE 33 | import Flame.Html.Attribute as HA 34 | 35 | -- | The model represents the state of the app 36 | type Model = Int 37 | 38 | -- | Data type used to represent events 39 | data Message = Increment | Decrement 40 | 41 | -- | Initial state of the app 42 | init :: Model 43 | init = 0 44 | 45 | -- | `update` is called to handle events 46 | update :: Model -> Message -> Model 47 | update model = case _ of 48 | Increment -> model + 1 49 | Decrement -> model - 1 50 | 51 | -- | `view` is called whenever the model is updated 52 | view :: Model -> Html Message 53 | view model = HE.main "main" [ 54 | HE.button [HA.onClick Decrement] "-", 55 | HE.text $ show model, 56 | HE.button [HA.onClick Increment] "+" 57 | ] 58 | 59 | -- | Events that come from outside the `view` 60 | subscribe :: Array (Subscription Message) 61 | subscribe = [] 62 | 63 | -- | Mount the application on the given selector 64 | main :: Effect Unit 65 | main = FAN.mount_ (QuerySelector "body") { 66 | init, 67 | view, 68 | update, 69 | subscribe 70 | } 71 | ``` 72 | 73 | ### Tools 74 | 75 | [breeze](https://github.com/easafe/haskell-breeze) can be used to generate Flame markup from HTML 76 | 77 | ### Licensing 78 | 79 | Licenses for loadash, stage0 and snabbdom-to-html added under [licenses](licenses/) since parts of the rendering code was adapted from these projects 80 | 81 | ### Funding 82 | 83 | If this project is useful for you, consider [throwing a buck](https://asafe.dev/donate) to keep development possible 84 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/keyed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flame v1.0.0 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/keyed/index.js: -------------------------------------------------------------------------------- 1 | require('./dce-output/Main').main(); -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/keyed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-framework-benchmark-keyed-flame", 3 | "sideEffects": false, 4 | "version": "1.0.0", 5 | "description": "Purescript Flame JS Benchmark", 6 | "main": "index.js", 7 | "js-framework-benchmark": { 8 | "frameworkVersion": "1.0.0" 9 | }, 10 | "scripts": { 11 | "postinstall": "spago install", 12 | "clean": "rm -rf dist output .spago node_modules", 13 | "build": "spago build", 14 | "build-prod": "spago build --purs-args '--codegen corefn,js' && zephyr -f Main.main && webpack --config=webpack.flame.config.js" 15 | }, 16 | "keywords": [ 17 | "purescript", 18 | "flame" 19 | ], 20 | "author": "Eduardo Asafe ", 21 | "license": "ISC", 22 | "homepage": "https://github.com/krausest/js-framework-benchmark", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/krausest/js-framework-benchmark.git" 26 | }, 27 | "devDependencies": { 28 | "purescript": "0.14.4", 29 | "spago": "0.20.3", 30 | "webpack": "^4.44.1", 31 | "webpack-cli": "^3.3.12" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/keyed/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.0-20220527/packages.dhall 3 | sha256:15dd8041480502850e4043ea2977ed22d6ab3fc24d565211acde6f8c5152a799 4 | 5 | let overrides = {=} 6 | 7 | let additions = {=} 8 | 9 | in upstream // overrides // additions 10 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/keyed/spago.dhall: -------------------------------------------------------------------------------- 1 | { name = "js-framework-benchmark-flame" 2 | , dependencies = [ 3 | "flame", 4 | "aff", 5 | "arrays", 6 | "effect", 7 | "maybe", 8 | "prelude" ] 9 | , packages = ./packages.dhall 10 | , sources = [ "src/**/*.purs" ] 11 | } 12 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/keyed/src/Main.js: -------------------------------------------------------------------------------- 1 | function _random(max) { 2 | return Math.round(Math.random() * 1000) % max; 3 | } 4 | 5 | var adjectives = [ 6 | "pretty", 7 | "large", 8 | "big", 9 | "small", 10 | "tall", 11 | "short", 12 | "long", 13 | "handsome", 14 | "plain", 15 | "quaint", 16 | "clean", 17 | "elegant", 18 | "easy", 19 | "angry", 20 | "crazy", 21 | "helpful", 22 | "mushy", 23 | "odd", 24 | "unsightly", 25 | "adorable", 26 | "important", 27 | "inexpensive", 28 | "cheap", 29 | "expensive", 30 | "fancy" 31 | ], 32 | colours = [ 33 | "red", 34 | "yellow", 35 | "blue", 36 | "green", 37 | "pink", 38 | "brown", 39 | "purple", 40 | "brown", 41 | "white", 42 | "black", 43 | "orange" 44 | ], 45 | nouns = [ 46 | "table", 47 | "chair", 48 | "house", 49 | "bbq", 50 | "desk", 51 | "car", 52 | "pony", 53 | "cookie", 54 | "sandwich", 55 | "burger", 56 | "pizza", 57 | "mouse", 58 | "keyboard" 59 | ]; 60 | 61 | export function createRandomNRows_(count, lastId) { 62 | var data = []; 63 | 64 | for (var i = 0; i < count; i++) 65 | data.push({ 66 | id: ++lastId, 67 | selected: false, 68 | label: 69 | adjectives[_random(adjectives.length)] + 70 | " " + 71 | colours[_random(colours.length)] + 72 | " " + 73 | nouns[_random(nouns.length)] 74 | }); 75 | 76 | return data; 77 | } 78 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/keyed/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Data.Array ((!!)) 6 | import Data.Array as DA 7 | import Data.Maybe (Maybe(..)) 8 | import Effect (Effect) 9 | import Effect.Aff (Aff) 10 | import Effect.Class (liftEffect) 11 | import Effect.Uncurried (EffectFn2) 12 | import Effect.Uncurried as EU 13 | import Flame (Html, ListUpdate, QuerySelector(..), (:>)) 14 | import Flame as F 15 | import Flame.Html.Attribute as HA 16 | import Flame.Html.Element as HE 17 | 18 | import Flame.Types(NodeData) 19 | 20 | data Message = 21 | Create Int | 22 | DisplayCreated (Array Row) | 23 | AppendOneThousand | 24 | DisplayAppended (Array Row) | 25 | UpdateEveryTenth | 26 | Clear | 27 | Swap | 28 | Remove Int | 29 | Select Int 30 | 31 | type Model = { 32 | rows :: Array Row, 33 | lastID :: Int 34 | } 35 | 36 | type Row = { 37 | id :: Int, 38 | label :: String, 39 | selected :: Boolean 40 | } 41 | 42 | type Button = { 43 | id :: String, 44 | label :: String, 45 | message :: Message 46 | } 47 | 48 | foreign import createRandomNRows_ :: EffectFn2 Int Int (Array Row) 49 | 50 | createRandomNRows :: Int -> Int -> Aff (Array Row) 51 | createRandomNRows n lastID = liftEffect (EU.runEffectFn2 createRandomNRows_ n lastID) 52 | 53 | main :: Effect Unit 54 | main = F.mount_ (QuerySelector "body") { 55 | init: model :> [], 56 | subscribe: [], 57 | view, 58 | update 59 | } 60 | 61 | model :: Model 62 | model = { 63 | rows: [], 64 | lastID: 0 65 | } 66 | 67 | view :: Model -> Html Message 68 | view model = HE.div [HA.class' "container"] [ 69 | jumbotron, 70 | HE.table [HA.class' "table table-hover table-striped test-data"] [ 71 | HE.tbody_ (map renderLazyRow model.rows) 72 | ], 73 | footer 74 | ] 75 | 76 | jumbotron :: Html Message 77 | jumbotron = HE.div [ HA.class' "jumbotron" ] [ 78 | HE.div [ HA.class' "row" ] [ 79 | HE.div [ HA.class' "col-md-6" ] [ 80 | HE.h1_ [ HE.text "Flame 1.0.0 (keyed)" ] 81 | ], 82 | HE.div [ HA.class' "col-md-6" ] [ 83 | map renderActionButton buttons 84 | ] 85 | ] 86 | ] 87 | 88 | renderActionButton :: Button -> Html Message 89 | renderActionButton button = HE.div [ HA.class' "col-sm-6 smallpad" ] [ 90 | HE.button [ 91 | HA.class' "btn btn-primary btn-block", 92 | HA.id button.id, 93 | HA.createAttribute "ref" "text", 94 | HA.onClick button.message 95 | ] [ HE.text button.label ] 96 | ] 97 | 98 | buttons :: Array Button 99 | buttons = [ 100 | { id: "run", label: "Create 1,000 rows", message: Create 1000 }, 101 | { id: "runlots", label: "Create 10,000 rows", message: Create 10000 }, 102 | { id: "add", label: "Append 1,000 rows", message: AppendOneThousand }, 103 | { id: "update", label: "Update every 10th row", message: UpdateEveryTenth }, 104 | { id: "clear", label: "Clear", message: Clear }, 105 | { id: "swaprows", label: "Swap Rows", message: Swap } 106 | ] 107 | 108 | renderLazyRow :: Row -> Html Message 109 | renderLazyRow row = HE.lazy (Just (show row.id)) renderRow row 110 | 111 | renderRow :: Row -> Html Message 112 | renderRow row = HE.tr [ HA.class' { "danger": row.selected }, HA.key (show row.id)] [ 113 | HE.td colMd1 [ HE.text (show row.id) ], 114 | HE.td colMd4 [ HE.a [ HA.onClick (Select row.id) ] [ HE.text row.label ] ], 115 | HE.td colMd1 [ HE.a [ HA.onClick (Remove row.id) ] removeIcon ], 116 | spacer 117 | ] 118 | 119 | removeIcon :: Array (Html Message) 120 | removeIcon = [ 121 | HE.span' [ HA.class' "glyphicon glyphicon-remove", HA.createAttribute "aria-hidden" "true"] 122 | ] 123 | 124 | colMd1 :: Array (NodeData Message) 125 | colMd1 = [ HA.class' "col-md-1" ] 126 | 127 | colMd4 :: Array (NodeData Message) 128 | colMd4 = [ HA.class' "col-md-4" ] 129 | 130 | spacer :: Html Message 131 | spacer = HE.td' [ HA.class' "col-md-6" ] 132 | 133 | footer :: Html Message 134 | footer = HE.span' [ HA.class' "preloadicon glyphicon glyphicon-remove", HA.createAttribute "aria-hidden" "true" ] 135 | 136 | update :: ListUpdate Model Message 137 | update model = 138 | case _ of 139 | Create amount -> model :> [map (\rows -> Just (DisplayCreated rows)) (createRandomNRows amount model.lastID)] 140 | DisplayCreated rows -> F.noMessages (model { lastID = model.lastID + DA.length rows, rows = rows }) 141 | 142 | AppendOneThousand -> 143 | let amount = 1000 144 | in model :> [map (\rows -> Just (DisplayAppended rows)) (createRandomNRows amount model.lastID)] 145 | DisplayAppended newRows -> F.noMessages (model { lastID = model.lastID + DA.length newRows, rows = model.rows <> newRows }) 146 | 147 | UpdateEveryTenth -> F.noMessages model { rows = DA.mapWithIndex updateLabel model.rows } 148 | 149 | Clear -> F.noMessages (model { rows = [] }) 150 | 151 | Swap -> 152 | F.noMessages 153 | (case swapRows model.rows 1 998 of 154 | Nothing -> model 155 | Just swappedRows -> model { rows = swappedRows }) 156 | 157 | Remove id -> F.noMessages (model { rows = DA.filter (\r -> r.id /= id) model.rows }) 158 | 159 | Select id -> F.noMessages (model { rows = map (select id) model.rows }) 160 | 161 | updateLabel index row = 162 | if index `mod` 10 == 0 then 163 | row { label = row.label <> " !!!" } 164 | else 165 | row 166 | 167 | swapRows arr index otherIndex = do 168 | rowA <- arr !! index 169 | rowB <- arr !! otherIndex 170 | arrA <- DA.updateAt index rowB arr 171 | arrB <- DA.updateAt otherIndex rowA arrA 172 | pure arrB 173 | 174 | select id row 175 | | row.id == id = row { selected = true } 176 | | row.selected = row { selected = false } 177 | | otherwise = row 178 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/keyed/webpack.flame.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'production', 7 | optimization: { 8 | usedExports: true 9 | }, 10 | entry: { 11 | index: './index.js', 12 | }, 13 | 14 | output: { 15 | path: path.resolve(__dirname, 'dist'), 16 | filename: 'bundle.js' 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/non-keyed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flame v1.0.0 (non-keyed) 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/non-keyed/index.js: -------------------------------------------------------------------------------- 1 | require('./dce-output/Main').main(); -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/non-keyed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-framework-benchmark-non-keyed-flame", 3 | "sideEffects": false, 4 | "version": "1.0.0", 5 | "description": "Purescript Flame JS Benchmark", 6 | "main": "index.js", 7 | "js-framework-benchmark": { 8 | "frameworkVersion": "1.0.0" 9 | }, 10 | "scripts": { 11 | "postinstall": "spago install", 12 | "clean": "rm -rf dist output .spago node_modules", 13 | "build": "spago build", 14 | "build-prod": "spago build --purs-args '--codegen corefn,js' && zephyr -f Main.main && webpack --config=webpack.flame.config.js" 15 | }, 16 | "keywords": [ 17 | "purescript", 18 | "flame" 19 | ], 20 | "author": "Eduardo Asafe ", 21 | "license": "ISC", 22 | "homepage": "https://github.com/krausest/js-framework-benchmark", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/krausest/js-framework-benchmark.git" 26 | }, 27 | "devDependencies": { 28 | "purescript": "0.14.4", 29 | "spago": "0.20.3", 30 | "webpack": "^4.44.1", 31 | "webpack-cli": "^3.3.12" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/non-keyed/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.0-20220527/packages.dhall 3 | sha256:15dd8041480502850e4043ea2977ed22d6ab3fc24d565211acde6f8c5152a799 4 | 5 | let overrides = {=} 6 | 7 | let additions = {=} 8 | 9 | in upstream // overrides // additions 10 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/non-keyed/spago.dhall: -------------------------------------------------------------------------------- 1 | { name = "js-framework-benchmark-flame" 2 | , dependencies = [ "flame", 3 | "aff", 4 | "arrays", 5 | "effect", 6 | "maybe", 7 | "web-dom", 8 | "prelude" ] 9 | , packages = ./packages.dhall 10 | , sources = [ "src/**/*.purs" ] 11 | } 12 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/non-keyed/src/Main.js: -------------------------------------------------------------------------------- 1 | function _random(max) { 2 | return Math.round(Math.random() * 1000) % max; 3 | } 4 | 5 | var adjectives = [ 6 | "pretty", 7 | "large", 8 | "big", 9 | "small", 10 | "tall", 11 | "short", 12 | "long", 13 | "handsome", 14 | "plain", 15 | "quaint", 16 | "clean", 17 | "elegant", 18 | "easy", 19 | "angry", 20 | "crazy", 21 | "helpful", 22 | "mushy", 23 | "odd", 24 | "unsightly", 25 | "adorable", 26 | "important", 27 | "inexpensive", 28 | "cheap", 29 | "expensive", 30 | "fancy" 31 | ], 32 | colours = [ 33 | "red", 34 | "yellow", 35 | "blue", 36 | "green", 37 | "pink", 38 | "brown", 39 | "purple", 40 | "brown", 41 | "white", 42 | "black", 43 | "orange" 44 | ], 45 | nouns = [ 46 | "table", 47 | "chair", 48 | "house", 49 | "bbq", 50 | "desk", 51 | "car", 52 | "pony", 53 | "cookie", 54 | "sandwich", 55 | "burger", 56 | "pizza", 57 | "mouse", 58 | "keyboard" 59 | ]; 60 | 61 | export function createRandomNRows_(count, lastId) { 62 | var data = []; 63 | 64 | for (var i = 0; i < count; i++) 65 | data.push({ 66 | id: ++lastId, 67 | selected: false, 68 | label: 69 | adjectives[_random(adjectives.length)] + 70 | " " + 71 | colours[_random(colours.length)] + 72 | " " + 73 | nouns[_random(nouns.length)] 74 | }); 75 | 76 | return data; 77 | } 78 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/non-keyed/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude (Unit, bind, map, mod, pure, show, (+), (/=), (<>), (==), otherwise) 4 | 5 | import Data.Array ((!!)) 6 | import Data.Array as DA 7 | import Data.Maybe (Maybe(..)) 8 | import Effect (Effect) 9 | import Effect.Aff (Aff) 10 | import Effect.Class (liftEffect) 11 | import Effect.Uncurried (EffectFn2) 12 | import Effect.Uncurried as EU 13 | import Flame.Application.EffectList (ListUpdate) 14 | import Flame.Types((:>), Html, NodeData) 15 | import Web.DOM.ParentNode(QuerySelector(..)) 16 | import Flame.Application.EffectList as F 17 | import Flame.Html.Attribute as HA 18 | import Flame.Html.Element as HE 19 | 20 | data Message = 21 | Create Int | 22 | DisplayCreated (Array Row) | 23 | AppendOneThousand | 24 | DisplayAppended (Array Row) | 25 | UpdateEveryTenth | 26 | Clear | 27 | Swap | 28 | Remove Int | 29 | Select Int 30 | 31 | type Model = { 32 | rows :: Array Row, 33 | lastID :: Int 34 | } 35 | 36 | type Row = { 37 | id :: Int, 38 | label :: String, 39 | selected :: Boolean 40 | } 41 | 42 | type Button = { 43 | id :: String, 44 | label :: String, 45 | message :: Message 46 | } 47 | 48 | foreign import createRandomNRows_ :: EffectFn2 Int Int (Array Row) 49 | 50 | createRandomNRows :: Int -> Int -> Aff (Array Row) 51 | createRandomNRows n lastID = liftEffect (EU.runEffectFn2 createRandomNRows_ n lastID) 52 | 53 | main :: Effect Unit 54 | main = F.mount_ (QuerySelector "main") { 55 | init: model :> [], 56 | view, 57 | subscribe: [], 58 | update 59 | } 60 | 61 | model :: Model 62 | model = { 63 | rows: [], 64 | lastID: 0 65 | } 66 | 67 | view :: Model -> Html Message 68 | view model = HE.div [HA.class' "container"] [ 69 | jumbotron, 70 | HE.table [HA.class' "table table-hover table-striped test-data"] [ 71 | HE.tbody_ (map renderLazyRow model.rows) 72 | ], 73 | footer 74 | ] 75 | 76 | jumbotron :: Html Message 77 | jumbotron = HE.div [ HA.class' "jumbotron" ] [ 78 | HE.div [ HA.class' "row" ] [ 79 | HE.div [ HA.class' "col-md-6" ] [ 80 | HE.h1_ [ HE.text "Flame 1.0.0 (non-keyed)" ] 81 | ], 82 | HE.div [ HA.class' "col-md-6" ] [ 83 | map renderActionButton buttons 84 | ] 85 | ] 86 | ] 87 | 88 | buttons :: Array Button 89 | buttons = [ 90 | { id: "run", label: "Create 1,000 rows", message: Create 1000 }, 91 | { id: "runlots", label: "Create 10,000 rows", message: Create 10000 }, 92 | { id: "add", label: "Append 1,000 rows", message: AppendOneThousand }, 93 | { id: "update", label: "Update every 10th row", message: UpdateEveryTenth }, 94 | { id: "clear", label: "Clear", message: Clear }, 95 | { id: "swaprows", label: "Swap Rows", message: Swap } 96 | ] 97 | 98 | renderActionButton :: Button -> Html Message 99 | renderActionButton button = HE.div [ HA.class' "col-sm-6 smallpad" ] [ 100 | HE.button [ 101 | HA.class' "btn btn-primary btn-block", 102 | HA.id button.id, 103 | HA.createAttribute "ref" "text", 104 | HA.onClick button.message 105 | ] [ HE.text button.label ] 106 | ] 107 | 108 | renderLazyRow :: Row -> Html Message 109 | renderLazyRow row = HE.lazy Nothing renderRow row 110 | 111 | renderRow :: Row -> Html Message 112 | renderRow row = HE.tr [ HA.class' { "danger": row.selected }] [ 113 | HE.td colMd1 [ HE.text (show row.id) ], 114 | HE.td colMd4 [ HE.a [ HA.onClick (Select row.id) ] [ HE.text row.label ] ], 115 | HE.td colMd1 [ HE.a [ HA.onClick (Remove row.id) ] removeIcon ], 116 | spacer 117 | ] 118 | 119 | removeIcon :: Array (Html Message) 120 | removeIcon = [ 121 | HE.span' [ HA.class' "glyphicon glyphicon-remove", HA.createAttribute "aria-hidden" "true"] 122 | ] 123 | 124 | colMd1 :: Array (NodeData Message) 125 | colMd1 = [ HA.class' "col-md-1" ] 126 | 127 | colMd4 :: Array (NodeData Message) 128 | colMd4 = [ HA.class' "col-md-4" ] 129 | 130 | spacer :: Html Message 131 | spacer = HE.td' [ HA.class' "col-md-6" ] 132 | 133 | footer :: Html Message 134 | footer = HE.span' [ HA.class' "preloadicon glyphicon glyphicon-remove", HA.createAttribute "aria-hidden" "true" ] 135 | 136 | update :: ListUpdate Model Message 137 | update model = 138 | case _ of 139 | Create amount -> model :> [map (\rows -> Just (DisplayCreated rows)) (createRandomNRows amount model.lastID)] 140 | DisplayCreated rows -> F.noMessages (model { lastID = model.lastID + DA.length rows, rows = rows }) 141 | 142 | AppendOneThousand -> 143 | let amount = 1000 144 | in model :> [map (\rows -> Just (DisplayAppended rows)) (createRandomNRows amount model.lastID)] 145 | DisplayAppended newRows -> F.noMessages (model { lastID = model.lastID + DA.length newRows, rows = model.rows <> newRows}) 146 | 147 | UpdateEveryTenth -> F.noMessages model { rows = DA.mapWithIndex updateLabel model.rows } 148 | 149 | Clear -> F.noMessages (model { rows = [] }) 150 | 151 | Swap -> 152 | F.noMessages 153 | (case swapRows model.rows 1 998 of 154 | Nothing -> model 155 | Just swappedRows -> model { rows = swappedRows }) 156 | 157 | Remove id -> F.noMessages (model { rows = DA.filter (\r -> r.id /= id) model.rows }) 158 | 159 | Select id -> F.noMessages (model { rows = map (select id) model.rows }) 160 | 161 | updateLabel index row = 162 | if index `mod` 10 == 0 then 163 | row { label = row.label <> " !!!" } 164 | else 165 | row 166 | 167 | swapRows arr index otherIndex = do 168 | rowA <- arr !! index 169 | rowB <- arr !! otherIndex 170 | arrA <- DA.updateAt index rowB arr 171 | arrB <- DA.updateAt otherIndex rowA arrA 172 | pure arrB 173 | 174 | select id row 175 | | row.id == id = row { selected = true } 176 | | row.selected = row { selected = false } 177 | | otherwise = row 178 | -------------------------------------------------------------------------------- /benchmarks/js-framework-benchmark/non-keyed/webpack.flame.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'production', 7 | optimization: { 8 | usedExports: true 9 | }, 10 | entry: { 11 | index: './index.js', 12 | }, 13 | 14 | output: { 15 | path: path.resolve(__dirname, 'dist'), 16 | filename: 'bundle.js' 17 | } 18 | }; -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-flame", 3 | "license": [ 4 | "MIT" 5 | ], 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/easafe/purescript-flame" 9 | }, 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "output" 15 | ], 16 | "dependencies": { 17 | "purescript-aff": "^v7.0.0", 18 | "purescript-argonaut-codecs": "^v9.1.0", 19 | "purescript-argonaut-core": "^v7.0.0", 20 | "purescript-argonaut-generic": "^v8.0.0", 21 | "purescript-arrays": "^v7.0.0", 22 | "purescript-bifunctors": "^v6.0.0", 23 | "purescript-console": "^v6.0.0", 24 | "purescript-effect": "^v4.0.0", 25 | "purescript-either": "^v6.1.0", 26 | "purescript-exceptions": "^v6.0.0", 27 | "purescript-foldable-traversable": "^v6.0.0", 28 | "purescript-foreign": "^v7.0.0", 29 | "purescript-foreign-object": "^v4.0.0", 30 | "purescript-maybe": "^v6.0.0", 31 | "purescript-newtype": "^v5.0.0", 32 | "purescript-nullable": "^v6.0.0", 33 | "purescript-partial": "^v4.0.0", 34 | "purescript-prelude": "^v6.0.1", 35 | "purescript-random": "^v6.0.0", 36 | "purescript-refs": "^v6.0.0", 37 | "purescript-spec": "^v7.0.0", 38 | "purescript-strings": "^v6.0.1", 39 | "purescript-tuples": "^v7.0.0", 40 | "purescript-typelevel-prelude": "^v7.0.0", 41 | "purescript-unsafe-coerce": "^v6.0.0", 42 | "purescript-web-dom": "^v6.0.0", 43 | "purescript-web-events": "^v4.0.0", 44 | "purescript-web-html": "^v4.0.0", 45 | "purescript-web-uievents": "^v4.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | flame.asafe.dev -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Hello! This is where you manage which Jekyll version is used to run. 4 | # When you want to use a different version, change it below, save the 5 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 6 | # 7 | # bundle exec jekyll serve 8 | # 9 | # This will help ensure the proper Jekyll version is running. 10 | # Happy Jekylling! 11 | gem "jekyll", "~> 3.8.5" 12 | 13 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 14 | gem "minima", "~> 2.0" 15 | 16 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 17 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 18 | # gem "github-pages", group: :jekyll_plugins 19 | 20 | # If you have any plugins, put them here! 21 | group :jekyll_plugins do 22 | gem "jekyll-feed", "~> 0.6" 23 | end 24 | 25 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 26 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.0" if Gem.win_platform? 30 | 31 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | colorator (1.1.0) 7 | concurrent-ruby (1.1.5) 8 | em-websocket (0.5.1) 9 | eventmachine (>= 0.12.9) 10 | http_parser.rb (~> 0.6.0) 11 | eventmachine (1.2.7) 12 | ffi (1.10.0) 13 | forwardable-extended (2.6.0) 14 | http_parser.rb (0.6.0) 15 | i18n (0.9.5) 16 | concurrent-ruby (~> 1.0) 17 | jekyll (3.8.5) 18 | addressable (~> 2.4) 19 | colorator (~> 1.0) 20 | em-websocket (~> 0.5) 21 | i18n (~> 0.7) 22 | jekyll-sass-converter (~> 1.0) 23 | jekyll-watch (~> 2.0) 24 | kramdown (~> 1.14) 25 | liquid (~> 4.0) 26 | mercenary (~> 0.3.3) 27 | pathutil (~> 0.9) 28 | rouge (>= 1.7, < 4) 29 | safe_yaml (~> 1.0) 30 | jekyll-feed (0.12.1) 31 | jekyll (>= 3.7, < 5.0) 32 | jekyll-sass-converter (1.5.2) 33 | sass (~> 3.4) 34 | jekyll-seo-tag (2.6.0) 35 | jekyll (~> 3.3) 36 | jekyll-watch (2.2.1) 37 | listen (~> 3.0) 38 | kramdown (1.17.0) 39 | liquid (4.0.3) 40 | listen (3.1.5) 41 | rb-fsevent (~> 0.9, >= 0.9.4) 42 | rb-inotify (~> 0.9, >= 0.9.7) 43 | ruby_dep (~> 1.2) 44 | mercenary (0.3.6) 45 | minima (2.5.0) 46 | jekyll (~> 3.5) 47 | jekyll-feed (~> 0.9) 48 | jekyll-seo-tag (~> 2.1) 49 | pathutil (0.16.2) 50 | forwardable-extended (~> 2.6) 51 | public_suffix (4.0.6) 52 | rb-fsevent (0.10.3) 53 | rb-inotify (0.10.0) 54 | ffi (~> 1.0) 55 | rouge (3.3.0) 56 | ruby_dep (1.5.0) 57 | safe_yaml (1.0.5) 58 | sass (3.7.4) 59 | sass-listen (~> 4.0.0) 60 | sass-listen (4.0.0) 61 | rb-fsevent (~> 0.9, >= 0.9.4) 62 | rb-inotify (~> 0.9, >= 0.9.7) 63 | 64 | PLATFORMS 65 | ruby 66 | 67 | DEPENDENCIES 68 | jekyll (~> 3.8.5) 69 | jekyll-feed (~> 0.6) 70 | minima (~> 2.0) 71 | tzinfo-data 72 | 73 | BUNDLED WITH 74 | 2.0.1 75 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Jekyll! 2 | # 3 | # This config file is meant for settings that affect your whole blog, values 4 | # which you are expected to set up once and rarely edit after that. If you find 5 | # yourself editing this file very often, consider using Jekyll's data files 6 | # feature for the data you need to update frequently. 7 | # 8 | # For technical reasons, this file is *NOT* reloaded automatically when you use 9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process. 10 | 11 | # Site settings 12 | # These are used to personalize your new site. If you look in the HTML files, 13 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. 14 | # You can create any custom variable you would like, and they will be accessible 15 | # in the templates via {{ site.myvariable }}. 16 | 17 | # Build settings 18 | markdown: kramdown 19 | highlighter: rouge 20 | 21 | # Exclude from processing. 22 | # The following items will not be processed, by default. Create a custom list 23 | # to override the default setting. 24 | # exclude: 25 | # - Gemfile 26 | # - Gemfile.lock 27 | # - node_modules 28 | # - vendor/bundle/ 29 | # - vendor/cache/ 30 | # - vendor/gems/ 31 | # - vendor/ruby/ 32 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flame - {{ page.title }} 7 | 8 | 9 | 10 |
11 | 28 |
29 |
30 | {{ content }} 31 |
32 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/assets/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans'); 2 | @import url('https://fonts.googleapis.com/css?family=Catamaran:400,700'); 3 | html { 4 | height: 100%; 5 | box-sizing: border-box; 6 | background-color: #212120; 7 | color: rgb(238, 238, 238); 8 | } 9 | 10 | * { 11 | background: inherit 12 | } 13 | 14 | body { 15 | margin: 0; 16 | min-height: 100%; 17 | font-family: "Open sans", "sans serif"; 18 | font-size: 18px; 19 | line-height: 1.51857143; 20 | display: flex; 21 | } 22 | 23 | h1, h2, h3, h4 { 24 | -webkit-margin-before: 0.0em; 25 | -webkit-margin-after: 0.0em; 26 | font-weight: bold; 27 | color: #c53a34; 28 | font-family: "Catamaran"; 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | font-weight: bold; 34 | } 35 | 36 | a:visited { 37 | text-decoration: none 38 | } 39 | 40 | h1 { 41 | font-size: 2em; 42 | } 43 | 44 | h2 { 45 | font-size: 1.6em; 46 | } 47 | 48 | h3 { 49 | font-size: 1.3em; 50 | } 51 | 52 | ul { 53 | padding: 0; 54 | } 55 | 56 | ul li { 57 | list-style-type: none; 58 | padding-bottom: 14px; 59 | } 60 | 61 | .menu { 62 | padding: 40px; 63 | padding-right: 60px; 64 | display: flex; 65 | position: sticky; 66 | top: 10px; 67 | z-index: 1; 68 | flex-flow: column; 69 | } 70 | 71 | .content { 72 | padding: 60px; 73 | width: 1024px; 74 | } 75 | 76 | .content a { 77 | font-family: "Open sans", "sans serif"; 78 | } 79 | 80 | a.direction, .menu a:not(.project-name) { 81 | color: #212120; 82 | background-color: #c53a34; 83 | padding: 3px 5px; 84 | font-family: "Catamaran"; 85 | display: inline-block; 86 | border-radius: 2px; 87 | font-size: 1.1em; 88 | } 89 | 90 | .content a:not(.direction), .menu a.current, .menu a:hover { 91 | color: #c53a34; 92 | background-color: #212120; 93 | } 94 | 95 | .content ul li { 96 | list-style-type: circle; 97 | padding: 5px 98 | } 99 | 100 | .project-name { 101 | color: #c53a34; 102 | background-color: #212120; 103 | font-size: 1.3em; 104 | padding: 0; 105 | } 106 | 107 | .previous { 108 | display: inline-block; 109 | margin-right: 20px; 110 | margin-bottom: 20px; 111 | } 112 | 113 | .current { 114 | border-bottom: 3px solid #c53a34; 115 | } 116 | 117 | @media (max-width:600px) { 118 | .menu, .content { 119 | max-width: 100%; 120 | padding: 5px 10px; 121 | } 122 | body { 123 | display: block; 124 | } 125 | div { 126 | width: 100%; 127 | display: block; 128 | } 129 | 130 | p { 131 | display: block; 132 | word-wrap: break-word; 133 | max-width: 97vw; 134 | 135 | } 136 | 137 | } 138 | 139 | .highlight .c { 140 | color: #75715e 141 | } 142 | 143 | /* Comment */ 144 | 145 | .highlight .err { 146 | color: #960050; 147 | background-color: #1e0010 148 | } 149 | 150 | /* Error */ 151 | 152 | .highlight .k { 153 | color: #66d9ef 154 | } 155 | 156 | /* Keyword */ 157 | 158 | .highlight .l { 159 | color: #ae81ff 160 | } 161 | 162 | /* Literal */ 163 | 164 | .highlight .n { 165 | color: #f8f8f2 166 | } 167 | 168 | /* Name */ 169 | 170 | .highlight .o { 171 | color: #f92672 172 | } 173 | 174 | /* Operator */ 175 | 176 | .highlight .p { 177 | color: #f8f8f2 178 | } 179 | 180 | /* Punctuation */ 181 | 182 | .highlight .cm { 183 | color: #75715e 184 | } 185 | 186 | /* Comment.Multiline */ 187 | 188 | .highlight .cp { 189 | color: #75715e 190 | } 191 | 192 | /* Comment.Preproc */ 193 | 194 | .highlight .c1 { 195 | color: #75715e 196 | } 197 | 198 | /* Comment.Single */ 199 | 200 | .highlight .cs { 201 | color: #75715e 202 | } 203 | 204 | /* Comment.Special */ 205 | 206 | .highlight .ge { 207 | font-style: italic 208 | } 209 | 210 | /* Generic.Emph */ 211 | 212 | .highlight .gs { 213 | font-weight: bold 214 | } 215 | 216 | /* Generic.Strong */ 217 | 218 | .highlight .kc { 219 | color: #66d9ef 220 | } 221 | 222 | /* Keyword.Constant */ 223 | 224 | .highlight .kd { 225 | color: #66d9ef 226 | } 227 | 228 | /* Keyword.Declaration */ 229 | 230 | .highlight .kn { 231 | color: #f92672 232 | } 233 | 234 | /* Keyword.Namespace */ 235 | 236 | .highlight .kp { 237 | color: #66d9ef 238 | } 239 | 240 | /* Keyword.Pseudo */ 241 | 242 | .highlight .kr { 243 | color: #66d9ef 244 | } 245 | 246 | /* Keyword.Reserved */ 247 | 248 | .highlight .kt { 249 | color: #66d9ef 250 | } 251 | 252 | /* Keyword.Type */ 253 | 254 | .highlight .ld { 255 | color: #e6db74 256 | } 257 | 258 | /* Literal.Date */ 259 | 260 | .highlight .m { 261 | color: #ae81ff 262 | } 263 | 264 | /* Literal.Number */ 265 | 266 | .highlight .s { 267 | color: #e6db74 268 | } 269 | 270 | /* Literal.String */ 271 | 272 | .highlight .na { 273 | color: #a6e22e 274 | } 275 | 276 | /* Name.Attribute */ 277 | 278 | .highlight .nb { 279 | color: #f8f8f2 280 | } 281 | 282 | /* Name.Builtin */ 283 | 284 | .highlight .nc { 285 | color: #a6e22e 286 | } 287 | 288 | /* Name.Class */ 289 | 290 | .highlight .no { 291 | color: #66d9ef 292 | } 293 | 294 | /* Name.Constant */ 295 | 296 | .highlight .nd { 297 | color: #a6e22e 298 | } 299 | 300 | /* Name.Decorator */ 301 | 302 | .highlight .ni { 303 | color: #f8f8f2 304 | } 305 | 306 | /* Name.Entity */ 307 | 308 | .highlight .ne { 309 | color: #a6e22e 310 | } 311 | 312 | /* Name.Exception */ 313 | 314 | .highlight .nf { 315 | color: #a6e22e 316 | } 317 | 318 | /* Name.Function */ 319 | 320 | .highlight .nl { 321 | color: #f8f8f2 322 | } 323 | 324 | /* Name.Label */ 325 | 326 | .highlight .nn { 327 | color: #f8f8f2 328 | } 329 | 330 | /* Name.Namespace */ 331 | 332 | .highlight .nx { 333 | color: #a6e22e 334 | } 335 | 336 | /* Name.Other */ 337 | 338 | .highlight .py { 339 | color: #f8f8f2 340 | } 341 | 342 | /* Name.Property */ 343 | 344 | .highlight .nt { 345 | color: #f92672 346 | } 347 | 348 | /* Name.Tag */ 349 | 350 | .highlight .nv { 351 | color: #f8f8f2 352 | } 353 | 354 | /* Name.Variable */ 355 | 356 | .highlight .ow { 357 | color: #f92672 358 | } 359 | 360 | /* Operator.Word */ 361 | 362 | .highlight .w { 363 | color: #f8f8f2 364 | } 365 | 366 | /* Text.Whitespace */ 367 | 368 | .highlight .mf { 369 | color: #ae81ff 370 | } 371 | 372 | /* Literal.Number.Float */ 373 | 374 | .highlight .mh { 375 | color: #ae81ff 376 | } 377 | 378 | /* Literal.Number.Hex */ 379 | 380 | .highlight .mi { 381 | color: #ae81ff 382 | } 383 | 384 | /* Literal.Number.Integer */ 385 | 386 | .highlight .mo { 387 | color: #ae81ff 388 | } 389 | 390 | /* Literal.Number.Oct */ 391 | 392 | .highlight .sb { 393 | color: #e6db74 394 | } 395 | 396 | /* Literal.String.Backtick */ 397 | 398 | .highlight .sc { 399 | color: #e6db74 400 | } 401 | 402 | /* Literal.String.Char */ 403 | 404 | .highlight .sd { 405 | color: #e6db74 406 | } 407 | 408 | /* Literal.String.Doc */ 409 | 410 | .highlight .s2 { 411 | color: #e6db74 412 | } 413 | 414 | /* Literal.String.Double */ 415 | 416 | .highlight .se { 417 | color: #ae81ff 418 | } 419 | 420 | /* Literal.String.Escape */ 421 | 422 | .highlight .sh { 423 | color: #e6db74 424 | } 425 | 426 | /* Literal.String.Heredoc */ 427 | 428 | .highlight .si { 429 | color: #e6db74 430 | } 431 | 432 | /* Literal.String.Interpol */ 433 | 434 | .highlight .sx { 435 | color: #e6db74 436 | } 437 | 438 | /* Literal.String.Other */ 439 | 440 | .highlight .sr { 441 | color: #e6db74 442 | } 443 | 444 | /* Literal.String.Regex */ 445 | 446 | .highlight .s1 { 447 | color: #e6db74 448 | } 449 | 450 | /* Literal.String.Single */ 451 | 452 | .highlight .ss { 453 | color: #e6db74 454 | } 455 | 456 | /* Literal.String.Symbol */ 457 | 458 | .highlight .bp { 459 | color: #f8f8f2 460 | } 461 | 462 | /* Name.Builtin.Pseudo */ 463 | 464 | .highlight .vc { 465 | color: #f8f8f2 466 | } 467 | 468 | /* Name.Variable.Class */ 469 | 470 | .highlight .vg { 471 | color: #f8f8f2 472 | } 473 | 474 | /* Name.Variable.Global */ 475 | 476 | .highlight .vi { 477 | color: #f8f8f2 478 | } 479 | 480 | /* Name.Variable.Instance */ 481 | 482 | .highlight .il { 483 | color: #ae81ff 484 | } 485 | 486 | /* Literal.Number.Integer.Long */ 487 | 488 | .highlight .gh {} 489 | 490 | /* Generic Heading & Diff Header */ 491 | 492 | .highlight .gu { 493 | color: #75715e; 494 | } 495 | 496 | /* Generic.Subheading & Diff Unified/Comment? */ 497 | 498 | .highlight .gd { 499 | color: #f92672; 500 | } 501 | 502 | /* Generic.Deleted & Diff Deleted */ 503 | 504 | .highlight .gi { 505 | color: #a6e22e; 506 | } 507 | 508 | /* Generic.Inserted & Diff Inserted */ 509 | 510 | code.highlighter-rouge { 511 | border: 1px solid rgba(255, 255, 255, .1); 512 | padding: 0 .3em; 513 | border-radius: 3px; 514 | font-size: 1.1em; 515 | } -------------------------------------------------------------------------------- /docs/assets/img/benchmark-keyed-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easafe/purescript-flame/90265f96366a00e5b46e963001172798f776e2f4/docs/assets/img/benchmark-keyed-1.png -------------------------------------------------------------------------------- /docs/assets/img/benchmark-keyed-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easafe/purescript-flame/90265f96366a00e5b46e963001172798f776e2f4/docs/assets/img/benchmark-keyed-2.png -------------------------------------------------------------------------------- /docs/assets/img/benchmark-non-keyed-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easafe/purescript-flame/90265f96366a00e5b46e963001172798f776e2f4/docs/assets/img/benchmark-non-keyed-1.png -------------------------------------------------------------------------------- /docs/assets/img/benchmark-non-keyed-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easafe/purescript-flame/90265f96366a00e5b46e963001172798f776e2f4/docs/assets/img/benchmark-non-keyed-2.png -------------------------------------------------------------------------------- /docs/benchmarks.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Benchmarks 4 | permalink: /benchmarks 5 | --- 6 | 7 | ## Benchmarks 8 | 9 | Flame performs comparatively to similar JavaScript or PureScript libraries in usual metrics like bundle size, initialization speed, memory usage, etc. For what it is worth, here is how it stacks against a few popular frameworks (on an old Linux Core i5 with 16GB of ram): 10 | 11 | ![benchmark-keyed-1](assets/img/benchmark-keyed-1.png) 12 | ![benchmark-keyed-2](assets/img/benchmark-keyed-2.png) 13 | 14 | ![benchmark-non-keyed-1](assets/img/benchmark-non-keyed-1.png) 15 | ![benchmark-non-keyed-2](assets/img/benchmark-non-keyed-2.png) 16 | 17 | See also the official results at the [js web frameworks benchmark](https://krausest.github.io/js-framework-benchmark/index.html). 18 | 19 | These figures should be taken with a grain of salt, however. For most applications, virtually any front-end framework is "fast" enough. Techniques like server-side rendering, caching, lazy rendering, etc go a long before we actually have to think about DOM manipulation performance. -------------------------------------------------------------------------------- /docs/concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Main concepts 4 | permalink: /concepts 5 | --- 6 | 7 | ## Main concepts 8 | 9 | A Flame application consists of the following record 10 | 11 | ```haskell 12 | type Application model message = { 13 | init :: model, 14 | view :: model -> Html message, 15 | update :: model -> message -> model, 16 | subscribe :: Array (Subscription message) 17 | } 18 | ``` 19 | The type variable `model` refers to the state of the application. `message`, on the other hand, describes the kinds of events the application can handle. 20 | 21 | ### Application state 22 | 23 | In the counter example we set our model as a simple type alias 24 | 25 | ```haskell 26 | type Model = Int 27 | ``` 28 | 29 | that is, the state of our application is a single integer. In a real world application, the model will probably be something more interesting -- Flame makes no assumption about how it is structured. 30 | 31 | With our model type declared, we can define the initial state of the application 32 | 33 | ```haskell 34 | init :: Model 35 | init = 0 36 | ``` 37 | 38 | The first time the application is rendered, Flame calls the view function with `init`. 39 | 40 | ### Application markup 41 | 42 | The `view` function maps the current state to markup. Whenever the model is updated, Flame patches the DOM by calling `view` with the new state. 43 | 44 | In the counter example, the view is defined as 45 | 46 | ```haskell 47 | view :: Model -> Html Message 48 | view model = HE.main "main" [ 49 | HE.button [HA.onClick Decrement] "-", 50 | HE.text $ show model, 51 | HE.button [HA.onClick Increment] "+" 52 | ] 53 | ``` 54 | 55 | The `message`s raised on events are used to signal how the application state should be updated. 56 | 57 | See [Defining views](views) for an in depth look at views. 58 | 59 | ### State updating 60 | 61 | The `update` function handles events, returning an updated model. In a Flame application, we reify native events as a custom data type. In the counter example, we are interested in the following events: 62 | 63 | ```haskell 64 | data Message = Increment | Decrement 65 | ``` 66 | 67 | and thus our update function looks like 68 | 69 | ```haskell 70 | update :: Model -> Message -> Model 71 | update model = case _ of 72 | Increment -> model + 1 73 | Decrement -> model - 1 74 | ``` 75 | 76 | See [Handling events](events) for an in depth look at update strategies. 77 | 78 | ### Subscriptions 79 | 80 | Finally, we can specify events that come from outside of the `view` as an array to `subscribe`. Such events include `window`, `document` -- and even messages arbitrarily raised by user code. These messages will then be handled in the usual way by the `update` function. 81 | 82 | In the counter example no external events are handled, so the subscription list is empty 83 | 84 | ```haskell 85 | subscribe :: Array (Subscription Message) 86 | subscribe = [] 87 | } 88 | ``` 89 | 90 | See [Handling external events](events#handling-external-events) for an in depth look at subscriptions. 91 | 92 | ### Rendering 93 | 94 | Having all pieces put together, we can either render the application to the DOM, as in the case of the counter example 95 | 96 | ```haskell 97 | main :: Effect Unit 98 | main = FAN.mount_ (QuerySelector "body") { 99 | init, 100 | view, 101 | update, 102 | subscribe 103 | } 104 | ``` 105 | 106 | or as a `String` with `Flame.Renderer.String.render`, which can be used server-side. 107 | 108 | See [Rendering the app](rendering) for an in depth look at rendering. 109 | 110 | 111 | Next: Defining views 112 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easafe/purescript-flame/90265f96366a00e5b46e963001172798f776e2f4/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Getting started 4 | --- 5 | 6 | ## Simple, fast & type safe web applications 7 | 8 | Flame is a PureScript front-end framework inspired by the Elm architecture with focus on simplicity and performance. Featuring: 9 | 10 | * Message based state updating -- see [Handling events](events) 11 | 12 | * Subscriptions -- see [Handling external events](events#subscriptions) 13 | 14 | * Server side rendering -- see [Rendering the app](rendering) 15 | 16 | * Performance comparable to native JavaScript frameworks -- see [benchmarks](benchmarks) 17 | 18 | * Parse HTML into Flame markup with [breeze](https://github.com/easafe/haskell-breeze) 19 | 20 | ## Quick start 21 | 22 | Install: 23 | 24 | ```bash 25 | spago install flame 26 | ``` 27 | 28 | Example counter app: 29 | 30 | ```haskell 31 | module Counter.Main where 32 | 33 | import Prelude 34 | 35 | import Effect (Effect) 36 | import Flame (Html, QuerySelector(..), Subscription) 37 | -- Side effects free updating; see docs for other examples 38 | import Flame.Application.NoEffects as FAN 39 | import Flame.Html.Element as HE 40 | import Flame.Html.Attribute as HA 41 | 42 | -- | The model represents the state of the app 43 | type Model = Int 44 | 45 | -- | Data type used to represent events 46 | data Message = Increment | Decrement 47 | 48 | -- | Initial state of the app 49 | init :: Model 50 | init = 0 51 | 52 | -- | `update` is called to handle events 53 | update :: Model -> Message -> Model 54 | update model = case _ of 55 | Increment -> model + 1 56 | Decrement -> model - 1 57 | 58 | -- | `view` is called whenever the model is updated 59 | view :: Model -> Html Message 60 | view model = HE.main "main" [ 61 | HE.button [HA.onClick Decrement] "-", 62 | HE.text $ show model, 63 | HE.button [HA.onClick Increment] "+" 64 | ] 65 | 66 | -- | Events that come from outside the `view` 67 | subscribe :: Array (Subscription Message) 68 | subscribe = [] 69 | 70 | -- | Mount the application on the given selector 71 | main :: Effect Unit 72 | main = FAN.mount_ (QuerySelector "body") { 73 | init, 74 | view, 75 | update, 76 | subscribe 77 | } 78 | ``` 79 | 80 | Next: Main concepts 81 | -------------------------------------------------------------------------------- /docs/rendering.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Rendering the app 4 | permalink: /rendering 5 | --- 6 | 7 | ## Rendering the app 8 | 9 | With all pieces in place 10 | 11 | ```haskell 12 | type Application model message = { 13 | init :: model, 14 | view :: model -> Html message, 15 | update :: model -> message -> model, 16 | subscribe :: Array (Subscription message) 17 | } 18 | ``` 19 | 20 | let's talk about actually rendering the application. 21 | 22 | ### DOM rendering 23 | 24 | The mount functions we saw previously in [Handling events](events) sets up a Flame application on the client side 25 | 26 | ```haskell 27 | mount_ :: forall model message. QuerySelector -> Application model message -> Effect Unit 28 | 29 | mount :: forall id model message. Show id => QuerySelector -> AppId id message -> Application model message -> Effect Unit 30 | ``` 31 | 32 | The first parameter is a CSS selector used as mount point. The application markup is added as children nodes to the mount point, otherwise it is left untouched. Several applications can live in the same `document` provided they are mounted on different selectors. 33 | 34 | ### Server side rendering 35 | 36 | We can render a Flame application as a markup string server-side in two different ways: 37 | 38 | #### Static markup 39 | 40 | The module `Flame.Renderer.String` exports the function 41 | 42 | ```haskell 43 | render :: forall message. Html message -> Effect String 44 | ``` 45 | 46 | which can be used to generate markup as a string, e.g., for a static page or website or template. This way, we can render regular `view` functions using the full expressiveness of PureScript server-side, but no `message` events will be raised. 47 | 48 | #### Pre rendered application 49 | 50 | The module `Flame` provides 51 | 52 | ```haskell 53 | type PreApplication model message = { 54 | init :: model, 55 | view :: model -> Html message 56 | } 57 | 58 | preMount :: forall model message. SerializeState model => QuerySelector -> PreApplication model message -> Effect String 59 | ``` 60 | 61 | which can used to render server-side the initial state of an application. On client side, we can use 62 | 63 | ```haskell 64 | type ResumedApplication model message = { 65 | init :: Array (Aff (Maybe message)), -- only the (optional) initial message to be raised 66 | view :: model -> Html message, 67 | update :: model -> message -> Tuple model (Array (Aff (Maybe message))), -- update is only available client side 68 | subscribe :: Array (Subscription message) -- subscriptions are only available client side 69 | } 70 | 71 | resumeMount_ :: forall model message. UnserializeState model => QuerySelector -> ResumedApplication model message -> Effect Unit 72 | --or 73 | resumeMount :: forall id model message. UnserializeState model => Show id => QuerySelector -> AppId id message -> ResumedApplication model message -> Effect Unit 74 | ``` 75 | 76 | to install event handlers in the pre rendered markup. The `SerializeState`/`UnserializeState` type class automatically parses the initial state as JSON in case of records or `Generic` instances. The `QuerySelector` passed to `preMount` and `resumeMount` must match -- otherwise the application will crash with an exception. To avoid diffing issues, the same `view` function should be used on the server and client side as well. 77 | 78 | See the [Dice application](https://github.com/easafe/purescript-flame/tree/master/examples/ServerSideRendering) for an example of how to pre render an application on server-side. 79 | 80 | 81 | -------------------------------------------------------------------------------- /docs/views.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Defining views 4 | permalink: /views 5 | --- 6 | 7 | ## Defining views 8 | 9 | In the application record 10 | 11 | ```haskell 12 | type Application model message = { 13 | init :: model, 14 | view :: model -> Html message, 15 | update :: model -> message -> model, 16 | subscribe :: Array (Subscription message) 17 | } 18 | ``` 19 | 20 | `view` maps the current state to markup. Whenever the model is updated, Flame patches the DOM by calling the `view` function with the new state. 21 | 22 | A custom DSL, defined by the type `Html`, is used to write markup. Alternatively, [breeze](https://github.com/easafe/haskell-breeze) can generate Flame views from HTML. 23 | 24 | You will likely need to qualify imports, e.g., prefix HE for HTML elements and HA for HTML attributes and events 25 | 26 | ```haskell 27 | import Flame.Html.Element as HE 28 | import Flame.Html.Attribute as HA 29 | ``` 30 | 31 | ### Attributes and events 32 | 33 | The module `Flame.Html.Attribute` exports 34 | 35 | * Regular name=value attributes such as `id` or `type'` 36 | 37 | * Helpers like `class'` or `style` that accept records 38 | 39 | * Presential attributes, such as `disabled` or `checked`, expecting boolean parameters 40 | 41 | * Events, e.g., `onClick` or `onInput`, expecting a `message` data constructor 42 | 43 | * A special attribute, `key` used to enable ["keyed" rendering](https://www.stefankrause.net/wp/?p=342) 44 | 45 | See the [API reference](https://pursuit.purescript.org/packages/purescript-flame) for a complete list of attributes. In the case you need to define your own attributes/events, Flame provides the combinators 46 | 47 | ```haskell 48 | HA.createAttibute 49 | HA.createProperty --for DOM properties 50 | HA.createEvent 51 | HA.createRawEvent 52 | ``` 53 | 54 | ### Elements 55 | 56 | The module `Flame.Html.Element` exports HTML elements, such as `div`, `body`, etc, following the convention 57 | 58 | * Functions named `element` expects attributes and children elements 59 | 60 | ```haskell 61 | HE.div [HA.id "my-div"] [HE.text "text content"] -- renders
text content
62 | ``` 63 | 64 | * Functions named `element_` (trailing underscore) expects children elements but no attributes 65 | 66 | ```haskell 67 | HE.div_ [HE.text "text content"] -- renders
text content
68 | ``` 69 | 70 | * Functions named `element'` (trailing quote) expects attributes but no children elements 71 | 72 | ```haskell 73 | HE.div' [HA.id "my-div"] -- renders
74 | ``` 75 | 76 | (a few elements that usually have no children like br or input have `element` behave as `element'`) 77 | 78 | Attributes and children elements are passed as arrays 79 | 80 | ```haskell 81 | HE.div [HA.id "my-div", HA.disabled False, HA.title "div title"] [ 82 | HE.span' [HA.id "special-span"], 83 | HE.br, 84 | HE.span_ [HE.text "I am regular"], 85 | ] 86 | {- renders 87 |
88 | 89 |
90 | I am regular 91 |
92 | -} 93 | ``` 94 | 95 | But for some common cases, the markup DSL also defines convenience type classes so we can write 96 | 97 | * `HE.element "my-element" _` instead of `HE.element [HA.id "my-element"] _` to declare an element with only id as attribute 98 | 99 | * `HE.element _ "text content"` instead of `HE.element _ [HE.text "text content"]` to declare elements with only text as children 100 | 101 | * `HE.element (HA.attribute _) _` instead of `HE.element [HA.attribute _] _` to declare elements with a single attribute 102 | 103 | * `HE.element _ $ HE.element _ _` instead of `HE.element _ [HE.Element _ _]` to declare elements with a single child element 104 | 105 | Flame also offers a few special elements for cases where finer control is necessary 106 | 107 | * Managed elements 108 | 109 | `HE.managed` takes user supplied functions to manipulate an element's DOM node 110 | 111 | ```haskell 112 | type NodeRenderer arg = { 113 | createNode :: arg -> Effect Node, 114 | updateNode :: Node -> arg -> arg -> Effect Node 115 | } 116 | 117 | managed :: forall arg nd message. ToNode nd message NodeData => NodeRenderer arg -> nd -> arg -> Html message 118 | ``` 119 | 120 | On rendering, Flame calls `createNode` only once and from then on `updateNode`. These functions can check on their local state `arg` to decide whether/how to change a DOM node. For easy of use, the elements attributes and events are still automatically patched -- otherwise, `HE.managed_` should be used 121 | 122 | ```haskell 123 | managed_ :: forall arg message. NodeRenderer arg -> arg -> Html message 124 | ``` 125 | 126 | * Lazy elements 127 | 128 | Lazy elements are only re-rendered if their local state `arg` changes 129 | 130 | ```haskell 131 | lazy :: forall arg message. Maybe Key -> (arg -> Html message) -> arg -> Html message 132 | ``` 133 | 134 | This is useful to avoid recomputing potentially expensive views such as large lists. 135 | 136 | * Fragments 137 | 138 | Fragments are wrappers 139 | 140 | ```haskell 141 | fragment :: forall children message. ToNode children message Html => children -> Html message 142 | ``` 143 | 144 | meaning that only their children elements will be rendered to the DOM. Fragments are useful in cases where having an extra parent element is unnecessary, or wherever [`DocumentFragment`](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment) could be used. 145 | 146 | See the [API reference](https://pursuit.purescript.org/packages/purescript-flame) for a complete list of elements. In the case you need to define your own elements, Flame provides a few combinators as well 147 | 148 | ```haskell 149 | HE.createElement 150 | HE.createElement_ 151 | HE.createElement' 152 | HE.createEmptyElement 153 | ``` 154 | 155 | ### Combining attributes and events 156 | 157 | For most attributes, later declarations overwrite previous ones 158 | 159 | ```haskell 160 | HE.div' [HA.title "title", HA.title "title 2"] 161 | {- renders 162 |
163 |
164 | -} 165 | ``` 166 | 167 | ```haskell 168 | HE.input [HA.type' "input", HA.value "test", HA.value "not a test!"] 169 | {- renders 170 | 171 | with value set to "not a test!" 172 | -} 173 | ``` 174 | 175 | However, classes, inline styles and events behave differently: 176 | 177 | * Classes and styles 178 | 179 | All uses of `HE.class'` on a single element are merged 180 | 181 | ```haskell 182 | HE.div' [HA.class' "a b", HA.class' { c: true }] 183 | {- renders 184 |
185 |
186 | -} 187 | ``` 188 | 189 | So is `HE.style` 190 | 191 | ```haskell 192 | HE.div' [HA.style { display: "flex", color: "red" }, HA.style { order: "1" }] 193 | {- renders 194 |
195 |
196 | -} 197 | ``` 198 | 199 | * Events 200 | 201 | Different messages for the same event on a single element are raised in the order they were declared. For example, clicking on a `div` similar to 202 | 203 | ```haskell 204 | HE.div' [HA.onClick Message1, HA.onClick Message2] 205 | ``` 206 | 207 | will result on the `update` function being called with `Message1` and after once again with `Message2`. 208 | 209 | ### View logic 210 | 211 | A `view` is just a regular PureScript function: we can compose it or pass it around as any other value. For example, we can use the model in attributes 212 | 213 | ```haskell 214 | type Model = { 215 | done :: Int, 216 | enabled :: Boolean 217 | } 218 | 219 | data Message = Do 220 | 221 | view :: Model -> Html Message 222 | view model = HE.div [HA.class' { usefulClass: model.enabled }] $ HE.input [HA.type' "button", HA.value "Do thing number " <> show $ model.done, HA.onClick Do] 223 | ``` 224 | 225 | or to selective alter the markup 226 | 227 | ```haskell 228 | type Name = String 229 | 230 | type Model = Maybe Name 231 | 232 | data Message = Update Name | Greet 233 | 234 | view :: Model -> Html Message 235 | view = case _ of 236 | Nothing -> HE.div_ [ 237 | HE.input [HA.type' "text", HA.onInput Update], 238 | HE.input [HA.type' "button", HA.value "Greet!", HA.onClick Greet] 239 | ] 240 | Just name -> "Greetings, " <> name <> "!" 241 | ``` 242 | 243 | as well create "partial views" without the need for any special syntax 244 | 245 | ```haskell 246 | header :: forall model message. model -> Html message 247 | header = ... 248 | 249 | footer :: forall model message. model -> Html message 250 | footer = ... 251 | 252 | view :: Model -> Html Message 253 | view model = HE.content' [ 254 | header, 255 | ... 256 | footer 257 | ] 258 | ``` 259 | 260 | `Html` is also a `Functor` so mapping `message`s works as expected. For instance, the [counters example](https://github.com/easafe/purescript-flame/tree/master/examples/Counters) markup is a list of counters 261 | 262 | ```haskell 263 | view :: Model -> Html Message 264 | view model = HE.main "main" [ 265 | HE.button [HA.onClick Add] "Add", 266 | HE.div_ $ DA.mapWithIndex viewCounter model 267 | ] 268 | where viewCounter index model' = HE.div [HA.style { display: "flex" }] [ 269 | CounterMessage index <$> ECM.view model', 270 | HE.button [HA.onClick $ Remove index] "Remove" 271 | ] 272 | ``` 273 | 274 | 275 | Next: Handling events 276 | -------------------------------------------------------------------------------- /examples.dhall: -------------------------------------------------------------------------------- 1 | let conf = ./spago.dhall 2 | 3 | in conf // { 4 | sources = conf.sources # [ "examples/**/*.purs" ], 5 | dependencies = conf.dependencies # [ "httpure" , "affjax", "affjax-web", "node-fs-aff", "js-timers", "web-storage"] 6 | } -------------------------------------------------------------------------------- /examples/Affjax/Affjax.purs: -------------------------------------------------------------------------------- 1 | module Examples.EffectList.Affjax.Main where 2 | 3 | import Prelude 4 | 5 | import Affjax.Web as A 6 | import Affjax.ResponseFormat as AR 7 | import Data.Either (Either(..)) 8 | import Data.Maybe (Maybe(..)) 9 | import Effect (Effect) 10 | import Flame (QuerySelector(..), Html, (:>), ListUpdate) 11 | import Flame as F 12 | import Flame.Html.Attribute as HA 13 | import Flame.Html.Element as HE 14 | 15 | type Model = 16 | { url ∷ String 17 | , result ∷ Result 18 | } 19 | 20 | data Message = UpdateUrl String | Fetch | Fetched Result 21 | 22 | data Result = NotFetched | Fetching | Ok String | Error String 23 | 24 | derive instance eqResult ∷ Eq Result 25 | 26 | init ∷ Model 27 | init = 28 | { url: "https://httpbin.org/get" 29 | , result: NotFetched 30 | } 31 | 32 | update ∷ ListUpdate Model Message 33 | update model = 34 | case _ of 35 | UpdateUrl url → F.noMessages $ model { url = url, result = NotFetched } 36 | Fetch → model { result = Fetching } :> 37 | [ do 38 | response ← A.get AR.string model.url 39 | pure <<< Just <<< Fetched $ case response of 40 | Left error → Error $ A.printError error 41 | Right payload → Ok payload.body 42 | ] 43 | Fetched result → F.noMessages $ model { result = result } 44 | 45 | view ∷ Model → Html Message 46 | view { url, result } = HE.main "main" 47 | [ HE.input [ HA.onInput UpdateUrl, HA.value url, HA.type' "text" ] 48 | , HE.button [ HA.onClick Fetch, HA.disabled $ result == Fetching ] "Fetch" 49 | , case result of 50 | NotFetched → 51 | HE.div_ "Not Fetched..." 52 | Fetching → 53 | HE.div_ "Fetching..." 54 | Ok ok → 55 | HE.pre_ <<< HE.code_ $ "Ok: " <> ok 56 | Error error → 57 | HE.div_ $ "Error: " <> error 58 | ] 59 | 60 | main ∷ Effect Unit 61 | main = F.mount_ (QuerySelector "body") 62 | { init: F.noMessages init 63 | , subscribe: [] 64 | , update 65 | , view 66 | } 67 | -------------------------------------------------------------------------------- /examples/Affjax/README.md: -------------------------------------------------------------------------------- 1 | # Flame 2 | 3 | ## AJAX example 4 | 5 | An example of how to perform AJAX requests using `affjax` and effect list update. 6 | 7 | Build it with `npm run example-affjax-list` and open affjax.html on your favorite browser. -------------------------------------------------------------------------------- /examples/Affjax/affjax.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Affjax Example 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/Affjax/affjax.js: -------------------------------------------------------------------------------- 1 | import { main } from '../../output/Examples.EffectList.Affjax.Main'; 2 | main(); -------------------------------------------------------------------------------- /examples/Counter/Counter.purs: -------------------------------------------------------------------------------- 1 | -- | Counter example using side effects free updating 2 | module Examples.NoEffects.Counter.Main where 3 | 4 | import Prelude 5 | 6 | import Effect (Effect) 7 | import Flame (QuerySelector(..), Html) 8 | import Flame.Application.NoEffects as FAN 9 | import Flame.Html.Element as HE 10 | import Flame.Html.Attribute as HA 11 | 12 | -- | The model represents the state of the app 13 | type Model = Int 14 | 15 | -- | This datatype is used to signal events to `update` 16 | data Message = Increment | Decrement 17 | 18 | -- | Initial state of the app 19 | init ∷ Model 20 | init = 0 21 | 22 | -- | `update` is called to handle events 23 | update ∷ Model → Message → Model 24 | update model = case _ of 25 | Increment → model + 1 26 | Decrement → model - 1 27 | 28 | -- | `view` updates the app markup whenever the model is updated 29 | view ∷ Model → Html Message 30 | view model = HE.main "main" 31 | [ HE.button [ HA.onClick Decrement ] "-" 32 | , HE.text $ show model 33 | , HE.button [ HA.onClick Increment ] "+" 34 | ] 35 | 36 | -- | Mount the application on the given selector 37 | main ∷ Effect Unit 38 | main = FAN.mount_ (QuerySelector "body") 39 | { init 40 | , subscribe: [] 41 | , update 42 | , view 43 | } -------------------------------------------------------------------------------- /examples/Counter/README.md: -------------------------------------------------------------------------------- 1 | # Flame 2 | 3 | ## Counter example 4 | 5 | The classical counter example built using side effects free updating. 6 | 7 | Build it with `npm run example-counter` and open counter.html on your favorite browser. -------------------------------------------------------------------------------- /examples/Counter/counter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Counter Example 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/Counter/counter.js: -------------------------------------------------------------------------------- 1 | import { main } from '../../output/Examples.NoEffects.Counter.Main'; 2 | main(); -------------------------------------------------------------------------------- /examples/Counters/Counters.purs: -------------------------------------------------------------------------------- 1 | module Examples.NoEffects.Counters.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Array ((!!)) 6 | import Data.Array as DA 7 | import Data.Maybe (Maybe(..)) 8 | import Data.Maybe as DM 9 | import Effect (Effect) 10 | import Examples.NoEffects.Counter.Main as ECM 11 | import Flame (QuerySelector(..), Html) 12 | import Flame.Application.NoEffects as FAN 13 | import Flame.Html.Attribute as HA 14 | import Flame.Html.Element as HE 15 | 16 | type Model = Array ECM.Model 17 | 18 | data Message = Add | Remove Int | CounterMessage Int ECM.Message 19 | 20 | init ∷ Model 21 | init = [] 22 | 23 | update ∷ Model → Message → Model 24 | update model = case _ of 25 | Add → DA.snoc model ECM.init 26 | Remove index → DM.fromMaybe model $ DA.deleteAt index model 27 | CounterMessage index message → 28 | case model !! index of 29 | Nothing → model 30 | Just model' → DM.fromMaybe model $ DA.updateAt index (ECM.update model' message) model 31 | 32 | view ∷ Model → Html Message 33 | view model = HE.main "main" 34 | [ HE.button [ HA.onClick Add ] "Add" 35 | , HE.div_ $ DA.mapWithIndex viewCounter model 36 | ] 37 | where 38 | viewCounter index model' = HE.div [ HA.style { display: "flex" } ] 39 | [ CounterMessage index <$> ECM.view model' 40 | , HE.button [ HA.onClick $ Remove index ] "Remove" 41 | ] 42 | 43 | main ∷ Effect Unit 44 | main = FAN.mount_ (QuerySelector "body") 45 | { init 46 | , subscribe: [] 47 | , update 48 | , view 49 | } 50 | -------------------------------------------------------------------------------- /examples/Counters/README.md: -------------------------------------------------------------------------------- 1 | # Flame 2 | 3 | ## Counters example 4 | 5 | A list of counters built using side effects free updating. 6 | 7 | Build it with `npm run example-counters` and open counters.html on your favorite browser. -------------------------------------------------------------------------------- /examples/Counters/counters.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Counters Example 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/Counters/counters.js: -------------------------------------------------------------------------------- 1 | import { main } from '../../output/Examples.NoEffects.Counters.Main'; 2 | main(); -------------------------------------------------------------------------------- /examples/Dice/Dice.purs: -------------------------------------------------------------------------------- 1 | module Examples.EffectList.Dice.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Effect (Effect) 7 | import Effect.Aff (Aff) 8 | import Effect.Class (liftEffect) 9 | import Effect.Random as ER 10 | import Flame (QuerySelector(..), Html, (:>)) 11 | import Flame as F 12 | import Flame.Html.Attribute as HA 13 | import Flame.Html.Element as HE 14 | import Data.Tuple (Tuple) 15 | 16 | type Model = Maybe Int 17 | 18 | init ∷ Model 19 | init = Nothing 20 | 21 | data Message = Roll | Update Int 22 | 23 | update ∷ Model → Message → Tuple Model (Array (Aff (Maybe Message))) 24 | update model = case _ of 25 | Roll → model :> 26 | [ Just <<< Update <$> liftEffect (ER.randomInt 1 6) 27 | ] 28 | Update int → Just int :> [] 29 | 30 | view ∷ Model → Html Message 31 | view model = HE.main "main" 32 | [ HE.text (show model) 33 | , HE.button [ HA.onClick Roll ] "Roll" 34 | ] 35 | 36 | main ∷ Effect Unit 37 | main = F.mount_ (QuerySelector "body") 38 | { init: init :> [] 39 | , subscribe: [] 40 | , update 41 | , view 42 | } 43 | -------------------------------------------------------------------------------- /examples/Dice/README.md: -------------------------------------------------------------------------------- 1 | # Flame 2 | 3 | ## Dice example 4 | 5 | A dice rolling app built using effect list updating. 6 | 7 | Build it with `npm run example-dice` and open dice.html on your favorite browser. -------------------------------------------------------------------------------- /examples/Dice/dice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dice Example 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/Dice/dice.js: -------------------------------------------------------------------------------- 1 | import { main } from '../../output/Examples.EffectList.Dice.Main'; 2 | main(); -------------------------------------------------------------------------------- /examples/EffectfulAffjax/Affjax.purs: -------------------------------------------------------------------------------- 1 | module Examples.Effectful.Affjax.Main where 2 | 3 | import Prelude 4 | 5 | import Affjax.Web as A 6 | import Affjax.ResponseFormat as AR 7 | import Data.Either (Either(..)) 8 | import Data.Maybe (Maybe(..)) 9 | import Effect (Effect) 10 | import Flame (QuerySelector(..), Html, (:>)) 11 | import Flame.Application.Effectful (AffUpdate) 12 | import Flame.Application.Effectful as FAE 13 | import Flame.Html.Attribute as HA 14 | import Flame.Html.Element as HE 15 | 16 | type Model = 17 | { url ∷ String 18 | , result ∷ Result 19 | } 20 | 21 | data Message = UpdateUrl String | Fetch 22 | 23 | data Result = NotFetched | Fetching | Ok String | Error String 24 | 25 | derive instance eqResult ∷ Eq Result 26 | 27 | init ∷ Model 28 | init = 29 | { url: "https://httpbin.org/get" 30 | , result: NotFetched 31 | } 32 | 33 | update ∷ AffUpdate Model Message 34 | update { display, model, message } = 35 | case message of 36 | UpdateUrl url → FAE.diff { url, result: NotFetched } 37 | Fetch → do 38 | display $ FAE.diff' { result: Fetching } 39 | response ← A.get AR.string model.url 40 | FAE.diff <<< { result: _ } $ case response of 41 | Left error → Error $ A.printError error 42 | Right payload → Ok payload.body 43 | 44 | view ∷ Model → Html Message 45 | view { url, result } = HE.main "main" 46 | [ HE.input [ HA.onInput UpdateUrl, HA.value url, HA.type' "text" ] 47 | , HE.button [ HA.onClick Fetch, HA.disabled $ result == Fetching ] "Fetch" 48 | , case result of 49 | NotFetched → 50 | HE.div_ "Not Fetched..." 51 | Fetching → 52 | HE.div_ "Fetching..." 53 | Ok ok → 54 | HE.pre_ <<< HE.code_ $ "Ok: " <> ok 55 | Error error → 56 | HE.div_ $ "Error: " <> error 57 | ] 58 | 59 | main ∷ Effect Unit 60 | main = FAE.mount_ (QuerySelector "body") 61 | { init: init :> Nothing 62 | , subscribe: [] 63 | , update 64 | , view 65 | } 66 | -------------------------------------------------------------------------------- /examples/EffectfulAffjax/README.md: -------------------------------------------------------------------------------- 1 | # Flame 2 | 3 | ## AJAX example 4 | 5 | An example of how to perform AJAX requests using `affjax` and effectful update. 6 | 7 | Build it with `npm run example-affjax` and open affjax.html on your favorite browser. -------------------------------------------------------------------------------- /examples/EffectfulAffjax/affjax.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Affjax Example 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/EffectfulAffjax/affjax.js: -------------------------------------------------------------------------------- 1 | import { main } from '../../output/Examples.Effectful.Affjax.Main'; 2 | main(); -------------------------------------------------------------------------------- /examples/EffectfulDice/Dice.purs: -------------------------------------------------------------------------------- 1 | module Examples.Effectful.Dice.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Effect (Effect) 7 | import Effect.Class (liftEffect) 8 | import Effect.Random as ER 9 | import Flame (QuerySelector(..), Html, (:>)) 10 | import Flame.Application.Effectful (AffUpdate) 11 | import Flame.Application.Effectful as FAE 12 | import Flame.Html.Attribute as HA 13 | import Flame.Html.Element as HE 14 | 15 | type Model = Maybe Int 16 | 17 | init ∷ Model 18 | init = Nothing 19 | 20 | data Message = Roll 21 | 22 | update ∷ AffUpdate Model Message 23 | update { model } = map (const <<< Just) $ liftEffect $ ER.randomInt 1 6 24 | 25 | view ∷ Model → Html Message 26 | view model = HE.main "main" 27 | [ HE.text (show model) 28 | , HE.button [ HA.onClick Roll ] "Roll" 29 | ] 30 | 31 | main ∷ Effect Unit 32 | main = FAE.mount_ (QuerySelector "body") 33 | { init: init :> Nothing 34 | , subscribe: [] 35 | , update 36 | , view 37 | } 38 | -------------------------------------------------------------------------------- /examples/EffectfulDice/README.md: -------------------------------------------------------------------------------- 1 | # Flame 2 | 3 | ## Dice example 4 | 5 | A dice rolling app built using effectful updating. 6 | 7 | Build it with `npm run example-dice-aff` and open dice.html on your favorite browser. -------------------------------------------------------------------------------- /examples/EffectfulDice/dice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dice Example 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/EffectfulDice/dice.js: -------------------------------------------------------------------------------- 1 | import { main } from '../../output/Examples.Effectful.Dice.Main'; 2 | main(); -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Flame 2 | 3 | Build all examples with 4 | ```bash 5 | npm run build-examples 6 | ``` 7 | or go to each individual folder to build them separately. 8 | 9 | ## Try on [Try Purescript](try.purescript.org) 10 | You can also load each example from the following urls: 11 | | Example | Link | 12 | | --------- | --------------------------------------------------------------------------------------------------------- | 13 | | Affjax | [Link](https://try.purescript.org?github=/easafe/purescript-flame/master/examples/Affjax/Affjax.purs) | 14 | | Counter | [Link](https://try.purescript.org?github=/easafe/purescript-flame/master/examples/Counter/Counter.purs) | 15 | | Dice | [Link](https://try.purescript.org?github=/easafe/purescript-flame/master/examples/Dice/Dice.purs) | 16 | | EffectfulAffjax | [Link](https://try.purescript.org?github=/easafe/purescript-flame/master/examples/EffectfulAffjax/Affjax.purs) | 17 | | EffectfulDice | [Link](https://try.purescript.org?github=/easafe/purescript-flame/master/examples/EffectfulDice/Dice.purs) | 18 | | SpecialElements | [Link](https://try.purescript.org?github=/easafe/purescript-flame/master/examples/SpecialElements/Special.purs) | 19 | | Subscriptions | [Link](https://try.purescript.org?github=/easafe/purescript-flame/master/examples/Subscriptions/Subscriptions.purs) | 20 | | Todo | [Link](https://try.purescript.org?github=/easafe/purescript-flame/master/examples/Todo/Todo.purs) | 21 | -------------------------------------------------------------------------------- /examples/ServerSideRendering/Client/ServerSideRendering.purs: -------------------------------------------------------------------------------- 1 | -- | Client side application 2 | module Examples.EffectList.ServerSideRendering.Client.Main where 3 | 4 | import Prelude 5 | 6 | import Data.Maybe (Maybe(..)) 7 | import Effect (Effect) 8 | import Effect.Aff (Aff) 9 | import Effect.Class (liftEffect) 10 | import Effect.Random as ER 11 | import Flame (QuerySelector(..), (:>)) 12 | import Flame as F 13 | import Data.Tuple (Tuple) 14 | import Examples.EffectList.ServerSideRendering.Shared (Model(..), Message(..)) 15 | import Examples.EffectList.ServerSideRendering.Shared as EESS 16 | 17 | update ∷ Model → Message → Tuple Model (Array (Aff (Maybe Message))) 18 | update m@(Model model) = case _ of 19 | Roll → m :> 20 | [ Just <<< Update <$> liftEffect (ER.randomInt 1 6) 21 | ] 22 | Update int → Model (Just int) :> [] 23 | 24 | main ∷ Effect Unit 25 | main = F.resumeMount_ (QuerySelector "#main") 26 | { init: [] 27 | , subscribe: [] 28 | , update 29 | , view: EESS.view 30 | } 31 | -------------------------------------------------------------------------------- /examples/ServerSideRendering/Client/server-side-rendering-client.js: -------------------------------------------------------------------------------- 1 | import { main } from '../../../output/Examples.EffectList.ServerSideRendering.Client.Main'; 2 | main(); -------------------------------------------------------------------------------- /examples/ServerSideRendering/README.md: -------------------------------------------------------------------------------- 1 | # Flame 2 | 3 | ## Dice example 4 | 5 | A dice app using effect list updating that is pre rendered on server-side. 6 | 7 | Build it and start a node server with `npm run server-side-rendering` and open http://localhost:3000 on your favorite browser. -------------------------------------------------------------------------------- /examples/ServerSideRendering/Server/ServerSideRendering.purs: -------------------------------------------------------------------------------- 1 | -- | Quick and dirty server, the relevant functions for example sake are serveHTML and markup 2 | module Examples.EffectList.ServerSideRendering.Server.Main where 3 | 4 | import Prelude 5 | 6 | import Data.Maybe (Maybe(..)) 7 | import Effect.Class (liftEffect) 8 | import Effect.Console as EC 9 | import Examples.EffectList.ServerSideRendering.Shared (Model(..), Message) 10 | import Examples.EffectList.ServerSideRendering.Shared as EESS 11 | import Flame (QuerySelector(..), Html) 12 | import Flame as F 13 | import Flame.Html.Attribute as HA 14 | import Flame.Html.Element as HE 15 | import HTTPure (ResponseM, ServerM, Request) 16 | import HTTPure as H 17 | import Node.FS.Aff as FSA 18 | 19 | -- | Boot up the server 20 | main ∷ ServerM 21 | main = H.serve 3000 routes $ EC.log "Server running on http://localhost:3000" 22 | 23 | routes ∷ Request → ResponseM 24 | routes { path: p } 25 | | p == [ scriptName ] = serveJavaScript 26 | | otherwise = serveHTML 27 | 28 | serveJavaScript ∷ ResponseM 29 | serveJavaScript = do 30 | contents ← FSA.readFile ("examples/ServerSideRendering/dist/" <> scriptName) 31 | H.ok' javaScriptContentType contents 32 | where 33 | javaScriptContentType = H.header "Content-Type" "text/javascript" 34 | 35 | serveHTML ∷ ResponseM 36 | serveHTML = do 37 | stringContents ← liftEffect $ F.preMount (QuerySelector "#main") { init: Model Nothing, view: markup } 38 | H.ok' htmlContentType stringContents 39 | where 40 | htmlContentType = H.header "Content-Type" "text/html" 41 | 42 | markup ∷ Model → Html Message 43 | markup model = HE.html_ 44 | [ HE.head_ 45 | [ HE.title "Server Side Rendering Dice Example" 46 | , HE.meta $ HA.charset "utf-8" 47 | ] 48 | , HE.body_ $ EESS.view model 49 | , HE.script' [ HA.type' "text/javascript", HA.src scriptName ] 50 | ] 51 | 52 | scriptName ∷ String 53 | scriptName = "server-side-rendering-client.js" -------------------------------------------------------------------------------- /examples/ServerSideRendering/Server/server-side-rendering-server.js: -------------------------------------------------------------------------------- 1 | import { main } from '../../../output/Examples.EffectList.ServerSideRendering.Server.Main/index.js'; 2 | main(); -------------------------------------------------------------------------------- /examples/ServerSideRendering/Shared/ServerSideRendering.purs: -------------------------------------------------------------------------------- 1 | -- | Definitions common code to server and client 2 | -- | 3 | -- | The same view, and model type are reused; the generic instance is necessary to serialize the inital state 4 | module Examples.EffectList.ServerSideRendering.Shared where 5 | 6 | import Prelude 7 | 8 | import Data.Generic.Rep (class Generic) 9 | import Data.Maybe (Maybe) 10 | import Flame (Html) 11 | import Flame.Html.Attribute as HA 12 | import Flame.Html.Element as HE 13 | 14 | newtype Model = Model (Maybe Int) 15 | 16 | derive instance modelGeneric ∷ Generic Model _ 17 | 18 | data Message = Roll | Update Int 19 | 20 | view ∷ Model → Html Message 21 | view (Model model) = HE.main "main" 22 | [ HE.text (show model) 23 | , HE.button [ HA.onClick Roll ] "Roll" 24 | ] -------------------------------------------------------------------------------- /examples/SpecialElements/README.md: -------------------------------------------------------------------------------- 1 | # Flame 2 | 3 | ## Special elements example 4 | 5 | A history saving dice rolling app to showcase fragments, keyed lists and lazy and managed elements. 6 | 7 | Build it with `npm run example-special` and open special.html on your favorite browser. -------------------------------------------------------------------------------- /examples/SpecialElements/Special.purs: -------------------------------------------------------------------------------- 1 | module Examples.EffectList.Special.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Array ((:)) 6 | import Data.Maybe (Maybe(..)) 7 | import Data.Tuple (Tuple) 8 | import Effect (Effect) 9 | import Effect.Aff (Aff) 10 | import Effect.Class (liftEffect) 11 | import Effect.Random as ER 12 | import Flame (QuerySelector(..), Html, (:>)) 13 | import Flame as F 14 | import Flame.Html.Attribute as HA 15 | import Flame.Html.Element as HE 16 | 17 | type Model = 18 | { current ∷ Maybe Int 19 | , history ∷ Array Int 20 | } 21 | 22 | init ∷ Model 23 | init = 24 | { current: Nothing 25 | , history: [] 26 | } 27 | 28 | data Message = Roll | Update Int 29 | 30 | update ∷ Model → Message → Tuple Model (Array (Aff (Maybe Message))) 31 | update model@{ history } = case _ of 32 | Roll → model :> 33 | [ Just <<< Update <$> liftEffect (ER.randomInt 1 6) 34 | ] 35 | Update roll → 36 | model 37 | { current = Just roll 38 | , history = roll : history 39 | } :> [] 40 | 41 | view ∷ Model → Html Message 42 | view model@{ current, history } = HE.fragment 43 | [ -- only children elements will be rendered 44 | HE.text $ show current 45 | , HE.button [ HA.onClick Roll ] "Roll" 46 | , HE.br 47 | , HE.span_ "History" 48 | , HE.div_ $ map lazyEntry history 49 | ] 50 | 51 | lazyEntry ∷ Int → Html Message 52 | lazyEntry roll = HE.lazy Nothing toEntry roll -- lazy node will only be recomputed in case the roll changes 53 | where 54 | rolled = show roll 55 | toEntry = const (HE.div (HA.key rolled) rolled) -- keyed rendering for rolls 56 | 57 | main ∷ Effect Unit 58 | main = F.mount_ (QuerySelector "body") 59 | { init: init :> [] 60 | , subscribe: [] 61 | , update 62 | , view 63 | } 64 | -------------------------------------------------------------------------------- /examples/SpecialElements/special.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Special elements Example 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/SpecialElements/special.js: -------------------------------------------------------------------------------- 1 | import { main } from '../../output/Examples.EffectList.Special.Main'; 2 | main(); -------------------------------------------------------------------------------- /examples/Subscriptions/README.md: -------------------------------------------------------------------------------- 1 | # Flame 2 | 3 | ## Subscriptions example 4 | 5 | A history saving dice rolling app to showcase fragments, keyed lists and lazy and managed elements. 6 | 7 | Build it with `npm run example-special` and open special.html on your favorite browser. -------------------------------------------------------------------------------- /examples/Subscriptions/Subscriptions.purs: -------------------------------------------------------------------------------- 1 | module Examples.EffectList.Subscriptions.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.Tuple (Tuple) 7 | import Effect (Effect) 8 | import Effect.Aff (Aff) 9 | import Effect.Class (liftEffect) 10 | import Effect.Random as ER 11 | import Effect.Timer as ET 12 | import Flame (AppId(..), Html, QuerySelector(..), Subscription, (:>)) 13 | import Flame as F 14 | import Flame.Html.Element as HE 15 | import Flame.Subscription as FS 16 | import Flame.Subscription.Document as FSD 17 | 18 | type Model = 19 | { roll ∷ Maybe Int 20 | , from ∷ String 21 | } 22 | 23 | init ∷ Model 24 | init = 25 | { roll: Nothing 26 | , from: "" 27 | } 28 | 29 | data Message 30 | = IntervalRoll 31 | | ClickRoll 32 | | Update String Int 33 | 34 | update ∷ Model → Message → Tuple Model (Array (Aff (Maybe Message))) 35 | update model = case _ of 36 | IntervalRoll → model :> next "interval" 37 | ClickRoll → model :> next "click" 38 | Update from int → 39 | { roll: Just int 40 | , from 41 | } :> [] 42 | where 43 | next from = [ Just <<< Update from <$> liftEffect (ER.randomInt 1 6) ] 44 | 45 | view ∷ Model → Html Message 46 | view { roll, from } = HE.text $ case roll of 47 | Nothing → "No rolls!" 48 | Just r → "Roll from " <> from <> ": " <> show r 49 | 50 | subscribe ∷ Array (Subscription Message) 51 | subscribe = 52 | [ FSD.onClick ClickRoll -- `document` click event 53 | ] 54 | 55 | main ∷ Effect Unit 56 | main = do 57 | let id = AppId "dice-rolling" 58 | F.mount (QuerySelector "body") id 59 | { init: init :> [] 60 | , subscribe 61 | , update 62 | , view 63 | } 64 | -- roll dice every 5 seconds 65 | void $ ET.setInterval 5000 (FS.send id IntervalRoll) 66 | -------------------------------------------------------------------------------- /examples/Subscriptions/subscriptions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dice Example 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/Subscriptions/subscriptions.js: -------------------------------------------------------------------------------- 1 | import { main } from '../../output/Examples.EffectList.Subscriptions.Main'; 2 | main(); -------------------------------------------------------------------------------- /examples/Todo/README.md: -------------------------------------------------------------------------------- 1 | # Flame 2 | 3 | ## TODO example 4 | 5 | The classical TODO example built using effect list updating. 6 | 7 | Build it with `npm run example-todo` and open todo.html on your favorite browser. -------------------------------------------------------------------------------- /examples/Todo/Todo.purs: -------------------------------------------------------------------------------- 1 | module Examples.EffectList.Todo.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Argonaut.Core as DAC 6 | import Data.Argonaut.Encode as DAE 7 | import Data.Array as DA 8 | import Data.Maybe (Maybe(..)) 9 | import Data.Maybe as DM 10 | import Data.Tuple (Tuple(..)) 11 | import Effect (Effect) 12 | import Effect.Aff (Aff) 13 | import Effect.Class (liftEffect) 14 | import Flame (QuerySelector(..), Html, Key, (:>)) 15 | import Flame as F 16 | import Flame.Html.Attribute as HA 17 | import Flame.Html.Element as HE 18 | import Web.HTML as WH 19 | import Web.HTML.Window as WHW 20 | import Web.Storage.Storage as WSS 21 | 22 | type Index = Int 23 | 24 | type Model = 25 | { input ∷ String 26 | , todos ∷ Array String 27 | , editing ∷ Index 28 | } 29 | 30 | todoLocalStorageKey ∷ String 31 | todoLocalStorageKey = "todos" 32 | 33 | notEditing ∷ Index 34 | notEditing = -1 35 | 36 | init ∷ Model 37 | init = 38 | { input: "" 39 | , todos: [] 40 | , editing: notEditing 41 | } 42 | 43 | data Message = Add (Tuple Key String) | Edit Index | Update (Tuple Key String) | Remove Index 44 | 45 | update ∷ Model → Message → Tuple Model (Array (Aff (Maybe Message))) 46 | update model message = 47 | let 48 | newModel = 49 | case message of 50 | Edit index → model { editing = index } 51 | Remove index → model { todos = DM.fromMaybe model.todos $ DA.deleteAt index model.todos } 52 | message' → saveOnEnter model message' 53 | in 54 | newModel :> [ liftEffect $ serialize newModel ] 55 | 56 | where 57 | saveOnEnter updatedModel (Add (Tuple "Enter" todo)) = updatedModel 58 | { todos = DA.snoc updatedModel.todos todo 59 | , input = "" 60 | } 61 | saveOnEnter updatedModel (Add (Tuple _ todo)) = updatedModel { input = todo } 62 | saveOnEnter updatedModel (Update (Tuple "Enter" todo)) = updatedModel 63 | { todos = DM.fromMaybe updatedModel.todos $ DA.updateAt updatedModel.editing todo updatedModel.todos 64 | , editing = notEditing 65 | } 66 | saveOnEnter updatedModel _ = updatedModel 67 | 68 | serialize updatedModel = do 69 | window ← WH.window 70 | localStorage ← WHW.localStorage window 71 | WSS.setItem todoLocalStorageKey (DAC.stringify $ DAE.encodeJson updatedModel.todos) localStorage 72 | pure Nothing 73 | 74 | view ∷ Model → Html Message 75 | view model = HE.main "main" 76 | [ HE.h1_ "todos" 77 | , HE.input [ HA.type' "text", HA.placeholder "What needs to be done?", HA.value model.input, HA.onKeyup Add ] 78 | , HE.div_ $ DA.mapWithIndex todoItem model.todos 79 | , HE.text "Double click to edit a todo" 80 | ] 81 | where 82 | todoItem index todo = HE.div_ 83 | [ if index == model.editing then 84 | HE.input [ HA.onKeyup Update, HA.value todo ] 85 | else 86 | HE.span [ HA.onDblclick (Edit index) ] todo 87 | , HE.button [ HA.onClick $ Remove index ] "remove" 88 | ] 89 | 90 | main ∷ Effect Unit 91 | main = F.mount_ (QuerySelector "body") 92 | { init: init :> [] 93 | , subscribe: [] 94 | , update 95 | , view 96 | } 97 | -------------------------------------------------------------------------------- /examples/Todo/todo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo Example 6 | 7 | 8 |
9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/Todo/todo.js: -------------------------------------------------------------------------------- 1 | import { main } from '../../output/Examples.EffectList.Todo.Main'; 2 | main(); -------------------------------------------------------------------------------- /licenses/LOADASH-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright JS Foundation and other contributors 4 | 5 | Based on Underscore.js, copyright Jeremy Ashkenas, 6 | DocumentCloud and Investigative Reporters & Editors 7 | 8 | This software consists of voluntary contributions made by many 9 | individuals. For exact contribution history, see the revision history 10 | available at https://github.com/lodash/lodash 11 | 12 | The following license applies to all parts of this software except as 13 | documented below: 14 | 15 | ==== 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining 18 | a copy of this software and associated documentation files (the 19 | "Software"), to deal in the Software without restriction, including 20 | without limitation the rights to use, copy, modify, merge, publish, 21 | distribute, sublicense, and/or sell copies of the Software, and to 22 | permit persons to whom the Software is furnished to do so, subject to 23 | the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be 26 | included in all copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 29 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 30 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 31 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 32 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 33 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 34 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 35 | 36 | ==== 37 | 38 | Copyright and related rights for sample code are waived via CC0. Sample 39 | code is defined as all source code displayed within the prose of the 40 | documentation. 41 | 42 | CC0: http://creativecommons.org/publicdomain/zero/1.0/ 43 | 44 | ==== 45 | 46 | Files located in the node_modules and vendor directories are externally 47 | maintained libraries used by this software which have their own 48 | licenses; we recommend you read them, as their terms may differ from the 49 | terms above. 50 | -------------------------------------------------------------------------------- /licenses/SNABBDOM-TO-HTML-LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /licenses/STAGE0-LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pavel Martynov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-flame", 3 | "license": "MIT", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/easafe/purescript-flame.git" 7 | }, 8 | "files": [ 9 | "package.json" 10 | ], 11 | "type": "module", 12 | "scripts": { 13 | "example-dice-aff": "parcel build examples/EffectfulDice/dice.js --dist-dir examples/EffectfulDice/dist/", 14 | "example-server-side-rendering-client": "parcel build examples/ServerSideRendering/Client/server-side-rendering-client.js --dist-dir examples/ServerSideRendering/dist", 15 | "example-server-side-rendering": "npm run example-server-side-rendering-client", 16 | "example-affjax": "parcel build examples/EffectfulAffjax/affjax.js --dist-dir examples/EffectfulAffjax/dist", 17 | "example-affjax-list": "parcel build examples/Affjax/affjax.js --dist-dir examples/Affjax/dist", 18 | "example-counter": "parcel build examples/Counter/counter.js --dist-dir examples/Counter/dist", 19 | "example-counters": "parcel build examples/Counters/counters.js --dist-dir examples/Counters/dist", 20 | "example-dice": "parcel build examples/Dice/dice.js --dist-dir examples/Dice/dist", 21 | "example-effectful-dice": "parcel build examples/EffectfulDice/dice.js --dist-dir examples/EffectfulDice/dist", 22 | "example-special": "parcel build examples/SpecialElements/special.js --dist-dir examples/SpecialElements/dist", 23 | "example-subscriptions": "parcel build examples/Subscriptions/subscriptions.js --dist-dir examples/Subscriptions/dist", 24 | "example-todo": "parcel build examples/Todo/todo.js --dist-dir examples/Todo/dist", 25 | "scratchpad": "parcel watch test/scratchpadloader.js --dist-dir test/dist", 26 | "server-side-rendering": "npm run example-server-side-rendering && node examples/ServerSideRendering/Server/server-side-rendering-server.js", 27 | "build-examples": "npm run example-affjax-list && npm run example-affjax && npm run example-counter && npm run example-counters && npm run example-dice && npm run example-todo && npm run example-special && npm run example-subscriptions && npm run example-effectful-dice", 28 | "test": "spago -x examples.dhall test && npm run build-examples" 29 | }, 30 | "devDependencies": { 31 | "jsdom": "^16.5.3", 32 | "parcel": "^2.7.0" 33 | }, 34 | "alias": { 35 | "xhr2": false 36 | } 37 | } -------------------------------------------------------------------------------- /packages.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to your new Dhall package-set! 3 | 4 | Below are instructions for how to edit this file for most use 5 | cases, so that you don't need to know Dhall to use it. 6 | 7 | ## Warning: Don't Move This Top-Level Comment! 8 | 9 | Due to how `dhall format` currently works, this comment's 10 | instructions cannot appear near corresponding sections below 11 | because `dhall format` will delete the comment. However, 12 | it will not delete a top-level comment like this one. 13 | 14 | ## Use Cases 15 | 16 | Most will want to do one or both of these options: 17 | 1. Override/Patch a package's dependency 18 | 2. Add a package not already in the default package set 19 | 20 | This file will continue to work whether you use one or both options. 21 | Instructions for each option are explained below. 22 | 23 | ### Overriding/Patching a package 24 | 25 | Purpose: 26 | - Change a package's dependency to a newer/older release than the 27 | default package set's release 28 | - Use your own modified version of some dependency that may 29 | include new API, changed API, removed API by 30 | using your custom git repo of the library rather than 31 | the package set's repo 32 | 33 | Syntax: 34 | Replace the overrides' "{=}" (an empty record) with the following idea 35 | The "//" or "⫽" means "merge these two records and 36 | when they have the same value, use the one on the right:" 37 | ------------------------------- 38 | let overrides = 39 | { packageName = 40 | upstream.packageName // { updateEntity1 = "new value", updateEntity2 = "new value" } 41 | , packageName = 42 | upstream.packageName // { version = "v4.0.0" } 43 | , packageName = 44 | upstream.packageName // { repo = "https://www.example.com/path/to/new/repo.git" } 45 | } 46 | ------------------------------- 47 | 48 | Example: 49 | ------------------------------- 50 | let overrides = 51 | { halogen = 52 | upstream.halogen // { version = "master" } 53 | , halogen-vdom = 54 | upstream.halogen-vdom // { version = "v4.0.0" } 55 | } 56 | ------------------------------- 57 | 58 | ### Additions 59 | 60 | Purpose: 61 | - Add packages that aren't already included in the default package set 62 | 63 | Syntax: 64 | Replace the additions' "{=}" (an empty record) with the following idea: 65 | ------------------------------- 66 | let additions = 67 | { package-name = 68 | { dependencies = 69 | [ "dependency1" 70 | , "dependency2" 71 | ] 72 | , repo = 73 | "https://example.com/path/to/git/repo.git" 74 | , version = 75 | "tag ('v4.0.0') or branch ('master')" 76 | } 77 | , package-name = 78 | { dependencies = 79 | [ "dependency1" 80 | , "dependency2" 81 | ] 82 | , repo = 83 | "https://example.com/path/to/git/repo.git" 84 | , version = 85 | "tag ('v4.0.0') or branch ('master')" 86 | } 87 | , etc. 88 | } 89 | ------------------------------- 90 | 91 | Example: 92 | ------------------------------- 93 | let additions = 94 | { benchotron = 95 | { dependencies = 96 | [ "arrays" 97 | , "exists" 98 | , "profunctor" 99 | , "strings" 100 | , "quickcheck" 101 | , "lcg" 102 | , "transformers" 103 | , "foldable-traversable" 104 | , "exceptions" 105 | , "node-fs" 106 | , "node-buffer" 107 | , "node-readline" 108 | , "datetime" 109 | , "now" 110 | ] 111 | , repo = 112 | "https://github.com/hdgarrood/purescript-benchotron.git" 113 | , version = 114 | "v7.0.0" 115 | } 116 | } 117 | ------------------------------- 118 | -} 119 | let upstream = 120 | https://github.com/purescript/package-sets/releases/download/psc-0.15.4-20220822/packages.dhall 121 | sha256:908b4ffbfba37a0a4edf806513a555d0dbcdd0cde7abd621f8d018d2e8ecf828 122 | 123 | let overrides = {=} 124 | 125 | let additions = {=} 126 | 127 | in upstream // overrides // additions 128 | -------------------------------------------------------------------------------- /spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "flame" 6 | , license = "MIT" 7 | , repository = "https://github.com/easafe/purescript-flame" 8 | , dependencies = 9 | [ "aff" 10 | , "argonaut-codecs" 11 | , "argonaut-core" 12 | , "argonaut-generic" 13 | , "arrays" 14 | , "bifunctors" 15 | , "console" 16 | , "effect" 17 | , "either" 18 | , "exceptions" 19 | , "foldable-traversable" 20 | , "foreign" 21 | , "foreign-object" 22 | , "maybe" 23 | , "newtype" 24 | , "nullable" 25 | , "partial" 26 | , "prelude" 27 | , "random" 28 | , "refs" 29 | , "strings" 30 | , "spec" 31 | , "tuples" 32 | , "typelevel-prelude" 33 | , "unsafe-coerce" 34 | , "web-dom" 35 | , "web-events" 36 | , "web-html" 37 | , "web-uievents" 38 | ] 39 | , packages = ./packages.dhall 40 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 41 | } 42 | -------------------------------------------------------------------------------- /src/Flame.purs: -------------------------------------------------------------------------------- 1 | -- | Entry module for a default Flame application 2 | module Flame (module Exported) where 3 | 4 | import Flame.Application.EffectList (Application, noMessages, ResumedApplication, ListUpdate, mount, mount_, resumeMount, resumeMount_) as Exported 5 | import Flame.Application.Internal.PreMount (preMount) as Exported 6 | import Flame.Types (Html, (:>), Key, PreApplication, AppId(..), Subscription) as Exported 7 | import Web.DOM.ParentNode (QuerySelector(..)) as Exported 8 | -------------------------------------------------------------------------------- /src/Flame/Application/EffectList.purs: -------------------------------------------------------------------------------- 1 | -- | The Elm like way to run a Flame application 2 | -- | 3 | -- | The update function returns an array of side effects 4 | module Flame.Application.EffectList 5 | ( ListUpdate 6 | , Application 7 | , noMessages 8 | , mount 9 | , mount_ 10 | , ResumedApplication 11 | , resumeMount 12 | , resumeMount_ 13 | ) where 14 | 15 | import Data.Either (Either(..)) 16 | import Data.Foldable as DF 17 | import Data.Maybe (Maybe(..)) 18 | import Data.Tuple (Tuple(..)) 19 | import Effect (Effect) 20 | import Effect.Aff (Aff) 21 | import Effect.Aff as EA 22 | import Effect.Console as EC 23 | import Effect.Exception as EE 24 | import Effect.Ref as ER 25 | import Flame.Application.Internal.Dom as FAD 26 | import Flame.Application.Internal.PreMount as FAP 27 | import Flame.Internal.Equality as FIE 28 | import Flame.Renderer.Internal.Dom as FRD 29 | import Flame.Serialization (class UnserializeState) 30 | import Flame.Subscription.Internal.Listener as FSIL 31 | import Flame.Types (App, AppId(..), ApplicationId, DomNode, DomRenderingState, (:>)) 32 | import Prelude (class Show, Unit, bind, discard, map, pure, show, unit, when, ($), (<>)) 33 | 34 | import Unsafe.Coerce as UC 35 | import Web.DOM.ParentNode (QuerySelector(..)) 36 | 37 | type ListUpdate model message = model → message → Tuple model (Array (Aff (Maybe message))) 38 | 39 | -- | `Application` contains 40 | -- | * `init` – the initial model and a list of messages to invoke `update` with 41 | -- | * `view` – a function to update your markup 42 | -- | * `update` – a function to update your model 43 | -- | * `subscribe` – list of external events 44 | type Application model message = App model message 45 | ( init ∷ Tuple model (Array (Aff (Maybe message))) 46 | , update ∷ ListUpdate model message 47 | ) 48 | 49 | -- | `ResumedApplication` contains 50 | -- | * `init` – initial list of messages to invoke `update` with 51 | -- | * `view` – a function to update your markup 52 | -- | * `update` – a function to update your model 53 | -- | * `subscribe` – list of external events 54 | type ResumedApplication model message = App model message 55 | ( init ∷ Array (Aff (Maybe message)) 56 | , update ∷ ListUpdate model message 57 | ) 58 | 59 | noMessages ∷ ∀ model message. model → Tuple model (Array (Aff (Maybe message))) 60 | noMessages model = model :> [] 61 | 62 | noAppId ∷ ∀ message. Maybe (AppId Unit message) 63 | noAppId = Nothing 64 | 65 | showId ∷ ∀ id message. Show id ⇒ (AppId id message) → String 66 | showId (AppId id) = show id 67 | 68 | -- | Mount a Flame application on the given selector which was rendered server-side 69 | resumeMount_ ∷ ∀ model message. UnserializeState model ⇒ QuerySelector → ResumedApplication model message → Effect Unit 70 | resumeMount_ selector = resumeMountWith selector noAppId 71 | 72 | -- | Mount on the given selector a Flame application which was rendered server-side and can be fed arbitrary external messages 73 | resumeMount ∷ ∀ id model message. UnserializeState model ⇒ Show id ⇒ QuerySelector → AppId id message → ResumedApplication model message → Effect Unit 74 | resumeMount selector appId = resumeMountWith selector (Just appId) 75 | 76 | -- | Mount on the given selector a Flame application which was rendered server-side and can be fed arbitrary external messages 77 | resumeMountWith ∷ ∀ id model message. UnserializeState model ⇒ Show id ⇒ QuerySelector → Maybe (AppId id message) → ResumedApplication model message → Effect Unit 78 | resumeMountWith (QuerySelector selector) appId { update, view, init, subscribe } = do 79 | initialModel ← FAP.serializedState selector 80 | maybeElement ← FAD.querySelector selector 81 | case maybeElement of 82 | Just parent → run parent true (map showId appId) 83 | { init: initialModel :> init 84 | , view 85 | , update 86 | , subscribe 87 | } 88 | Nothing → EE.throw $ "Error resuming application mount: no element matching selector " <> selector <> " found!" 89 | 90 | -- | Mount a Flame application on the given selector 91 | mount_ ∷ ∀ model message. QuerySelector → Application model message → Effect Unit 92 | mount_ selector = mountWith selector noAppId 93 | 94 | -- | Mount a Flame application that can be fed arbitrary external messages 95 | mount ∷ ∀ id model message. Show id ⇒ QuerySelector → AppId id message → Application model message → Effect Unit 96 | mount selector appId = mountWith selector (Just appId) 97 | 98 | mountWith ∷ ∀ id model message. Show id ⇒ QuerySelector → Maybe (AppId id message) → Application model message → Effect Unit 99 | mountWith (QuerySelector selector) appId application = do 100 | maybeElement ← FAD.querySelector selector 101 | case maybeElement of 102 | Just parent → run parent false (map showId appId) application 103 | Nothing → EE.throw $ "Error mounting application" 104 | 105 | -- | Keeps the state in a `Ref` and call `Flame.Renderer.render` for every update 106 | run ∷ ∀ model message. DomNode → Boolean → Maybe ApplicationId → Application model message → Effect Unit 107 | run parent isResumed appId { update, view, init: Tuple initialModel initialAffs, subscribe } = do 108 | modelState ← ER.new initialModel 109 | renderingState ← ER.new (UC.unsafeCoerce 21 ∷ DomRenderingState) 110 | 111 | let --the function which actually run events 112 | runUpdate message = do 113 | currentModel ← ER.read modelState 114 | let Tuple model affs = update currentModel message 115 | when (FIE.modelHasChanged currentModel model) $ render model 116 | runMessages affs 117 | 118 | runMessages affs = 119 | DF.for_ affs $ EA.runAff_ 120 | ( case _ of 121 | Left error → EC.log $ EE.message error 122 | Right (Just message) → runUpdate message 123 | _ → pure unit 124 | ) 125 | 126 | --the function which renders to the dom 127 | render model = do 128 | rendering ← ER.read renderingState 129 | FRD.resume rendering $ view model 130 | ER.write model modelState 131 | 132 | rendering ← 133 | if isResumed then 134 | FRD.startFrom parent runUpdate $ view initialModel 135 | else 136 | FRD.start parent runUpdate $ view initialModel 137 | ER.write rendering renderingState 138 | 139 | runMessages initialAffs 140 | 141 | --subscriptions are used for external events 142 | case appId of 143 | Nothing → pure unit 144 | Just id → FSIL.createMessageListener id runUpdate 145 | DF.traverse_ (FSIL.createSubscription runUpdate) subscribe -------------------------------------------------------------------------------- /src/Flame/Application/Effectful.js: -------------------------------------------------------------------------------- 1 | export function unsafeMergeFields(model) { 2 | return function (subset) { 3 | let copy = {}; 4 | 5 | for (let key of Object.keys(model)) { 6 | copy[key] = model[key]; 7 | } 8 | 9 | for (let key of Object.keys(subset)) { 10 | copy[key] = subset[key]; 11 | } 12 | 13 | return copy; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Flame/Application/Effectful.purs: -------------------------------------------------------------------------------- 1 | -- | Run a flame application with unbounded side effects 2 | -- | 3 | -- | The update function carries context information and runs on `Aff` 4 | module Flame.Application.Effectful 5 | ( Application 6 | , mount 7 | , mount_ 8 | , AffUpdate 9 | , Environment 10 | , ResumedApplication 11 | , resumeMount 12 | , resumeMount_ 13 | , noChanges 14 | , class Diff 15 | , diff' 16 | , diff 17 | ) where 18 | 19 | import Data.Either as DET 20 | import Data.Foldable as DF 21 | import Data.Maybe (Maybe(..)) 22 | import Data.Newtype (class Newtype) 23 | import Data.Newtype as DN 24 | import Data.Tuple (Tuple(..)) 25 | import Effect (Effect) 26 | import Effect.Aff (Aff) 27 | import Effect.Aff as EA 28 | import Effect.Class (liftEffect) 29 | import Effect.Console as EC 30 | import Effect.Exception as EE 31 | import Effect.Ref as ER 32 | import Flame.Application.Internal.Dom as FAD 33 | import Flame.Application.Internal.PreMount as FAP 34 | import Flame.Renderer.Internal.Dom as FRD 35 | import Flame.Serialization (class UnserializeState) 36 | import Flame.Subscription.Internal.Listener as FSIL 37 | import Flame.Types (App, AppId(..), ApplicationId, DomNode, DomRenderingState, (:>)) 38 | import Prelude (class Functor, class Show, Unit, bind, discard, flip, identity, map, pure, show, unit, ($), (<<<), (<>)) 39 | import Prim.Row (class Union, class Nub) 40 | import Unsafe.Coerce as UC 41 | import Web.DOM.ParentNode (QuerySelector(..)) 42 | 43 | foreign import unsafeMergeFields ∷ ∀ model subset. Record model → Record subset → Record model 44 | 45 | type AffUpdate model message = Environment model message → Aff (model → model) 46 | 47 | -- | `Application` contains 48 | -- | * `init` – the initial model and an optional message to invoke `update` with 49 | -- | * `view` – a function to update your markup 50 | -- | * `update` – a function to update your model 51 | -- | * `subscribe` – list of external events 52 | type Application model message = App model message 53 | ( init ∷ Tuple model (Maybe message) 54 | , update ∷ AffUpdate model message 55 | ) 56 | 57 | -- | `ResumedApplication` contains 58 | -- | * `init` – initial list of messages to invoke `update` with 59 | -- | * `view` – a function to update your markup 60 | -- | * `update` – a function to update your model 61 | -- | * `subscribe` – list of external events 62 | type ResumedApplication model message = App model message 63 | ( init ∷ Maybe message 64 | , update ∷ AffUpdate model message 65 | ) 66 | 67 | -- | `Environment` contains context information for `Application.update` 68 | -- | * `model` – the current model 69 | -- | * `message` – the current message 70 | -- | * `view` – forcefully update view with given model changes 71 | type Environment model message = 72 | { model ∷ model 73 | , message ∷ message 74 | , display ∷ (model → model) → Aff Unit 75 | } 76 | 77 | -- | Convenience type class to update only the given fields of a model 78 | class Diff changed model where 79 | diff' ∷ changed → (model → model) 80 | 81 | instance recordDiff ∷ (Union changed t model, Nub changed c) ⇒ Diff (Record changed) (Record model) where 82 | diff' changed = \model → unsafeMergeFields model changed 83 | else instance functorRecordDiff ∷ (Functor f, Union changed t model, Nub changed c) ⇒ Diff (Record changed) (f (Record model)) where 84 | diff' changed = map (flip unsafeMergeFields changed) 85 | else instance newtypeRecordDiff ∷ (Newtype newtypeModel (Record model), Union changed t model, Nub changed c) ⇒ Diff (Record changed) newtypeModel where 86 | diff' changed = \model → DN.wrap $ unsafeMergeFields (DN.unwrap model) changed 87 | 88 | -- | Wraps diff' in Aff 89 | diff ∷ ∀ changed model. Diff changed model ⇒ changed → Aff (model → model) 90 | diff = pure <<< diff' 91 | 92 | noChanges ∷ ∀ model. Aff (model → model) 93 | noChanges = pure identity 94 | 95 | noAppId ∷ ∀ message. Maybe (AppId Unit message) 96 | noAppId = Nothing 97 | 98 | showId ∷ ∀ id message. Show id ⇒ (AppId id message) → String 99 | showId (AppId id) = show id 100 | 101 | -- | Mount a Flame application on the given selector which was rendered server-side 102 | resumeMount_ ∷ ∀ model message. UnserializeState model ⇒ QuerySelector → ResumedApplication model message → Effect Unit 103 | resumeMount_ selector = resumeMountWith selector noAppId 104 | 105 | -- | Mount on the given selector a Flame application which was rendered server-side and can be fed arbitrary external messages 106 | resumeMount ∷ ∀ id model message. UnserializeState model ⇒ Show id ⇒ QuerySelector → AppId id message → ResumedApplication model message → Effect Unit 107 | resumeMount selector appId = resumeMountWith selector (Just appId) 108 | 109 | -- | Mount on the given selector a Flame application which was rendered server-side and can be fed arbitrary external messages 110 | resumeMountWith ∷ ∀ id model message. UnserializeState model ⇒ Show id ⇒ QuerySelector → Maybe (AppId id message) → ResumedApplication model message → Effect Unit 111 | resumeMountWith (QuerySelector selector) appId { update, view, init, subscribe } = do 112 | initialModel ← FAP.serializedState selector 113 | maybeElement ← FAD.querySelector selector 114 | case maybeElement of 115 | Just parent → run parent true (map showId appId) 116 | { init: initialModel :> init 117 | , view 118 | , update 119 | , subscribe 120 | } 121 | Nothing → EE.throw $ "Error resuming application mount: no element matching selector " <> selector <> " found!" 122 | 123 | -- | Mount a Flame application on the given selector 124 | mount_ ∷ ∀ model message. QuerySelector → Application model message → Effect Unit 125 | mount_ selector = mountWith selector noAppId 126 | 127 | -- | Mount a Flame application that can be fed arbitrary external messages 128 | mount ∷ ∀ id model message. Show id ⇒ QuerySelector → AppId id message → Application model message → Effect Unit 129 | mount selector appId = mountWith selector (Just appId) 130 | 131 | mountWith ∷ ∀ id model message. Show id ⇒ QuerySelector → Maybe (AppId id message) → Application model message → Effect Unit 132 | mountWith (QuerySelector selector) appId application = do 133 | maybeElement ← FAD.querySelector selector 134 | case maybeElement of 135 | Just parent → run parent false (map showId appId) application 136 | Nothing → EE.throw $ "Error mounting application" 137 | 138 | -- | `run` keeps the state in a `Ref` and call `Flame.Renderer.Internal.Dom.render` for every update 139 | run ∷ ∀ model message. DomNode → Boolean → Maybe ApplicationId → Application model message → Effect Unit 140 | run parent isResumed appId { init: Tuple initialModel initialMessage, update, view, subscribe } = do 141 | modelState ← ER.new initialModel 142 | renderingState ← ER.new (UC.unsafeCoerce 21 ∷ DomRenderingState) 143 | 144 | let --the function which actually run events 145 | runUpdate message = do 146 | model ← ER.read modelState 147 | EA.runAff_ (DET.either (EC.log <<< EE.message) render) $ update { display: renderFromUpdate, model, message } 148 | 149 | --the function which renders to the dom 150 | render recordUpdate = do 151 | model ← ER.read modelState 152 | rendering ← ER.read renderingState 153 | let updatedModel = recordUpdate model 154 | FRD.resume rendering $ view updatedModel 155 | ER.write updatedModel modelState 156 | 157 | --the function used to arbitraly render the view from inside Environment.update 158 | renderFromUpdate recordUpdate = liftEffect $ render recordUpdate 159 | 160 | rendering ← 161 | if isResumed then 162 | FRD.startFrom parent runUpdate $ view initialModel 163 | else 164 | FRD.start parent runUpdate $ view initialModel 165 | ER.write rendering renderingState 166 | 167 | case initialMessage of 168 | Nothing → pure unit 169 | Just message → runUpdate message 170 | 171 | --subscriptions are used for external events 172 | case appId of 173 | Nothing → pure unit 174 | Just id → FSIL.createMessageListener id runUpdate 175 | DF.traverse_ (FSIL.createSubscription runUpdate) subscribe -------------------------------------------------------------------------------- /src/Flame/Application/Internal/Dom.js: -------------------------------------------------------------------------------- 1 | 2 | export function querySelector_(selector) { 3 | return document.querySelector(selector); 4 | } 5 | 6 | export function textContent_(element) { 7 | return element.textContent || ''; 8 | } 9 | 10 | export function removeElement_(selector) { 11 | document.querySelector(selector).remove(); 12 | } 13 | 14 | export function createWindowListener_(eventName, updater) { 15 | window.addEventListener(eventName, function(event) { 16 | updater(event)(); 17 | }); 18 | } 19 | 20 | export function createDocumentListener_(eventName, updater) { 21 | document.addEventListener(eventName, function(event) { 22 | updater(event)(); 23 | }); 24 | } 25 | 26 | export function createCustomListener_(eventName, updater) { 27 | document.addEventListener(eventName, function (event) { 28 | updater(event.detail)(); 29 | }); 30 | } 31 | 32 | export function dispatchCustomEvent_(eventName, payload) { 33 | document.dispatchEvent(new CustomEvent(eventName, { detail: payload } )); 34 | } -------------------------------------------------------------------------------- /src/Flame/Application/Internal/Dom.purs: -------------------------------------------------------------------------------- 1 | module Flame.Application.Internal.Dom 2 | ( querySelector 3 | , textContent 4 | , removeElement 5 | , createWindowListener 6 | , createDocumentListener 7 | , createCustomListener 8 | , dispatchCustomEvent 9 | ) where 10 | 11 | import Prelude 12 | 13 | import Data.Maybe (Maybe) 14 | import Data.Nullable (Nullable) 15 | import Data.Nullable as DN 16 | import Effect (Effect) 17 | import Effect.Uncurried (EffectFn1, EffectFn2) 18 | import Effect.Uncurried as EU 19 | import Flame.Types (DomNode, EventName) 20 | import Foreign (Foreign) 21 | 22 | foreign import querySelector_ ∷ EffectFn1 String (Nullable DomNode) 23 | foreign import textContent_ ∷ EffectFn1 DomNode String 24 | foreign import removeElement_ ∷ EffectFn1 String Unit 25 | foreign import createWindowListener_ ∷ EffectFn2 EventName (Foreign → Effect Unit) Unit 26 | foreign import createDocumentListener_ ∷ EffectFn2 EventName (Foreign → Effect Unit) Unit 27 | foreign import createCustomListener_ ∷ EffectFn2 EventName (Foreign → Effect Unit) Unit 28 | foreign import dispatchCustomEvent_ ∷ ∀ message. EffectFn2 EventName message Unit 29 | 30 | querySelector ∷ String → Effect (Maybe DomNode) 31 | querySelector selector = do 32 | selected ← EU.runEffectFn1 querySelector_ selector 33 | pure $ DN.toMaybe selected 34 | 35 | textContent ∷ DomNode → Effect String 36 | textContent = EU.runEffectFn1 textContent_ 37 | 38 | removeElement ∷ String → Effect Unit 39 | removeElement = EU.runEffectFn1 removeElement_ 40 | 41 | createWindowListener ∷ EventName → (Foreign → Effect Unit) → Effect Unit 42 | createWindowListener = EU.runEffectFn2 createWindowListener_ 43 | 44 | createDocumentListener ∷ EventName → (Foreign → Effect Unit) → Effect Unit 45 | createDocumentListener = EU.runEffectFn2 createDocumentListener_ 46 | 47 | createCustomListener ∷ EventName → (Foreign → Effect Unit) → Effect Unit 48 | createCustomListener = EU.runEffectFn2 createCustomListener_ 49 | 50 | dispatchCustomEvent ∷ ∀ message. EventName → message → Effect Unit 51 | dispatchCustomEvent = EU.runEffectFn2 dispatchCustomEvent_ -------------------------------------------------------------------------------- /src/Flame/Application/Internal/PreMount.js: -------------------------------------------------------------------------------- 1 | let textNode = 1, 2 | // elementNode = 2, 3 | // svgNode = 3, 4 | fragmentNode = 4, 5 | lazyNode = 5; 6 | 7 | export function injectState(stateHtml) { 8 | return function (html) { 9 | return injectTo(stateHtml, html); 10 | }; 11 | } 12 | 13 | function injectTo(stateHtml, html) { 14 | switch (html.nodeType) { 15 | case lazyNode: 16 | html.rendered = html.render(html.arg); 17 | html.render = undefined; 18 | 19 | return injectTo(stateHtml, html.rendered); 20 | case textNode: 21 | return { 22 | nodeType: fragmentNode, 23 | children: [stateHtml, html] 24 | }; 25 | case fragmentNode: 26 | html.children.unshift(stateHtml); 27 | 28 | return html; 29 | default: 30 | if (html.children === undefined) 31 | html.children = []; 32 | //if the view is a complete page, the state has to be added to the body 33 | // otherwise it won't show up on the final markup 34 | if (html.tag === "html") 35 | for (let c of html.children) { 36 | if (c.tag === "body") { 37 | injectTo(stateHtml, c); 38 | break; 39 | } 40 | } 41 | else 42 | html.children.unshift(stateHtml); 43 | 44 | return html; 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/Flame/Application/Internal/PreMount.purs: -------------------------------------------------------------------------------- 1 | module Flame.Application.Internal.PreMount where 2 | 3 | import Data.Either (Either(..)) 4 | import Data.Maybe (Maybe(..)) 5 | import Data.String.Regex as DSR 6 | import Data.String.Regex.Flags (global) 7 | import Data.String.Regex.Unsafe as DSRU 8 | import Effect (Effect) 9 | import Effect.Exception as EE 10 | import Flame.Application.Internal.Dom as FAD 11 | import Flame.Html.Attribute as HA 12 | import Flame.Html.Element as HE 13 | import Flame.Renderer.String as FRS 14 | import Flame.Serialization (class UnserializeState, class SerializeState) 15 | import Flame.Serialization as FS 16 | import Flame.Types (Html, PreApplication) 17 | import Prelude (bind, discard, pure, ($), (<>)) 18 | import Web.DOM.ParentNode (QuerySelector(..)) 19 | 20 | foreign import injectState ∷ ∀ message. Html message → Html message → Html message 21 | 22 | tagSerializedState ∷ String 23 | tagSerializedState = "template-state" 24 | 25 | idSerializedState ∷ String → String 26 | idSerializedState = ("pre-mount-" <> _) 27 | 28 | attributeSerializedState ∷ String → String 29 | attributeSerializedState = ("__pre-mount-" <> _) 30 | 31 | onlyLetters ∷ String → String 32 | onlyLetters = DSR.replace (DSRU.unsafeRegex "[^aA-zZ]" global) "" 33 | 34 | selectorSerializedState ∷ String → String 35 | selectorSerializedState selector = tagSerializedState <> "#" <> idSerializedState selector <> "[" <> attributeSerializedState selector <> "=" <> selector <> "]" 36 | 37 | serializedState ∷ ∀ model. UnserializeState model ⇒ String → Effect model 38 | serializedState selector = do 39 | maybeElement ← FAD.querySelector stateSelector 40 | case maybeElement of 41 | Just el → do 42 | contents ← FAD.textContent el 43 | case FS.unserialize contents of 44 | Right model → do 45 | FAD.removeElement stateSelector 46 | pure model 47 | Left message → EE.throw $ "Error resuming application mount: serialized state is invalid! " <> message 48 | Nothing → EE.throw $ "Error resuming application mount: serialized state (" <> stateSelector <> ") not found!" 49 | where 50 | stateSelector = selectorSerializedState $ onlyLetters selector 51 | 52 | preMount ∷ ∀ model message. SerializeState model ⇒ QuerySelector → PreApplication model message → Effect String 53 | preMount (QuerySelector selector) application = do 54 | let html = injectState state $ application.view application.init 55 | FRS.render html 56 | where 57 | sanitizedSelector = onlyLetters selector 58 | state = 59 | HE.createElement tagSerializedState 60 | [ HA.style { display: "none" } 61 | , HA.id $ idSerializedState sanitizedSelector 62 | , HA.createAttribute (attributeSerializedState sanitizedSelector) sanitizedSelector 63 | ] $ FS.serialize application.init -------------------------------------------------------------------------------- /src/Flame/Application/NoEffects.purs: -------------------------------------------------------------------------------- 1 | -- | Run a Flame application without side effects 2 | -- | 3 | -- | The update function is a pure function from model and message raised 4 | module Flame.Application.NoEffects 5 | ( Application 6 | , mount 7 | , mount_ 8 | , ResumedApplication 9 | , resumeMount 10 | , resumeMount_ 11 | ) where 12 | 13 | import Effect (Effect) 14 | import Flame.Application.EffectList as FAE 15 | import Flame.Serialization (class UnserializeState) 16 | import Flame.Types (App, AppId, (:>)) 17 | import Prelude (class Show, Unit, ($), (<<<)) 18 | 19 | import Web.DOM.ParentNode (QuerySelector) 20 | 21 | -- | `Application` contains 22 | -- | * `init` – the initial model 23 | -- | * `view` – a function to update your markup 24 | -- | * `update` – a function to update your model 25 | -- | * `subscribe` – list of external events 26 | type Application model message = App model message 27 | ( init ∷ model 28 | , update ∷ model → message → model 29 | ) 30 | 31 | -- | `ResumedApplication` contains 32 | -- | * `view` – a function to update your markup 33 | -- | * `update` – a function to update your model 34 | -- | * `subscribe` – list of external events 35 | type ResumedApplication model message = App model message 36 | ( update ∷ model → message → model 37 | ) 38 | 39 | -- | Mount a Flame application on the given selector which was rendered server-side 40 | resumeMount_ ∷ ∀ model message. UnserializeState model ⇒ QuerySelector → ResumedApplication model message → Effect Unit 41 | resumeMount_ selector = FAE.resumeMount_ selector <<< toResumedApplication 42 | 43 | -- | Mount on the given selector a Flame application which was rendered server-side and can be fed arbitrary external messages 44 | resumeMount ∷ ∀ id model message. UnserializeState model ⇒ Show id ⇒ QuerySelector → AppId id message → ResumedApplication model message → Effect Unit 45 | resumeMount selector appId = FAE.resumeMount selector appId <<< toResumedApplication 46 | 47 | toResumedApplication ∷ ∀ model message. ResumedApplication model message → FAE.ResumedApplication model message 48 | toResumedApplication { update, view, subscribe } = 49 | { init: [] 50 | , update: \model message → update model message :> [] 51 | , view 52 | , subscribe 53 | } 54 | 55 | -- | Mount a Flame application that can be fed arbitrary external messages 56 | mount ∷ ∀ id model message. Show id ⇒ QuerySelector → AppId id message → Application model message → Effect Unit 57 | mount selector appId application = FAE.mount selector appId $ application 58 | { init = application.init :> [] 59 | , update = \model message → application.update model message :> [] 60 | } 61 | 62 | -- | Mount a Flame application on the given selector, discarding the message Channel 63 | mount_ ∷ ∀ model message. QuerySelector → Application model message → Effect Unit 64 | mount_ selector application = FAE.mount_ selector $ application 65 | { init = application.init :> [] 66 | , update = \model message → application.update model message :> [] 67 | } -------------------------------------------------------------------------------- /src/Flame/Html/Attribute.purs: -------------------------------------------------------------------------------- 1 | -- | Convenience module to simplify export list 2 | module Flame.Html.Attribute (module Exported) where 3 | 4 | import Flame.Html.Attribute.Internal 5 | ( class ToClassList 6 | , class ToStyleList 7 | , ToBooleanAttribute 8 | , ToIntAttribute 9 | , ToNumberAttribute 10 | , ToStringAttribute 11 | , accentHeight 12 | , accept 13 | , acceptCharset 14 | , accessKey 15 | , accumulate 16 | , action 17 | , additive 18 | , align 19 | , alignmentBaseline 20 | , alt 21 | , ascent 22 | , autocomplete 23 | , autofocus 24 | , autoplay 25 | , azimuth 26 | , baseFrequency 27 | , baseProfile 28 | , baselineShift 29 | , begin 30 | , bias 31 | , calcMode 32 | , charset 33 | , checked 34 | , class' 35 | , clipPathAttr 36 | , clipPathUnits 37 | , clipRule 38 | , color 39 | , colorInterpolation 40 | , colorInterpolationFilters 41 | , colorProfileAttr 42 | , colorRendering 43 | , cols 44 | , colspan 45 | , content 46 | , contentEditable 47 | , contentScriptType 48 | , contentStyleType 49 | , contextmenu 50 | , controls 51 | , coords 52 | , createAttribute 53 | , createAttributeName 54 | , createAttributeType 55 | , createProperty 56 | , cursorAttr 57 | , cx 58 | , cy 59 | , d 60 | , datetime 61 | , default 62 | , diffuseConstant 63 | , dir 64 | , direction 65 | , disabled 66 | , display 67 | , divisor 68 | , dominantBaseline 69 | , download 70 | , downloadAs 71 | , draggable 72 | , dropzone 73 | , dur 74 | , dx 75 | , dy 76 | , edgeMode 77 | , elevation 78 | , enctype 79 | , end 80 | , externalResourcesRequired 81 | , fill 82 | , fillOpacity 83 | , fillRule 84 | , filterAttr 85 | , filterUnits 86 | , floodColor 87 | , floodOpacity 88 | , fontFamily 89 | , fontSize 90 | , fontSizeAdjust 91 | , fontStretch 92 | , fontStyle 93 | , fontVariant 94 | , fontWeight 95 | , for 96 | , fr 97 | , from 98 | , fx 99 | , fy 100 | , gradientTransform 101 | , gradientUnits 102 | , headers 103 | , height 104 | , hidden 105 | , href 106 | , hreflang 107 | , id 108 | , imageRendering 109 | , in' 110 | , in2 111 | , isMap 112 | , itemprop 113 | , k1 114 | , k2 115 | , k3 116 | , k4 117 | , kernelMatrix 118 | , kernelUnitLength 119 | , key 120 | , kerning 121 | , keySplines 122 | , keyTimes 123 | , kind 124 | , lang 125 | , lengthAdjust 126 | , letterSpacing 127 | , lightingColor 128 | , limitingConeAngle 129 | , list 130 | , local 131 | , loop 132 | , manifest 133 | , markerEnd 134 | , markerHeight 135 | , markerMid 136 | , markerStart 137 | , markerUnits 138 | , markerWidth 139 | , maskAttr 140 | , maskContentUnits 141 | , maskUnits 142 | , max 143 | , maxlength 144 | , media 145 | , method 146 | , min 147 | , minlength 148 | , mode 149 | , multiple 150 | , name 151 | , noValidate 152 | , numOctaves 153 | , opacity 154 | , operator 155 | , order 156 | , overflow 157 | , overlinePosition 158 | , overlineThickness 159 | , paintOrder 160 | , pathLength 161 | , pattern 162 | , patternContentUnits 163 | , patternTransform 164 | , patternUnits 165 | , ping 166 | , placeholder 167 | , pointerEvents 168 | , points 169 | , pointsAtX 170 | , pointsAtY 171 | , pointsAtZ 172 | , poster 173 | , preload 174 | , preserveAlpha 175 | , preserveAspectRatio 176 | , primitiveUnits 177 | , pubdate 178 | , r 179 | , radius 180 | , readOnly 181 | , refX 182 | , refY 183 | , rel 184 | , repeatCount 185 | , repeatDur 186 | , required 187 | , requiredFeatures 188 | , restart 189 | , result 190 | , reversed 191 | , rows 192 | , rowspan 193 | , rx 194 | , ry 195 | , sandbox 196 | , scale 197 | , scope 198 | , seed 199 | , selected 200 | , shape 201 | , shapeRendering 202 | , size 203 | , specularConstant 204 | , specularExponent 205 | , spellcheck 206 | , src 207 | , srcdoc 208 | , srclang 209 | , start 210 | , stdDeviation 211 | , step 212 | , stitchTiles 213 | , stopColor 214 | , stopOpacity 215 | , strikethroughPosition 216 | , strikethroughThickness 217 | , stroke 218 | , strokeDasharray 219 | , strokeDashoffset 220 | , strokeLinecap 221 | , strokeLinejoin 222 | , strokeMiterlimit 223 | , strokeOpacity 224 | , strokeWidth 225 | , style 226 | , style1 227 | , styleAttr 228 | , surfaceScale 229 | , tabindex 230 | , target 231 | , targetX 232 | , targetY 233 | , textAnchor 234 | , textDecoration 235 | , textLength 236 | , textRendering 237 | , title 238 | , to 239 | , toStyleList 240 | , transform 241 | , type' 242 | , underlinePosition 243 | , underlineThickness 244 | , useMap 245 | , value 246 | , values 247 | , vectorEffect 248 | , version 249 | , viewBox 250 | , visibility 251 | , width 252 | , wordSpacing 253 | , wrap 254 | , writingMode 255 | , x 256 | , x1 257 | , x2 258 | , xChannelSelector 259 | , y 260 | , y1 261 | , y2 262 | , yChannelSelector 263 | , innerHtml 264 | ) as Exported 265 | import Flame.Html.Event (EventName, ToEvent, ToRawEvent, ToMaybeEvent, ToSpecialEvent, createEvent, createEventMessage, createRawEvent, onBlur, onBlur', onCheck, onClick, onClick', onChange, onChange', onContextmenu, onContextmenu', onDblclick, onDblclick', onDrag, onDrag', onDragend, onDragend', onDragenter, onDragenter', onDragleave, onDragleave', onDragover, onDragover', onDragstart, onDragstart', onDrop, onDrop', onError, onError', onFocus, onFocus', onFocusin, onFocusin', onFocusout, onFocusout', onInput, onInput', onKeydown, onKeydown', onKeypress, onKeypress', onKeyup, onKeyup', onMousedown, onMousedown', onMouseenter, onMouseenter', onMouseleave, onMouseleave', onMousemove, onMousemove', onMouseout, onMouseout', onMouseover, onMouseover', onMouseup, onMouseup', onReset, onReset', onScroll, onScroll', onSelect, onSelect', onSubmit, onSubmit', onLoad, onLoad', onUnload, onUnload', onWheel, onWheel') as Exported 266 | -------------------------------------------------------------------------------- /src/Flame/Html/Attribute/Event.js: -------------------------------------------------------------------------------- 1 | let messageEventData = 5, 2 | rawEventData = 6; 3 | 4 | export function createEvent_(name) { 5 | return function (message) { 6 | return [messageEventData, name, message]; 7 | }; 8 | } 9 | 10 | export function createRawEvent_(name) { 11 | return function (handler) { 12 | return [rawEventData, name, handler]; 13 | }; 14 | } 15 | 16 | export function nodeValue_(event) { 17 | if (event.target.contentEditable === true || event.target.contentEditable === "true" || event.target.contentEditable === "") 18 | return event.target.innerText; 19 | 20 | return event.target.value; 21 | } 22 | 23 | export function checkedValue_(event) { 24 | if (event.target.tagName === "INPUT" && (event.target.type === "checkbox" || event.target.type === "radio")) 25 | return event.target.checked; 26 | 27 | return false; 28 | } 29 | 30 | export function preventDefault_(event) { 31 | event.preventDefault(); 32 | } 33 | 34 | export function key_(event) { 35 | if (event.type === "keyup" || event.type === "keydown" || event.type === "keypress") 36 | return event.key; 37 | 38 | return ""; 39 | } 40 | 41 | export function selection_(event) { 42 | if (event.target.tagName === "INPUT" && event.target.type == "text" || event.target.tagName === "TEXTAREA") 43 | return event.target.value.substring(event.target.selectionStart, event.target.selectionEnd); 44 | 45 | return ""; 46 | } 47 | -------------------------------------------------------------------------------- /src/Flame/Html/Attribute/Event.purs: -------------------------------------------------------------------------------- 1 | -- | Definition of HTML events that can be fired from views 2 | module Flame.Html.Event (EventName, ToEvent, ToRawEvent, ToMaybeEvent, ToSpecialEvent, createEvent, createEventMessage, createRawEvent, onBlur, onBlur', onCheck, onClick, onClick', onChange, onChange', onContextmenu, onContextmenu', onDblclick, onDblclick', onDrag, onDrag', onDragend, onDragend', onDragenter, onDragenter', onDragleave, onDragleave', onDragover, onDragover', onDragstart, onDragstart', onDrop, onDrop', onError, onError', onFocus, onFocus', onFocusin, onFocusin', onFocusout, onFocusout', onInput, onInput', onKeydown, onKeydown', onKeypress, onKeypress', onKeyup, onKeyup', onMousedown, onMousedown', onMouseenter, onMouseenter', onMouseleave, onMouseleave', onMousemove, onMousemove', onMouseout, onMouseout', onLoad, onLoad', onUnload, onUnload', onMouseover, onMouseover', onMouseup, onMouseup', onReset, onReset', onScroll, onScroll', onSelect, onSelect', onSubmit, onSubmit', onWheel, onWheel') where 3 | 4 | import Prelude 5 | 6 | import Data.Maybe (Maybe(..)) 7 | import Data.Tuple (Tuple(..)) 8 | import Effect (Effect) 9 | import Effect.Uncurried (EffectFn1) 10 | import Effect.Uncurried as FU 11 | import Flame.Types (NodeData, Key) 12 | import Web.Event.Event (Event) 13 | 14 | type EventName = String 15 | 16 | type ToEvent message = message → NodeData message 17 | 18 | type ToRawEvent message = (Event → message) → NodeData message 19 | 20 | type ToMaybeEvent message = (Event → Maybe message) → NodeData message 21 | 22 | type ToSpecialEvent message t = (t → message) → NodeData message 23 | 24 | --this way we dont need to worry about every possible element type 25 | foreign import nodeValue_ ∷ EffectFn1 Event String 26 | foreign import checkedValue_ ∷ EffectFn1 Event Boolean 27 | foreign import preventDefault_ ∷ EffectFn1 Event Unit 28 | foreign import key_ ∷ EffectFn1 Event Key 29 | foreign import selection_ ∷ EffectFn1 Event String 30 | foreign import createEvent_ ∷ ∀ message. EventName → message → (NodeData message) 31 | foreign import createRawEvent_ ∷ ∀ message. EventName → (Event → Effect (Maybe message)) → (NodeData message) 32 | 33 | nodeValue ∷ Event → Effect String 34 | nodeValue = FU.runEffectFn1 nodeValue_ 35 | 36 | checkedValue ∷ Event → Effect Boolean 37 | checkedValue = FU.runEffectFn1 checkedValue_ 38 | 39 | preventDefault ∷ Event → Effect Unit 40 | preventDefault = FU.runEffectFn1 preventDefault_ 41 | 42 | key ∷ Event → Effect String 43 | key = FU.runEffectFn1 key_ 44 | 45 | selection ∷ Event → Effect String 46 | selection = FU.runEffectFn1 selection_ 47 | 48 | -- | Raises the given `message` for the event 49 | createEvent ∷ ∀ message. EventName → message → NodeData message 50 | createEvent name message = createEvent_ name message 51 | 52 | -- | Raises the given `message` for the given event, but also supplies the event itself 53 | createRawEvent ∷ ∀ message. EventName → (Event → Effect (Maybe message)) → NodeData message 54 | createRawEvent name handler = createRawEvent_ name handler 55 | 56 | -- | Helper for `message`s that expect an event 57 | createEventMessage ∷ ∀ message. EventName → (Event → message) → NodeData message 58 | createEventMessage eventName constructor = createRawEvent eventName (pure <<< Just <<< constructor) 59 | 60 | onScroll ∷ ∀ message. ToEvent message 61 | onScroll = createEvent "scroll" 62 | 63 | onScroll' ∷ ∀ message. ToRawEvent message 64 | onScroll' = createEventMessage "scroll" 65 | 66 | onClick ∷ ∀ message. ToEvent message 67 | onClick = createEvent "click" 68 | 69 | onClick' ∷ ∀ message. ToRawEvent message 70 | onClick' = createEventMessage "click" 71 | 72 | onLoad ∷ ∀ message. ToEvent message 73 | onLoad = createEvent "load" 74 | 75 | onLoad' ∷ ∀ message. ToRawEvent message 76 | onLoad' = createEventMessage "load" 77 | 78 | onUnload ∷ ∀ message. ToEvent message 79 | onUnload = createEvent "unload" 80 | 81 | onUnload' ∷ ∀ message. ToRawEvent message 82 | onUnload' = createEventMessage "unload" 83 | 84 | onChange ∷ ∀ message. ToEvent message 85 | onChange = createEvent "change" 86 | 87 | onChange' ∷ ∀ message. ToRawEvent message 88 | onChange' = createEventMessage "change" 89 | 90 | -- | This event fires when the value of an input, select, textarea, contenteditable or designMode on elements changes 91 | onInput ∷ ∀ message. ToSpecialEvent message String 92 | onInput constructor = createRawEvent "input" handler 93 | where 94 | handler event = Just <<< constructor <$> nodeValue event 95 | 96 | onInput' ∷ ∀ message. ToRawEvent message 97 | onInput' = createEventMessage "input" 98 | 99 | -- | Helper for `input` event of checkboxes and radios 100 | onCheck ∷ ∀ message. ToSpecialEvent message Boolean 101 | onCheck constructor = createRawEvent "input" handler 102 | where 103 | handler event = Just <<< constructor <$> checkedValue event 104 | 105 | onSubmit ∷ ∀ message. ToEvent message 106 | onSubmit message = createRawEvent "submit" handler 107 | where 108 | handler event = do 109 | preventDefault event 110 | pure $ Just message 111 | 112 | onSubmit' ∷ ∀ message. ToRawEvent message 113 | onSubmit' constructor = createRawEvent "submit" handler 114 | where 115 | handler event = do 116 | preventDefault event 117 | pure <<< Just $ constructor event 118 | 119 | onFocus ∷ ∀ message. ToEvent message 120 | onFocus = createEvent "focus" 121 | 122 | onFocus' ∷ ∀ message. ToRawEvent message 123 | onFocus' = createEventMessage "focus" 124 | 125 | onFocusin ∷ ∀ message. ToEvent message 126 | onFocusin = createEvent "focusin" 127 | 128 | onFocusin' ∷ ∀ message. ToRawEvent message 129 | onFocusin' = createEventMessage "focusin" 130 | 131 | onFocusout ∷ ∀ message. ToEvent message 132 | onFocusout = createEvent "focusout" 133 | 134 | onFocusout' ∷ ∀ message. ToRawEvent message 135 | onFocusout' = createEventMessage "focusout" 136 | 137 | onBlur ∷ ∀ message. ToEvent message 138 | onBlur = createEvent "blur" 139 | 140 | onBlur' ∷ ∀ message. ToRawEvent message 141 | onBlur' = createEventMessage "blur" 142 | 143 | onReset ∷ ∀ message. ToEvent message 144 | onReset = createEvent "reset" 145 | 146 | onReset' ∷ ∀ message. ToRawEvent message 147 | onReset' = createEventMessage "reset" 148 | 149 | onKeydown ∷ ∀ message. ToSpecialEvent message (Tuple Key String) 150 | onKeydown constructor = createRawEvent "keydown" (keyInput constructor) 151 | 152 | onKeydown' ∷ ∀ message. ToRawEvent message 153 | onKeydown' = createEventMessage "keydown" 154 | 155 | onKeypress ∷ ∀ message. ToSpecialEvent message (Tuple Key String) 156 | onKeypress constructor = createRawEvent "keypress" (keyInput constructor) 157 | 158 | onKeypress' ∷ ∀ message. ToRawEvent message 159 | onKeypress' = createEventMessage "keypress" 160 | 161 | onKeyup ∷ ∀ message. ToSpecialEvent message (Tuple Key String) 162 | onKeyup constructor = createRawEvent "keyup" (keyInput constructor) 163 | 164 | onKeyup' ∷ ∀ message. ToRawEvent message 165 | onKeyup' = createEventMessage "keyup" 166 | 167 | keyInput ∷ ∀ message. (Tuple Key String → message) → Event → Effect (Maybe message) 168 | keyInput constructor event = do 169 | down ← key event 170 | value ← nodeValue event 171 | pure <<< Just <<< constructor $ Tuple down value 172 | 173 | onContextmenu ∷ ∀ message. ToEvent message 174 | onContextmenu = createEvent "contextmenu" 175 | 176 | onContextmenu' ∷ ∀ message. ToRawEvent message 177 | onContextmenu' = createEventMessage "contextmenu" 178 | 179 | onDblclick ∷ ∀ message. ToEvent message 180 | onDblclick = createEvent "dblclick" 181 | 182 | onDblclick' ∷ ∀ message. ToRawEvent message 183 | onDblclick' = createEventMessage "dblclick" 184 | 185 | onMousedown ∷ ∀ message. ToEvent message 186 | onMousedown = createEvent "mousedown" 187 | 188 | onMousedown' ∷ ∀ message. ToRawEvent message 189 | onMousedown' = createEventMessage "mousedown" 190 | 191 | onMouseenter ∷ ∀ message. ToEvent message 192 | onMouseenter = createEvent "mouseenter" 193 | 194 | onMouseenter' ∷ ∀ message. ToRawEvent message 195 | onMouseenter' = createEventMessage "mouseenter" 196 | 197 | onMouseleave ∷ ∀ message. ToEvent message 198 | onMouseleave = createEvent "mouseleave" 199 | 200 | onMouseleave' ∷ ∀ message. ToRawEvent message 201 | onMouseleave' = createEventMessage "mouseleave" 202 | 203 | onMousemove ∷ ∀ message. ToEvent message 204 | onMousemove = createEvent "mousemove" 205 | 206 | onMousemove' ∷ ∀ message. ToRawEvent message 207 | onMousemove' = createEventMessage "mousemove" 208 | 209 | onMouseover ∷ ∀ message. ToEvent message 210 | onMouseover = createEvent "mouseover" 211 | 212 | onMouseover' ∷ ∀ message. ToRawEvent message 213 | onMouseover' = createEventMessage "mouseover" 214 | 215 | onMouseout ∷ ∀ message. ToEvent message 216 | onMouseout = createEvent "mouseout" 217 | 218 | onMouseout' ∷ ∀ message. ToRawEvent message 219 | onMouseout' = createEventMessage "mouseout" 220 | 221 | onMouseup ∷ ∀ message. ToEvent message 222 | onMouseup = createEvent "mouseup" 223 | 224 | onMouseup' ∷ ∀ message. ToRawEvent message 225 | onMouseup' = createEventMessage "mouseup" 226 | 227 | onSelect ∷ ∀ message. ToSpecialEvent message String 228 | onSelect constructor = createRawEvent "select" handler 229 | where 230 | handler event = Just <<< constructor <$> selection event 231 | 232 | onSelect' ∷ ∀ message. ToRawEvent message 233 | onSelect' = createEventMessage "select" 234 | 235 | onWheel ∷ ∀ message. ToEvent message 236 | onWheel = createEvent "wheel" 237 | 238 | onWheel' ∷ ∀ message. ToRawEvent message 239 | onWheel' = createEventMessage "wheel" 240 | 241 | onDrag ∷ ∀ message. ToEvent message 242 | onDrag = createEvent "drag" 243 | 244 | onDrag' ∷ ∀ message. ToRawEvent message 245 | onDrag' = createEventMessage "drag" 246 | 247 | onDragend ∷ ∀ message. ToEvent message 248 | onDragend = createEvent "dragend" 249 | 250 | onDragend' ∷ ∀ message. ToRawEvent message 251 | onDragend' = createEventMessage "dragend" 252 | 253 | onDragenter ∷ ∀ message. ToEvent message 254 | onDragenter = createEvent "dragenter" 255 | 256 | onDragenter' ∷ ∀ message. ToRawEvent message 257 | onDragenter' = createEventMessage "dragenter" 258 | 259 | onDragstart ∷ ∀ message. ToEvent message 260 | onDragstart = createEvent "dragstart" 261 | 262 | onDragstart' ∷ ∀ message. ToRawEvent message 263 | onDragstart' = createEventMessage "dragstart" 264 | 265 | onDragleave ∷ ∀ message. ToEvent message 266 | onDragleave = createEvent "dragleave" 267 | 268 | onDragleave' ∷ ∀ message. ToRawEvent message 269 | onDragleave' = createEventMessage "dragleave" 270 | 271 | onDragover ∷ ∀ message. ToEvent message 272 | onDragover = createEvent "dragover" 273 | 274 | onDragover' ∷ ∀ message. ToRawEvent message 275 | onDragover' = createEventMessage "dragover" 276 | 277 | onDrop ∷ ∀ message. ToEvent message 278 | onDrop = createEvent "drop" 279 | 280 | onDrop' ∷ ∀ message. ToRawEvent message 281 | onDrop' = createEventMessage "drop" 282 | 283 | onError ∷ ∀ message. ToEvent message 284 | onError = createEvent "error" 285 | 286 | onError' ∷ ∀ message. ToRawEvent message 287 | onError' = createEventMessage "error" -------------------------------------------------------------------------------- /src/Flame/Html/Attribute/Internal.js: -------------------------------------------------------------------------------- 1 | let styleData = 1, 2 | classData = 2, 3 | propertyData = 3, 4 | attributeData = 4, 5 | keyData = 7; 6 | 7 | export function createProperty(name) { 8 | return function (value) { 9 | return [propertyData, name, value]; 10 | }; 11 | } 12 | 13 | export function createAttribute(name) { 14 | return function (value) { 15 | return [attributeData, name, value]; 16 | }; 17 | } 18 | 19 | export function createClass(array) { 20 | return [classData, array]; 21 | } 22 | 23 | export function createStyle(object) { 24 | return [styleData, object]; 25 | } 26 | 27 | export function createKey(value) { 28 | return [keyData, value]; 29 | } -------------------------------------------------------------------------------- /src/Flame/Html/Element.js: -------------------------------------------------------------------------------- 1 | let textNode = 1, 2 | elementNode = 2, 3 | svgNode = 3, 4 | lazyNode = 5, 5 | managedNode = 6; 6 | let styleData = 1, 7 | classData = 2, 8 | propertyData = 3, 9 | attributeData = 4, 10 | keyData = 7; 11 | 12 | export function createElementNode(tag) { 13 | return function (nodeData) { 14 | return function (potentialChildren) { 15 | let children = potentialChildren, 16 | text = undefined; 17 | 18 | if (potentialChildren.length === 1 && potentialChildren[0].nodeType == textNode) { 19 | children = undefined; 20 | text = potentialChildren[0].text; 21 | } 22 | 23 | return { 24 | nodeType: elementNode, 25 | node: undefined, 26 | tag: tag, 27 | nodeData: fromNodeData(nodeData), 28 | children: children, 29 | text: text 30 | }; 31 | }; 32 | }; 33 | } 34 | 35 | export function createDatalessElementNode(tag) { 36 | return function (potentialChildren) { 37 | let children = potentialChildren, 38 | text = undefined; 39 | 40 | if (potentialChildren.length === 1 && potentialChildren[0].nodeType == textNode) { 41 | children = undefined; 42 | text = potentialChildren[0].text; 43 | } 44 | 45 | return { 46 | nodeType: elementNode, 47 | node: undefined, 48 | tag: tag, 49 | nodeData: {}, 50 | children: children, 51 | text: text 52 | }; 53 | }; 54 | } 55 | 56 | export function createSingleElementNode(tag) { 57 | return function (nodeData) { 58 | return { 59 | nodeType: elementNode, 60 | node: undefined, 61 | tag: tag, 62 | nodeData: fromNodeData(nodeData) 63 | }; 64 | }; 65 | } 66 | 67 | export function createEmptyElement(tag) { 68 | return { 69 | nodeType: tag.trim().toLowerCase() === 'svg' ? svgNode : elementNode, 70 | node: undefined, 71 | tag: tag, 72 | nodeData: {} 73 | }; 74 | } 75 | 76 | 77 | export function text(value) { 78 | return { 79 | nodeType: textNode, 80 | node: undefined, 81 | text: value 82 | }; 83 | } 84 | 85 | export function createLazyNode(nodeData) { 86 | return function (render) { 87 | return function (arg) { 88 | let key = nodeData[0]; 89 | 90 | return { 91 | nodeType: lazyNode, 92 | node: undefined, 93 | nodeData: key === undefined ? undefined : { key: key }, 94 | render: render, 95 | arg: arg, 96 | rendered: undefined 97 | }; 98 | }; 99 | }; 100 | } 101 | 102 | export function createManagedNode(render) { 103 | return function (nodeData) { 104 | return function (arg) { 105 | return { 106 | nodeType: managedNode, 107 | node: undefined, 108 | nodeData: fromNodeData(nodeData), 109 | createNode: render.createNode, 110 | updateNode: render.updateNode, 111 | arg: arg 112 | }; 113 | }; 114 | }; 115 | } 116 | 117 | export function createDatalessManagedNode(render) { 118 | return function (arg) { 119 | return { 120 | nodeType: managedNode, 121 | node: undefined, 122 | nodeData: {}, 123 | createNode: render.createNode, 124 | updateNode: render.updateNode, 125 | arg: arg 126 | }; 127 | }; 128 | } 129 | 130 | export function createSvgNode(nodeData) { 131 | return function (children) { 132 | return { 133 | nodeType: svgNode, 134 | node: undefined, 135 | tag: 'svg', 136 | nodeData: fromNodeData(nodeData), 137 | children: asSvg(children) 138 | }; 139 | }; 140 | } 141 | 142 | export function createDatalessSvgNode(children) { 143 | return { 144 | nodeType: svgNode, 145 | node: undefined, 146 | tag: 'svg', 147 | nodeData: {}, 148 | children: asSvg(children) 149 | }; 150 | } 151 | 152 | export function createSingleSvgNode(nodeData) { 153 | return { 154 | nodeType: svgNode, 155 | node: undefined, 156 | tag: 'svg', 157 | nodeData: fromNodeData(nodeData) 158 | }; 159 | } 160 | 161 | function asSvg(elements) { 162 | for (let e of elements) { 163 | if (e.nodeType === elementNode) 164 | e.nodeType = svgNode; 165 | if (e.children !== null && typeof e.children !== 'undefined') 166 | e.children = asSvg(e.children); 167 | } 168 | 169 | return elements; 170 | } 171 | 172 | function fromNodeData(allData) { 173 | let nodeData = {}; 174 | 175 | if (allData !== undefined) 176 | for (let data of allData) { 177 | let dataOne = data[1]; 178 | //[0] also always contain the data type 179 | switch (data[0]) { 180 | case styleData: 181 | if (nodeData.styles === undefined) 182 | nodeData.styles = {}; 183 | 184 | for (let key in dataOne) 185 | nodeData.styles[key] = dataOne[key]; 186 | break; 187 | case classData: 188 | if (nodeData.classes === undefined) 189 | nodeData.classes = []; 190 | 191 | nodeData.classes = nodeData.classes.concat(dataOne); 192 | break; 193 | case propertyData: 194 | if (nodeData.properties === undefined) 195 | nodeData.properties = {}; 196 | 197 | nodeData.properties[dataOne] = data[2]; 198 | break; 199 | case attributeData: 200 | if (nodeData.attributes === undefined) 201 | nodeData.attributes = {}; 202 | 203 | nodeData.attributes[dataOne] = data[2]; 204 | break; 205 | case keyData: 206 | nodeData.key = dataOne; 207 | break; 208 | default: 209 | if (nodeData.events === undefined) 210 | nodeData.events = {}; 211 | 212 | if (nodeData.events[dataOne] === undefined) 213 | nodeData.events[dataOne] = []; 214 | 215 | nodeData.events[dataOne].push(data[2]); 216 | } 217 | } 218 | 219 | return nodeData; 220 | } 221 | -------------------------------------------------------------------------------- /src/Flame/Internal/Equality.js: -------------------------------------------------------------------------------- 1 | export function compareReference(a) { 2 | return function (b) { 3 | return a === b; 4 | } 5 | } -------------------------------------------------------------------------------- /src/Flame/Internal/Equality.purs: -------------------------------------------------------------------------------- 1 | module Flame.Internal.Equality where 2 | 3 | import Prelude (not, ($)) 4 | 5 | foreign import compareReference ∷ ∀ a. a → a → Boolean 6 | 7 | modelHasChanged ∷ ∀ model. model → model → Boolean 8 | modelHasChanged old new = not $ compareReference old new -------------------------------------------------------------------------------- /src/Flame/Internal/Fragment.js: -------------------------------------------------------------------------------- 1 | let fragmentNode = 4; 2 | 3 | export function createFragmentNode(children) { 4 | return { 5 | nodeType: fragmentNode, 6 | node: undefined, 7 | children: children 8 | }; 9 | } -------------------------------------------------------------------------------- /src/Flame/Internal/Fragment.purs: -------------------------------------------------------------------------------- 1 | module Flame.Internal.Fragment where 2 | 3 | foreign import createFragmentNode ∷ ∀ html message. Array (html message) → html message -------------------------------------------------------------------------------- /src/Flame/Renderer/Internal/Dom.purs: -------------------------------------------------------------------------------- 1 | -- | Renders changes to the DOM 2 | module Flame.Renderer.Internal.Dom 3 | ( start 4 | , startFrom 5 | , resume 6 | ) where 7 | 8 | import Data.Maybe (Maybe(..)) 9 | import Effect (Effect) 10 | import Effect.Uncurried (EffectFn2, EffectFn4) 11 | import Effect.Uncurried as EU 12 | import Flame.Types (DomNode, DomRenderingState, Html) 13 | import Prelude (Unit, pure, unit) 14 | 15 | -- | Events that are messages rather than callbacks need to be wrapped from the FFI 16 | type MessageWrapper message = message → Maybe message 17 | 18 | foreign import start_ ∷ ∀ message. EffectFn4 (MessageWrapper message) DomNode (Maybe message → Effect Unit) (Html message) DomRenderingState 19 | foreign import startFrom_ ∷ ∀ message. EffectFn4 (MessageWrapper message) DomNode (Maybe message → Effect Unit) (Html message) DomRenderingState 20 | foreign import resume_ ∷ ∀ message. EffectFn2 DomRenderingState (Html message) Unit 21 | 22 | -- | Mounts the application on a DOM node 23 | -- | 24 | -- | The node will be set as the parent and otherwise unmodified 25 | start ∷ ∀ message. DomNode → (message → Effect Unit) → Html message → Effect DomRenderingState 26 | start parent updater = EU.runEffectFn4 start_ Just parent (maybeUpdater updater) 27 | 28 | -- | Hydrates a server-side rendered application 29 | startFrom ∷ ∀ message. DomNode → (message → Effect Unit) → Html message → Effect DomRenderingState 30 | startFrom parent updater = EU.runEffectFn4 startFrom_ Just parent (maybeUpdater updater) 31 | 32 | maybeUpdater ∷ ∀ message. (message → Effect Unit) → (Maybe message → Effect Unit) 33 | maybeUpdater updater = case _ of 34 | Just message → updater message 35 | _ → pure unit 36 | 37 | -- | Patches the application 38 | resume ∷ ∀ message. DomRenderingState → Html message → Effect Unit 39 | resume = EU.runEffectFn2 resume_ 40 | -------------------------------------------------------------------------------- /src/Flame/Renderer/String.js: -------------------------------------------------------------------------------- 1 | let textNode = 1, 2 | elementNode = 2, 3 | svgNode = 3, 4 | fragmentNode = 4, 5 | lazyNode = 5, 6 | managedNode = 6; 7 | let reUnescapedHtml = /[&<>"']/g, 8 | reHasUnescapedHtml = RegExp(reUnescapedHtml.source), 9 | htmlEscapes = new Map([ 10 | ['&', '&'], 11 | ['<', '<'], 12 | ['>', '>'], 13 | ['"', '"'], 14 | ["'", '''] 15 | ]); 16 | let containerElements = new Set([ 17 | 'a', 18 | 'defs', 19 | 'glyph', 20 | 'g', 21 | 'marker', 22 | 'mask', 23 | 'missing-glyph', 24 | 'pattern', 25 | 'svg', 26 | 'switch', 27 | 'symbol', 28 | 'text', 29 | 'desc', 30 | 'metadata', 31 | 'title' 32 | ]), 33 | voidElements = new Set([ 34 | 'area', 35 | 'base', 36 | 'br', 37 | 'col', 38 | 'embed', 39 | 'hr', 40 | 'img', 41 | 'input', 42 | 'keygen', 43 | 'link', 44 | 'meta', 45 | 'param', 46 | 'source', 47 | 'track', 48 | 'wbr' 49 | ]); 50 | let omitProperties = new Set([ 51 | 'attributes', 52 | 'childElementCount', 53 | 'children', 54 | 'classList', 55 | 'clientHeight', 56 | 'clientLeft', 57 | 'clientTop', 58 | 'clientWidth', 59 | 'currentStyle', 60 | 'firstElementChild', 61 | 'innerHTML', 62 | 'lastElementChild', 63 | 'nextElementSibling', 64 | 'ongotpointercapture', 65 | 'onlostpointercapture', 66 | 'onwheel', 67 | 'outerHTML', 68 | 'previousElementSibling', 69 | 'runtimeStyle', 70 | 'scrollHeight', 71 | 'scrollLeft', 72 | 'scrollLeftMax', 73 | 'scrollTop', 74 | 'scrollTopMax', 75 | 'scrollWidth', 76 | 'tabStop', 77 | 'tag' 78 | ]); 79 | let booleanAttributes = new Set([ 80 | 'disabled', 81 | 'visible', 82 | 'checked', 83 | 'readonly', 84 | 'required', 85 | 'allowfullscreen', 86 | 'autofocus', 87 | 'autoplay', 88 | 'compact', 89 | 'controls', 90 | 'default', 91 | 'formnovalidate', 92 | 'hidden', 93 | 'ismap', 94 | 'itemscope', 95 | 'loop', 96 | 'multiple', 97 | 'muted', 98 | 'noresize', 99 | 'noshade', 100 | 'novalidate', 101 | 'nowrap', 102 | 'open', 103 | 'reversed', 104 | 'seamless', 105 | 'selected', 106 | 'sortable', 107 | 'truespeed', 108 | 'typemustmatch' 109 | ]); 110 | 111 | /** String rendering adapted from https://github.com/snabbdom/snabbdom-to-html */ 112 | export function render_(html) { 113 | let docType = '', 114 | rendered = stringify(html); 115 | 116 | if (html.nodeType === elementNode && html.tag === 'html') 117 | rendered = docType + rendered; 118 | 119 | return rendered; 120 | } 121 | 122 | function stringify(html) { 123 | switch (html.nodeType) { 124 | case textNode: 125 | return escape(html.text); 126 | case lazyNode: 127 | return stringify(html.render(html.arg)); 128 | case fragmentNode: 129 | let childrenTag = new Array(html.children.length); 130 | 131 | for (let i = 0; i < html.children.length; ++i) 132 | childrenTag.push(stringify(html.children[i])); 133 | 134 | return childrenTag.join(''); 135 | //skip for now, as element creation needs polyfills on server-side 136 | case managedNode: 137 | return ''; 138 | default: 139 | let isSvg = html.nodeType === svgNode, 140 | stringfiedNodeData = stringifyNodeData(html.nodeData), 141 | tag = html.tag, 142 | markup = ['<' + tag]; 143 | 144 | if (stringfiedNodeData.length > 0) 145 | markup.push(' ' + stringfiedNodeData); 146 | 147 | if (isSvg && !containerElements.has(tag)) 148 | markup.push(' /'); 149 | 150 | markup.push('>'); 151 | 152 | if (!voidElements.has(tag) && !isSvg || isSvg && containerElements.has(tag)) { 153 | if (html.nodeData.properties !== undefined && html.nodeData.properties.innerHTML !== undefined) 154 | markup.push(html.nodeData.properties.innerHTML); 155 | else if (html.text !== undefined) 156 | markup.push(escape(html.text)); 157 | else if (html.children !== undefined && html.children.length > 0) 158 | for (let i = 0; i < html.children.length; ++i) 159 | markup.push(stringify(html.children[i])); 160 | 161 | markup.push(''); 162 | } 163 | 164 | return markup.join(''); 165 | } 166 | } 167 | 168 | function stringifyNodeData(nodeData) { 169 | let result = [], 170 | mapped = new Map(); 171 | 172 | if (nodeData.styles !== undefined) 173 | setStyles(mapped, nodeData.styles); 174 | 175 | if (nodeData.classes !== undefined && nodeData.classes.length > 0) 176 | setClasses(mapped, nodeData.classes); 177 | 178 | if (nodeData.properties !== undefined) 179 | setProperties(mapped, nodeData.properties); 180 | 181 | if (nodeData.attributes !== undefined) 182 | setAttributes(mapped, nodeData.attributes); 183 | 184 | for (let keyValue of mapped) 185 | if (keyValue[1].length > 0) 186 | result.push(keyValue[0] + '="' + keyValue[1] + '"'); 187 | 188 | return result.join(' '); 189 | } 190 | 191 | function setStyles(mapped, styles) { 192 | let values = []; 193 | 194 | for (let key in styles) 195 | values.push(key + ': ' + escape(styles[key])); 196 | 197 | if (values.length > 0) 198 | mapped.set('style', values.join('; ')); 199 | } 200 | 201 | function setClasses(mapped, classes) { 202 | mapped.set('class', classes.join(' ')); 203 | } 204 | 205 | function setProperties(mapped, properties) { 206 | for (let key in properties) 207 | if (!omitProperties.has(key)) { 208 | let value = properties[key]; 209 | 210 | if (booleanAttributes.has(key)) { 211 | if (value) 212 | mapped.set(key, key); 213 | } 214 | else 215 | mapped.set(key, escape(value)); 216 | } 217 | } 218 | 219 | function setAttributes(mapped, attributes) { 220 | for (let key in attributes) 221 | mapped.set(key, escape(attributes[key])); 222 | } 223 | 224 | // from loadash https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L14251 225 | function escape(string) { 226 | return reHasUnescapedHtml.test(string) ? string.replace(reUnescapedHtml, escapeHtmlChar) : string; 227 | } 228 | 229 | function escapeHtmlChar(key) { 230 | return htmlEscapes.get(key); 231 | } 232 | -------------------------------------------------------------------------------- /src/Flame/Renderer/String.purs: -------------------------------------------------------------------------------- 1 | module Flame.Renderer.String (render) where 2 | 3 | import Effect (Effect) 4 | import Effect.Uncurried (EffectFn1) 5 | import Effect.Uncurried as EU 6 | import Flame.Types (Html) 7 | 8 | foreign import render_ ∷ ∀ message. EffectFn1 (Html message) String 9 | 10 | -- | Render markup into a string, useful for server-side rendering 11 | -- | 12 | -- | If the root tag is `html`, doctype is automatically added 13 | render ∷ ∀ message. Html message → Effect String 14 | render = EU.runEffectFn1 render_ -------------------------------------------------------------------------------- /src/Flame/Serialize.purs: -------------------------------------------------------------------------------- 1 | module Flame.Serialization (class SerializeState, serialize, class UnserializeState, unserialize, unsafeUnserialize) where 2 | 3 | import Data.Argonaut.Core as DAC 4 | import Data.Argonaut.Decode (JsonDecodeError) 5 | import Data.Argonaut.Decode as DAD 6 | import Data.Argonaut.Decode.Class (class GDecodeJson) 7 | import Data.Argonaut.Decode.Generic (class DecodeRep) 8 | import Data.Argonaut.Decode.Generic as DADEG 9 | import Data.Argonaut.Encode as DAE 10 | import Data.Argonaut.Encode.Class (class GEncodeJson) 11 | import Data.Argonaut.Encode.Generic (class EncodeRep) 12 | import Data.Argonaut.Encode.Generic as DAEG 13 | import Data.Bifunctor as DB 14 | import Data.Either (Either(..)) 15 | import Data.Generic.Rep (class Generic) 16 | import Partial as P 17 | import Partial.Unsafe as PU 18 | import Prelude (bind, show, (<<<), ($)) 19 | import Prim.RowList (class RowToList) 20 | 21 | class UnserializeState m where 22 | unserialize ∷ String → Either String m 23 | 24 | instance recordUnserializeState ∷ (GDecodeJson m list, RowToList m list) ⇒ UnserializeState (Record m) where 25 | unserialize model = jsonStringError do 26 | json ← DAD.parseJson model 27 | DAD.decodeJson json 28 | else instance genericUnserializeState ∷ (Generic m r, DecodeRep r) ⇒ UnserializeState m where 29 | unserialize model = jsonStringError do 30 | json ← DAD.parseJson model 31 | DADEG.genericDecodeJson json 32 | 33 | class SerializeState m where 34 | serialize ∷ m → String 35 | 36 | instance encodeJsonSerializeState ∷ (GEncodeJson m list, RowToList m list) ⇒ SerializeState (Record m) where 37 | serialize = DAC.stringify <<< DAE.encodeJson 38 | else instance genericSerializeState ∷ (Generic m r, EncodeRep r) ⇒ SerializeState m where 39 | serialize = DAC.stringify <<< DAEG.genericEncodeJson 40 | 41 | jsonStringError ∷ ∀ a. Either JsonDecodeError a → Either String a 42 | jsonStringError = DB.lmap DAD.printJsonDecodeError 43 | 44 | unsafeUnserialize ∷ ∀ m. UnserializeState m ⇒ String → m 45 | unsafeUnserialize str = PU.unsafePartial case unserialize str of 46 | Right m → m 47 | Left err → P.crashWith $ show err -------------------------------------------------------------------------------- /src/Flame/Subscription.purs: -------------------------------------------------------------------------------- 1 | -- | Defines helpers for events from outside the view (e.g., custom/window or document events) 2 | -- | For view events, see `Flame.Html.Attribute` 3 | module Flame.Subscription 4 | ( send 5 | , onCustomEvent 6 | , onCustomEvent' 7 | ) where 8 | 9 | import Data.Tuple.Nested as DTN 10 | import Effect (Effect) 11 | import Flame.Application.Internal.Dom as FAID 12 | import Flame.Serialization (class UnserializeState) 13 | import Flame.Serialization as FS 14 | import Flame.Types (AppId(..), Source(..), Subscription) 15 | import Foreign as F 16 | import Prelude (class Show, Unit, const, show, (<<<)) 17 | import Web.Event.Event (EventType(..)) 18 | 19 | -- | Raises an arbitrary message on the given application 20 | send ∷ ∀ id message. Show id ⇒ AppId id message → message → Effect Unit 21 | send (AppId id) message = FAID.dispatchCustomEvent (show id) message 22 | 23 | -- | Subscribe to a `CustomEvent` 24 | -- | 25 | -- | `arg` must be serializable since it might come from external JavaScript 26 | onCustomEvent ∷ ∀ arg message. UnserializeState arg ⇒ EventType → (arg → message) → Subscription message 27 | onCustomEvent (EventType eventName) message = DTN.tuple3 Custom eventName (message <<< FS.unsafeUnserialize <<< F.unsafeFromForeign) 28 | 29 | -- | Subscribe to a `CustomEvent` that has no data associated 30 | onCustomEvent' ∷ ∀ message. EventType → message → Subscription message 31 | onCustomEvent' (EventType eventName) message = DTN.tuple3 Custom eventName (const message) 32 | -------------------------------------------------------------------------------- /src/Flame/Subscription/Document.purs: -------------------------------------------------------------------------------- 1 | -- | Defines events for the native `document` object 2 | module Flame.Subscription.Document (onBlur, onBlur', onClick, onClick', onContextmenu, onContextmenu', onDblclick, onDblclick', onDrag, onDrag', onDragend, onDragend', onDragenter, onDragenter', onDragleave, onDragleave', onDragover, onDragover', onDragstart, onDragstart', onDrop, onDrop', onFocus, onFocus', onKeydown, onKeydown', onKeypress, onKeypress', onKeyup, onKeyup', onScroll, onScroll', onWheel, onWheel') where 3 | 4 | import Flame.Subscription.Internal.Create as FSIC 5 | import Flame.Types (Key, Source(..), Subscription) 6 | import Web.Event.Event (Event) 7 | 8 | -- | click event fired for the document 9 | onClick ∷ ∀ message. message → Subscription message 10 | onClick = FSIC.createSubscription Document "click" 11 | 12 | onClick' ∷ ∀ message. (Event → message) → Subscription message 13 | onClick' = FSIC.createRawSubscription Document "click" 14 | 15 | onScroll ∷ ∀ message. message → Subscription message 16 | onScroll = FSIC.createSubscription Document "scroll" 17 | 18 | onScroll' ∷ ∀ message. (Event → message) → Subscription message 19 | onScroll' = FSIC.createRawSubscription Document "scroll" 20 | 21 | onFocus ∷ ∀ message. message → Subscription message 22 | onFocus = FSIC.createSubscription Document "focus" 23 | 24 | onFocus' ∷ ∀ message. (Event → message) → Subscription message 25 | onFocus' = FSIC.createRawSubscription Document "focus" 26 | 27 | onBlur ∷ ∀ message. message → Subscription message 28 | onBlur = FSIC.createSubscription Document "blur" 29 | 30 | onBlur' ∷ ∀ message. (Event → message) → Subscription message 31 | onBlur' = FSIC.createRawSubscription Document "blur" 32 | 33 | onKeydown ∷ ∀ message. (Key → message) → Subscription message 34 | onKeydown = FSIC.createRawSubscription Document "keydown" 35 | 36 | onKeydown' ∷ ∀ message. (Event → message) → Subscription message 37 | onKeydown' = FSIC.createRawSubscription Document "keydown" 38 | 39 | onKeypress ∷ ∀ message. (Key → message) → Subscription message 40 | onKeypress = FSIC.createRawSubscription Document "keypress" 41 | 42 | onKeypress' ∷ ∀ message. (Event → message) → Subscription message 43 | onKeypress' = FSIC.createRawSubscription Document "keypress" 44 | 45 | onKeyup ∷ ∀ message. (Key → message) → Subscription message 46 | onKeyup = FSIC.createRawSubscription Document "keyup" 47 | 48 | onKeyup' ∷ ∀ message. (Event → message) → Subscription message 49 | onKeyup' = FSIC.createRawSubscription Document "keyup" 50 | 51 | onContextmenu ∷ ∀ message. message → Subscription message 52 | onContextmenu = FSIC.createSubscription Document "contextmenu" 53 | 54 | onContextmenu' ∷ ∀ message. (Event → message) → Subscription message 55 | onContextmenu' = FSIC.createRawSubscription Document "contextmenu" 56 | 57 | onDblclick ∷ ∀ message. message → Subscription message 58 | onDblclick = FSIC.createSubscription Document "dblclick" 59 | 60 | onDblclick' ∷ ∀ message. (Event → message) → Subscription message 61 | onDblclick' = FSIC.createRawSubscription Document "dblclick" 62 | 63 | onWheel ∷ ∀ message. message → Subscription message 64 | onWheel = FSIC.createSubscription Document "wheel" 65 | 66 | onWheel' ∷ ∀ message. (Event → message) → Subscription message 67 | onWheel' = FSIC.createRawSubscription Document "wheel" 68 | 69 | onDrag ∷ ∀ message. message → Subscription message 70 | onDrag = FSIC.createSubscription Document "drag" 71 | 72 | onDrag' ∷ ∀ message. (Event → message) → Subscription message 73 | onDrag' = FSIC.createRawSubscription Document "drag" 74 | 75 | onDragend ∷ ∀ message. message → Subscription message 76 | onDragend = FSIC.createSubscription Document "dragend" 77 | 78 | onDragend' ∷ ∀ message. (Event → message) → Subscription message 79 | onDragend' = FSIC.createRawSubscription Document "dragend" 80 | 81 | onDragenter ∷ ∀ message. message → Subscription message 82 | onDragenter = FSIC.createSubscription Document "dragenter" 83 | 84 | onDragenter' ∷ ∀ message. (Event → message) → Subscription message 85 | onDragenter' = FSIC.createRawSubscription Document "dragenter" 86 | 87 | onDragstart ∷ ∀ message. message → Subscription message 88 | onDragstart = FSIC.createSubscription Document "dragstart" 89 | 90 | onDragstart' ∷ ∀ message. (Event → message) → Subscription message 91 | onDragstart' = FSIC.createRawSubscription Document "dragstart" 92 | 93 | onDragleave ∷ ∀ message. message → Subscription message 94 | onDragleave = FSIC.createSubscription Document "dragleave" 95 | 96 | onDragleave' ∷ ∀ message. (Event → message) → Subscription message 97 | onDragleave' = FSIC.createRawSubscription Document "dragleave" 98 | 99 | onDragover ∷ ∀ message. message → Subscription message 100 | onDragover = FSIC.createSubscription Document "dragover" 101 | 102 | onDragover' ∷ ∀ message. (Event → message) → Subscription message 103 | onDragover' = FSIC.createRawSubscription Document "dragover" 104 | 105 | onDrop ∷ ∀ message. message → Subscription message 106 | onDrop = FSIC.createSubscription Document "drop" 107 | 108 | onDrop' ∷ ∀ message. (Event → message) → Subscription message 109 | onDrop' = FSIC.createRawSubscription Document "drop" -------------------------------------------------------------------------------- /src/Flame/Subscription/Internal/Listener.js: -------------------------------------------------------------------------------- 1 | //this is not ideal, e.g. if the library is loaded twice 2 | let applicationIds = new Set(); 3 | 4 | export function checkApplicationId_(id) { 5 | if (applicationIds.has(id)) 6 | throw `Error mounting application: id ${id} already registered!`; 7 | 8 | applicationIds.add(id); 9 | } -------------------------------------------------------------------------------- /src/Flame/Subscription/Internal/Listener.purs: -------------------------------------------------------------------------------- 1 | module Flame.Subscription.Internal.Listener 2 | ( createMessageListener 3 | , createSubscription 4 | ) where 5 | 6 | import Data.Tuple (Tuple(..)) 7 | import Effect (Effect) 8 | import Effect.Uncurried (EffectFn1) 9 | import Effect.Uncurried as EU 10 | import Flame.Application.Internal.Dom as FAID 11 | import Flame.Types (ApplicationId, Source(..), Subscription) 12 | import Foreign as F 13 | import Prelude (Unit, discard, (<<<)) 14 | 15 | foreign import checkApplicationId_ ∷ EffectFn1 String Unit 16 | 17 | -- | Raises an error if application id is not unique 18 | checkApplicationId ∷ String → Effect Unit 19 | checkApplicationId = EU.runEffectFn1 checkApplicationId_ 20 | 21 | -- | Listener for external messages 22 | -- | 23 | -- | Implemented as a custom event because messages can be raised from anywhere 24 | createMessageListener ∷ ∀ message. ApplicationId → (message → Effect Unit) → Effect Unit 25 | createMessageListener appId updater = do 26 | checkApplicationId appId 27 | FAID.createCustomListener appId (updater <<< F.unsafeFromForeign) 28 | 29 | -- | Events from `Application.subscribe` 30 | createSubscription ∷ ∀ message. (message → Effect Unit) → Subscription message → Effect Unit 31 | createSubscription updater (Tuple source (Tuple eventName (Tuple toMessage _))) = case source of 32 | Window → FAID.createWindowListener eventName (updater <<< toMessage) 33 | Document → FAID.createDocumentListener eventName (updater <<< toMessage) 34 | Custom → FAID.createCustomListener eventName (updater <<< toMessage) -------------------------------------------------------------------------------- /src/Flame/Subscription/Internal/Source.purs: -------------------------------------------------------------------------------- 1 | module Flame.Subscription.Internal.Create 2 | ( createSubscription 3 | , createRawSubscription 4 | ) where 5 | 6 | import Data.Tuple.Nested as DTN 7 | import Flame.Html.Event (EventName) 8 | import Flame.Types (Source, Subscription) 9 | import Foreign as F 10 | import Prelude (const, (<<<)) 11 | 12 | createSubscription ∷ ∀ message. Source → EventName → message → Subscription message 13 | createSubscription source eventName message = DTN.tuple3 source eventName (const message) 14 | 15 | createRawSubscription ∷ ∀ arg message. Source → EventName → (arg → message) → Subscription message 16 | createRawSubscription source eventName message = DTN.tuple3 source eventName (message <<< F.unsafeFromForeign) 17 | -------------------------------------------------------------------------------- /src/Flame/Subscription/Unsafe/CustomEvent.purs: -------------------------------------------------------------------------------- 1 | module Flame.Subscription.Unsafe.CustomEvent 2 | ( broadcast 3 | , broadcast' 4 | ) where 5 | 6 | import Effect (Effect) 7 | import Flame.Application.Internal.Dom as FAID 8 | import Flame.Serialization (class SerializeState) 9 | import Flame.Serialization as FS 10 | import Prelude (Unit, unit, (<<<)) 11 | import Web.Event.Event (EventType(..)) 12 | 13 | -- | Broadcast a `CustomEvent` to all applications 14 | -- | 15 | -- | This is considered unsafe as there is no guarantee that the payload matches listeners' expectations 16 | broadcast ∷ ∀ arg. SerializeState arg ⇒ EventType → arg → Effect Unit 17 | broadcast (EventType eventName) = FAID.dispatchCustomEvent eventName <<< FS.serialize 18 | 19 | -- | Broadcast a `CustomEvent` that has no data associated to all applications 20 | -- | 21 | -- | This is considered unsafe as there is no guarantee that the payload matches listeners' expectations 22 | broadcast' ∷ EventType → Effect Unit 23 | broadcast' (EventType eventName) = FAID.dispatchCustomEvent eventName unit -------------------------------------------------------------------------------- /src/Flame/Subscription/Window.purs: -------------------------------------------------------------------------------- 1 | -- | Defines events for the native `window` object 2 | module Flame.Subscription.Window (onError, onError', onLoad, onLoad', onOffline, onOffline', onOnline, onOnline', onResize, onResize', onUnload, onUnload', onFocus, onFocus', onPopstate, onPopstate') where 3 | 4 | import Flame.Subscription.Internal.Create as FSIC 5 | import Flame.Types (Source(..), Subscription) 6 | import Web.Event.Internal.Types (Event) 7 | 8 | onPopstate ∷ ∀ message. message → Subscription message 9 | onPopstate = FSIC.createSubscription Window "popstate" 10 | 11 | onPopstate' ∷ ∀ message. (Event → message) → Subscription message 12 | onPopstate' = FSIC.createRawSubscription Window "popstate" 13 | 14 | onFocus ∷ ∀ message. message → Subscription message 15 | onFocus = FSIC.createSubscription Window "focus" 16 | 17 | onFocus' ∷ ∀ message. (Event → message) → Subscription message 18 | onFocus' = FSIC.createRawSubscription Window "focus" 19 | 20 | onError ∷ ∀ message. message → Subscription message 21 | onError = FSIC.createSubscription Window "error" 22 | 23 | onError' ∷ ∀ message. (Event → message) → Subscription message 24 | onError' = FSIC.createRawSubscription Window "error" 25 | 26 | onResize ∷ ∀ message. message → Subscription message 27 | onResize = FSIC.createSubscription Window "resize" 28 | 29 | onResize' ∷ ∀ message. (Event → message) → Subscription message 30 | onResize' = FSIC.createRawSubscription Window "resize" 31 | 32 | onOffline ∷ ∀ message. message → Subscription message 33 | onOffline = FSIC.createSubscription Window "offline" 34 | 35 | onOffline' ∷ ∀ message. (Event → message) → Subscription message 36 | onOffline' = FSIC.createRawSubscription Window "offline" 37 | 38 | onOnline ∷ ∀ message. message → Subscription message 39 | onOnline = FSIC.createSubscription Window "online" 40 | 41 | onOnline' ∷ ∀ message. (Event → message) → Subscription message 42 | onOnline' = FSIC.createRawSubscription Window "online" 43 | 44 | onLoad ∷ ∀ message. message → Subscription message 45 | onLoad = FSIC.createSubscription Window "load" 46 | 47 | onLoad' ∷ ∀ message. (Event → message) → Subscription message 48 | onLoad' = FSIC.createRawSubscription Window "load" 49 | 50 | onUnload ∷ ∀ message. message → Subscription message 51 | onUnload = FSIC.createSubscription Window "unload" 52 | 53 | onUnload' ∷ ∀ message. (Event → message) → Subscription message 54 | onUnload' = FSIC.createRawSubscription Window "unload" 55 | 56 | -------------------------------------------------------------------------------- /src/Flame/Types.js: -------------------------------------------------------------------------------- 1 | export function messageMapper(mapper) { 2 | return function (html) { 3 | return addMessageMapper(html, mapper); 4 | }; 5 | } 6 | 7 | function addMessageMapper(html, mapper) { 8 | if (html.nodeType !== 1 && html.nodeType !== 4) 9 | mapHtml(html, mapper); 10 | 11 | if (html.children !== undefined && html.children.length > 0) 12 | for (let i = 0; i < html.children.length; ++i) 13 | addMessageMapper(html.children[i], mapper); 14 | 15 | return html; 16 | } 17 | 18 | function mapHtml(html, mapper) { 19 | if (html.messageMapper) { 20 | let previousMessageMapper = html.messageMapper; 21 | 22 | html.messageMapper = function (message) { 23 | return mapper(previousMessageMapper(message)); 24 | }; 25 | } 26 | else 27 | html.messageMapper = mapper; 28 | } -------------------------------------------------------------------------------- /src/Flame/Types.purs: -------------------------------------------------------------------------------- 1 | -- | Types common to Flame modules 2 | module Flame.Types 3 | ( PreApplication 4 | , App 5 | , (:>) 6 | , ToNodeData 7 | , Tag 8 | , Key 9 | , AppId(..) 10 | , ApplicationId 11 | , Subscription 12 | , DomRenderingState 13 | , Source(..) 14 | , EventName 15 | , DomNode 16 | , NodeData 17 | , Html 18 | ) where 19 | 20 | import Data.Maybe (Maybe) 21 | import Data.Tuple (Tuple(..)) 22 | import Data.Tuple.Nested (Tuple3) 23 | import Foreign (Foreign) 24 | import Prelude (class Functor, class Show, map) 25 | 26 | -- | `PreApplication` contains 27 | -- | * `init` – the initial model 28 | -- | * `view` – a function to update your markup 29 | type PreApplication model message = 30 | { init ∷ model 31 | , view ∷ model → Html message 32 | } 33 | 34 | type ApplicationId = String 35 | 36 | newtype AppId ∷ ∀ k. Type → k → Type 37 | newtype AppId a b = AppId a 38 | 39 | type EventName = String 40 | 41 | data Source = Window | Document | Custom 42 | 43 | -- | Subscriptions are events from outside the view, e.g. `window`, `document` or `CustomEvent` 44 | type Subscription message = Tuple3 Source EventName (Foreign → message) 45 | 46 | -- | Abstracts over common fields of an `Application` 47 | type App model message extension = 48 | { view ∷ model → Html message 49 | , subscribe ∷ Array (Subscription message) 50 | | extension 51 | } 52 | 53 | -- | Infix tuple constructor 54 | infixr 6 Tuple as :> 55 | 56 | type ToNodeData value = ∀ message. value → NodeData message 57 | 58 | type Tag = String 59 | type Key = String 60 | 61 | -- | FFI class that keeps track of DOM rendering 62 | foreign import data DomRenderingState ∷ Type 63 | 64 | -- | A make believe type for DOM nodes 65 | foreign import data DomNode ∷ Type 66 | 67 | instance Show DomNode where 68 | show _ = "dom node" 69 | 70 | -- | Attributes and properties of virtual nodes 71 | foreign import data NodeData ∷ Type → Type 72 | 73 | --Html can actually be typed, but since it is only used in FFI code, I don't think it'd be very useful 74 | -- | The type of virtual nodes 75 | foreign import data Html ∷ Type → Type 76 | 77 | --we support events that are not fired if the message is Nothing 78 | foreign import messageMapper ∷ ∀ message mapped. (Maybe message → Maybe mapped) → Html message → Html mapped 79 | 80 | instance Functor Html where 81 | map f html = messageMapper (map f) html 82 | -------------------------------------------------------------------------------- /test/Basic/EffectList.purs: -------------------------------------------------------------------------------- 1 | module Test.Basic.EffectList (mount) where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.Maybe as DM 7 | import Data.String as DS 8 | import Data.String.CodeUnits as DSC 9 | import Data.Tuple (Tuple) 10 | import Effect (Effect) 11 | import Effect.Aff (Aff) 12 | import Effect.Class (liftEffect) 13 | import Effect.Random as ER 14 | import Flame (QuerySelector(..), Html, (:>)) 15 | import Flame.Application.EffectList as FAE 16 | import Flame.Html.Attribute as HA 17 | import Flame.Html.Element as HE 18 | import Partial.Unsafe as UP 19 | import Web.Event.Event as WEE 20 | import Web.UIEvent.KeyboardEvent as WUK 21 | 22 | type Model = String 23 | 24 | data Message = Current String | Cut | Submit 25 | 26 | update ∷ Model → Message → Tuple Model (Array (Aff (Maybe Message))) 27 | update model = case _ of 28 | Cut → model :> 29 | [ Just <<< Current <$> cut model 30 | ] 31 | Submit → "thanks" :> [] 32 | Current text → text :> [] 33 | where 34 | cut text = do 35 | amount ← liftEffect <<< ER.randomInt 1 $ DSC.length text 36 | pure $ DS.drop amount text 37 | 38 | view ∷ Model → Html Message 39 | view model = HE.main_ 40 | [ HE.span [ HA.id "text-output" ] model 41 | , 42 | --we add extra events for each input to test if the correct message is used 43 | HE.input [ HA.id "text-input", HA.type' "text", HA.onInput Current, HA.onFocus Cut, onEnterPressed Submit ] 44 | , HE.input [ HA.id "cut-button", HA.type' "button", HA.onClick Cut, HA.onFocus (Current "") ] 45 | ] 46 | where 47 | onEnterPressed message = HA.createRawEvent "keypress" $ \event → do 48 | let pressed = WUK.key $ UP.unsafePartial (DM.fromJust $ WUK.fromEvent event) 49 | case pressed of 50 | "Enter" → do 51 | WEE.preventDefault event 52 | pure $ Just message 53 | _ → pure Nothing 54 | 55 | mount ∷ Effect Unit 56 | mount = FAE.mount_ (QuerySelector "#mount-point") 57 | { init: "" :> [] 58 | , subscribe: [] 59 | , update 60 | , view 61 | } 62 | -------------------------------------------------------------------------------- /test/Basic/Effectful.purs: -------------------------------------------------------------------------------- 1 | module Test.Basic.Effectful (mount) where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Effect (Effect) 7 | import Effect.Aff (Aff) 8 | import Flame (QuerySelector(..), Html, (:>)) 9 | import Flame.Application.Effectful (Environment) 10 | import Flame.Application.Effectful as FAE 11 | import Flame.Html.Attribute as HA 12 | import Flame.Html.Element as HE 13 | 14 | type Model = 15 | { increments ∷ Int 16 | , decrements ∷ Int 17 | , luckyNumber ∷ Int 18 | } 19 | 20 | data Message = Increment | Decrement | Bogus 21 | 22 | init ∷ Model 23 | init = { increments: 0, decrements: 0, luckyNumber: 0 } 24 | 25 | update ∷ Environment Model Message → Aff (Model → Model) 26 | update { display, model, message } = case message of 27 | Increment → do 28 | display (const $ model { luckyNumber = model.increments - 2 }) 29 | pure <<< const $ model { increments = model.increments + 1 } 30 | Decrement → pure (_ { luckyNumber = model.increments + 2, decrements = model.decrements - 1 }) 31 | Bogus → FAE.noChanges 32 | 33 | view ∷ Model → Html Message 34 | view model = HE.main_ 35 | [ HE.span "text-output-increment" $ show model.increments 36 | , HE.span "text-output-decrement" $ show model.decrements 37 | , HE.span "text-output-lucky-number" $ show model.luckyNumber 38 | , HE.br 39 | , 40 | --we add extra events for each button to test if the correct message is used 41 | HE.button [ HA.id "decrement-button", HA.onClick Decrement, HA.onFocus Increment, HA.onDrag Increment ] "-" 42 | , HE.button [ HA.id "increment-button", HA.onClick Increment, HA.onFocus Decrement, HA.onDrag Bogus ] "+" 43 | ] 44 | 45 | mount ∷ Effect Unit 46 | mount = FAE.mount_ (QuerySelector "#mount-point") 47 | { init: init :> Just Decrement 48 | , subscribe: [] 49 | , update 50 | , view 51 | } 52 | -------------------------------------------------------------------------------- /test/Basic/NoEffects.purs: -------------------------------------------------------------------------------- 1 | module Test.Basic.NoEffects (mount) where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Flame (QuerySelector(..), Html) 7 | import Flame.Application.NoEffects as FAN 8 | import Flame.Html.Element as HE 9 | import Flame.Html.Attribute as HA 10 | 11 | type Model = Int 12 | 13 | data Message = Increment | Decrement 14 | 15 | init ∷ Model 16 | init = 0 17 | 18 | update ∷ Model → Message → Model 19 | update model = case _ of 20 | Increment → model + 1 21 | Decrement → model - 1 22 | 23 | view ∷ Model → Html Message 24 | view model = HE.main_ 25 | [ HE.button [ HA.id "decrement-button", HA.onClick Decrement ] "-" 26 | , HE.span "text-output" $ show model 27 | , HE.button [ HA.id "increment-button", HA.onClick Increment ] "+" 28 | ] 29 | 30 | mount ∷ Effect Unit 31 | mount = FAN.mount_ (QuerySelector "#mount-point") 32 | { init 33 | , subscribe: [] 34 | , update 35 | , view 36 | } 37 | -------------------------------------------------------------------------------- /test/Effectful/SlowEffects.purs: -------------------------------------------------------------------------------- 1 | module Test.Effectful.SlowEffects (mount) where 2 | 3 | import Prelude 4 | 5 | import Data.Array as DA 6 | import Data.Maybe (Maybe(..)) 7 | import Effect (Effect) 8 | import Effect.Aff (Aff, Milliseconds(..)) 9 | import Effect.Aff as EA 10 | import Flame (QuerySelector(..), Html, (:>)) 11 | import Flame.Application.Effectful (Environment) 12 | import Flame.Application.Effectful as FAE 13 | import Flame.Html.Attribute as HA 14 | import Flame.Html.Element as HE 15 | 16 | type Model = 17 | { current ∷ Int 18 | , numbers ∷ Array Int 19 | } 20 | 21 | data Message = Bump | BumpAndSnoc 22 | 23 | init ∷ Model 24 | init = { current: 0, numbers: [] } 25 | 26 | update ∷ Environment Model Message → Aff (Model → Model) 27 | update { message } = case message of 28 | Bump → pure $ \m@{ current } → m { current = current + 1 } 29 | BumpAndSnoc → do 30 | EA.delay $ Milliseconds 500.0 31 | pure $ \m@{ current, numbers } → m { current = current + 1, numbers = DA.snoc numbers 0 } 32 | 33 | view ∷ Model → Html Message 34 | view { current, numbers } = HE.main_ 35 | [ HE.span "text-output-current" $ show current 36 | , HE.span "text-output-numbers" $ show numbers 37 | , HE.br 38 | , HE.button [ HA.id "bump-button", HA.onClick Bump ] "-" 39 | , HE.button [ HA.id "snoc-button", HA.onClick BumpAndSnoc ] "+" 40 | ] 41 | 42 | mount ∷ Effect Unit 43 | mount = FAE.mount_ (QuerySelector "#mount-point") 44 | { init: init :> Nothing 45 | , subscribe: [] 46 | , update 47 | , view 48 | } 49 | -------------------------------------------------------------------------------- /test/Functor/Basic.purs: -------------------------------------------------------------------------------- 1 | module Test.Functor.Basic where 2 | 3 | import Prelude 4 | 5 | import Data.Array ((!!)) 6 | import Data.Array as DA 7 | import Data.Maybe (Maybe(..)) 8 | import Data.Maybe as DM 9 | import Effect (Effect) 10 | import Flame (QuerySelector(..), Html) 11 | import Flame.Application.NoEffects as FAN 12 | import Flame.Html.Attribute as HA 13 | import Flame.Html.Element as HE 14 | 15 | type Model = Array NestedModel 16 | 17 | data Message = Add | Remove Int | CounterMessage Int NestedMessage 18 | 19 | init ∷ Model 20 | init = [] 21 | 22 | update ∷ Model → Message → Model 23 | update model = case _ of 24 | Add → DA.snoc model nestedInit 25 | Remove index → DM.fromMaybe model $ DA.deleteAt index model 26 | CounterMessage index message → 27 | case model !! index of 28 | Nothing → model 29 | Just model' → DM.fromMaybe model $ DA.updateAt index (nestedUpdate model' message) model 30 | 31 | view ∷ Model → Html Message 32 | view model = HE.main "main" 33 | [ HE.button [ HA.id "add-button", HA.onClick Add ] "Add" 34 | , HE.div_ $ DA.mapWithIndex viewCounter model 35 | ] 36 | where 37 | viewCounter index model' = HE.div [ HA.style { display: "flex" } ] 38 | [ CounterMessage index <$> nestedView index model' 39 | , HE.button [ HA.onClick $ Remove index ] "Remove" 40 | ] 41 | 42 | -- | The model represents the state of the app 43 | type NestedModel = Int 44 | 45 | -- | This datatype is used to signal events to `update` 46 | data NestedMessage = Increment | Decrement 47 | 48 | nestedInit ∷ NestedModel 49 | nestedInit = 0 50 | 51 | nestedUpdate ∷ NestedModel → NestedMessage → NestedModel 52 | nestedUpdate model = case _ of 53 | Increment → model + 1 54 | Decrement → model - 1 55 | 56 | -- | `view` updates the app markup whenever the model is updated 57 | nestedView ∷ Int → NestedModel → Html NestedMessage 58 | nestedView index model = HE.main ("main-" <> show index) 59 | [ HE.button [ HA.id ("decrement-button-" <> show index), HA.onClick Decrement ] "-" 60 | , HE.span ("text-output-" <> show index) $ show model 61 | , HE.button [ HA.id ("increment-button-" <> show index), HA.onClick Increment ] "+" 62 | ] 63 | 64 | mount ∷ Effect Unit 65 | mount = FAN.mount_ (QuerySelector "#mount-point") 66 | { init 67 | , subscribe: [] 68 | , update 69 | , view 70 | } 71 | -------------------------------------------------------------------------------- /test/Functor/Lazy.purs: -------------------------------------------------------------------------------- 1 | module Test.Functor.Lazy where 2 | 3 | import Prelude 4 | import Effect 5 | import Flame 6 | import Flame.Html.Element 7 | import Data.Maybe 8 | import Flame.Html.Element as H 9 | import Flame.Html.Attribute as HA 10 | import Flame.Html.Event as E 11 | 12 | data CounterMsg = Increment Int 13 | 14 | type CounterModel = { count ∷ Int } 15 | 16 | initCounter ∷ CounterModel 17 | initCounter = { count: 1 } 18 | 19 | updateCounter ∷ CounterModel → CounterMsg → CounterModel 20 | updateCounter model (Increment val) = model { count = model.count + val } 21 | 22 | counterView ∷ CounterModel → Html CounterMsg 23 | counterView = lazy Nothing counterView_ 24 | 25 | counterView_ ∷ CounterModel → Html CounterMsg 26 | counterView_ model = H.main "main" 27 | [ H.button [ HA.id "add-button", E.onClick $ Increment 1000 ] [ H.text $ "Current Value: " <> show model.count ] 28 | ] 29 | 30 | data Msg = PageMsg PageMsg 31 | 32 | data PageMsg = CounterMsg CounterMsg 33 | 34 | type Model = { counter ∷ CounterModel } 35 | 36 | init = { counter: initCounter } :> [] 37 | 38 | update model (PageMsg (CounterMsg msg)) = model { counter = updateCounter model.counter msg } :> [] 39 | 40 | view ∷ Model → Html Msg 41 | view model = H.div_ [ PageMsg <$> CounterMsg <$> counterView model.counter ] 42 | 43 | mount ∷ Effect Unit 44 | mount = mount_ (QuerySelector "#mount-point") 45 | { subscribe: [] 46 | , init 47 | , update 48 | , view 49 | } 50 | -------------------------------------------------------------------------------- /test/Main.js: -------------------------------------------------------------------------------- 1 | import jsdom from "jsdom"; 2 | const enviroment = (new jsdom.JSDOM('', { runScripts: "outside-only" })); 3 | 4 | global.window = enviroment.window; 5 | global.document = enviroment.window.document; 6 | global.SVGElement = enviroment.window.SVGElement; 7 | global.CustomEvent = enviroment.window.CustomEvent; 8 | 9 | export function unsafeCreateEnviroment() { 10 | //removes event listeners and child nodes 11 | document.body = document.body.cloneNode(false); 12 | document.body.innerHTML = '
'; 13 | } 14 | 15 | export function clickEvent() { 16 | return new window.Event('click', { bubbles: true }); 17 | } 18 | 19 | export function inputEvent() { 20 | return new window.Event('input', { bubbles: true }); 21 | } 22 | 23 | export function keydownEvent() { 24 | return new window.KeyboardEvent('keydown', { key: 'q', bubbles: true }); 25 | } 26 | 27 | export function enterPressedEvent() { 28 | return new window.KeyboardEvent('keypress', { key: 'Enter', bubbles: true }); 29 | } 30 | 31 | export function errorEvent() { 32 | return new window.Event('error', { bubbles: true }); 33 | } 34 | 35 | export function offlineEvent() { 36 | return new window.Event('offline', { bubbles: true }); 37 | } 38 | 39 | export function getCssText(node) { 40 | return node.style.cssText; 41 | } 42 | 43 | export function getAllAttributes(node) { 44 | let attributes = []; 45 | 46 | for (let i = 0; i < node.attributes.length; i++) 47 | attributes.push(node.attributes[i].name + ':' + node.attributes[i].value); 48 | 49 | return attributes.join(' '); 50 | } 51 | 52 | export function getAllProperties(node) { 53 | return function (list) { 54 | let properties = []; 55 | 56 | for (let p of list) 57 | if (node[p]) 58 | properties.push(node[p] + ''); 59 | 60 | return properties; 61 | }; 62 | } 63 | 64 | export function innerHtml_(node, html) { 65 | node.innerHTML = html; 66 | } 67 | 68 | export function createSvg() { 69 | return document.createElementNS('http://www.w3.org/1999/xhtml', 'svg'); 70 | } 71 | 72 | export function createDiv() { 73 | return document.createElement('div'); 74 | } -------------------------------------------------------------------------------- /test/ServerSideRendering/Effectful.js: -------------------------------------------------------------------------------- 1 | export function setInnerHTML(selector, html) { 2 | document.querySelector(selector).innerHTML = html; 3 | } -------------------------------------------------------------------------------- /test/ServerSideRendering/Effectful.purs: -------------------------------------------------------------------------------- 1 | module Test.ServerSideRendering.Effectful (preMount, mount) where 2 | 3 | import Prelude 4 | 5 | import Data.Generic.Rep (class Generic) 6 | import Data.Maybe (Maybe(..)) 7 | import Effect (Effect) 8 | import Effect.Uncurried (EffectFn2) 9 | import Effect.Uncurried as EU 10 | import Flame (QuerySelector(..), Html) 11 | import Flame.Application.Effectful (AffUpdate) 12 | import Flame.Application.Effectful as FAE 13 | import Flame.Html.Attribute as HA 14 | import Flame as F 15 | import Flame.Html.Element as HE 16 | import Web.Event.Internal.Types (Event) 17 | 18 | foreign import setInnerHTML ∷ EffectFn2 String String Unit 19 | 20 | -- | The model represents the state of the app 21 | newtype Model = Model Int 22 | 23 | derive instance genericModel ∷ Generic Model _ 24 | 25 | -- | This datatype is used to signal events to `update` 26 | data Message = Increment | Decrement Event 27 | 28 | -- | `update` is called to handle events 29 | update ∷ AffUpdate Model Message 30 | update { model: Model m, message } = 31 | pure $ const 32 | ( Model $ case message of 33 | Increment → m + 1 34 | Decrement _ → m - 1 35 | ) 36 | 37 | -- | `view` is called whenever the model is updated 38 | view ∷ Model → Html Message 39 | view model = HE.main "my-id" $ children model <> [ HE.span_ "rendered!" ] 40 | 41 | preView ∷ Model → Html Message 42 | preView model = HE.main "my-id" $ children model 43 | 44 | children ∷ Model → Array (Html Message) 45 | children (Model model) = 46 | [ HE.span "text-output" $ show model 47 | , HE.br 48 | , HE.button [ HA.id "increment-button", HA.onClick Increment ] "+" 49 | ] 50 | 51 | preMount ∷ Effect Unit 52 | preMount = do 53 | contents ← F.preMount (QuerySelector "#my-id") { init: Model 2, view: preView } 54 | EU.runEffectFn2 setInnerHTML "#mount-point" contents 55 | 56 | -- | Mount the application on the given selector 57 | mount ∷ Effect Unit 58 | mount = FAE.resumeMount_ (QuerySelector "#my-id") 59 | { init: Nothing 60 | , subscribe: [] 61 | , update 62 | , view 63 | } -------------------------------------------------------------------------------- /test/ServerSideRendering/FragmentNode.js: -------------------------------------------------------------------------------- 1 | export function setInnerHTML(selector, html) { 2 | document.querySelector(selector).innerHTML = html; 3 | } -------------------------------------------------------------------------------- /test/ServerSideRendering/FragmentNode.purs: -------------------------------------------------------------------------------- 1 | module Test.ServerSideRendering.FragmentNode (preMount, mount) where 2 | 3 | import Prelude 4 | 5 | import Data.Generic.Rep (class Generic) 6 | import Data.Maybe (Maybe(..)) 7 | import Effect (Effect) 8 | import Effect.Uncurried (EffectFn2) 9 | import Effect.Uncurried as EU 10 | import Flame (QuerySelector(..), Html) 11 | import Flame.Application.Effectful (AffUpdate) 12 | import Flame.Application.Effectful as FAE 13 | import Flame.Html.Attribute as HA 14 | import Flame as F 15 | import Flame.Html.Element as HE 16 | import Web.Event.Internal.Types (Event) 17 | 18 | foreign import setInnerHTML ∷ EffectFn2 String String Unit 19 | 20 | -- | The model represents the state of the app 21 | newtype Model = Model Int 22 | 23 | derive instance genericModel ∷ Generic Model _ 24 | 25 | -- | This datatype is used to signal events to `update` 26 | data Message = Increment | Decrement Event 27 | 28 | -- | `update` is called to handle events 29 | update ∷ AffUpdate Model Message 30 | update { model: Model m, message } = 31 | pure $ const 32 | ( Model $ case message of 33 | Increment → m + 1 34 | Decrement _ → m - 1 35 | ) 36 | 37 | -- | `view` is called whenever the model is updated 38 | view ∷ Model → Html Message 39 | view model = HE.main "my-id" $ children model <> [ HE.span_ "rendered!" ] 40 | 41 | preView ∷ Model → Html Message 42 | preView model = HE.main "my-id" $ children model 43 | 44 | children ∷ Model → Array (Html Message) 45 | children (Model model) = 46 | [ HE.fragment 47 | [ HE.span "text-output" $ show model 48 | , HE.br 49 | ] 50 | , HE.button [ HA.id "increment-button", HA.onClick Increment ] "+" 51 | ] 52 | 53 | preMount ∷ Effect Unit 54 | preMount = do 55 | contents ← F.preMount (QuerySelector "#my-id") { init: Model 2, view: preView } 56 | EU.runEffectFn2 setInnerHTML "#mount-point" contents 57 | 58 | -- | Mount the application on the given selector 59 | mount ∷ Effect Unit 60 | mount = FAE.resumeMount_ (QuerySelector "#my-id") 61 | { init: Nothing 62 | , subscribe: [] 63 | , update 64 | , view 65 | } -------------------------------------------------------------------------------- /test/ServerSideRendering/ManagedNode.js: -------------------------------------------------------------------------------- 1 | export function setInnerHTML(selector, html) { 2 | document.querySelector(selector).innerHTML = html; 3 | } 4 | 5 | export function setElementInnerHTML(element, html) { 6 | element.innerHTML = html; 7 | } -------------------------------------------------------------------------------- /test/ServerSideRendering/ManagedNode.purs: -------------------------------------------------------------------------------- 1 | module Test.ServerSideRendering.ManagedNode (preMount, mount) where 2 | 3 | import Prelude 4 | 5 | import Data.Generic.Rep (class Generic) 6 | import Data.Maybe (Maybe(..), fromJust) 7 | import Effect (Effect) 8 | import Effect.Uncurried (EffectFn2) 9 | import Effect.Uncurried as EU 10 | import Flame (QuerySelector(..), Html) 11 | import Flame as F 12 | import Flame.Application.Effectful (AffUpdate) 13 | import Flame.Application.Effectful as FAE 14 | import Flame.Html.Attribute as HA 15 | import Flame.Html.Element (NodeRenderer) 16 | import Flame.Html.Element as HE 17 | import Partial.Unsafe as PU 18 | import Web.DOM.Document as WDD 19 | import Web.DOM.Element (Element) 20 | import Web.DOM.Element as WDE 21 | import Web.Event.Internal.Types (Event) 22 | import Web.HTML as WH 23 | import Web.HTML.HTMLDocument as WHH 24 | import Web.HTML.Window as WHW 25 | 26 | foreign import setInnerHTML ∷ EffectFn2 String String Unit 27 | foreign import setElementInnerHTML ∷ EffectFn2 Element String Unit 28 | 29 | -- | The model represents the state of the app 30 | newtype Model = Model Int 31 | 32 | derive instance genericModel ∷ Generic Model _ 33 | 34 | -- | This datatype is used to signal events to `update` 35 | data Message = Increment | Decrement Event 36 | 37 | -- | `update` is called to handle events 38 | update ∷ AffUpdate Model Message 39 | update { model: Model m, message } = 40 | pure $ const 41 | ( Model $ case message of 42 | Increment → m + 1 43 | Decrement _ → m - 1 44 | ) 45 | 46 | -- | `view` is called whenever the model is updated 47 | view ∷ Model → Html Message 48 | view model = HE.main "my-id" $ children model 49 | 50 | preView ∷ Model → Html Message 51 | preView model = HE.main "my-id" $ children model 52 | 53 | nodeRenderer ∷ NodeRenderer Int 54 | nodeRenderer = 55 | { createNode: \arg → do 56 | window ← WH.window 57 | document ← WHW.document window 58 | element ← WDD.createElement "span" $ WHH.toDocument document 59 | EU.runEffectFn2 setElementInnerHTML element $ show arg 60 | pure $ WDE.toNode element 61 | , updateNode: \node _ arg → do 62 | EU.runEffectFn2 setElementInnerHTML (PU.unsafePartial (fromJust $ WDE.fromNode node)) $ show arg 63 | pure node 64 | } 65 | 66 | children ∷ Model → Array (Html Message) 67 | children (Model model) = 68 | [ HE.managed nodeRenderer [ HA.id "text-output" ] model 69 | , HE.br 70 | , HE.button [ HA.id "increment-button", HA.onClick Increment ] "+" 71 | ] 72 | 73 | preMount ∷ Effect Unit 74 | preMount = do 75 | contents ← F.preMount (QuerySelector "#my-id") { init: Model 2, view: preView } 76 | EU.runEffectFn2 setInnerHTML "#mount-point" contents 77 | 78 | -- | Mount the application on the given selector 79 | mount ∷ Effect Unit 80 | mount = FAE.resumeMount_ (QuerySelector "#my-id") 81 | { init: Nothing 82 | , subscribe: [] 83 | , update 84 | , view 85 | } -------------------------------------------------------------------------------- /test/Subscription/Broadcast.purs: -------------------------------------------------------------------------------- 1 | module Test.Subscription.Broadcast (mount, TSBMessage(..)) where 2 | 3 | -- | Counter example using a side effects free function 4 | 5 | import Prelude 6 | 7 | import Data.Maybe (Maybe) 8 | import Data.Maybe as DM 9 | import Data.Tuple (Tuple) 10 | import Effect (Effect) 11 | import Effect.Aff (Aff) 12 | import Flame (QuerySelector(..), Html, (:>)) 13 | import Flame.Application.EffectList as FAE 14 | import Flame.Html.Element as HE 15 | import Flame.Subscription as FS 16 | import Web.Event.Event (EventType(..)) 17 | 18 | -- | The model represents the state of the app 19 | type Model = Int 20 | 21 | -- | This datatype is used to signal events to `update` 22 | data TSBMessage = TEELIncrement | TEELDecrement (Maybe Int) 23 | 24 | -- | `update` is called to handle events 25 | update ∷ Model → TSBMessage → Tuple Model (Array (Aff (Maybe TSBMessage))) 26 | update model = case _ of 27 | TEELIncrement → (model + 1) :> [] 28 | TEELDecrement amount → (model - (DM.fromMaybe 1 amount)) :> [] 29 | 30 | -- | `view` is called whenever the model is updated 31 | view ∷ Model → Html TSBMessage 32 | view model = HE.main "main" 33 | [ HE.span "text-output" $ show model 34 | ] 35 | 36 | -- | Mount the application on the given selector 37 | mount ∷ Effect Unit 38 | mount = do 39 | FAE.mount_ (QuerySelector "#mount-point") 40 | { init: 0 :> [] 41 | , subscribe: [ FS.onCustomEvent' (EventType "increment-event") TEELIncrement, FS.onCustomEvent (EventType "decrement-event") TEELDecrement ] 42 | , update 43 | , view 44 | } -------------------------------------------------------------------------------- /test/Subscription/EffectList.purs: -------------------------------------------------------------------------------- 1 | module Test.Subscription.EffectList (mount, TEELMessage(..)) where 2 | 3 | -- | Counter example using a side effects free function 4 | 5 | import Prelude 6 | 7 | import Data.Maybe (Maybe) 8 | import Data.Tuple (Tuple) 9 | import Effect (Effect) 10 | import Effect.Aff (Aff) 11 | import Flame (QuerySelector(..), Html, (:>)) 12 | import Flame.Application.EffectList as FAE 13 | import Flame.Html.Element as HE 14 | import Flame.Types (AppId(..)) 15 | 16 | -- | The model represents the state of the app 17 | type Model = Int 18 | 19 | -- | This datatype is used to signal events to `update` 20 | data TEELMessage = TEELIncrement | TEELDecrement 21 | 22 | -- | `update` is called to handle events 23 | update ∷ Model → TEELMessage → Tuple Model (Array (Aff (Maybe TEELMessage))) 24 | update model = case _ of 25 | TEELIncrement → (model + 1) :> [] 26 | TEELDecrement → (model - 1) :> [] 27 | 28 | -- | `view` is called whenever the model is updated 29 | view ∷ Model → Html TEELMessage 30 | view model = HE.main "main" 31 | [ HE.span "text-output" $ show model 32 | ] 33 | 34 | -- | Mount the application on the given selector 35 | mount ∷ Effect (AppId String TEELMessage) 36 | mount = do 37 | let id = AppId "teel" 38 | FAE.mount (QuerySelector "#mount-point") id 39 | { init: 0 :> [] 40 | , subscribe: [] 41 | , update 42 | , view 43 | } 44 | pure id -------------------------------------------------------------------------------- /test/Subscription/Effectful.purs: -------------------------------------------------------------------------------- 1 | module Test.Subscription.Effectful (mount) where 2 | 3 | -- | Counter example using a side effects free function 4 | 5 | import Prelude 6 | 7 | import Data.Maybe (Maybe(..)) 8 | import Effect (Effect) 9 | import Flame (QuerySelector(..), Html, (:>)) 10 | import Flame.Application.Effectful (AffUpdate) 11 | import Flame.Application.Effectful as FAE 12 | import Flame.Html.Attribute as HA 13 | import Flame.Html.Element as HE 14 | import Flame.Subscription.Window as FEW 15 | import Web.Event.Internal.Types (Event) 16 | 17 | -- | The model represents the state of the app 18 | type Model = Int 19 | 20 | -- | This datatype is used to signal events to `update` 21 | data Message = Increment | Decrement Event 22 | 23 | -- | `update` is called to handle events 24 | update ∷ AffUpdate Model Message 25 | update { model, message } = 26 | pure $ 27 | ( case message of 28 | Increment → (_ + 1) 29 | Decrement _ → (_ - 1) 30 | ) 31 | 32 | -- | `view` is called whenever the model is updated 33 | view ∷ Model → Html Message 34 | view model = HE.main "main" 35 | [ HE.span "text-output" $ show model 36 | , HE.br 37 | , HE.button (HA.onClick Increment) "+" 38 | ] 39 | 40 | -- | Mount the application on the given selector 41 | mount ∷ Effect Unit 42 | mount = do 43 | FAE.mount_ (QuerySelector "#mount-point") 44 | { init: 5 :> Nothing 45 | , subscribe: [ FEW.onError' Decrement, FEW.onOffline Increment ] 46 | , update 47 | , view 48 | } -------------------------------------------------------------------------------- /test/Subscription/NoEffects.purs: -------------------------------------------------------------------------------- 1 | module Test.Subscription.NoEffects (mount) where 2 | 3 | -- | Counter example using a side effects free function 4 | 5 | import Prelude 6 | 7 | import Effect (Effect) 8 | import Flame (QuerySelector(..), Html) 9 | import Flame.Application.NoEffects as FAN 10 | import Flame.Html.Element as HE 11 | import Flame.Subscription.Document as FED 12 | import Web.Event.Internal.Types (Event) 13 | 14 | -- | The model represents the state of the app 15 | type Model = Int 16 | 17 | -- | This datatype is used to signal events to `update` 18 | data Message = Increment String | Decrement Event 19 | 20 | -- | `update` is called to handle events 21 | update ∷ Model → Message → Model 22 | update model = case _ of 23 | Increment _ → model + 1 24 | Decrement _ → model - 1 25 | 26 | -- | `view` is called whenever the model is updated 27 | view ∷ Model → Html Message 28 | view model = HE.main "main" 29 | [ HE.span "text-output" $ show model 30 | ] 31 | 32 | -- | Mount the application on the given selector 33 | mount ∷ Effect Unit 34 | mount = do 35 | FAN.mount_ (QuerySelector "#mount-point") 36 | { init: 0 37 | , subscribe: [ FED.onClick' Decrement, FED.onKeydown Increment ] 38 | , update 39 | , view 40 | } --------------------------------------------------------------------------------