diff --git a/bower.json b/bower.json index 5847ab0..112c983 100644 --- a/bower.json +++ b/bower.json @@ -9,6 +9,9 @@ "dependencies": { "purescript-functions": "^3.0.0", "purescript-eff": "^3.1.0", - "purescript-unsafe-coerce": "^3.0.0" + "purescript-unsafe-coerce": "^3.0.0", + "purescript-nullable": "^3.0.0", + "purescript-typelevel-prelude": "^2.6.0", + "purescript-record": "^0.2.6" } } diff --git a/examples/controlled-input/.gitignore b/examples/controlled-input/.gitignore new file mode 100644 index 0000000..645684d --- /dev/null +++ b/examples/controlled-input/.gitignore @@ -0,0 +1,4 @@ +output +html/index.js +package-lock.json +node_modules diff --git a/examples/controlled-input/Makefile b/examples/controlled-input/Makefile new file mode 100644 index 0000000..f266443 --- /dev/null +++ b/examples/controlled-input/Makefile @@ -0,0 +1,5 @@ +all: + purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs' + purs bundle --module ControlledInput output/*/*.js > output/bundle.js + echo 'module.exports = PS.ControlledInput;' >> output/bundle.js + node_modules/browserify/bin/cmd.js output/bundle.js index.js -o html/index.js diff --git a/examples/controlled-input/README.md b/examples/controlled-input/README.md new file mode 100644 index 0000000..2004b87 --- /dev/null +++ b/examples/controlled-input/README.md @@ -0,0 +1,12 @@ +# Controlled Input Example + +## Building + +``` +npm install +make all +``` + +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/html/index.html b/examples/controlled-input/html/index.html new file mode 100644 index 0000000..6b93b7c --- /dev/null +++ b/examples/controlled-input/html/index.html @@ -0,0 +1,10 @@ + + + + react-basic example + + +
+ + + diff --git a/examples/controlled-input/index.js b/examples/controlled-input/index.js new file mode 100644 index 0000000..cbf31c0 --- /dev/null +++ b/examples/controlled-input/index.js @@ -0,0 +1,10 @@ +"use strict"; + +var React = require("react"); +var ReactDOM = require("react-dom"); +var ControlledInput = require("./output/bundle.js"); + +ReactDOM.render( + React.createElement(ControlledInput.component), + document.getElementById("container") +); diff --git a/examples/controlled-input/package.json b/examples/controlled-input/package.json new file mode 100644 index 0000000..0750eb8 --- /dev/null +++ b/examples/controlled-input/package.json @@ -0,0 +1,15 @@ +{ + "name": "counter", + "version": "1.0.0", + "description": "", + "keywords": [], + "author": "", + "dependencies": { + "create-react-class": "^15.6.2", + "react": "^16.2.0", + "react-dom": "^16.2.0" + }, + "devDependencies": { + "browserify": "^16.1.0" + } +} diff --git a/examples/controlled-input/src/ControlledInput.purs b/examples/controlled-input/src/ControlledInput.purs new file mode 100644 index 0000000..2a36b73 --- /dev/null +++ b/examples/controlled-input/src/ControlledInput.purs @@ -0,0 +1,29 @@ +module ControlledInput where + +import Prelude + +import Data.Maybe (Maybe(..), fromMaybe, maybe) +import Data.Nullable (toMaybe) +import React.Basic (ReactComponent, react) +import React.Basic.DOM as R +import React.Basic.DOM.Events (targetValue, timeStamp) +import React.Basic.DOM.Events as Events + +component :: ReactComponent {} +component = react + { displayName: "Counter" + , initialState: { value: "hello world", timeStamp: Nothing } + , receiveProps: \_ _ _ -> pure unit + , render: \_ state setState -> + R.div_ + [ R.p_ [ R.input { onChange: Events.handler (Events.preventDefault >>> Events.merge { targetValue, timeStamp }) \{ timeStamp, targetValue } -> + setState \_ -> { value: fromMaybe "" (toMaybe targetValue) + , timeStamp: Just timeStamp + } + , value: state.value + } + ] + , R.p_ [ R.text ("Current value = " <> show state.value) ] + , R.p_ (maybe [] (\ts -> [R.text ("Changed at: " <> show ts)]) state.timeStamp) + ] + } diff --git a/generated-docs/React/Basic.md b/generated-docs/React/Basic.md index 8a71233..7908205 100644 --- a/generated-docs/React/Basic.md +++ b/generated-docs/React/Basic.md @@ -3,7 +3,7 @@ #### `react` ``` purescript -react :: forall props state fx. { displayName :: String, initialState :: { | state }, receiveProps :: props -> { | state } -> (SetState state fx) -> Eff (react :: ReactFX | fx) Unit, render :: props -> { | state } -> (SetState state fx) -> JSX } -> ReactComponent props +react :: forall props state fx. { displayName :: String, initialState :: { | state }, receiveProps :: { | props } -> { | state } -> (SetState state fx) -> Eff (react :: ReactFX | fx) Unit, render :: { | props } -> { | state } -> (SetState state fx) -> JSX } -> ReactComponent { | props } ``` Create a React component from a _specification_ of that component. @@ -19,7 +19,7 @@ module (and re-exported here). #### `stateless` ``` purescript -stateless :: forall props. { displayName :: String, render :: props -> JSX } -> ReactComponent props +stateless :: forall props. { displayName :: String, render :: { | props } -> JSX } -> ReactComponent { | props } ``` Create a stateless React component. @@ -30,7 +30,7 @@ components which don't use state. #### `createElement` ``` purescript -createElement :: forall props. ReactComponent props -> props -> JSX +createElement :: forall props. ReactComponent { | props } -> { | props } -> JSX ``` Create a `JSX` node from a React component, by providing the props. @@ -63,33 +63,6 @@ Render an Array of children without a wrapping component. Provide a key when dynamically rendering multiple fragments along side each other. - -### Re-exported from React.Basic.Types: - -#### `SyntheticEvent` - -``` purescript -type SyntheticEvent = { bubbles :: Boolean, cancelable :: Boolean, currentTarget :: DOMNode, defaultPrevented :: Boolean, eventPhase :: Number, isTrusted :: Boolean, target :: DOMNode, timeStamp :: Number, "type" :: String } -``` - -Event data that we receive from React. - -#### `ReactFX` - -``` purescript -data ReactFX :: Effect -``` - -A placeholder effect for all React FFI. - -#### `ReactComponent` - -``` purescript -data ReactComponent :: Type -> Type -``` - -A React component which can be used from JavaScript. - #### `JSX` ``` purescript @@ -98,28 +71,20 @@ data JSX :: Type A virtual DOM element. -#### `EventHandler` +#### `ReactComponent` ``` purescript -type EventHandler = EffFn1 (react :: ReactFX) SyntheticEvent Unit +data ReactComponent :: Type -> Type ``` -An event handler, which receives a `SyntheticEvent` and performs some -effects in return. +A React component which can be used from JavaScript. -#### `DOMNode` +#### `ReactFX` ``` purescript -data DOMNode :: Type +data ReactFX :: Effect ``` -An _actual_ DOM node (not a virtual DOM element!) - -#### `CSS` - -``` purescript -data CSS :: Type -``` +A placeholder effect for all React FFI. -An abstract type representing records of CSS attributes. diff --git a/generated-docs/React/Basic/DOM.md b/generated-docs/React/Basic/DOM.md index e79d885..3bcf6d6 100644 --- a/generated-docs/React/Basic/DOM.md +++ b/generated-docs/React/Basic/DOM.md @@ -15,6 +15,14 @@ text :: String -> JSX Create a text node. +#### `CSS` + +``` purescript +data CSS :: Type +``` + +An abstract type representing records of CSS attributes. + #### `css` ``` purescript diff --git a/generated-docs/React/Basic/DOM/Events.md b/generated-docs/React/Basic/DOM/Events.md new file mode 100644 index 0000000..0ef5c7c --- /dev/null +++ b/generated-docs/React/Basic/DOM/Events.md @@ -0,0 +1,221 @@ +## Module React.Basic.DOM.Events + +This module defines safe event function and property accessors. + +#### `EventHandler` + +``` purescript +type EventHandler = EffFn1 (react :: ReactFX) SyntheticEvent Unit +``` + +An event handler, which receives a `SyntheticEvent` and performs some +effects in return. + +#### `SyntheticEvent` + +``` purescript +data SyntheticEvent :: Type +``` + +Event data that we receive from React. + +#### `DOMNode` + +``` purescript +data DOMNode :: Type +``` + +An _actual_ DOM node (not a virtual DOM element!) + +#### `DOMEvent` + +``` purescript +data DOMEvent :: Type +``` + +The underlying browser Event. + +#### `EventFn` + +``` purescript +newtype EventFn a b +``` + +Encapsulates a safe event operation. `EventFn`s can be composed +to perform multiple operations. + +For example: + +```purs +input { onChange: handler (preventDefault >>> targetValue) + \value -> setState \_ -> { value } + } +``` + +##### Instances +``` purescript +Semigroupoid EventFn +Category EventFn +(IsSymbol l, RowCons l (EventFn a b) fns_rest fns, RowCons l b r_rest r, RowLacks l fns_rest, RowLacks l r_rest, Merge rest fns_rest a r_rest) => Merge (Cons l (EventFn a b) rest) fns a r +``` + +#### `handler` + +``` purescript +handler :: forall a. EventFn SyntheticEvent a -> (a -> Eff (react :: ReactFX) Unit) -> EventHandler +``` + +Create an `EventHandler`, given an `EventFn` and a callback. + +For example: + +```purs +input { onChange: handler targetValue + \value -> setState \_ -> { value } + } +``` + +#### `merge` + +``` purescript +merge :: forall a fns fns_list r. RowToList fns fns_list => Merge fns_list fns a r => { | fns } -> EventFn a ({ | r }) +``` + +Merge multiple `EventFn` operations and collect their results. + +For example: + +```purs +input { onChange: handler (merge { targetValue, timeStamp }) + \{ targetValue, timeStamp } -> setState \_ -> { ... } + } +``` + +#### `bubbles` + +``` purescript +bubbles :: EventFn SyntheticEvent Boolean +``` + +#### `cancelable` + +``` purescript +cancelable :: EventFn SyntheticEvent Boolean +``` + +#### `currentTarget` + +``` purescript +currentTarget :: EventFn SyntheticEvent DOMNode +``` + +#### `eventPhase` + +``` purescript +eventPhase :: EventFn SyntheticEvent Int +``` + +#### `eventPhaseNone` + +``` purescript +eventPhaseNone :: Int +``` + +#### `eventPhaseCapturing` + +``` purescript +eventPhaseCapturing :: Int +``` + +#### `eventPhaseAtTarget` + +``` purescript +eventPhaseAtTarget :: Int +``` + +#### `eventPhaseBubbling` + +``` purescript +eventPhaseBubbling :: Int +``` + +#### `isTrusted` + +``` purescript +isTrusted :: EventFn SyntheticEvent Boolean +``` + +#### `nativeEvent` + +``` purescript +nativeEvent :: EventFn SyntheticEvent DOMEvent +``` + +#### `preventDefault` + +``` purescript +preventDefault :: EventFn SyntheticEvent SyntheticEvent +``` + +#### `isDefaultPrevented` + +``` purescript +isDefaultPrevented :: EventFn SyntheticEvent Boolean +``` + +#### `stopPropagation` + +``` purescript +stopPropagation :: EventFn SyntheticEvent SyntheticEvent +``` + +#### `isPropagationStopped` + +``` purescript +isPropagationStopped :: EventFn SyntheticEvent Boolean +``` + +#### `target` + +``` purescript +target :: EventFn SyntheticEvent DOMNode +``` + +#### `targetChecked` + +``` purescript +targetChecked :: EventFn SyntheticEvent (Nullable Boolean) +``` + +#### `targetValue` + +``` purescript +targetValue :: EventFn SyntheticEvent (Nullable String) +``` + +#### `timeStamp` + +``` purescript +timeStamp :: EventFn SyntheticEvent Number +``` + +#### `type_` + +``` purescript +type_ :: EventFn SyntheticEvent String +``` + +#### `Merge` + +``` purescript +class Merge (rl :: RowList) fns a r | rl -> fns, rl a -> r where + mergeImpl :: RLProxy rl -> { | fns } -> EventFn a ({ | r }) +``` + +##### Instances +``` purescript +Merge Nil () a () +(IsSymbol l, RowCons l (EventFn a b) fns_rest fns, RowCons l b r_rest r, RowLacks l fns_rest, RowLacks l r_rest, Merge rest fns_rest a r_rest) => Merge (Cons l (EventFn a b) rest) fns a r +``` + + diff --git a/generated-docs/React/Basic/Types.md b/generated-docs/React/Basic/Types.md deleted file mode 100644 index 4a94906..0000000 --- a/generated-docs/React/Basic/Types.md +++ /dev/null @@ -1,60 +0,0 @@ -## Module React.Basic.Types - -#### `JSX` - -``` purescript -data JSX :: Type -``` - -A virtual DOM element. - -#### `ReactComponent` - -``` purescript -data ReactComponent :: Type -> Type -``` - -A React component which can be used from JavaScript. - -#### `ReactFX` - -``` purescript -data ReactFX :: Effect -``` - -A placeholder effect for all React FFI. - -#### `DOMNode` - -``` purescript -data DOMNode :: Type -``` - -An _actual_ DOM node (not a virtual DOM element!) - -#### `CSS` - -``` purescript -data CSS :: Type -``` - -An abstract type representing records of CSS attributes. - -#### `SyntheticEvent` - -``` purescript -type SyntheticEvent = { bubbles :: Boolean, cancelable :: Boolean, currentTarget :: DOMNode, defaultPrevented :: Boolean, eventPhase :: Number, isTrusted :: Boolean, target :: DOMNode, timeStamp :: Number, "type" :: String } -``` - -Event data that we receive from React. - -#### `EventHandler` - -``` purescript -type EventHandler = EffFn1 (react :: ReactFX) SyntheticEvent Unit -``` - -An event handler, which receives a `SyntheticEvent` and performs some -effects in return. - - diff --git a/src/React/Basic.purs b/src/React/Basic.purs index 5db5353..daf2002 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -5,7 +5,9 @@ module React.Basic , createElementKeyed , fragment , fragmentKeyed - , module React.Basic.Types + , JSX + , ReactComponent + , ReactFX ) where import Prelude @@ -13,8 +15,15 @@ import Prelude import Control.Monad.Eff (Eff, kind Effect) import Control.Monad.Eff.Uncurried (EffFn3, mkEffFn3) import Data.Function.Uncurried (Fn2, Fn3, mkFn3, runFn2) -import React.Basic.Types (CSS, EventHandler, JSX, ReactComponent, ReactFX) -import React.Basic.Types as React.Basic.Types + +-- | A virtual DOM element. +foreign import data JSX :: Type + +-- | A React component which can be used from JavaScript. +foreign import data ReactComponent :: Type -> Type + +-- | A placeholder effect for all React FFI. +foreign import data ReactFX :: Effect -- | Create a React component from a _specification_ of that component. -- | diff --git a/src/React/Basic/DOM.purs b/src/React/Basic/DOM.purs index 680d633..64a0bcb 100644 --- a/src/React/Basic/DOM.purs +++ b/src/React/Basic/DOM.purs @@ -7,14 +7,17 @@ module React.Basic.DOM where -import React.Basic (ReactComponent, createElement) -import React.Basic.Types (CSS, JSX, EventHandler) +import React.Basic (JSX, ReactComponent, createElement) +import React.Basic.DOM.Events (EventHandler) import Unsafe.Coerce (unsafeCoerce) -- | Create a text node. text :: String -> JSX text = unsafeCoerce +-- | An abstract type representing records of CSS attributes. +foreign import data CSS :: Type + -- | Create a value of type `CSS` (which can be provided to the `style` property) -- | from a plain record of CSS attributes. -- | @@ -2162,4 +2165,3 @@ wbr => Record attrs -> JSX wbr = createElement (unsafeCreateDOMComponent "wbr") - diff --git a/src/React/Basic/DOM/Events.js b/src/React/Basic/DOM/Events.js new file mode 100644 index 0000000..29ad335 --- /dev/null +++ b/src/React/Basic/DOM/Events.js @@ -0,0 +1,20 @@ +"use strict"; + +exports.unsafePreventDefault = function(e) { + e.preventDefault(); + return e; +}; + +exports.unsafeIsDefaultPrevented = function(e) { + e.isDefaultPrevented(); + return e; +}; + +exports.unsafeStopPropagation = function(e) { + e.stopPropagation(); + return e; +}; + +exports.unsafeIsPropagationStopped = function(e) { + return e.isPropagationStopped(); +}; diff --git a/src/React/Basic/DOM/Events.purs b/src/React/Basic/DOM/Events.purs new file mode 100644 index 0000000..ae0ea61 --- /dev/null +++ b/src/React/Basic/DOM/Events.purs @@ -0,0 +1,188 @@ +-- | This module defines safe event function and property accessors. + +module React.Basic.DOM.Events + ( EventHandler + , SyntheticEvent + , DOMNode + , DOMEvent + , EventFn + , handler + , merge + , bubbles + , cancelable + , currentTarget + , eventPhase + , eventPhaseNone + , eventPhaseCapturing + , eventPhaseAtTarget + , eventPhaseBubbling + , isTrusted + , nativeEvent + , preventDefault + , isDefaultPrevented + , stopPropagation + , isPropagationStopped + , target + , targetChecked + , targetValue + , timeStamp + , type_ + , class Merge + , mergeImpl + ) where + +import Prelude + +import Control.Monad.Eff (Eff) +import Control.Monad.Eff.Uncurried (EffFn1, mkEffFn1) +import Data.Nullable (Nullable) +import Data.Record (delete, get, insert) +import Data.Symbol (class IsSymbol, SProxy(SProxy)) +import React.Basic (ReactFX) +import Type.Row (kind RowList, class RowToList, class RowLacks, RLProxy(..), Cons, Nil) +import Unsafe.Coerce (unsafeCoerce) + +-- | An event handler, which receives a `SyntheticEvent` and performs some +-- | effects in return. +type EventHandler = EffFn1 (react :: ReactFX) SyntheticEvent Unit + +-- | Event data that we receive from React. +foreign import data SyntheticEvent :: Type + +-- | An _actual_ DOM node (not a virtual DOM element!) +foreign import data DOMNode :: Type + +-- | The underlying browser Event. +foreign import data DOMEvent :: Type + +-- | Encapsulates a safe event operation. `EventFn`s can be composed +-- | to perform multiple operations. +-- | +-- | For example: +-- | +-- | ```purs +-- | input { onChange: handler (preventDefault >>> targetValue) +-- | \value -> setState \_ -> { value } +-- | } +-- | ``` +newtype EventFn a b = EventFn (a -> b) + +derive newtype instance semigroupoidBuilder :: Semigroupoid EventFn +derive newtype instance categoryBuilder :: Category EventFn + +-- | Create an `EventHandler`, given an `EventFn` and a callback. +-- | +-- | For example: +-- | +-- | ```purs +-- | input { onChange: handler targetValue +-- | \value -> setState \_ -> { value } +-- | } +-- | ``` +handler :: forall a. EventFn SyntheticEvent a -> (a -> Eff (react :: ReactFX) Unit) -> EventHandler +handler (EventFn fn) cb = mkEffFn1 $ fn >>> cb + +class Merge (rl :: RowList) fns a r | rl -> fns, rl a -> r where + mergeImpl :: RLProxy rl -> Record fns -> EventFn a (Record r) + +instance mergeNil :: Merge Nil () a () where + mergeImpl _ _ = EventFn \_ -> {} + +instance mergeCons + :: ( IsSymbol l + , RowCons l (EventFn a b) fns_rest fns + , RowCons l b r_rest r + , RowLacks l fns_rest + , RowLacks l r_rest + , Merge rest fns_rest a r_rest + ) + => Merge (Cons l (EventFn a b) rest) fns a r + where + mergeImpl _ fns = EventFn \a -> + let EventFn inner = mergeImpl (RLProxy :: RLProxy rest) (delete l fns) + EventFn f = get l fns + in insert l (f a) (inner a) + where + l = SProxy :: SProxy l + +-- | Merge multiple `EventFn` operations and collect their results. +-- | +-- | For example: +-- | +-- | ```purs +-- | input { onChange: handler (merge { targetValue, timeStamp }) +-- | \{ targetValue, timeStamp } -> setState \_ -> { ... } +-- | } +-- | ``` +merge + :: forall a fns fns_list r + . RowToList fns fns_list + => Merge fns_list fns a r + => Record fns + -> EventFn a (Record r) +merge = mergeImpl (RLProxy :: RLProxy fns_list) + +bubbles :: EventFn SyntheticEvent Boolean +bubbles = EventFn \e -> (unsafeCoerce e).bubbles + +cancelable :: EventFn SyntheticEvent Boolean +cancelable = EventFn \e -> (unsafeCoerce e).cancelable + +currentTarget :: EventFn SyntheticEvent DOMNode +currentTarget = EventFn \e -> (unsafeCoerce e).currentTarget + +eventPhase :: EventFn SyntheticEvent Int +eventPhase = EventFn \e -> (unsafeCoerce e).eventPhase + +eventPhaseNone :: Int +eventPhaseNone = 0 + +eventPhaseCapturing :: Int +eventPhaseCapturing = 1 + +eventPhaseAtTarget :: Int +eventPhaseAtTarget = 2 + +eventPhaseBubbling :: Int +eventPhaseBubbling = 3 + +isTrusted :: EventFn SyntheticEvent Boolean +isTrusted = EventFn \e -> (unsafeCoerce e).isTrusted + +nativeEvent :: EventFn SyntheticEvent DOMEvent +nativeEvent = EventFn \e -> (unsafeCoerce e).nativeEvent + +preventDefault :: EventFn SyntheticEvent SyntheticEvent +preventDefault = EventFn unsafePreventDefault + +foreign import unsafePreventDefault :: SyntheticEvent -> SyntheticEvent + +isDefaultPrevented :: EventFn SyntheticEvent Boolean +isDefaultPrevented = EventFn unsafeIsDefaultPrevented + +foreign import unsafeIsDefaultPrevented :: SyntheticEvent -> Boolean + +stopPropagation :: EventFn SyntheticEvent SyntheticEvent +stopPropagation = EventFn unsafeStopPropagation + +foreign import unsafeStopPropagation :: SyntheticEvent -> SyntheticEvent + +isPropagationStopped :: EventFn SyntheticEvent Boolean +isPropagationStopped = EventFn unsafeIsPropagationStopped + +foreign import unsafeIsPropagationStopped :: SyntheticEvent -> Boolean + +target :: EventFn SyntheticEvent DOMNode +target = EventFn \e -> (unsafeCoerce e).target + +targetChecked :: EventFn SyntheticEvent (Nullable Boolean) +targetChecked = EventFn \e -> (unsafeCoerce e).target.checked + +targetValue :: EventFn SyntheticEvent (Nullable String) +targetValue = EventFn \e -> (unsafeCoerce e).target.value + +timeStamp :: EventFn SyntheticEvent Number +timeStamp = EventFn \e -> (unsafeCoerce e).timeStamp + +type_ :: EventFn SyntheticEvent String +type_ = EventFn \e -> (unsafeCoerce e)."type" diff --git a/src/React/Basic/Types.purs b/src/React/Basic/Types.purs deleted file mode 100644 index 98ffe36..0000000 --- a/src/React/Basic/Types.purs +++ /dev/null @@ -1,38 +0,0 @@ -module React.Basic.Types where - -import Prelude - -import Control.Monad.Eff (kind Effect) -import Control.Monad.Eff.Uncurried (EffFn1) - --- | A virtual DOM element. -foreign import data JSX :: Type - --- | A React component which can be used from JavaScript. -foreign import data ReactComponent :: Type -> Type - --- | A placeholder effect for all React FFI. -foreign import data ReactFX :: Effect - --- | An _actual_ DOM node (not a virtual DOM element!) -foreign import data DOMNode :: Type - --- | An abstract type representing records of CSS attributes. -foreign import data CSS :: Type - --- | Event data that we receive from React. -type SyntheticEvent = - { bubbles :: Boolean - , cancelable :: Boolean - , currentTarget :: DOMNode - , defaultPrevented :: Boolean - , eventPhase :: Number - , isTrusted :: Boolean - , target :: DOMNode - , timeStamp :: Number - , type :: String - } - --- | An event handler, which receives a `SyntheticEvent` and performs some --- | effects in return. -type EventHandler = EffFn1 (react :: ReactFX) SyntheticEvent Unit