├── examples ├── counters │ ├── list-counter.gif │ ├── single-counter.gif │ ├── index.html │ └── src │ │ ├── Version1.purs │ │ ├── Main.purs │ │ └── Version2.purs ├── README.md ├── continuous-time │ ├── index.html │ └── src │ │ └── Main.purs ├── email-validator │ ├── index.html │ └── src │ │ └── Main.purs ├── timer │ ├── index.html │ └── src │ │ └── Main.purs ├── zip-codes │ ├── index.html │ └── src │ │ └── Main.purs ├── fahrenheit-celsius │ ├── index.html │ └── src │ │ └── Main.purs ├── package-lock.json ├── todomvc │ ├── index.html │ └── src │ │ └── Main.purs ├── bower.json └── package.json ├── test └── Main.purs ├── .gitignore ├── .travis.yml ├── src ├── Turbine.js ├── Turbine │ ├── HTML.js │ └── HTML.purs └── Turbine.purs ├── bower.json ├── package.json ├── LICENSE ├── README.md ├── docs ├── getting-started-tutorial.md └── tutorial.md └── resources └── behaviorstream.svg /examples/counters/list-counter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funkia/purescript-turbine/HEAD/examples/counters/list-counter.gif -------------------------------------------------------------------------------- /examples/counters/single-counter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funkia/purescript-turbine/HEAD/examples/counters/single-counter.gif -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | 7 | main :: Effect Unit 8 | main = do 9 | pure unit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | /.pulp-cache/ 4 | output/ 5 | bundle.js 6 | /generated-docs/ 7 | /.psc-package/ 8 | /.psc* 9 | /.purs* 10 | /.psa* 11 | tags 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | node_js: 4 | - "8" 5 | install: 6 | - npm install -g bower pulp purescript 7 | - npm install 8 | script: 9 | - bower install --production 10 | - npm run build 11 | - bower install 12 | - npm test -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | To run the examples 4 | 5 | 1. Run `npm i` in this directory. 6 | 2. Build an example with `npm run name-of-example`. For instance `npm run 7 | todomvc`. 8 | 3. Open the `index.html` file in the examples directory. For example `open todomvc/index.html`. 9 | -------------------------------------------------------------------------------- /examples/counters/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Counters 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/continuous-time/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Continuous time 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/email-validator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Email validator 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/timer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Timer 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/zip-codes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ZIP code validator 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/fahrenheit-celsius/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fahrenheit celsius converter 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-turbine-examples", 3 | "requires": true, 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "todomvc-app-css": { 7 | "version": "2.2.0", 8 | "resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.2.0.tgz", 9 | "integrity": "sha512-H03oc3QOxiGXv+MqnotcduZIwoGX8A8QbSx9J4U2Z5R96LrK+dvQmRDTgeCc0nlkPBhd3nUL4EbfS7l0TccM5g==" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TodoMVC 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/counters/src/Version1.purs: -------------------------------------------------------------------------------- 1 | module Counters.Version1 2 | ( counter 3 | ) where 4 | 5 | import Prelude 6 | 7 | import Hareactive.Combinators (accum) 8 | import Turbine (Component, use, component, output, ()) 9 | import Turbine.HTML as E 10 | 11 | counter :: Int -> Component {} {} 12 | counter id = component \on -> do 13 | count <- accum (+) 0 on.change 14 | ( 15 | E.div {} ( 16 | E.text "Counter " 17 | E.span {} (E.textB $ map show count) 18 | E.button {} (E.text "+" ) `use` (\o -> { change: o.click $> 1 }) 19 | E.button {} (E.text "-" ) `use` (\o -> { change: o.click $> -1 }) 20 | ) 21 | ) `output` {} 22 | -------------------------------------------------------------------------------- /examples/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-turbine-examples", 3 | "ignore": [ 4 | "**/.*", 5 | "node_modules", 6 | "bower_components", 7 | "output" 8 | ], 9 | "dependencies": { 10 | "purescript-prelude": "^4.1.1", 11 | "purescript-console": "^4.2.0", 12 | "purescript-functions": "^4.0.0", 13 | "purescript-record": "^2.0.1", 14 | "purescript-strings": "^4.0.1", 15 | "purescript-hareactive": "^0.1.2", 16 | "purescript-numbers": "^7.0.0", 17 | "purescript-affjax": "^9.0.0", 18 | "purescript-argonaut-core": "^5.0.1", 19 | "purescript-argonaut-codecs": "^6.0.2" 20 | }, 21 | "devDependencies": { 22 | "purescript-psci-support": "^4.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Turbine.js: -------------------------------------------------------------------------------- 1 | var T = require('@funkia/turbine'); 2 | 3 | exports._runComponent = T.runComponent; 4 | 5 | exports._map = function(f, c) { 6 | return c.map(f); 7 | }; 8 | 9 | exports._apply = function(f, c) { 10 | return c.ap(f); 11 | }; 12 | 13 | exports._bind = function(c, f) { 14 | return c.chain(f); 15 | }; 16 | 17 | exports._merge = function() { 18 | return T.merge; 19 | }; 20 | 21 | exports.component = T.component; 22 | 23 | exports._use = function() { 24 | return function(c, r) { 25 | return T.use(r, c); 26 | }; 27 | }; 28 | 29 | exports._list = function() { 30 | return T.list; 31 | } 32 | 33 | exports._modelView = function(model, view) { 34 | return T.modelView(model, view)(); 35 | } 36 | 37 | exports.dynamic = T.dynamic; 38 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-turbine", 3 | "license": "MIT", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/funkia/purescript-turbine.git" 7 | }, 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "output" 13 | ], 14 | "dependencies": { 15 | "purescript-prelude": "^4.1.1", 16 | "purescript-console": "^4.2.0", 17 | "purescript-functions": "^4.0.0", 18 | "purescript-record": "^2.0.1", 19 | "purescript-effect": "^2.0.1", 20 | "purescript-arrays": "^5.3.0", 21 | "purescript-hareactive": "0.1.2", 22 | "purescript-web-html": "^2.2.1", 23 | "purescript-web-events": "^2.0.1", 24 | "purescript-web-uievents": "^2.0.0", 25 | "purescript-typelevel-prelude": "^5.0.0" 26 | }, 27 | "devDependencies": { 28 | "purescript-psci-support": "^4.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-turbine", 3 | "version": "0.0.1", 4 | "description": "PureScript bindings for Turbine", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "postinstall": "bower install", 11 | "build": "pulp build", 12 | "test": "pulp test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/funkia/purescript-turbine.git" 17 | }, 18 | "keywords": [ 19 | "purescript", 20 | "turbine", 21 | "frp", 22 | "hareactive", 23 | "pure" 24 | ], 25 | "author": "Funkia", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/funkia/purescript-turbine/issues" 29 | }, 30 | "homepage": "https://github.com/funkia/purescript-turbine#readme", 31 | "devDependencies": { 32 | "@funkia/hareactive": "0.3.4", 33 | "@funkia/turbine": "0.4.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/counters/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Counters.Main where 2 | 3 | import Prelude 4 | 5 | import Counters.Version1 as Version1 6 | import Counters.Version2 as Version2 7 | import Effect (Effect) 8 | import Hareactive.Combinators (stepper) 9 | import Turbine (Component, component, dynamic, use, output, runComponent, ()) 10 | import Turbine.HTML as E 11 | 12 | data Version = One | Two 13 | 14 | versionToComponent :: Version -> Component {} {} 15 | versionToComponent One = Version1.counter 0 16 | versionToComponent Two = Version2.counterList [0] 17 | 18 | app :: Component {} {} 19 | app = component \on -> do 20 | version <- stepper One on.selectVersion 21 | ( 22 | E.button {} (E.text "Version 1") `use` (\o -> { selectVersion: o.click $> One }) 23 | E.button {} (E.text "Version 2") `use` (\o -> { selectVersion: o.click $> Two }) 24 | (dynamic (versionToComponent <$> version)) 25 | ) `output` {} 26 | 27 | main :: Effect Unit 28 | main = runComponent "#mount" app 29 | -------------------------------------------------------------------------------- /examples/email-validator/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Data.Either (fromRight) 7 | import Data.String.Regex (Regex, regex, test) 8 | import Data.String.Regex.Flags (ignoreCase) 9 | import Partial.Unsafe (unsafePartial) 10 | import Turbine (Component, runComponent, use, component, output, ()) 11 | import Turbine.HTML as E 12 | 13 | emailRegex :: Regex 14 | emailRegex = unsafePartial $ fromRight $ regex ".+@.+\\..+" ignoreCase 15 | 16 | isValidEmail :: String -> Boolean 17 | isValidEmail = test emailRegex 18 | 19 | validToString :: Boolean -> String 20 | validToString = if _ then "valid" else "invalid" 21 | 22 | app :: Component {} {} 23 | app = component \{ email } -> 24 | ( 25 | E.h1 {} (E.text "Email validator") 26 | E.input {} `use` (\o -> { email: o.value }) 27 | E.p {} ( 28 | E.text "Email is " 29 | E.textB (validToString <$> (isValidEmail <$> email)) 30 | ) 31 | ) `output` {} 32 | 33 | main :: Effect Unit 34 | main = runComponent "#mount" app 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Funkia 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 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-turbine-examples", 3 | "scripts": { 4 | "link-turbine": "cd ..; bower link; cd examples; bower link purescript-turbine", 5 | "postinstall": "bower install; npm run link-turbine", 6 | "email-validator": "pulp browserify --main Main --include email-validator/src --to email-validator/bundle.js", 7 | "counters": "pulp browserify --main Counters.Main --include counters/src --to counters/bundle.js", 8 | "fahrenheit-celsius": "pulp browserify --main FahrenheitCelsius.Main --include fahrenheit-celsius/src --to fahrenheit-celsius/bundle.js", 9 | "todomvc": "pulp browserify --main TodoMVC.Main --include todomvc/src --to todomvc/bundle.js", 10 | "timer": "pulp browserify --main Timer.Main --include timer/src --to timer/bundle.js", 11 | "zip-codes": "pulp browserify --main ZipCodes.Main --include zip-codes/src --to zip-codes/bundle.js", 12 | "continuous-time": "pulp browserify --main ContinousTime.Main --include continuous-time --to continuous-time/bundle.js", 13 | "test": "npm run email-validator && npm run counters && npm run fahrenheit-celsius && npm run todomvc && npm run continuous-time" 14 | }, 15 | "dependencies": { 16 | "todomvc-app-css": "^2.1.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/continuous-time/src/Main.purs: -------------------------------------------------------------------------------- 1 | module ContinousTime.Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Hareactive.Types (Behavior, Stream) 7 | import Hareactive.Combinators (time, stepper, snapshot) 8 | import Data.Array (head) 9 | import Data.Maybe (fromMaybe) 10 | import Data.String (split, Pattern(..)) 11 | import Data.JSDate (fromTime, toTimeString) 12 | import Turbine (Component, runComponent, use, component, output, ()) 13 | import Turbine.HTML as E 14 | 15 | formatTime :: Number -> String 16 | formatTime = fromTime >>> toTimeString >>> split (Pattern " ") >>> head >>> fromMaybe "" 17 | 18 | type AppModelOut = 19 | { time :: Behavior Number 20 | , message :: Behavior String 21 | } 22 | 23 | type AppViewOut = { snapClick :: Stream Unit } 24 | 25 | app :: Component {} {} 26 | app = component \on -> do 27 | let msgFromClick = 28 | map (\t -> "You last pressed the button at " <> formatTime t) 29 | (snapshot time on.snapClick) 30 | message <- stepper "You've not clicked the button yet" msgFromClick 31 | ( E.h1 {} (E.text "Continuous") 32 | E.p {} (E.textB $ formatTime <$> time) 33 | E.button {} (E.text "Click to snap time") `use` (\o -> { snapClick: o.click }) 34 | E.p {} (E.textB message) 35 | ) `output` {} 36 | 37 | main :: Effect Unit 38 | main = runComponent "#mount" app 39 | -------------------------------------------------------------------------------- /examples/timer/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Timer.Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Hareactive.Combinators as H 7 | import Hareactive.Types (Behavior, Stream) 8 | import Math as Math 9 | import Turbine (Component, modelView, use, runComponent, static, ()) 10 | import Turbine.HTML as E 11 | 12 | resetOn :: forall a b. Behavior (Behavior a) -> Stream b -> Behavior (Behavior a) 13 | resetOn b reset = b >>= \bi -> H.switcherFrom bi (H.snapshot b reset) 14 | 15 | initialMaxTime :: Number 16 | initialMaxTime = 10.0 17 | 18 | timer :: Component _ {} 19 | timer = modelView model view 20 | where 21 | model input = do 22 | let change = (\max cur -> if cur < max then 0.001 else 0.0) <$> input.maxTime <*> input.elapsed 23 | elapsed <- H.sample $ resetOn (H.integrateFrom change) input.resetTimer 24 | pure { maxTime: input.maxTime, elapsed } 25 | view input = 26 | E.div {} ( 27 | E.h1 {} (E.text "Timer") 28 | E.span {} (E.text "0") 29 | E.progress { value: input.elapsed, max: input.maxTime } (E.empty) 30 | E.span {} (E.textB (show <$> input.maxTime)) 31 | E.div {} ( 32 | E.text "Elapsed seconds: " 33 | E.textB (show <$> Math.floor <$> input.elapsed) 34 | ) 35 | E.inputRange (static { value: show initialMaxTime, type: "range", min: 0.0, max: 60.0 }) 36 | `use` (\o -> { maxTime: o.value }) 37 | E.div {} ( 38 | E.button {} (E.text "Reset") `use` (\o -> { resetTimer: o.click }) 39 | ) 40 | ) `use` (\o -> { elapsed: input.elapsed }) 41 | 42 | main :: Effect Unit 43 | main = runComponent "#mount" timer 44 | -------------------------------------------------------------------------------- /examples/fahrenheit-celsius/src/Main.purs: -------------------------------------------------------------------------------- 1 | module FahrenheitCelsius.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Number (fromString) 6 | import Effect (Effect) 7 | import Hareactive.Types (Behavior, Stream) 8 | import Hareactive.Combinators (changes, filterJust, stepper) 9 | import Turbine (Component, component, use, output, runComponent, ()) 10 | import Turbine.HTML as E 11 | 12 | fahrenToCelsius :: Number -> Number 13 | fahrenToCelsius f = (f - 32.0) / 1.8 14 | 15 | celsiusToFahren :: Number -> Number 16 | celsiusToFahren c = (c * 9.0) / 5.0 + 32.0 17 | 18 | type TemperatureConverterOutput = 19 | { fahren :: Behavior String 20 | , celsius :: Behavior String 21 | } 22 | 23 | parseNumbers :: Stream String -> Stream Number 24 | parseNumbers = filterJust <<< (map fromString) 25 | 26 | temperatureConverter :: Component TemperatureConverterOutput {} 27 | temperatureConverter = component \on -> do 28 | let celsiusNrChange = parseNumbers on.celsiusChange 29 | fahrenNrChange = parseNumbers on.fahrenChange 30 | celsius <- stepper "0" (on.celsiusChange <> (show <$> fahrenToCelsius <$> fahrenNrChange)) 31 | fahren <- stepper "0" (on.fahrenChange <> (show <$> celsiusToFahren <$> celsiusNrChange)) 32 | ( E.div {} ( 33 | E.div {} ( 34 | E.label {} (E.text "Fahrenheit") 35 | E.input { value: fahren } `use` (\o -> { fahrenChange: changes o.value }) 36 | ) 37 | E.div {} ( 38 | E.label {} (E.text "Celsius") 39 | E.input { value: celsius } `use` (\o -> { celsiusChange: changes o.value }) 40 | ) 41 | ) 42 | ) `output` { fahren, celsius } 43 | 44 | main :: Effect Unit 45 | main = runComponent "#mount" temperatureConverter 46 | -------------------------------------------------------------------------------- /examples/counters/src/Version2.purs: -------------------------------------------------------------------------------- 1 | module Counters.Version2 2 | ( counterList 3 | ) where 4 | 5 | import Prelude 6 | 7 | import Control.Apply (lift2) 8 | import Data.Array (cons, filter) 9 | import Data.Foldable (fold, foldr) 10 | import Hareactive.Combinators (accum, scan, shiftCurrent) 11 | import Hareactive.Types (Behavior, Stream) 12 | import Turbine (Component, list, use, component, output, ()) 13 | import Turbine.HTML as E 14 | 15 | type CounterOut = 16 | { count :: Behavior Int 17 | , delete :: Stream Int 18 | } 19 | 20 | counter :: Int -> Component CounterOut {} 21 | counter id = component \on -> do 22 | count <- accum (+) 0 on.change 23 | ( 24 | E.div {} ( 25 | E.text "Counter " 26 | E.span {} (E.textB $ map show count) 27 | E.button {} (E.text "+" ) `use` (\o -> { change: o.click $> 1 }) 28 | E.button {} (E.text "-" ) `use` (\o -> { change: o.click $> -1 }) 29 | E.button {} (E.text "x") `use` (\o -> { delete: o.click }) 30 | ) 31 | ) `output` { count, delete: on.delete $> id } 32 | 33 | counterList :: Array Int -> Component {} {} 34 | counterList init = component \on -> do 35 | let sum = on.listOut >>= (map (_.count) >>> foldr (lift2 (+)) (pure 0)) 36 | let removeId = map (fold <<< map (_.delete)) on.listOut 37 | let removeCounter = map (\i -> filter (i /= _)) (shiftCurrent removeId) 38 | nextId <- scan (+) 0 (on.addCounter $> 1) 39 | let appendCounter = cons <$> nextId 40 | counterIds <- accum ($) init (appendCounter <> removeCounter) 41 | ( 42 | E.div {} ( 43 | E.h1 {} (E.text "Counters") 44 | E.span {} (E.textB (map (\n -> "Sum " <> show n) sum)) 45 | E.button {} (E.text "Add counter") `use` (\o -> { addCounter: o.click }) 46 | list (\id -> counter id `use` identity) counterIds identity `use` (\o -> { listOut: o }) 47 | ) 48 | ) `output` {} 49 | -------------------------------------------------------------------------------- /examples/zip-codes/src/Main.purs: -------------------------------------------------------------------------------- 1 | module ZipCodes.Main where 2 | 3 | import Prelude 4 | 5 | import Affjax as AX 6 | import Affjax.ResponseFormat as ResponseFormat 7 | import Affjax.StatusCode (StatusCode(..)) 8 | import Data.Argonaut.Decode (class DecodeJson, decodeJson, (.:)) 9 | import Data.Array (head) 10 | import Data.Either (fromRight, hush) 11 | import Data.Maybe (fromMaybe) 12 | import Data.String.Regex (Regex, regex, test) 13 | import Data.String.Regex.Flags (ignoreCase) 14 | import Effect (Effect) 15 | import Effect.Aff (Aff) 16 | import Hareactive.Combinators (changes, split, filterJust, stepper, runStreamAff) 17 | import Partial.Unsafe (unsafePartial) 18 | import Turbine (Component, component, output, use, runComponent, (), static) 19 | import Turbine.HTML as E 20 | 21 | zipRegex :: Regex 22 | zipRegex = unsafePartial $ fromRight $ regex "^\\d{5}$" ignoreCase 23 | 24 | isValidZip :: String -> Boolean 25 | isValidZip = test zipRegex 26 | 27 | apiUrl :: String 28 | apiUrl = "http://api.zippopotam.us/us/" 29 | 30 | newtype Place = Place { name :: String 31 | , state :: String 32 | } 33 | 34 | instance decodeJsonAppUser :: DecodeJson Place where 35 | decodeJson json = do 36 | obj <- decodeJson json 37 | name <- obj .: "place name" 38 | state <- obj .: "state" 39 | pure $ Place { name, state } 40 | 41 | type ZipResult = { places :: Array Place 42 | , country :: String 43 | } 44 | 45 | fetchZip :: String -> Aff String 46 | fetchZip zipCode = do 47 | res <- AX.get ResponseFormat.json (apiUrl <> zipCode) 48 | pure 49 | if res.status == StatusCode 404 50 | then "Zip code does not exist" 51 | else fromMaybe "Zip code lookup failed" $ do 52 | result :: ZipResult <- res.body # hush >>= (decodeJson >>> hush) 53 | Place place <- head result.places 54 | pure $ "Valid zip code for " <> place.name <> ", " <> place.state <> ", " <> result.country 55 | 56 | app :: Component {} {} 57 | app = component \on -> do 58 | let zipCodeChange = changes on.zipCode 59 | { pass: validZipCodeChange, fail: invalidZipCodeChange } = split isValidZip zipCodeChange 60 | fetchResult <- runStreamAff $ map fetchZip validZipCodeChange 61 | let statusChange = 62 | (invalidZipCodeChange $> "Not a valid zip code") <> 63 | (validZipCodeChange $> "Loading ...") <> 64 | (fetchResult <#> hush # filterJust) 65 | status <- stepper "" statusChange 66 | ( E.div {} ( 67 | E.span {} (E.text "Please type a valid US zip code: ") 68 | E.input (static { placeholder: "Zip code" }) `use` (\o -> { zipCode: o.value }) 69 | E.br 70 | E.span {} (E.textB status) 71 | ) 72 | ) `output` {} 73 | 74 | main :: Effect Unit 75 | main = runComponent "#mount" app 76 | -------------------------------------------------------------------------------- /src/Turbine/HTML.js: -------------------------------------------------------------------------------- 1 | var T = require('@funkia/turbine'); 2 | 3 | // Function for class descriptions 4 | 5 | function arrayOf(a) { 6 | return [a]; 7 | } 8 | 9 | exports.processAttributes = function(attrs) { 10 | // This function handles the small differences between the way the attributes 11 | // object is structured in purescript-turbine and vanilla Turbine. 12 | var newAttrs = Object.assign({}, attrs); 13 | if (attrs.classes !== undefined) { 14 | // Only create an array if necessary 15 | newAttrs.class = [attrs.class, attrs.classes]; 16 | delete newAttrs.classes; 17 | } 18 | return newAttrs; 19 | } 20 | 21 | exports.staticClass = arrayOf; 22 | 23 | exports.dynamicClass = arrayOf; 24 | 25 | exports._toggleClass = function(name, behavior) { 26 | var obj = {}; 27 | obj[name] = behavior; 28 | return [obj]; 29 | }; 30 | 31 | exports._h1 = function() { 32 | return T.elements.h1; 33 | }; 34 | 35 | exports._h2 = function() { 36 | return T.elements.h2; 37 | }; 38 | 39 | exports._h3 = function() { 40 | return T.elements.h3; 41 | }; 42 | 43 | exports._h4 = function() { 44 | return T.elements.h4; 45 | }; 46 | 47 | exports._h5 = function() { 48 | return T.elements.h5; 49 | }; 50 | 51 | exports._h6 = function() { 52 | return T.elements.h6; 53 | }; 54 | 55 | exports._ul = function() { 56 | return T.elements.ul; 57 | }; 58 | 59 | exports._li = function() { 60 | return T.elements.li; 61 | }; 62 | 63 | exports._span = function() { 64 | return T.elements.span; 65 | }; 66 | 67 | exports._div = function() { 68 | return T.elements.div; 69 | }; 70 | 71 | exports._input = function() { 72 | return T.elements.input; 73 | }; 74 | 75 | exports._inputRange = function () { 76 | return function(attrs) { 77 | var attrs2 = Object.assign({ type: "range" }, attrs) 78 | return T.elements.input(attrs2); 79 | } ; 80 | }; 81 | 82 | exports._textarea = function() { 83 | return T.elements.textarea; 84 | }; 85 | 86 | exports._checkbox = function() { 87 | return T.elements.checkbox; 88 | }; 89 | 90 | exports._a = function() { 91 | return T.elements.a; 92 | }; 93 | 94 | exports._p = function() { 95 | return T.elements.p; 96 | }; 97 | 98 | exports._button = function() { 99 | return T.elements.button; 100 | }; 101 | 102 | exports._label = function() { 103 | return T.elements.label; 104 | }; 105 | 106 | exports._header = function() { 107 | return T.elements.header; 108 | }; 109 | 110 | exports._footer = function() { 111 | return T.elements.footer; 112 | }; 113 | 114 | exports._section = function() { 115 | return T.elements.section; 116 | }; 117 | 118 | exports._table = function() { 119 | return T.elements.table; 120 | }; 121 | 122 | exports._th = function() { 123 | return T.elements.th; 124 | }; 125 | 126 | exports._tr = function() { 127 | return T.elements.tr; 128 | }; 129 | 130 | exports._td = function() { 131 | return T.elements.td; 132 | }; 133 | 134 | exports._progress = function() { 135 | return T.elements.progress; 136 | }; 137 | 138 | exports._text = T.text; 139 | 140 | exports._textB = T.dynamic; 141 | 142 | exports.br = T.elements.br; 143 | 144 | exports.empty = T.emptyComponent; 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-turbine 2 | 3 | [![Turbine on Pursuit](https://pursuit.purescript.org/packages/purescript-turbine/badge)](https://pursuit.purescript.org/packages/purescript-turbine) 4 | [![Build status](https://travis-ci.org/funkia/purescript-turbine.svg?branch=master)](https://travis-ci.org/funkia/purescript-turbine) 5 | 6 | A purely functional library for building user interfaces powered by FRP. 7 | 8 | * Based on higher-order FRP with support for continuous time. 9 | * Concise and powerful thanks to FRP. 10 | * No big global state/model. Everything is encapsulated in components. 11 | * Type-safe communication between views and models. 12 | * Model logic and view code is kept separate for logic-less views. 13 | * Highly modular and composable thanks to encapsulated stateful components. 14 | * Avoids using virtual DOM by utilizing FRP to make all changes to the DOM in reaction to changing behaviors. 15 | * Easy to reason about thanks to tractable reactive data flow. 16 | 17 | ## Table of contents 18 | 19 | * [Inline Examples](#inline-examples) 20 | * [Examples](#examples) 21 | * [Installation](#installation) 22 | * [Documentation](#documentation) 23 | 24 | ## Inline Examples 25 | 26 | ### Single counter 27 | 28 | ![single counter GIF](examples/counters/single-counter.gif) 29 | 30 | ```purescript 31 | counter id = component \on -> do 32 | count <- accum (+) 0 on.change 33 | ( H.div {} ( 34 | H.text "Counter " 35 | H.span {} (H.textB $ map show count) 36 | H.button {} (H.text "+" ) `use` (\o -> { change: o.click $> 1 }) 37 | H.button {} (H.text "-" ) `use` (\o -> { change: o.click $> -1 }) 38 | ) 39 | ) `output` {} 40 | 41 | main = runComponent "#mount" (counter 0) 42 | ``` 43 | 44 | ### List of counters 45 | 46 | Show a list of counters. New counters can be added to the list. Existing 47 | counters can be deleted. The aggregated sum of all the counters is shown. 48 | 49 | ![list of counters GIF](examples/counters/list-counter.gif) 50 | 51 | ```purescript 52 | counter id = component \on -> do 53 | count <- accum (+) 0 on.change 54 | ( H.div {} ( 55 | H.text "Counter " 56 | H.span {} (H.textB $ map show count) 57 | H.button {} (H.text "+" ) `use` (\o -> { change: o.click $> 1 }) 58 | H.button {} (H.text "-" ) `use` (\o -> { change: o.click $> -1 }) 59 | H.button {} (H.text "x") `use` (\o -> { delete: o.click }) 60 | ) 61 | ) `output` { count, delete: on.delete $> id } 62 | 63 | counterList init = component \on -> do 64 | let sum = on.listOut >>= (map (_.count) >>> foldr (lift2 (+)) (pure 0)) 65 | let removeId = map (fold <<< map (_.delete)) on.listOut 66 | let removeCounter = map (\i -> filter (i /= _)) (shiftCurrent removeId) 67 | nextId <- scan (+) 0 (on.addCounter $> 1) 68 | let appendCounter = cons <$> nextId 69 | counterIds <- accum ($) init (appendCounter <> removeCounter) 70 | ( H.div {} ( 71 | H.h1 {} (H.text "Counters") 72 | H.span {} (H.textB (map (\n -> "Sum " <> show n) sum)) 73 | H.button {} (H.text "Add counter") `use` (\o -> { addCounter: o.click }) 74 | list (\id -> counter id `use` identity) counterIds identity `use` (\o -> { listOut: o }) 75 | ) 76 | ) `output` {} 77 | 78 | main = runComponent "#mount" (counterList [0]) 79 | ``` 80 | 81 | ## Installation 82 | 83 | The following installs Hareactive and Turbine. Hareactive is the FRP library 84 | that Turbine builds upon and is a hard dependency. 85 | 86 | ``` 87 | npm i @funkia/hareactive 88 | bower install --save purescript-hareactive 89 | npm i @funkia/turbine 90 | bower install --save purescript-turbine 91 | ``` 92 | 93 | Alternatively, use the 94 | [purescript-turbine-starter](https://github.com/funkia/purescript-turbine-starter), 95 | a project template that contains Turbine and Hareactive pre-setup. 96 | 97 | ## Documentation 98 | 99 | The best place to start is the [getting started 100 | tutorial](/docs/getting-started-tutorial.md). It is a quick start tutorial 101 | which aims to explain the basics of Turbine as briefly as possible. 102 | 103 | There is also an older, slightly outdated, [tutorial](./docs/tutorial.md). 104 | 105 | Both [API documentation for 106 | Turbine](https://pursuit.purescript.org/packages/purescript-turbine) and [API 107 | documentation for 108 | Hareactive](https://pursuit.purescript.org/packages/purescript-hareactive) is 109 | on Pursuit. 110 | 111 | Taking a look at the [examples](#example) is also a great way to see Turbine 112 | used in practice and to see how specific things are accomplished. 113 | 114 | ## Examples 115 | 116 | - [Email validator](/examples/email-validator) -- A simple email validator. 117 | - [Fahrenheit celsius converter](/examples/fahrenheit-celsius) -- Conversion between fahrenheit and celsius. 118 | - [Counters](/examples/counters) -- A list of counters. This example shows how to create a dynamic list of components. 119 | - [Continuous time](/examples/continuous-time) -- A very simple example showing how to work with continuous time. 120 | - [Timer](/examples/timer) -- A more complicated example demonstrating continuous time by implementing a timer with a progress bar. 121 | - [Zip codes](/examples/zip-codes) -- A zip code validator. Shows how to perform `Effect`s using FRP. 122 | - [TodoMVC](/examples/todomvc) -- The classic TodoMVC example (still has a couple of bugs and no routing). 123 | 124 | -------------------------------------------------------------------------------- /examples/todomvc/src/Main.purs: -------------------------------------------------------------------------------- 1 | module TodoMVC.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Array (filter, null, snoc, length) 6 | import Data.Traversable (fold) 7 | import Effect (Effect) 8 | import Hareactive.Combinators as H 9 | import Hareactive.Types (Behavior, Stream, Now) 10 | import Turbine (Component, modelView, use, runComponent, withStatic, (), list) 11 | import Turbine.HTML as E 12 | import Web.UIEvent.KeyboardEvent as KE 13 | 14 | 15 | isKey :: String -> KE.KeyboardEvent -> Boolean 16 | isKey key event = (KE.key event) == key 17 | 18 | type NewTodo = 19 | { id :: Int 20 | , name :: String 21 | } 22 | 23 | todoInput :: Component { clearedValue :: Behavior String, addItem :: Stream String } {} 24 | todoInput = modelView model view 25 | where 26 | model { keyup, value } = do 27 | let enterPressed = H.filter (isKey "Enter") keyup 28 | clearedValue <- H.stepper "" ((enterPressed $> "") <> H.changes value) 29 | let addItem = H.filter (_ /= "") $ H.snapshot clearedValue enterPressed 30 | pure { clearedValue, addItem } 31 | view input = 32 | E.input ({ value: input.clearedValue, class: pure "new-todo" } `withStatic` { 33 | autofocus: true, 34 | placeholder: "What needs to be done?" 35 | }) `use` (\o -> { keyup: o.keyup, value: o.value }) 36 | 37 | type TodoItemOut = 38 | { isComplete :: Behavior Boolean 39 | , name :: Behavior String 40 | , isEditing :: Behavior Boolean 41 | , delete :: Stream Int 42 | } 43 | 44 | todoItem :: NewTodo -> Component TodoItemOut {} 45 | todoItem options = modelView model view 46 | where 47 | model input = do 48 | isComplete <- H.stepper false input.toggleTodo 49 | let cancelEditing = H.filter (isKey "Escape") input.nameKeyup 50 | let finishEditing = H.filter (isKey "Enter") input.nameKeyup 51 | -- Editing should stop either on enter or on escape 52 | let stopEditing = cancelEditing <> finishEditing 53 | -- The name when editing started 54 | initialName <- H.stepper "" (H.snapshot input.name input.startEditing) 55 | -- When editing is canceled the name should be reset to what is was when 56 | -- editing begun. 57 | let cancelName = H.snapshot initialName cancelEditing 58 | isEditing <- H.toggle false (input.startEditing) stopEditing 59 | name <- H.stepper options.name (H.changes input.name <> cancelName) 60 | -- If the delete button is clicked we should signal to parent 61 | let delete = input.deleteClicked $> options.id 62 | pure { isComplete, name, isEditing, delete } 63 | view input = 64 | E.li ({ class: pure "todo" 65 | , classes: E.toggleClass "completed" input.isComplete 66 | <> E.toggleClass "editing" input.isEditing 67 | }) ( 68 | E.div ({ class: pure "view" }) ( 69 | E.checkbox 70 | ({ checked: input.isComplete 71 | , class: pure "toggle" 72 | }) `use` (\o -> { toggleTodo: o.checkedChange }) 73 | E.label {} (E.textB input.name) `use` (\o -> { startEditing: o.dblclick }) 74 | E.button { class: pure "destroy" } (E.text "") `use` (\o -> { deleteClicked: o.click }) 75 | ) 76 | E.input ({ value: input.name, class: pure "edit" }) `use` (\o -> { 77 | name: o.value, 78 | nameKeyup: o.keyup, 79 | nameBlur: o.blur 80 | }) 81 | ) 82 | 83 | -- Footer 84 | 85 | formatRemainder :: Int -> String 86 | formatRemainder n = (show n) <> " item" <> (if n == 1 then "" else "s") <> " left" 87 | 88 | todoFooter options = modelView model view 89 | where 90 | model input = do 91 | let itemsLeft = H.moment (\at -> length $ filter (not <<< at <<< (_.isComplete)) (at options.todos)) 92 | pure { todos: options.todos, itemsLeft } 93 | view input = 94 | let 95 | hidden = map null input.todos 96 | in 97 | E.footer { class: pure "footer", classes: E.toggleClass "hidden" hidden } ( 98 | E.span { class: pure "footer" } ( 99 | E.textB (formatRemainder <$> input.itemsLeft) 100 | ) 101 | E.ul { class: pure "filters" } ( 102 | E.text "filters" 103 | ) 104 | E.button {} (E.text "Clear completed") 105 | ) 106 | 107 | type TodoAppModelOut = { todos :: Behavior (Array NewTodo), items :: Behavior (Array TodoItemOut) } 108 | 109 | type TodoAppViewOut = { addItem :: Stream String, items :: Behavior (Array TodoItemOut) } 110 | 111 | todoAppModel :: TodoAppViewOut -> Now TodoAppModelOut 112 | todoAppModel input = do 113 | nextId <- H.accum (+) 0 (input.addItem $> 1) 114 | let itemToDelete = H.shiftCurrent $ map (fold <<< map _.delete) input.items 115 | let newTodo = H.snapshotWith (\name id -> { name, id }) nextId input.addItem 116 | todos <- H.accum ($) [] ( 117 | (flip snoc <$> newTodo) <> 118 | ((\id -> filter ((_ /= id) <<< (_.id))) <$> itemToDelete) 119 | ) 120 | pure { todos, items: input.items } 121 | 122 | todoAppView :: TodoAppModelOut -> Component _ TodoAppViewOut 123 | todoAppView input = 124 | E.section { class: pure "todoapp" } ( 125 | E.header { class: pure "header" } ( 126 | E.h1 {} (E.text "todo") 127 | todoInput `use` (\o -> { addItem: o.addItem }) 128 | E.ul { class: pure "todo-list" } ( 129 | list (\i -> todoItem i `use` identity) input.todos (_.id) `use` (\o -> { items: o }) 130 | ) 131 | todoFooter { todos: input.items } 132 | ) 133 | ) 134 | app :: Component TodoAppModelOut {} 135 | app = modelView todoAppModel todoAppView 136 | 137 | main :: Effect Unit 138 | main = runComponent "#mount" app 139 | -------------------------------------------------------------------------------- /src/Turbine.purs: -------------------------------------------------------------------------------- 1 | module Turbine 2 | ( Component 3 | , runComponent 4 | , modelView 5 | , merge 6 | , () 7 | , dynamic 8 | , use 9 | , component 10 | , ComponentResult 11 | , output 12 | , class Key 13 | , keyNoop 14 | , list 15 | , class MapRecord 16 | , mapRecordBuilder 17 | , static 18 | , withStatic 19 | ) where 20 | 21 | import Prelude 22 | 23 | import Data.Function.Uncurried (Fn2, runFn2, Fn3, runFn3) 24 | import Data.Symbol (class IsSymbol, SProxy(..)) 25 | import Effect (Effect) 26 | import Effect.Uncurried (EffectFn2, runEffectFn2) 27 | import Hareactive.Types (Behavior, Now) 28 | import Prim.Row (class Union) 29 | import Prim.Row as Row 30 | import Prim.RowList as RL 31 | import Record as R 32 | import Record.Builder (Builder) 33 | import Record.Builder as Builder 34 | import Type.Data.RowList (RLProxy(..)) 35 | 36 | foreign import data Component :: Type -> Type -> Type 37 | 38 | -- Component instances 39 | 40 | instance functorComponent :: Functor (Component a) where 41 | map = runFn2 _map 42 | 43 | foreign import _map :: forall a o p. Fn2 (o -> p) (Component a o) (Component a p) 44 | 45 | instance applyComponent :: Apply (Component a) where 46 | apply = runFn2 _apply 47 | 48 | foreign import _apply :: forall a o p. Fn2 (Component a (o -> p)) (Component a o) (Component a p) 49 | 50 | instance bindComponent :: Bind (Component a) where 51 | bind = runFn2 _bind 52 | 53 | foreign import _bind :: forall a o p. Fn2 (Component a o) (o -> Component a p) (Component a p) 54 | 55 | modelView :: forall a o p. (o -> Now p) -> (p -> Component a o) -> Component p {} 56 | modelView m v = runFn2 _modelView m v 57 | 58 | foreign import _modelView :: forall a o p. Fn2 (o -> Now p) (p -> Component a o) (Component p {}) 59 | 60 | runComponent :: forall a o. String -> Component a o -> Effect Unit 61 | runComponent = runEffectFn2 _runComponent 62 | 63 | foreign import _runComponent :: forall a o. EffectFn2 String (Component a o) Unit 64 | 65 | -- | Turns a behavior of a component into a component of a behavior. This 66 | -- | function is used to create dynamic HTML where the structure of the HTML 67 | -- | should change over time. 68 | -- | 69 | -- | ```purescript 70 | -- | dynamic (map (\b -> if b else (div {} ) then) behavior) 71 | -- | ``` 72 | foreign import dynamic :: forall a o. Behavior (Component a o) -> Component (Behavior o) {} 73 | 74 | -- | Type class implemented by `Int`, `Number`, and `String`. Used to represent 75 | -- | overloads. 76 | class Key a where 77 | keyNoop :: a -> a 78 | 79 | instance keyInt :: Key Int where 80 | keyNoop = identity 81 | 82 | instance keyNumber :: Key Number where 83 | keyNoop = identity 84 | 85 | instance keyString :: Key String where 86 | keyNoop = identity 87 | 88 | list :: forall a b o k. Key k => 89 | (a -> Component b o) -> Behavior (Array a) -> (a -> k) -> Component (Behavior (Array o)) {} 90 | list = runFn3 _list 91 | 92 | foreign import _list :: forall a b o k. Key k => 93 | Fn3 (a -> Component b o) (Behavior (Array a)) (a -> k) (Component (Behavior (Array o)) {}) 94 | 95 | -- | Combines two components and merges their selected output. 96 | merge :: forall a o b p q. Union o p q => Component a { | o } -> Component b { | p } -> Component {} { | q } 97 | merge = runFn2 _merge 98 | 99 | foreign import _merge :: forall a o b p q. Union o p q => Fn2 (Component a { | o }) (Component b { | p }) (Component {} { | q }) 100 | infixl 0 merge as 101 | 102 | -- | Copies non-selected output into selected output. 103 | -- | 104 | -- | This function is often used in infix form as in the following example. 105 | -- | 106 | -- | ```purescript 107 | -- | button (text "Fire missiles!") `use` (\o -> { fireMissiles }) 108 | -- | ``` 109 | use :: forall a o p q. Union o p q => Component a { | o } -> (a -> { | p }) -> Component {} { | q } 110 | use = runFn2 _use 111 | 112 | foreign import _use :: forall a o p q. Union o p q => Fn2 (Component a { | o }) (a -> { | p }) (Component {} { | q }) 113 | 114 | type ComponentResult a o p = 115 | { component :: Component a o 116 | , available :: p 117 | } 118 | 119 | output :: forall a o p f. Applicative f => Component a o -> p -> f (ComponentResult a o p) 120 | output c a = pure { component: c, available: a } 121 | 122 | foreign import component :: forall a o p. (o -> Now (ComponentResult a o p)) -> Component p {} 123 | 124 | mapHeterogenousRecord :: forall row xs f row' 125 | . RL.RowToList row xs 126 | => MapRecord xs row f () row' 127 | => (forall a. a -> f a) 128 | -> Record row 129 | -> Record row' 130 | mapHeterogenousRecord f r = Builder.build builder {} 131 | where 132 | builder = mapRecordBuilder (RLProxy :: RLProxy xs) f r 133 | 134 | class MapRecord (xs :: RL.RowList) (row :: # Type) f (from :: # Type) (to :: # Type) 135 | | xs -> row f from to where 136 | mapRecordBuilder :: RLProxy xs -> (forall a. a -> f a) -> Record row -> Builder { | from } { | to } 137 | 138 | instance mapRecordCons :: 139 | ( IsSymbol name 140 | , Row.Cons name a trash row 141 | , MapRecord tail row f from from' 142 | , Row.Lacks name from' 143 | , Row.Cons name (f a) from' to 144 | ) => MapRecord (RL.Cons name a tail) row f from to where 145 | mapRecordBuilder _ f r = 146 | first <<< rest 147 | where 148 | nameP = SProxy :: SProxy name 149 | val = f $ R.get nameP r 150 | rest = mapRecordBuilder (RLProxy :: RLProxy tail) f r 151 | first = Builder.insert nameP val 152 | 153 | instance mapRecordNil :: MapRecord RL.Nil row f () () where 154 | mapRecordBuilder _ _ _ = identity 155 | 156 | -- | A helper function used to convert static values in records into constant 157 | -- | behaviors. 158 | -- | 159 | -- | Component functions often takes a large amount of behaviors as input. But, 160 | -- | sometimes all that is required is static values, that is, constant 161 | -- | behaviors. In these cases it can sometimes be tedious to write records like 162 | -- | the following: 163 | -- | 164 | -- | ```purescript 165 | -- | { foo: pure 1, bar: pure 2, baz: pure 3, more: pure 4, fields: pure 5 } 166 | -- | ``` 167 | -- | 168 | -- | The `static` function applies `pure` to each value in the given record. As 169 | -- | such, the above can be shortened into the following. 170 | -- | 171 | -- | ```purescript 172 | -- | static { foo: 1, bar: 2, baz: 3, more: 4, fields: 5 } 173 | -- | ``` 174 | static :: forall a c row. RL.RowToList row c => MapRecord c row Behavior () a => { | row } -> { | a } 175 | static = mapHeterogenousRecord pure 176 | 177 | -- | A function closely related to `static`. Usefull in cases where a component 178 | -- | function is to be supplied with both a set of static values (constant 179 | -- | behaviors). The function applies `static` to its seconds argument and 180 | -- | merges the two records. 181 | -- | 182 | -- | It is often used in infix form as in the following example. 183 | -- | 184 | -- | ```purescript 185 | -- | { foo: behA, bar: behB } `withStatic` { baz: 3, more: 4, fields: 5 } 186 | -- | ``` 187 | withStatic :: forall o p q q' p' xs 188 | . RL.RowToList p xs 189 | => MapRecord xs p Behavior () p' 190 | => Union o p' q' 191 | => Row.Nub q' q 192 | => { | o } -> { | p } -> { | q } 193 | withStatic a b = R.merge a (static b) 194 | -------------------------------------------------------------------------------- /docs/getting-started-tutorial.md: -------------------------------------------------------------------------------- 1 | # Getting started tutorial 2 | 3 | This is a quick start tutorial which aims to explain the basics of Turbine as 4 | briefly as possible. 5 | 6 | Turbine is a fairly small library build on top of the FRP library Hareactive. 7 | When learning Turbine the bulk of the work is actually to learn Hareactive and 8 | FRP. This tutorial covers most of Turbine but only the essential types in 9 | Hareactive and a small fraction of its API. 10 | 11 | ## A small Turbine app 12 | 13 | The following code is a small complete Turbine app: 14 | 15 | ```purescript 16 | import Hareactive.Combinators (accum) 17 | import Turbine (Component, use, component, output, (), runComponent) 18 | import Turbine.HTML as E 19 | 20 | counter :: Component { count :: Behavior Int } {} 21 | counter id = component \on -> do 22 | count <- accum (+) 0 (on.increment <> on.decrement) 23 | ( E.div { class: pure "wrapper" } ( 24 | E.text "Counter " 25 | E.span {} (E.textB $ map show count) 26 | E.button {} (E.text "+" ) `use` (\o -> { increment: o.click $> 1 }) 27 | E.button {} (E.text "-" ) `use` (\o -> { decrement: o.click $> -1 }) 28 | ) 29 | ) `output` { count } 30 | 31 | main = runComponent "#mount" counter 32 | ``` 33 | 34 | The code creates an application that functions as shown in the GIF below. 35 | 36 | ![single counter GIF](/examples/counters/single-counter.gif) 37 | 38 | This small example uses almost every important function in Turbine. Hence, by 39 | explaining every detail of the example from top to bottom this tutorial covers 40 | most of Turbine. 41 | 42 | ## The types 43 | 44 | Let's first consider each of the types used in the example. The three key FRP 45 | types from Hareactive all come into play: 46 | 47 | * [Behavior](https://pursuit.purescript.org/packages/purescript-hareactive/docs/Hareactive.Types#t:Behavior): 48 | A `Behavior a` represents a value of type `a` that changes over time. In the 49 | example above `count` has the type `Behavior Int` because the count changes 50 | over time in response to the buttons. 51 | * [Stream](https://pursuit.purescript.org/packages/purescript-hareactive/docs/Hareactive.Types#t:Stream): 52 | A `Stream a` represents events or occurrences that happens at specific 53 | moments in time. In the example, `increment` and `decrement` are streams 54 | created from the click events on each button. 55 | * [Now](https://pursuit.purescript.org/packages/purescript-hareactive/docs/Hareactive.Types#t:Now). 56 | A `Now a` represents a computation that runs in an atomic moment of time, 57 | which has access to the current time, and which can have side-effects. A `Now 58 | a` can be though of as equivalent to `Time -> Effect a`. In the example the 59 | function passed to `component` returns a `Now` hence the `do` makes the 60 | function run in the `Now`-monad. 61 | 62 | The primary type which Turbine adds on top of Hareactive is 63 | [Component](https://pursuit.purescript.org/packages/purescript-turbine/docs/Turbine#t:Component). 64 | A component is an encapsulated description of a piece of user interface as well 65 | as the logic and state controlling it. The `Component` type constructor takes 66 | two type parameters. For instance, `counter` in the example is of the type: 67 | 68 | ```purescript 69 | counter :: Component { count :: Behavior Int } {} 70 | ``` 71 | 72 | The first argument to `Component` is called the component's _available output_ 73 | and the second is called the component's _selected output_. Similarly to how 74 | `Effect a` denotes a computation with side-effects that produces a value of 75 | type `a` the type `Component a b` denotes a component that when constructed 76 | produces two outputs, one of type `a` and one of type `b`. A component's 77 | available output represents all the events and values that it exposes and the 78 | selected output is the part of this which the user of the component has 79 | explicitly declared interest in. 80 | 81 | If you think of a DOM element, then the available output corresponds to all the 82 | events that we _could_ listen to by calling `addEventListener` on the element 83 | and the selected output corresponds to the things that we have already declared 84 | interest in by calling `addEventListener` on the element. 85 | 86 | ## `component` 87 | 88 | In the example `counter` is a component created with the `component` function. 89 | A Turbine application is structured using components and the `component` 90 | function is the primary way to create custom components. It has the type: 91 | 92 | ```purecript 93 | component :: forall a o p. (o -> Now (ComponentResult a o p)) -> Component p {} 94 | ``` 95 | 96 | Values of type `ComponentResult` are constructed with the `output` function: 97 | 98 | ```purescript 99 | output :: forall a o p. Component a o -> p -> Now (ComponentResult a o p) 100 | ``` 101 | 102 | Hence, the function given to `component` essentially returns two values. A 103 | component of type `Component a o` and a value of some type `p`. The function 104 | then receives an argument of type `o`. Notice the recursive dependency: the 105 | selected output of the returned component is passed as input to the function. 106 | 107 | The selected output of the component returned in the example has the type `{ 108 | increment :: Stream Int, decrement :: Stream Int }`. These streams come from 109 | the HTML view construct further down and they correspond to the click events 110 | from each of the buttons. The `increment` stream has an occurrence with the 111 | value `1` whenever the "+" button is pressed and the `decrement` stream has an 112 | occurrence with the value `-1` whenever the "-" is pressed. 113 | 114 | Streams are monoids and the expression `on.increment <> on.decrement` results 115 | in a new stream that combines the occurrences of both streams. 116 | 117 | We then apply `accum` to the combined streams: `accum (+) 0 (on.increment <> 118 | on.decrement)`. The `accum` function can be though of as a "fold over time" and 119 | it has a type that resembles that of `foldr`. 120 | 121 | ```purescript 122 | accum :: forall a b. (a -> b -> b) -> b -> Stream a -> Now (Behavior b) 123 | ``` 124 | 125 | `accum` returns a value in the `Now` monad because whenever we accumulate state 126 | over time the result depends on when we start accumulating. If you count how 127 | many cars pass by a certain road starting from right now you are not going to 128 | have the same count as someone who started counting yesterday. Hence, `accum` 129 | returns a `Now` and the `Now` returned to `component` is run when the component 130 | is constructed. Thus, our counter will begin accumulating right when it is 131 | constructed. It starts with the initial value `0` (the second argument to 132 | `accum`) and every time the stream has an occurrence `+` (the first argument to 133 | `accum`) is applied to the current value and the value of the occurrence. The 134 | result becomes the new current value. 135 | 136 | As in the example below `component` is typically used following this pattern or 137 | template: 138 | 139 | ```purecript 140 | myComponent = component \on -> do 141 | // Logic/model 142 | ... 143 | // View 144 | view `output` { ... } 145 | ``` 146 | 147 | As indicated returned component is often called the componen's "view" and the 148 | code above is called the component's "logic" or "model". The view can use 149 | values defined in the model and the model can use selected output from the 150 | view. This circular dependency arises naturally when using FRP to build UI. 151 | 152 | ## Building HTML 153 | 154 | The next part of the example is the part that constructs the HTML for the 155 | counter: 156 | 157 | ```purescript 158 | E.div { class: pure "wrapper" } ( 159 | E.text "Counter " 160 | E.span {} (E.textB $ map show count) 161 | E.button {} (E.text "+" ) `use` (\o -> { increment: o.click $> 1 }) 162 | E.button {} (E.text "-" ) `use` (\o -> { decrement: o.click $> -1 }) 163 | ) 164 | ``` 165 | 166 | The module `Turbine.HTML` contains element functions for creating components 167 | that correspond to HTML elements. These takes a record of attributes and a 168 | child component---except in cases when a HTML doesn't support one or both of 169 | those things. 170 | 171 | As mentioned, in FRP behaviors are used to represent values that change over 172 | time. To support creating HTML that change over time behaviors can be given to 173 | the element functions. Since we want the outer `div` to constantly have the 174 | class `wrapper` we use `pure "wrapper"` which creates a constant behavior. 175 | 176 | The function `textB` has the type `Behavior String -> Component {} {}`. It 177 | takes a behavior and returns a component which creates a text node that changes 178 | its content as the given behavior changes. Therefore the expression `E.textB 179 | count` creates a text node that always shows the current count. 180 | 181 | Components are composed with the `merge` function, used as its `` operator. 182 | The expression `a b` is a new component which creates the HTML from `a` 183 | followed by the HTML from `b`. `merge` has the type: 184 | 185 | ```purescript 186 | merge :: forall a o b p q. Union o p q => Component a { | o } -> Component b { | p } -> Component {} { | q } 187 | ``` 188 | 189 | From the constraint `Union o p q` the PureScript compiler infers `q` to be the 190 | union of `o` and `p`. This means that when combining two components `merge` 191 | throws away the two component's available output (notice how `a` and `b` does 192 | not appear in the return type) and it combines the two component's selected 193 | output as the selected output of the resulting component. Hence, when composing 194 | components their selected output propagates out into the final component while 195 | the available output it discarded. 196 | 197 | If, for some component, we want to use parts of its available output we apply 198 | the `use` function. This function copies output from the available part into 199 | the selected part. It has the type: 200 | 201 | ```purescript 202 | use :: forall a o p q. Union o p q => Component a { | o } -> (a -> { | p }) -> Component {} { | q } 203 | ``` 204 | 205 | In other words, `use` takes a component and a function. The component's 206 | available output is passed to the function which must return a record. A new 207 | component is returned with its selected output being the union of the existing 208 | selected output and what the function returned. 209 | 210 | We are now equipped with the knowledge necessary to understand this part of the 211 | example: 212 | 213 | ```purescript 214 | E.button {} (E.text "+" ) `use` (\o -> { increment: o.click $> 1 }) 215 | E.button {} (E.text "-" ) `use` (\o -> { decrement: o.click $> -1 }) 216 | ``` 217 | 218 | The expression `E.button {} (E.text "+")` has the type `Compenent { click :: 219 | Stream ClickEvent, ... } {}`. In other words, a stream of click events is part 220 | of the available output from a button. For each of the buttons we apply `use` 221 | to select the click stream first with the property name `increment` and then 222 | with the property name `decrement`. Streams are functors and we use `$>` to 223 | turn the value of each event or occurrence into 1 and -1 respectively. Finally, 224 | we compose the two components with `` and the resulting type of the above 225 | becomes: 226 | 227 | ```purescript 228 | Component {} { increment :: Stream Int, decrement :: Stream Int } 229 | ``` 230 | 231 | This composed component is then given as the child component to the `div` function: 232 | 233 | ``` 234 | div :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 235 | ``` 236 | 237 | Notice how the `div` function propagates the selected output of its child into 238 | the selected output of the returned component. All the element functions behave 239 | in this way and, combined with how `` works, this ensures that selected 240 | output always "bubbles up" when HTML is constructed. The final component passed 241 | to `output` therefore has the selected output `{ increment :: Stream Int, 242 | decrement :: Stream Int }` and this gets passed as input to the function given 243 | to `component`. We have come full circle. 244 | 245 | ## Running the application 246 | 247 | The last line calls `runComponent` which has the type: 248 | 249 | ```purescript 250 | runComponent :: forall a o. String -> Component a o -> Effect Unit 251 | ``` 252 | 253 | The function takes a [CSS selector 254 | string](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) and a 255 | component. It then runs the component with the first element matching the 256 | selector as its parent. Similarly to how `Effect` _describes_ side-effects a 257 | `Component` only describes a piece of UI or a part of an application. A Turbine 258 | application always has an invocation of `runComponent` which then runs the 259 | entire application. 260 | -------------------------------------------------------------------------------- /resources/behaviorstream.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 52 | 54 | 57 | 64 | 71 | 78 | 85 | 88 | 90 | 96 | 98 | time 100 | 101 | 102 | time 108 | 109 | 110 | 113 | 115 | 121 | 123 | value 125 | 126 | 127 | value 133 | 134 | 135 | 138 | 140 | 146 | 148 | 150 | Behavior 152 | 153 | 154 | 155 | 156 | 157 | 164 | 171 | 178 | 185 | 192 | 195 | 197 | 203 | 205 | time 207 | 208 | 209 | time 215 | 216 | 217 | 220 | 222 | 228 | 230 | value 232 | 233 | 234 | value 240 | 241 | 242 | 245 | 247 | 253 | 255 | 257 | 259 | Stream 261 | 262 | 263 | 264 | 265 | 266 | 267 | 274 | 281 | 288 | 295 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /src/Turbine/HTML.purs: -------------------------------------------------------------------------------- 1 | -- | This module contains Turbines DSL for constructing HTML. 2 | -- | 3 | -- | This module is typically imported qualified as. 4 | -- | ```purescript 5 | -- | import Turbine.HTML as H 6 | -- | ``` 7 | module Turbine.HTML 8 | ( Attributes' 9 | , Output' 10 | , h1 11 | , div 12 | , br 13 | , p 14 | , a 15 | , text 16 | , textB 17 | , ul 18 | , li 19 | , span 20 | , InputAttrs' 21 | , InputAttrs 22 | , InputOut' 23 | , InputOut 24 | , input 25 | , checkbox 26 | , inputRange 27 | , textarea 28 | , button 29 | , label 30 | , section 31 | , header 32 | , footer 33 | , table 34 | , th 35 | , tr 36 | , td 37 | , progress 38 | , empty 39 | , class Subrow 40 | , class RecordOf 41 | , class RecordOfGo 42 | , toArray 43 | , toArrayGo 44 | , ClassDescription 45 | , ClassElement 46 | , staticClass 47 | , dynamicClass 48 | , toggleClass 49 | ) where 50 | 51 | import Prelude hiding (div) 52 | 53 | import Data.Function.Uncurried (Fn2, Fn1, runFn1, runFn2) 54 | import Data.Symbol (class IsSymbol, SProxy(..)) 55 | import Hareactive.Types (Behavior, Stream) 56 | import Prim.Row (class Union) 57 | import Prim.Row as Row 58 | import Prim.RowList as RL 59 | import Record as R 60 | import Turbine (Component) 61 | import Type.Data.RowList (RLProxy(..)) 62 | import Type.Row (type (+)) 63 | import Web.UIEvent.FocusEvent (FocusEvent) 64 | import Web.UIEvent.InputEvent (InputEvent) 65 | import Web.UIEvent.KeyboardEvent (KeyboardEvent) 66 | import Web.UIEvent.MouseEvent (MouseEvent) 67 | 68 | class Subrow (r :: # Type) (s :: # Type) 69 | 70 | instance subrow :: Union r t s => Subrow r s 71 | 72 | class RecordOfGo (xs :: RL.RowList) (row :: # Type) a | xs -> row a where 73 | toArrayGo :: RLProxy xs -> Record row -> Array a 74 | 75 | instance recordOfConsGo :: 76 | ( IsSymbol name -- Name should be a symbol 77 | , RL.RowToList row rx 78 | , Row.Cons name a trash row 79 | , RecordOfGo xs' row a -- Recursive invocation 80 | ) => RecordOfGo (RL.Cons name a xs') row a where 81 | toArrayGo _ rec = [val] <> (toArrayGo rest rec) 82 | where 83 | nameP = SProxy :: SProxy name 84 | rest = (RLProxy :: RLProxy xs') 85 | val = R.get nameP rec 86 | 87 | instance recordOfNilGo :: RecordOfGo RL.Nil row a where 88 | toArrayGo _ _ = [] 89 | 90 | class RecordOf a (row :: # Type) | row -> a where 91 | toArray :: forall xs. RL.RowToList row xs => RecordOfGo xs row a => { | row } -> Array a 92 | 93 | instance recordOf :: 94 | ( RL.RowToList row xs 95 | , RecordOfGo xs row a 96 | ) => RecordOf a row where 97 | toArray = toArrayGo (RLProxy :: RLProxy xs) 98 | 99 | foreign import data ClassElement :: Type 100 | 101 | newtype ClassDescription = ClassDescription (Array ClassElement) 102 | 103 | -- | Creates a static class from a string of space separated class names. 104 | -- | 105 | -- | ```purescript 106 | -- | staticClass "foo bar baz" 107 | -- | ``` 108 | foreign import staticClass :: String -> ClassDescription 109 | 110 | -- | Creates a dynamic class from a string valued behavior. At any point in time 111 | -- | the element will have the class named in the behavior at that point in 112 | -- | time. 113 | -- | 114 | -- | ```purescript 115 | -- | dynamicClass stringValuedBehavior 116 | -- | ``` 117 | foreign import dynamicClass :: Behavior String -> ClassDescription 118 | 119 | -- | Takes a class name and a boolean valued behavior. When the behavior is 120 | -- | `true` the element has the class and when the behavior is `false` the 121 | -- | element does not have the class. 122 | -- | 123 | -- | ```purescript 124 | -- | toggleClass "active" isActiveBehavior 125 | -- | <> toggleClass "selected" isSelectedBehavior 126 | -- | ``` 127 | toggleClass :: String -> Behavior Boolean -> ClassDescription 128 | toggleClass = runFn2 _toggleClass 129 | 130 | foreign import _toggleClass :: Fn2 String (Behavior Boolean) ClassDescription 131 | 132 | -- | Class descriptions form a semigroup. This makes it convenient to combine 133 | -- | several types of description and add them to the same element. 134 | -- | 135 | -- | ```purescript 136 | -- | E.div { classes: staticClass "foo" 137 | -- | <> dynamicClass beh 138 | -- | <> toggleClass "selected" isSelected 139 | -- | } 140 | -- | ``` 141 | derive newtype instance semigroupClassDescription :: Semigroup ClassDescription 142 | 143 | -- Elements 144 | 145 | -- | This type describes the attributes that all HTML elements accepts. 146 | type Attributes' r = 147 | ( class :: Behavior String 148 | , classes :: ClassDescription 149 | , id :: Behavior String 150 | | r 151 | ) 152 | 153 | type Attributes = Attributes' () 154 | 155 | -- | This type describes the output that all HTML elements output. 156 | type Output' r = 157 | ( click :: Stream MouseEvent 158 | , dblclick :: Stream MouseEvent 159 | , keydown :: Stream KeyboardEvent 160 | , keyup :: Stream KeyboardEvent 161 | , blur :: Stream FocusEvent 162 | | r 163 | ) 164 | 165 | type Output = Record (Output' ()) 166 | 167 | -- This type is not really accurate. But, it is much simpler than an accurate 168 | -- type and this "simplification" does not leak out into the API. 169 | foreign import processAttributes :: forall r. Record r -> Record r 170 | 171 | div :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 172 | div = runFn2 _div <<< processAttributes 173 | 174 | foreign import _div :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 175 | 176 | ul :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 177 | ul = runFn2 _ul <<< processAttributes 178 | 179 | foreign import _ul :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 180 | 181 | li :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 182 | li = runFn2 _li <<< processAttributes 183 | 184 | foreign import _li :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 185 | 186 | span :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 187 | span = runFn2 _span <<< processAttributes 188 | 189 | foreign import _span :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 190 | 191 | p :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 192 | p = runFn2 _p <<< processAttributes 193 | 194 | foreign import _p :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 195 | 196 | h1 :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 197 | h1 = runFn2 _h1 <<< processAttributes 198 | 199 | foreign import _h1 :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 200 | 201 | h2 :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 202 | h2 = runFn2 _h2 <<< processAttributes 203 | 204 | foreign import _h2 :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 205 | 206 | h3 :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 207 | h3 = runFn2 _h3 <<< processAttributes 208 | 209 | foreign import _h3 :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 210 | 211 | h4 :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 212 | h4 = runFn2 _h4 <<< processAttributes 213 | 214 | foreign import _h4 :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 215 | 216 | h5 :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 217 | h5 = runFn2 _h5 <<< processAttributes 218 | 219 | foreign import _h5 :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 220 | 221 | h6 :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 222 | h6 = runFn2 _h6 <<< processAttributes 223 | 224 | foreign import _h6 :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 225 | 226 | label :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 227 | label = runFn2 _label <<< processAttributes 228 | 229 | foreign import _label :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 230 | 231 | section :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 232 | section = runFn2 _section <<< processAttributes 233 | 234 | foreign import _section :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 235 | 236 | header :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 237 | header = runFn2 _header <<< processAttributes 238 | 239 | foreign import _header :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 240 | 241 | footer :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 242 | footer = runFn2 _footer <<< processAttributes 243 | 244 | foreign import _footer :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 245 | 246 | type ButtonOut = { click :: Stream Unit } 247 | 248 | button :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component ButtonOut o 249 | button = runFn2 _button <<< processAttributes 250 | 251 | foreign import _button :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component ButtonOut o) 252 | 253 | a :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 254 | a = runFn2 _a <<< processAttributes 255 | 256 | foreign import _a :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 257 | 258 | type InputAttrs' r = 259 | ( placeholder :: Behavior String 260 | , type :: Behavior String 261 | , value :: Behavior String 262 | , autofocus :: Behavior Boolean 263 | | Attributes' + r 264 | ) 265 | 266 | type InputAttrs = InputAttrs' () 267 | 268 | type InputOut' r = 269 | ( value :: Behavior String 270 | , input :: Stream InputEvent 271 | , keyup :: Stream KeyboardEvent 272 | | Output' + r 273 | ) 274 | 275 | type InputOut = Record (InputOut' ()) 276 | 277 | input :: forall r. Subrow r InputAttrs => Record r -> Component InputOut {} 278 | input = runFn1 _input <<< processAttributes 279 | 280 | foreign import _input :: forall r. Subrow r InputAttrs => Fn1 (Record r) (Component InputOut {}) 281 | 282 | type InputRangeAttrs' r = 283 | ( min :: Behavior Number 284 | , max :: Behavior Number 285 | | InputAttrs' + r 286 | ) 287 | 288 | type InputRangeOut' r = 289 | ( value :: Behavior Number 290 | | InputOut' + r 291 | ) 292 | 293 | type InputRangeOut = Record (InputRangeOut' ()) 294 | 295 | -- | An input element with the `type` attribute set to `range`. Compared to a 296 | -- | normal a normal input element this variant accepts three additional 297 | -- | attributes all of which are numbers: `max`, `min`, and `step`. Additionally 298 | -- | the `value` output is a `Number` and not a `String`. 299 | inputRange :: forall r. Subrow r (InputRangeAttrs' ()) => Record r -> Component { | (InputRangeOut' ()) } {} 300 | inputRange = runFn1 _inputRange <<< processAttributes 301 | 302 | foreign import _inputRange :: forall r. Subrow r (InputRangeAttrs' ()) => Fn1 (Record r) (Component ({ | InputRangeOut' ()}) {}) 303 | 304 | type CheckboxAttrs' r = 305 | ( checked :: Behavior Boolean 306 | | Attributes' + r 307 | ) 308 | 309 | type CheckboxAttrs = CheckboxAttrs' () 310 | 311 | type CheckboxOut' r = 312 | ( checked :: Behavior Boolean 313 | , checkedChange :: Stream Boolean 314 | | Output' + r 315 | ) 316 | 317 | type CheckboxOutput = Record (CheckboxOut' ()) 318 | 319 | -- | An input element with the `type` attribute set to `checkbox`. 320 | -- | 321 | -- | Most notably a `checkbox` outputs a behavior named `checked` denoting 322 | -- | whether or not the checkbox is currently checked. 323 | checkbox :: forall r. Subrow r CheckboxAttrs => Record r -> Component CheckboxOutput {} 324 | checkbox = runFn1 _checkbox <<< processAttributes 325 | 326 | foreign import _checkbox :: forall r. Subrow r CheckboxAttrs => Fn1 (Record r) (Component CheckboxOutput {}) 327 | 328 | type TextareaAttrs' r = 329 | ( rows :: Behavior Int 330 | , cols :: Behavior Int 331 | | InputAttrs' + r 332 | ) 333 | 334 | type TextareaAttrs = TextareaAttrs' () 335 | 336 | -- | A textarea element. Accepts `rows` and `cols` attributes. 337 | textarea :: forall r. Subrow r TextareaAttrs => Record r -> Component InputOut {} 338 | textarea = runFn1 _textarea <<< processAttributes 339 | 340 | foreign import _textarea :: forall r. Subrow r TextareaAttrs => Fn1 (Record r) (Component InputOut {}) 341 | 342 | type ProgressAttrs' r = 343 | ( value :: Behavior Number 344 | , max :: Behavior Number 345 | | Attributes' + r 346 | ) 347 | 348 | type ProgressAttrs = ProgressAttrs' () 349 | 350 | -- | A progress element. At accepts `value` and `max` both of which must be 351 | -- | `Number` valued. 352 | progress :: forall r a o. Subrow r ProgressAttrs => Record r -> Component a o -> Component Output o 353 | progress = runFn2 _progress <<< processAttributes 354 | 355 | foreign import _progress :: forall r a o. Subrow r ProgressAttrs => Fn2 (Record r) (Component a o) (Component Output o) 356 | 357 | -- | Creates a static text node based on the given string. 358 | -- | 359 | -- | For a dynamic version see [`textB`](#v:textB). 360 | text :: String -> Component Unit {} 361 | text = _text 362 | 363 | foreign import _text :: String -> Component Unit {} 364 | 365 | -- | Creates a dynamic text node based on the given string valued behavior. The 366 | -- | value of the text node is always equal to the value of the behavior. 367 | textB :: Behavior String -> Component Unit {} 368 | textB = _textB 369 | 370 | foreign import _textB :: Behavior String -> Component Unit {} 371 | 372 | table :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 373 | table = runFn2 _table <<< processAttributes 374 | 375 | foreign import _table :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 376 | 377 | th :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 378 | th = runFn2 _th <<< processAttributes 379 | 380 | foreign import _th :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 381 | 382 | tr :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 383 | tr = runFn2 _tr <<< processAttributes 384 | 385 | foreign import _tr :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 386 | 387 | td :: forall r a o. Subrow r Attributes => Record r -> Component a o -> Component Output o 388 | td = runFn2 _td <<< processAttributes 389 | 390 | foreign import _td :: forall r a o. Subrow r Attributes => Fn2 (Record r) (Component a o) (Component Output o) 391 | 392 | -- | A `br` element. Note that this is a constant and not a function since a 393 | -- | `br` elements takes neither attributes nor children. 394 | foreign import br :: Component Unit {} 395 | 396 | -- | An empty component corresponding to no HTML nor effects. 397 | foreign import empty :: Component Unit {} 398 | -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | This tutorial introduces and explains the core concepts in Turbine. Along the 4 | way we build a few simple applications to exemplify the material. 5 | 6 | Turbine is based on _functional reactive programming_ (FRP). It uses the FRP 7 | library [Hareactive](#https://github.com/funkia/purescript-hareactive). It is 8 | important to understand that Turbine is a relatively small layer on top of 9 | Hareactive that provides abstractions for building HTML in a way that is 10 | connected with FRP. Hareactive is a substantially larger library than Turbine. 11 | Both in terms of implementation but, more importantly for the tutorial, also in 12 | terms of the size of the API and of how much there is to learn. Hence, when 13 | learning Turbine the bulk of the learning is actually to learn FRP. This 14 | tutorial assumes no prior experience with FRP and hence it can also be seen as 15 | a tutorial to FRP in general and Hareactive in particular. 16 | 17 | If you want to you can follow along the tutorial yourself and potentially 18 | experiment with the code examples. You can do so by cloning the Turbine starter 19 | template with the following commands. 20 | 21 | ``` 22 | git clone https://github.com/funkia/purescript-turbine-starter turbine-tutorial 23 | cd turbine-tutorial 24 | npm i 25 | ``` 26 | 27 | After having executed the above you can run `npm run build` and afterwards you 28 | should see the text "Hello, world!" if you open the `index.html` file in a 29 | browser. Along the way you should make changes to the file `src/Main.purs`, 30 | rebuild the app with `npm run build`, and then observe the changes in a 31 | browser. 32 | 33 | ### Component 34 | 35 | The central type in Turbine is `Component`. As a first approximation a 36 | `Component` represents a piece of user interface. For instance, it could be 37 | an input field or a button. More concretely, a `Component` contains a 38 | description of how to create a piece of HTML. Components are composable. Hence 39 | an input field and a button can be composed together and the result is another 40 | component. A component also describes any state, logic, and side-effects 41 | associated with the component. As an example, two input fields and a button can 42 | be composed to describe the UI of a login form. The logic for the login form 43 | and the side-effects for performing the HTTP requests for the login can be 44 | "attached" to the view. 45 | 46 | A Turbine application is constructed by composing components. Components 47 | divide the app into separate chunks that can be implemented in isolation. A 48 | Turbine application is "components all the way down". 49 | 50 | The `Component` type has the following kind: 51 | 52 | ``` 53 | Component :: Type -> Type -> Type 54 | ``` 55 | 56 | That is, it is parameterized by two types. The purpose of these are explained 57 | later in the tutorial. 58 | 59 | ### Creating static HTML 60 | 61 | In this section we will explain how to create static HTML with Turbine. Turbine 62 | contains functions for creating components that correspond to single HTML 63 | elements. These live in the module `Turbine.HTML`, which is typically imported 64 | qualified like this: 65 | 66 | ```purescript 67 | import Turbine.HTML as H 68 | ``` 69 | 70 | For every HTML element the `Turbine.HTML` module exports a corresponding 71 | function. For the HTML element `div` there is a `div` funtion. For the `span` 72 | element there is a `span` function, and so on. The first argument to these 73 | functions is a record of attributes. For HTML elements that can contain 74 | children, the corresponding function takes a second argument, as well. This 75 | argument must be a component and describes the child of the element. Here are 76 | a few examples: 77 | 78 | ```purescript 79 | myInput = H.input { placeholder: "Write here", class: "form-input" } 80 | myButton = H.button {} (H.text "Click me") 81 | myDivWithButton = H.div { class: "div-class" } myButton 82 | ``` 83 | 84 | The `H.text` function used above takes a string and returns a component 85 | corresponding to a text node of the given string. 86 | 87 | Components are composed together with the `` operator. As a first 88 | approximation, `` is similar to the semigroup operator `<>`, which has the 89 | type `a -> a -> a`. The type of ``, however, is slightly more complex, 90 | since components keep track of more information at the type level than a 91 | typical semigroup. Writing `component1 component2` creates a new component 92 | which represents the HTML from the first component followed by the HTML for the 93 | second component. As an example the code: 94 | 95 | ```purescript 96 | myLoginForm = 97 | H.input { placeholder: "Username" } 98 | H.input { placeholder: "Password" } 99 | H.label {} (H.text "Remember login") 100 | H.checkbox {} 101 | ``` 102 | 103 | Corresponds to the following HTML: 104 | 105 | ```html 106 | 107 | input placeholder="Password" /> 108 | 109 | 110 | ``` 111 | 112 | If you add the code above to `Main.purs` and change the definition of `Main` 113 | into the following: 114 | 115 | ```diff 116 | -app = H.text "Hello, world!" 117 | +app = myLoginForm 118 | ``` 119 | 120 | Then you should see HTML corresponding to the HTML above. 121 | 122 | 123 | By combining `` with the fact that each element function accepts a child 124 | component as its second argument, we can create arbitrary HTML of any 125 | complexity. In this tutorial we will build a simple counter application 126 | (similar to the one [shown above](#single-counter)). To this end let us create 127 | the HTML which we will use going forward. 128 | 129 | ```purescript 130 | counterView = 131 | H.div {} ( 132 | H.text "Counter " 133 | H.span {} (H.text "0") 134 | H.button {} (H.text "+") 135 | H.button {} (H.text "-") 136 | ) 137 | ``` 138 | 139 | Here we have hard coded the value `0` into the user interface. The intended 140 | outcome is that the displayed number is dynamic. It should increment every time the 141 | `+` button is pressed and decrement every time the `-` button is pressed. But, 142 | in order to achieve that we need to learn a little bit of FRP. 143 | 144 | ### A short interlude on FRP 145 | 146 | At its essence functional reactive programming can be seen as providing 147 | abstractions for representing phenomena that _depend on time_ in a purely 148 | functional way. FRP contains two key data-types `Behavior` and `Stream`: 149 | 150 | * A `Behavior` represents a value that changes over time. 151 | * A `Stream` represents events or occurrences that take place at discrete moments 152 | in time. 153 | 154 | For instance, `Behavior Number` represents a changing number and `Behavior 155 | String` represents a changing string. On the other hand, a `Stream Number` 156 | represents numbers associated with discrete moments in time, and `Stream String` 157 | represents strings associated with discrete moments in time. 158 | 159 | > Note: What we call `Stream` is often called `Event` in other FRP libraries. 160 | 161 | The difference between behaviors and streams can be illustrated as below: 162 | 163 | ![illustration of behavior and stream](../resources/behaviorstream.svg) 164 | 165 | As the image indicates, a behavior can be seen as a function from time. That is, at 166 | any specific moment in time it has a value. A stream on the other hand only has 167 | values, or occurrences, at specific punctuations in time. 168 | 169 | Initially, the distinction between a behavior and a stream may be unclear. 170 | Fortunately, when one becomes familiar with the two abstractions, the choice of 171 | which one to use becomes unambiguous. A simple heuristic to determine whether a 172 | particular thing should be represented as a behavior or stream is to ask 173 | the question, "does this thing have a notion of a current value?". If "yes", it 174 | is a behavior. If "no", it is a stream. Turbine uses behaviors and streams 175 | to represent any dynamic UI value using FRP. Here are a few examples: 176 | 177 | * The value of an input field is represented as a `Behavior String`. Because 178 | the input field always has a "current value", its value is represented as a 179 | behavior. 180 | * The clicking of a button is represented as a `Stream ClickEvent`. A click of 181 | the button is an event that happens at discrete moment in time, hence a stream 182 | is used. 183 | * Whether or not a checkbox is checked is represented as a `Behavior Boolean`. 184 | 185 | ### Dynamic HTML 186 | 187 | In the counter component above, we hard coded the value `0` into the view. The 188 | goal is to have the displayed number _change over time_. And, as mentioned, 189 | in FRP we use behaviors to represent values that changes over time. Thus, we 190 | parameterize the HTML above such that it takes as argument a record of a 191 | behavior of the type `Behavior Number`: 192 | 193 | ```purescript 194 | counterView { count :: Behavior String } -> Component _ _ 195 | counterView { count } = 196 | H.div {} ( 197 | H.text "Counter " 198 | H.span {} (H.textB (map show count)) 199 | H.button {} (H.text "+") 200 | H.button {} (H.text "-") 201 | ) 202 | ``` 203 | 204 | We also changed `H.text "0"` into `H.textB (map show count)`. The `textB` 205 | function is similar to `text` except that, instead of taking an argument of type 206 | `String`, it takes an argument of type `Behavior String.` It then returns a 207 | component that describes _dynamic HTML_. The value of the text node will be 208 | kept up to date with the value of the behavior. 209 | 210 | We have now modified the view such that it takes as _input_ a dynamic count 211 | which it displays in the UI. Next we must declare the view's _output_ such that 212 | the clicks of the two buttons. 213 | 214 | ### Output 215 | 216 | Recall that the `Component` type is parameterized by two types. Both of these 217 | are, by convention, almost always records. The first of them is called the 218 | component's _selected output_ and the second is called the component's _available 219 | output_. If you are familiar with `addEventListener` in the DOM API then, as an 220 | analogy, the available output can be thought of the events that we _could_ 221 | listen to by calling `addEventListener` with the event name. The selected 222 | output, on the other hand, is the output that we have explicitly declared that 223 | we are interested in. 224 | 225 | When a component is initially created its selected output is usually `{}`. This 226 | matches the intuition that a newly constructed component has not had any of its 227 | available output selected yet. The available output on the other hand will be a 228 | record of all the various streams, behaviors, and other things that the 229 | component produces. 230 | 231 | As an example, consider this slightly simplified type of the `button` function: 232 | 233 | ```purescript 234 | button :: { | a } -> Component {} { click :: Stream ClickEvent 235 | dbclick :: Stream ClickEvent 236 | -- ... and so on 237 | } 238 | ``` 239 | 240 | This type tells us that when given a record of attributes the `button` function 241 | returns a component with available output as declared by the last object. It 242 | includes, among other things, a field of type `click :: Stream ClickEvent`. 243 | This stream has an occurrence whenever the button is pressed. 244 | 245 | As another example, consider the type of the `input` function: 246 | 247 | ```purescript 248 | input :: { | a } -> Component {} { value :: Behavior String 249 | , blur :: Stream FocusEvent 250 | , keydown :: Stream KeyboardEvent 251 | , keyup :: Stream KeyboardEvent 252 | -- ... and so on 253 | } 254 | ``` 255 | 256 | From this we see that a component constructed by the `input` function has among 257 | its available output a field of type `value :: Behavior String`. This behavior 258 | describes the current value of the input field. 259 | 260 | Available output can be selected by using the 261 | [`output`](https://pursuit.purescript.org/packages/purescript-turbine/0.0.4/docs/Turbine#v:output) 262 | function. Its type is as follows: 263 | 264 | ```purescript 265 | output :: forall a o p q. Union o p q => Component { | o } a -> (a -> { | p }) -> Component { | q } a 266 | ``` 267 | 268 | Let us unpack the type piece by piece. The `output` function takes as 269 | arguments a component and a function. The type variable `a` is the component's 270 | available output. The function takes the available output, the `a`, and returns 271 | a record of `p`. The given component's selected output is the type variable `o`. 272 | Per the constraint `Union o p q` the type variable `q` becomes the union of `o` 273 | and `p`. The returned component has the type `Component { | q } a`. In other 274 | words, the given function receives the component's available output, returns a 275 | record, and this record is then merged into the returned component's selected 276 | output. The end result is that `output` moves output from the available part 277 | into the selected part. 278 | 279 | The `output` function is often used infix as in the following example. 280 | 281 | ```purescript 282 | usernameField = H.input {} `output` (\o -> { username: o.value }) 283 | ``` 284 | 285 | In the above code we are selecting the `value` behavior that the component 286 | created by `input` outputs. By returning a record with a field named `username` 287 | we are in a sense moving the behavior from the available output into the 288 | selected output and renaming it at the same time. As defined above 289 | `usernameField` has the type `Component { username :: Behavior String } { ... 290 | }`. 291 | 292 | As the piece in the puzzle to understand how output works we must now 293 | consider the type of the `` operator which is an alias for the 294 | [merge](https://pursuit.purescript.org/packages/purescript-turbine/0.0.4/docs/Turbine#v:merge) 295 | function. 296 | 297 | ```purescript 298 | merge :: forall a o b p q. Union o p q => Component { | o } a -> Component { | p } b -> Component { | q } { | q } 299 | ``` 300 | 301 | Due to the `Union o p q` constraint `merge` takes two components and returns a 302 | new component that is their combination. This combination has as its selected 303 | output the union of the two components' selected output. 304 | 305 | Let us return to the example with the login form from earlier. Consider how 306 | we might get output from the view and how the types interact: 307 | 308 | ```purescript 309 | myLoginForm = 310 | H.input { placeholder: "Username" } `output` (\o -> { username: o.value }) 311 | H.input { placeholder: "Password" } `output` (\o -> { password: o.value }) 312 | H.label {} (H.text "Remember login") 313 | H.checkbox {} `output` (\o -> { rememberLogin: o.checked }) 314 | ``` 315 | 316 | Each invocation of `output` selects some output and each invocation of `` 317 | merges these in the combined components. The end result is that `myLoginForm` 318 | has the type: 319 | 320 | ```purescript 321 | myLoginForm :: Component { username :: Behavior String 322 | , password :: Behavior String 323 | , rememberLogin :: Stream Boolean 324 | } 325 | { ... } 326 | ``` 327 | 328 | ### Model view 329 | 330 | The 331 | [`modelView`](https://pursuit.purescript.org/packages/purescript-turbine/0.0.4/docs/Turbine#v:modelView) 332 | function is a key part of Turbine. It is the primary way to create custom 333 | components with custom logic. It takes a _model_ and a _view_. The model is a 334 | function that returns a computation in the 335 | [Now](https://pursuit.purescript.org/packages/purescript-hareactive/0.0.9/docs/Hareactive.Types#t:Now) 336 | monad. The view is a function that returns a component: 337 | 338 | ```purescript 339 | modelView :: forall o p a x. (o -> x -> Now p) -> (p -> x -> Component o a) -> (x -> Component { } p) 340 | ``` 341 | --------------------------------------------------------------------------------