From 96f6ad31795c4482d064e79b2b2b754ad506f036 Mon Sep 17 00:00:00 2001 From: Madeline Trotter Date: Thu, 2 Apr 2020 15:12:30 -0700 Subject: [PATCH 1/4] Simplify useAff --- .gitignore | 1 + Makefile | 30 ++-- examples/README.md | 11 ++ examples/aff/Makefile | 21 ++- examples/aff/README.md | 11 -- examples/aff/package.json | 9 -- examples/aff/src/AffEx.purs | 109 -------------- examples/aff/src/Example.purs | 122 ++++++++++++++++ examples/aff/src/Main.purs | 12 +- examples/component/Makefile | 21 ++- examples/component/README.md | 11 -- examples/component/package.json | 9 -- .../src/{Container.purs => Example.purs} | 14 +- examples/component/src/Main.purs | 12 +- examples/context/Makefile | 21 ++- examples/context/README.md | 11 -- examples/context/package.json | 9 -- .../src/{Context.purs => Example.purs} | 14 +- examples/context/src/Main.purs | 12 +- examples/controlled-input/Makefile | 21 ++- examples/controlled-input/README.md | 11 -- examples/controlled-input/package.json | 9 -- .../{ControlledInput.purs => Example.purs} | 18 +-- examples/controlled-input/src/Main.purs | 12 +- examples/counter/Makefile | 21 ++- examples/counter/README.md | 11 -- examples/counter/package.json | 9 -- .../counter/src/{Counter.js => Example.js} | 0 .../src/{Counter.purs => Example.purs} | 6 +- examples/counter/src/Main.purs | 12 +- examples/memo-callback/Makefile | 21 ++- examples/memo-callback/README.md | 11 -- examples/memo-callback/package.json | 9 -- .../src/{MemoCallback.purs => Example.purs} | 6 +- examples/memo-callback/src/Main.purs | 12 +- examples/reducer/Makefile | 21 ++- examples/reducer/README.md | 11 -- examples/reducer/package.json | 9 -- examples/reducer/src/Main.purs | 12 +- examples/reducer/src/Reducer.purs | 6 +- examples/refs/Makefile | 21 ++- examples/refs/README.md | 11 -- examples/refs/package.json | 9 -- examples/refs/src/{Refs.purs => Example.purs} | 6 +- examples/refs/src/Main.purs | 12 +- examples/todo-app/Makefile | 21 ++- examples/todo-app/README.md | 11 -- examples/todo-app/package.json | 10 -- .../src/{TodoApp.purs => Example.purs} | 134 +++++++++--------- examples/todo-app/src/Main.purs | 12 +- packages.dhall | 128 +++++++++++++++++ serve.json | 8 ++ spago.dhall | 18 +++ src/React/Basic/Hooks/Aff.purs | 25 ++-- test/Main.purs | 11 ++ 55 files changed, 635 insertions(+), 510 deletions(-) create mode 100644 examples/README.md delete mode 100644 examples/aff/README.md delete mode 100644 examples/aff/package.json delete mode 100644 examples/aff/src/AffEx.purs create mode 100644 examples/aff/src/Example.purs delete mode 100644 examples/component/README.md delete mode 100644 examples/component/package.json rename examples/component/src/{Container.purs => Example.purs} (50%) delete mode 100644 examples/context/README.md delete mode 100644 examples/context/package.json rename examples/context/src/{Context.purs => Example.purs} (85%) delete mode 100644 examples/controlled-input/README.md delete mode 100644 examples/controlled-input/package.json rename examples/controlled-input/src/{ControlledInput.purs => Example.purs} (78%) delete mode 100644 examples/counter/README.md delete mode 100644 examples/counter/package.json rename examples/counter/src/{Counter.js => Example.js} (100%) rename examples/counter/src/{Counter.purs => Example.purs} (89%) delete mode 100644 examples/memo-callback/README.md delete mode 100644 examples/memo-callback/package.json rename examples/memo-callback/src/{MemoCallback.purs => Example.purs} (89%) delete mode 100644 examples/reducer/README.md delete mode 100644 examples/reducer/package.json delete mode 100644 examples/refs/README.md delete mode 100644 examples/refs/package.json rename examples/refs/src/{Refs.purs => Example.purs} (97%) delete mode 100644 examples/todo-app/README.md delete mode 100644 examples/todo-app/package.json rename examples/todo-app/src/{TodoApp.purs => Example.purs} (50%) create mode 100644 packages.dhall create mode 100644 serve.json create mode 100644 spago.dhall create mode 100644 test/Main.purs diff --git a/.gitignore b/.gitignore index 0e0ea73..4c3a6d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /bower_components/ /node_modules/ +/.spago/ /.pulp-cache/ /output/ /generated-docs/ diff --git a/Makefile b/Makefile index 9e75347..931a939 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,27 @@ -all: build examples +TOPTARGETS := all clean -build: bower_components node_modules - npx pulp build +EXAMPLES := $(wildcard examples/*/.) -examples: bower_components node_modules - find examples -maxdepth 2 -type f -iname makefile -execdir make \; +all: build $(EXAMPLES) -bower_components: node_modules - npx bower --allow-root install +build: output + +output: .spago node_modules + npx spago build + +.spago: node_modules + npx spago install node_modules: - npm i --no-save bower pulp purescript + npm i --no-save spago purescript bower pulp + +clean: $(EXAMPLES) + rm -rf node_modules bower_components .spago output + +$(EXAMPLES): + cd $@ && $(MAKE) $(MAKECMDGOALS) + +serve: + npx serve -.PHONY: build examples +.PHONY: build clean $(TOPTARGETS) $(EXAMPLES) serve \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..30b54a6 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +# Examples + +## Building an example + +In the example folder: + +```sh +make +``` + +Then open `html/index.html` in your browser. diff --git a/examples/aff/Makefile b/examples/aff/Makefile index ecacfbe..86ea307 100644 --- a/examples/aff/Makefile +++ b/examples/aff/Makefile @@ -1,8 +1,19 @@ -all: node_modules - purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs' - purs bundle -m Main --main Main output/*/*.js > output/bundle.js - node_modules/.bin/browserify output/bundle.js -o html/index.js +parent_sources := $$(npx spago -x ../../spago.dhall sources | sed 's/^/..\/..\//' | tr '\n' ' ') + +all: output node_modules + npx purs bundle -m Main --main Main output/*/*.js > output/bundle.js + npx browserify output/bundle.js -o html/index.js + +output: node_modules + set -o noglob && \ + npx purs compile \ + src/**/*.purs \ + $(parent_sources) node_modules: - npm install + npm i --no-save browserify react react-dom + +clean: + rm -rf node_modules output +.PHONY: all output clean \ No newline at end of file diff --git a/examples/aff/README.md b/examples/aff/README.md deleted file mode 100644 index a5e283f..0000000 --- a/examples/aff/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Aff Example - -## Building - -```sh -make -``` - -This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. - -Then open `html/index.html` in your browser. diff --git a/examples/aff/package.json b/examples/aff/package.json deleted file mode 100644 index d7137af..0000000 --- a/examples/aff/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "react": "16.8.6", - "react-dom": "16.8.6" - }, - "devDependencies": { - "browserify": "16.2.3" - } -} diff --git a/examples/aff/src/AffEx.purs b/examples/aff/src/AffEx.purs deleted file mode 100644 index fc95b68..0000000 --- a/examples/aff/src/AffEx.purs +++ /dev/null @@ -1,109 +0,0 @@ -module AffEx where - -import Prelude -import Data.Either (either) -import Data.Maybe (Maybe(..), maybe) -import Effect (Effect) -import Effect.Aff (Aff, Milliseconds(..), delay, error, message, throwError) -import React.Basic.DOM as R -import React.Basic.Events (handler_) -import React.Basic.Hooks (type (/\), ReactComponent, Hook, JSX, component, element, fragment, useState, (/\)) -import React.Basic.Hooks as React -import React.Basic.Hooks.Aff (useAff) - -mkAffEx :: Effect (ReactComponent {}) -mkAffEx = do - -- A component for fetching and rendering a Cat entity. - catDetails <- mkCatDetails - component "AffEx" \props -> React.do - catKey /\ catChooser <- useCatKeyChooser - pure - $ R.div - { style: R.css { display: "flex", flexFlow: "column" } - , children: - [ R.h2_ [ R.text "Cat chooser" ] - , R.p_ - [ R.text - $ "Select a key to fetch! If you get bored (how would you even!?) " - <> "try holding your arrow keys to select really fast! The result " - <> "always matches the chosen key." - ] - , catChooser - , R.p_ - [ case catKey of - Nothing -> mempty - Just k -> element catDetails { catKey: k } - ] - ] - } - where - -- This hook packages up some interactive UI and the current - -- selection the user has made via that UI. - useCatKeyChooser :: Hook _ ((Maybe (Key Cat)) /\ JSX) - useCatKeyChooser = React.do - catKey /\ setCatKey <- useState Nothing - let - catChoice key = - R.label_ - [ R.input - { type: "radio" - , name: "cat-key" - , checked: Just key == catKey - , onChange: - handler_ do - setCatKey \_ -> Just key - } - , R.text $ showCatKey key - ] - - showCatKey :: Key Cat -> String - showCatKey (Key key) = "Cat " <> key - pure $ catKey - /\ fragment - [ catChoice $ Key "abc" - , catChoice $ Key "def" - , catChoice $ Key "ghi" - , catChoice $ Key "xyz" - ] - - -- Hooks can't be used conditionally but components can! - -- Not needing to deal with a `Maybe` key simplifies this - -- compoennt a bit. - mkCatDetails :: Effect (ReactComponent { catKey :: Key Cat }) - mkCatDetails = do - component "CatDetails" \{ catKey } -> React.do - cat <- useAff catKey $ fetch catKey - pure $ R.text - $ maybe "Loading..." (either message showCat) cat - where - showCat (Cat { name }) = "A cat named " <> name - --- Typed keys are a great way to tie entity-specific behavior --- to an ID. We can use this phantom type to write a class --- for generic, type-safe data fetching. -newtype Key entity - = Key String - -derive instance eqKey :: Eq (Key entity) - -class Fetch entity where - fetch :: Key entity -> Aff entity - --- An example entity -newtype Cat - = Cat { name :: String } - -instance fetchCat :: Fetch Cat where - fetch = case _ of - Key "abc" -> do - delay $ Milliseconds 300.0 - pure $ Cat { name: "Herb" } - Key "def" -> do - delay $ Milliseconds 600.0 - pure $ Cat { name: "Maxi" } - Key "ghi" -> do - delay $ Milliseconds 900.0 - pure $ Cat { name: "Chloe" } - _ -> do - delay $ Milliseconds 900.0 - throwError $ error "Cat not found (intended example behavior 😅)" diff --git a/examples/aff/src/Example.purs b/examples/aff/src/Example.purs new file mode 100644 index 0000000..6cad111 --- /dev/null +++ b/examples/aff/src/Example.purs @@ -0,0 +1,122 @@ +module Example where + +import Prelude +import Data.Foldable (find) +import Data.Maybe (Maybe(..)) +import Data.Newtype (class Newtype, un) +import Effect (Effect) +import Effect.Aff (Aff, Milliseconds(..), delay, error, throwError) +import React.Basic.DOM as R +import React.Basic.Events (handler_) +import React.Basic.Hooks (type (/\), ReactComponent, Hook, JSX, component, element, fragment, useState, (/\)) +import React.Basic.Hooks as React +import React.Basic.Hooks.Aff (useAff) + +mkExample :: Effect (ReactComponent {}) +mkExample = do + -- A component for fetching and rendering a Cat entity. + catDetails <- mkCatDetails + component "AffEx" \props -> React.do + catKey /\ catChooser <- useCatKeyChooser + pure + $ R.div + { style: R.css { display: "flex", flexFlow: "column" } + , children: + [ R.h2_ [ R.text "Cat chooser" ] + , R.p_ + [ R.text + $ "Select a key to fetch! If you get bored (how would you even!?) " + <> "try holding your arrow keys to select really fast! The result " + <> "always matches the chosen key." + ] + , catChooser + , R.p_ + [ case catKey of + Nothing -> mempty + Just k -> element catDetails { catKey: k } + ] + ] + } + where + -- This hook packages up some interactive UI and the current + -- selection the user has made via that UI. + useCatKeyChooser :: Hook _ ((Maybe (Key Cat)) /\ JSX) + useCatKeyChooser = React.do + catKey /\ setCatKey <- useState Nothing + let + catChoice k = + R.label_ + [ R.input + { type: "radio" + , name: "cat-key" + , checked: Just k == catKey + , onChange: + handler_ do + setCatKey \_ -> Just k + } + , R.text $ " " <> showCatKey k + ] + + showCatKey :: Key Cat -> String + showCatKey (Key k) = "Cat " <> k + pure $ catKey /\ fragment (map (catChoice <<< key) fakeDb) + + -- Hooks can't be used conditionally but components can! + -- Not needing to deal with a `Maybe` key simplifies this + -- compoennt a bit. + mkCatDetails :: Effect (ReactComponent { catKey :: Key Cat }) + mkCatDetails = do + component "CatDetails" \{ catKey } -> React.do + catState <- useAff catKey $ fetch catKey + pure case map entity catState of + Nothing -> R.text "Loading..." + Just (Cat { name }) -> R.text $ "A cat named " <> name + +-- Typed keys are a great way to tie entity-specific behavior +-- to an ID. We can use this phantom type to write a class +-- for generic, type-safe data fetching. +newtype Key entity + = Key String + +derive instance eqKey :: Eq (Key entity) + +derive instance ntKey :: Newtype (Key entity) _ + +-- An entity wrapper. In a real app this would hold other metadata +-- such as create and update dates. +data Entity entity + = Entity (Key entity) entity + +key :: forall entity. Entity entity -> Key entity +key (Entity k _) = k + +entity :: forall entity. Entity entity -> entity +entity (Entity _ e) = e + +class Fetch entity where + fetch :: Key entity -> Aff (Entity entity) + +-- An example entity +newtype Cat + = Cat { name :: String } + +fakeDb :: Array (Entity Cat) +fakeDb = + [ Entity (Key "abc") (Cat { name: "Herb" }) + , Entity (Key "def") (Cat { name: "Maxi" }) + , Entity (Key "ghi") (Cat { name: "Chloe" }) + ] + +instance fetchCat :: Fetch Cat where + fetch k = do + delay $ Milliseconds 300.0 + -- pretend this happens on the server + case fakeDb # find (key >>> (_ == k)) of + Nothing -> + -- This should never happen in a normal application path + -- if only the server can generate keys :) + throwError + $ error + $ "DB error: Cat not found for key " + <> un Key k + Just e -> pure e diff --git a/examples/aff/src/Main.purs b/examples/aff/src/Main.purs index c426e10..f9c6e57 100644 --- a/examples/aff/src/Main.purs +++ b/examples/aff/src/Main.purs @@ -1,12 +1,11 @@ module Main where import Prelude - -import AffEx (mkAffEx) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) -import React.Basic.Hooks(element) +import Example (mkExample) +import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -18,7 +17,8 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> do - affEx <- mkAffEx - let app = element affEx {} + Just c -> do + ex <- mkExample + let + app = element ex {} render app c diff --git a/examples/component/Makefile b/examples/component/Makefile index ecacfbe..86ea307 100644 --- a/examples/component/Makefile +++ b/examples/component/Makefile @@ -1,8 +1,19 @@ -all: node_modules - purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs' - purs bundle -m Main --main Main output/*/*.js > output/bundle.js - node_modules/.bin/browserify output/bundle.js -o html/index.js +parent_sources := $$(npx spago -x ../../spago.dhall sources | sed 's/^/..\/..\//' | tr '\n' ' ') + +all: output node_modules + npx purs bundle -m Main --main Main output/*/*.js > output/bundle.js + npx browserify output/bundle.js -o html/index.js + +output: node_modules + set -o noglob && \ + npx purs compile \ + src/**/*.purs \ + $(parent_sources) node_modules: - npm install + npm i --no-save browserify react react-dom + +clean: + rm -rf node_modules output +.PHONY: all output clean \ No newline at end of file diff --git a/examples/component/README.md b/examples/component/README.md deleted file mode 100644 index aef84a0..0000000 --- a/examples/component/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Component Example - -## Building - -```sh -make -``` - -This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. - -Then open `html/index.html` in your browser. diff --git a/examples/component/package.json b/examples/component/package.json deleted file mode 100644 index 6f42fc0..0000000 --- a/examples/component/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "react": "16.8.6", - "react-dom": "16.8.6" - }, - "devDependencies": { - "browserify": "^16.2.3" - } -} diff --git a/examples/component/src/Container.purs b/examples/component/src/Example.purs similarity index 50% rename from examples/component/src/Container.purs rename to examples/component/src/Example.purs index 4a82c5d..53fc09a 100644 --- a/examples/component/src/Container.purs +++ b/examples/component/src/Example.purs @@ -1,4 +1,4 @@ -module Container where +module Example where import Prelude import Effect (Effect) @@ -6,14 +6,16 @@ import React.Basic.DOM as R import React.Basic.Hooks (ReactComponent, component, element) import ToggleButton (mkToggleButton) -mkToggleButtonContainer :: Effect (ReactComponent {}) -mkToggleButtonContainer = do +mkExample :: Effect (ReactComponent {}) +mkExample = do + -- create the child components this parent will use toggleButton <- mkToggleButton + -- create the parent component component "Container" \_ -> pure $ R.div { children: - [ element toggleButton { label: "A" } - , element toggleButton { label: "B" } - ] + [ element toggleButton { label: "A" } + , element toggleButton { label: "B" } + ] } diff --git a/examples/component/src/Main.purs b/examples/component/src/Main.purs index adad18a..f9c6e57 100644 --- a/examples/component/src/Main.purs +++ b/examples/component/src/Main.purs @@ -1,12 +1,11 @@ module Main where import Prelude - -import Container (mkToggleButtonContainer) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) -import React.Basic.Hooks(element) +import Example (mkExample) +import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -18,7 +17,8 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> do - toggleButtonContainer <- mkToggleButtonContainer - let app = element toggleButtonContainer {} + Just c -> do + ex <- mkExample + let + app = element ex {} render app c diff --git a/examples/context/Makefile b/examples/context/Makefile index ecacfbe..86ea307 100644 --- a/examples/context/Makefile +++ b/examples/context/Makefile @@ -1,8 +1,19 @@ -all: node_modules - purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs' - purs bundle -m Main --main Main output/*/*.js > output/bundle.js - node_modules/.bin/browserify output/bundle.js -o html/index.js +parent_sources := $$(npx spago -x ../../spago.dhall sources | sed 's/^/..\/..\//' | tr '\n' ' ') + +all: output node_modules + npx purs bundle -m Main --main Main output/*/*.js > output/bundle.js + npx browserify output/bundle.js -o html/index.js + +output: node_modules + set -o noglob && \ + npx purs compile \ + src/**/*.purs \ + $(parent_sources) node_modules: - npm install + npm i --no-save browserify react react-dom + +clean: + rm -rf node_modules output +.PHONY: all output clean \ No newline at end of file diff --git a/examples/context/README.md b/examples/context/README.md deleted file mode 100644 index ca5e77a..0000000 --- a/examples/context/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Context Example - -## Building - -```sh -make -``` - -This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. - -Then open `html/index.html` in your browser. diff --git a/examples/context/package.json b/examples/context/package.json deleted file mode 100644 index d7137af..0000000 --- a/examples/context/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "react": "16.8.6", - "react-dom": "16.8.6" - }, - "devDependencies": { - "browserify": "16.2.3" - } -} diff --git a/examples/context/src/Context.purs b/examples/context/src/Example.purs similarity index 85% rename from examples/context/src/Context.purs rename to examples/context/src/Example.purs index 68203b0..72a6aeb 100644 --- a/examples/context/src/Context.purs +++ b/examples/context/src/Example.purs @@ -1,4 +1,4 @@ -module Context where +module Example where import Prelude import Effect (Effect) @@ -7,8 +7,8 @@ import React.Basic.Events (handler_) import React.Basic.Hooks (type (/\), ReactComponent, JSX, ReactContext, component, createContext, element, provider, useContext, useState, (/\)) import React.Basic.Hooks as React -mkContext :: Effect (ReactComponent {}) -mkContext = do +mkExample :: Effect (ReactComponent {}) +mkExample = do counterContext <- createContext (0 /\ pure unit) store <- mkStore counterContext counter <- mkCounter counterContext @@ -16,10 +16,10 @@ mkContext = do pure $ element store { content: - [ element counter {} - , element counter {} - , element counter {} - ] + [ element counter {} + , element counter {} + , element counter {} + ] } mkStore :: diff --git a/examples/context/src/Main.purs b/examples/context/src/Main.purs index 25a62f7..f9c6e57 100644 --- a/examples/context/src/Main.purs +++ b/examples/context/src/Main.purs @@ -1,12 +1,11 @@ module Main where import Prelude - -import Context (mkContext) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) -import React.Basic.Hooks(element) +import Example (mkExample) +import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -18,7 +17,8 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> do - context <- mkContext - let app = element context {} + Just c -> do + ex <- mkExample + let + app = element ex {} render app c diff --git a/examples/controlled-input/Makefile b/examples/controlled-input/Makefile index ecacfbe..760f56d 100644 --- a/examples/controlled-input/Makefile +++ b/examples/controlled-input/Makefile @@ -1,8 +1,19 @@ -all: node_modules - purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs' - purs bundle -m Main --main Main output/*/*.js > output/bundle.js - node_modules/.bin/browserify output/bundle.js -o html/index.js +parent_sources := $$(npx spago -x ../../spago.dhall sources | sed 's/^/..\/..\//' | tr '\n' ' ') + +all: output node_modules + npx purs bundle -m Main --main Main output/*/*.js > output/bundle.js + npx browserify output/bundle.js -o html/index.js + +output: node_modules + set -o noglob && \ + npx purs compile \ + src/**/*.purs \ + $(parent_sources) node_modules: - npm install + npm i --no-save purescript browserify react react-dom + +clean: + rm -rf node_modules output +.PHONY: all output clean \ No newline at end of file diff --git a/examples/controlled-input/README.md b/examples/controlled-input/README.md deleted file mode 100644 index a36ddf3..0000000 --- a/examples/controlled-input/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Controlled Input Example - -## Building - -```sh -make -``` - -This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. - -Then open `html/index.html` in your browser. diff --git a/examples/controlled-input/package.json b/examples/controlled-input/package.json deleted file mode 100644 index 6f42fc0..0000000 --- a/examples/controlled-input/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "react": "16.8.6", - "react-dom": "16.8.6" - }, - "devDependencies": { - "browserify": "^16.2.3" - } -} diff --git a/examples/controlled-input/src/ControlledInput.purs b/examples/controlled-input/src/Example.purs similarity index 78% rename from examples/controlled-input/src/ControlledInput.purs rename to examples/controlled-input/src/Example.purs index 53ddec7..d77c47c 100644 --- a/examples/controlled-input/src/ControlledInput.purs +++ b/examples/controlled-input/src/Example.purs @@ -1,4 +1,4 @@ -module ControlledInput where +module Example where import Prelude import Data.Maybe (Maybe(..), fromMaybe, maybe) @@ -9,8 +9,8 @@ import React.Basic.Events (EventHandler, handler, merge) import React.Basic.Hooks (ReactComponent, UseState, Hook, component, fragment, useState, (/\)) import React.Basic.Hooks as React -mkControlledInput :: Effect (ReactComponent {}) -mkControlledInput = do +mkExample :: Effect (ReactComponent {}) +mkExample = do component "ControlledInput" \props -> React.do firstName <- useInput "hello" lastName <- useInput "world" @@ -39,12 +39,12 @@ useInput initialValue = React.do { value, lastChanged } /\ replaceState <- useState { value: initialValue, lastChanged: Nothing } pure { onChange: - handler - (preventDefault >>> stopPropagation >>> merge { targetValue, timeStamp }) \{ timeStamp, targetValue } -> do - replaceState \_ -> - { value: fromMaybe "" targetValue - , lastChanged: Just timeStamp - } + handler + (preventDefault >>> stopPropagation >>> merge { targetValue, timeStamp }) \{ timeStamp, targetValue } -> do + replaceState \_ -> + { value: fromMaybe "" targetValue + , lastChanged: Just timeStamp + } , value , lastChanged } diff --git a/examples/controlled-input/src/Main.purs b/examples/controlled-input/src/Main.purs index f15627e..f9c6e57 100644 --- a/examples/controlled-input/src/Main.purs +++ b/examples/controlled-input/src/Main.purs @@ -1,12 +1,11 @@ module Main where import Prelude - -import ControlledInput (mkControlledInput) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) -import React.Basic.Hooks(element) +import Example (mkExample) +import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -18,7 +17,8 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> do - controlledInput <- mkControlledInput - let app = element controlledInput {} + Just c -> do + ex <- mkExample + let + app = element ex {} render app c diff --git a/examples/counter/Makefile b/examples/counter/Makefile index ecacfbe..86ea307 100644 --- a/examples/counter/Makefile +++ b/examples/counter/Makefile @@ -1,8 +1,19 @@ -all: node_modules - purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs' - purs bundle -m Main --main Main output/*/*.js > output/bundle.js - node_modules/.bin/browserify output/bundle.js -o html/index.js +parent_sources := $$(npx spago -x ../../spago.dhall sources | sed 's/^/..\/..\//' | tr '\n' ' ') + +all: output node_modules + npx purs bundle -m Main --main Main output/*/*.js > output/bundle.js + npx browserify output/bundle.js -o html/index.js + +output: node_modules + set -o noglob && \ + npx purs compile \ + src/**/*.purs \ + $(parent_sources) node_modules: - npm install + npm i --no-save browserify react react-dom + +clean: + rm -rf node_modules output +.PHONY: all output clean \ No newline at end of file diff --git a/examples/counter/README.md b/examples/counter/README.md deleted file mode 100644 index 88847e5..0000000 --- a/examples/counter/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Counter Example - -## Building - -```sh -make -``` - -This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. - -Then open `html/index.html` in your browser. diff --git a/examples/counter/package.json b/examples/counter/package.json deleted file mode 100644 index d7137af..0000000 --- a/examples/counter/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "react": "16.8.6", - "react-dom": "16.8.6" - }, - "devDependencies": { - "browserify": "16.2.3" - } -} diff --git a/examples/counter/src/Counter.js b/examples/counter/src/Example.js similarity index 100% rename from examples/counter/src/Counter.js rename to examples/counter/src/Example.js diff --git a/examples/counter/src/Counter.purs b/examples/counter/src/Example.purs similarity index 89% rename from examples/counter/src/Counter.purs rename to examples/counter/src/Example.purs index bbed475..f92f4c6 100644 --- a/examples/counter/src/Counter.purs +++ b/examples/counter/src/Example.purs @@ -1,4 +1,4 @@ -module Counter where +module Example where import Prelude import Effect (Effect) @@ -7,8 +7,8 @@ import React.Basic.Events (handler_) import React.Basic.Hooks (ReactComponent, component, fragment, useEffect, useState, (/\)) import React.Basic.Hooks as React -mkCounter :: Effect (ReactComponent {}) -mkCounter = do +mkExample :: Effect (ReactComponent {}) +mkExample = do component "Counter" \props -> React.do counter /\ setCounter <- useState 0 useEffect counter do diff --git a/examples/counter/src/Main.purs b/examples/counter/src/Main.purs index b0449bb..f9c6e57 100644 --- a/examples/counter/src/Main.purs +++ b/examples/counter/src/Main.purs @@ -1,12 +1,11 @@ module Main where import Prelude - -import Counter (mkCounter) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) -import React.Basic.Hooks(element) +import Example (mkExample) +import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -18,7 +17,8 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> do - counter <- mkCounter - let app = element counter {} + Just c -> do + ex <- mkExample + let + app = element ex {} render app c diff --git a/examples/memo-callback/Makefile b/examples/memo-callback/Makefile index ecacfbe..86ea307 100644 --- a/examples/memo-callback/Makefile +++ b/examples/memo-callback/Makefile @@ -1,8 +1,19 @@ -all: node_modules - purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs' - purs bundle -m Main --main Main output/*/*.js > output/bundle.js - node_modules/.bin/browserify output/bundle.js -o html/index.js +parent_sources := $$(npx spago -x ../../spago.dhall sources | sed 's/^/..\/..\//' | tr '\n' ' ') + +all: output node_modules + npx purs bundle -m Main --main Main output/*/*.js > output/bundle.js + npx browserify output/bundle.js -o html/index.js + +output: node_modules + set -o noglob && \ + npx purs compile \ + src/**/*.purs \ + $(parent_sources) node_modules: - npm install + npm i --no-save browserify react react-dom + +clean: + rm -rf node_modules output +.PHONY: all output clean \ No newline at end of file diff --git a/examples/memo-callback/README.md b/examples/memo-callback/README.md deleted file mode 100644 index a9700cf..0000000 --- a/examples/memo-callback/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Memo/Callback Example - -## Building - -```sh -make -``` - -This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. - -Then open `html/index.html` in your browser. diff --git a/examples/memo-callback/package.json b/examples/memo-callback/package.json deleted file mode 100644 index d7137af..0000000 --- a/examples/memo-callback/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "react": "16.8.6", - "react-dom": "16.8.6" - }, - "devDependencies": { - "browserify": "16.2.3" - } -} diff --git a/examples/memo-callback/src/MemoCallback.purs b/examples/memo-callback/src/Example.purs similarity index 89% rename from examples/memo-callback/src/MemoCallback.purs rename to examples/memo-callback/src/Example.purs index a8f6667..a42a303 100644 --- a/examples/memo-callback/src/MemoCallback.purs +++ b/examples/memo-callback/src/Example.purs @@ -1,4 +1,4 @@ -module MemoCallback where +module Example where import Prelude import Effect (Effect) @@ -8,8 +8,8 @@ import React.Basic.Events (handler_) import React.Basic.Hooks (ReactComponent, UnsafeReference(..), component, fragment, useCallback, useEffect, useState, (/\)) import React.Basic.Hooks as React -mkMemoCallback :: Effect (ReactComponent {}) -mkMemoCallback = do +mkExample :: Effect (ReactComponent {}) +mkExample = do component "MemoCallback" \props -> React.do counter /\ setCounter <- useState 0 increment <- diff --git a/examples/memo-callback/src/Main.purs b/examples/memo-callback/src/Main.purs index e6025d5..f9c6e57 100644 --- a/examples/memo-callback/src/Main.purs +++ b/examples/memo-callback/src/Main.purs @@ -1,12 +1,11 @@ module Main where import Prelude - -import MemoCallback (mkMemoCallback) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) -import React.Basic.Hooks(element) +import Example (mkExample) +import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -18,7 +17,8 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> do - memoCallback <- mkMemoCallback - let app = element memoCallback {} + Just c -> do + ex <- mkExample + let + app = element ex {} render app c diff --git a/examples/reducer/Makefile b/examples/reducer/Makefile index ecacfbe..86ea307 100644 --- a/examples/reducer/Makefile +++ b/examples/reducer/Makefile @@ -1,8 +1,19 @@ -all: node_modules - purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs' - purs bundle -m Main --main Main output/*/*.js > output/bundle.js - node_modules/.bin/browserify output/bundle.js -o html/index.js +parent_sources := $$(npx spago -x ../../spago.dhall sources | sed 's/^/..\/..\//' | tr '\n' ' ') + +all: output node_modules + npx purs bundle -m Main --main Main output/*/*.js > output/bundle.js + npx browserify output/bundle.js -o html/index.js + +output: node_modules + set -o noglob && \ + npx purs compile \ + src/**/*.purs \ + $(parent_sources) node_modules: - npm install + npm i --no-save browserify react react-dom + +clean: + rm -rf node_modules output +.PHONY: all output clean \ No newline at end of file diff --git a/examples/reducer/README.md b/examples/reducer/README.md deleted file mode 100644 index 8c2ad2e..0000000 --- a/examples/reducer/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Reducer Example - -## Building - -```sh -make -``` - -This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. - -Then open `html/index.html` in your browser. diff --git a/examples/reducer/package.json b/examples/reducer/package.json deleted file mode 100644 index d7137af..0000000 --- a/examples/reducer/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "react": "16.8.6", - "react-dom": "16.8.6" - }, - "devDependencies": { - "browserify": "16.2.3" - } -} diff --git a/examples/reducer/src/Main.purs b/examples/reducer/src/Main.purs index abd490f..f9c6e57 100644 --- a/examples/reducer/src/Main.purs +++ b/examples/reducer/src/Main.purs @@ -1,12 +1,11 @@ module Main where import Prelude - -import Reducer (mkReducer) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) -import React.Basic.Hooks(element) +import Example (mkExample) +import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -18,7 +17,8 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> do - reducer <- mkReducer - let app = element reducer {} + Just c -> do + ex <- mkExample + let + app = element ex {} render app c diff --git a/examples/reducer/src/Reducer.purs b/examples/reducer/src/Reducer.purs index ccc7738..f7084e1 100644 --- a/examples/reducer/src/Reducer.purs +++ b/examples/reducer/src/Reducer.purs @@ -1,4 +1,4 @@ -module Reducer where +module Example where import Prelude import Effect (Effect) @@ -11,8 +11,8 @@ data Action = Increment | Decrement -mkReducer :: Effect (ReactComponent {}) -mkReducer = do +mkExample :: Effect (ReactComponent {}) +mkExample = do component "Reducer" \props -> React.do state /\ dispatch <- useReducer { counter: 0 } \state -> case _ of diff --git a/examples/refs/Makefile b/examples/refs/Makefile index ecacfbe..86ea307 100644 --- a/examples/refs/Makefile +++ b/examples/refs/Makefile @@ -1,8 +1,19 @@ -all: node_modules - purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs' - purs bundle -m Main --main Main output/*/*.js > output/bundle.js - node_modules/.bin/browserify output/bundle.js -o html/index.js +parent_sources := $$(npx spago -x ../../spago.dhall sources | sed 's/^/..\/..\//' | tr '\n' ' ') + +all: output node_modules + npx purs bundle -m Main --main Main output/*/*.js > output/bundle.js + npx browserify output/bundle.js -o html/index.js + +output: node_modules + set -o noglob && \ + npx purs compile \ + src/**/*.purs \ + $(parent_sources) node_modules: - npm install + npm i --no-save browserify react react-dom + +clean: + rm -rf node_modules output +.PHONY: all output clean \ No newline at end of file diff --git a/examples/refs/README.md b/examples/refs/README.md deleted file mode 100644 index baa86ae..0000000 --- a/examples/refs/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Refs Example - -## Building - -```sh -make -``` - -This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. - -Then open `html/index.html` in your browser. diff --git a/examples/refs/package.json b/examples/refs/package.json deleted file mode 100644 index d7137af..0000000 --- a/examples/refs/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "react": "16.8.6", - "react-dom": "16.8.6" - }, - "devDependencies": { - "browserify": "16.2.3" - } -} diff --git a/examples/refs/src/Refs.purs b/examples/refs/src/Example.purs similarity index 97% rename from examples/refs/src/Refs.purs rename to examples/refs/src/Example.purs index 7f1c874..34f9dd1 100644 --- a/examples/refs/src/Refs.purs +++ b/examples/refs/src/Example.purs @@ -1,4 +1,4 @@ -module Refs where +module Example where import Prelude import Data.Int (round) @@ -19,8 +19,8 @@ import Web.HTML.HTMLElement (getBoundingClientRect) import Web.HTML.HTMLElement as HTMLElement import Web.HTML.Window as Window -mkRefs :: Effect (ReactComponent {}) -mkRefs = do +mkExample :: Effect (ReactComponent {}) +mkExample = do component "Refs" \props -> React.do mouseDistance1 /\ buttonRef1 <- useNodeDistanceFromMouse mouseDistance2 /\ buttonRef2 <- useNodeDistanceFromMouse diff --git a/examples/refs/src/Main.purs b/examples/refs/src/Main.purs index 0e5deea..f9c6e57 100644 --- a/examples/refs/src/Main.purs +++ b/examples/refs/src/Main.purs @@ -1,12 +1,11 @@ module Main where import Prelude - -import Refs (mkRefs) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) -import React.Basic.Hooks(element) +import Example (mkExample) +import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -18,7 +17,8 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> do - refs <- mkRefs - let app = element refs {} + Just c -> do + ex <- mkExample + let + app = element ex {} render app c diff --git a/examples/todo-app/Makefile b/examples/todo-app/Makefile index ecacfbe..86ea307 100644 --- a/examples/todo-app/Makefile +++ b/examples/todo-app/Makefile @@ -1,8 +1,19 @@ -all: node_modules - purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs' - purs bundle -m Main --main Main output/*/*.js > output/bundle.js - node_modules/.bin/browserify output/bundle.js -o html/index.js +parent_sources := $$(npx spago -x ../../spago.dhall sources | sed 's/^/..\/..\//' | tr '\n' ' ') + +all: output node_modules + npx purs bundle -m Main --main Main output/*/*.js > output/bundle.js + npx browserify output/bundle.js -o html/index.js + +output: node_modules + set -o noglob && \ + npx purs compile \ + src/**/*.purs \ + $(parent_sources) node_modules: - npm install + npm i --no-save browserify react react-dom + +clean: + rm -rf node_modules output +.PHONY: all output clean \ No newline at end of file diff --git a/examples/todo-app/README.md b/examples/todo-app/README.md deleted file mode 100644 index 55d0bc0..0000000 --- a/examples/todo-app/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Todo App Example - -## Building - -```sh -make -``` - -This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. - -Then open `html/index.html` in your browser. diff --git a/examples/todo-app/package.json b/examples/todo-app/package.json deleted file mode 100644 index 61b965b..0000000 --- a/examples/todo-app/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "dependencies": { - "react": "16.8.6", - "react-dom": "16.8.6", - "uuid": "3.3.2" - }, - "devDependencies": { - "browserify": "16.2.3" - } -} diff --git a/examples/todo-app/src/TodoApp.purs b/examples/todo-app/src/Example.purs similarity index 50% rename from examples/todo-app/src/TodoApp.purs rename to examples/todo-app/src/Example.purs index d4027c5..9e913db 100644 --- a/examples/todo-app/src/TodoApp.purs +++ b/examples/todo-app/src/Example.purs @@ -1,4 +1,4 @@ -module TodoApp where +module Example where import Prelude import Data.Array as Array @@ -47,8 +47,8 @@ reducer state = case _ of Nothing -> state SetFilter filter -> state { filter = filter } -mkTodoApp :: Effect (ReactComponent {}) -mkTodoApp = do +mkExample :: Effect (ReactComponent {}) +mkExample = do let initialState = { todos: [], filter: All } todoInput <- memo mkTodoInput @@ -59,25 +59,25 @@ mkTodoApp = do pure $ R.div { children: - [ element todoInput { dispatch } - , R.div_ - $ flip Array.mapWithIndex state.todos \id todo -> - if state.filter == All - || (todo.isComplete && state.filter == Complete) - || (not todo.isComplete && state.filter == Incomplete) then - elementKeyed todoRow { key: show id, id, todo, dispatch } - else - empty - , element todoFilters { filter: state.filter, dispatch } - ] + [ element todoInput { dispatch } + , R.div_ + $ flip Array.mapWithIndex state.todos \id todo -> + if state.filter == All + || (todo.isComplete && state.filter == Complete) + || (not todo.isComplete && state.filter == Incomplete) then + elementKeyed todoRow { key: show id, id, todo, dispatch } + else + empty + , element todoFilters { filter: state.filter, dispatch } + ] , style: - R.css - { maxWidth: "600px" - , margin: "auto" - , padding: "16px" - , fontFamily: "sans-serif" - , fontSize: "16px" - } + R.css + { maxWidth: "600px" + , margin: "auto" + , padding: "16px" + , fontFamily: "sans-serif" + , fontSize: "16px" + } } where todoAppEl = RB.element $ R.unsafeCreateDOMComponent "todo-app" @@ -89,19 +89,19 @@ mkTodoInput = do pure $ R.form { onSubmit: - handler (preventDefault >>> stopPropagation) \_ -> do - props.dispatch $ CreateTodo value - setValue $ const "" + handler (preventDefault >>> stopPropagation) \_ -> do + props.dispatch $ CreateTodo value + setValue $ const "" , children: - [ R.input - { value - , onChange: - handler (preventDefault >>> stopPropagation >>> targetValue) - $ traverse_ (setValue <<< const) - , style: R.css { lineHeight: "32px", width: "100%", boxSizing: "border-box" } - , placeholder: "Enter a task" - } - ] + [ R.input + { value + , onChange: + handler (preventDefault >>> stopPropagation >>> targetValue) + $ traverse_ (setValue <<< const) + , style: R.css { lineHeight: "32px", width: "100%", boxSizing: "border-box" } + , placeholder: "Enter a task" + } + ] , style: R.css { marginBottom: "16px", width: "100%" } } @@ -111,30 +111,30 @@ mkTodoRow = pure $ R.div { children: - [ R.label - { children: - [ R.input - { type: "checkbox" - , checked: props.todo.isComplete - , onChange: Events.handler_ $ props.dispatch $ ToggleTodo props.id - , tabIndex: 0 - } - , R.text props.todo.task - ] - , style: R.css { lineHeight: "32px", fontSize: "24px", flex: "1 0 auto" } - } - , R.a - { children: [ R.text "❌" ] - , onClick: handler_ $ props.dispatch $ DeleteTodo props.id - , style: R.css { cursor: "pointer" } - } - ] + [ R.label + { children: + [ R.input + { type: "checkbox" + , checked: props.todo.isComplete + , onChange: Events.handler_ $ props.dispatch $ ToggleTodo props.id + , tabIndex: 0 + } + , R.text props.todo.task + ] + , style: R.css { lineHeight: "32px", fontSize: "24px", flex: "1 0 auto" } + } + , R.a + { children: [ R.text "❌" ] + , onClick: handler_ $ props.dispatch $ DeleteTodo props.id + , style: R.css { cursor: "pointer" } + } + ] , style: - R.css - { display: "flex" - , flexFlow: "row" - , alignItems: "center" - } + R.css + { display: "flex" + , flexFlow: "row" + , alignItems: "center" + } } mkTodoFilters :: Effect (ReactComponent { filter :: TodoFilter, dispatch :: Action -> Effect Unit }) @@ -146,20 +146,20 @@ mkTodoFilters = { children: [ R.text label ] , onClick: handler_ $ props.dispatch $ SetFilter f , style: - if props.filter == f then - R.css { cursor: "pointer", fontWeight: "bold" } - else - R.css { cursor: "pointer" } + if props.filter == f then + R.css { cursor: "pointer", fontWeight: "bold" } + else + R.css { cursor: "pointer" } } pure $ R.div { children: - [ R.hr { style: R.css { color: "lightgrey" } } - , filterLink All "All" - , R.text " / " - , filterLink Complete "Complete" - , R.text " / " - , filterLink Incomplete "Incomplete" - ] + [ R.hr { style: R.css { color: "lightgrey" } } + , filterLink All "All" + , R.text " / " + , filterLink Complete "Complete" + , R.text " / " + , filterLink Incomplete "Incomplete" + ] , style: R.css { marginTop: "16px" } } diff --git a/examples/todo-app/src/Main.purs b/examples/todo-app/src/Main.purs index fd50257..f9c6e57 100644 --- a/examples/todo-app/src/Main.purs +++ b/examples/todo-app/src/Main.purs @@ -1,12 +1,11 @@ module Main where import Prelude - -import TodoApp (mkTodoApp) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) -import React.Basic.Hooks(element) +import Example (mkExample) +import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -18,7 +17,8 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> do - todoApp <- mkTodoApp - let app = element todoApp {} + Just c -> do + ex <- mkExample + let + app = element ex {} render app c diff --git a/packages.dhall b/packages.dhall new file mode 100644 index 0000000..829e553 --- /dev/null +++ b/packages.dhall @@ -0,0 +1,128 @@ +{- +Welcome to your new Dhall package-set! + +Below are instructions for how to edit this file for most use +cases, so that you don't need to know Dhall to use it. + +## Warning: Don't Move This Top-Level Comment! + +Due to how `dhall format` currently works, this comment's +instructions cannot appear near corresponding sections below +because `dhall format` will delete the comment. However, +it will not delete a top-level comment like this one. + +## Use Cases + +Most will want to do one or both of these options: +1. Override/Patch a package's dependency +2. Add a package not already in the default package set + +This file will continue to work whether you use one or both options. +Instructions for each option are explained below. + +### Overriding/Patching a package + +Purpose: +- Change a package's dependency to a newer/older release than the + default package set's release +- Use your own modified version of some dependency that may + include new API, changed API, removed API by + using your custom git repo of the library rather than + the package set's repo + +Syntax: +Replace the overrides' "{=}" (an empty record) with the following idea +The "//" or "⫽" means "merge these two records and + when they have the same value, use the one on the right:" +------------------------------- +let overrides = + { packageName = + upstream.packageName // { updateEntity1 = "new value", updateEntity2 = "new value" } + , packageName = + upstream.packageName // { version = "v4.0.0" } + , packageName = + upstream.packageName // { repo = "https://www.example.com/path/to/new/repo.git" } + } +------------------------------- + +Example: +------------------------------- +let overrides = + { halogen = + upstream.halogen // { version = "master" } + , halogen-vdom = + upstream.halogen-vdom // { version = "v4.0.0" } + } +------------------------------- + +### Additions + +Purpose: +- Add packages that aren't already included in the default package set + +Syntax: +Replace the additions' "{=}" (an empty record) with the following idea: +------------------------------- +let additions = + { package-name = + { dependencies = + [ "dependency1" + , "dependency2" + ] + , repo = + "https://example.com/path/to/git/repo.git" + , version = + "tag ('v4.0.0') or branch ('master')" + } + , package-name = + { dependencies = + [ "dependency1" + , "dependency2" + ] + , repo = + "https://example.com/path/to/git/repo.git" + , version = + "tag ('v4.0.0') or branch ('master')" + } + , etc. + } +------------------------------- + +Example: +------------------------------- +let additions = + { benchotron = + { dependencies = + [ "arrays" + , "exists" + , "profunctor" + , "strings" + , "quickcheck" + , "lcg" + , "transformers" + , "foldable-traversable" + , "exceptions" + , "node-fs" + , "node-buffer" + , "node-readline" + , "datetime" + , "now" + ] + , repo = + "https://github.com/hdgarrood/purescript-benchotron.git" + , version = + "v7.0.0" + } + } +------------------------------- +-} + + +let upstream = + https://github.com/purescript/package-sets/releases/download/psc-0.13.6-20200331/packages.dhall sha256:350af1fdc68c91251138198f03ceedc4f8ed6651ee2af8a2177f87bcd64570d4 + +let overrides = {=} + +let additions = {=} + +in upstream // overrides // additions diff --git a/serve.json b/serve.json new file mode 100644 index 0000000..14a719d --- /dev/null +++ b/serve.json @@ -0,0 +1,8 @@ +{ + "public": "examples", + "rewrites": [ + { "source": "/:ex", "destination": "/:ex/html/index.html" }, + { "source": "/:ex/index.js", "destination": "/:ex/html/index.js" } + ], + "unlisted": ["README.md"] +} diff --git a/spago.dhall b/spago.dhall new file mode 100644 index 0000000..654c397 --- /dev/null +++ b/spago.dhall @@ -0,0 +1,18 @@ +{- +Welcome to a Spago project! +You can edit this file as you like. +-} +{ name = "react-basic-hooks" +, dependencies = + [ "aff" + , "console" + , "effect" + , "indexed-monad" + , "prelude" + , "psci-support" + , "react-basic" + , "unsafe-reference" + ] +, packages = ./packages.dhall +, sources = [ "src/**/*.purs", "test/**/*.purs" ] +} diff --git a/src/React/Basic/Hooks/Aff.purs b/src/React/Basic/Hooks/Aff.purs index 6553e01..f955412 100644 --- a/src/React/Basic/Hooks/Aff.purs +++ b/src/React/Basic/Hooks/Aff.purs @@ -1,38 +1,41 @@ module React.Basic.Hooks.Aff where import Prelude -import Data.Either (Either) import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype) -import Effect.Aff (Aff, error, killFiber, launchAff_, runAff) -import Effect.Exception (Error) +import Effect.Aff (Aff, error, killFiber, launchAff, launchAff_) +import Effect.Class (liftEffect) import React.Basic.Hooks (Hook, UseEffect, UseState, coerceHook, useEffect, useState, (/\)) import React.Basic.Hooks as React newtype UseAff key a hooks - = UseAff (UseEffect key (UseState (Result a) hooks)) + = UseAff (UseEffect key (UseState (Maybe a) hooks)) derive instance ntUseAff :: Newtype (UseAff key a hooks) _ -type Result a - = Maybe (Either Error a) - -- | `useAff` is used for asynchronous effects or `Aff`. The asynchronous effect -- | is re-run whenever the key changes. If another `Aff` runs when the key --- | changes before the previous async resolves, it will cancel the previous in --- | flight async effect. +-- | changes before the previous async resolves, it will cancel the previous +-- | in-flight effect. +-- | +-- | *Note: Aff failures are thrown. If you need to capture an error state, be +-- | sure to capture it in your data type!* useAff :: forall key a. Eq key => key -> Aff a -> - Hook (UseAff key a) (Result a) + Hook (UseAff key a) (Maybe a) useAff key aff = coerceHook React.do result /\ setResult <- useState Nothing useEffect key do setResult (const Nothing) - fiber <- runAff (setResult <<< const <<< Just) aff + fiber <- + launchAff do + r <- aff + liftEffect do + setResult \_ -> Just r pure do launchAff_ do killFiber (error "Effect hook discarded.") fiber diff --git a/test/Main.purs b/test/Main.purs new file mode 100644 index 0000000..f91f98c --- /dev/null +++ b/test/Main.purs @@ -0,0 +1,11 @@ +module Test.Main where + +import Prelude + +import Effect (Effect) +import Effect.Class.Console (log) + +main :: Effect Unit +main = do + log "🍝" + log "You should add some tests." From 07aae1a90d2aa136b524cabb8b0c724936c1c8ce Mon Sep 17 00:00:00 2001 From: Madeline Trotter Date: Mon, 6 Apr 2020 23:56:46 -0700 Subject: [PATCH 2/4] Refactor the base hook api to be a bit more PureScript-y --- examples/memo-callback/src/Example.purs | 45 +++++- src/React/Basic/Hooks.js | 49 +++---- src/React/Basic/Hooks.purs | 176 +++++++++++++----------- 3 files changed, 154 insertions(+), 116 deletions(-) diff --git a/examples/memo-callback/src/Example.purs b/examples/memo-callback/src/Example.purs index a42a303..de04f4d 100644 --- a/examples/memo-callback/src/Example.purs +++ b/examples/memo-callback/src/Example.purs @@ -1,26 +1,57 @@ module Example where import Prelude +import Data.Foldable (traverse_) +import Data.Int as Int +import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Console (log) import React.Basic.DOM as R -import React.Basic.Events (handler_) -import React.Basic.Hooks (ReactComponent, UnsafeReference(..), component, fragment, useCallback, useEffect, useState, (/\)) +import React.Basic.DOM.Events (targetValue) +import React.Basic.Events (handler, handler_) +import React.Basic.Hooks (ReactComponent, UnsafeReference(..), component, fragment, useEffect, useLazy, useMemo, useState, (/\)) import React.Basic.Hooks as React mkExample :: Effect (ReactComponent {}) mkExample = do component "MemoCallback" \props -> React.do - counter /\ setCounter <- useState 0 + initialValue /\ setInitialValue <- useState 0 + counter /\ setCounter <- useState initialValue increment <- - useCallback (UnsafeReference setCounter) - $ setCounter (_ + 1) + useLazy + (UnsafeReference setCounter) \_ -> setCounter (_ + 1) + reset <- + useLazy + (initialValue /\ UnsafeReference setCounter) \_ -> setCounter \_ -> initialValue useEffect (UnsafeReference increment) do - log "increment updated" + log "increment updated -- this should only get logged once!" + pure mempty + useEffect (UnsafeReference reset) do + log "reset updated -- this should only get logged when the 'initialValue' is changed!" + pure mempty + let + memoTestVal = Just 1 + memoTest <- useMemo memoTestVal + useEffect (UnsafeReference memoTest) do + log "memoTest updated -- this should only get logged once!" pure mempty pure $ fragment - [ R.button + [ R.text "Change initial value:" + , R.div_ + [ R.input + { value: show initialValue + , onChange: + handler targetValue + $ (_ >>= Int.fromString) + >>> traverse_ (const >>> setInitialValue) + } + , R.button + { onClick: handler_ reset + , children: [ R.text "Reset" ] + } + ] + , R.button { onClick: handler_ increment , children: [ R.text $ "Increment: " <> show counter ] } diff --git a/src/React/Basic/Hooks.js b/src/React/Basic/Hooks.js index 9a54690..f18163a 100644 --- a/src/React/Basic/Hooks.js +++ b/src/React/Basic/Hooks.js @@ -2,19 +2,19 @@ var React = require("react"); -exports.reactChildrenToArray = function(children) { +exports.reactChildrenToArray = function (children) { return React.Children.toArray(children); }; exports.memo_ = React.memo; -exports.useState_ = function(tuple, initialState) { +exports.useState_ = function (tuple, initialState) { var r = React.useState(initialState); var state = r[0]; var setState = r[1]; if (!setState.hasOwnProperty("$$reactBasicHooks$$cachedSetState")) { - setState.$$reactBasicHooks$$cachedSetState = function(update) { - return function() { + setState.$$reactBasicHooks$$cachedSetState = function (update) { + return function () { return setState(update); }; }; @@ -22,23 +22,23 @@ exports.useState_ = function(tuple, initialState) { return tuple(state, setState.$$reactBasicHooks$$cachedSetState); }; -exports.useEffect_ = function(eq, key, effect) { - var memoizedKey = exports.useEqCache_(eq, key); +exports.useEffect_ = function (eq, key, effect) { + var memoizedKey = exports.useMemo_(eq, key); React.useEffect(effect, [memoizedKey]); }; -exports.useLayoutEffect_ = function(eq, key, effect) { - var memoizedKey = exports.useEqCache_(eq, key); +exports.useLayoutEffect_ = function (eq, key, effect) { + var memoizedKey = exports.useMemo_(eq, key); React.useLayoutEffect(effect, [memoizedKey]); }; -exports.useReducer_ = function(tuple, reducer, initialState, initialAction) { +exports.useReducer_ = function (tuple, reducer, initialState, initialAction) { var r = React.useReducer(reducer, initialState, initialAction); var state = r[0]; var dispatch = r[1]; if (!dispatch.hasOwnProperty("$$reactBasicHooks$$cachedDispatch")) { - dispatch.$$reactBasicHooks$$cachedDispatch = function(action) { - return function() { + dispatch.$$reactBasicHooks$$cachedDispatch = function (action) { + return function () { return dispatch(action); }; }; @@ -48,27 +48,17 @@ exports.useReducer_ = function(tuple, reducer, initialState, initialAction) { exports.useRef_ = React.useRef; -exports.readRef_ = function(ref) { +exports.readRef_ = function (ref) { return ref.current; }; -exports.writeRef_ = function(ref, a) { +exports.writeRef_ = function (ref, a) { ref.current = a; }; exports.useContext_ = React.useContext; -exports.useMemo_ = function(eq, key, computeA) { - var memoizedKey = exports.useEqCache_(eq, key); - return React.useMemo(computeA, [memoizedKey]); -}; - -exports.useCallback_ = function(eq, key, cb) { - var memoizedKey = exports.useEqCache_(eq, key); - return React.useCallback(cb, [memoizedKey]); -}; - -exports.useEqCache_ = function(eq, a) { +exports.useMemo_ = function (eq, a) { var memoRef = React.useRef(a); if (memoRef.current !== a && !eq(memoRef.current, a)) { memoRef.current = a; @@ -76,15 +66,20 @@ exports.useEqCache_ = function(eq, a) { return memoRef.current; }; -exports.unsafeSetDisplayName = function(displayName, component) { +exports.useLazy_ = function (eq, key, computeA) { + var memoizedKey = exports.useMemo_(eq, key); + return React.useMemo(computeA, [memoizedKey]); +}; + +exports.unsafeSetDisplayName = function (displayName, component) { component.displayName = displayName; - component.toString = function() { + component.toString = function () { return displayName; }; return component; }; -exports.displayName = function(component) { +exports.displayName = function (component) { return typeof component === "string" ? component : component.displayName || "[unknown]"; diff --git a/src/React/Basic/Hooks.purs b/src/React/Basic/Hooks.purs index f7ca14c..941a1bc 100644 --- a/src/React/Basic/Hooks.purs +++ b/src/React/Basic/Hooks.purs @@ -4,31 +4,31 @@ module React.Basic.Hooks , componentFromHook , ReactChildren , memo - , UseState , useState - , UseEffect + , UseState , useEffect - , UseLayoutEffect + , useEffectOnce + , useEffectAlways + , UseEffect , useLayoutEffect - , UseReducer + , useLayoutEffectOnce + , useLayoutEffectAlways + , UseLayoutEffect , useReducer - , UseRef + , UseReducer , readRef , readRefMaybe , writeRef , reactChildrenFromArray , reactChildrenToArray - , renderRef - , renderRefMaybe , useRef - , UseContext + , UseRef , useContext - , UseMemo + , UseContext , useMemo - , UseCallback - , useCallback - , UseEqCache - , useEqCache + , UseMemo + , useLazy + , UseLazy , UnsafeReference(..) , displayName , module React.Basic.Hooks.Internal @@ -128,14 +128,15 @@ foreign import reactChildrenToArray :: forall a. ReactChildren a -> Array a reactChildrenFromArray :: forall a. Array a -> ReactChildren a reactChildrenFromArray = unsafeCoerce +-- | Prevents a component from re-rendering if its new props are referentially +-- | equal to its old props (not value-based equality. This is due to the underlying +-- | React implementation. memo :: forall props. Effect (ReactComponent props) -> Effect (ReactComponent props) memo = flip Prelude.bind (runEffectFn1 memo_) -foreign import data UseState :: Type -> Type -> Type - useState :: forall state. state -> @@ -144,29 +145,53 @@ useState initialState = unsafeHook do runEffectFn2 useState_ (mkFn2 Tuple) initialState -foreign import data UseEffect :: Type -> Type -> Type +foreign import data UseState :: Type -> Type -> Type --- | The effect will be run when the component is mounted, and the effect --- | returned from the function will be run on cleanup +-- | Runs the given effect when the component is mounted and any time the given +-- | dependencies change. The effect should return its cleanup function. For +-- | example, if the effect registers a global event listener, it should return +-- | and Effect which removes the listener. useEffect :: - forall key. - Eq key => - key -> + forall deps. + Eq deps => + deps -> Effect (Effect Unit) -> - Hook (UseEffect key) Unit -useEffect key effect = unsafeHook (runEffectFn3 useEffect_ (mkFn2 eq) key effect) + Hook (UseEffect deps) Unit +useEffect deps effect = unsafeHook (runEffectFn3 useEffect_ (mkFn2 eq) deps effect) -foreign import data UseLayoutEffect :: Type -> Type -> Type +-- | Like `useEffect`, but the effect is only performed a single time per component +-- | instance. Prefer `useEffect` with a proper dependency list whenever possible! +useEffectOnce :: Effect (Effect Unit) -> Hook (UseEffect Unit) Unit +useEffectOnce effect = unsafeHook (runEffectFn3 useEffect_ (mkFn2 \_ _ -> true) unit effect) + +-- | Like `useEffect`, but the effect is performed on every render. Prefer `useEffect` +-- | with a proper dependency list whenever possible! +useEffectAlways :: Effect (Effect Unit) -> Hook (UseEffect Unit) Unit +useEffectAlways effect = unsafeHook (runEffectFn3 useEffect_ (mkFn2 \_ _ -> false) unit effect) + +foreign import data UseEffect :: Type -> Type -> Type +-- | Like `useEffect`, but the effect is performed on every render. Prefer `useEffect` +-- | with a proper dependency list whenever possible! useLayoutEffect :: - forall key. - Eq key => - key -> + forall deps. + Eq deps => + deps -> Effect (Effect Unit) -> - Hook (UseLayoutEffect key) Unit + Hook (UseLayoutEffect deps) Unit useLayoutEffect keys effect = unsafeHook (runEffectFn3 useLayoutEffect_ (mkFn2 eq) keys effect) -foreign import data UseReducer :: Type -> Type -> Type -> Type +-- | Like `useLayoutEffect`, but the effect is only performed a single time per component +-- | instance. Prefer `useLayoutEffect` with a proper dependency list whenever possible! +useLayoutEffectOnce :: Effect (Effect Unit) -> Hook (UseLayoutEffect Unit) Unit +useLayoutEffectOnce effect = unsafeHook (runEffectFn3 useLayoutEffect_ (mkFn2 \_ _ -> true) unit effect) + +-- | Like `useLayoutEffect`, but the effect is performed on every render. Prefer `useLayoutEffect` +-- | with a proper dependency list whenever possible! +useLayoutEffectAlways :: Effect (Effect Unit) -> Hook (UseLayoutEffect Unit) Unit +useLayoutEffectAlways effect = unsafeHook (runEffectFn3 useLayoutEffect_ (mkFn2 \_ _ -> false) unit effect) + +foreign import data UseLayoutEffect :: Type -> Type -> Type useReducer :: forall state action. @@ -180,7 +205,7 @@ useReducer initialState reducer = (mkFn2 reducer) initialState -foreign import data UseRef :: Type -> Type -> Type +foreign import data UseReducer :: Type -> Type -> Type -> Type useRef :: forall a. a -> Hook (UseRef a) (Ref a) useRef initialValue = @@ -196,45 +221,40 @@ readRefMaybe a = map toMaybe (readRef a) writeRef :: forall a. Ref a -> a -> Effect Unit writeRef = runEffectFn2 writeRef_ -renderRef :: forall a. Ref a -> Pure a -renderRef ref = unsafeRenderEffect (readRef ref) - -renderRefMaybe :: forall a. Ref (Nullable a) -> Pure (Maybe a) -renderRefMaybe a = unsafeRenderEffect (readRefMaybe a) - -foreign import data UseContext :: Type -> Type -> Type +foreign import data UseRef :: Type -> Type -> Type useContext :: forall a. ReactContext a -> Hook (UseContext a) a useContext context = unsafeHook (runEffectFn1 useContext_ context) -foreign import data UseMemo :: Type -> Type -> Type -> Type +foreign import data UseContext :: Type -> Type -> Type +-- | Use this hook to memoize a value based on a set of deps. This is +-- | useful when you need to take advantage of `memo` and need to pass +-- | referentially equal values to a child component. This is purely +-- | a performance optimization and shouldn't change the behavior of +-- | your component. +-- | +-- | If building a value of `a` is expensive, try `useLazy`. useMemo :: - forall key a. - Eq key => - key -> - (Unit -> a) -> - Hook (UseMemo key a) a -useMemo key computeA = unsafeHook (runEffectFn3 useMemo_ (mkFn2 eq) key computeA) - -foreign import data UseCallback :: Type -> Type -> Type -> Type - -useCallback :: - forall key a. - Eq key => - key -> - a -> - Hook (UseCallback key a) a -useCallback key computeA = unsafeHook (runEffectFn3 useCallback_ (mkFn2 eq) key computeA) - -foreign import data UseEqCache :: Type -> Type -> Type - -useEqCache :: forall a. Eq a => a -> - Hook (UseCallback a a) a -useEqCache a = unsafeHook (runEffectFn2 useEqCache_ (mkFn2 eq) a) + Hook (UseMemo a) a +useMemo a = unsafeHook (runEffectFn2 useMemo_ (mkFn2 eq) a) + +foreign import data UseMemo :: Type -> Type -> Type + +-- | Lazily compute a value. The result is cached in the component +-- | instance until the deps change. +useLazy :: + forall deps a. + Eq deps => + deps -> + (Unit -> a) -> + Hook (UseLazy deps a) a +useLazy deps computeA = unsafeHook (runEffectFn3 useLazy_ (mkFn2 eq) deps computeA) + +foreign import data UseLazy :: Type -> Type -> Type -> Type newtype UnsafeReference a = UnsafeReference a @@ -274,18 +294,18 @@ foreign import useState_ :: (state /\ ((state -> state) -> Effect Unit)) foreign import useEffect_ :: - forall key. + forall deps. EffectFn3 - (Fn2 key key Boolean) - key + (Fn2 deps deps Boolean) + deps (Effect (Effect Unit)) Unit foreign import useLayoutEffect_ :: - forall key. + forall deps. EffectFn3 - (Fn2 key key Boolean) - key + (Fn2 deps deps Boolean) + deps (Effect (Effect Unit)) Unit @@ -323,24 +343,16 @@ foreign import useContext_ :: a foreign import useMemo_ :: - forall key a. - EffectFn3 - (Fn2 key key Boolean) - key - (Unit -> a) - a - -foreign import useCallback_ :: - forall key a. - EffectFn3 - (Fn2 key key Boolean) - key - a - a - -foreign import useEqCache_ :: forall a. EffectFn2 (Fn2 a a Boolean) a a + +foreign import useLazy_ :: + forall deps a. + EffectFn3 + (Fn2 deps deps Boolean) + deps + (Unit -> a) + a From 17f925191c010dc56f2b74b11419d93998d5b03f Mon Sep 17 00:00:00 2001 From: Madeline Trotter Date: Wed, 8 Apr 2020 02:28:37 -0700 Subject: [PATCH 3/4] New toys c: --- .prettierrc | 3 + README.md | 17 +- examples/aff/src/Example.purs | 122 ++- examples/suspense/.gitignore | 4 + examples/suspense/Makefile | 19 + examples/suspense/html/index.html | 10 + examples/suspense/src/Example.purs | 155 +++ examples/suspense/src/Main.purs | 24 + package-lock.json | 1209 +++++++++++++++++++++ packages.dhall | 2 +- spago.dhall | 1 + src/React/Basic/DOM/Concurrent.js | 7 + src/React/Basic/DOM/Concurrent.purs | 9 + src/React/Basic/Hooks.js | 12 +- src/React/Basic/Hooks.purs | 4 +- src/React/Basic/Hooks/Aff.purs | 50 +- src/React/Basic/Hooks/ErrorBoundary.js | 21 + src/React/Basic/Hooks/ErrorBoundary.purs | 41 + src/React/Basic/Hooks/Internal.purs | 22 +- src/React/Basic/Hooks/ResetToken.purs | 42 +- src/React/Basic/Hooks/Suspense.js | 5 + src/React/Basic/Hooks/Suspense.purs | 58 + src/React/Basic/Hooks/Suspense/Store.purs | 101 ++ src/React/Basic/StrictMode.js | 5 + src/React/Basic/StrictMode.purs | 10 + 25 files changed, 1837 insertions(+), 116 deletions(-) create mode 100644 .prettierrc create mode 100644 examples/suspense/.gitignore create mode 100644 examples/suspense/Makefile create mode 100644 examples/suspense/html/index.html create mode 100644 examples/suspense/src/Example.purs create mode 100644 examples/suspense/src/Main.purs create mode 100644 package-lock.json create mode 100644 src/React/Basic/DOM/Concurrent.js create mode 100644 src/React/Basic/DOM/Concurrent.purs create mode 100644 src/React/Basic/Hooks/ErrorBoundary.js create mode 100644 src/React/Basic/Hooks/ErrorBoundary.purs create mode 100644 src/React/Basic/Hooks/Suspense.js create mode 100644 src/React/Basic/Hooks/Suspense.purs create mode 100644 src/React/Basic/Hooks/Suspense/Store.purs create mode 100644 src/React/Basic/StrictMode.js create mode 100644 src/React/Basic/StrictMode.purs diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..36b3563 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "trailingComma": "none" +} diff --git a/README.md b/README.md index 2794bba..56e66d5 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,12 @@ mkCounter = do More examples: -- [Counter with an effect](./examples/counter/src/Counter.purs) -- [Reducer/action-style](./examples/reducer/src/Reducer.purs) -- [Controlled inputs](./examples/controlled-input/src/ControlledInput.purs) -- Components: [Parent](./examples/component/src/Container.purs) and [Child](./examples/component/src/ToggleButton.purs) -- [Refs to DOM nodes](./examples/refs/src/Refs.purs) (and extracting hook logic from a component for reuse) -- [A Todo App](./examples/todo-app/src/TodoApp.purs) (components, inputs, state) -- [Context](./examples/context/src/Context.purs) (creating and consuming React context) -- [Aff helper](./examples/aff/src/AffEx.purs) (async state management) +- [Counter with an effect](./examples/counter/src/Example.purs) +- [Reducer/action-style](./examples/reducer/src/Example.purs) +- [Controlled inputs](./examples/controlled-input/src/Example.purs) +- Components: [Parent](./examples/component/src/Example.purs) and [Child](./examples/component/src/ToggleButton.purs) +- [Refs to DOM nodes](./examples/refs/src/Example.purs) (and extracting hook logic from a component for reuse) +- [A Todo App](./examples/todo-app/src/Example.purs) (components, inputs, state) +- [Context](./examples/context/src/Example.purs) (creating and consuming React context) +- [Aff](./examples/aff/src/Example.purs) (rendering async data, using error boundaries) +- [Suspense](./examples/suspense/src/Example.purs) (experimental, React Suspense demo -- similar to the Aff example, but the loading state is managed by the parent instead of the detail rendering component) diff --git a/examples/aff/src/Example.purs b/examples/aff/src/Example.purs index 6cad111..511da6c 100644 --- a/examples/aff/src/Example.purs +++ b/examples/aff/src/Example.purs @@ -5,73 +5,95 @@ import Data.Foldable (find) import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype, un) import Effect (Effect) -import Effect.Aff (Aff, Milliseconds(..), delay, error, throwError) +import Effect.Aff (Aff, Milliseconds(..), delay, error, message, throwError) import React.Basic.DOM as R +import React.Basic.DOM.Events (capture_) import React.Basic.Events (handler_) -import React.Basic.Hooks (type (/\), ReactComponent, Hook, JSX, component, element, fragment, useState, (/\)) +import React.Basic.Hooks (JSX, ReactComponent, component, element, fragment, useState, (/\)) import React.Basic.Hooks as React import React.Basic.Hooks.Aff (useAff) +import React.Basic.Hooks.ErrorBoundary (mkErrorBoundary) mkExample :: Effect (ReactComponent {}) mkExample = do - -- A component for fetching and rendering a Cat entity. + errorBoundary <- mkErrorBoundary "AffExErrorBoundary" catDetails <- mkCatDetails component "AffEx" \props -> React.do - catKey /\ catChooser <- useCatKeyChooser + catKey /\ setCatKey <- useState Nothing + let + reset = setCatKey \_ -> Nothing pure - $ R.div - { style: R.css { display: "flex", flexFlow: "column" } - , children: - [ R.h2_ [ R.text "Cat chooser" ] - , R.p_ - [ R.text - $ "Select a key to fetch! If you get bored (how would you even!?) " - <> "try holding your arrow keys to select really fast! The result " - <> "always matches the chosen key." - ] - , catChooser - , R.p_ - [ case catKey of + $ R.div_ + [ R.h2_ [ R.text "Cat chooser" ] + , errorBoundary \{ error, dismissError } -> case error of + Just e -> renderAppError e (reset *> dismissError) + Nothing -> + fragment + [ catKeyList catKey setCatKey + , case catKey of Nothing -> mempty - Just k -> element catDetails { catKey: k } + Just k -> catDetails { catKey: k } ] + ] + where + -- This component is the main `useAff` demo. It receives a key + -- as a prop and renders both the loading state and the final + -- result. + mkCatDetails :: Effect ({ catKey :: Key Cat } -> JSX) + mkCatDetails = + map element do + component "CatDetails" \{ catKey } -> React.do + catState <- useAff catKey $ fetch catKey + pure + $ R.p_ + [ case map entity catState of + Nothing -> R.text "Loading..." + Just (Cat { name }) -> R.text $ "A cat named " <> name ] + + renderAppError error resetApp = + fragment + [ R.p_ [ R.text "Error!" ] + , R.p_ [ R.text $ message error ] + , R.button + { onClick: capture_ do resetApp + , children: [ R.text "Reset" ] } - where - -- This hook packages up some interactive UI and the current - -- selection the user has made via that UI. - useCatKeyChooser :: Hook _ ((Maybe (Key Cat)) /\ JSX) - useCatKeyChooser = React.do - catKey /\ setCatKey <- useState Nothing - let - catChoice k = - R.label_ - [ R.input - { type: "radio" - , name: "cat-key" - , checked: Just k == catKey - , onChange: - handler_ do - setCatKey \_ -> Just k - } - , R.text $ " " <> showCatKey k - ] + ] - showCatKey :: Key Cat -> String - showCatKey (Key k) = "Cat " <> k - pure $ catKey /\ fragment (map (catChoice <<< key) fakeDb) + catKeyList selectedCatKey setCatKey = + let + cats = + fakeDb + <> [ Entity + (Key "error (choose to throw a React render error)") + (Cat { name: "" }) + ] - -- Hooks can't be used conditionally but components can! - -- Not needing to deal with a `Maybe` key simplifies this - -- compoennt a bit. - mkCatDetails :: Effect (ReactComponent { catKey :: Key Cat }) - mkCatDetails = do - component "CatDetails" \{ catKey } -> React.do - catState <- useAff catKey $ fetch catKey - pure case map entity catState of - Nothing -> R.text "Loading..." - Just (Cat { name }) -> R.text $ "A cat named " <> name + catKeyRadioButton k = + R.div_ + [ R.label_ + [ R.input + { type: "radio" + , name: "cat-key" + , checked: Just k == selectedCatKey + , onChange: + handler_ do + setCatKey \_ -> Just k + } + , R.text $ " Cat " <> un Key k + ] + ] + in + fragment $ map (catKeyRadioButton <<< key) cats +-- +-- The bits below this point aren't directly relevant to the example, +-- just a slightly more interesting data model than returing a single +-- string. +-- +-- +-- -- Typed keys are a great way to tie entity-specific behavior -- to an ID. We can use this phantom type to write a class -- for generic, type-safe data fetching. diff --git a/examples/suspense/.gitignore b/examples/suspense/.gitignore new file mode 100644 index 0000000..645684d --- /dev/null +++ b/examples/suspense/.gitignore @@ -0,0 +1,4 @@ +output +html/index.js +package-lock.json +node_modules diff --git a/examples/suspense/Makefile b/examples/suspense/Makefile new file mode 100644 index 0000000..86ea307 --- /dev/null +++ b/examples/suspense/Makefile @@ -0,0 +1,19 @@ +parent_sources := $$(npx spago -x ../../spago.dhall sources | sed 's/^/..\/..\//' | tr '\n' ' ') + +all: output node_modules + npx purs bundle -m Main --main Main output/*/*.js > output/bundle.js + npx browserify output/bundle.js -o html/index.js + +output: node_modules + set -o noglob && \ + npx purs compile \ + src/**/*.purs \ + $(parent_sources) + +node_modules: + npm i --no-save browserify react react-dom + +clean: + rm -rf node_modules output + +.PHONY: all output clean \ No newline at end of file diff --git a/examples/suspense/html/index.html b/examples/suspense/html/index.html new file mode 100644 index 0000000..6b93b7c --- /dev/null +++ b/examples/suspense/html/index.html @@ -0,0 +1,10 @@ + + + + react-basic example + + +
+ + + diff --git a/examples/suspense/src/Example.purs b/examples/suspense/src/Example.purs new file mode 100644 index 0000000..a8298ae --- /dev/null +++ b/examples/suspense/src/Example.purs @@ -0,0 +1,155 @@ +module Example where + +import Prelude +import Data.Foldable (find) +import Data.Maybe (Maybe(..)) +import Data.Newtype (class Newtype, un) +import Data.Time.Duration (Seconds(..), fromDuration) +import Effect (Effect) +import Effect.Aff (Aff, Milliseconds(..), delay, error, message, throwError) +import React.Basic.DOM as R +import React.Basic.DOM.Events (capture_) +import React.Basic.Events (handler_) +import React.Basic.Hooks (JSX, ReactComponent, component, element, fragment, useState, (/\)) +import React.Basic.Hooks as React +import React.Basic.Hooks.ErrorBoundary (mkErrorBoundary) +import React.Basic.Hooks.Suspense (suspend, suspense) +import React.Basic.Hooks.Suspense.Store (SuspenseStore, get, mkSuspenseStore) + +mkExample :: Effect (ReactComponent {}) +mkExample = do + errorBoundary <- mkErrorBoundary "SuspenseExErrorBoundary" + catDetails <- mkCatDetails + component "SuspenseEx" \props -> React.do + catKey /\ setCatKey <- useState Nothing + let + reset = setCatKey \_ -> Nothing + pure + $ R.div_ + [ R.h2_ [ R.text "Cat chooser" ] + , errorBoundary \{ error, dismissError } -> case error of + Just e -> renderAppError e (reset *> dismissError) + Nothing -> + fragment + [ catKeyList catKey setCatKey + , case catKey of + Nothing -> mempty + Just k -> + R.p_ + [ suspense + { fallback: R.text "Loading..." + , children: [ catDetails { catKey: k } ] + } + ] + ] + ] + where + -- This component is the main `suspense` demo (but don't forget the `suspense` + -- element above!). It receives a key as a prop and renders the result as though + -- it were synchronously available. + mkCatDetails :: Effect ({ catKey :: Key Cat } -> JSX) + mkCatDetails = do + catStore :: SuspenseStore (Key Cat) _ <- + mkSuspenseStore (Just $ fromDuration $ Seconds 10.0) fetch + element + <$> component "CatDetails" \{ catKey } -> React.do + cat <- suspend $ get catStore catKey + pure + $ R.p_ + [ case entity cat of + Cat { name } -> R.text $ "A cat named " <> name + ] + + renderAppError error resetApp = + fragment + [ R.p_ [ R.text "Error!" ] + , R.p_ [ R.text $ message error ] + , R.button + { onClick: capture_ do resetApp + , children: [ R.text "Reset" ] + } + ] + + catKeyList selectedCatKey setCatKey = + let + cats = + fakeDb + <> [ Entity + (Key "error (choose to throw a React render error)") + (Cat { name: "" }) + ] + + catKeyRadioButton k = + R.div_ + [ R.label_ + [ R.input + { type: "radio" + , name: "cat-key" + , checked: Just k == selectedCatKey + , onChange: + handler_ do + setCatKey \_ -> Just k + } + , R.text $ " Cat " <> un Key k + ] + ] + in + fragment $ map (catKeyRadioButton <<< key) cats + +-- +-- The bits below this point aren't directly relevant to the example, +-- just a slightly more interesting data model than returing a single +-- string. +-- +-- +-- +-- Typed keys are a great way to tie entity-specific behavior +-- to an ID. We can use this phantom type to write a class +-- for generic, type-safe data fetching. +newtype Key entity + = Key String + +derive instance eqKey :: Eq (Key entity) + +derive instance ordKey :: Ord (Key entity) + +derive instance ntKey :: Newtype (Key entity) _ + +-- An entity wrapper. In a real app this would hold other metadata +-- such as create and update dates. +data Entity entity + = Entity (Key entity) entity + +key :: forall entity. Entity entity -> Key entity +key (Entity k _) = k + +entity :: forall entity. Entity entity -> entity +entity (Entity _ e) = e + +class Fetch entity where + fetch :: Key entity -> Aff (Entity entity) + +-- An example entity +newtype Cat + = Cat { name :: String } + +fakeDb :: Array (Entity Cat) +fakeDb = + [ Entity (Key "abc") (Cat { name: "Herb" }) + , Entity (Key "def") (Cat { name: "Maxi" }) + , Entity (Key "ghi") (Cat { name: "Chloe" }) + ] + +instance fetchCat :: Fetch Cat where + fetch k = do + delay $ Milliseconds 300.0 + -- pretend this happens on the server + case fakeDb # find (key >>> (_ == k)) of + Nothing -> + -- This should never happen in a normal application path + -- if only the server can generate keys :) + throwError + $ error + $ "DB error: Cat not found for key " + <> un Key k + Just e -> pure e diff --git a/examples/suspense/src/Main.purs b/examples/suspense/src/Main.purs new file mode 100644 index 0000000..f9c6e57 --- /dev/null +++ b/examples/suspense/src/Main.purs @@ -0,0 +1,24 @@ +module Main where + +import Prelude +import Data.Maybe (Maybe(..)) +import Effect (Effect) +import Effect.Exception (throw) +import Example (mkExample) +import React.Basic.Hooks (element) +import React.Basic.DOM (render) +import Web.DOM.NonElementParentNode (getElementById) +import Web.HTML (window) +import Web.HTML.HTMLDocument (toNonElementParentNode) +import Web.HTML.Window (document) + +main :: Effect Unit +main = do + container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) + case container of + Nothing -> throw "Container element not found." + Just c -> do + ex <- mkExample + let + app = element ex {} + render app c diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b99ad87 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1209 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "ajv": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "arch": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", + "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" + }, + "cacache": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz", + "integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==", + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz", + "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "execa": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", + "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^3.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==" + }, + "filesize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-4.2.1.tgz", + "integrity": "sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA==" + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "requires": { + "minipass": "^2.6.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "requires": { + "pump": "^3.0.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globule": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.1.tgz", + "integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.12", + "minimatch": "~3.0.2" + } + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "keypress": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.2.1.tgz", + "integrity": "sha1-HoBFQlABjbrUw/6USX1uZ7YmnHc=", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "requires": { + "chalk": "^2.4.2" + } + }, + "log-update": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-3.4.0.tgz", + "integrity": "sha512-ILKe88NeMt4gmDvk/eb615U/IVn7K9KWGkoYbdatQ69Z65nj1ZzjM6fHXfcs0Uge+e+EGnMW7DY4T9yko8vWFg==", + "requires": { + "ansi-escapes": "^3.2.0", + "cli-cursor": "^2.1.0", + "wrap-ansi": "^5.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "requires": { + "mime-db": "1.43.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "requires": { + "minipass": "^2.9.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "npm-run-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", + "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", + "requires": { + "path-key": "^3.0.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==" + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + }, + "pscid": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/pscid/-/pscid-2.9.0.tgz", + "integrity": "sha512-YktAYj/QmnJ12jh3smI/XkhBJyYF8wNjdLRdR3FcU8MRQE0ryrYM23vri9hTIBoA5Fv3AXt3l2bcpAZ8Ota3aA==", + "dev": true, + "requires": { + "gaze": "^1.1.3", + "glob": "^7.1.6", + "keypress": "^0.2.1", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "purescript": { + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/purescript/-/purescript-0.13.6.tgz", + "integrity": "sha512-PC93xqr0zDs5l5xnfTlptKzv5jBWbML+dwtpDCZkOOH7h9wgLusQfU4PNfHvdwrSmsBntalGm+Cbd6VrokN7Sg==", + "requires": { + "purescript-installer": "^0.2.0" + } + }, + "purescript-installer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/purescript-installer/-/purescript-installer-0.2.5.tgz", + "integrity": "sha512-fQAWWP5a7scuchXecjpU4r4KEgSPuS6bBnaP01k9f71qqD28HaJ2m4PXHFkhkR4oATAxTPIGCtmTwtVoiBOHog==", + "requires": { + "arch": "^2.1.1", + "byline": "^5.0.0", + "cacache": "^11.3.2", + "chalk": "^2.4.2", + "env-paths": "^2.2.0", + "execa": "^2.0.3", + "filesize": "^4.1.2", + "is-plain-obj": "^2.0.0", + "log-symbols": "^3.0.0", + "log-update": "^3.2.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "ms": "^2.1.2", + "once": "^1.4.0", + "pump": "^3.0.0", + "request": "^2.88.0", + "rimraf": "^2.6.3", + "tar": "^4.4.6", + "which": "^1.3.1", + "zen-observable": "^0.8.14" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + } + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "requires": { + "aproba": "^1.1.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "spago": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/spago/-/spago-0.14.0.tgz", + "integrity": "sha512-nQMbxZkj5a9c1j36aUd7dp2BhlSByFkfu7/I4bA4nAnL39IAsoxz/p9B6FFs/TKCDcoaWjjjIvnOmei9G4j8SA==", + "requires": { + "request": "^2.88.0", + "tar": "^4.4.8" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + } + } +} diff --git a/packages.dhall b/packages.dhall index 829e553..d293079 100644 --- a/packages.dhall +++ b/packages.dhall @@ -119,7 +119,7 @@ let additions = let upstream = - https://github.com/purescript/package-sets/releases/download/psc-0.13.6-20200331/packages.dhall sha256:350af1fdc68c91251138198f03ceedc4f8ed6651ee2af8a2177f87bcd64570d4 + https://github.com/purescript/package-sets/releases/download/psc-0.13.6-20200404/packages.dhall sha256:f239f2e215d0cbd5c203307701748581938f74c4c78f4aeffa32c11c131ef7b6 let overrides = {=} diff --git a/spago.dhall b/spago.dhall index 654c397..73ac0df 100644 --- a/spago.dhall +++ b/spago.dhall @@ -5,6 +5,7 @@ You can edit this file as you like. { name = "react-basic-hooks" , dependencies = [ "aff" + , "aff-promise" , "console" , "effect" , "indexed-monad" diff --git a/src/React/Basic/DOM/Concurrent.js b/src/React/Basic/DOM/Concurrent.js new file mode 100644 index 0000000..3ca26d1 --- /dev/null +++ b/src/React/Basic/DOM/Concurrent.js @@ -0,0 +1,7 @@ +"use strict"; + +const ReactDOM = require("react-dom"); + +exports.renderConcurrentMode = (jsx) => (element) => () => { + return ReactDOM.createRoot(element).render(jsx); +}; diff --git a/src/React/Basic/DOM/Concurrent.purs b/src/React/Basic/DOM/Concurrent.purs new file mode 100644 index 0000000..b671a24 --- /dev/null +++ b/src/React/Basic/DOM/Concurrent.purs @@ -0,0 +1,9 @@ +-- TODO: move this file to `react-basic` +module React.Basic.DOM.Concurrent where + +import Prelude +import Effect (Effect) +import React.Basic (JSX) +import Web.DOM (Element) + +foreign import renderConcurrentMode :: JSX -> Element -> Effect Unit diff --git a/src/React/Basic/Hooks.js b/src/React/Basic/Hooks.js index f18163a..26f7e20 100644 --- a/src/React/Basic/Hooks.js +++ b/src/React/Basic/Hooks.js @@ -22,13 +22,13 @@ exports.useState_ = function (tuple, initialState) { return tuple(state, setState.$$reactBasicHooks$$cachedSetState); }; -exports.useEffect_ = function (eq, key, effect) { - var memoizedKey = exports.useMemo_(eq, key); +exports.useEffect_ = function (eq, deps, effect) { + var memoizedKey = exports.useMemo_(eq, deps); React.useEffect(effect, [memoizedKey]); }; -exports.useLayoutEffect_ = function (eq, key, effect) { - var memoizedKey = exports.useMemo_(eq, key); +exports.useLayoutEffect_ = function (eq, deps, effect) { + var memoizedKey = exports.useMemo_(eq, deps); React.useLayoutEffect(effect, [memoizedKey]); }; @@ -66,8 +66,8 @@ exports.useMemo_ = function (eq, a) { return memoRef.current; }; -exports.useLazy_ = function (eq, key, computeA) { - var memoizedKey = exports.useMemo_(eq, key); +exports.useLazy_ = function (eq, deps, computeA) { + var memoizedKey = exports.useMemo_(eq, deps); return React.useMemo(computeA, [memoizedKey]); }; diff --git a/src/React/Basic/Hooks.purs b/src/React/Basic/Hooks.purs index 941a1bc..8b30a0b 100644 --- a/src/React/Basic/Hooks.purs +++ b/src/React/Basic/Hooks.purs @@ -129,8 +129,8 @@ reactChildrenFromArray :: forall a. Array a -> ReactChildren a reactChildrenFromArray = unsafeCoerce -- | Prevents a component from re-rendering if its new props are referentially --- | equal to its old props (not value-based equality. This is due to the underlying --- | React implementation. +-- | equal to its old props (not value-based equality -- this is due to the underlying +-- | React implementation). memo :: forall props. Effect (ReactComponent props) -> diff --git a/src/React/Basic/Hooks/Aff.purs b/src/React/Basic/Hooks/Aff.purs index f955412..14d7973 100644 --- a/src/React/Basic/Hooks/Aff.purs +++ b/src/React/Basic/Hooks/Aff.purs @@ -1,42 +1,50 @@ -module React.Basic.Hooks.Aff where +module React.Basic.Hooks.Aff + ( useAff + , UseAff + ) where import Prelude +import Data.Either (Either(..)) import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype) -import Effect.Aff (Aff, error, killFiber, launchAff, launchAff_) +import Effect.Aff (Aff, Error, error, killFiber, launchAff, launchAff_, throwError, try) import Effect.Class (liftEffect) -import React.Basic.Hooks (Hook, UseEffect, UseState, coerceHook, useEffect, useState, (/\)) +import React.Basic.Hooks (Hook, UseEffect, UseState, coerceHook, unsafeRenderEffect, useEffect, useState, (/\)) import React.Basic.Hooks as React -newtype UseAff key a hooks - = UseAff (UseEffect key (UseState (Maybe a) hooks)) - -derive instance ntUseAff :: Newtype (UseAff key a hooks) _ - -- | `useAff` is used for asynchronous effects or `Aff`. The asynchronous effect --- | is re-run whenever the key changes. If another `Aff` runs when the key --- | changes before the previous async resolves, it will cancel the previous +-- | is re-run whenever the deps change. If another `Aff` runs when the deps +-- | change before the previous async resolves, it will cancel the previous -- | in-flight effect. -- | --- | *Note: Aff failures are thrown. If you need to capture an error state, be --- | sure to capture it in your data type!* +-- | *Note: This hook requires parent components to handle error states! Don't +-- | forget to implement a React error boundary or avoid `Aff` errors entirely +-- | by incorporating them into your result type!* useAff :: - forall key a. - Eq key => - key -> + forall deps a. + Eq deps => + deps -> Aff a -> - Hook (UseAff key a) (Maybe a) -useAff key aff = + Hook (UseAff deps a) (Maybe a) +useAff deps aff = coerceHook React.do result /\ setResult <- useState Nothing - useEffect key do + useEffect deps do setResult (const Nothing) fiber <- launchAff do - r <- aff + r <- try aff liftEffect do setResult \_ -> Just r pure do launchAff_ do - killFiber (error "Effect hook discarded.") fiber - pure result + killFiber (error "Fiber cancelled for newer request") fiber + unsafeRenderEffect case result of + Just (Left err) -> throwError err + Just (Right a) -> pure (Just a) + Nothing -> pure Nothing + +newtype UseAff deps a hooks + = UseAff (UseEffect deps (UseState (Maybe (Either Error a)) hooks)) + +derive instance ntUseAff :: Newtype (UseAff deps a hooks) _ diff --git a/src/React/Basic/Hooks/ErrorBoundary.js b/src/React/Basic/Hooks/ErrorBoundary.js new file mode 100644 index 0000000..07dd22d --- /dev/null +++ b/src/React/Basic/Hooks/ErrorBoundary.js @@ -0,0 +1,21 @@ +"use strict"; + +const React = require("react"); + +exports.errorBoundary_ = (name) => () => { + class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { error: null }; + } + render() { + return this.props.render({ + error: this.state.error, + dismissError: () => this.setState({ error: null }) + }); + } + } + ErrorBoundary.displayName = name; + ErrorBoundary.getDerivedStateFromError = (error) => ({ error }); + return ErrorBoundary; +}; diff --git a/src/React/Basic/Hooks/ErrorBoundary.purs b/src/React/Basic/Hooks/ErrorBoundary.purs new file mode 100644 index 0000000..7a156c7 --- /dev/null +++ b/src/React/Basic/Hooks/ErrorBoundary.purs @@ -0,0 +1,41 @@ +module React.Basic.Hooks.ErrorBoundary + ( mkErrorBoundary + ) where + +import Prelude +import Data.Maybe (Maybe) +import Data.Nullable (Nullable, toMaybe) +import Effect (Effect) +import Effect.Aff (Error) +import React.Basic.Hooks (JSX, ReactComponent, element) + +-- | Create a React error boundary with the given name. The resulting +-- | component takes a render callback which exposes the error if one +-- | exists and an effect for dismissing the error. +mkErrorBoundary :: + String -> + Effect + ( ({ error :: Maybe Error, dismissError :: Effect Unit } -> JSX) -> + JSX + ) +mkErrorBoundary name = do + c <- errorBoundary_ name + pure $ element c <<< mapProps + where + mapProps render = + { render: + \{ error, dismissError } -> + render { error: toMaybe error, dismissError } + } + +foreign import errorBoundary_ :: + String -> + Effect + ( ReactComponent + { render :: + { error :: Nullable Error + , dismissError :: Effect Unit + } -> + JSX + } + ) diff --git a/src/React/Basic/Hooks/Internal.purs b/src/React/Basic/Hooks/Internal.purs index 8d42cd9..cc3279d 100644 --- a/src/React/Basic/Hooks/Internal.purs +++ b/src/React/Basic/Hooks/Internal.purs @@ -44,11 +44,11 @@ newtype Render x y a -- | to the full original type and `UseAff` is never seen: -- | -- | ```purs --- | type UseAff key a hooks --- | = UseEffect key (UseState (Result a) hooks) +-- | type UseAff deps a hooks +-- | = UseEffect deps (UseState (Result a) hooks) -- | --- | useAff :: ... -> Hook (UseAff key a) (Result a) --- | useAff key aff = React.do +-- | useAff :: ... -> Hook (UseAff deps a) (Result a) +-- | useAff deps aff = React.do -- | ... -- | ``` -- | @@ -56,13 +56,13 @@ newtype Render x y a -- | instead, hiding the internal implementation: -- | -- | ```purs --- | newtype UseAff key a hooks --- | = UseAff (UseEffect key (UseState (Result a) hooks)) +-- | newtype UseAff deps a hooks +-- | = UseAff (UseEffect deps (UseState (Result a) hooks)) -- | --- | derive instance ntUseAff :: Newtype (UseAff key a hooks) _ +-- | derive instance ntUseAff :: Newtype (UseAff deps a hooks) _ -- | --- | useAff :: ... -> Hook (UseAff key a) (Result a) --- | useAff key aff = coerceHook React.do +-- | useAff :: ... -> Hook (UseAff deps a) (Result a) +-- | useAff deps aff = coerceHook React.do -- | ... -- | ``` -- | @@ -88,8 +88,8 @@ coerceHook (Render a) = Render a -- | type (the `newHook` type variable used here) _MUST_ -- | contain all relevant types. For example, `UseState` -- | has a phantom type to track the type of the value contained. --- | `useEffect` tracks the type used as the key. `useAff` tracks --- | both the key and the resulting response's type. Forgetting +-- | `useEffect` tracks the type used as the deps. `useAff` tracks +-- | both the deps and the resulting response's type. Forgetting -- | to do this allows the consumer to reorder hook effects. If -- | `useState` didn't track the return type the following -- | extremely unsafe code would be allowed: diff --git a/src/React/Basic/Hooks/ResetToken.purs b/src/React/Basic/Hooks/ResetToken.purs index d56d6ef..2f854a0 100644 --- a/src/React/Basic/Hooks/ResetToken.purs +++ b/src/React/Basic/Hooks/ResetToken.purs @@ -5,26 +5,37 @@ module React.Basic.Hooks.ResetToken ) where import Prelude - +import Data.Newtype (class Newtype) import Effect (Effect) -import Effect.Unsafe (unsafePerformEffect) -import React.Basic.Hooks (Hook, UseState, type (/\), useState, (/\)) +import React.Basic.Hooks (type (/\), Hook, UseState, coerceHook, unsafeRenderEffect, useState, (/\)) import React.Basic.Hooks as React import Unsafe.Coerce (unsafeCoerce) import Unsafe.Reference (unsafeRefEq) -type UseResetToken = UseState ResetToken - --- Useful for resetting effects or component state in place of a --- real key, i.e. a "reset" or "force rerender" button. +-- | Useful for resetting effects or component state. A `ResetToken` can be +-- | used alongside other hook dependencies to force a reevaluation of +-- | whatever depends on those dependencies. +-- | +-- | For example, an effect or API call which depends on the state of a +-- | search bar with filters. You may want a button in the UI either for UX +-- | reasons or to refresh possibly stale data. In this case you would +-- | include a `ResetToken` in your search effect/aff's dependencies and +-- | call run `useResetToken`'s reset effect in the button's `onClick`. useResetToken :: Hook UseResetToken (ResetToken /\ (Effect Unit)) -useResetToken = React.do - resetToken /\ setResetToken <- useState initialResetToken - let - reset = do - resetToken' <- createResetToken - setResetToken \_ -> resetToken' - pure (resetToken /\ reset) +useResetToken = + coerceHook React.do + initialResetToken <- unsafeRenderEffect createResetToken + resetToken /\ setResetToken <- useState initialResetToken + let + reset = do + resetToken' <- createResetToken + setResetToken \_ -> resetToken' + pure (resetToken /\ reset) + +newtype UseResetToken hooks + = UseResetToken (UseState ResetToken hooks) + +derive instance ntUseResetToken :: Newtype (UseResetToken hooks) _ foreign import data ResetToken :: Type @@ -33,6 +44,3 @@ instance eqResetToken :: Eq ResetToken where createResetToken :: Effect ResetToken createResetToken = unsafeCoerce \_ -> {} - -initialResetToken :: ResetToken -initialResetToken = unsafePerformEffect createResetToken \ No newline at end of file diff --git a/src/React/Basic/Hooks/Suspense.js b/src/React/Basic/Hooks/Suspense.js new file mode 100644 index 0000000..f622173 --- /dev/null +++ b/src/React/Basic/Hooks/Suspense.js @@ -0,0 +1,5 @@ +"use strict"; + +const React = require("react"); + +exports.suspense_ = React.Suspense; diff --git a/src/React/Basic/Hooks/Suspense.purs b/src/React/Basic/Hooks/Suspense.purs new file mode 100644 index 0000000..4d49aaf --- /dev/null +++ b/src/React/Basic/Hooks/Suspense.purs @@ -0,0 +1,58 @@ +module React.Basic.Hooks.Suspense + ( suspend + , Suspended(..) + , SuspenseResult(..) + , suspense + ) where + +import Prelude +import Control.Promise (Promise) +import Control.Promise as Promise +import Effect (Effect) +import Effect.Aff (Error, Fiber, joinFiber, throwError) +import React.Basic.Hooks (JSX, Pure, ReactComponent, element, unsafeRenderEffect) +import Unsafe.Coerce (unsafeCoerce) + +-- | Suspend rendering until a result exists. +-- | +-- | *Note: Error and loading states are thrown to React! Don't forget +-- | to implement a React error boundary and ensure `suspend` is +-- | only called from a child of at least one `suspense` parent!* +-- | +-- | *Note: You probably shouldn't be using this function directly. It's +-- | primarily for library authors to build abstractions on top of, as +-- | it requires things like caching mechanisms external to the +-- | component tree.* +-- | +-- | *Warning: React's Suspense API is still experimental. It requires +-- | some manual setup as well as specific versions of React. The API +-- | is also not final and these functions may change.* +suspend :: forall a. Suspended a -> Pure a +suspend (Suspended e) = React.do + unsafeRenderEffect do + result <- e + case result of + InProgress fiber -> + unsafeThrowPromise + =<< Promise.fromAff (joinFiber fiber) + Failed err -> throwError err + Complete a -> pure a + +newtype Suspended a + = Suspended (Effect (SuspenseResult a)) + +data SuspenseResult a + = InProgress (Fiber a) + | Failed Error + | Complete a + +suspense :: { fallback :: JSX, children :: Array JSX } -> JSX +suspense = element suspense_ + +foreign import suspense_ :: ReactComponent { children :: Array JSX, fallback :: JSX } + +-- | Dangerously throw a `Promise` as though it were an `Error`. +-- | React's Suspense API catches thrown `Promise`s and suspends +-- | rendering until they complete. +unsafeThrowPromise :: forall a. Promise a -> Effect a +unsafeThrowPromise = throwError <<< (unsafeCoerce :: Promise a -> Error) diff --git a/src/React/Basic/Hooks/Suspense/Store.purs b/src/React/Basic/Hooks/Suspense/Store.purs new file mode 100644 index 0000000..d0056af --- /dev/null +++ b/src/React/Basic/Hooks/Suspense/Store.purs @@ -0,0 +1,101 @@ +module React.Basic.Hooks.Suspense.Store + ( mkSuspenseStore + , SuspenseStore + , get + , get' + ) where + +import Prelude +import Control.Alt ((<|>)) +import Data.DateTime.Instant (Instant, unInstant) +import Data.Either (Either(..)) +import Data.Map (Map) +import Data.Map as Map +import Data.Maybe (Maybe(..)) +import Data.Time.Duration (Milliseconds) +import Effect (Effect) +import Effect.Aff (Aff, attempt, launchAff, throwError) +import Effect.Class (liftEffect) +import Effect.Now (now) +import Effect.Ref (Ref) +import Effect.Ref as Ref +import React.Basic.Hooks (type (/\), (/\)) +import React.Basic.Hooks.Suspense (Suspended(..), SuspenseResult(..)) + +-- | Simple key-based cache. +mkSuspenseStore :: + forall k v. + Ord k => + Maybe Milliseconds -> + (k -> Aff v) -> + Effect (SuspenseStore k v) +mkSuspenseStore defaultMaxAge backend = do + ref <- Ref.new mempty + let + tryFromCache itemMaxAge k = do + rMaybe <- Map.lookup k <$> Ref.read ref + case rMaybe of + Nothing -> pure Nothing + Just (r /\ d) -> do + case itemMaxAge <|> defaultMaxAge of + Nothing -> pure (Just r) + Just maxAge -> do + now' <- now + if unInstant now' < unInstant d <> maxAge then + pure (Just r) + else + pure Nothing + + getCacheOrBackend itemMaxAge k = do + c <- tryFromCache itemMaxAge k + case c of + Just v -> pure v + Nothing -> do + fiber <- + launchAff do + r <- attempt do backend k + liftEffect do + let + v = case r of + Left e -> Failed e + Right v' -> Complete v' + d <- now + _ <- + ref + # Ref.modify + ( k + # Map.alter case _ of + Nothing -> Just (v /\ d) + Just r'@(v' /\ d') -> + if d > d' then + Just (v /\ d) + else + Just r' + ) + case r of + Left e -> throwError e + Right v' -> pure v' + let + v = InProgress fiber + d <- now + _ <- ref # Ref.modify (Map.insert k (v /\ d)) + pure v + pure + $ SuspenseStore + { cache: ref + , get: \k -> Suspended do getCacheOrBackend Nothing k + , get': \d k -> Suspended do getCacheOrBackend (Just d) k + } + +newtype SuspenseStore k v + = SuspenseStore + { cache :: Ref (Map k (SuspenseResult v /\ Instant)) + , get :: k -> Suspended v + , get' :: Milliseconds -> k -> Suspended v + } + +get :: forall k v. SuspenseStore k v -> k -> Suspended v +get (SuspenseStore s) = s.get + +get' :: forall k v. SuspenseStore k v -> Milliseconds -> k -> Suspended v +get' (SuspenseStore s) = s.get' diff --git a/src/React/Basic/StrictMode.js b/src/React/Basic/StrictMode.js new file mode 100644 index 0000000..c0b58ae --- /dev/null +++ b/src/React/Basic/StrictMode.js @@ -0,0 +1,5 @@ +"use strict"; + +const React = require("react"); + +exports.strictMode_ = React.StrictMode; diff --git a/src/React/Basic/StrictMode.purs b/src/React/Basic/StrictMode.purs new file mode 100644 index 0000000..88abb65 --- /dev/null +++ b/src/React/Basic/StrictMode.purs @@ -0,0 +1,10 @@ +-- TODO: move this file to `react-basic` +module React.Basic.StrictMode where + +import Prelude +import React.Basic (JSX, ReactComponent, element) + +strictMode :: JSX -> JSX +strictMode = element strictMode_ <<< { children: _ } + +foreign import strictMode_ :: ReactComponent { children :: JSX } From 61ce4a4bc739090bd0bdca3ce9850f1fae15f202 Mon Sep 17 00:00:00 2001 From: Madeline Trotter Date: Thu, 9 Apr 2020 00:43:19 -0700 Subject: [PATCH 4/4] Optimize `component` for less boilerplate The existing functions have been renamed to `reactComponent`, etc --- examples/aff/src/Example.purs | 28 ++++++------ examples/aff/src/Main.purs | 5 +-- examples/component/src/Example.purs | 9 ++-- examples/component/src/Main.purs | 5 +-- examples/component/src/ToggleButton.purs | 11 +++-- examples/context/src/Example.purs | 26 +++++------ examples/context/src/Main.purs | 5 +-- examples/controlled-input/src/Example.purs | 7 ++- examples/controlled-input/src/Main.purs | 5 +-- examples/counter/src/Example.js | 7 --- examples/counter/src/Example.purs | 12 +++-- examples/counter/src/Main.purs | 5 +-- examples/memo-callback/src/Example.purs | 7 ++- examples/memo-callback/src/Main.purs | 5 +-- .../src/{Reducer.purs => Example.purs} | 7 ++- examples/reducer/src/Main.purs | 5 +-- examples/refs/src/Example.purs | 7 ++- examples/refs/src/Main.purs | 5 +-- examples/suspense/src/Example.purs | 26 +++++------ examples/suspense/src/Main.purs | 5 +-- examples/todo-app/src/Example.purs | 25 ++++++----- examples/todo-app/src/Main.purs | 5 +-- src/React/Basic/Hooks.purs | 44 ++++++++++++++----- 23 files changed, 118 insertions(+), 148 deletions(-) delete mode 100644 examples/counter/src/Example.js rename examples/reducer/src/{Reducer.purs => Example.purs} (81%) diff --git a/examples/aff/src/Example.purs b/examples/aff/src/Example.purs index 511da6c..d8c9e75 100644 --- a/examples/aff/src/Example.purs +++ b/examples/aff/src/Example.purs @@ -4,17 +4,16 @@ import Prelude import Data.Foldable (find) import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype, un) -import Effect (Effect) import Effect.Aff (Aff, Milliseconds(..), delay, error, message, throwError) import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) import React.Basic.Events (handler_) -import React.Basic.Hooks (JSX, ReactComponent, component, element, fragment, useState, (/\)) +import React.Basic.Hooks (Component, component, fragment, useState, (/\)) import React.Basic.Hooks as React import React.Basic.Hooks.Aff (useAff) import React.Basic.Hooks.ErrorBoundary (mkErrorBoundary) -mkExample :: Effect (ReactComponent {}) +mkExample :: Component Unit mkExample = do errorBoundary <- mkErrorBoundary "AffExErrorBoundary" catDetails <- mkCatDetails @@ -32,24 +31,23 @@ mkExample = do [ catKeyList catKey setCatKey , case catKey of Nothing -> mempty - Just k -> catDetails { catKey: k } + Just k -> catDetails k ] ] where -- This component is the main `useAff` demo. It receives a key -- as a prop and renders both the loading state and the final -- result. - mkCatDetails :: Effect ({ catKey :: Key Cat } -> JSX) - mkCatDetails = - map element do - component "CatDetails" \{ catKey } -> React.do - catState <- useAff catKey $ fetch catKey - pure - $ R.p_ - [ case map entity catState of - Nothing -> R.text "Loading..." - Just (Cat { name }) -> R.text $ "A cat named " <> name - ] + mkCatDetails :: Component (Key Cat) + mkCatDetails = do + component "CatDetails" \catKey -> React.do + catState <- useAff catKey $ fetch catKey + pure + $ R.p_ + [ case map entity catState of + Nothing -> R.text "Loading..." + Just (Cat { name }) -> R.text $ "A cat named " <> name + ] renderAppError error resetApp = fragment diff --git a/examples/aff/src/Main.purs b/examples/aff/src/Main.purs index f9c6e57..edc6177 100644 --- a/examples/aff/src/Main.purs +++ b/examples/aff/src/Main.purs @@ -5,7 +5,6 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) import Example (mkExample) -import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -19,6 +18,4 @@ main = do Nothing -> throw "Container element not found." Just c -> do ex <- mkExample - let - app = element ex {} - render app c + render (ex unit) c diff --git a/examples/component/src/Example.purs b/examples/component/src/Example.purs index 53fc09a..b27a0da 100644 --- a/examples/component/src/Example.purs +++ b/examples/component/src/Example.purs @@ -1,12 +1,11 @@ module Example where import Prelude -import Effect (Effect) import React.Basic.DOM as R -import React.Basic.Hooks (ReactComponent, component, element) +import React.Basic.Hooks (Component, component) import ToggleButton (mkToggleButton) -mkExample :: Effect (ReactComponent {}) +mkExample :: Component Unit mkExample = do -- create the child components this parent will use toggleButton <- mkToggleButton @@ -15,7 +14,7 @@ mkExample = do pure $ R.div { children: - [ element toggleButton { label: "A" } - , element toggleButton { label: "B" } + [ toggleButton { label: "A" } + , toggleButton { label: "B" } ] } diff --git a/examples/component/src/Main.purs b/examples/component/src/Main.purs index f9c6e57..edc6177 100644 --- a/examples/component/src/Main.purs +++ b/examples/component/src/Main.purs @@ -5,7 +5,6 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) import Example (mkExample) -import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -19,6 +18,4 @@ main = do Nothing -> throw "Container element not found." Just c -> do ex <- mkExample - let - app = element ex {} - render app c + render (ex unit) c diff --git a/examples/component/src/ToggleButton.purs b/examples/component/src/ToggleButton.purs index 8e4f119..38f5b4c 100644 --- a/examples/component/src/ToggleButton.purs +++ b/examples/component/src/ToggleButton.purs @@ -1,14 +1,13 @@ module ToggleButton where import Prelude -import Effect (Effect) import Effect.Console (log) import React.Basic.DOM as R import React.Basic.Events (handler_) -import React.Basic.Hooks (ReactComponent, component, useEffect, useState, (/\)) +import React.Basic.Hooks (Component, component, useEffect, useState, (/\)) import React.Basic.Hooks as React -mkToggleButton :: Effect (ReactComponent { label :: String }) +mkToggleButton :: Component { label :: String } mkToggleButton = do component "ToggleButton" \{ label } -> React.do on /\ setOn <- useState false @@ -19,7 +18,7 @@ mkToggleButton = do $ R.button { onClick: handler_ $ setOn not , children: - [ R.text label - , R.text if on then " On" else " Off" - ] + [ R.text label + , R.text if on then " On" else " Off" + ] } diff --git a/examples/context/src/Example.purs b/examples/context/src/Example.purs index 72a6aeb..41a1c96 100644 --- a/examples/context/src/Example.purs +++ b/examples/context/src/Example.purs @@ -4,29 +4,27 @@ import Prelude import Effect (Effect) import React.Basic.DOM as R import React.Basic.Events (handler_) -import React.Basic.Hooks (type (/\), ReactComponent, JSX, ReactContext, component, createContext, element, provider, useContext, useState, (/\)) +import React.Basic.Hooks (type (/\), JSX, ReactContext, Component, component, createContext, provider, useContext, useState, (/\)) import React.Basic.Hooks as React -mkExample :: Effect (ReactComponent {}) +mkExample :: Component Unit mkExample = do counterContext <- createContext (0 /\ pure unit) store <- mkStore counterContext counter <- mkCounter counterContext - component "Context" \props -> React.do + component "Context" \_ -> React.do pure - $ element store - { content: - [ element counter {} - , element counter {} - , element counter {} - ] - } + $ store + [ counter unit + , counter unit + , counter unit + ] mkStore :: ReactContext (Int /\ (Effect Unit)) -> - Effect (ReactComponent { content :: Array JSX }) + Component (Array JSX) mkStore context = do - component "Store" \{ content } -> React.do + component "Store" \content -> React.do counter /\ setCounter <- useState 0 let increment = setCounter (_ + 1) @@ -37,9 +35,9 @@ mkStore context = do mkCounter :: ReactContext (Int /\ (Effect Unit)) -> - Effect (ReactComponent {}) + Component Unit mkCounter counterContext = do - component "Counter" \props -> React.do + component "Counter" \_ -> React.do counter /\ increment <- useContext counterContext pure $ R.button diff --git a/examples/context/src/Main.purs b/examples/context/src/Main.purs index f9c6e57..edc6177 100644 --- a/examples/context/src/Main.purs +++ b/examples/context/src/Main.purs @@ -5,7 +5,6 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) import Example (mkExample) -import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -19,6 +18,4 @@ main = do Nothing -> throw "Container element not found." Just c -> do ex <- mkExample - let - app = element ex {} - render app c + render (ex unit) c diff --git a/examples/controlled-input/src/Example.purs b/examples/controlled-input/src/Example.purs index d77c47c..81853a3 100644 --- a/examples/controlled-input/src/Example.purs +++ b/examples/controlled-input/src/Example.purs @@ -2,16 +2,15 @@ module Example where import Prelude import Data.Maybe (Maybe(..), fromMaybe, maybe) -import Effect (Effect) import React.Basic.DOM as R import React.Basic.DOM.Events (preventDefault, stopPropagation, targetValue, timeStamp) import React.Basic.Events (EventHandler, handler, merge) -import React.Basic.Hooks (ReactComponent, UseState, Hook, component, fragment, useState, (/\)) +import React.Basic.Hooks (Component, Hook, UseState, component, fragment, useState, (/\)) import React.Basic.Hooks as React -mkExample :: Effect (ReactComponent {}) +mkExample :: Component Unit mkExample = do - component "ControlledInput" \props -> React.do + component "ControlledInput" \_ -> React.do firstName <- useInput "hello" lastName <- useInput "world" pure diff --git a/examples/controlled-input/src/Main.purs b/examples/controlled-input/src/Main.purs index f9c6e57..edc6177 100644 --- a/examples/controlled-input/src/Main.purs +++ b/examples/controlled-input/src/Main.purs @@ -5,7 +5,6 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) import Example (mkExample) -import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -19,6 +18,4 @@ main = do Nothing -> throw "Container element not found." Just c -> do ex <- mkExample - let - app = element ex {} - render app c + render (ex unit) c diff --git a/examples/counter/src/Example.js b/examples/counter/src/Example.js deleted file mode 100644 index 97f35c0..0000000 --- a/examples/counter/src/Example.js +++ /dev/null @@ -1,7 +0,0 @@ -"use strict"; - -exports.setDocumentTitle = function(title) { - return function() { - document.title = title; - }; -}; diff --git a/examples/counter/src/Example.purs b/examples/counter/src/Example.purs index f92f4c6..ed1cafe 100644 --- a/examples/counter/src/Example.purs +++ b/examples/counter/src/Example.purs @@ -1,18 +1,18 @@ module Example where import Prelude -import Effect (Effect) +import Effect.Class.Console (log) import React.Basic.DOM as R import React.Basic.Events (handler_) -import React.Basic.Hooks (ReactComponent, component, fragment, useEffect, useState, (/\)) +import React.Basic.Hooks (Component, component, fragment, useEffect, useState, (/\)) import React.Basic.Hooks as React -mkExample :: Effect (ReactComponent {}) +mkExample :: Component Unit mkExample = do - component "Counter" \props -> React.do + component "Counter" \_ -> React.do counter /\ setCounter <- useState 0 useEffect counter do - setDocumentTitle $ "Count: " <> show counter + log $ "Count: " <> show counter pure mempty pure $ fragment @@ -21,5 +21,3 @@ mkExample = do , children: [ R.text $ "Increment: " <> show counter ] } ] - -foreign import setDocumentTitle :: String -> Effect Unit diff --git a/examples/counter/src/Main.purs b/examples/counter/src/Main.purs index f9c6e57..edc6177 100644 --- a/examples/counter/src/Main.purs +++ b/examples/counter/src/Main.purs @@ -5,7 +5,6 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) import Example (mkExample) -import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -19,6 +18,4 @@ main = do Nothing -> throw "Container element not found." Just c -> do ex <- mkExample - let - app = element ex {} - render app c + render (ex unit) c diff --git a/examples/memo-callback/src/Example.purs b/examples/memo-callback/src/Example.purs index de04f4d..78cc1a0 100644 --- a/examples/memo-callback/src/Example.purs +++ b/examples/memo-callback/src/Example.purs @@ -4,17 +4,16 @@ import Prelude import Data.Foldable (traverse_) import Data.Int as Int import Data.Maybe (Maybe(..)) -import Effect (Effect) import Effect.Console (log) import React.Basic.DOM as R import React.Basic.DOM.Events (targetValue) import React.Basic.Events (handler, handler_) -import React.Basic.Hooks (ReactComponent, UnsafeReference(..), component, fragment, useEffect, useLazy, useMemo, useState, (/\)) +import React.Basic.Hooks (Component, UnsafeReference(..), component, fragment, useEffect, useLazy, useMemo, useState, (/\)) import React.Basic.Hooks as React -mkExample :: Effect (ReactComponent {}) +mkExample :: Component Unit mkExample = do - component "MemoCallback" \props -> React.do + component "MemoCallback" \_ -> React.do initialValue /\ setInitialValue <- useState 0 counter /\ setCounter <- useState initialValue increment <- diff --git a/examples/memo-callback/src/Main.purs b/examples/memo-callback/src/Main.purs index f9c6e57..edc6177 100644 --- a/examples/memo-callback/src/Main.purs +++ b/examples/memo-callback/src/Main.purs @@ -5,7 +5,6 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) import Example (mkExample) -import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -19,6 +18,4 @@ main = do Nothing -> throw "Container element not found." Just c -> do ex <- mkExample - let - app = element ex {} - render app c + render (ex unit) c diff --git a/examples/reducer/src/Reducer.purs b/examples/reducer/src/Example.purs similarity index 81% rename from examples/reducer/src/Reducer.purs rename to examples/reducer/src/Example.purs index f7084e1..24c0457 100644 --- a/examples/reducer/src/Reducer.purs +++ b/examples/reducer/src/Example.purs @@ -1,19 +1,18 @@ module Example where import Prelude -import Effect (Effect) import React.Basic.DOM as R import React.Basic.Events (handler_) -import React.Basic.Hooks (ReactComponent, component, fragment, useReducer, (/\)) +import React.Basic.Hooks (Component, component, fragment, useReducer, (/\)) import React.Basic.Hooks as React data Action = Increment | Decrement -mkExample :: Effect (ReactComponent {}) +mkExample :: Component Unit mkExample = do - component "Reducer" \props -> React.do + component "Reducer" \_ -> React.do state /\ dispatch <- useReducer { counter: 0 } \state -> case _ of Increment -> state { counter = state.counter + 1 } diff --git a/examples/reducer/src/Main.purs b/examples/reducer/src/Main.purs index f9c6e57..edc6177 100644 --- a/examples/reducer/src/Main.purs +++ b/examples/reducer/src/Main.purs @@ -5,7 +5,6 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) import Example (mkExample) -import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -19,6 +18,4 @@ main = do Nothing -> throw "Container element not found." Just c -> do ex <- mkExample - let - app = element ex {} - render app c + render (ex unit) c diff --git a/examples/refs/src/Example.purs b/examples/refs/src/Example.purs index 34f9dd1..928c51a 100644 --- a/examples/refs/src/Example.purs +++ b/examples/refs/src/Example.purs @@ -5,10 +5,9 @@ import Data.Int (round) import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype) import Data.Nullable (Nullable, null) -import Effect (Effect) import Math (pow, sqrt) import React.Basic.DOM as R -import React.Basic.Hooks (type (/\), Hook, ReactComponent, Ref, UseEffect, UseRef, UseState, component, fragment, coerceHook, readRefMaybe, useEffect, useRef, useState, (/\)) +import React.Basic.Hooks (type (/\), Component, Hook, Ref, UseEffect, UseRef, UseState, coerceHook, component, fragment, readRefMaybe, useEffect, useRef, useState, (/\)) import React.Basic.Hooks as React import Unsafe.Coerce (unsafeCoerce) import Web.DOM (Node) @@ -19,9 +18,9 @@ import Web.HTML.HTMLElement (getBoundingClientRect) import Web.HTML.HTMLElement as HTMLElement import Web.HTML.Window as Window -mkExample :: Effect (ReactComponent {}) +mkExample :: Component Unit mkExample = do - component "Refs" \props -> React.do + component "Refs" \_ -> React.do mouseDistance1 /\ buttonRef1 <- useNodeDistanceFromMouse mouseDistance2 /\ buttonRef2 <- useNodeDistanceFromMouse mouseDistance3 /\ buttonRef3 <- useNodeDistanceFromMouse diff --git a/examples/refs/src/Main.purs b/examples/refs/src/Main.purs index f9c6e57..edc6177 100644 --- a/examples/refs/src/Main.purs +++ b/examples/refs/src/Main.purs @@ -5,7 +5,6 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) import Example (mkExample) -import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -19,6 +18,4 @@ main = do Nothing -> throw "Container element not found." Just c -> do ex <- mkExample - let - app = element ex {} - render app c + render (ex unit) c diff --git a/examples/suspense/src/Example.purs b/examples/suspense/src/Example.purs index a8298ae..29d82d5 100644 --- a/examples/suspense/src/Example.purs +++ b/examples/suspense/src/Example.purs @@ -5,22 +5,21 @@ import Data.Foldable (find) import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype, un) import Data.Time.Duration (Seconds(..), fromDuration) -import Effect (Effect) import Effect.Aff (Aff, Milliseconds(..), delay, error, message, throwError) import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) import React.Basic.Events (handler_) -import React.Basic.Hooks (JSX, ReactComponent, component, element, fragment, useState, (/\)) +import React.Basic.Hooks (Component, component, fragment, useState, (/\)) import React.Basic.Hooks as React import React.Basic.Hooks.ErrorBoundary (mkErrorBoundary) import React.Basic.Hooks.Suspense (suspend, suspense) import React.Basic.Hooks.Suspense.Store (SuspenseStore, get, mkSuspenseStore) -mkExample :: Effect (ReactComponent {}) +mkExample :: Component Unit mkExample = do errorBoundary <- mkErrorBoundary "SuspenseExErrorBoundary" catDetails <- mkCatDetails - component "SuspenseEx" \props -> React.do + component "SuspenseEx" \_ -> React.do catKey /\ setCatKey <- useState Nothing let reset = setCatKey \_ -> Nothing @@ -38,7 +37,7 @@ mkExample = do R.p_ [ suspense { fallback: R.text "Loading..." - , children: [ catDetails { catKey: k } ] + , children: [ catDetails k ] } ] ] @@ -47,18 +46,17 @@ mkExample = do -- This component is the main `suspense` demo (but don't forget the `suspense` -- element above!). It receives a key as a prop and renders the result as though -- it were synchronously available. - mkCatDetails :: Effect ({ catKey :: Key Cat } -> JSX) + mkCatDetails :: Component (Key Cat) mkCatDetails = do catStore :: SuspenseStore (Key Cat) _ <- mkSuspenseStore (Just $ fromDuration $ Seconds 10.0) fetch - element - <$> component "CatDetails" \{ catKey } -> React.do - cat <- suspend $ get catStore catKey - pure - $ R.p_ - [ case entity cat of - Cat { name } -> R.text $ "A cat named " <> name - ] + component "CatDetails" \catKey -> React.do + cat <- suspend $ get catStore catKey + pure + $ R.p_ + [ case entity cat of + Cat { name } -> R.text $ "A cat named " <> name + ] renderAppError error resetApp = fragment diff --git a/examples/suspense/src/Main.purs b/examples/suspense/src/Main.purs index f9c6e57..edc6177 100644 --- a/examples/suspense/src/Main.purs +++ b/examples/suspense/src/Main.purs @@ -5,7 +5,6 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) import Example (mkExample) -import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -19,6 +18,4 @@ main = do Nothing -> throw "Container element not found." Just c -> do ex <- mkExample - let - app = element ex {} - render app c + render (ex unit) c diff --git a/examples/todo-app/src/Example.purs b/examples/todo-app/src/Example.purs index 9e913db..a13c087 100644 --- a/examples/todo-app/src/Example.purs +++ b/examples/todo-app/src/Example.purs @@ -5,12 +5,13 @@ import Data.Array as Array import Data.Foldable (traverse_) import Data.Maybe (Maybe(..)) import Effect (Effect) +import React.Basic (keyed) import React.Basic as RB import React.Basic.DOM as R import React.Basic.DOM.Events (preventDefault, stopPropagation, targetValue) import React.Basic.Events (handler, handler_) import React.Basic.Events as Events -import React.Basic.Hooks (ReactComponent, component, element, elementKeyed, empty, memo, useReducer, useState, (/\)) +import React.Basic.Hooks (Component, component, empty, useReducer, useState, (/\)) import React.Basic.Hooks as React data Action @@ -47,28 +48,28 @@ reducer state = case _ of Nothing -> state SetFilter filter -> state { filter = filter } -mkExample :: Effect (ReactComponent {}) +mkExample :: Component Unit mkExample = do let initialState = { todos: [], filter: All } - todoInput <- memo mkTodoInput - todoRow <- memo mkTodoRow - todoFilters <- memo mkTodoFilters - component "TodoApp" \props -> React.do + todoInput <- mkTodoInput + todoRow <- mkTodoRow + todoFilters <- mkTodoFilters + component "TodoApp" \_ -> React.do state /\ dispatch <- useReducer initialState reducer pure $ R.div { children: - [ element todoInput { dispatch } + [ todoInput { dispatch } , R.div_ $ flip Array.mapWithIndex state.todos \id todo -> if state.filter == All || (todo.isComplete && state.filter == Complete) || (not todo.isComplete && state.filter == Incomplete) then - elementKeyed todoRow { key: show id, id, todo, dispatch } + keyed (show id) $ todoRow { id, todo, dispatch } else empty - , element todoFilters { filter: state.filter, dispatch } + , todoFilters { filter: state.filter, dispatch } ] , style: R.css @@ -82,7 +83,7 @@ mkExample = do where todoAppEl = RB.element $ R.unsafeCreateDOMComponent "todo-app" -mkTodoInput :: Effect (ReactComponent { dispatch :: Action -> Effect Unit }) +mkTodoInput :: Component { dispatch :: Action -> Effect Unit } mkTodoInput = do component "TodoInput" \props -> React.do value /\ setValue <- useState "" @@ -105,7 +106,7 @@ mkTodoInput = do , style: R.css { marginBottom: "16px", width: "100%" } } -mkTodoRow :: Effect (ReactComponent { id :: Int, todo :: Todo, dispatch :: Action -> Effect Unit }) +mkTodoRow :: Component { id :: Int, todo :: Todo, dispatch :: Action -> Effect Unit } mkTodoRow = component "Todo" \props -> React.do pure @@ -137,7 +138,7 @@ mkTodoRow = } } -mkTodoFilters :: Effect (ReactComponent { filter :: TodoFilter, dispatch :: Action -> Effect Unit }) +mkTodoFilters :: Component { filter :: TodoFilter, dispatch :: Action -> Effect Unit } mkTodoFilters = component "TodoFilters" \props -> React.do let diff --git a/examples/todo-app/src/Main.purs b/examples/todo-app/src/Main.purs index f9c6e57..edc6177 100644 --- a/examples/todo-app/src/Main.purs +++ b/examples/todo-app/src/Main.purs @@ -5,7 +5,6 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) import Example (mkExample) -import React.Basic.Hooks (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -19,6 +18,4 @@ main = do Nothing -> throw "Container element not found." Just c -> do ex <- mkExample - let - app = element ex {} - render app c + render (ex unit) c diff --git a/src/React/Basic/Hooks.purs b/src/React/Basic/Hooks.purs index 8b30a0b..d060486 100644 --- a/src/React/Basic/Hooks.purs +++ b/src/React/Basic/Hooks.purs @@ -1,7 +1,9 @@ module React.Basic.Hooks - ( component - , componentWithChildren - , componentFromHook + ( Component + , component + , reactComponent + , reactComponentWithChildren + , reactComponentFromHook , ReactChildren , memo , useState @@ -52,13 +54,31 @@ import React.Basic.Hooks.Internal (Hook, Pure, Render, bind, discard, coerceHook import Unsafe.Coerce (unsafeCoerce) import Unsafe.Reference (unsafeRefEq) +-- | A simple type alias to clean up component definitions. +type Component props + = Effect (props -> JSX) + +-- | Create a component function given a display name and render function. +-- | Creating components is effectful because React uses the function +-- | instance as the component's "identity" or "type". Components should +-- | be created during a bootstrap phase and not within component +-- | lifecycles or render functions. +component :: + forall hooks props. + String -> + (props -> Render Unit hooks JSX) -> + Component props +component name renderFn = Prelude.do + c <- reactComponent name (renderFn <<< _.nested) + pure (element c <<< { nested: _ }) + -- | Create a React component given a display name and render function. -- | Creating components is effectful because React uses the function -- | instance as the component's "identity" or "type". Components should -- | be created during a bootstrap phase and not within component -- | lifecycles or render functions. See `componentWithChildren` if -- | you need to use the `children` prop. -component :: +reactComponent :: forall hooks props. Lacks "children" props => Lacks "key" props => @@ -66,26 +86,26 @@ component :: String -> ({ | props } -> Render Unit hooks JSX) -> Effect (ReactComponent { | props }) -component = unsafeComponent +reactComponent = unsafeReactComponent -- | Create a React component given a display name and render function. -- | This is the same as `component` but allows the use of the `children` -- | prop. -componentWithChildren :: +reactComponentWithChildren :: forall hooks props children. Lacks "key" props => Lacks "ref" props => String -> ({ children :: ReactChildren children | props } -> Render Unit hooks JSX) -> Effect (ReactComponent { children :: ReactChildren children | props }) -componentWithChildren = unsafeComponent +reactComponentWithChildren = unsafeReactComponent -- | Convert a hook to a render-prop component. The value returned from the -- | hook will be passed to the `render` prop, a function from that value -- | to `JSX`. -- | -- | This function is useful for consuming a hook within a non-hook component. -componentFromHook :: +reactComponentFromHook :: forall hooks props r. Lacks "children" props => Lacks "key" props => @@ -93,17 +113,17 @@ componentFromHook :: String -> ({ render :: r -> JSX | props } -> Hook hooks r) -> Effect (ReactComponent { render :: r -> JSX | props }) -componentFromHook name propsToHook = do - component name \props -> map props.render $ propsToHook props +reactComponentFromHook name propsToHook = do + reactComponent name \props -> map props.render $ propsToHook props -unsafeComponent :: +unsafeReactComponent :: forall hooks props. Lacks "key" props => Lacks "ref" props => String -> ({ | props } -> Render Unit hooks JSX) -> Effect (ReactComponent { | props }) -unsafeComponent name renderFn = +unsafeReactComponent name renderFn = let c = unsafeReactFunctionComponent