├── .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 
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 | 
12 | 
13 |
14 | 
15 | 
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 | Previous: Getting started
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 | Previous: Handling events
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 | Previous: Main concepts
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('' + tag + '>');
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 | }
--------------------------------------------------------------------------------