├── src
└── Pha
│ ├── Html
│ ├── Events.js
│ ├── Util.purs
│ ├── Keyed.purs
│ ├── Core.js
│ ├── Attributes.purs
│ ├── Elements.purs
│ ├── Core.purs
│ └── Events.purs
│ ├── Html.purs
│ ├── App
│ ├── Internal.purs
│ └── Internal.js
│ ├── Update.purs
│ ├── Util.js
│ ├── Util.purs
│ ├── Subscriptions.purs
│ ├── Svg.purs
│ ├── Svg
│ └── Attributes.purs
│ ├── Update
│ └── Internal.purs
│ └── App.purs
├── test.dhall
├── packages.dhall
├── .gitignore
├── examples
├── dist
│ ├── ex-counter2.html
│ └── bundle.css
├── Counter.purs
└── Counter2.purs
├── package.json
├── spago.dhall
├── LICENSE
└── README.md
/src/Pha/Html/Events.js:
--------------------------------------------------------------------------------
1 | export const valueImpl = (el, nothing, just) =>
2 | typeof el.value === "string" ? just(el.value) : nothing
--------------------------------------------------------------------------------
/test.dhall:
--------------------------------------------------------------------------------
1 | let conf = ./spago.dhall
2 | in conf // {
3 | sources = conf.sources # [ "examples/**/*.purs" ],
4 | backend = "purs-backend-es build",
5 | dependencies = conf.dependencies # ["arrays", "integers"]
6 | }
--------------------------------------------------------------------------------
/packages.dhall:
--------------------------------------------------------------------------------
1 | let upstream =
2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.12-20231104/packages.dhall
3 | sha256:79344f3278e5c29920e693ee057ea75347755c019f6fe28fb8a0e06de2a85bfc
4 |
5 | in upstream
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /bower_components/
2 | /node_modules/
3 | /.pulp-cache/
4 | /output/
5 | /output-es/
6 | /generated-docs/
7 | /.psc-package/
8 | /.psc*
9 | /.purs*
10 | /.psa*
11 | /.spago
12 | /yarn.lock
13 | /package-lock.json
14 | /examples/dist/bundle-counter2.js
15 |
--------------------------------------------------------------------------------
/examples/dist/ex-counter2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Purescript Pha - Example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/Pha/Html.purs:
--------------------------------------------------------------------------------
1 | module Pha.Html (module Export) where
2 |
3 | import Pha.Html.Core (Event, EventHandler, Prop, Html, attr, class', class_, empty, fromMaybe, elem, keyed, lazy, lazy2, lazy3, lazy4, lazy5, maybe, on_, noProp, prop, style, text, when) as Export
4 | import Pha.Html.Elements (a, br, button, div, footer, form, h1, h2, h3, h4, h5, h6, header, hr, img, input, label, li, main, nav, ol, option, p, section, select, span, table, td, textarea, tr, ul) as Export
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "esbuild": "^0.19.5",
4 | "purescript": "^0.15.12",
5 | "purescript-psa": "^0.8.2",
6 | "purs-backend-es": "^1.4.2",
7 | "spago": "^0.21.0"
8 | },
9 | "scripts": {
10 | "clean": "rimraf output && rimraf .spago",
11 | "build": "spago build --purs-args '--censor-lib --strict'",
12 | "example-counter2": "spago -x test.dhall build && purs-backend-es bundle-app --no-build -m Example.Counter2 --to examples/dist/bundle-counter2.js"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Pha/App/Internal.purs:
--------------------------------------------------------------------------------
1 | module Pha.App.Internal where
2 | import Prelude
3 | import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn5)
4 | import Pha.Html.Core (Html, Event, EventHandler)
5 | import Web.Event.EventTarget (EventTarget)
6 | import Web.DOM.Node (Node)
7 |
8 | foreign import getAction ∷ ∀msg. EffectFn2 EventTarget String (EventHandler msg)
9 | foreign import unsafePatch ∷ ∀msg. EffectFn5 Node Node (Html msg) (Html msg) (EffectFn1 Event Unit) Node
10 | foreign import copyVNode ∷ ∀msg. Html msg → Html msg
11 | foreign import unsafeLinkNode ∷ ∀msg. Node → Html msg → Html msg
--------------------------------------------------------------------------------
/src/Pha/Update.purs:
--------------------------------------------------------------------------------
1 | module Pha.Update
2 | ( delay
3 | , module I
4 | , module Exports
5 | ) where
6 |
7 | import Prelude
8 | import Pha.Update.Internal (Subscription, SubscriptionId, Update, hoist, mapMessage, mapModel, subscribe, unsubscribe) as I
9 | import Control.Monad.State.Class (get, gets, put, modify, modify_) as Exports
10 | import Effect.Aff (Milliseconds)
11 | import Effect.Aff as Aff
12 | import Effect.Aff.Class (class MonadAff, liftAff)
13 | import Effect.Aff (Milliseconds(..)) as Exports
14 | import Effect.Aff.Class (liftAff) as Exports
15 | import Effect.Class (liftEffect) as Exports
16 |
17 | delay ∷ ∀model msg m. MonadAff m ⇒ Milliseconds → I.Update model msg m Unit
18 | delay = liftAff <<< Aff.delay
19 |
--------------------------------------------------------------------------------
/examples/dist/bundle.css:
--------------------------------------------------------------------------------
1 | .counter {
2 | width: 5rem;
3 | height: 2rem;
4 | background-color: snow;
5 | border: solid black thick;
6 | }
7 |
8 | .box {
9 | width: 5rem;
10 | height: 5rem;
11 | background-color: snow;
12 | border: solid black thick;
13 | }
14 |
15 | .box.even {
16 | background-color: green;
17 | }
18 |
19 | .puzzle {
20 | position: relative;
21 | width: 12rem;
22 | height: 12rem;
23 | }
24 |
25 | .puzzle-item {
26 | position: absolute;
27 | transition: all 500ms linear;
28 | width: 25%;
29 | height: 25%;
30 | background-color: yellow;
31 | border: solid grey thin;
32 | text-align: center;
33 | vertical-align: center;
34 | }
--------------------------------------------------------------------------------
/spago.dhall:
--------------------------------------------------------------------------------
1 | {-
2 | Welcome to a Spago project!
3 | You can edit this file as you like.
4 | -}
5 | { name = "pha"
6 | , license = "MIT"
7 | , dependencies =
8 | [ "aff"
9 | , "bifunctors"
10 | , "effect"
11 | , "foldable-traversable"
12 | , "free"
13 | , "functions"
14 | , "maybe"
15 | , "ordered-collections"
16 | , "prelude"
17 | , "profunctor-lenses"
18 | , "refs"
19 | , "tailrec"
20 | , "transformers"
21 | , "tuples"
22 | , "unsafe-coerce"
23 | , "unsafe-reference"
24 | , "web-dom"
25 | , "web-events"
26 | , "web-html"
27 | , "web-pointerevents"
28 | , "web-uievents"
29 | ]
30 | , packages = ./packages.dhall
31 | , repository = "https://github.com/gbagan/purescript-pha"
32 | , sources = [ "src/**/*.purs" ]
33 | }
34 |
--------------------------------------------------------------------------------
/examples/Counter.purs:
--------------------------------------------------------------------------------
1 | module Example.Counter where
2 | import Prelude
3 | import Effect (Effect)
4 | import Pha.Html (Html)
5 | import Pha.App (sandbox)
6 | import Pha.Html as H
7 | import Pha.Html.Events as E
8 |
9 | type State = Int
10 | data Msg = Increment | Decrement
11 |
12 | init ∷ State
13 | init = 0
14 |
15 | update ∷ Msg → State → State
16 | update Increment n = n + 1
17 | update Decrement n = n - 1
18 |
19 | view ∷ State → Html Msg
20 | view counter =
21 | H.div []
22 | [ H.button [E.onClick \_ → Decrement] [H.text "-"]
23 | , H.span [] [H.text $ show counter]
24 | , H.button [E.onClick \_ → Increment] [H.text "+"]
25 | ]
26 |
27 | main ∷ Effect Unit
28 | main = sandbox {init, update, view, selector: "#root"}
--------------------------------------------------------------------------------
/src/Pha/Html/Util.purs:
--------------------------------------------------------------------------------
1 | module Pha.Html.Util
2 | where
3 |
4 | import Prelude
5 |
6 | import Pha.Svg.Attributes (class IsLength, toString)
7 |
8 | -- | 63.7 → "63.7px"
9 | px ∷ Number → String
10 | px a = show a <> "px"
11 |
12 | -- | 63 → 63.px
13 | px' ∷ Int → String
14 | px' a = show a <> "px"
15 |
16 | -- | 0.7 → "70%"
17 | pc ∷ Number → String
18 | pc a = show (100.0 * a) <> "%"
19 |
20 | -- | translate (px 40.0) (px 30.0) → "translate(40px,30px)"
21 | translate ∷ forall x y. IsLength x => IsLength y => x → y → String
22 | translate x y = "translate(" <> toString x <> "," <> toString y <> ")"
23 |
24 | -- | rgbColor 128 64 30 → "rgb(128,64,30)"
25 | rgbColor ∷ Int → Int → Int → String
26 | rgbColor r g' b = "rgb(" <> show r <> "," <> show g' <> "," <> show b <> ")"
--------------------------------------------------------------------------------
/src/Pha/Util.js:
--------------------------------------------------------------------------------
1 | const objEq = (a, b) => {
2 | if (a === undefined)
3 | return false
4 | for (let x in a)
5 | if (a[x] !== b[x])
6 | return false
7 | return true
8 | }
9 |
10 | export const memoizeImpl = f => g => {
11 | let u = undefined;
12 | let a = undefined;
13 | let res = undefined;
14 | return v => {
15 | if (u === v)
16 | return res
17 | u = v;
18 | const b = f(v);
19 | if (a === b)
20 | return res;
21 | a = b;
22 | res = g(b);
23 | return res;
24 | }
25 | }
26 |
27 | export const memoizeObj = f => g => {
28 | let u = undefined;
29 | let a = undefined;
30 | let res = undefined;
31 | return v => {
32 | if (u === v)
33 | return res;
34 | u = v;
35 | const b = f(v);
36 | if (objEq(a, b))
37 | return res;
38 | a = b;
39 | res = g(b);
40 | return res;
41 | }
42 | }
--------------------------------------------------------------------------------
/src/Pha/Util.purs:
--------------------------------------------------------------------------------
1 | module Pha.Util (memoize, memoCompose, memoCompose2, memoCompose3, memoCompose') where
2 |
3 | import Prelude
4 |
5 | foreign import memoizeImpl :: forall a b c. (a -> b) -> (b -> c) -> a -> c
6 | foreign import memoizeObj :: forall a b c. (a -> b) -> (b -> c) -> a -> c
7 |
8 | -- | Memoize the function f.
9 | -- | If the argument of f differs from the previous call, then f is recomputed.
10 | memoize :: forall a b. (a -> b) -> a -> b
11 | memoize = memoizeImpl identity
12 |
13 | -- | Memoize the composition of two functions
14 | memoCompose :: forall a b c. (a -> b) -> (b -> c) -> a -> c
15 | memoCompose = memoizeImpl
16 |
17 | memoCompose2 :: forall a b c d. (a -> b) -> (a -> c) -> (b -> c -> d) -> a -> d
18 | memoCompose2 f g h = memoCompose' (\v -> {a: f v, b: g v}) \{a, b} -> h a b
19 |
20 | memoCompose3 :: forall a b c d e. (a -> b) -> (a -> c) -> (a -> d) -> (b -> c -> d -> e) -> a -> e
21 | memoCompose3 f g h l = memoCompose' (\v -> {a: f v, b: g v, c: h v}) \{a, b, c} -> l a b c
22 |
23 | memoCompose' :: forall a b c. (a -> Record b) -> (Record b -> c) -> a -> c
24 | memoCompose' = memoizeObj
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Guillaume Bagan
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 |
--------------------------------------------------------------------------------
/src/Pha/Html/Keyed.purs:
--------------------------------------------------------------------------------
1 | module Pha.Html.Keyed where
2 |
3 | import Pha.Html.Core (Html, KeyedHtml, Prop, keyed)
4 |
5 | a ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
6 | a = keyed "a"
7 |
8 | button ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
9 | button = keyed "button"
10 |
11 | div ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
12 | div = keyed "div"
13 |
14 | footer ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
15 | footer = keyed "footer"
16 |
17 | form ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
18 | form = keyed "form"
19 |
20 | header ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
21 | header = keyed "header"
22 |
23 | li ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
24 | li = keyed "li"
25 |
26 | p ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
27 | p = keyed "p"
28 |
29 | main ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
30 | main = keyed "main"
31 |
32 | menu ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
33 | menu = keyed "menu"
34 |
35 | nav ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
36 | nav = keyed "nav"
37 |
38 | ol ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
39 | ol = keyed "ol"
40 |
41 | select ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
42 | select = keyed "select"
43 |
44 | section ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
45 | section = keyed "section"
46 |
47 | span ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
48 | span = keyed "span"
49 |
50 | ul ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
51 | ul = keyed "ul"
52 |
53 |
54 | --- SVG
55 |
56 | svg ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
57 | svg = keyed "svg"
58 |
59 | g ∷ ∀msg. Array (Prop msg) → Array (KeyedHtml msg) → Html msg
60 | g = keyed "g"
61 |
--------------------------------------------------------------------------------
/src/Pha/Subscriptions.purs:
--------------------------------------------------------------------------------
1 | module Pha.Subscriptions (eventListener, onKeyDown, onHashChange, onResize) where
2 |
3 | import Prelude
4 |
5 | import Data.Maybe (Maybe(..))
6 | import Effect.Class (class MonadEffect, liftEffect)
7 | import Pha.Html.Core (EventHandler)
8 | import Pha.Update (SubscriptionId, Update, subscribe)
9 | import Web.Event.Event as E
10 | import Web.Event.EventTarget as ET
11 | import Web.HTML (window)
12 | import Web.HTML.Event.HashChangeEvent (HashChangeEvent)
13 | import Web.HTML.Event.HashChangeEvent as HCE
14 | import Web.HTML.Window as W
15 | import Web.UIEvent.KeyboardEvent as KE
16 | import Web.UIEvent.UIEvent as UI
17 |
18 | eventListener ∷ ∀ msg model m. String → ET.EventTarget → EventHandler msg → Update model msg m SubscriptionId
19 | eventListener name target decoder = subscribe \dispatch → do
20 | listener ← ET.eventListener (handleEvent dispatch)
21 | ET.addEventListener (E.EventType name) listener false target
22 | pure $ ET.removeEventListener (E.EventType name) listener false target
23 | where
24 | handleEvent dispatch ev =
25 | decoder ev >>= case _ of
26 | Nothing → pure unit
27 | Just msg → dispatch msg
28 |
29 | onKeyDown ∷ ∀ msg model m. MonadEffect m ⇒ (String → Maybe msg) → Update model msg m SubscriptionId
30 | onKeyDown f = do
31 | target ← liftEffect $ W.toEventTarget <$> window
32 | eventListener "keydown" target \ev → pure $ f =<< KE.key <$> KE.fromEvent ev
33 |
34 | onHashChange ∷ ∀ msg model m. MonadEffect m ⇒ (HashChangeEvent → Maybe msg) → Update model msg m SubscriptionId
35 | onHashChange f = do
36 | target ← liftEffect $ W.toEventTarget <$> window
37 | eventListener "hashchange" target \ev → pure $ f =<< HCE.fromEvent ev
38 |
39 | onResize ∷ ∀ msg model m. MonadEffect m ⇒ (UI.UIEvent → Maybe msg) → Update model msg m SubscriptionId
40 | onResize f = do
41 | target ← liftEffect $ W.toEventTarget <$> window
42 | eventListener "resize" target \ev → pure $ f =<< UI.fromEvent ev
--------------------------------------------------------------------------------
/src/Pha/Svg.purs:
--------------------------------------------------------------------------------
1 | module Pha.Svg where
2 |
3 | import Pha.Html.Core (Html, Prop, elem)
4 |
5 | --- SVG
6 |
7 | svg ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
8 | svg = elem "svg"
9 |
10 | circle ∷ ∀msg. Array (Prop msg) → Html msg
11 | circle props = elem "circle" props []
12 |
13 | clipPath ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
14 | clipPath = elem "clipPath"
15 |
16 | defs ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
17 | defs = elem "defs"
18 |
19 | ellipse ∷ ∀msg. Array (Prop msg) → Html msg
20 | ellipse props = elem "ellipse" props []
21 |
22 | foreignObject ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
23 | foreignObject = elem "foreignObject"
24 |
25 | g ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
26 | g = elem "g"
27 |
28 | image ∷ ∀msg. Array (Prop msg) → Html msg
29 | image attrs = elem "image" attrs []
30 |
31 | line ∷ ∀msg. Array (Prop msg) → Html msg
32 | line props = elem "line" props []
33 |
34 | linearGradient ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
35 | linearGradient = elem "linearGradient"
36 |
37 | path ∷ ∀msg. Array (Prop msg) → Html msg
38 | path props = elem "path" props []
39 |
40 | pattern ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
41 | pattern = elem "pattern"
42 |
43 | polygon ∷ ∀msg. Array (Prop msg) → Html msg
44 | polygon props = elem "polygon" props []
45 |
46 | polyline ∷ ∀msg. Array (Prop msg) → Html msg
47 | polyline props = elem "polyline" props []
48 |
49 | rect ∷ ∀msg. Array (Prop msg) → Html msg
50 | rect props = elem "rect" props []
51 |
52 | stop ∷ ∀msg. Array (Prop msg) → Html msg
53 | stop props = elem "stop" props []
54 |
55 | -- | Create a SVG text element
56 | text ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
57 | text = elem "text"
58 |
59 | textPath ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
60 | textPath = elem "textPath"
61 |
62 | tspan ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
63 | tspan = elem "tspan"
64 |
65 | use ∷ ∀msg. Array (Prop msg) → Html msg
66 | use props = elem "use" props []
--------------------------------------------------------------------------------
/src/Pha/Html/Core.js:
--------------------------------------------------------------------------------
1 | /*
2 | 0: property
3 | 1: attribute
4 | 2: event
5 | 3: class
6 | 4: style
7 | */
8 |
9 | const compose = (f, g) => f && g ? x => f(g(x)) : f || g
10 |
11 | const _h = (tag, ps, children, keyed=false) => {
12 | const style = []
13 | const props = {}
14 | const attrs = {}
15 | const events = {}
16 | const vdom = {tag, children, props, attrs, events, node: null, keyed}
17 | const n = ps.length
18 | for (let i = 0; i < n; i++) {
19 | const [t, k, v] = ps[i]
20 | if (t === 0)
21 | props[k] = v
22 | else if (t === 1)
23 | attrs[k] = v
24 | else if (t === 2)
25 | events[k] = v
26 | else if (t === 3)
27 | attrs.class = attrs.class ? attrs.class + " " + k : k
28 | else if (t === 4)
29 | style.push(k + ":" + v)
30 | }
31 | const style_ = style.join(";")
32 | if (style_)
33 | attrs.style = style_
34 | return vdom
35 | }
36 |
37 | export const elemImpl = (tag, ps, children) => _h(tag, ps, children.map(html => ({key: null, html})))
38 |
39 | export const keyedImpl = (tag, ps, children) => _h(tag, ps, children, true)
40 |
41 | const createTextVNode = text => ({
42 | tag: text,
43 | children: [],
44 | type: 3
45 | })
46 |
47 | export const mapView = (mapf, vnode) => ({...vnode, mapf: compose(vnode.mapf, mapf)})
48 | export const mapProp = (mapf, prop) => prop[0] == 2 ? [2, mapf(prop[1])] : prop
49 | export const propImpl = (k, v) => [0, k, v]
50 | export const attrImpl = (k, v) => [1, k, v]
51 | export const unsafeOnWithEffectImpl = (k, v) => [2, k, v]
52 | export const class_ = cls => [3, cls]
53 | export const noProp = [-1]
54 | export const styleImpl = (k, v) => [4, k, v]
55 | export const text = createTextVNode
56 | export const lazyImpl = (view, val) => ({ memo: [val], type: view})
57 | export const lazy2Impl = (view, val1, val2) => ({ memo: [val1, val2], type: view})
58 | export const lazy3Impl = (view, val1, val2, val3) => ({ memo: [val1, val2, val3], type: view})
59 | export const lazy4Impl = (view, val1, val2, val3, val4) => ({ memo: [val1, val2, val3, val4], type: view})
60 | export const lazy5Impl = (view, val1, val2, val3, val4, val5) => ({ memo: [val1, val2, val3, val4, val5], type: view})
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # purescript-pha
2 | a simple and fast Elm-like library also inspired by Halogen.
3 |
4 | ### Documentation
5 | Documentation is [published on Pursuit](https://pursuit.purescript.org/packages/purescript-pha)
6 |
7 | ### Minimal example
8 | ```purescript
9 | module Example.Counter where
10 | import Prelude
11 | import Effect (Effect)
12 | import Pha.App (sandbox)
13 | import Pha.Html (Html)
14 | import Pha.Html as H
15 | import Pha.Html.Attributes as P
16 | import Pha.Html.Events as E
17 |
18 | type Model = Int
19 | data Msg = Increment | Decrement
20 |
21 | init ∷ Model
22 | init = 0
23 |
24 | update ∷ Msg → Model → Model
25 | update Increment n = n + 1
26 | update Decrement n = n - 1
27 |
28 | view ∷ Model → Html Msg
29 | view counter =
30 | H.div []
31 | [ H.button [E.onClick \_ → Decrement] [H.text "-"]
32 | , H.span [] [H.text $ show counter]
33 | , H.button [E.onClick \_ → Increment] [H.text "+"]
34 | ]
35 |
36 | main ∷ Effect Unit
37 | main = sandbox {init, update, view, selector: "#root"}
38 | ```
39 |
40 | ### Example with side effects
41 | ```purescript
42 | module Example.Counter where
43 | import Prelude
44 | import Effect (Effect)
45 | import Effect.Aff (Aff)
46 | import Effect.Random (randomInt)
47 | import Pha.App (app)
48 | import Pha.Html (Html)
49 | import Pha.Html as H
50 | import Pha.Html.Attributes as P
51 | import Pha.Html.Events as E
52 | import Pha.Update (Update, liftEffect, put)
53 |
54 | type Model = Int
55 | data Msg = RollDice
56 |
57 | update ∷ Msg → Update Model Msg Aff Unit
58 | update RollDice = put =<< liftEffect (randomInt 1 6)
59 |
60 | view ∷ State → Html Msg
61 | view dice =
62 | H.div []
63 | [ H.button [E.onClick \_ → RollDice] [H.text "Roll dice"]
64 | , H.span [] [H.text $ show dice]
65 | ]
66 |
67 | main ∷ Effect Unit
68 | main =
69 | app
70 | { init: { model: 0, msg: Just RollDice }
71 | , view
72 | , update
73 | , selector: "#root"
74 | }
75 | ```
76 |
77 | ### More examples
78 |
79 | https://github.com/gbagan/purescript-pha-examples/tree/master/src
80 |
81 | ### Some projects using purescript-pha
82 |
83 | - https://github.com/gbagan/valise-mam
84 | - https://github.com/gbagan/nim-machine
85 | - https://github.com/gbagan/neuron
86 | - https://github.com/gbagan/graphparams
87 | - https://github.com/gbagan/sudoku-solver
88 |
--------------------------------------------------------------------------------
/src/Pha/Html/Attributes.purs:
--------------------------------------------------------------------------------
1 | module Pha.Html.Attributes where
2 | import Prelude
3 | import Pha.Html.Core (Prop, attr, prop)
4 |
5 | action ∷ ∀msg. String → Prop msg
6 | action = attr "action"
7 |
8 | alt ∷ ∀msg. String → Prop msg
9 | alt = attr "alt"
10 |
11 | charset ∷ ∀msg. String → Prop msg
12 | charset = attr "charset"
13 |
14 | checked ∷ ∀msg. Boolean → Prop msg
15 | checked b = prop "checked" b
16 |
17 | cols ∷ ∀msg. Int → Prop msg
18 | cols = attr "cols" <<< show
19 |
20 | colSpan ∷ ∀msg. Int → Prop msg
21 | colSpan = attr "colSpan" <<< show
22 |
23 | disabled ∷ ∀msg. Boolean → Prop msg
24 | disabled b = prop "disabled" b
25 |
26 | download ∷ ∀msg. String → Prop msg
27 | download = attr "download"
28 |
29 | height ∷ ∀msg. Int → Prop msg
30 | height = prop "height"
31 |
32 | hidden ∷ ∀msg. Boolean → Prop msg
33 | hidden b = prop "hidden" b
34 |
35 | href ∷ ∀msg. String → Prop msg
36 | href = attr "href"
37 |
38 | id ∷ ∀msg. String → Prop msg
39 | id = attr "id"
40 |
41 | max ∷ ∀msg. Int → Prop msg
42 | max = attr "max" <<< show
43 |
44 | maxlength ∷ ∀msg. Int → Prop msg
45 | maxlength = attr "maxlength" <<< show
46 |
47 | min ∷ ∀msg. Int → Prop msg
48 | min = attr "min" <<< show
49 |
50 | minlength ∷ ∀msg. Int → Prop msg
51 | minlength = attr "minlength" <<< show
52 |
53 | name ∷ ∀msg. String → Prop msg
54 | name = attr "name"
55 |
56 | placeholder ∷ ∀msg. String → Prop msg
57 | placeholder = attr "placeholder"
58 |
59 | readonly ∷ ∀msg. Boolean → Prop msg
60 | readonly b = prop "selected" b
61 |
62 | rel ∷ ∀msg. String → Prop msg
63 | rel = attr "rel"
64 |
65 |
66 | required ∷ ∀msg. Boolean → Prop msg
67 | required b = prop "selected" b
68 |
69 | rows ∷ ∀msg. Int → Prop msg
70 | rows = attr "rows" <<< show
71 |
72 | rowSpan ∷ ∀msg. Int → Prop msg
73 | rowSpan = attr "rowSpan" <<< show
74 |
75 | selected ∷ ∀msg. Boolean → Prop msg
76 | selected b = prop "selected" b
77 |
78 | size ∷ ∀msg. Int → Prop msg
79 | size = attr "size" <<< show
80 |
81 | src ∷ ∀msg. String → Prop msg
82 | src = attr "src"
83 |
84 | target ∷ ∀msg. String → Prop msg
85 | target = attr "target"
86 |
87 | title ∷ ∀msg. String → Prop msg
88 | title = attr "title"
89 |
90 | type_ ∷ ∀msg. String → Prop msg
91 | type_ = attr "type"
92 |
93 | value ∷ ∀msg. String → Prop msg
94 | value = prop "value"
95 |
96 | width ∷ ∀msg. Int → Prop msg
97 | width = prop "width"
--------------------------------------------------------------------------------
/examples/Counter2.purs:
--------------------------------------------------------------------------------
1 | module Example.Counter2 where
2 | import Prelude hiding (div)
3 | import Data.Int (even)
4 | import Data.Maybe (Maybe(..))
5 | import Data.Array ((..), replicate)
6 | import Effect (Effect)
7 | import Effect.Aff (Aff)
8 | import Pha.Html (Html)
9 | import Pha.Html as H
10 | import Pha.Html.Attributes as P
11 | import Pha.Html.Events as E
12 | import Pha.App (app)
13 | import Pha.Update (Update, Milliseconds(..), modify_, delay)
14 | import Pha.Subscriptions (onKeyDown)
15 |
16 | type Model = Int
17 |
18 | -- initial state
19 | model ∷ Model
20 | model = 0
21 |
22 | data Msg = Init | Increment | DelayedIncrement
23 |
24 | update ∷ Msg → Update Model Msg Aff Unit
25 | update Init = void $ onKeyDown keyDownHandler
26 | update Increment = modify_ (_ + 1)
27 | update DelayedIncrement = do
28 | delay (Milliseconds 1000.0)
29 | modify_ (_ + 1)
30 |
31 | spanCounter ∷ Int → Html Msg
32 | spanCounter v = H.span [] [H.text $ show v]
33 |
34 | view ∷ Model → Html Msg
35 | view counter =
36 | H.div []
37 | [ H.div [H.class_ "counter"] [H.text $ show counter]
38 | , H.button [E.onClick \_ → Increment] [H.text "Increment"]
39 | , H.button [E.onClick \_ → DelayedIncrement] [H.text "Delayed Increment"]
40 | , H.div []
41 | [ H.span [] [H.text "green when the counter is even"]
42 | , H.div
43 | [ H.class_ "box"
44 | , P.width counter
45 | , H.style "background-color" $ if even counter then "blue" else "red"
46 | ] []
47 | ]
48 |
49 | , H.h3 [] [H.text "press I to increment the counter"]
50 |
51 | , H.hr []
52 | , H.h3 [] [H.text "keyed"]
53 |
54 | , H.keyed "div" [] $
55 | ((0 .. (counter `mod` 4)) <#> \i →
56 | {key: "r" <> show i, html: H.text ("r" <> show i)}
57 | ) <>
58 | [{key: "test", html: H.text "test"}]
59 | <>
60 | ((0 .. (counter `mod` 4)) <#> \i →
61 | {key: "q" <> show i, html: H.text ("q" <> show i)}
62 | )
63 | , H.hr []
64 | , H.h3 [] [H.text "non keyed"]
65 | , H.div [] $
66 | ((0 .. (counter `mod` 4)) <#> H.text <<< show)
67 | <> [H.text "test"]
68 | <> ((0 .. (counter `mod` 4)) <#> H.text <<< show)
69 | , H.hr []
70 | , H.h3 [] [H.text "lazy"]
71 | , H.lazy spanCounter (counter / 2)
72 |
73 | , H.hr []
74 | , H.h3 [] [ H.text "duplicate" ]
75 | , H.div [] $
76 | replicate (counter `mod` 4) (H.text "t")
77 | ]
78 |
79 | keyDownHandler ∷ String → Maybe Msg
80 | keyDownHandler "i" = Just Increment
81 | keyDownHandler _ = Nothing
82 |
83 | main ∷ Effect Unit
84 | main = app
85 | { init: {model, msg: Just Init}
86 | , view
87 | , update
88 | , selector: "#root"
89 | }
--------------------------------------------------------------------------------
/src/Pha/Svg/Attributes.purs:
--------------------------------------------------------------------------------
1 | module Pha.Svg.Attributes where
2 |
3 | import Prelude
4 | import Pha.Html.Core (Prop, attr)
5 |
6 | class IsLength :: Type → Constraint
7 | class IsLength a where
8 | toString :: a → String
9 |
10 | instance IsLength Int where
11 | toString = show
12 |
13 | instance IsLength Number where
14 | toString = show
15 |
16 | instance IsLength String where
17 | toString = identity
18 |
19 |
20 | x ∷ ∀msg a. IsLength a => a → Prop msg
21 | x = attr "x" <<< toString
22 | y ∷ ∀msg a. IsLength a => a → Prop msg
23 | y = attr "y" <<< toString
24 | x1 ∷ ∀msg a. IsLength a => a → Prop msg
25 | x1 = attr "x1" <<< toString
26 | y1 ∷ ∀msg a. IsLength a => a → Prop msg
27 | y1 = attr "y1" <<< toString
28 | x2 ∷ ∀msg a. IsLength a => a → Prop msg
29 | x2 = attr "x2" <<< toString
30 | y2 ∷ ∀msg a. IsLength a => a → Prop msg
31 | y2 = attr "y2" <<< toString
32 | cx ∷ ∀msg a. IsLength a => a → Prop msg
33 | cx = attr "cx" <<< toString
34 | cy ∷ ∀msg a. IsLength a => a → Prop msg
35 | cy = attr "cy" <<< toString
36 | dx ∷ ∀msg a. IsLength a => a → Prop msg
37 | dx = attr "dx" <<< toString
38 | dy ∷ ∀msg a. IsLength a => a → Prop msg
39 | dy = attr "dy" <<< toString
40 | r ∷ ∀msg a. IsLength a => a → Prop msg
41 | r = attr "r" <<< toString
42 | rx ∷ ∀msg a. IsLength a => a → Prop msg
43 | rx = attr "rx" <<< toString
44 | ry ∷ ∀msg a. IsLength a => a → Prop msg
45 | ry = attr "ry" <<< toString
46 |
47 | clipPath ∷ ∀msg. String → Prop msg
48 | clipPath = attr "clip-path"
49 |
50 | d ∷ ∀msg. String → Prop msg
51 | d = attr "d"
52 |
53 | width ∷ ∀msg i. IsLength i => i → Prop msg
54 | width i = attr "width" (toString i)
55 |
56 | height ∷ ∀msg i. IsLength i => i → Prop msg
57 | height i = attr "height" (toString i)
58 |
59 | opacity ∷ ∀msg. Number → Prop msg
60 | opacity = attr "opacity" <<< show
61 |
62 | fill ∷ ∀msg. String → Prop msg
63 | fill = attr "fill"
64 |
65 | fillOpacity ∷ ∀msg. Number → Prop msg
66 | fillOpacity = attr "fill-opacity" <<< show
67 |
68 | fillRule ∷ ∀msg. String → Prop msg
69 | fillRule = attr "fill-rule"
70 |
71 | fontSize ∷ ∀msg a. IsLength a => a → Prop msg
72 | fontSize = attr "font-size" <<< toString
73 |
74 | offset ∷ ∀msg. String → Prop msg
75 | offset = attr "offset"
76 |
77 | patternTransform ∷ ∀msg. String → Prop msg
78 | patternTransform = attr "pattern-transform"
79 |
80 | patternUnits ∷ ∀msg. String → Prop msg
81 | patternUnits = attr "pattern-units"
82 |
83 | points ∷ ∀msg. String → Prop msg
84 | points = attr "points"
85 |
86 | viewBox ∷ ∀msg. Number → Number → Number → Number → Prop msg
87 | viewBox a b c d2 = attr "viewBox" $ show a <> " " <> show b <> " " <> show c <> " " <> show d2
88 |
89 | transform ∷ ∀msg. String → Prop msg
90 | transform = attr "transform"
91 |
92 | stopColor ∷ ∀msg. String → Prop msg
93 | stopColor = attr "stop-color"
94 |
95 | stroke ∷ ∀msg. String → Prop msg
96 | stroke = attr "stroke"
97 |
98 | strokeDasharray ∷ ∀msg. String → Prop msg
99 | strokeDasharray = attr "stroke-dasharray"
100 |
101 | strokeOpacity ∷ ∀msg. Number → Prop msg
102 | strokeOpacity = attr "stroke-opacity" <<< show
103 |
104 | strokeWidth ∷ ∀msg. Number → Prop msg
105 | strokeWidth = attr "stroke-width" <<< show
--------------------------------------------------------------------------------
/src/Pha/Update/Internal.purs:
--------------------------------------------------------------------------------
1 | module Pha.Update.Internal where
2 |
3 | import Prelude
4 |
5 | import Control.Monad.Free (Free, liftF, hoistFree)
6 | import Data.Bifunctor (lmap)
7 | import Data.Lens (Lens', view, set)
8 | import Data.Tuple (Tuple(..))
9 | import Effect (Effect)
10 | import Effect.Aff.Class (class MonadAff, liftAff)
11 | import Effect.Class (class MonadEffect, liftEffect)
12 | import Control.Monad.Rec.Class (class MonadRec)
13 | import Control.Monad.Reader.Class (class MonadAsk, ask)
14 | import Control.Monad.State.Class (class MonadState)
15 | import Control.Monad.Trans.Class (class MonadTrans)
16 | import Unsafe.Reference (unsafeRefEq)
17 |
18 | newtype SubscriptionId = SubscriptionId Int
19 |
20 | derive newtype instance Eq SubscriptionId
21 | derive newtype instance Ord SubscriptionId
22 |
23 | type Subscription msg = (msg → Effect Unit) → Effect (Effect Unit)
24 |
25 | data UpdateF model msg m a
26 | = State (model → Tuple a model)
27 | | Lift (m a)
28 | | Subscribe ((msg → Effect Unit) → Effect (Effect Unit)) (SubscriptionId → a)
29 | | Unsubscribe SubscriptionId a
30 |
31 | instance Functor m ⇒ Functor (UpdateF model msg m) where
32 | map f (State k) = State (lmap f <<< k)
33 | map f (Lift q) = Lift (map f q)
34 | map f (Subscribe g a) = Subscribe g (f <<< a)
35 | map f (Unsubscribe id a) = Unsubscribe id (f a)
36 |
37 | newtype Update model msg m a = Update (Free (UpdateF model msg m) a)
38 |
39 | derive newtype instance Functor (Update model msg m)
40 | derive newtype instance Apply (Update model msg m)
41 | derive newtype instance Applicative (Update model state m)
42 | derive newtype instance Bind (Update model msg m)
43 | derive newtype instance Monad (Update model msg m)
44 | derive newtype instance MonadRec (Update model msg m)
45 |
46 | instance MonadState model (Update model msg m) where
47 | state = Update <<< liftF <<< State
48 |
49 | instance MonadTrans (Update model msg) where
50 | lift = Update <<< liftF <<< Lift
51 |
52 | instance MonadEffect m ⇒ MonadEffect (Update model msg m) where
53 | liftEffect = Update <<< liftF <<< Lift <<< liftEffect
54 |
55 | instance MonadAff m ⇒ MonadAff (Update model msg m) where
56 | liftAff = Update <<< liftF <<< Lift <<< liftAff
57 |
58 | instance MonadAsk r m => MonadAsk r (Update model msg m) where
59 | ask = Update $ liftF $ Lift ask
60 |
61 | subscribe ∷ ∀ model msg m. Subscription msg → Update model msg m SubscriptionId
62 | subscribe f = Update $ liftF $ Subscribe f identity
63 |
64 | unsubscribe ∷ ∀ model msg m. SubscriptionId → Update model msg m Unit
65 | unsubscribe id = Update $ liftF $ Unsubscribe id unit
66 |
67 | mapMessage ∷ ∀ model msg msg' m. (msg → msg') → Update model msg m ~> Update model msg' m
68 | mapMessage f (Update m) = Update $ m # hoistFree case _ of
69 | State k → State k
70 | Lift x → Lift x
71 | Subscribe g a → Subscribe (\dispatch → g \msg → dispatch (f msg)) a
72 | Unsubscribe id a → Unsubscribe id a
73 |
74 | mapModel ∷ ∀ model model' msg m. Lens' model model' → Update model' msg m ~> Update model msg m
75 | mapModel lens (Update m) = Update $ m # hoistFree case _ of
76 | State k → State \s →
77 | let
78 | s2 = view lens s
79 | Tuple a s3 = k s2
80 | in
81 | if unsafeRefEq s2 s3 then
82 | Tuple a s
83 | else
84 | Tuple a (set lens s3 s)
85 |
86 | Lift x → Lift x
87 | Subscribe x a → Subscribe x a
88 | Unsubscribe id a → Unsubscribe id a
89 |
90 | hoist ∷ ∀ model msg m m'. (m ~> m') → Update model msg m ~> Update model msg m'
91 | hoist f (Update m) = Update $ m # hoistFree case _ of
92 | State k → State k
93 | Lift x → Lift (f x)
94 | Subscribe g a → Subscribe g a
95 | Unsubscribe id a → Unsubscribe id a
--------------------------------------------------------------------------------
/src/Pha/Html/Elements.purs:
--------------------------------------------------------------------------------
1 | module Pha.Html.Elements where
2 |
3 | import Pha.Html.Core (Html, Prop, elem)
4 |
5 | a ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
6 | a = elem "a"
7 |
8 | abbr ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
9 | abbr = elem "abbr"
10 |
11 | area ∷ ∀msg. Array (Prop msg) → Html msg
12 | area attrs = elem "area" attrs []
13 |
14 | article ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
15 | article = elem "article"
16 |
17 | aside ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
18 | aside = elem "aside"
19 |
20 | audio ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
21 | audio = elem "audio"
22 |
23 | b ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
24 | b = elem "b"
25 |
26 | br ∷ ∀msg. Html msg
27 | br = elem "br" [] []
28 |
29 | button ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
30 | button = elem "button"
31 |
32 | canvas ∷ ∀msg. Array (Prop msg) → Html msg
33 | canvas attrs = elem "canvas" attrs []
34 |
35 | div ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
36 | div = elem "div"
37 |
38 | em ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
39 | em = elem "em"
40 |
41 | footer ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
42 | footer = elem "footer"
43 |
44 | form ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
45 | form = elem "form"
46 |
47 | h1 ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
48 | h1 = elem "h1"
49 |
50 | h2 ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
51 | h2 = elem "h2"
52 |
53 | h3 ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
54 | h3 = elem "h3"
55 |
56 | h4 ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
57 | h4 = elem "h4"
58 |
59 | h5 ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
60 | h5 = elem "h5"
61 |
62 | h6 ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
63 | h6 = elem "h6"
64 |
65 | header ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
66 | header = elem "header"
67 |
68 | hr ∷ ∀msg. Array (Prop msg) → Html msg
69 | hr attrs = elem "hr" attrs []
70 |
71 | i ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
72 | i = elem "i"
73 |
74 | img ∷ ∀msg. Array (Prop msg) → Html msg
75 | img attrs = elem "img" attrs []
76 |
77 | input ∷ ∀msg. Array (Prop msg) → Html msg
78 | input attrs = elem "input" attrs []
79 |
80 | label ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
81 | label = elem "label"
82 |
83 | li ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
84 | li = elem "li"
85 |
86 | link ∷ ∀msg. Array (Prop msg) → Html msg
87 | link attrs = elem "link" attrs []
88 |
89 | main ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
90 | main = elem "main"
91 |
92 | menu ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
93 | menu = elem "menu"
94 |
95 | nav ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
96 | nav = elem "nav"
97 |
98 | ol ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
99 | ol = elem "ol"
100 |
101 | option ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
102 | option = elem "option"
103 |
104 | p ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
105 | p = elem "p"
106 |
107 | pre ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
108 | pre = elem "pre"
109 |
110 | section ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
111 | section = elem "section"
112 |
113 | select ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
114 | select = elem "select"
115 |
116 | span ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
117 | span = elem "span"
118 |
119 | table ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
120 | table = elem "table"
121 |
122 | td ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
123 | td = elem "td"
124 |
125 | textarea ∷ ∀msg. Array (Prop msg) → Html msg
126 | textarea attrs = elem "textarea" attrs []
127 |
128 | tr ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
129 | tr = elem "tr"
130 |
131 | ul ∷ ∀msg. Array (Prop msg) → Array (Html msg) → Html msg
132 | ul = elem "ul"
133 |
--------------------------------------------------------------------------------
/src/Pha/Html/Core.purs:
--------------------------------------------------------------------------------
1 | module Pha.Html.Core
2 | ( EventHandler
3 | , Html
4 | , KeyedHtml
5 | , Prop
6 | , elem
7 | , keyed
8 | , text
9 | , attr
10 | , prop
11 | , noProp
12 | , style
13 | , class_
14 | , class'
15 | , empty
16 | , lazy
17 | , lazy2
18 | , lazy3
19 | , lazy4
20 | , lazy5
21 | , when
22 | , fromMaybe
23 | , maybe
24 | , on_
25 | , unsafeOnWithEffect
26 | , module E
27 | )
28 | where
29 | import Prelude hiding (when)
30 | import Effect (Effect)
31 | import Data.Function.Uncurried (Fn2, Fn3, Fn4, Fn5, Fn6, mkFn2, mkFn3, mkFn4, mkFn5, runFn2, runFn3, runFn4, runFn5, runFn6)
32 | import Data.Maybe (Maybe)
33 | import Data.Maybe as M
34 | import Web.Event.Event (Event) as E
35 | import Web.Event.Event (Event)
36 |
37 | foreign import data Html ∷ Type → Type
38 | type KeyedHtml a = {key ∷ String, html ∷ Html a}
39 |
40 | type EventHandler msg = Event → Effect (Maybe msg)
41 |
42 | foreign import data Prop ∷ Type → Type
43 |
44 | -- | Create a HTML attribute.
45 | attr ∷ ∀msg. String → String → Prop msg
46 | attr key val = runFn2 attrImpl key val
47 |
48 | -- | Create a HTML property.
49 | prop ∷ ∀msg value. String → value → Prop msg
50 | prop key val = runFn2 propImpl key val
51 |
52 | foreign import attrImpl ∷ ∀msg. Fn2 String String (Prop msg)
53 |
54 | foreign import propImpl ∷ ∀msg value. Fn2 String value (Prop msg)
55 |
56 | -- | Create a CSS class.
57 | foreign import class_ ∷ ∀msg. String → Prop msg
58 |
59 | foreign import noProp ∷ ∀msg. Prop msg
60 |
61 | -- | Create a CSS class if the boolean is True.
62 | class' ∷ ∀msg. String → Boolean → Prop msg
63 | class' c b = if b then class_ c else noProp
64 |
65 | foreign import unsafeOnWithEffectImpl ∷ ∀msg. Fn2 String (EventHandler msg) (Prop msg)
66 |
67 | unsafeOnWithEffect ∷ ∀msg. String → EventHandler msg → Prop msg
68 | unsafeOnWithEffect name handler = runFn2 unsafeOnWithEffectImpl name handler
69 |
70 | on_ ∷ ∀msg. String → (Event → Maybe msg) → Prop msg
71 | on_ n handler = unsafeOnWithEffect n \ev → pure (handler ev)
72 |
73 | -- | Create a CSS property.
74 | style ∷ ∀msg. String → String → Prop msg
75 | style key val = runFn2 styleImpl key val
76 |
77 | foreign import styleImpl ∷ ∀msg. Fn2 String String (Prop msg)
78 |
79 | -- | Create a HTML element.
80 | elem ∷ ∀msg. String → Array (Prop msg) → Array (Html msg) → Html msg
81 | elem name attrs children = runFn3 elemImpl name attrs children
82 |
83 | foreign import elemImpl ∷ ∀msg. Fn3 String (Array (Prop msg)) (Array (Html msg)) (Html msg)
84 |
85 | -- | Create a HTML element where children are keyed.
86 | keyed ∷ ∀msg. String → Array (Prop msg) → Array (KeyedHtml msg) → Html msg
87 | keyed name attrs children = runFn3 keyedImpl name attrs children
88 |
89 | foreign import keyedImpl ∷ ∀msg. Fn3 String (Array (Prop msg)) (Array (KeyedHtml msg)) (Html msg)
90 |
91 | -- | Create a text node HTML value.
92 | foreign import text ∷ ∀msg. String → Html msg
93 |
94 | -- | Create a empty HTML value.
95 | empty ∷ ∀msg. Html msg
96 | empty = text ""
97 |
98 |
99 | -- | Creates a lazy node.
100 | -- |
101 | -- | Lazy nodes are only updated if the parameter changes (compared by reference)
102 | lazy ∷ ∀a msg. (a → Html msg) → a → Html msg
103 | lazy f a = runFn2 lazyImpl f a
104 |
105 | -- | Same as `lazy` but checks on two arguments.
106 | lazy2 ∷ ∀a b msg. (a → b → Html msg) → a → b → Html msg
107 | lazy2 f a b = runFn3 lazy2Impl (mkFn2 f) a b
108 |
109 | -- | Same as `lazy` but checks on three arguments.
110 | lazy3 ∷ ∀a b c msg. (a → b → c → Html msg) → a → b → c → Html msg
111 | lazy3 f a b c = runFn4 lazy3Impl (mkFn3 f) a b c
112 |
113 | -- | Same as `lazy` but checks on four arguments.
114 | lazy4 ∷ ∀a b c d msg. (a → b → c → d → Html msg) → a → b → c → d → Html msg
115 | lazy4 f a b c d = runFn5 lazy4Impl (mkFn4 f) a b c d
116 |
117 | -- | Same as `lazy` but checks on five arguments.
118 | lazy5 ∷ ∀a b c d e msg. (a → b → c → d → e → Html msg) → a → b → c → d → e → Html msg
119 | lazy5 f a b c d = runFn6 lazy5Impl (mkFn5 f) a b c d
120 |
121 | foreign import lazyImpl ∷ ∀a msg. Fn2 (a → Html msg) a (Html msg)
122 | foreign import lazy2Impl ∷ ∀a b msg. Fn3 (Fn2 a b (Html msg)) a b (Html msg)
123 | foreign import lazy3Impl ∷ ∀a b c msg. Fn4 (Fn3 a b c (Html msg)) a b c (Html msg)
124 | foreign import lazy4Impl ∷ ∀a b c d msg. Fn5 (Fn4 a b c d (Html msg)) a b c d (Html msg)
125 | foreign import lazy5Impl ∷ ∀a b c d e msg. Fn6 (Fn5 a b c d e (Html msg)) a b c d e (Html msg)
126 |
127 | -- | Create a VDOM tree only if the boolean is True
128 | when ∷ ∀msg. Boolean → (Unit → Html msg) → Html msg
129 | when cond vdom = if cond then vdom unit else empty
130 |
131 | -- | ```purescript
132 | -- | fromMaybe (Just html) = html
133 | -- | fromMaybe Nothing = empty
134 | -- | ```
135 | fromMaybe ∷ ∀msg. Maybe (Html msg) → Html msg
136 | fromMaybe = M.fromMaybe empty
137 |
138 | maybe ∷ ∀a msg. Maybe a → (a → Html msg) → Html msg
139 | maybe = flip (M.maybe empty)
140 |
141 | mapHandler :: ∀a b. (a → b) → EventHandler a → EventHandler b
142 | mapHandler fn handler ev = map (map fn) (handler ev)
143 |
144 | foreign import mapProp ∷ ∀a b. Fn2 (EventHandler a → EventHandler b) (Prop a) (Prop b)
145 |
146 | instance Functor Prop where
147 | map fn pr = runFn2 mapProp (mapHandler fn) pr
148 |
149 | foreign import mapView ∷ ∀a b. Fn2 (EventHandler a → EventHandler b) (Html a) (Html b)
150 |
151 | instance Functor Html where
152 | map fn html = runFn2 mapView (mapHandler fn) html
153 |
--------------------------------------------------------------------------------
/src/Pha/Html/Events.purs:
--------------------------------------------------------------------------------
1 | module Pha.Html.Events
2 | ( on
3 | , onClick
4 | , onClick'
5 | , onAuxClick
6 | , onContextMenu
7 | , onContextMenuPrevent
8 | , onPointerDown
9 | , onPointerDown'
10 | , onPointerEnter
11 | , onPointerEnter'
12 | , onPointerLeave
13 | , onPointerLeave'
14 | , onPointerMove
15 | , onPointerMove'
16 | , onPointerOut
17 | , onPointerOut'
18 | , onPointerOver
19 | , onPointerOver'
20 | , onPointerUp
21 | , onPointerUp'
22 | , onBlur
23 | , onFocus
24 | , onFocusIn
25 | , onFocusOut
26 | , onChecked
27 | , onValueChange
28 | ) where
29 |
30 | import Prelude hiding (div)
31 |
32 | import Data.Maybe (Maybe(..))
33 | import Effect (Effect)
34 | import Effect.Uncurried (EffectFn3, runEffectFn3)
35 | import Pha.Html.Core (Prop, EventHandler, unsafeOnWithEffect)
36 | import Unsafe.Coerce (unsafeCoerce)
37 | import Web.Event.Event (Event)
38 | import Web.Event.Event as Event
39 | import Web.Event.EventTarget (EventTarget)
40 | import Web.HTML.HTMLInputElement as HTMLInput
41 | import Web.PointerEvent (PointerEvent)
42 | import Web.UIEvent.FocusEvent (FocusEvent)
43 | import Web.UIEvent.MouseEvent (MouseEvent)
44 |
45 | on ∷ ∀ msg. String → EventHandler msg → Prop msg
46 | on = unsafeOnWithEffect
47 |
48 | mouseCoerce ∷ Event → MouseEvent
49 | mouseCoerce = unsafeCoerce
50 |
51 | pointerCoerce ∷ Event → PointerEvent
52 | pointerCoerce = unsafeCoerce
53 |
54 | focusCoerce ∷ Event → FocusEvent
55 | focusCoerce = unsafeCoerce
56 |
57 | onClick ∷ ∀ msg. (MouseEvent → msg) → Prop msg
58 | onClick handler = on "click" (pure <<< Just <<< handler <<< mouseCoerce)
59 |
60 | onClick' ∷ ∀ msg. (MouseEvent → Effect (Maybe msg)) → Prop msg
61 | onClick' handler = on "click" (handler <<< mouseCoerce)
62 |
63 | onAuxClick ∷ ∀ msg. (MouseEvent → msg) → Prop msg
64 | onAuxClick handler = on "auxclick" (pure <<< Just <<< handler <<< mouseCoerce)
65 |
66 | onContextMenu ∷ ∀ msg. (MouseEvent → msg) → Prop msg
67 | onContextMenu handler = on "contextmenu" (pure <<< Just <<< handler <<< mouseCoerce)
68 |
69 | onContextMenuPrevent ∷ ∀ msg. (MouseEvent → msg) → Prop msg
70 | onContextMenuPrevent handler = on "contextmenu" \ev → do
71 | Event.preventDefault ev
72 | pure <<< Just <<< handler <<< mouseCoerce $ ev
73 |
74 | onPointerUp ∷ ∀ msg. (PointerEvent → msg) → Prop msg
75 | onPointerUp handler = on "pointerup" (pure <<< Just <<< handler <<< pointerCoerce)
76 |
77 | onPointerUp' ∷ ∀ msg. (PointerEvent → Effect (Maybe msg)) → Prop msg
78 | onPointerUp' handler = on "pointerup" (handler <<< pointerCoerce)
79 |
80 | onPointerDown ∷ ∀ msg. (PointerEvent → msg) → Prop msg
81 | onPointerDown handler = on "pointerdown" (pure <<< Just <<< handler <<< pointerCoerce)
82 |
83 | onPointerDown' ∷ ∀ msg. (PointerEvent → Effect (Maybe msg)) → Prop msg
84 | onPointerDown' handler = on "pointerdown" (handler <<< pointerCoerce)
85 |
86 | onPointerEnter ∷ ∀ msg. (PointerEvent → msg) → Prop msg
87 | onPointerEnter handler = on "pointerenter" (pure <<< Just <<< handler <<< pointerCoerce)
88 |
89 | onPointerEnter' ∷ ∀ msg. (PointerEvent → Effect (Maybe msg)) → Prop msg
90 | onPointerEnter' handler = on "pointerenter" (handler <<< pointerCoerce)
91 |
92 | onPointerLeave ∷ ∀ msg. (PointerEvent → msg) → Prop msg
93 | onPointerLeave handler = on "pointerleave" (pure <<< Just <<< handler <<< pointerCoerce)
94 |
95 | onPointerLeave' ∷ ∀ msg. (PointerEvent → Effect (Maybe msg)) → Prop msg
96 | onPointerLeave' handler = on "pointerleave" (handler <<< pointerCoerce)
97 |
98 | onPointerOver ∷ ∀ msg. (PointerEvent → msg) → Prop msg
99 | onPointerOver handler = on "pointerover" (pure <<< Just <<< handler <<< pointerCoerce)
100 |
101 | onPointerOver' ∷ ∀ msg. (PointerEvent → Effect (Maybe msg)) → Prop msg
102 | onPointerOver' handler = on "pointerover" (handler <<< pointerCoerce)
103 |
104 | onPointerOut ∷ ∀ msg. (PointerEvent → msg) → Prop msg
105 | onPointerOut handler = on "pointerout" (pure <<< Just <<< handler <<< pointerCoerce)
106 |
107 | onPointerOut' ∷ ∀ msg. (PointerEvent → Effect (Maybe msg)) → Prop msg
108 | onPointerOut' handler = on "pointerout" (handler <<< pointerCoerce)
109 |
110 | onPointerMove ∷ ∀ msg. (PointerEvent → msg) → Prop msg
111 | onPointerMove handler = on "pointermove" (pure <<< Just <<< handler <<< pointerCoerce)
112 |
113 | onPointerMove' ∷ ∀ msg. (PointerEvent → Effect (Maybe msg)) → Prop msg
114 | onPointerMove' handler = on "pointermove" (handler <<< pointerCoerce)
115 |
116 |
117 | onBlur ∷ ∀ msg. (FocusEvent → msg) → Prop msg
118 | onBlur handler = on "blur" (pure <<< Just <<< handler <<< focusCoerce)
119 |
120 | onFocus ∷ ∀ msg. (FocusEvent → msg) → Prop msg
121 | onFocus handler = on "focus" (pure <<< Just <<< handler <<< focusCoerce)
122 |
123 | onFocusIn ∷ ∀ msg. (FocusEvent → msg) → Prop msg
124 | onFocusIn handler = on "focusin" (pure <<< Just <<< handler <<< focusCoerce)
125 |
126 | onFocusOut ∷ ∀ msg. (FocusEvent → msg) → Prop msg
127 | onFocusOut handler = on "focusout" (pure <<< Just <<< handler <<< focusCoerce)
128 |
129 | foreign import valueImpl :: ∀ a. EffectFn3 EventTarget (Maybe a) (a -> Maybe a) (Maybe String)
130 |
131 | onValueChange ∷ ∀ msg. (String → msg) → Prop msg
132 | onValueChange f = on "change" fn
133 | where
134 | fn ev =
135 | case Event.currentTarget ev of
136 | Just target → map f <$> runEffectFn3 valueImpl target Nothing Just
137 | Nothing → pure Nothing
138 |
139 | onChecked ∷ ∀ msg. (Boolean → msg) → Prop msg
140 | onChecked f = on "change" fn
141 | where
142 | fn ev =
143 | case Event.currentTarget ev >>= HTMLInput.fromEventTarget of
144 | Nothing → pure Nothing
145 | Just target → Just <$> f <$> HTMLInput.checked target
146 |
--------------------------------------------------------------------------------
/src/Pha/App.purs:
--------------------------------------------------------------------------------
1 | module Pha.App (app, sandbox) where
2 |
3 | import Prelude
4 |
5 | import Control.Monad.Free (runFreeM)
6 | import Data.Foldable (for_)
7 | import Data.Map (Map)
8 | import Data.Map as Map
9 | import Data.Maybe (Maybe(..))
10 | import Data.Tuple (Tuple(..))
11 | import Effect (Effect)
12 | import Effect.Uncurried(mkEffectFn1, runEffectFn2, runEffectFn5)
13 | import Effect.Aff (Aff, launchAff_)
14 | import Effect.Class (liftEffect)
15 | import Effect.Ref (Ref)
16 | import Effect.Ref as Ref
17 | import Pha.App.Internal as I
18 | import Pha.Html.Core (Html, Event, EventHandler, text)
19 | import Pha.Update.Internal (UpdateF(..), Update(..), SubscriptionId(..))
20 | import Unsafe.Reference (unsafeRefEq)
21 | import Web.DOM.Document (createTextNode)
22 | import Web.DOM.Element as El
23 | import Web.DOM.Node as Node
24 | import Web.DOM.ParentNode (QuerySelector(..), querySelector)
25 | import Web.DOM.Text as Text
26 | import Web.Event.Event (EventType(..))
27 | import Web.Event.Event as Ev
28 | import Web.HTML (window)
29 | import Web.HTML.HTMLDocument (toParentNode, toDocument)
30 | import Web.HTML.Window (document)
31 |
32 | newtype IApp model msg = IApp
33 | { state ∷ Ref model
34 | , node ∷ Ref Node.Node
35 | , vdom ∷ Ref (Html msg)
36 | , subscriptions ∷ Ref (Map SubscriptionId (Effect Unit))
37 | , freshId ∷ Ref Int
38 | , update ∷ IApp model msg → msg → Effect Unit
39 | , view ∷ model → Html msg
40 | }
41 |
42 | app'
43 | ∷ ∀ msg model
44 | . { init ∷ { model ∷ model, msg ∷ Maybe msg }
45 | , view ∷ model → Html msg
46 | , update ∷ IApp model msg → msg → Effect Unit
47 | , selector ∷ String
48 | }
49 | → Effect Unit
50 | app' { init: { model, msg }, update, view, selector } = do
51 | parentNode ← window >>= document <#> toParentNode
52 | selected ← map El.toNode <$> querySelector (QuerySelector selector) parentNode
53 | for_ selected \node_ → do
54 | state ← Ref.new model
55 | emptyNode ← window >>= document <#> toDocument >>= createTextNode "" <#> Text.toNode
56 | Node.appendChild emptyNode node_
57 | node ← Ref.new emptyNode
58 | vdom ← Ref.new $ I.unsafeLinkNode emptyNode (text "")
59 | subscriptions ← Ref.new $ Map.empty
60 | freshId ← Ref.new 0
61 | let iapp = IApp { view, update, state, node, vdom, subscriptions, freshId }
62 | render iapp (view model)
63 | for_ msg (dispatch iapp)
64 |
65 | render ∷ ∀ model msg. IApp model msg → Html msg → Effect Unit
66 | render iapp@(IApp { vdom, node }) newVDom = do
67 | oldVDom ← Ref.read vdom
68 | node1 ← Ref.read node
69 | pnode ← Node.parentNode node1
70 | for_ pnode \pnode' → do
71 | let vdom2 = I.copyVNode newVDom
72 | node2 ← runEffectFn5 I.unsafePatch pnode' node1 oldVDom vdom2 listener
73 | Ref.write node2 node
74 | Ref.write vdom2 vdom
75 | where
76 | listener = mkEffectFn1 \e → do
77 | let EventType t = Ev.type_ e
78 | for_ (Ev.currentTarget e) \target → do
79 | fn ← runEffectFn2 I.getAction target t
80 | dispatchEvent iapp e fn
81 |
82 | getState ∷ ∀ model msg. IApp model msg → Effect model
83 | getState (IApp { state }) = Ref.read state
84 |
85 | setState ∷ ∀ model msg. IApp model msg → model → Effect Unit
86 | setState iapp@(IApp { state, view }) newState = do
87 | oldState ← Ref.read state
88 | unless (unsafeRefEq oldState newState) do
89 | Ref.write newState state
90 | render iapp $ view newState
91 |
92 | dispatch ∷ ∀ model msg. IApp model msg → msg → Effect Unit
93 | -- eta expansion pour casser la dépendance cyclique
94 | dispatch iapp@(IApp { update }) = update iapp
95 |
96 | dispatchEvent ∷ ∀ model msg. IApp model msg → Event → EventHandler msg → Effect Unit
97 | dispatchEvent iapp ev handler = do
98 | msg' ← handler ev
99 | for_ msg' (dispatch iapp)
100 |
101 | getFreshId ∷ ∀ model msg. IApp model msg → Effect SubscriptionId
102 | getFreshId (IApp { freshId }) = do
103 | id ← Ref.read freshId
104 | Ref.write (id + 1) freshId
105 | pure $ SubscriptionId id
106 |
107 | interpret
108 | ∷ ∀ model msg
109 | . (msg → Update model msg Aff Unit)
110 | → IApp model msg
111 | → Update model msg Aff Unit
112 | → Aff Unit
113 | interpret update iapp@(IApp { subscriptions }) (Update m) = runFreeM go m
114 | where
115 | go (State k) = do
116 | st ← liftEffect $ getState iapp
117 | let Tuple a st2 = k st
118 | liftEffect $ setState iapp st2
119 | pure a
120 | go (Lift a) = a
121 | go (Subscribe f next) = do
122 | canceler ← liftEffect $ f \msg → launchAff_ $ interpret update iapp (update msg)
123 | id ← liftEffect $ getFreshId iapp
124 | liftEffect $ Ref.modify_ (Map.insert id canceler) subscriptions
125 | pure (next id)
126 | go (Unsubscribe id a) = do
127 | subs ← liftEffect $ Ref.read subscriptions
128 | for_ (Map.lookup id subs) liftEffect
129 | pure a
130 |
131 | -- | ```purescript
132 | -- | app ∷ ∀msg model.
133 | -- | { init ∷ {model ∷ model, msg ∷ Maybe msg}
134 | -- | , view ∷ model → Html msg
135 | -- | , update ∷ msg → Update model msg Aff Unit
136 | -- | , selector ∷ String
137 | -- | } → Effect Unit
138 | -- | ```
139 |
140 | app
141 | ∷ ∀ msg model
142 | . { init ∷ { model ∷ model, msg ∷ Maybe msg }
143 | , view ∷ model → Html msg
144 | , update ∷ msg → Update model msg Aff Unit
145 | , selector ∷ String
146 | }
147 | → Effect Unit
148 |
149 | app { init, view, update, selector } = app' { init, view, selector, update: update' }
150 | where
151 | update' iapp msg = launchAff_ $ interpret update iapp (update msg)
152 |
153 | -- | ```purescript
154 | -- | sandbox ∷ ∀msg model.
155 | -- | { init ∷ model
156 | -- | , view ∷ model → Html msg
157 | -- | , update ∷ msg → model → model
158 | -- | , selector ∷ String
159 | -- | } → Effect Unit
160 | -- | ```
161 |
162 | sandbox
163 | ∷ ∀ msg model
164 | . { init ∷ model
165 | , view ∷ model → Html msg
166 | , update ∷ msg → model → model
167 | , selector ∷ String
168 | }
169 | → Effect Unit
170 |
171 | sandbox { init, view, update, selector } =
172 | app'
173 | { init: { model: init, msg: Nothing }
174 | , view
175 | , update: \iapp msg → do
176 | st ← getState iapp
177 | setState iapp (update msg st)
178 | , selector
179 | }
180 |
--------------------------------------------------------------------------------
/src/Pha/App/Internal.js:
--------------------------------------------------------------------------------
1 | // code comes from hyperapp by Jorge Bucaran
2 | // https://github.com/jorgebucaran/hyperapp
3 | // modified by Guillaume Bagan
4 |
5 | const TEXT_NODE = 3
6 |
7 | const compose = (f, g) => f && g ? x => f(g(x)) : f || g
8 |
9 | const patchProperty = (node, key, newValue) => {
10 | node[key] = newValue;
11 | }
12 |
13 | const patchAttribute = (node, key, newValue) => {
14 | if (newValue == null || (key === "class" && !newValue)) {
15 | node.removeAttribute(key)
16 | } else {
17 | node.setAttribute(key, newValue)
18 | }
19 | }
20 |
21 | const patchEvent = (node, key, oldValue, newValue, listener, mapf) => {
22 | if (!node.actions)
23 | node.actions = {}
24 | node.actions[key] = mapf && newValue ? mapf(newValue) : newValue
25 | if (!newValue) {
26 | node.removeEventListener(key, listener)
27 | } else if (!oldValue) {
28 | node.addEventListener(key, listener)
29 | }
30 | }
31 |
32 | const createNode = (vnode, listener, isSvg, mapf) => {
33 | const node =
34 | vnode.type === TEXT_NODE
35 | ? document.createTextNode(vnode.tag)
36 | : (isSvg = isSvg || vnode.tag === "svg")
37 | ? document.createElementNS("http://www.w3.org/2000/svg", vnode.tag)
38 | : document.createElement(vnode.tag)
39 | const props = vnode.props
40 | const attrs = vnode.attrs
41 | const events = vnode.events
42 | const mapf2 = compose(mapf, vnode.mapf)
43 |
44 |
45 | for (let k in props) {
46 | patchProperty(node, k, props[k])
47 | }
48 | for (let k in attrs) {
49 | patchAttribute(node, k, attrs[k])
50 | }
51 | for (let k in events) {
52 | patchEvent(node, k, null, events[k], listener, mapf2)
53 | }
54 |
55 | for (let i = 0, len = vnode.children.length; i < len; i++) {
56 | node.appendChild(
57 | createNode(
58 | getVNode(vnode.children[i]).html,
59 | listener,
60 | isSvg,
61 | mapf2
62 | )
63 | )
64 | }
65 | vnode.node = node
66 | return node
67 | }
68 |
69 | const patch = (parent, node, oldVNode, newVNode, listener, isSvg, mapf) => {
70 | if (oldVNode === newVNode) return
71 |
72 | if (oldVNode != null && oldVNode.type === TEXT_NODE && newVNode.type === TEXT_NODE) {
73 | if (oldVNode.tag !== newVNode.tag)
74 | node.nodeValue = newVNode.tag
75 | } else if (oldVNode == null || oldVNode.tag !== newVNode.tag) {
76 | node = parent.insertBefore(
77 | createNode(newVNode, listener, isSvg, mapf),
78 | node
79 | )
80 | if (oldVNode) {
81 | oldVNode.node.remove()
82 | }
83 | } else {
84 | const oldProps = oldVNode.props
85 | const newProps = newVNode.props
86 |
87 | for (let i in {...oldProps, ...newProps}) {
88 | if (oldProps[i] !== newProps[i]) {
89 | patchProperty(node, i, newProps[i])
90 | }
91 | }
92 |
93 | const oldAttrs = oldVNode.attrs
94 | const newAttrs = newVNode.attrs
95 |
96 | for (let i in {...oldAttrs, ...newAttrs}) {
97 | if (oldAttrs[i] !== newAttrs[i]) {
98 | patchAttribute(node, i, newAttrs[i])
99 | }
100 | }
101 |
102 | const oldEvents = oldVNode.events
103 | const newEvents = newVNode.events
104 |
105 | for (let i in {...oldEvents, ...newEvents}) {
106 | if (oldEvents[i] !== newEvents[i]) {
107 | patchEvent(node, i, oldEvents[i], newEvents[i], listener, mapf)
108 | }
109 | }
110 |
111 | const oldVKids = oldVNode.children
112 | const newVKids = newVNode.children
113 | let oldTail = oldVKids.length - 1
114 | let newTail = newVKids.length - 1
115 |
116 | mapf = compose(mapf, newVNode.mapf)
117 | isSvg = isSvg || newVNode.tag === "svg"
118 |
119 | if(!newVNode.keyed) {
120 | for (let i = 0; i <= oldTail && i <= newTail; i++) {
121 | const oldVNode = oldVKids[i].html
122 | const newVNode = getVNode(newVKids[i], oldVNode).html
123 | patch(node, oldVNode.node, oldVNode, newVNode, listener, isSvg, mapf)
124 | }
125 | for (let i = oldTail + 1; i <= newTail; i++) {
126 | const newVNode = getVNode(newVKids[i], oldVNode).html
127 | node.appendChild(
128 | createNode(newVNode, listener, isSvg, mapf)
129 | )
130 | }
131 | for (let i = newTail + 1; i <= oldTail; i++) {
132 | oldVKids[i].html.node.remove()
133 | }
134 |
135 | } else { // node.keyed == true
136 | let oldHead = 0
137 | let newHead = 0
138 | while (newHead <= newTail && oldHead <= oldTail) {
139 | const {key: oldKey, html: oldVNode} = oldVKids[oldHead]
140 | if (oldKey !== newVKids[newHead].key)
141 | break
142 | const newKNode = getVNode(newVKids[newHead], oldVNode) ////////////////////
143 | patch(node, oldVNode.node, oldVNode, newKNode.html, listener, isSvg, mapf)
144 | newHead++
145 | oldHead++
146 | }
147 |
148 | while (newHead <= newTail && oldHead <= oldTail) {
149 | const {key: oldKey, html: oldVNode} = oldVKids[oldTail]
150 | if (oldKey !== newVKids[newTail].key)
151 | break
152 | const newKNode = getVNode(newVKids[newTail], oldVNode) ////////////////////
153 | patch(node, oldVNode.node, oldVNode, newKNode.html, listener, isSvg, mapf)
154 | newTail--
155 | oldTail--
156 | }
157 |
158 | if (oldHead > oldTail) {
159 | while (newHead <= newTail) {
160 | const newVNode = getVNode(newVKids[newHead]).html
161 | node.insertBefore(
162 | createNode(newVNode, listener, isSvg, mapf),
163 | oldVKids[oldHead] && oldVKids[oldHead].html.node
164 | )
165 | newHead++
166 | }
167 | } else if (newHead > newTail) {
168 | while (oldHead <= oldTail) {
169 | oldVKids[oldHead].html.node.remove()
170 | oldHead++
171 | }
172 | } else {
173 | const keyed = {}
174 | const newKeyed = {}
175 | for (let i = oldHead; i <= oldTail; i++) {
176 | keyed[oldVKids[i].key] = oldVKids[i].html
177 | }
178 |
179 | while (newHead <= newTail) {
180 | const {key: oldKey, html: oldVKid} = oldVKids[oldHead] || {key: null, html: null}
181 | const {key: newKey, html: newVKid} = getVNode(newVKids[newHead], oldVKid)
182 |
183 | if (newKeyed[oldKey] || oldVKids[oldHead+1] && newKey === oldVKids[oldHead+1].key) {
184 | oldHead++
185 | continue
186 | }
187 | if (oldKey === newKey) {
188 | patch(node, oldVKid.node, oldVKid, newVKid, listener, isSvg, mapf)
189 | newKeyed[newKey] = true
190 | oldHead++
191 | } else {
192 | const vkid = keyed[newKey]
193 | if (vkid != null) {
194 | patch(
195 | node,
196 | node.insertBefore(vkid.node, oldVKid.node),
197 | vkid,
198 | newVKids[newHead].html,
199 | listener,
200 | isSvg,
201 | mapf
202 | )
203 | newKeyed[newKey] = true
204 | } else {
205 | patch(node, oldVKid && oldVKid.node, null, newVKids[newHead].html, listener, isSvg, mapf)
206 | }
207 | }
208 | newHead++
209 | }
210 | /*
211 | while (oldHead <= oldTail) {
212 | // dans certaines situations, removeChild est appelé ici et
213 | // dans le cas juste après
214 | console.log("3", oldVKids[oldHead].html.node)
215 | node.removeChild(oldVKids[oldHead].html.node)
216 | oldHead++
217 | }
218 | */
219 | for (let i in keyed) {
220 | if (!newKeyed[i]) {
221 | keyed[i].node.remove()
222 | }
223 | }
224 | }
225 | }
226 | }
227 | newVNode.node = node
228 | return node
229 | }
230 |
231 | const propsChanged = (a, b) => {
232 | for (let i = 0; i < a.length; i++)
233 | if (a[i] !== b[i])
234 | return true
235 | return false
236 | }
237 |
238 | const getVNode = (newVNode, oldVNode) => {
239 | if (typeof newVNode.html.type === "function") {
240 | if (!oldVNode || oldVNode.memo == null || propsChanged(oldVNode.memo, newVNode.html.memo)) {
241 | oldVNode = copyVNode(newVNode.html.type(...newVNode.html.memo))
242 | oldVNode.memo = newVNode.html.memo
243 | }
244 | newVNode.html = oldVNode
245 | }
246 | return newVNode
247 | }
248 |
249 | export const copyVNode = vnode => ({
250 | ...vnode,
251 | children: vnode.children && vnode.children.map(({key, html}) => ({key, html: copyVNode(html)}))
252 | })
253 | export const getAction = (target, type) => target.actions[type]
254 | export const unsafePatch = patch
255 |
256 | export const unsafeLinkNode = node => vdom => { vdom.node = node; return vdom; }
--------------------------------------------------------------------------------