From a4e85a2932146092d74ced0edfbd4c38de73d599 Mon Sep 17 00:00:00 2001 From: Michael Trotter Date: Wed, 31 Oct 2018 23:11:23 -0600 Subject: [PATCH 01/12] Reimplement on React Hooks --- examples/component/package.json | 4 +- examples/component/src/Container.purs | 24 +- examples/component/src/Main.purs | 10 +- examples/component/src/ToggleButton.purs | 53 +- examples/controlled-input/package.json | 4 +- .../controlled-input/src/ControlledInput.purs | 71 ++- examples/controlled-input/src/Main.purs | 10 +- examples/counter/package.json | 4 +- examples/counter/src/Counter.js | 7 + examples/counter/src/Counter.purs | 38 +- examples/counter/src/Main.purs | 10 +- examples/legacy-v2/package.json | 4 +- examples/legacy-v2/src/Compat.purs | 49 -- src/React/Basic.js | 223 +------- src/React/Basic.purs | 490 ++++-------------- src/React/Basic/Compat.purs | 39 ++ src/React/Basic/DOM.js | 6 +- src/React/Basic/DOM.purs | 24 +- src/React/Basic/DOM/Events.purs | 13 +- src/React/Basic/DOM/Internal.purs | 4 +- 20 files changed, 270 insertions(+), 817 deletions(-) create mode 100644 examples/counter/src/Counter.js delete mode 100644 examples/legacy-v2/src/Compat.purs create mode 100644 src/React/Basic/Compat.purs diff --git a/examples/component/package.json b/examples/component/package.json index 624a6f3..e743eff 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -1,7 +1,7 @@ { "dependencies": { - "react": "^16.4.2", - "react-dom": "^16.4.2" + "react": "16.7.0-alpha.0", + "react-dom": "16.7.0-alpha.0" }, "devDependencies": { "browserify": "^16.2.2" diff --git a/examples/component/src/Container.purs b/examples/component/src/Container.purs index 1e6e877..8f40ea0 100644 --- a/examples/component/src/Container.purs +++ b/examples/component/src/Container.purs @@ -2,18 +2,18 @@ module Container where import Prelude -import React.Basic (Component, JSX, createComponent, makeStateless) +import React.Basic (CreateComponent, component, element) import React.Basic.DOM as R -import ToggleButton (toggleButton) +import ToggleButton (mkToggleButton) -component :: Component Unit -component = createComponent "Container" +mkToggleButtonContainer :: CreateComponent {} +mkToggleButtonContainer = do + toggleButton <- mkToggleButton -toggleButtonContainer :: JSX -toggleButtonContainer = unit # makeStateless component \_ -> - R.div - { children: - [ toggleButton { label: "A" } - , toggleButton { label: "B" } - ] - } + component "Container" \_ -> do + pure $ R.div + { children: + [ element toggleButton { label: "A" } + , element toggleButton { label: "B" } + ] + } diff --git a/examples/component/src/Main.purs b/examples/component/src/Main.purs index 67193a8..3c32365 100644 --- a/examples/component/src/Main.purs +++ b/examples/component/src/Main.purs @@ -2,10 +2,11 @@ module Main where import Prelude -import Container (toggleButtonContainer) +import Container (mkToggleButtonContainer) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) +import React.Basic (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -17,6 +18,7 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> - let app = toggleButtonContainer - in render app c + Just c -> do + toggleButtonContainer <- mkToggleButtonContainer + let app = element toggleButtonContainer {} + render app c diff --git a/examples/component/src/ToggleButton.purs b/examples/component/src/ToggleButton.purs index 49430ae..c89c3e9 100644 --- a/examples/component/src/ToggleButton.purs +++ b/examples/component/src/ToggleButton.purs @@ -3,40 +3,23 @@ module ToggleButton where import Prelude import Effect.Console (log) -import React.Basic (Component, JSX, StateUpdate(..), capture_, createComponent, make) +import React.Basic (CreateComponent, component, ref, useEffect, useState, (/\)) import React.Basic.DOM as R - -component :: Component Props -component = createComponent "ToggleButton" - -type Props = - { label :: String - } - -data Action - = Toggle - -toggleButton :: Props -> JSX -toggleButton = make component - { initialState: - { on: false +import React.Basic.DOM.Events (capture_) + +mkToggleButton :: CreateComponent { label :: String } +mkToggleButton = do + component "ToggleButton" \{ label } -> do + on /\ setOn <- useState false + + useEffect [ref on] do + log $ "State: " <> if on then "On" else "Off" + pure (pure unit) + + pure $ R.button + { onClick: capture_ $ setOn not + , children: + [ R.text label + , R.text if on then " On" else " Off" + ] } - - , update: \self -> case _ of - Toggle -> - UpdateAndSideEffects - self.state { on = not self.state.on } - \nextSelf -> do - log $ "next state: " <> show nextSelf.state - - , render: \self -> - R.button - { onClick: capture_ self Toggle - , children: - [ R.text self.props.label - , R.text if self.state.on - then " On" - else " Off" - ] - } - } diff --git a/examples/controlled-input/package.json b/examples/controlled-input/package.json index 624a6f3..e743eff 100644 --- a/examples/controlled-input/package.json +++ b/examples/controlled-input/package.json @@ -1,7 +1,7 @@ { "dependencies": { - "react": "^16.4.2", - "react-dom": "^16.4.2" + "react": "16.7.0-alpha.0", + "react-dom": "16.7.0-alpha.0" }, "devDependencies": { "browserify": "^16.2.2" diff --git a/examples/controlled-input/src/ControlledInput.purs b/examples/controlled-input/src/ControlledInput.purs index 7824483..cbf31ef 100644 --- a/examples/controlled-input/src/ControlledInput.purs +++ b/examples/controlled-input/src/ControlledInput.purs @@ -3,43 +3,40 @@ module ControlledInput where import Prelude import Data.Maybe (Maybe(..), fromMaybe, maybe) -import React.Basic (Component, JSX, StateUpdate(..), capture, createComponent, make) -import React.Basic as React +import React.Basic (CreateComponent, Render, component, fragment, useState, (/\)) import React.Basic.DOM as R -import React.Basic.DOM.Events (targetValue, timeStamp) -import React.Basic.Events (merge) - -component :: Component Props -component = createComponent "ControlledInput" - -type Props = Unit - -data Action - = ValueChanged String Number - -controlledInput :: Props -> JSX -controlledInput = make component - { initialState: - { value: "hello world" - , timestamp: Nothing - } - - , update: \self -> case _ of - ValueChanged value timestamp -> - Update self.state - { value = value - , timestamp = Just timestamp - } +import React.Basic.DOM.Events (capture, targetValue, timeStamp) +import React.Basic.Events (EventHandler, merge) + +mkControlledInput :: CreateComponent {} +mkControlledInput = + component "ControlledInput" \props -> do + firstName <- useInput "hello" + lastName <- useInput "world" + + pure $ R.form_ + [ renderInput firstName + , renderInput lastName + ] + where + renderInput input = + fragment + [ R.input { onChange: input.onChange, value: input.value } + , R.p_ [ R.text ("Current value = " <> show input.value) ] + , R.p_ [ R.text ("Changed at = " <> maybe "never" show input.lastChanged) ] + ] - , render: \self -> - React.fragment - [ R.input - { onChange: - capture self (merge { targetValue, timeStamp }) - \{ timeStamp, targetValue } -> ValueChanged (fromMaybe "" targetValue) timeStamp - , value: self.state.value +useInput :: String -> Render { onChange :: EventHandler, value :: String, lastChanged :: Maybe Number } +useInput initialValue = do + { value, lastChanged } /\ replaceState <- useState { value: initialValue, lastChanged: Nothing } + pure + { onChange: capture + (merge { targetValue, timeStamp }) + \{ timeStamp, targetValue } -> do + replaceState \_ -> + { value: fromMaybe "" targetValue + , lastChanged: Just timeStamp } - , R.p_ [ R.text ("Current value = " <> show self.state.value) ] - , R.p_ [ R.text ("Changed at = " <> maybe "never" show self.state.timestamp) ] - ] - } + , value + , lastChanged + } diff --git a/examples/controlled-input/src/Main.purs b/examples/controlled-input/src/Main.purs index 8651738..2af6387 100644 --- a/examples/controlled-input/src/Main.purs +++ b/examples/controlled-input/src/Main.purs @@ -2,10 +2,11 @@ module Main where import Prelude -import ControlledInput (controlledInput) +import ControlledInput (mkControlledInput) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) +import React.Basic (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -17,6 +18,7 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> - let app = controlledInput unit - in render app c + Just c -> do + controlledInput <- mkControlledInput + let app = element controlledInput {} + render app c diff --git a/examples/counter/package.json b/examples/counter/package.json index 583c22b..7c5faf4 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -1,7 +1,7 @@ { "dependencies": { - "react": "16.6.0", - "react-dom": "16.6.0" + "react": "16.7.0-alpha.0", + "react-dom": "16.7.0-alpha.0" }, "devDependencies": { "browserify": "16.2.3" diff --git a/examples/counter/src/Counter.js b/examples/counter/src/Counter.js new file mode 100644 index 0000000..97f35c0 --- /dev/null +++ b/examples/counter/src/Counter.js @@ -0,0 +1,7 @@ +"use strict"; + +exports.setDocumentTitle = function(title) { + return function() { + document.title = title; + }; +}; diff --git a/examples/counter/src/Counter.purs b/examples/counter/src/Counter.purs index cbea449..9e89e21 100644 --- a/examples/counter/src/Counter.purs +++ b/examples/counter/src/Counter.purs @@ -2,31 +2,23 @@ module Counter where import Prelude - -import React.Basic (Component, JSX, StateUpdate(..), capture_, createComponent, make) +import Effect (Effect) +import React.Basic (CreateComponent, component, ref, useEffect, useState, (/\)) import React.Basic.DOM as R +import React.Basic.DOM.Events (capture_) -component :: Component Props -component = createComponent "Counter" - -type Props = - { label :: String - } - -data Action - = Increment +mkCounter :: CreateComponent {} +mkCounter = do + component "Counter" \props -> do + counter /\ setCounter <- useState 0 -counter :: Props -> JSX -counter = make component { initialState, update, render } - where - initialState = { counter: 0, dummy: 0 } + useEffect [ref counter] do + setDocumentTitle $ "Count: " <> show counter + pure (pure unit) - update self = case _ of - Increment -> - Update self.state { counter = self.state.counter + 1 } + pure $ R.button + { onClick: capture_ $ setCounter (_ + 1) + , children: [ R.text $ "Increment: " <> show counter ] + } - render self = - R.button - { onClick: capture_ self Increment - , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ] - } +foreign import setDocumentTitle :: String -> Effect Unit diff --git a/examples/counter/src/Main.purs b/examples/counter/src/Main.purs index fa07fd4..b702611 100644 --- a/examples/counter/src/Main.purs +++ b/examples/counter/src/Main.purs @@ -2,10 +2,11 @@ module Main where import Prelude -import Counter (counter) +import Counter (mkCounter) import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (throw) +import React.Basic (element) import React.Basic.DOM (render) import Web.DOM.NonElementParentNode (getElementById) import Web.HTML (window) @@ -17,6 +18,7 @@ main = do container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window) case container of Nothing -> throw "Container element not found." - Just c -> - let app = counter { label: "Increment" } - in render app c + Just c -> do + counter <- mkCounter + let app = element counter {} + render app c diff --git a/examples/legacy-v2/package.json b/examples/legacy-v2/package.json index 624a6f3..e743eff 100644 --- a/examples/legacy-v2/package.json +++ b/examples/legacy-v2/package.json @@ -1,7 +1,7 @@ { "dependencies": { - "react": "^16.4.2", - "react-dom": "^16.4.2" + "react": "16.7.0-alpha.0", + "react-dom": "16.7.0-alpha.0" }, "devDependencies": { "browserify": "^16.2.2" diff --git a/examples/legacy-v2/src/Compat.purs b/examples/legacy-v2/src/Compat.purs deleted file mode 100644 index 30a32cc..0000000 --- a/examples/legacy-v2/src/Compat.purs +++ /dev/null @@ -1,49 +0,0 @@ -module React.Basic.Compat - ( Component - , component - , stateless - , module React.Basic - ) where - -import Prelude - -import Effect (Effect) -import React.Basic (JSX, ReactComponent, Self, StateUpdate(..), createComponent, element, elementKeyed, empty, fragment, make, makeStateless, send, toReactComponent) - -type Component = ReactComponent - --- | Supports a common subset of the v2 API to ease the upgrade process -component - :: forall props state - . { displayName :: String - , initialState :: { | state } - , receiveProps :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> Effect Unit - , render :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> JSX - } - -> ReactComponent { | props } -component { displayName, initialState, receiveProps, render } = - toReactComponent identity (createComponent displayName) - { initialState: initialState - , didMount: receiveProps <<< selfToLegacySelf - , didUpdate: receiveProps <<< selfToLegacySelf - , update: \self stateUpdate -> Update (stateUpdate self.state) - , render: render <<< selfToLegacySelf - } - where - selfToLegacySelf self@{ props, state } = - { props - , state - , setState: send self - } - --- | Supports a common subset of the v2 API to ease the upgrade process -stateless - :: forall props - . { displayName :: String - , render :: { | props } -> JSX - } - -> ReactComponent { | props } -stateless { displayName, render } = - toReactComponent identity (createComponent displayName) - { render: \self -> render self.props - } diff --git a/src/React/Basic.js b/src/React/Basic.js index 2ffd725..6ab8747 100644 --- a/src/React/Basic.js +++ b/src/React/Basic.js @@ -3,149 +3,13 @@ var React = require("react"); var Fragment = React.Fragment || "div"; -exports.createComponent = (function() { - // Begin component prototype functions - // (`this`-dependent, defined outside `createComponent` - // for a slight performance boost) - function toSelf() { - var self = { - props: this.props.$$props, - state: this.state === null ? null : this.state.$$state, - instance_: this - }; - return self; - } - - function shouldComponentUpdate(nextProps, nextState) { - var shouldUpdate = this.$$spec.shouldUpdate; - return shouldUpdate === undefined - ? true - : shouldUpdate(this.toSelf())(nextProps.$$props)( - nextState === null ? null : nextState.$$state - ); - } - - function componentDidMount() { - var didMount = this.$$spec.didMount; - if (didMount !== undefined) { - didMount(this.toSelf())(); - } - } - - function componentDidUpdate() { - var didUpdate = this.$$spec.didUpdate; - if (didUpdate !== undefined) { - didUpdate(this.toSelf())(); - } - } - - function componentWillUnmount() { - this.$$mounted = false; - var willUnmount = this.$$spec.willUnmount; - if (willUnmount !== undefined) { - willUnmount(this.toSelf())(); - } - } - - function render() { - return this.$$spec.render(this.toSelf()); - } - // End component prototype functions - - return function(displayName) { - var Component = function constructor(props) { - this.$$mounted = true; - this.$$spec = props.$$spec; - this.state = - // React may optimize components with no state, - // so we leave state null if it was left as - // the default value. - this.$$spec.initialState === undefined - ? null - : { $$state: this.$$spec.initialState }; - return this; - }; - - Component.displayName = displayName; - Component.prototype = Object.create(React.Component.prototype); - Component.prototype.constructor = Component; - Component.prototype.toSelf = toSelf; - Component.prototype.shouldComponentUpdate = shouldComponentUpdate; - Component.prototype.componentDidMount = componentDidMount; - Component.prototype.componentDidUpdate = componentDidUpdate; - Component.prototype.componentWillUnmount = componentWillUnmount; - Component.prototype.render = render; - - return Component; - }; -})(); - -exports.send_ = function(buildStateUpdate) { - return function(self, action) { - if (!self.instance_.$$mounted) { - exports.warningUnmountedComponentAction(self, action); - return; - } - if (self.instance_.$$spec.update === undefined) { - exports.warningDefaultUpdate(self, action); - return; - } - var sideEffects = null; - self.instance_.setState( - function(s) { - var setStateContext = self.instance_.toSelf(); - setStateContext.state = s.$$state; - var updates = buildStateUpdate( - self.instance_.$$spec.update(setStateContext)(action) - ); - if (updates.effects !== null) { - sideEffects = updates.effects; - } - if (updates.state !== null && updates.state !== s.$$state) { - return { $$state: updates.state }; - } else { - return null; - } - }, - function() { - if (sideEffects !== null) { - sideEffects(this.toSelf())(); - } - } - ); - }; -}; - -exports.readProps = function(self) { - return self.instance_.props.$$props; -}; - -exports.readState = function(self) { - var state = self.instance_.state; - return state === null ? null : state.$$state; +exports.useState_ = function(initialState) { + var state = React.useState(initialState); + return { value: state[0], setValue: state[1] }; }; -exports.make = function(_unionDict) { - return function($$type) { - return function($$spec) { - var $$specPadded = { - initialState: $$spec.initialState, - update: $$spec.update, - render: $$spec.render, - shouldUpdate: $$spec.shouldUpdate, - didMount: $$spec.didMount, - didUpdate: $$spec.didUpdate, - willUnmount: $$spec.willUnmount - }; - return function($$props) { - var props = { - $$props: $$props, - $$spec: $$specPadded - }; - return React.createElement($$type, props); - }; - }; - }; +exports.useEffect_ = function(effect, inputs) { + React.useEffect(effect, inputs); }; exports.empty = null; @@ -167,75 +31,16 @@ exports.fragment = function(children) { return React.createElement.apply(null, [Fragment, {}].concat(children)); }; -exports.displayNameFromComponent = function($$type) { - return $$type.displayName || "[unknown]"; +exports.displayName = function(component) { + return typeof component === "string" + ? component + : component.displayName || "[unknown]"; }; -exports.displayNameFromSelf = function(self) { - return exports.displayNameFromComponent(self.instance_.constructor); -}; - -exports.toReactComponent = function(_unionDict) { - return function(fromJSProps) { - return function($$type) { - return function($$spec) { - var $$specPadded = { - initialState: $$spec.initialState, - update: $$spec.update, - render: $$spec.render, - shouldUpdate: $$spec.shouldUpdate, - didMount: $$spec.didMount, - didUpdate: $$spec.didUpdate, - willUnmount: $$spec.willUnmount - }; - - var Component = function constructor() { - return this; - }; - - Component.prototype = Object.create(React.Component.prototype); - - Component.displayName = $$type.displayName + " (Wrapper)"; - - Component.prototype.render = function() { - var props = { - $$props: fromJSProps(this.props), - $$spec: $$specPadded - }; - return React.createElement($$type, props); - }; - - return Component; - }; - }; +exports.unsafeSetDisplayName = function(displayName, component) { + component.displayName = displayName; + component.toString = function() { + return displayName; }; -}; - -exports.warningDefaultUpdate = function(self, action) { - console.error( - "A " + - exports.displayNameFromSelf(self) + - " component received an action but has no `update` function defined. Override the default `update` function to handle this action." - ); - console.error("Self:", self); - console.error("Action:", action); -}; - -exports.warningUnmountedComponentAction = function(self, action) { - console.error( - "An unmounted " + - exports.displayNameFromSelf(self) + - " component received the action below. Actions received by unmounted components usually indicate a memory leak. Make sure to unsubscribe from any async work in `willUnmount`." - ); - console.error("Self:", self); - console.error("Action:", action); -}; - -exports.warningFailedAsyncAction = function(self, error) { - console.error( - "An async action failed in a " + - exports.displayNameFromSelf(self) + - " component." - ); - console.error(error); + return component; }; diff --git a/src/React/Basic.purs b/src/React/Basic.purs index b6bae67..5c49fa0 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -1,321 +1,102 @@ module React.Basic - ( ComponentSpec - , createComponent - , Component - , StateUpdate(..) - , Self - , send - , sendAsync - , capture - , capture_ - , monitor - , monitor_ - , readProps - , readState - , make - , makeStateless + ( Component + , Render + , CreateComponent , JSX + , component + , useState + , useEffect + , Ref + , class ToRef + , ref , empty , keyed , fragment , element , elementKeyed - , displayNameFromComponent - , displayNameFromSelf - , ReactComponent - , ReactComponentInstance - , toReactComponent + , displayName + , module Data.Tuple + , module Data.Tuple.Nested ) where import Prelude -import Data.Either (Either(..)) -import Data.Function.Uncurried (Fn1, Fn2, runFn1, runFn2) -import Data.Nullable (Nullable, notNull, null) +import Data.Function.Uncurried (Fn2, runFn2) +import Data.Tuple (Tuple(..)) +import Data.Tuple.Nested (tuple2, (/\)) import Effect (Effect) -import Effect.Aff (Aff, Error, runAff_) -import Effect.Uncurried (EffectFn2, runEffectFn2) -import React.Basic.DOM.Events (preventDefault, stopPropagation) -import React.Basic.Events (EventFn, EventHandler, SyntheticEvent, handler) -import Type.Row (class Union) +import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, runEffectFn1, runEffectFn2) +import Unsafe.Coerce (unsafeCoerce) --- | `ComponentSpec` represents a React-Basic component implementation. --- | --- | These are the properties your component definition may override --- | with specific implementations. None are required to be overridden, unless --- | an overridden function interacts with `state`, in which case `initialState` --- | is required (the compiler enforces this). While you _can_ use `state` and --- | dispatch actions without defining `update`, doing so doesn't make much sense --- | so the default `update` implementation will emit a warning. --- | --- | - `initialState` --- | - The component's starting state. --- | - Avoid mirroring prop values in state. --- | - `update` --- | - All state updates go through `update`. --- | - `update` is called when `send` is used to dispatch an action. --- | - State changes are described using `StateUpdate`. Only `Update` and `UpdateAndSideEffects` will cause rerenders and a call to `didUpdate`. --- | - Side effects requested are only invoked _after_ any corrosponding state update has completed its render cycle and the changes have been applied. This means it is safe to interact with the DOM in a side effect, for example. --- | - `render` --- | - Takes a current snapshot of the component (`Self`) and converts it to renderable `JSX`. --- | - `shouldUpdate` --- | - Can be useful for occasional performance optimizations. Rarely necessary. --- | - `didMount` --- | - The React component's `componentDidMount` lifecycle. Useful for initiating an action on first mount, such as fetching data from a server. --- | - `didUpdate` --- | - The React component's `componentDidUpdate` lifecycle. Rarely necessary. --- | - `willUnmount` --- | - The React component's `componentWillUpdate` lifecycle. Any subscriptions or timers created in `didMount` or `didUpdate` should be disposed of here. --- | --- | The component spec is generally not exported from your component --- | module and this type is rarely used explicitly. The simplified alias --- | `Component` is usually sufficient, and `make` will validate whether --- | your component's types line up. --- | --- | For example: --- | --- | ```purs --- | component :: Component Props --- | component = createComponent "Counter" --- | --- | type Props = --- | { label :: String --- | } --- | --- | data Action --- | = Increment --- | --- | counter :: Props -> JSX --- | counter = make component --- | { initialState = { counter: 0 } --- | --- | , update = \self action -> case action of --- | Increment -> --- | Update self.state { counter = self.state.counter + 1 } --- | --- | , render = \self -> --- | R.button --- | { onClick: capture_ self Increment --- | , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ] --- | } --- | } --- | ``` --- | --- | This example component overrides `initialState`, `update`, and `render`. --- | --- | __*Note:* A `ComponentSpec` is *not* a valid React component by itself. If you would like to use --- | a React-Basic component from JavaScript, use `toReactComponent`.__ --- | --- | __*See also:* `Component`, `ComponentSpec`, `make`, `makeStateless`__ -type ComponentSpec props state action = - ( initialState :: state - , update :: Self props state action -> action -> StateUpdate props state action - , render :: Self props state action -> JSX - , shouldUpdate :: Self props state action -> props -> state -> Boolean - , didMount :: Self props state action -> Effect Unit - , didUpdate :: Self props state action -> Effect Unit - , willUnmount :: Self props state action -> Effect Unit - ) - --- | Creates a `ComponentSpec` with a given Display Name. --- | --- | The resulting component spec is usually given the simplified `Component` type: --- | --- | ```purs --- | component :: Component Props --- | component = createComponent "Counter" --- | ``` --- | --- | This function should be used at the module level and considered side effecting. --- | This is because React uses referential equality when deciding whether a new --- | `JSX` tree is a valid update, or if it needs to be replaced entirely --- | (expensive and clears component state lower in the tree). --- | --- | __*Note:* A specific type for the props in `Component props` should always be chosen at this point. --- | It's technically possible to declare the component with `forall props. Component props` --- | but doing so is unsafe. Leaving the prop type open allows the use of a single `Component` --- | definition in multiple React-Basic components that may have different prop types. Because --- | component lifecycles are managed by React, it's possible for incompatible prop values to --- | be passed into a lifecycle function.__ --- | --- | __*Note:* A `Component` is *not* a valid React component by itself. If you would like to use --- | a React-Basic component from JavaScript, use `toReactComponent`.__ --- | --- | __*See also:* `Component`, `ComponentSpec`, `make`, `makeStateless`__ -foreign import createComponent - :: forall props - . String - -> Component props +newtype Component props = Component (EffectFn1 props JSX) --- | A simplified alias for `ComponentSpec`. This type is usually used to represent --- | the default component type returned from `createComponent`. --- type Component props = forall state action. ComponentSpec props state action +newtype Render a = Render (Effect a) --- | Opaque component information for internal use. --- | --- | __*For the curious:* This is the "class" React will use to render and --- | identify the component. It receives the `ComponentSpec` as a prop and knows --- | how to defer behavior to it. It requires very specific props and is not useful by --- | itself from JavaScript. For JavaScript interop, see `toReactComponent`.__ -data Component props - --- | Used by the `update` function to describe the kind of state update and/or side --- | effects desired. --- | --- | __*See also:* `ComponentSpec`__ -data StateUpdate props state action - = NoUpdate - | Update state - | SideEffects (Self props state action -> Effect Unit) - | UpdateAndSideEffects state (Self props state action -> Effect Unit) - --- | `Self` represents the component instance at a particular point in time. --- | --- | - `props` --- | - A snapshot of `props` taken when this `Self` was created. --- | - `state` --- | - A snapshot of `state` taken when this `Self` was created. --- | - `instance_` --- | - Unsafe escape hatch to the underlying component instance (`this` in the JavaScript React paradigm). Avoid as much as possible, but it's still frequently better than rewriting an entire component in JavaScript. --- | --- | __*See also:* `ComponentSpec`, `send`, `capture`, `readProps`, `readState`__ -type Self props state action = - { props :: props - , state :: state - , instance_ :: ReactComponentInstance - } - --- | Dispatch an `action` into the component to be handled by `update`. --- | --- | __*See also:* `update`, `capture`__ -send :: forall props state action. Self props state action -> action -> Effect Unit -send = runEffectFn2 (runFn1 send_ buildStateUpdate) +instance functorRender :: Functor Render where + map f (Render a) = Render (map f a) --- | Convenience function for sending an action when an `Aff` completes. --- | --- | __*Note:* Potential failure should be handled in the given `Aff` and converted --- | to an action, as the default error handler will simply log the error to --- | the console.__ --- | --- | __*See also:* `send`__ -sendAsync - :: forall props state action - . Self props state action - -> Aff action - -> Effect Unit -sendAsync self work = runAff_ handle work - where - handle (Right action) = send self action - handle (Left err) = runEffectFn2 warningFailedAsyncAction self err - --- | Create a capturing\* `EventHandler` to send an action when an event occurs. For --- | more complicated event handlers requiring `Effect`, use `handler` from `React.Basic.Events`. --- | --- | __\*calls `preventDefault` and `stopPropagation`__ --- | --- | __*See also:* `update`, `capture_`, `monitor`, `React.Basic.Events`__ -capture :: forall props state action a. Self props state action -> EventFn SyntheticEvent a -> (a -> action) -> EventHandler -capture self eventFn = monitor self (preventDefault >>> stopPropagation >>> eventFn) +instance applyRender :: Apply Render where + apply (Render f) (Render a) = Render (apply f a) --- | Like `capture`, but for actions which don't need to extract information from the Event. --- | --- | __*See also:* `update`, `capture`, `monitor_`__ -capture_ :: forall props state action. Self props state action -> action -> EventHandler -capture_ self action = capture self identity \_ -> action +instance applicativeRender :: Applicative Render where + pure a = Render (pure a) --- | Like `capture`, but does not cancel the event. --- | --- | __*See also:* `update`, `capture`, `monitor\_`__ -monitor :: forall props state action a. Self props state action -> EventFn SyntheticEvent a -> (a -> action) -> EventHandler -monitor self eventFn makeAction = handler eventFn \a -> send self (makeAction a) +instance bindRender :: Bind Render where + bind (Render m) f = Render (bind m \a -> case f a of Render b -> b) --- | Like `capture_`, but does not cancel the event. --- | --- | __*See also:* `update`, `monitor`, `capture_`, `React.Basic.Events`__ -monitor_ :: forall props state action. Self props state action -> action -> EventHandler -monitor_ self action = monitor self identity \_ -> action +type CreateComponent props = Effect (Component props) --- | Read the most up to date `props` directly from the component instance --- | associated with this `Self`. --- | --- | _Note: This function is for specific, asynchronous edge cases. --- | Generally, the `props` snapshot on `Self` is sufficient. --- | --- | __*See also:* `Self`__ -foreign import readProps :: forall props state action. Self props state action -> Effect props +component :: forall props. String -> (props -> Render JSX) -> CreateComponent props +component name render = + let c = Component (mkEffectFn1 (unsafeCoerce render)) + in runEffectFn2 unsafeSetDisplayName name c --- | Read the most up to date `state` directly from the component instance --- | associated with this `Self`. --- | --- | _Note: This function is for specific, asynchronous edge cases. --- | Generally, the `state` snapshot on `Self` is sufficient. --- | --- | __*See also:* `Self`__ -foreign import readState :: forall props state action. Self props state action -> Effect state +-- | useState +useState :: forall state. state -> Render (Tuple state ((state -> state) -> Effect Unit)) +useState initialState = Render do + { value, setValue } <- runEffectFn1 useState_ initialState + pure (Tuple value (runEffectFn1 setValue)) --- | Turn a `Component` into a usable render function. --- | This is where you will want to provide customized implementations: --- | --- | ```purs --- | component :: Component Props --- | component = createComponent "Counter" --- | --- | type Props = --- | { label :: String --- | } --- | --- | data Action --- | = Increment --- | --- | counter :: Props -> JSX --- | counter = make component --- | { initialState = { counter: 0 } --- | --- | , update = \self action -> case action of --- | Increment -> --- | Update self.state { counter = self.state.counter + 1 } --- | --- | , render = \self -> --- | R.button --- | { onClick: capture_ self Increment --- | , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ] --- | } --- | } --- | ``` --- | --- | __*See also:* `makeStateless`, `createComponent`, `Component`, `ComponentSpec`__ -foreign import make - :: forall spec spec_ props state action - . Union spec spec_ (ComponentSpec props state action) - => Component props - -> { render :: Self props state action -> JSX | spec } - -> props - -> JSX +foreign import useState_ + :: forall state + . EffectFn1 + state + { value :: state + , setValue :: EffectFn1 (state -> state) Unit + } --- | Makes stateless component definition slightly less verbose: --- | --- | ```purs --- | component :: Component Props --- | component = createComponent "Xyz" --- | --- | myComponent :: Props -> JSX --- | myComponent = makeStateless component \props -> JSX --- | ``` --- | --- | __*Note:* The only difference between a stateless React-Basic component and --- | a plain `props -> JSX` function is the presense of the component name --- | in React's dev tools and error stacks. It's just a conceptual boundary. --- | If this isn't important simply write a `props -> JSX` function.__ --- | --- | __*See also:* `make`, `createComponent`, `Component`, `ComponentSpec`__ -makeStateless - :: forall props - . Component props - -> (props -> JSX) - -> props - -> JSX -makeStateless component render = - make component { render: \self -> render self.props } +-- | useState +useEffect :: Array Ref -> Effect (Effect Unit) -> Render Unit +useEffect refs effect = Render (runEffectFn2 useEffect_ effect refs) + +foreign import useEffect_ + :: EffectFn2 + (Effect (Effect Unit)) + (Array Ref) + Unit + +data Ref + +class ToRef a where + ref :: a -> Ref + +instance trString :: ToRef String where + ref = unsafeCoerce + +instance trInt :: ToRef Int where + ref = unsafeCoerce + +instance trNumber :: ToRef Number where + ref = unsafeCoerce + +instance trBoolean :: ToRef Boolean where + ref = unsafeCoerce + +instance trRecord :: ToRef (Record a) where + ref = unsafeCoerce + +instance trArray :: ToRef (Array a) where + ref = unsafeCoerce -- | Represents rendered React VDOM (the result of calling `React.createElement` -- | in JavaScript). @@ -357,28 +138,28 @@ keyed = runFn2 keyed_ -- | __*See also:* `JSX`__ foreign import fragment :: Array JSX -> JSX --- | Create a `JSX` node from a `ReactComponent`, by providing the props. +-- | Create a `JSX` node from a `Component`, by providing the props. -- | -- | This function is for non-React-Basic React components, such as those -- | imported from FFI. -- | --- | __*See also:* `ReactComponent`, `elementKeyed`__ +-- | __*See also:* `Component`, `elementKeyed`__ element :: forall props - . ReactComponent { | props } + . Component { | props } -> { | props } -> JSX -element = runFn2 element_ +element (Component c) props = runFn2 element_ c props --- | Create a `JSX` node from a `ReactComponent`, by providing the props and a key. +-- | Create a `JSX` node from a `Component`, by providing the props and a key. -- | -- | This function is for non-React-Basic React components, such as those -- | imported from FFI. -- | --- | __*See also:* `ReactComponent`, `element`, React's documentation regarding the special `key` prop__ +-- | __*See also:* `Component`, `element`, React's documentation regarding the special `key` prop__ elementKeyed :: forall props - . ReactComponent { | props } + . Component { | props } -> { key :: String | props } -> JSX elementKeyed = runFn2 elementKeyed_ @@ -387,120 +168,27 @@ elementKeyed = runFn2 elementKeyed_ -- | error messages in logs. -- | -- | __*See also:* `displayNameFromSelf`, `createComponent`__ -foreign import displayNameFromComponent +foreign import displayName :: forall props . Component props -> String --- | Retrieve the Display Name from a `Self`. Useful for debugging and improving --- | error messages in logs. --- | --- | __*See also:* `displayNameFromComponent`, `createComponent`__ -foreign import displayNameFromSelf - :: forall props state action - . Self props state action - -> String - --- | Represents a traditional React component. Useful for JavaScript interop and --- | FFI. For example: --- | --- | ```purs --- | foreign import ComponentRequiringJSHacks :: ReactComponent { someProp :: String } --- | ``` --- | --- | __*See also:* `element`, `toReactComponent`__ -data ReactComponent props - --- | An opaque representation of a React component's instance (`this` in the JavaScript --- | React paradigm). It exists as an escape hatch to unsafe behavior. Use it with --- | caution. -data ReactComponentInstance - --- | Convert a React-Basic `ComponentSpec` to a JavaScript-friendly React component. --- | This function should only be used for JS interop and not normal React-Basic usage. --- | --- | __*Note:* Like `createComponent`, `toReactComponent` is side effecting in that --- | it creates a "class" React will see as unique each time it's called. Lift --- | any usage up to the module level, usage in `render` or any other function, --- | and applying any type classes to the `props`.__ --- | --- | __*See also:* `ReactComponent`__ -foreign import toReactComponent - :: forall spec spec_ jsProps props state action - . Union spec spec_ (ComponentSpec props state action) - => ({ | jsProps } -> props) - -> Component props - -> { render :: Self props state action -> JSX | spec } - -> ReactComponent { | jsProps } -- | -- | Internal utility or FFI functions -- | -buildStateUpdate - :: forall props state action - . StateUpdate props state action - -> { state :: Nullable state - , effects :: Nullable (Self props state action -> Effect Unit) - } -buildStateUpdate = case _ of - NoUpdate -> - { state: null - , effects: null - } - Update state_ -> - { state: notNull state_ - , effects: null - } - SideEffects effects -> - { state: null - , effects: notNull effects - } - UpdateAndSideEffects state_ effects -> - { state: notNull state_ - , effects: notNull effects - } - -foreign import send_ - :: forall props state action - . Fn1 - (StateUpdate props state action - -> { state :: Nullable state - , effects :: Nullable (Self props state action -> Effect Unit) - }) - (EffectFn2 - (Self props state action) - action - Unit) +foreign import unsafeSetDisplayName + :: forall props + . EffectFn2 String (Component props) (Component props) foreign import keyed_ :: Fn2 String JSX JSX foreign import element_ :: forall props - . Fn2 (ReactComponent { | props }) { | props } JSX + . Fn2 (EffectFn1 { | props } JSX) { | props } JSX foreign import elementKeyed_ :: forall props - . Fn2 (ReactComponent { | props }) { key :: String | props } JSX - -foreign import warningDefaultUpdate - :: forall props state action - . EffectFn2 - (Self props state action) - action - Unit - -foreign import warningUnmountedComponentAction - :: forall props state action - . EffectFn2 - (Self props state action) - action - Unit - -foreign import warningFailedAsyncAction - :: forall props state action - . EffectFn2 - (Self props state action) - Error - Unit + . Fn2 (Component { | props }) { key :: String | props } JSX diff --git a/src/React/Basic/Compat.purs b/src/React/Basic/Compat.purs new file mode 100644 index 0000000..3d23a58 --- /dev/null +++ b/src/React/Basic/Compat.purs @@ -0,0 +1,39 @@ +module React.Basic.Compat + ( component + , stateless + , module React.Basic + ) where + +import Prelude + +import Effect (Effect) +import Effect.Unsafe (unsafePerformEffect) +import React.Basic (Component, JSX, element) +import React.Basic (Tuple(..), component, ref, useEffect, useState) as React + +-- | Supports a common subset of the v2 API to ease the upgrade process +component + :: forall props state + . { displayName :: String + , initialState :: { | state } + , receiveProps :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> Effect Unit + , render :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> JSX + } + -> Component { | props } +component { displayName, initialState, receiveProps, render } = unsafePerformEffect do + React.component displayName \props -> do + React.Tuple state setState <- React.useState initialState + React.useEffect [React.ref props, React.ref state] do + receiveProps { props, state, setState } + pure $ pure unit + pure $ render { props, state, setState } + +-- | Supports a common subset of the v2 API to ease the upgrade process +stateless + :: forall props + . { displayName :: String + , render :: { | props } -> JSX + } + -> Component { | props } +stateless { displayName, render } = unsafePerformEffect do + React.component displayName (pure <<< render) diff --git a/src/React/Basic/DOM.js b/src/React/Basic/DOM.js index eee88bc..f4b8acd 100644 --- a/src/React/Basic/DOM.js +++ b/src/React/Basic/DOM.js @@ -14,14 +14,10 @@ exports.unmountComponentAtNode_ = function(node) { return ReactDOM.unmountComponentAtNode(node); }; -exports.findDOMNode_ = function(instance) { - return ReactDOM.findDOMNode(instance); -}; - exports.createPortal_ = function(jsx, node) { return ReactDOM.createPortal(jsx, node); }; exports.mergeStyles = function(styles) { - return Object.assign.apply(null, [ {} ].concat(styles)); + return Object.assign.apply(null, [{}].concat(styles)); }; diff --git a/src/React/Basic/DOM.purs b/src/React/Basic/DOM.purs index 85add7e..32a0d7d 100644 --- a/src/React/Basic/DOM.purs +++ b/src/React/Basic/DOM.purs @@ -12,7 +12,6 @@ module React.Basic.DOM , hydrate , hydrate' , unmount - , findDOMNode , createPortal , text , css @@ -22,18 +21,14 @@ module React.Basic.DOM import Prelude -import Data.Either (Either) import Data.Function.Uncurried (Fn2, runFn2) -import Data.Maybe (Maybe(..)) -import Data.Nullable (Nullable, toMaybe) import Effect (Effect) -import Effect.Exception (Error, throw, try) import Effect.Uncurried (EffectFn1, EffectFn3, runEffectFn1, runEffectFn3) -import React.Basic (ReactComponentInstance, JSX) +import React.Basic (JSX) import React.Basic.DOM.Generated (Props_a, Props_abbr, Props_address, Props_area, Props_article, Props_aside, Props_audio, Props_b, Props_base, Props_bdi, Props_bdo, Props_blockquote, Props_body, Props_br, Props_button, Props_canvas, Props_caption, Props_cite, Props_code, Props_col, Props_colgroup, Props_data, Props_datalist, Props_dd, Props_del, Props_details, Props_dfn, Props_dialog, Props_div, Props_dl, Props_dt, Props_em, Props_embed, Props_fieldset, Props_figcaption, Props_figure, Props_footer, Props_form, Props_h1, Props_h2, Props_h3, Props_h4, Props_h5, Props_h6, Props_head, Props_header, Props_hgroup, Props_hr, Props_html, Props_i, Props_iframe, Props_img, Props_input, Props_ins, Props_kbd, Props_keygen, Props_label, Props_legend, Props_li, Props_link, Props_main, Props_map, Props_mark, Props_math, Props_menu, Props_menuitem, Props_meta, Props_meter, Props_nav, Props_noscript, Props_object, Props_ol, Props_optgroup, Props_option, Props_output, Props_p, Props_param, Props_picture, Props_pre, Props_progress, Props_q, Props_rb, Props_rp, Props_rt, Props_rtc, Props_ruby, Props_s, Props_samp, Props_script, Props_section, Props_select, Props_slot, Props_small, Props_source, Props_span, Props_strong, Props_style, Props_sub, Props_summary, Props_sup, Props_svg, Props_table, Props_tbody, Props_td, Props_template, Props_textarea, Props_tfoot, Props_th, Props_thead, Props_time, Props_title, Props_tr, Props_track, Props_u, Props_ul, Props_var, Props_video, Props_wbr, a, a_, abbr, abbr_, address, address_, area, article, article_, aside, aside_, audio, audio_, b, b_, base, bdi, bdi_, bdo, bdo_, blockquote, blockquote_, body, body_, br, button, button_, canvas, canvas_, caption, caption_, cite, cite_, code, code_, col, colgroup, colgroup_, data', data_, datalist, datalist_, dd, dd_, del, del_, details, details_, dfn, dfn_, dialog, dialog_, div, div_, dl, dl_, dt, dt_, em, em_, embed, fieldset, fieldset_, figcaption, figcaption_, figure, figure_, footer, footer_, form, form_, h1, h1_, h2, h2_, h3, h3_, h4, h4_, h5, h5_, h6, h6_, head, head_, header, header_, hgroup, hgroup_, hr, html, html_, i, i_, iframe, iframe_, img, input, ins, ins_, kbd, kbd_, keygen, keygen_, label, label_, legend, legend_, li, li_, link, main, main_, map, map_, mark, mark_, math, math_, menu, menu_, menuitem, menuitem_, meta, meter, meter_, nav, nav_, noscript, noscript_, object, object_, ol, ol_, optgroup, optgroup_, option, option_, output, output_, p, p_, param, picture, picture_, pre, pre_, progress, progress_, q, q_, rb, rb_, rp, rp_, rt, rt_, rtc, rtc_, ruby, ruby_, s, s_, samp, samp_, script, script_, section, section_, select, select_, slot, slot_, small, small_, source, span, span_, strong, strong_, style, style_, sub, sub_, summary, summary_, sup, sup_, svg, svg_, table, table_, tbody, tbody_, td, td_, template, template_, textarea, textarea_, tfoot, tfoot_, th, th_, thead, thead_, time, time_, title, title_, tr, tr_, track, u, u_, ul, ul_, var, var_, video, video_, wbr) as Generated import React.Basic.DOM.Internal (CSS, SharedProps, unsafeCreateDOMComponent) as Internal import Unsafe.Coerce (unsafeCoerce) -import Web.DOM (Element, Node) +import Web.DOM (Element) -- | Render or update/re-render a component tree into -- | a DOM element. @@ -85,21 +80,6 @@ unmount = runEffectFn1 unmountComponentAtNode_ foreign import unmountComponentAtNode_ :: EffectFn1 Element Boolean --- | Returns the current DOM node associated with the given --- | instance, or an Error if no node was found or the given --- | instance was not mounted. --- | --- | Note: Relies on `ReactDOM.findDOMNode` -findDOMNode :: ReactComponentInstance -> Effect (Either Error Node) -findDOMNode instance_ = try do - node <- runEffectFn1 findDOMNode_ instance_ - case toMaybe node of - Nothing -> throw "Node not found." - Just n -> pure n - --- | Warning: Relies on `ReactDOM.findDOMNode` which may throw exceptions -foreign import findDOMNode_ :: EffectFn1 ReactComponentInstance (Nullable Node) - -- | Divert a render tree into a separate DOM node. The node's -- | content will be overwritten and managed by React, similar -- | to `render` and `hydrate`. diff --git a/src/React/Basic/DOM/Events.purs b/src/React/Basic/DOM/Events.purs index 55fa568..df19d27 100644 --- a/src/React/Basic/DOM/Events.purs +++ b/src/React/Basic/DOM/Events.purs @@ -1,7 +1,9 @@ -- | This module defines safe DOM event function and property accessors. module React.Basic.DOM.Events - ( bubbles + ( capture + , capture_ + , bubbles , cancelable , eventPhase , eventPhaseNone @@ -44,11 +46,18 @@ import Prelude import Data.Maybe (Maybe) import Data.Nullable (toMaybe) +import Effect (Effect) import Effect.Unsafe (unsafePerformEffect) -import React.Basic.Events (EventFn, SyntheticEvent, unsafeEventFn) +import React.Basic.Events (EventFn, EventHandler, SyntheticEvent, handler, unsafeEventFn) import Unsafe.Coerce (unsafeCoerce) import Web.Event.Internal.Types (Event, EventTarget) +capture :: forall a. EventFn SyntheticEvent a -> (a -> Effect Unit) -> EventHandler +capture eventFn = handler (preventDefault >>> stopPropagation >>> eventFn) + +capture_ :: Effect Unit -> EventHandler +capture_ = capture identity <<< const + -- | General event fields bubbles :: EventFn SyntheticEvent Boolean diff --git a/src/React/Basic/DOM/Internal.purs b/src/React/Basic/DOM/Internal.purs index 156dd44..c3b6240 100644 --- a/src/React/Basic/DOM/Internal.purs +++ b/src/React/Basic/DOM/Internal.purs @@ -1,6 +1,6 @@ module React.Basic.DOM.Internal where -import React.Basic (ReactComponent) +import React.Basic (Component) import React.Basic.Events (EventHandler) import Unsafe.Coerce (unsafeCoerce) @@ -93,5 +93,5 @@ type SharedProps specific = | specific ) -unsafeCreateDOMComponent :: forall props. String -> ReactComponent props +unsafeCreateDOMComponent :: forall props. String -> Component props unsafeCreateDOMComponent = unsafeCoerce From f98c4c926d3c20838afb2c833cd0462f6fd6317e Mon Sep 17 00:00:00 2001 From: Michael Trotter Date: Thu, 1 Nov 2018 13:28:19 -0600 Subject: [PATCH 02/12] Add missing Compat exports --- src/React/Basic/Compat.purs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/React/Basic/Compat.purs b/src/React/Basic/Compat.purs index 3d23a58..966542b 100644 --- a/src/React/Basic/Compat.purs +++ b/src/React/Basic/Compat.purs @@ -8,7 +8,7 @@ import Prelude import Effect (Effect) import Effect.Unsafe (unsafePerformEffect) -import React.Basic (Component, JSX, element) +import React.Basic (Component, JSX, element, elementKeyed, empty, keyed, fragment) import React.Basic (Tuple(..), component, ref, useEffect, useState) as React -- | Supports a common subset of the v2 API to ease the upgrade process From 674069048ee57a98448fcde52f6f670f7ad1fe4b Mon Sep 17 00:00:00 2001 From: Michael Trotter Date: Thu, 1 Nov 2018 14:35:41 -0600 Subject: [PATCH 03/12] Add useReducer --- bower.json | 7 +- src/React/Basic.js | 17 ++++- src/React/Basic.purs | 165 +++++++++++++++++++++++++++++++++---------- 3 files changed, 150 insertions(+), 39 deletions(-) diff --git a/bower.json b/bower.json index d0a28e5..a4cd8b5 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,12 @@ { "name": "purescript-react-basic", "license": "Apache-2.0", - "ignore": ["**/.*", "node_modules", "bower_components", "output"], + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "output" + ], "repository": { "type": "git", "url": "git://github.com/lumihq/purescript-react-basic.git" diff --git a/src/React/Basic.js b/src/React/Basic.js index 6ab8747..f4efb83 100644 --- a/src/React/Basic.js +++ b/src/React/Basic.js @@ -8,10 +8,23 @@ exports.useState_ = function(initialState) { return { value: state[0], setValue: state[1] }; }; -exports.useEffect_ = function(effect, inputs) { - React.useEffect(effect, inputs); +exports.useEffect_ = React.useEffect; + +exports.useReducer_ = function(reducer, initialState, initialAction) { + var state = React.useReducer(reducer, initialState, initialAction); + return { state: state[0], dispatch: state[1] }; +}; + +exports.readRef_ = function(ref) { + return ref.current; }; +exports.writeRef_ = function(ref, a) { + ref.current = a; +}; + +exports.useRef_ = React.useRef; + exports.empty = null; exports.keyed_ = function(key, child) { diff --git a/src/React/Basic.purs b/src/React/Basic.purs index 5c49fa0..0697793 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -6,9 +6,16 @@ module React.Basic , component , useState , useEffect + , useReducer + , StateUpdate(..) , Ref - , class ToRef - , ref + , readRef + , renderRef + , writeRef + , useRef + , Key + , class ToKey + , toKey , empty , keyed , fragment @@ -21,11 +28,13 @@ module React.Basic import Prelude -import Data.Function.Uncurried (Fn2, runFn2) +import Data.Function.Uncurried (Fn2, mkFn2, runFn2) +import Data.Maybe (Maybe) +import Data.Nullable (Nullable, toNullable) import Data.Tuple (Tuple(..)) import Data.Tuple.Nested (tuple2, (/\)) import Effect (Effect) -import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, runEffectFn1, runEffectFn2) +import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn3) import Unsafe.Coerce (unsafeCoerce) newtype Component props = Component (EffectFn1 props JSX) @@ -46,57 +55,98 @@ instance bindRender :: Bind Render where type CreateComponent props = Effect (Component props) -component :: forall props. String -> (props -> Render JSX) -> CreateComponent props +component + :: forall props + . String + -> (props -> Render JSX) + -> CreateComponent props component name render = let c = Component (mkEffectFn1 (unsafeCoerce render)) in runEffectFn2 unsafeSetDisplayName name c -- | useState -useState :: forall state. state -> Render (Tuple state ((state -> state) -> Effect Unit)) +useState + :: forall state + . state + -> Render (Tuple state ((state -> state) -> Effect Unit)) useState initialState = Render do { value, setValue } <- runEffectFn1 useState_ initialState pure (Tuple value (runEffectFn1 setValue)) -foreign import useState_ - :: forall state - . EffectFn1 - state - { value :: state - , setValue :: EffectFn1 (state -> state) Unit - } - --- | useState -useEffect :: Array Ref -> Effect (Effect Unit) -> Render Unit +-- | useEffect +useEffect :: Array Key -> Effect (Effect Unit) -> Render Unit useEffect refs effect = Render (runEffectFn2 useEffect_ effect refs) -foreign import useEffect_ - :: EffectFn2 - (Effect (Effect Unit)) - (Array Ref) - Unit +-- | useReducer +-- | TODO: add note about conditionally updating state +useReducer + :: forall state action + . ToKey state + => (state -> action -> state) + -> state + -> Maybe action + -> Render (Tuple state (action -> Effect Unit)) +useReducer reducer initialState initialAction = Render do + { state, dispatch } <- runEffectFn3 useReducer_ (mkFn2 reducer) initialState (toNullable initialAction) + pure (Tuple state (runEffectFn1 dispatch)) + +-- | Used by the `reducer` function to describe the kind of state +-- | update or side effects desired. +-- | +-- | __*See also:* `ComponentSpec`__ +data StateUpdate state + = NoUpdate + | Update state + | SideEffects (Effect Unit) + | UpdateAndSideEffects state (Effect Unit) + +data Ref a + +readRef :: forall a. Ref a -> Effect a +readRef = runEffectFn1 readRef_ + +renderRef :: forall a. Ref a -> Render a +renderRef ref = Render (readRef ref) + +writeRef :: forall a. Ref a -> a -> Effect Unit +writeRef = runEffectFn2 writeRef_ + +useRef + :: forall a + . a + -> Render (Ref a) +useRef initialValue = Render do + runEffectFn1 useRef_ initialValue -data Ref +-- | Keys represent values React uses to check for changes. +-- | This is done using JavaScript's reference equality (`===`), +-- | so complicated types may want to implement `ToKey` so that +-- | it returns a primative like a `String`. A timestamp appended +-- | to a unique ID, for example. Less strict cases can implement +-- | `ToKey` using `unsafeCoerce`, while some extreme cases may +-- | need a hashing or stringifying mechanism. +data Key -class ToRef a where - ref :: a -> Ref +class ToKey a where + toKey :: a -> Key -instance trString :: ToRef String where - ref = unsafeCoerce +instance trString :: ToKey String where + toKey = unsafeCoerce -instance trInt :: ToRef Int where - ref = unsafeCoerce +instance trInt :: ToKey Int where + toKey = unsafeCoerce -instance trNumber :: ToRef Number where - ref = unsafeCoerce +instance trNumber :: ToKey Number where + toKey = unsafeCoerce -instance trBoolean :: ToRef Boolean where - ref = unsafeCoerce +instance trBoolean :: ToKey Boolean where + toKey = unsafeCoerce -instance trRecord :: ToRef (Record a) where - ref = unsafeCoerce +instance trRecord :: ToKey (Record a) where + toKey = unsafeCoerce -instance trArray :: ToRef (Array a) where - ref = unsafeCoerce +instance trArray :: ToKey (Array a) where + toKey = unsafeCoerce -- | Represents rendered React VDOM (the result of calling `React.createElement` -- | in JavaScript). @@ -183,6 +233,49 @@ foreign import unsafeSetDisplayName :: forall props . EffectFn2 String (Component props) (Component props) +foreign import useState_ + :: forall state + . EffectFn1 + state + { value :: state + , setValue :: EffectFn1 (state -> state) Unit + } + +foreign import useEffect_ + :: EffectFn2 + (Effect (Effect Unit)) + (Array Key) + Unit + +foreign import useReducer_ + :: forall state action + . EffectFn3 + (Fn2 state action state) + state + (Nullable action) + { state :: state + , dispatch :: EffectFn1 action Unit + } + +foreign import readRef_ + :: forall a + . EffectFn1 + (Ref a) + a + +foreign import writeRef_ + :: forall a + . EffectFn2 + (Ref a) + a + Unit + +foreign import useRef_ + :: forall a + . EffectFn1 + a + (Ref a) + foreign import keyed_ :: Fn2 String JSX JSX foreign import element_ From a12e53cf8f5e1258ce4be9394a50698e6fb6ed43 Mon Sep 17 00:00:00 2001 From: Michael Trotter Date: Thu, 1 Nov 2018 17:12:58 -0600 Subject: [PATCH 04/12] Add Maybe and Nullable toKey --- examples/component/src/ToggleButton.purs | 4 ++-- examples/counter/src/Counter.purs | 4 ++-- src/React/Basic.purs | 6 ++++++ src/React/Basic/Compat.purs | 4 ++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/examples/component/src/ToggleButton.purs b/examples/component/src/ToggleButton.purs index c89c3e9..7c10ed8 100644 --- a/examples/component/src/ToggleButton.purs +++ b/examples/component/src/ToggleButton.purs @@ -3,7 +3,7 @@ module ToggleButton where import Prelude import Effect.Console (log) -import React.Basic (CreateComponent, component, ref, useEffect, useState, (/\)) +import React.Basic (CreateComponent, component, toKey, useEffect, useState, (/\)) import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) @@ -12,7 +12,7 @@ mkToggleButton = do component "ToggleButton" \{ label } -> do on /\ setOn <- useState false - useEffect [ref on] do + useEffect [toKey on] do log $ "State: " <> if on then "On" else "Off" pure (pure unit) diff --git a/examples/counter/src/Counter.purs b/examples/counter/src/Counter.purs index 9e89e21..aaaedf2 100644 --- a/examples/counter/src/Counter.purs +++ b/examples/counter/src/Counter.purs @@ -3,7 +3,7 @@ module Counter where import Prelude import Effect (Effect) -import React.Basic (CreateComponent, component, ref, useEffect, useState, (/\)) +import React.Basic (CreateComponent, component, toKey, useEffect, useState, (/\)) import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) @@ -12,7 +12,7 @@ mkCounter = do component "Counter" \props -> do counter /\ setCounter <- useState 0 - useEffect [ref counter] do + useEffect [toKey counter] do setDocumentTitle $ "Count: " <> show counter pure (pure unit) diff --git a/src/React/Basic.purs b/src/React/Basic.purs index 0697793..1bca12a 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -148,6 +148,12 @@ instance trRecord :: ToKey (Record a) where instance trArray :: ToKey (Array a) where toKey = unsafeCoerce +instance trNullable :: ToKey (Nullable a) where + toKey = unsafeCoerce + +instance trMaybe :: ToKey (Maybe a) where + toKey a = toKey (toNullable a) + -- | Represents rendered React VDOM (the result of calling `React.createElement` -- | in JavaScript). -- | diff --git a/src/React/Basic/Compat.purs b/src/React/Basic/Compat.purs index 966542b..6662481 100644 --- a/src/React/Basic/Compat.purs +++ b/src/React/Basic/Compat.purs @@ -9,7 +9,7 @@ import Prelude import Effect (Effect) import Effect.Unsafe (unsafePerformEffect) import React.Basic (Component, JSX, element, elementKeyed, empty, keyed, fragment) -import React.Basic (Tuple(..), component, ref, useEffect, useState) as React +import React.Basic (Tuple(..), component, toKey, useEffect, useState) as React -- | Supports a common subset of the v2 API to ease the upgrade process component @@ -23,7 +23,7 @@ component component { displayName, initialState, receiveProps, render } = unsafePerformEffect do React.component displayName \props -> do React.Tuple state setState <- React.useState initialState - React.useEffect [React.ref props, React.ref state] do + React.useEffect [React.toKey props, React.toKey state] do receiveProps { props, state, setState } pure $ pure unit pure $ render { props, state, setState } From 390960e9cf343df743a99172f430c1cf02bba8f0 Mon Sep 17 00:00:00 2001 From: Michael Trotter Date: Fri, 2 Nov 2018 11:51:45 -0600 Subject: [PATCH 05/12] Remove more dangerous instances --- examples/component/src/Container.purs | 4 +-- examples/component/src/ToggleButton.purs | 4 +-- .../controlled-input/src/ControlledInput.purs | 6 ++-- examples/counter/src/Counter.purs | 4 +-- src/React/Basic.purs | 28 +++++++++++-------- src/React/Basic/Compat.purs | 8 +++--- 6 files changed, 30 insertions(+), 24 deletions(-) diff --git a/examples/component/src/Container.purs b/examples/component/src/Container.purs index 8f40ea0..103e986 100644 --- a/examples/component/src/Container.purs +++ b/examples/component/src/Container.purs @@ -2,7 +2,7 @@ module Container where import Prelude -import React.Basic (CreateComponent, component, element) +import React.Basic (CreateComponent, component, element, render) import React.Basic.DOM as R import ToggleButton (mkToggleButton) @@ -11,7 +11,7 @@ mkToggleButtonContainer = do toggleButton <- mkToggleButton component "Container" \_ -> do - pure $ R.div + render $ R.div { children: [ element toggleButton { label: "A" } , element toggleButton { label: "B" } diff --git a/examples/component/src/ToggleButton.purs b/examples/component/src/ToggleButton.purs index 7c10ed8..31939a7 100644 --- a/examples/component/src/ToggleButton.purs +++ b/examples/component/src/ToggleButton.purs @@ -3,7 +3,7 @@ module ToggleButton where import Prelude import Effect.Console (log) -import React.Basic (CreateComponent, component, toKey, useEffect, useState, (/\)) +import React.Basic (CreateComponent, component, render, toKey, useEffect, useState, (/\)) import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) @@ -16,7 +16,7 @@ mkToggleButton = do log $ "State: " <> if on then "On" else "Off" pure (pure unit) - pure $ R.button + render $ R.button { onClick: capture_ $ setOn not , children: [ R.text label diff --git a/examples/controlled-input/src/ControlledInput.purs b/examples/controlled-input/src/ControlledInput.purs index cbf31ef..770ba3f 100644 --- a/examples/controlled-input/src/ControlledInput.purs +++ b/examples/controlled-input/src/ControlledInput.purs @@ -3,7 +3,7 @@ module ControlledInput where import Prelude import Data.Maybe (Maybe(..), fromMaybe, maybe) -import React.Basic (CreateComponent, Render, component, fragment, useState, (/\)) +import React.Basic (CreateComponent, Render, component, fragment, render, unsafeRender, useState, (/\)) import React.Basic.DOM as R import React.Basic.DOM.Events (capture, targetValue, timeStamp) import React.Basic.Events (EventHandler, merge) @@ -14,7 +14,7 @@ mkControlledInput = firstName <- useInput "hello" lastName <- useInput "world" - pure $ R.form_ + render $ R.form_ [ renderInput firstName , renderInput lastName ] @@ -29,7 +29,7 @@ mkControlledInput = useInput :: String -> Render { onChange :: EventHandler, value :: String, lastChanged :: Maybe Number } useInput initialValue = do { value, lastChanged } /\ replaceState <- useState { value: initialValue, lastChanged: Nothing } - pure + unsafeRender { onChange: capture (merge { targetValue, timeStamp }) \{ timeStamp, targetValue } -> do diff --git a/examples/counter/src/Counter.purs b/examples/counter/src/Counter.purs index aaaedf2..3bd0775 100644 --- a/examples/counter/src/Counter.purs +++ b/examples/counter/src/Counter.purs @@ -3,7 +3,7 @@ module Counter where import Prelude import Effect (Effect) -import React.Basic (CreateComponent, component, toKey, useEffect, useState, (/\)) +import React.Basic (CreateComponent, component, render, toKey, useEffect, useState, (/\)) import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) @@ -16,7 +16,7 @@ mkCounter = do setDocumentTitle $ "Count: " <> show counter pure (pure unit) - pure $ R.button + render $ R.button { onClick: capture_ $ setCounter (_ + 1) , children: [ R.text $ "Increment: " <> show counter ] } diff --git a/src/React/Basic.purs b/src/React/Basic.purs index 1bca12a..07b4ccd 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -1,6 +1,8 @@ module React.Basic ( Component , Render + , render + , unsafeRender , CreateComponent , JSX , component @@ -41,17 +43,21 @@ newtype Component props = Component (EffectFn1 props JSX) newtype Render a = Render (Effect a) -instance functorRender :: Functor Render where - map f (Render a) = Render (map f a) +derive newtype instance functorRender :: Functor Render +derive newtype instance applyRender :: Apply Render +derive newtype instance bindRender :: Bind Render -instance applyRender :: Apply Render where - apply (Render f) (Render a) = Render (apply f a) +-- | render +render :: JSX -> Render JSX +render jsx = Render (pure jsx) -instance applicativeRender :: Applicative Render where - pure a = Render (pure a) - -instance bindRender :: Bind Render where - bind (Render m) f = Render (bind m \a -> case f a of Render b -> b) +-- | Conditional logic is not allowed in Render, making +-- | Applicative `pure` unsafe. It's still occasionally +-- | required, however, to extract Render logic into more +-- | advanced helper functions. Never nest `unsafeRender` +-- | in a conditionally or dynamically (if, case, for). +unsafeRender :: forall a. a -> Render a +unsafeRender a = Render (pure a) type CreateComponent props = Effect (Component props) @@ -60,8 +66,8 @@ component . String -> (props -> Render JSX) -> CreateComponent props -component name render = - let c = Component (mkEffectFn1 (unsafeCoerce render)) +component name renderFn = + let c = Component (mkEffectFn1 (unsafeCoerce renderFn)) in runEffectFn2 unsafeSetDisplayName name c -- | useState diff --git a/src/React/Basic/Compat.purs b/src/React/Basic/Compat.purs index 6662481..c5d17cd 100644 --- a/src/React/Basic/Compat.purs +++ b/src/React/Basic/Compat.purs @@ -8,8 +8,8 @@ import Prelude import Effect (Effect) import Effect.Unsafe (unsafePerformEffect) -import React.Basic (Component, JSX, element, elementKeyed, empty, keyed, fragment) -import React.Basic (Tuple(..), component, toKey, useEffect, useState) as React +import React.Basic (Component, JSX, element, elementKeyed, empty, fragment, keyed, unsafeRender) +import React.Basic (Tuple(..), component, render, toKey, useEffect, useState) as React -- | Supports a common subset of the v2 API to ease the upgrade process component @@ -26,7 +26,7 @@ component { displayName, initialState, receiveProps, render } = unsafePerformEff React.useEffect [React.toKey props, React.toKey state] do receiveProps { props, state, setState } pure $ pure unit - pure $ render { props, state, setState } + React.render $ render { props, state, setState } -- | Supports a common subset of the v2 API to ease the upgrade process stateless @@ -36,4 +36,4 @@ stateless } -> Component { | props } stateless { displayName, render } = unsafePerformEffect do - React.component displayName (pure <<< render) + React.component displayName (React.render <<< render) From 0cc74b8b60beb2fa211384da8cceb7685047bfc1 Mon Sep 17 00:00:00 2001 From: Madeline Trotter Date: Sun, 16 Dec 2018 01:33:32 -0700 Subject: [PATCH 06/12] IxMonad! --- bower.json | 4 +- examples/component/src/Container.purs | 2 +- examples/component/src/ToggleButton.purs | 10 +- .../controlled-input/src/ControlledInput.purs | 39 +-- examples/counter/src/Counter.purs | 11 +- src/React/Basic.js | 34 +-- src/React/Basic.purs | 230 +++++++++--------- src/React/Basic/Compat.purs | 28 ++- 8 files changed, 193 insertions(+), 165 deletions(-) diff --git a/bower.json b/bower.json index a4cd8b5..7c0d96c 100644 --- a/bower.json +++ b/bower.json @@ -22,7 +22,9 @@ "purescript-typelevel-prelude": "^3.0.0", "purescript-unsafe-coerce": "^4.0.0", "purescript-web-dom": "^1.0.0", - "purescript-web-events": "^1.0.0" + "purescript-web-events": "^1.0.0", + "purescript-profunctor-lenses": "^5.0.0", + "purescript-indexed-monad": "^1.0.0" }, "devDependencies": { "purescript-web-html": "^1.0.0" diff --git a/examples/component/src/Container.purs b/examples/component/src/Container.purs index 103e986..6efcdc5 100644 --- a/examples/component/src/Container.purs +++ b/examples/component/src/Container.purs @@ -10,7 +10,7 @@ mkToggleButtonContainer :: CreateComponent {} mkToggleButtonContainer = do toggleButton <- mkToggleButton - component "Container" \_ -> do + component "Container" \_ _ _ -> do render $ R.div { children: [ element toggleButton { label: "A" } diff --git a/examples/component/src/ToggleButton.purs b/examples/component/src/ToggleButton.purs index 31939a7..efaa4a6 100644 --- a/examples/component/src/ToggleButton.purs +++ b/examples/component/src/ToggleButton.purs @@ -9,12 +9,10 @@ import React.Basic.DOM.Events (capture_) mkToggleButton :: CreateComponent { label :: String } mkToggleButton = do - component "ToggleButton" \{ label } -> do + component "ToggleButton" \bind discard { label } -> do on /\ setOn <- useState false - useEffect [toKey on] do - log $ "State: " <> if on then "On" else "Off" - pure (pure unit) + useEffect [toKey on] $ logState on render $ R.button { onClick: capture_ $ setOn not @@ -23,3 +21,7 @@ mkToggleButton = do , R.text if on then " On" else " Off" ] } + where + logState on = do + log $ "State: " <> if on then "On" else "Off" + pure (pure unit) diff --git a/examples/controlled-input/src/ControlledInput.purs b/examples/controlled-input/src/ControlledInput.purs index 770ba3f..e4766be 100644 --- a/examples/controlled-input/src/ControlledInput.purs +++ b/examples/controlled-input/src/ControlledInput.purs @@ -2,15 +2,17 @@ module ControlledInput where import Prelude +import Control.Applicative.Indexed (ipure) +import Control.Bind.Indexed (ibind) import Data.Maybe (Maybe(..), fromMaybe, maybe) -import React.Basic (CreateComponent, Render, component, fragment, render, unsafeRender, useState, (/\)) +import React.Basic (CreateComponent, Render, RenderState, component, fragment, render, useState, (/\)) import React.Basic.DOM as R import React.Basic.DOM.Events (capture, targetValue, timeStamp) import React.Basic.Events (EventHandler, merge) mkControlledInput :: CreateComponent {} mkControlledInput = - component "ControlledInput" \props -> do + component "ControlledInput" \bind discard props -> do firstName <- useInput "hello" lastName <- useInput "world" @@ -26,17 +28,22 @@ mkControlledInput = , R.p_ [ R.text ("Changed at = " <> maybe "never" show input.lastChanged) ] ] -useInput :: String -> Render { onChange :: EventHandler, value :: String, lastChanged :: Maybe Number } -useInput initialValue = do - { value, lastChanged } /\ replaceState <- useState { value: initialValue, lastChanged: Nothing } - unsafeRender - { onChange: capture - (merge { targetValue, timeStamp }) - \{ timeStamp, targetValue } -> do - replaceState \_ -> - { value: fromMaybe "" targetValue - , lastChanged: Just timeStamp - } - , value - , lastChanged - } +type InputState state = { value :: String, lastChanged :: Maybe Number | state } +useInput :: forall hooks. String -> Render hooks (RenderState (InputState ()) hooks) (InputState ( onChange :: EventHandler )) +useInput initialValue = + let + bind = ibind + discard = ibind + in do + { value, lastChanged } /\ replaceState <- useState { value: initialValue, lastChanged: Nothing } + ipure + { onChange: capture + (merge { targetValue, timeStamp }) + \{ timeStamp, targetValue } -> do + replaceState \_ -> + { value: fromMaybe "" targetValue + , lastChanged: Just timeStamp + } + , value + , lastChanged + } diff --git a/examples/counter/src/Counter.purs b/examples/counter/src/Counter.purs index 3bd0775..b21a861 100644 --- a/examples/counter/src/Counter.purs +++ b/examples/counter/src/Counter.purs @@ -9,16 +9,19 @@ import React.Basic.DOM.Events (capture_) mkCounter :: CreateComponent {} mkCounter = do - component "Counter" \props -> do + component "Counter" \bind discard props -> do counter /\ setCounter <- useState 0 - useEffect [toKey counter] do - setDocumentTitle $ "Count: " <> show counter - pure (pure unit) + useEffect [toKey counter] $ docTitle counter render $ R.button { onClick: capture_ $ setCounter (_ + 1) , children: [ R.text $ "Increment: " <> show counter ] } + where + docTitle counter = do + setDocumentTitle $ "Count: " <> show counter + pure (pure unit) + foreign import setDocumentTitle :: String -> Effect Unit diff --git a/src/React/Basic.js b/src/React/Basic.js index f4efb83..483af49 100644 --- a/src/React/Basic.js +++ b/src/React/Basic.js @@ -3,27 +3,33 @@ var React = require("react"); var Fragment = React.Fragment || "div"; -exports.useState_ = function(initialState) { - var state = React.useState(initialState); - return { value: state[0], setValue: state[1] }; +exports.useState_ = function(tuple, initialState) { + var r = React.useState(initialState); + var state = r[0]; + var setState = r[1]; + return tuple(state)(function(update) { + return function() { + return setState(update); + }; + }); }; exports.useEffect_ = React.useEffect; -exports.useReducer_ = function(reducer, initialState, initialAction) { - var state = React.useReducer(reducer, initialState, initialAction); - return { state: state[0], dispatch: state[1] }; -}; +// exports.useReducer_ = function(reducer, initialState, initialAction) { +// var state = React.useReducer(reducer, initialState, initialAction); +// return { state: state[0], dispatch: state[1] }; +// }; -exports.readRef_ = function(ref) { - return ref.current; -}; +// exports.readRef_ = function(ref) { +// return ref.current; +// }; -exports.writeRef_ = function(ref, a) { - ref.current = a; -}; +// exports.writeRef_ = function(ref, a) { +// ref.current = a; +// }; -exports.useRef_ = React.useRef; +// exports.useRef_ = React.useRef; exports.empty = null; diff --git a/src/React/Basic.purs b/src/React/Basic.purs index 07b4ccd..2f48d71 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -1,20 +1,21 @@ module React.Basic ( Component , Render + , RenderJSX , render - , unsafeRender , CreateComponent , JSX , component + , RenderState , useState + , RenderEffect , useEffect - , useReducer - , StateUpdate(..) - , Ref - , readRef - , renderRef - , writeRef - , useRef + -- , useReducer + -- , Ref + -- , readRef + -- , renderRef + -- , writeRef + -- , useRef , Key , class ToKey , toKey @@ -30,99 +31,103 @@ module React.Basic import Prelude -import Data.Function.Uncurried (Fn2, mkFn2, runFn2) +import Control.Applicative.Indexed (class IxApplicative, ipure) +import Control.Apply.Indexed (class IxApply) +import Control.Bind.Indexed (class IxBind, ibind) +import Data.Function.Uncurried (Fn2, runFn2) +import Data.Functor.Indexed (class IxFunctor) import Data.Maybe (Maybe) import Data.Nullable (Nullable, toNullable) import Data.Tuple (Tuple(..)) import Data.Tuple.Nested (tuple2, (/\)) import Effect (Effect) -import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn3) +import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, runEffectFn2) import Unsafe.Coerce (unsafeCoerce) newtype Component props = Component (EffectFn1 props JSX) -newtype Render a = Render (Effect a) +foreign import data Render :: Type -> Type -> Type -> Type -derive newtype instance functorRender :: Functor Render -derive newtype instance applyRender :: Apply Render -derive newtype instance bindRender :: Bind Render +instance ixFunctorRender :: IxFunctor Render where + imap = unsafeCoerce (map :: forall a b. (a -> b) -> Effect a -> Effect b) --- | render -render :: JSX -> Render JSX -render jsx = Render (pure jsx) +instance ixApplyRender :: IxApply Render where + iapply = unsafeCoerce (apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b) --- | Conditional logic is not allowed in Render, making --- | Applicative `pure` unsafe. It's still occasionally --- | required, however, to extract Render logic into more --- | advanced helper functions. Never nest `unsafeRender` --- | in a conditionally or dynamically (if, case, for). -unsafeRender :: forall a. a -> Render a -unsafeRender a = Render (pure a) +instance ixBindRender :: IxBind Render where + ibind = unsafeCoerce (bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b) + +instance ixApplicativeRender :: IxApplicative Render where + ipure = unsafeCoerce (pure :: forall a. a -> Effect a) + +type IxBindFn = forall m x y z a b. IxBind m => m x y a -> (a -> m y z b) -> m x z b + +type RenderFn props = forall hooks. IxBindFn -> IxBindFn -> props -> Render hooks RenderJSX JSX type CreateComponent props = Effect (Component props) component :: forall props . String - -> (props -> Render JSX) + -> RenderFn props -> CreateComponent props component name renderFn = - let c = Component (mkEffectFn1 (unsafeCoerce renderFn)) + let c = Component (mkEffectFn1 (unsafeCoerce (renderFn ibind ibind))) in runEffectFn2 unsafeSetDisplayName name c --- | useState +foreign import data RenderState :: Type -> Type -> Type + useState - :: forall state + :: forall hooks state . state - -> Render (Tuple state ((state -> state) -> Effect Unit)) -useState initialState = Render do - { value, setValue } <- runEffectFn1 useState_ initialState - pure (Tuple value (runEffectFn1 setValue)) - --- | useEffect -useEffect :: Array Key -> Effect (Effect Unit) -> Render Unit -useEffect refs effect = Render (runEffectFn2 useEffect_ effect refs) - --- | useReducer --- | TODO: add note about conditionally updating state -useReducer - :: forall state action - . ToKey state - => (state -> action -> state) - -> state - -> Maybe action - -> Render (Tuple state (action -> Effect Unit)) -useReducer reducer initialState initialAction = Render do - { state, dispatch } <- runEffectFn3 useReducer_ (mkFn2 reducer) initialState (toNullable initialAction) - pure (Tuple state (runEffectFn1 dispatch)) - --- | Used by the `reducer` function to describe the kind of state --- | update or side effects desired. --- | --- | __*See also:* `ComponentSpec`__ -data StateUpdate state - = NoUpdate - | Update state - | SideEffects (Effect Unit) - | UpdateAndSideEffects state (Effect Unit) - -data Ref a - -readRef :: forall a. Ref a -> Effect a -readRef = runEffectFn1 readRef_ - -renderRef :: forall a. Ref a -> Render a -renderRef ref = Render (readRef ref) - -writeRef :: forall a. Ref a -> a -> Effect Unit -writeRef = runEffectFn2 writeRef_ - -useRef - :: forall a - . a - -> Render (Ref a) -useRef initialValue = Render do - runEffectFn1 useRef_ initialValue + -> Render hooks (RenderState state hooks) (Tuple state ((state -> state) -> Effect Unit)) +useState initialState = unsafeCoerce do + runEffectFn2 useState_ Tuple initialState + +foreign import data RenderEffect :: Type -> Type + +useEffect + :: forall hooks + . Array Key + -> Effect (Effect Unit) + -> Render hooks (RenderEffect hooks) Unit +useEffect keys effect = unsafeCoerce (runEffectFn2 useEffect_ effect keys) + +foreign import data RenderJSX :: Type + +render :: forall hooks. JSX -> Render hooks RenderJSX JSX +render jsx = unsafeCoerce (ipure jsx :: forall a. Render a a JSX) + +-- -- | useReducer +-- -- | TODO: add note about conditionally updating state +-- useReducer +-- :: forall state action +-- . ToKey state +-- => (state -> action -> state) +-- -> state +-- -> Maybe action +-- -> Render (Tuple state (action -> Effect Unit)) +-- useReducer reducer initialState initialAction = Render do +-- { state, dispatch } <- runEffectFn3 useReducer_ (mkFn2 reducer) initialState (toNullable initialAction) +-- pure (Tuple state (runEffectFn1 dispatch)) + +-- data Ref a + +-- readRef :: forall a. Ref a -> Effect a +-- readRef = runEffectFn1 readRef_ + +-- renderRef :: forall a. Ref a -> Render a +-- renderRef ref = Render (readRef ref) + +-- writeRef :: forall a. Ref a -> a -> Effect Unit +-- writeRef = runEffectFn2 writeRef_ + +-- useRef +-- :: forall a +-- . a +-- -> Render (Ref a) +-- useRef initialValue = Render do +-- runEffectFn1 useRef_ initialValue -- | Keys represent values React uses to check for changes. -- | This is done using JavaScript's reference equality (`===`), @@ -208,8 +213,8 @@ foreign import fragment :: Array JSX -> JSX -- | __*See also:* `Component`, `elementKeyed`__ element :: forall props - . Component { | props } - -> { | props } + . Component {| props } + -> {| props } -> JSX element (Component c) props = runFn2 element_ c props @@ -221,7 +226,7 @@ element (Component c) props = runFn2 element_ c props -- | __*See also:* `Component`, `element`, React's documentation regarding the special `key` prop__ elementKeyed :: forall props - . Component { | props } + . Component {| props } -> { key :: String | props } -> JSX elementKeyed = runFn2 elementKeyed_ @@ -247,11 +252,10 @@ foreign import unsafeSetDisplayName foreign import useState_ :: forall state - . EffectFn1 + . EffectFn2 + (forall a b. a -> b -> Tuple a b) state - { value :: state - , setValue :: EffectFn1 (state -> state) Unit - } + (Tuple state ((state -> state) -> Effect Unit)) foreign import useEffect_ :: EffectFn2 @@ -259,34 +263,34 @@ foreign import useEffect_ (Array Key) Unit -foreign import useReducer_ - :: forall state action - . EffectFn3 - (Fn2 state action state) - state - (Nullable action) - { state :: state - , dispatch :: EffectFn1 action Unit - } - -foreign import readRef_ - :: forall a - . EffectFn1 - (Ref a) - a - -foreign import writeRef_ - :: forall a - . EffectFn2 - (Ref a) - a - Unit - -foreign import useRef_ - :: forall a - . EffectFn1 - a - (Ref a) +-- foreign import useReducer_ +-- :: forall state action +-- . EffectFn3 +-- (Fn2 state action state) +-- state +-- (Nullable action) +-- { state :: state +-- , dispatch :: EffectFn1 action Unit +-- } + +-- foreign import readRef_ +-- :: forall a +-- . EffectFn1 +-- (Ref a) +-- a + +-- foreign import writeRef_ +-- :: forall a +-- . EffectFn2 +-- (Ref a) +-- a +-- Unit + +-- foreign import useRef_ +-- :: forall a +-- . EffectFn1 +-- a +-- (Ref a) foreign import keyed_ :: Fn2 String JSX JSX @@ -296,4 +300,4 @@ foreign import element_ foreign import elementKeyed_ :: forall props - . Fn2 (Component { | props }) { key :: String | props } JSX + . Fn2 (Component {| props }) { key :: String | props } JSX diff --git a/src/React/Basic/Compat.purs b/src/React/Basic/Compat.purs index c5d17cd..8862c7f 100644 --- a/src/React/Basic/Compat.purs +++ b/src/React/Basic/Compat.purs @@ -8,32 +8,36 @@ import Prelude import Effect (Effect) import Effect.Unsafe (unsafePerformEffect) -import React.Basic (Component, JSX, element, elementKeyed, empty, fragment, keyed, unsafeRender) +import React.Basic (Component, JSX, RenderEffect, RenderState, element, elementKeyed, empty, fragment, keyed) import React.Basic (Tuple(..), component, render, toKey, useEffect, useState) as React -- | Supports a common subset of the v2 API to ease the upgrade process component :: forall props state . { displayName :: String - , initialState :: { | state } - , receiveProps :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> Effect Unit - , render :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> JSX + , initialState :: {| state } + , receiveProps :: { props :: {| props }, state :: {| state }, setState :: ({| state } -> {| state }) -> Effect Unit } -> Effect Unit + , render :: { props :: {| props }, state :: {| state }, setState :: ({| state } -> {| state }) -> Effect Unit } -> JSX } - -> Component { | props } + -> Component {| props } component { displayName, initialState, receiveProps, render } = unsafePerformEffect do - React.component displayName \props -> do + React.component displayName \bind discard props -> do React.Tuple state setState <- React.useState initialState - React.useEffect [React.toKey props, React.toKey state] do - receiveProps { props, state, setState } + React.useEffect [React.toKey props, React.toKey state] + (runReceiveProps { props, state, setState }) + React.render (render { props, state, setState }) + where + runReceiveProps self = do + receiveProps self pure $ pure unit - React.render $ render { props, state, setState } -- | Supports a common subset of the v2 API to ease the upgrade process stateless :: forall props . { displayName :: String - , render :: { | props } -> JSX + , render :: {| props } -> JSX } - -> Component { | props } + -> Component {| props } stateless { displayName, render } = unsafePerformEffect do - React.component displayName (React.render <<< render) + React.component displayName \bind discard props -> do + React.render (render props) From f4dbc986dd39a03fc03e737712a68fb8eea2dd42 Mon Sep 17 00:00:00 2001 From: Madeline Trotter Date: Mon, 17 Dec 2018 11:50:00 -0700 Subject: [PATCH 07/12] Use qualified-do --- examples/component/src/Container.purs | 2 +- examples/component/src/ToggleButton.purs | 11 +++-- .../controlled-input/src/ControlledInput.purs | 41 ++++++++++--------- examples/counter/src/Counter.purs | 12 +++--- src/React/Basic.purs | 21 ++++++---- src/React/Basic/Compat.purs | 15 +++---- 6 files changed, 52 insertions(+), 50 deletions(-) diff --git a/examples/component/src/Container.purs b/examples/component/src/Container.purs index 6efcdc5..ebabdc7 100644 --- a/examples/component/src/Container.purs +++ b/examples/component/src/Container.purs @@ -10,7 +10,7 @@ mkToggleButtonContainer :: CreateComponent {} mkToggleButtonContainer = do toggleButton <- mkToggleButton - component "Container" \_ _ _ -> do + component "Container" \_ -> render $ R.div { children: [ element toggleButton { label: "A" } diff --git a/examples/component/src/ToggleButton.purs b/examples/component/src/ToggleButton.purs index efaa4a6..210374a 100644 --- a/examples/component/src/ToggleButton.purs +++ b/examples/component/src/ToggleButton.purs @@ -4,15 +4,18 @@ import Prelude import Effect.Console (log) import React.Basic (CreateComponent, component, render, toKey, useEffect, useState, (/\)) +import React.Basic as React import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) mkToggleButton :: CreateComponent { label :: String } mkToggleButton = do - component "ToggleButton" \bind discard { label } -> do + component "ToggleButton" \{ label } -> React.do on /\ setOn <- useState false - useEffect [toKey on] $ logState on + useEffect [toKey on] do + log $ "State: " <> if on then "On" else "Off" + pure (pure unit) render $ R.button { onClick: capture_ $ setOn not @@ -21,7 +24,3 @@ mkToggleButton = do , R.text if on then " On" else " Off" ] } - where - logState on = do - log $ "State: " <> if on then "On" else "Off" - pure (pure unit) diff --git a/examples/controlled-input/src/ControlledInput.purs b/examples/controlled-input/src/ControlledInput.purs index e4766be..bfa3852 100644 --- a/examples/controlled-input/src/ControlledInput.purs +++ b/examples/controlled-input/src/ControlledInput.purs @@ -6,13 +6,14 @@ import Control.Applicative.Indexed (ipure) import Control.Bind.Indexed (ibind) import Data.Maybe (Maybe(..), fromMaybe, maybe) import React.Basic (CreateComponent, Render, RenderState, component, fragment, render, useState, (/\)) +import React.Basic as React import React.Basic.DOM as R import React.Basic.DOM.Events (capture, targetValue, timeStamp) import React.Basic.Events (EventHandler, merge) mkControlledInput :: CreateComponent {} mkControlledInput = - component "ControlledInput" \bind discard props -> do + component "ControlledInput" \props -> React.do firstName <- useInput "hello" lastName <- useInput "world" @@ -29,21 +30,23 @@ mkControlledInput = ] type InputState state = { value :: String, lastChanged :: Maybe Number | state } -useInput :: forall hooks. String -> Render hooks (RenderState (InputState ()) hooks) (InputState ( onChange :: EventHandler )) -useInput initialValue = - let - bind = ibind - discard = ibind - in do - { value, lastChanged } /\ replaceState <- useState { value: initialValue, lastChanged: Nothing } - ipure - { onChange: capture - (merge { targetValue, timeStamp }) - \{ timeStamp, targetValue } -> do - replaceState \_ -> - { value: fromMaybe "" targetValue - , lastChanged: Just timeStamp - } - , value - , lastChanged - } + +useInput + :: forall hooks + . String + -> Render hooks + (RenderState (InputState ()) hooks) + (InputState ( onChange :: EventHandler )) +useInput initialValue = React.do + { value, lastChanged } /\ replaceState <- useState { value: initialValue, lastChanged: Nothing } + ipure + { onChange: capture + (merge { targetValue, timeStamp }) + \{ timeStamp, targetValue } -> do + replaceState \_ -> + { value: fromMaybe "" targetValue + , lastChanged: Just timeStamp + } + , value + , lastChanged + } diff --git a/examples/counter/src/Counter.purs b/examples/counter/src/Counter.purs index b21a861..4d9a30a 100644 --- a/examples/counter/src/Counter.purs +++ b/examples/counter/src/Counter.purs @@ -4,24 +4,22 @@ import Prelude import Effect (Effect) import React.Basic (CreateComponent, component, render, toKey, useEffect, useState, (/\)) +import React.Basic as React import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) mkCounter :: CreateComponent {} mkCounter = do - component "Counter" \bind discard props -> do + component "Counter" \props -> React.do counter /\ setCounter <- useState 0 - useEffect [toKey counter] $ docTitle counter + useEffect [toKey counter] do + setDocumentTitle $ "Count: " <> show counter + pure mempty render $ R.button { onClick: capture_ $ setCounter (_ + 1) , children: [ R.text $ "Increment: " <> show counter ] } - where - docTitle counter = do - setDocumentTitle $ "Count: " <> show counter - pure (pure unit) - foreign import setDocumentTitle :: String -> Effect Unit diff --git a/src/React/Basic.purs b/src/React/Basic.purs index 2f48d71..b1dcbf7 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -1,6 +1,8 @@ module React.Basic ( Component , Render + , bind + , discard , RenderJSX , render , CreateComponent @@ -29,7 +31,8 @@ module React.Basic , module Data.Tuple.Nested ) where -import Prelude +import Prelude hiding (bind, discard) +import Prelude (bind) as Prelude import Control.Applicative.Indexed (class IxApplicative, ipure) import Control.Apply.Indexed (class IxApply) @@ -55,24 +58,26 @@ instance ixApplyRender :: IxApply Render where iapply = unsafeCoerce (apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b) instance ixBindRender :: IxBind Render where - ibind = unsafeCoerce (bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b) + ibind = unsafeCoerce (Prelude.bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b) -instance ixApplicativeRender :: IxApplicative Render where - ipure = unsafeCoerce (pure :: forall a. a -> Effect a) +bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b +bind = ibind -type IxBindFn = forall m x y z a b. IxBind m => m x y a -> (a -> m y z b) -> m x z b +discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b +discard = ibind -type RenderFn props = forall hooks. IxBindFn -> IxBindFn -> props -> Render hooks RenderJSX JSX +instance ixApplicativeRender :: IxApplicative Render where + ipure = unsafeCoerce (pure :: forall a. a -> Effect a) type CreateComponent props = Effect (Component props) component :: forall props . String - -> RenderFn props + -> (forall hooks. props -> Render hooks RenderJSX JSX) -> CreateComponent props component name renderFn = - let c = Component (mkEffectFn1 (unsafeCoerce (renderFn ibind ibind))) + let c = Component (mkEffectFn1 (unsafeCoerce renderFn)) in runEffectFn2 unsafeSetDisplayName name c foreign import data RenderState :: Type -> Type -> Type diff --git a/src/React/Basic/Compat.purs b/src/React/Basic/Compat.purs index 8862c7f..2768a7f 100644 --- a/src/React/Basic/Compat.purs +++ b/src/React/Basic/Compat.purs @@ -9,7 +9,7 @@ import Prelude import Effect (Effect) import Effect.Unsafe (unsafePerformEffect) import React.Basic (Component, JSX, RenderEffect, RenderState, element, elementKeyed, empty, fragment, keyed) -import React.Basic (Tuple(..), component, render, toKey, useEffect, useState) as React +import React.Basic (Tuple(..), bind, discard, component, render, toKey, useEffect, useState) as React -- | Supports a common subset of the v2 API to ease the upgrade process component @@ -21,15 +21,12 @@ component } -> Component {| props } component { displayName, initialState, receiveProps, render } = unsafePerformEffect do - React.component displayName \bind discard props -> do + React.component displayName \props -> React.do React.Tuple state setState <- React.useState initialState - React.useEffect [React.toKey props, React.toKey state] - (runReceiveProps { props, state, setState }) + React.useEffect [React.toKey props, React.toKey state] do + receiveProps { props, state, setState } + pure (pure unit) React.render (render { props, state, setState }) - where - runReceiveProps self = do - receiveProps self - pure $ pure unit -- | Supports a common subset of the v2 API to ease the upgrade process stateless @@ -39,5 +36,5 @@ stateless } -> Component {| props } stateless { displayName, render } = unsafePerformEffect do - React.component displayName \bind discard props -> do + React.component displayName \props -> React.do React.render (render props) From 828526ec9bc35a526893ecd187793cd78e8730b9 Mon Sep 17 00:00:00 2001 From: Madeline Trotter Date: Tue, 18 Dec 2018 20:46:30 -0700 Subject: [PATCH 08/12] Use reducer --- examples/component/package.json | 4 +- examples/controlled-input/package.json | 4 +- .../controlled-input/src/ControlledInput.purs | 1 - examples/counter/package.json | 4 +- examples/legacy-v2/package.json | 6 +- examples/reducer/.gitignore | 4 ++ examples/reducer/Makefile | 8 +++ examples/reducer/README.md | 12 ++++ examples/reducer/html/index.html | 10 +++ examples/reducer/package.json | 9 +++ examples/reducer/src/Main.purs | 24 +++++++ examples/reducer/src/Reducer.purs | 38 +++++++++++ src/React/Basic.js | 35 ++++++++-- src/React/Basic.purs | 67 +++++++++++-------- 14 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 examples/reducer/.gitignore create mode 100644 examples/reducer/Makefile create mode 100644 examples/reducer/README.md create mode 100644 examples/reducer/html/index.html create mode 100644 examples/reducer/package.json create mode 100644 examples/reducer/src/Main.purs create mode 100644 examples/reducer/src/Reducer.purs diff --git a/examples/component/package.json b/examples/component/package.json index e743eff..fbcf2be 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -1,7 +1,7 @@ { "dependencies": { - "react": "16.7.0-alpha.0", - "react-dom": "16.7.0-alpha.0" + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" }, "devDependencies": { "browserify": "^16.2.2" diff --git a/examples/controlled-input/package.json b/examples/controlled-input/package.json index e743eff..fbcf2be 100644 --- a/examples/controlled-input/package.json +++ b/examples/controlled-input/package.json @@ -1,7 +1,7 @@ { "dependencies": { - "react": "16.7.0-alpha.0", - "react-dom": "16.7.0-alpha.0" + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" }, "devDependencies": { "browserify": "^16.2.2" diff --git a/examples/controlled-input/src/ControlledInput.purs b/examples/controlled-input/src/ControlledInput.purs index bfa3852..12d0799 100644 --- a/examples/controlled-input/src/ControlledInput.purs +++ b/examples/controlled-input/src/ControlledInput.purs @@ -3,7 +3,6 @@ module ControlledInput where import Prelude import Control.Applicative.Indexed (ipure) -import Control.Bind.Indexed (ibind) import Data.Maybe (Maybe(..), fromMaybe, maybe) import React.Basic (CreateComponent, Render, RenderState, component, fragment, render, useState, (/\)) import React.Basic as React diff --git a/examples/counter/package.json b/examples/counter/package.json index 7c5faf4..4386ff4 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -1,7 +1,7 @@ { "dependencies": { - "react": "16.7.0-alpha.0", - "react-dom": "16.7.0-alpha.0" + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" }, "devDependencies": { "browserify": "16.2.3" diff --git a/examples/legacy-v2/package.json b/examples/legacy-v2/package.json index e743eff..4386ff4 100644 --- a/examples/legacy-v2/package.json +++ b/examples/legacy-v2/package.json @@ -1,9 +1,9 @@ { "dependencies": { - "react": "16.7.0-alpha.0", - "react-dom": "16.7.0-alpha.0" + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" }, "devDependencies": { - "browserify": "^16.2.2" + "browserify": "16.2.3" } } diff --git a/examples/reducer/.gitignore b/examples/reducer/.gitignore new file mode 100644 index 0000000..645684d --- /dev/null +++ b/examples/reducer/.gitignore @@ -0,0 +1,4 @@ +output +html/index.js +package-lock.json +node_modules diff --git a/examples/reducer/Makefile b/examples/reducer/Makefile new file mode 100644 index 0000000..ecacfbe --- /dev/null +++ b/examples/reducer/Makefile @@ -0,0 +1,8 @@ +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 + +node_modules: + npm install + diff --git a/examples/reducer/README.md b/examples/reducer/README.md new file mode 100644 index 0000000..f2418c0 --- /dev/null +++ b/examples/reducer/README.md @@ -0,0 +1,12 @@ +# Counter 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/reducer/html/index.html b/examples/reducer/html/index.html new file mode 100644 index 0000000..6b93b7c --- /dev/null +++ b/examples/reducer/html/index.html @@ -0,0 +1,10 @@ + + + + react-basic example + + +
+ + + diff --git a/examples/reducer/package.json b/examples/reducer/package.json new file mode 100644 index 0000000..4386ff4 --- /dev/null +++ b/examples/reducer/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" + }, + "devDependencies": { + "browserify": "16.2.3" + } +} diff --git a/examples/reducer/src/Main.purs b/examples/reducer/src/Main.purs new file mode 100644 index 0000000..0c52b92 --- /dev/null +++ b/examples/reducer/src/Main.purs @@ -0,0 +1,24 @@ +module Main where + +import Prelude + +import Reducer (mkReducer) +import Data.Maybe (Maybe(..)) +import Effect (Effect) +import Effect.Exception (throw) +import React.Basic (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 + reducer <- mkReducer + let app = element reducer {} + render app c diff --git a/examples/reducer/src/Reducer.purs b/examples/reducer/src/Reducer.purs new file mode 100644 index 0000000..c0a4c22 --- /dev/null +++ b/examples/reducer/src/Reducer.purs @@ -0,0 +1,38 @@ +module Reducer where + +import Prelude + +import Data.Maybe (Maybe(..)) +import React.Basic (CreateComponent, component, fragment, render, useReducer, (/\)) +import React.Basic as React +import React.Basic.DOM as R +import React.Basic.DOM.Events (capture_) + +data Action + = Increment + | Decrement + +mkReducer :: CreateComponent {} +mkReducer = do + component "Reducer" \props -> React.do + state /\ dispatch <- + useReducer + Nothing -- initial action + { counter: 0 } -- initial state + \state -> case _ of + Increment -> state { counter = state.counter + 1 } + Decrement -> state { counter = state.counter - 1 } + + render $ fragment + [ R.button + { onClick: capture_ $ dispatch Increment + , children: [ R.text $ "Increment" ] + } + , R.button + { onClick: capture_ $ dispatch Decrement + , children: [ R.text $ "Decrement" ] + } + , R.div_ + [ R.text $ show state.counter + ] + ] diff --git a/src/React/Basic.js b/src/React/Basic.js index 483af49..996686e 100644 --- a/src/React/Basic.js +++ b/src/React/Basic.js @@ -3,23 +3,48 @@ var React = require("react"); var Fragment = React.Fragment || "div"; +// foreign import useState_ +// :: forall state +// . EffectFn2 +// (forall a b. Fn2 a b (Tuple a b)) +// state +// (Tuple state ((state -> state) -> Effect Unit)) exports.useState_ = function(tuple, initialState) { var r = React.useState(initialState); var state = r[0]; var setState = r[1]; - return tuple(state)(function(update) { + return tuple(state, function(update) { return function() { return setState(update); }; }); }; +// foreign import useEffect_ +// :: EffectFn2 +// (Effect (Effect Unit)) +// (Array Key) +// Unit exports.useEffect_ = React.useEffect; -// exports.useReducer_ = function(reducer, initialState, initialAction) { -// var state = React.useReducer(reducer, initialState, initialAction); -// return { state: state[0], dispatch: state[1] }; -// }; +// foreign import useReducer_ +// :: forall state action +// . EffectFn4 +// (forall a b. Fn2 a b (Tuple a b)) +// (Fn2 state action state) +// state +// (Nullable action) +// (Tuple state (action -> Effect Unit)) +exports.useReducer_ = function(tuple, reducer, initialState, initialAction) { + var r = React.useReducer(reducer, initialState, initialAction); + var state = r[0]; + var dispatch = r[1]; + return tuple(state, function(action) { + return function() { + return dispatch(action); + }; + }); +}; // exports.readRef_ = function(ref) { // return ref.current; diff --git a/src/React/Basic.purs b/src/React/Basic.purs index b1dcbf7..c944f70 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -3,6 +3,7 @@ module React.Basic , Render , bind , discard + , pure , RenderJSX , render , CreateComponent @@ -12,7 +13,8 @@ module React.Basic , useState , RenderEffect , useEffect - -- , useReducer + , RenderReducer + , useReducer -- , Ref -- , readRef -- , renderRef @@ -31,20 +33,20 @@ module React.Basic , module Data.Tuple.Nested ) where -import Prelude hiding (bind, discard) -import Prelude (bind) as Prelude +import Prelude hiding (bind, discard, pure) import Control.Applicative.Indexed (class IxApplicative, ipure) import Control.Apply.Indexed (class IxApply) import Control.Bind.Indexed (class IxBind, ibind) -import Data.Function.Uncurried (Fn2, runFn2) +import Data.Function.Uncurried (Fn2, mkFn2, runFn2) import Data.Functor.Indexed (class IxFunctor) import Data.Maybe (Maybe) import Data.Nullable (Nullable, toNullable) import Data.Tuple (Tuple(..)) import Data.Tuple.Nested (tuple2, (/\)) import Effect (Effect) -import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, runEffectFn2) +import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn4, mkEffectFn1, runEffectFn2, runEffectFn4) +import Prelude (bind, pure) as Prelude import Unsafe.Coerce (unsafeCoerce) newtype Component props = Component (EffectFn1 props JSX) @@ -66,8 +68,11 @@ bind = ibind discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b discard = ibind +pure :: forall a x m. IxApplicative m => a -> m x x a +pure = ipure + instance ixApplicativeRender :: IxApplicative Render where - ipure = unsafeCoerce (pure :: forall a. a -> Effect a) + ipure = unsafeCoerce (Prelude.pure :: forall a. a -> Effect a) type CreateComponent props = Effect (Component props) @@ -87,7 +92,7 @@ useState . state -> Render hooks (RenderState state hooks) (Tuple state ((state -> state) -> Effect Unit)) useState initialState = unsafeCoerce do - runEffectFn2 useState_ Tuple initialState + runEffectFn2 useState_ (mkFn2 Tuple) initialState foreign import data RenderEffect :: Type -> Type @@ -103,18 +108,23 @@ foreign import data RenderJSX :: Type render :: forall hooks. JSX -> Render hooks RenderJSX JSX render jsx = unsafeCoerce (ipure jsx :: forall a. Render a a JSX) --- -- | useReducer --- -- | TODO: add note about conditionally updating state --- useReducer --- :: forall state action --- . ToKey state --- => (state -> action -> state) --- -> state --- -> Maybe action --- -> Render (Tuple state (action -> Effect Unit)) --- useReducer reducer initialState initialAction = Render do --- { state, dispatch } <- runEffectFn3 useReducer_ (mkFn2 reducer) initialState (toNullable initialAction) --- pure (Tuple state (runEffectFn1 dispatch)) +foreign import data RenderReducer :: Type -> Type -> Type -> Type + +-- | useReducer +-- | TODO: add note about conditionally updating state +useReducer + :: forall hooks state action + . ToKey state + => Maybe action + -> state + -> (state -> action -> state) + -> Render hooks (RenderReducer state action hooks) (Tuple state (action -> Effect Unit)) +useReducer initialAction initialState reducer = unsafeCoerce do + runEffectFn4 useReducer_ + (mkFn2 Tuple) + (mkFn2 reducer) + initialState + (toNullable initialAction) -- data Ref a @@ -258,7 +268,7 @@ foreign import unsafeSetDisplayName foreign import useState_ :: forall state . EffectFn2 - (forall a b. a -> b -> Tuple a b) + (forall a b. Fn2 a b (Tuple a b)) state (Tuple state ((state -> state) -> Effect Unit)) @@ -268,15 +278,14 @@ foreign import useEffect_ (Array Key) Unit --- foreign import useReducer_ --- :: forall state action --- . EffectFn3 --- (Fn2 state action state) --- state --- (Nullable action) --- { state :: state --- , dispatch :: EffectFn1 action Unit --- } +foreign import useReducer_ + :: forall state action + . EffectFn4 + (forall a b. Fn2 a b (Tuple a b)) + (Fn2 state action state) + state + (Nullable action) + (Tuple state (action -> Effect Unit)) -- foreign import readRef_ -- :: forall a From 6b1ea225ceb09b5fb2f02de26faa887ca51b2b3d Mon Sep 17 00:00:00 2001 From: Madeline Trotter Date: Wed, 19 Dec 2018 00:18:07 -0700 Subject: [PATCH 09/12] useRef --- examples/component/src/Container.purs | 2 +- examples/component/src/ToggleButton.purs | 4 +- .../controlled-input/src/ControlledInput.purs | 5 +- examples/counter/src/Counter.purs | 4 +- examples/legacy-v2/src/LegacyCounter.purs | 6 +- examples/reducer/src/Reducer.purs | 23 +- examples/refs/.gitignore | 4 + examples/refs/Makefile | 8 + examples/refs/README.md | 12 + examples/refs/html/index.html | 10 + examples/refs/package.json | 9 + examples/refs/src/Main.purs | 24 + examples/refs/src/Refs.purs | 90 ++++ generated-docs/React/Basic.md | 441 +++++++----------- generated-docs/React/Basic/Compat.md | 130 ++++++ generated-docs/React/Basic/DOM.md | 16 +- generated-docs/React/Basic/DOM/Events.md | 12 + generated-docs/React/Basic/DOM/Internal.md | 4 +- src/React/Basic.js | 14 +- src/React/Basic.purs | 172 +++---- src/React/Basic/Compat.purs | 9 +- src/React/Basic/DOM/Internal.purs | 11 +- 22 files changed, 616 insertions(+), 394 deletions(-) create mode 100644 examples/refs/.gitignore create mode 100644 examples/refs/Makefile create mode 100644 examples/refs/README.md create mode 100644 examples/refs/html/index.html create mode 100644 examples/refs/package.json create mode 100644 examples/refs/src/Main.purs create mode 100644 examples/refs/src/Refs.purs create mode 100644 generated-docs/React/Basic/Compat.md diff --git a/examples/component/src/Container.purs b/examples/component/src/Container.purs index ebabdc7..7b6737e 100644 --- a/examples/component/src/Container.purs +++ b/examples/component/src/Container.purs @@ -6,7 +6,7 @@ import React.Basic (CreateComponent, component, element, render) import React.Basic.DOM as R import ToggleButton (mkToggleButton) -mkToggleButtonContainer :: CreateComponent {} +mkToggleButtonContainer :: CreateComponent {} Unit mkToggleButtonContainer = do toggleButton <- mkToggleButton diff --git a/examples/component/src/ToggleButton.purs b/examples/component/src/ToggleButton.purs index 210374a..96248a4 100644 --- a/examples/component/src/ToggleButton.purs +++ b/examples/component/src/ToggleButton.purs @@ -3,12 +3,12 @@ module ToggleButton where import Prelude import Effect.Console (log) -import React.Basic (CreateComponent, component, render, toKey, useEffect, useState, (/\)) +import React.Basic (CreateComponent, RenderEffect, RenderState, component, render, toKey, useEffect, useState, (/\)) import React.Basic as React import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) -mkToggleButton :: CreateComponent { label :: String } +mkToggleButton :: CreateComponent { label :: String } (RenderEffect (RenderState Boolean Unit)) mkToggleButton = do component "ToggleButton" \{ label } -> React.do on /\ setOn <- useState false diff --git a/examples/controlled-input/src/ControlledInput.purs b/examples/controlled-input/src/ControlledInput.purs index 12d0799..cf134ec 100644 --- a/examples/controlled-input/src/ControlledInput.purs +++ b/examples/controlled-input/src/ControlledInput.purs @@ -10,7 +10,7 @@ import React.Basic.DOM as R import React.Basic.DOM.Events (capture, targetValue, timeStamp) import React.Basic.Events (EventHandler, merge) -mkControlledInput :: CreateComponent {} +mkControlledInput :: CreateComponent {} (RenderInput () (RenderInput () Unit)) mkControlledInput = component "ControlledInput" \props -> React.do firstName <- useInput "hello" @@ -30,11 +30,12 @@ mkControlledInput = type InputState state = { value :: String, lastChanged :: Maybe Number | state } +type RenderInput state hooks = RenderState (InputState state) hooks useInput :: forall hooks . String -> Render hooks - (RenderState (InputState ()) hooks) + (RenderInput () hooks) (InputState ( onChange :: EventHandler )) useInput initialValue = React.do { value, lastChanged } /\ replaceState <- useState { value: initialValue, lastChanged: Nothing } diff --git a/examples/counter/src/Counter.purs b/examples/counter/src/Counter.purs index 4d9a30a..2b79715 100644 --- a/examples/counter/src/Counter.purs +++ b/examples/counter/src/Counter.purs @@ -3,12 +3,12 @@ module Counter where import Prelude import Effect (Effect) -import React.Basic (CreateComponent, component, render, toKey, useEffect, useState, (/\)) +import React.Basic (CreateComponent, RenderEffect, RenderState, component, render, toKey, useEffect, useState, (/\)) import React.Basic as React import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) -mkCounter :: CreateComponent {} +mkCounter :: CreateComponent {} (RenderEffect (RenderState Int Unit)) mkCounter = do component "Counter" \props -> React.do counter /\ setCounter <- useState 0 diff --git a/examples/legacy-v2/src/LegacyCounter.purs b/examples/legacy-v2/src/LegacyCounter.purs index 74d8e26..971f446 100644 --- a/examples/legacy-v2/src/LegacyCounter.purs +++ b/examples/legacy-v2/src/LegacyCounter.purs @@ -2,7 +2,7 @@ module LegacyCounter where import Prelude -import React.Basic.Compat (Component, component, element, stateless) +import React.Basic.Compat (Component, RenderStatefulComponent, component, element, stateless) import React.Basic.DOM as R import React.Basic.Events as Events @@ -11,7 +11,7 @@ type Props = } -- | checks `component` -legacyCounter :: Component Props +legacyCounter :: Component Props (RenderStatefulComponent { counter :: Int }) legacyCounter = component { displayName: "LegacyCounter", initialState, receiveProps, render } where initialState = @@ -29,7 +29,7 @@ legacyCounter = component { displayName: "LegacyCounter", initialState, receiveP } -- | checks `stateless` -buttonLabel :: Component { label :: String, counter :: Int } +buttonLabel :: Component { label :: String, counter :: Int } Unit buttonLabel = stateless { displayName: "ButtonLabel", render } where render props = diff --git a/examples/reducer/src/Reducer.purs b/examples/reducer/src/Reducer.purs index c0a4c22..f786ce3 100644 --- a/examples/reducer/src/Reducer.purs +++ b/examples/reducer/src/Reducer.purs @@ -2,8 +2,7 @@ module Reducer where import Prelude -import Data.Maybe (Maybe(..)) -import React.Basic (CreateComponent, component, fragment, render, useReducer, (/\)) +import React.Basic (CreateComponent, RenderReducer, component, fragment, render, useReducer, (/\)) import React.Basic as React import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) @@ -12,26 +11,24 @@ data Action = Increment | Decrement -mkReducer :: CreateComponent {} +mkReducer :: CreateComponent {} (RenderReducer { counter :: Int } Action Unit) mkReducer = do component "Reducer" \props -> React.do + state /\ dispatch <- - useReducer - Nothing -- initial action - { counter: 0 } -- initial state - \state -> case _ of - Increment -> state { counter = state.counter + 1 } - Decrement -> state { counter = state.counter - 1 } + useReducer { counter: 0 } \state -> case _ of + Increment -> state { counter = state.counter + 1 } + Decrement -> state { counter = state.counter - 1 } render $ fragment [ R.button - { onClick: capture_ $ dispatch Increment - , children: [ R.text $ "Increment" ] - } - , R.button { onClick: capture_ $ dispatch Decrement , children: [ R.text $ "Decrement" ] } + , R.button + { onClick: capture_ $ dispatch Increment + , children: [ R.text $ "Increment" ] + } , R.div_ [ R.text $ show state.counter ] diff --git a/examples/refs/.gitignore b/examples/refs/.gitignore new file mode 100644 index 0000000..645684d --- /dev/null +++ b/examples/refs/.gitignore @@ -0,0 +1,4 @@ +output +html/index.js +package-lock.json +node_modules diff --git a/examples/refs/Makefile b/examples/refs/Makefile new file mode 100644 index 0000000..ecacfbe --- /dev/null +++ b/examples/refs/Makefile @@ -0,0 +1,8 @@ +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 + +node_modules: + npm install + diff --git a/examples/refs/README.md b/examples/refs/README.md new file mode 100644 index 0000000..f2418c0 --- /dev/null +++ b/examples/refs/README.md @@ -0,0 +1,12 @@ +# Counter 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/refs/html/index.html b/examples/refs/html/index.html new file mode 100644 index 0000000..6b93b7c --- /dev/null +++ b/examples/refs/html/index.html @@ -0,0 +1,10 @@ + + + + react-basic example + + +
+ + + diff --git a/examples/refs/package.json b/examples/refs/package.json new file mode 100644 index 0000000..4386ff4 --- /dev/null +++ b/examples/refs/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2" + }, + "devDependencies": { + "browserify": "16.2.3" + } +} diff --git a/examples/refs/src/Main.purs b/examples/refs/src/Main.purs new file mode 100644 index 0000000..ec70e25 --- /dev/null +++ b/examples/refs/src/Main.purs @@ -0,0 +1,24 @@ +module Main where + +import Prelude + +import Refs (mkRefs) +import Data.Maybe (Maybe(..)) +import Effect (Effect) +import Effect.Exception (throw) +import React.Basic (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 + refs <- mkRefs + let app = element refs {} + render app c diff --git a/examples/refs/src/Refs.purs b/examples/refs/src/Refs.purs new file mode 100644 index 0000000..0cf4c58 --- /dev/null +++ b/examples/refs/src/Refs.purs @@ -0,0 +1,90 @@ +module Refs where + +import Prelude + +import Data.Int (round) +import Data.Maybe (Maybe(..)) +import Data.Nullable (Nullable, null) +import Math (pow, sqrt) +import React.Basic (CreateComponent, Ref, Render, RenderEffect, RenderRef, RenderState, Tuple, component, fragment, readRefMaybe, render, useEffect, useRef, useState, (/\)) +import React.Basic as React +import React.Basic.DOM as R +import Unsafe.Coerce (unsafeCoerce) +import Web.DOM (Node) +import Web.Event.Event (EventType(..)) +import Web.Event.EventTarget (addEventListener, eventListener, removeEventListener) +import Web.HTML (window) +import Web.HTML.HTMLElement (getBoundingClientRect) +import Web.HTML.HTMLElement as HTMLElement +import Web.HTML.Window as Window + +mkRefs :: CreateComponent {} (RenderNodeDistance (RenderNodeDistance (RenderNodeDistance Unit))) +mkRefs = do + component "Refs" \props -> React.do + + mouseDistance1 /\ buttonRef1 <- useNodeDistanceFromMouse + mouseDistance2 /\ buttonRef2 <- useNodeDistanceFromMouse + mouseDistance3 /\ buttonRef3 <- useNodeDistanceFromMouse + + render $ fragment + [ R.button + { ref: buttonRef1 + , children: [ R.text $ show mouseDistance1 <> "px" ] + , style: R.css { width: "100px", position: "absolute", top: "20px", left: "200px" } + } + , R.button + { ref: buttonRef2 + , children: [ R.text $ show mouseDistance2 <> "px" ] + , style: R.css { width: "100px", position: "absolute", top: "60px", left: "40px" } + } + , R.button + { ref: buttonRef3 + , children: [ R.text $ show mouseDistance3 <> "px" ] + , style: R.css { width: "100px", position: "absolute", top: "120px", left: "90px" } + } + ] + +type RenderNodeDistance hooks = RenderEffect (RenderState Int (RenderRef (Nullable Node) hooks)) + +useNodeDistanceFromMouse + :: forall hooks + . Render + hooks + (RenderNodeDistance hooks) + (Tuple Int (Ref (Nullable Node))) +useNodeDistanceFromMouse = React.do + elementRef <- useRef null + mouseDistance /\ setMouseDistance <- useState 0 + + useEffect [] do + maybeElement <- map (HTMLElement.fromNode =<< _) (readRefMaybe elementRef) + case maybeElement of + Nothing -> pure (pure unit) + Just element -> do + mouseMoveListener <- eventListener \e -> do + { top, bottom, left, right } <- getBoundingClientRect element + let + mouseX = (unsafeCoerce e).clientX + mouseY = (unsafeCoerce e).clientY + distanceX = + if mouseX # between left right + then 0.0 + else if mouseX < left + then left - mouseX + else mouseX - right + distanceY = + if mouseY # between top bottom + then 0.0 + else if mouseY < top + then top - mouseY + else mouseY - bottom + distance = sqrt ((distanceX `pow` 2.0) + (distanceY `pow` 2.0)) + setMouseDistance \_ -> round distance + + let mouseMoveEventType = EventType "mousemove" + windowEventTarget <- map Window.toEventTarget window + addEventListener mouseMoveEventType mouseMoveListener false windowEventTarget + pure do + removeEventListener mouseMoveEventType mouseMoveListener false windowEventTarget + + React.pure (mouseDistance /\ elementRef) diff --git a/generated-docs/React/Basic.md b/generated-docs/React/Basic.md index 2832ee1..1d147dc 100644 --- a/generated-docs/React/Basic.md +++ b/generated-docs/React/Basic.md @@ -1,341 +1,216 @@ ## Module React.Basic -#### `ComponentSpec` +#### `Component` ``` purescript -type ComponentSpec props state initialState action = { initialState :: initialState, update :: Self props state action -> action -> StateUpdate props state action, render :: Self props state action -> JSX, shouldUpdate :: Self props state action -> props -> state -> Boolean, didMount :: Self props state action -> Effect Unit, didUpdate :: Self props state action -> Effect Unit, willUnmount :: Self props state action -> Effect Unit, "$$type" :: ComponentType props state action } +newtype Component props hooks ``` -`ComponentSpec` represents a React-Basic component implementation. - -These are the properties your component definition may override -with specific implementations. None are required to be overridden, unless -an overridden function interacts with `state`, in which case `initialState` -is required (the compiler enforces this). While you _can_ use `state` and -dispatch actions without defining `update`, doing so doesn't make much sense -so the default `update` implementation will emit a warning. - -- `initialState` - - The component's starting state. - - Avoid mirroring prop values in state. -- `update` - - All state updates go through `update`. - - `update` is called when `send` is used to dispatch an action. - - State changes are described using `StateUpdate`. Only `Update` and `UpdateAndSideEffects` will cause rerenders and a call to `didUpdate`. - - Side effects requested are only invoked _after_ any corrosponding state update has completed its render cycle and the changes have been applied. This means it is safe to interact with the DOM in a side effect, for example. -- `render` - - Takes a current snapshot of the component (`Self`) and converts it to renderable `JSX`. -- `shouldUpdate` - - Can be useful for occasional performance optimizations. Rarely necessary. -- `didMount` - - The React component's `componentDidMount` lifecycle. Useful for initiating an action on first mount, such as fetching data from a server. -- `didUpdate` - - The React component's `componentDidUpdate` lifecycle. Rarely necessary. -- `willUnmount` - - The React component's `componentWillUpdate` lifecycle. Any subscriptions or timers created in `didMount` or `didUpdate` should be disposed of here. - -The component spec is generally not exported from your component -module and this type is rarely used explicitly. The simplified alias -`Component` is usually sufficient, and `make` will validate whether -your component's types line up. - -For example: - -```purs -component :: Component -component = createComponent "Counter" - -type Props = - { label :: String - } +#### `Render` -data Action - = Increment - -counter :: Props -> JSX -counter = make component - { initialState = { counter: 0 } - - , update = \self action -> case action of - Increment -> - Update self.state { counter = self.state.counter + 1 } - - , render = \self -> - R.button - { onClick: capture_ self Increment - , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ] - } - } +``` purescript +newtype Render x y a ``` -This example component overrides `initialState`, `update`, and `render`. - -__*Note:* A `ComponentSpec` is *not* a valid React component by itself. If you would like to use - a React-Basic component from JavaScript, use `toReactComponent`.__ - -__*Note:* `$$type` is for internal use only. It needs to be on the type to - preserve its existence during a record update, as in the example above.__ - -__*See also:* `Component`, `ComponentSpec`, `make`, `makeStateless`__ - -#### `createComponent` - +##### Instances ``` purescript -createComponent :: forall props state action. String -> ComponentSpec props state Unit action +IxFunctor Render +IxApply Render +IxBind Render +IxApplicative Render ``` -Creates a `ComponentSpec` with a given Display Name. - -The resulting component spec is usually given the simplified `Component` type: +#### `bind` -```purs -component :: Component -component = createComponent "Counter" +``` purescript +bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b ``` -This function should be used at the module level and considered side effecting. -This is because React uses referential equality when deciding whether a new -`JSX` tree is a valid update, or if it needs to be replaced entirely -(expensive and clears component state lower in the tree). - -__*Note:* A `Component` is *not* a valid React component by itself. If you would like to use - a React-Basic component from JavaScript, use `toReactComponent`.__ - -__*See also:* `Component`, `ComponentSpec`, `make`, `makeStateless`__ - -#### `Component` +#### `discard` ``` purescript -type Component = forall props state action. ComponentSpec props state Unit action +discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b ``` -A simplified alias for `ComponentSpec`. This type is usually used to represent -the default component type returned from `createComponent`. - -#### `ComponentType` +#### `pure` ``` purescript -data ComponentType props state action +pure :: forall a x m. IxApplicative m => a -> m x x a ``` -Opaque component information for internal use. - -__*For the curious:* This is the "class" React will use to render and - identify the component. It receives the `ComponentSpec` as a prop and knows - how to defer behavior to it. It requires very specific props and is not useful by - itself from JavaScript. For JavaScript interop, see `toReactComponent`.__ - -#### `StateUpdate` +#### `RenderJSX` ``` purescript -data StateUpdate props state action - = NoUpdate - | Update state - | SideEffects (Self props state action -> Effect Unit) - | UpdateAndSideEffects state (Self props state action -> Effect Unit) +data RenderJSX :: Type -> Type ``` -Used by the `update` function to describe the kind of state update and/or side -effects desired. - -__*See also:* `ComponentSpec`__ - -#### `Self` +#### `render` ``` purescript -type Self props state action = { props :: props, state :: state, instance_ :: ReactComponentInstance } +render :: forall hooks. JSX -> Render hooks (RenderJSX hooks) JSX ``` -`Self` represents the component instance at a particular point in time. - -- `props` - - A snapshot of `props` taken when this `Self` was created. -- `state` - - A snapshot of `state` taken when this `Self` was created. -- `instance_` - - Unsafe escape hatch to the underlying component instance (`this` in the JavaScript React paradigm). Avoid as much as possible, but it's still frequently better than rewriting an entire component in JavaScript. - -__*See also:* `ComponentSpec`, `send`, `capture`, `readProps`, `readState`__ - -#### `send` +#### `CreateComponent` ``` purescript -send :: forall props state action. Self props state action -> action -> Effect Unit +type CreateComponent props hooks = Effect (Component props hooks) ``` -Dispatch an `action` into the component to be handled by `update`. - -__*See also:* `update`, `capture`__ - -#### `sendAsync` +#### `JSX` ``` purescript -sendAsync :: forall props state action. Self props state action -> Aff action -> Effect Unit +data JSX :: Type ``` -Convenience function for sending an action when an `Aff` completes. +Represents rendered React VDOM (the result of calling `React.createElement` +in JavaScript). -__*Note:* Potential failure should be handled in the given `Aff` and converted - to an action, as the default error handler will simply log the error to - the console.__ +`JSX` is a `Monoid`: -__*See also:* `send`__ +- `append` + - Merge two `JSX` nodes using `React.Fragment`. +- `mempty` + - The `empty` node; renders nothing. -#### `capture` +__*Hint:* Many useful utility functions already exist for Monoids. For example, + `guard` can be used to conditionally render a subtree of components.__ +##### Instances ``` purescript -capture :: forall props state action a. Self props state action -> EventFn SyntheticEvent a -> (a -> action) -> EventHandler +Semigroup JSX +Monoid JSX ``` -Create a capturing\* `EventHandler` to send an action when an event occurs. For -more complicated event handlers requiring `Effect`, use `handler` from `React.Basic.Events`. - -__\*calls `preventDefault` and `stopPropagation`__ - -__*See also:* `update`, `capture_`, `monitor`, `React.Basic.Events`__ - -#### `capture_` +#### `component` ``` purescript -capture_ :: forall props state action. Self props state action -> action -> EventHandler +component :: forall hooks props. String -> (props -> Render Unit (RenderJSX hooks) JSX) -> CreateComponent props hooks ``` -Like `capture`, but for actions which don't need to extract information from the Event. +#### `RenderState` -__*See also:* `update`, `capture`, `monitor_`__ +``` purescript +data RenderState :: Type -> Type -> Type +``` -#### `monitor` +#### `useState` ``` purescript -monitor :: forall props state action a. Self props state action -> EventFn SyntheticEvent a -> (a -> action) -> EventHandler +useState :: forall hooks state. state -> Render hooks (RenderState state hooks) (Tuple state ((state -> state) -> Effect Unit)) ``` -Like `capture`, but does not cancel the event. +#### `RenderEffect` -__*See also:* `update`, `capture`, `monitor\_`__ +``` purescript +data RenderEffect :: Type -> Type +``` -#### `monitor_` +#### `useEffect` ``` purescript -monitor_ :: forall props state action. Self props state action -> action -> EventHandler +useEffect :: forall hooks. Array Key -> Effect (Effect Unit) -> Render hooks (RenderEffect hooks) Unit ``` -Like `capture_`, but does not cancel the event. +#### `RenderReducer` -__*See also:* `update`, `monitor`, `capture_`, `React.Basic.Events`__ +``` purescript +data RenderReducer :: Type -> Type -> Type -> Type +``` -#### `readProps` +#### `useReducer` ``` purescript -readProps :: forall props state action. Self props state action -> Effect props +useReducer :: forall hooks state action. ToKey state => state -> (state -> action -> state) -> Render hooks (RenderReducer state action hooks) (Tuple state (action -> Effect Unit)) ``` -Read the most up to date `props` directly from the component instance -associated with this `Self`. +useReducer +TODO: add note about conditionally updating state -_Note: This function is for specific, asynchronous edge cases. - Generally, the `props` snapshot on `Self` is sufficient. +#### `RenderRef` -__*See also:* `Self`__ +``` purescript +data RenderRef :: Type -> Type -> Type +``` -#### `readState` +#### `Ref` ``` purescript -readState :: forall props state action. Self props state action -> Effect state +data Ref :: Type -> Type ``` -Read the most up to date `state` directly from the component instance -associated with this `Self`. - -_Note: This function is for specific, asynchronous edge cases. - Generally, the `state` snapshot on `Self` is sufficient. +#### `readRef` -__*See also:* `Self`__ +``` purescript +readRef :: forall a. Ref a -> Effect a +``` -#### `make` +#### `readRefMaybe` ``` purescript -make :: forall props state action. ComponentSpec props state state action -> props -> JSX +readRefMaybe :: forall a. Ref (Nullable a) -> Effect (Maybe a) ``` -Turn a `Component` into a usable render function. -This is where you will want to provide customized implementations: - -```purs -component :: Component -component = createComponent "Counter" +#### `writeRef` -type Props = - { label :: String - } +``` purescript +writeRef :: forall a. Ref a -> a -> Effect Unit +``` -data Action - = Increment +#### `renderRef` -counter :: Props -> JSX -counter = make component - { initialState = { counter: 0 } +``` purescript +renderRef :: forall hooks a. Ref a -> Render hooks hooks a +``` - , update = \self action -> case action of - Increment -> - Update self.state { counter = self.state.counter + 1 } +#### `renderRefMaybe` - , render = \self -> - R.button - { onClick: capture_ self Increment - , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ] - } - } +``` purescript +renderRefMaybe :: forall hooks a. Ref (Nullable a) -> Render hooks hooks (Maybe a) ``` -__*See also:* `makeStateless`, `createComponent`, `Component`, `ComponentSpec`__ - -#### `makeStateless` +#### `useRef` ``` purescript -makeStateless :: forall props. ComponentSpec props Unit Unit Unit -> (props -> JSX) -> props -> JSX +useRef :: forall hooks a. a -> Render hooks (RenderRef a hooks) (Ref a) ``` -Makes stateless component definition slightly less verbose: - -```purs -component :: Component -component = createComponent "Xyz" +#### `Key` -myComponent :: Props -> JSX -myComponent = makeStateless component \props -> JSX +``` purescript +data Key ``` -__*Note:* The only difference between a stateless React-Basic component and - a plain `props -> JSX` function is the presense of the component name - in React's dev tools and error stacks. It's just a conceptual boundary. - If this isn't important simply write a `props -> JSX` function.__ - -__*See also:* `make`, `createComponent`, `Component`, `ComponentSpec`__ +Keys represent values React uses to check for changes. +This is done using JavaScript's reference equality (`===`), +so complicated types may want to implement `ToKey` so that +it returns a primative like a `String`. A timestamp appended +to a unique ID, for example. Less strict cases can implement +`ToKey` using `unsafeToKey`, while some extreme cases may +need a hashing or stringifying mechanism. -#### `JSX` +#### `ToKey` ``` purescript -data JSX :: Type +class ToKey a where + toKey :: a -> Key ``` -Represents rendered React VDOM (the result of calling `React.createElement` -in JavaScript). - -`JSX` is a `Monoid`: - -- `append` - - Merge two `JSX` nodes using `React.Fragment`. -- `mempty` - - The `empty` node; renders nothing. +##### Instances +``` purescript +ToKey String +ToKey Int +ToKey Number +ToKey Boolean +ToKey { | a } +ToKey (Array a) +ToKey (Nullable a) +ToKey (Maybe a) +``` -__*Hint:* Many useful utility functions already exist for Monoids. For example, - `guard` can be used to conditionally render a subtree of components.__ +#### `unsafeToKey` -##### Instances ``` purescript -Semigroup JSX -Monoid JSX +unsafeToKey :: forall a. a -> Key ``` #### `empty` @@ -375,68 +250,108 @@ __*See also:* `JSX`__ #### `element` ``` purescript -element :: forall props. ReactComponent { | props } -> { | props } -> JSX +element :: forall hooks props. Component { | props } hooks -> { | props } -> JSX ``` -Create a `JSX` node from a `ReactComponent`, by providing the props. +Create a `JSX` node from a `Component`, by providing the props. This function is for non-React-Basic React components, such as those imported from FFI. -__*See also:* `ReactComponent`, `elementKeyed`__ +__*See also:* `Component`, `elementKeyed`__ #### `elementKeyed` ``` purescript -elementKeyed :: forall props. ReactComponent { | props } -> { key :: String | props } -> JSX +elementKeyed :: forall hooks props. Component { | props } hooks -> { key :: String | props } -> JSX ``` -Create a `JSX` node from a `ReactComponent`, by providing the props and a key. +Create a `JSX` node from a `Component`, by providing the props and a key. This function is for non-React-Basic React components, such as those imported from FFI. -__*See also:* `ReactComponent`, `element`, React's documentation regarding the special `key` prop__ +__*See also:* `Component`, `element`, React's documentation regarding the special `key` prop__ -#### `ReactComponent` +#### `displayName` ``` purescript -data ReactComponent props +displayName :: forall hooks props. Component props hooks -> String ``` -Represents a traditional React component. Useful for JavaScript interop and -FFI. For example: +Retrieve the Display Name from a `ComponentSpec`. Useful for debugging and improving +error messages in logs. -```purs -foreign import ComponentRequiringJSHacks :: ReactComponent { someProp :: String } -``` +__*See also:* `displayNameFromSelf`, `createComponent`__ + + +### Re-exported from Data.Tuple: -__*See also:* `element`, `toReactComponent`__ +#### `Tuple` -#### `ReactComponentInstance` +``` purescript +data Tuple a b + = Tuple a b +``` + +A simple product type for wrapping a pair of component values. +##### Instances ``` purescript -data ReactComponentInstance +(Show a, Show b) => Show (Tuple a b) +(Eq a, Eq b) => Eq (Tuple a b) +(Eq a) => Eq1 (Tuple a) +(Ord a, Ord b) => Ord (Tuple a b) +(Ord a) => Ord1 (Tuple a) +(Bounded a, Bounded b) => Bounded (Tuple a b) +Semigroupoid Tuple +(Semigroup a, Semigroup b) => Semigroup (Tuple a b) +(Monoid a, Monoid b) => Monoid (Tuple a b) +(Semiring a, Semiring b) => Semiring (Tuple a b) +(Ring a, Ring b) => Ring (Tuple a b) +(CommutativeRing a, CommutativeRing b) => CommutativeRing (Tuple a b) +(HeytingAlgebra a, HeytingAlgebra b) => HeytingAlgebra (Tuple a b) +(BooleanAlgebra a, BooleanAlgebra b) => BooleanAlgebra (Tuple a b) +Functor (Tuple a) +FunctorWithIndex Unit (Tuple a) +Invariant (Tuple a) +Bifunctor Tuple +(Semigroup a) => Apply (Tuple a) +Biapply Tuple +(Monoid a) => Applicative (Tuple a) +Biapplicative Tuple +(Semigroup a) => Bind (Tuple a) +(Monoid a) => Monad (Tuple a) +Extend (Tuple a) +Comonad (Tuple a) +(Lazy a, Lazy b) => Lazy (Tuple a b) +Foldable (Tuple a) +Foldable1 (Tuple a) +FoldableWithIndex Unit (Tuple a) +Bifoldable Tuple +Traversable (Tuple a) +Traversable1 (Tuple a) +TraversableWithIndex Unit (Tuple a) +Bitraversable Tuple +(TypeEquals a Unit) => Distributive (Tuple a) ``` -An opaque representation of a React component's instance (`this` in the JavaScript -React paradigm). It exists as an escape hatch to unsafe behavior. Use it with -caution. +### Re-exported from Data.Tuple.Nested: -#### `toReactComponent` +#### `tuple2` ``` purescript -toReactComponent :: forall jsProps props state action. ({ | jsProps } -> props) -> ComponentSpec props state state action -> ReactComponent { | jsProps } +tuple2 :: forall a b. a -> b -> Tuple2 a b ``` -Convert a React-Basic `ComponentSpec` to a JavaScript-friendly React component. -This function should only be used for JS interop and not normal React-Basic usage. +Given 2 values, creates a 2-tuple. -__*Note:* Like `createComponent`, `toReactComponent` is side effecting in that - it creates a "class" React will see as unique each time it's called. Lift - any usage up to the module level, usage in `render` or any other function, - and applying any type classes to the `props`.__ +#### `(/\)` -__*See also:* `ReactComponent`__ +``` purescript +infixr 6 Tuple as /\ +``` +Shorthand for constructing n-tuples as nested pairs. +`a /\ b /\ c /\ d /\ unit` becomes `Tuple a (Tuple b (Tuple c (Tuple d unit)))` diff --git a/generated-docs/React/Basic/Compat.md b/generated-docs/React/Basic/Compat.md new file mode 100644 index 0000000..91c3c1a --- /dev/null +++ b/generated-docs/React/Basic/Compat.md @@ -0,0 +1,130 @@ +## Module React.Basic.Compat + +#### `RenderStatefulComponent` + +``` purescript +type RenderStatefulComponent state = RenderEffect (RenderState state Unit) +``` + +#### `component` + +``` purescript +component :: forall props state. { displayName :: String, initialState :: { | state }, receiveProps :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> Effect Unit, render :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> JSX } -> Component { | props } (RenderStatefulComponent { | state }) +``` + +Supports a common subset of the v2 API to ease the upgrade process + +#### `stateless` + +``` purescript +stateless :: forall props. { displayName :: String, render :: { | props } -> JSX } -> Component { | props } Unit +``` + +Supports a common subset of the v2 API to ease the upgrade process + + +### Re-exported from React.Basic: + +#### `RenderState` + +``` purescript +data RenderState :: Type -> Type -> Type +``` + +#### `RenderEffect` + +``` purescript +data RenderEffect :: Type -> Type +``` + +#### `JSX` + +``` purescript +data JSX :: Type +``` + +Represents rendered React VDOM (the result of calling `React.createElement` +in JavaScript). + +`JSX` is a `Monoid`: + +- `append` + - Merge two `JSX` nodes using `React.Fragment`. +- `mempty` + - The `empty` node; renders nothing. + +__*Hint:* Many useful utility functions already exist for Monoids. For example, + `guard` can be used to conditionally render a subtree of components.__ + +##### Instances +``` purescript +Semigroup JSX +Monoid JSX +``` + +#### `Component` + +``` purescript +newtype Component props hooks +``` + +#### `keyed` + +``` purescript +keyed :: String -> JSX -> JSX +``` + +Apply a React key to a subtree. React-Basic usually hides React's warning about +using `key` props on components in an Array, but keys are still important for +any dynamic lists of child components. + +__*See also:* React's documentation regarding the special `key` prop__ + +#### `fragment` + +``` purescript +fragment :: Array JSX -> JSX +``` + +Render an Array of children without a wrapping component. + +__*See also:* `JSX`__ + +#### `empty` + +``` purescript +empty :: JSX +``` + +An empty `JSX` node. This is often useful when you would like to conditionally +show something, but you don't want to (or can't) modify the `children` prop +on the parent node. + +__*See also:* `JSX`, Monoid `guard`__ + +#### `elementKeyed` + +``` purescript +elementKeyed :: forall hooks props. Component { | props } hooks -> { key :: String | props } -> JSX +``` + +Create a `JSX` node from a `Component`, by providing the props and a key. + +This function is for non-React-Basic React components, such as those +imported from FFI. + +__*See also:* `Component`, `element`, React's documentation regarding the special `key` prop__ + +#### `element` + +``` purescript +element :: forall hooks props. Component { | props } hooks -> { | props } -> JSX +``` + +Create a `JSX` node from a `Component`, by providing the props. + +This function is for non-React-Basic React components, such as those +imported from FFI. + +__*See also:* `Component`, `elementKeyed`__ + diff --git a/generated-docs/React/Basic/DOM.md b/generated-docs/React/Basic/DOM.md index 7afcb7b..4a4fc47 100644 --- a/generated-docs/React/Basic/DOM.md +++ b/generated-docs/React/Basic/DOM.md @@ -71,18 +71,6 @@ if an app existed and was unmounted successfully. Note: Relies on `ReactDOM.unmountComponentAtNode` -#### `findDOMNode` - -``` purescript -findDOMNode :: ReactComponentInstance -> Effect (Either Error Node) -``` - -Returns the current DOM node associated with the given -instance, or an Error if no node was found or the given -instance was not mounted. - -Note: Relies on `ReactDOM.findDOMNode` - #### `createPortal` ``` purescript @@ -2178,7 +2166,7 @@ a :: forall attrs attrs_. Union attrs attrs_ (SharedProps Props_a) => { | attrs #### `SharedProps` ``` purescript -type SharedProps specific = (key :: String, about :: String, acceptCharset :: String, accessKey :: String, allowFullScreen :: Boolean, allowTransparency :: String, autoComplete :: String, autoFocus :: String, autoPlay :: Boolean, capture :: Boolean, cellPadding :: String, cellSpacing :: String, charSet :: String, classID :: String, className :: String, colSpan :: Number, contentEditable :: String, contextMenu :: String, crossOrigin :: String, datatype :: String, dateTime :: String, dir :: String, draggable :: String, encType :: String, formAction :: String, formEncType :: String, formMethod :: String, formNoValidate :: String, formTarget :: String, frameBorder :: String, hidden :: Boolean, hrefLang :: String, htmlFor :: String, httpEquiv :: String, icon :: String, id :: String, inlist :: String, inputMode :: String, is :: String, itemID :: String, itemProp :: String, itemRef :: String, itemScope :: Boolean, itemType :: String, keyParams :: String, keyType :: String, lang :: String, marginHeight :: String, marginWidth :: String, maxLength :: String, mediaGroup :: String, minLength :: String, noValidate :: String, prefix :: String, property :: String, radioGroup :: String, readOnly :: Boolean, resource :: String, role :: String, rowSpan :: Number, scoped :: Boolean, seamless :: Boolean, security :: String, spellCheck :: String, srcDoc :: String, srcLang :: String, srcSet :: String, style :: CSS, tabIndex :: String, title :: String, typeof :: String, unselectable :: String, useMap :: String, vocab :: String, wmode :: String, onBlur :: EventHandler, onClick :: EventHandler, onFocus :: EventHandler | specific) +type SharedProps specific = (key :: String, ref :: Ref (Nullable Node), about :: String, acceptCharset :: String, accessKey :: String, allowFullScreen :: Boolean, allowTransparency :: String, autoComplete :: String, autoFocus :: String, autoPlay :: Boolean, capture :: Boolean, cellPadding :: String, cellSpacing :: String, charSet :: String, classID :: String, className :: String, colSpan :: Number, contentEditable :: String, contextMenu :: String, crossOrigin :: String, datatype :: String, dateTime :: String, dir :: String, draggable :: String, encType :: String, formAction :: String, formEncType :: String, formMethod :: String, formNoValidate :: String, formTarget :: String, frameBorder :: String, hidden :: Boolean, hrefLang :: String, htmlFor :: String, httpEquiv :: String, icon :: String, id :: String, inlist :: String, inputMode :: String, is :: String, itemID :: String, itemProp :: String, itemRef :: String, itemScope :: Boolean, itemType :: String, keyParams :: String, keyType :: String, lang :: String, marginHeight :: String, marginWidth :: String, maxLength :: String, mediaGroup :: String, minLength :: String, noValidate :: String, prefix :: String, property :: String, radioGroup :: String, readOnly :: Boolean, resource :: String, role :: String, rowSpan :: Number, scoped :: Boolean, seamless :: Boolean, security :: String, spellCheck :: String, srcDoc :: String, srcLang :: String, srcSet :: String, style :: CSS, tabIndex :: String, title :: String, typeof :: String, unselectable :: String, useMap :: String, vocab :: String, wmode :: String, onBlur :: EventHandler, onClick :: EventHandler, onFocus :: EventHandler | specific) ``` Standard props which are shared by all DOM elements. @@ -2194,6 +2182,6 @@ An abstract type representing records of CSS attributes. #### `unsafeCreateDOMComponent` ``` purescript -unsafeCreateDOMComponent :: forall props. String -> ReactComponent props +unsafeCreateDOMComponent :: forall props. String -> Component props Unit ``` diff --git a/generated-docs/React/Basic/DOM/Events.md b/generated-docs/React/Basic/DOM/Events.md index 4b86a99..29f8cdb 100644 --- a/generated-docs/React/Basic/DOM/Events.md +++ b/generated-docs/React/Basic/DOM/Events.md @@ -2,6 +2,18 @@ This module defines safe DOM event function and property accessors. +#### `capture` + +``` purescript +capture :: forall a. EventFn SyntheticEvent a -> (a -> Effect Unit) -> EventHandler +``` + +#### `capture_` + +``` purescript +capture_ :: Effect Unit -> EventHandler +``` + #### `bubbles` ``` purescript diff --git a/generated-docs/React/Basic/DOM/Internal.md b/generated-docs/React/Basic/DOM/Internal.md index 8594101..0ace6b5 100644 --- a/generated-docs/React/Basic/DOM/Internal.md +++ b/generated-docs/React/Basic/DOM/Internal.md @@ -11,7 +11,7 @@ An abstract type representing records of CSS attributes. #### `SharedProps` ``` purescript -type SharedProps specific = (key :: String, about :: String, acceptCharset :: String, accessKey :: String, allowFullScreen :: Boolean, allowTransparency :: String, autoComplete :: String, autoFocus :: String, autoPlay :: Boolean, capture :: Boolean, cellPadding :: String, cellSpacing :: String, charSet :: String, classID :: String, className :: String, colSpan :: Number, contentEditable :: String, contextMenu :: String, crossOrigin :: String, datatype :: String, dateTime :: String, dir :: String, draggable :: String, encType :: String, formAction :: String, formEncType :: String, formMethod :: String, formNoValidate :: String, formTarget :: String, frameBorder :: String, hidden :: Boolean, hrefLang :: String, htmlFor :: String, httpEquiv :: String, icon :: String, id :: String, inlist :: String, inputMode :: String, is :: String, itemID :: String, itemProp :: String, itemRef :: String, itemScope :: Boolean, itemType :: String, keyParams :: String, keyType :: String, lang :: String, marginHeight :: String, marginWidth :: String, maxLength :: String, mediaGroup :: String, minLength :: String, noValidate :: String, prefix :: String, property :: String, radioGroup :: String, readOnly :: Boolean, resource :: String, role :: String, rowSpan :: Number, scoped :: Boolean, seamless :: Boolean, security :: String, spellCheck :: String, srcDoc :: String, srcLang :: String, srcSet :: String, style :: CSS, tabIndex :: String, title :: String, typeof :: String, unselectable :: String, useMap :: String, vocab :: String, wmode :: String, onBlur :: EventHandler, onClick :: EventHandler, onFocus :: EventHandler | specific) +type SharedProps specific = (key :: String, ref :: Ref (Nullable Node), about :: String, acceptCharset :: String, accessKey :: String, allowFullScreen :: Boolean, allowTransparency :: String, autoComplete :: String, autoFocus :: String, autoPlay :: Boolean, capture :: Boolean, cellPadding :: String, cellSpacing :: String, charSet :: String, classID :: String, className :: String, colSpan :: Number, contentEditable :: String, contextMenu :: String, crossOrigin :: String, datatype :: String, dateTime :: String, dir :: String, draggable :: String, encType :: String, formAction :: String, formEncType :: String, formMethod :: String, formNoValidate :: String, formTarget :: String, frameBorder :: String, hidden :: Boolean, hrefLang :: String, htmlFor :: String, httpEquiv :: String, icon :: String, id :: String, inlist :: String, inputMode :: String, is :: String, itemID :: String, itemProp :: String, itemRef :: String, itemScope :: Boolean, itemType :: String, keyParams :: String, keyType :: String, lang :: String, marginHeight :: String, marginWidth :: String, maxLength :: String, mediaGroup :: String, minLength :: String, noValidate :: String, prefix :: String, property :: String, radioGroup :: String, readOnly :: Boolean, resource :: String, role :: String, rowSpan :: Number, scoped :: Boolean, seamless :: Boolean, security :: String, spellCheck :: String, srcDoc :: String, srcLang :: String, srcSet :: String, style :: CSS, tabIndex :: String, title :: String, typeof :: String, unselectable :: String, useMap :: String, vocab :: String, wmode :: String, onBlur :: EventHandler, onClick :: EventHandler, onFocus :: EventHandler | specific) ``` Standard props which are shared by all DOM elements. @@ -19,7 +19,7 @@ Standard props which are shared by all DOM elements. #### `unsafeCreateDOMComponent` ``` purescript -unsafeCreateDOMComponent :: forall props. String -> ReactComponent props +unsafeCreateDOMComponent :: forall props. String -> Component props Unit ``` diff --git a/src/React/Basic.js b/src/React/Basic.js index 996686e..0b28dbb 100644 --- a/src/React/Basic.js +++ b/src/React/Basic.js @@ -46,15 +46,15 @@ exports.useReducer_ = function(tuple, reducer, initialState, initialAction) { }); }; -// exports.readRef_ = function(ref) { -// return ref.current; -// }; +exports.useRef_ = React.useRef; -// exports.writeRef_ = function(ref, a) { -// ref.current = a; -// }; +exports.readRef_ = function(ref) { + return ref.current; +}; -// exports.useRef_ = React.useRef; +exports.writeRef_ = function(ref, a) { + ref.current = a; +}; exports.empty = null; diff --git a/src/React/Basic.purs b/src/React/Basic.purs index c944f70..0a48b75 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -15,14 +15,18 @@ module React.Basic , useEffect , RenderReducer , useReducer - -- , Ref - -- , readRef - -- , renderRef - -- , writeRef - -- , useRef + , RenderRef + , Ref + , readRef + , readRefMaybe + , writeRef + , renderRef + , renderRefMaybe + , useRef , Key , class ToKey , toKey + , unsafeToKey , empty , keyed , fragment @@ -33,7 +37,7 @@ module React.Basic , module Data.Tuple.Nested ) where -import Prelude hiding (bind, discard, pure) +import Prelude hiding (bind,discard,pure) import Control.Applicative.Indexed (class IxApplicative, ipure) import Control.Apply.Indexed (class IxApply) @@ -41,26 +45,26 @@ import Control.Bind.Indexed (class IxBind, ibind) import Data.Function.Uncurried (Fn2, mkFn2, runFn2) import Data.Functor.Indexed (class IxFunctor) import Data.Maybe (Maybe) -import Data.Nullable (Nullable, toNullable) +import Data.Nullable (Nullable, toMaybe, toNullable) import Data.Tuple (Tuple(..)) import Data.Tuple.Nested (tuple2, (/\)) import Effect (Effect) -import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn4, mkEffectFn1, runEffectFn2, runEffectFn4) +import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn3) import Prelude (bind, pure) as Prelude import Unsafe.Coerce (unsafeCoerce) -newtype Component props = Component (EffectFn1 props JSX) +newtype Component props hooks = Component (EffectFn1 props JSX) -foreign import data Render :: Type -> Type -> Type -> Type +newtype Render x y a = Render (Effect a) instance ixFunctorRender :: IxFunctor Render where - imap = unsafeCoerce (map :: forall a b. (a -> b) -> Effect a -> Effect b) + imap f (Render a) = Render (map f a) instance ixApplyRender :: IxApply Render where - iapply = unsafeCoerce (apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b) + iapply (Render f) (Render a) = Render (apply f a) instance ixBindRender :: IxBind Render where - ibind = unsafeCoerce (Prelude.bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b) + ibind (Render m) f = Render (Prelude.bind m \a -> case f a of Render b -> b) bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b bind = ibind @@ -72,17 +76,17 @@ pure :: forall a x m. IxApplicative m => a -> m x x a pure = ipure instance ixApplicativeRender :: IxApplicative Render where - ipure = unsafeCoerce (Prelude.pure :: forall a. a -> Effect a) + ipure a = Render (Prelude.pure a) -type CreateComponent props = Effect (Component props) +type CreateComponent props hooks = Effect (Component props hooks) component - :: forall props + :: forall hooks props . String - -> (forall hooks. props -> Render hooks RenderJSX JSX) - -> CreateComponent props + -> (props -> Render Unit (RenderJSX hooks) JSX) + -> CreateComponent props hooks component name renderFn = - let c = Component (mkEffectFn1 (unsafeCoerce renderFn)) + let c = Component (mkEffectFn1 (\props -> case renderFn props of Render a -> a)) in runEffectFn2 unsafeSetDisplayName name c foreign import data RenderState :: Type -> Type -> Type @@ -91,7 +95,7 @@ useState :: forall hooks state . state -> Render hooks (RenderState state hooks) (Tuple state ((state -> state) -> Effect Unit)) -useState initialState = unsafeCoerce do +useState initialState = Render do runEffectFn2 useState_ (mkFn2 Tuple) initialState foreign import data RenderEffect :: Type -> Type @@ -101,12 +105,12 @@ useEffect . Array Key -> Effect (Effect Unit) -> Render hooks (RenderEffect hooks) Unit -useEffect keys effect = unsafeCoerce (runEffectFn2 useEffect_ effect keys) +useEffect keys effect = Render (runEffectFn2 useEffect_ effect keys) -foreign import data RenderJSX :: Type +foreign import data RenderJSX :: Type -> Type -render :: forall hooks. JSX -> Render hooks RenderJSX JSX -render jsx = unsafeCoerce (ipure jsx :: forall a. Render a a JSX) +render :: forall hooks. JSX -> Render hooks (RenderJSX hooks) JSX +render jsx = Render (Prelude.pure jsx) foreign import data RenderReducer :: Type -> Type -> Type -> Type @@ -115,67 +119,76 @@ foreign import data RenderReducer :: Type -> Type -> Type -> Type useReducer :: forall hooks state action . ToKey state - => Maybe action - -> state + => state -> (state -> action -> state) -> Render hooks (RenderReducer state action hooks) (Tuple state (action -> Effect Unit)) -useReducer initialAction initialState reducer = unsafeCoerce do - runEffectFn4 useReducer_ +useReducer initialState reducer = Render do + runEffectFn3 useReducer_ (mkFn2 Tuple) (mkFn2 reducer) initialState - (toNullable initialAction) --- data Ref a +foreign import data RenderRef :: Type -> Type -> Type + +foreign import data Ref :: Type -> Type + +useRef + :: forall hooks a + . a + -> Render hooks (RenderRef a hooks) (Ref a) +useRef initialValue = Render do + runEffectFn1 useRef_ initialValue + +readRef :: forall a. Ref a -> Effect a +readRef = runEffectFn1 readRef_ --- readRef :: forall a. Ref a -> Effect a --- readRef = runEffectFn1 readRef_ +readRefMaybe :: forall a. Ref (Nullable a) -> Effect (Maybe a) +readRefMaybe a = map toMaybe (readRef a) --- renderRef :: forall a. Ref a -> Render a --- renderRef ref = Render (readRef ref) +writeRef :: forall a. Ref a -> a -> Effect Unit +writeRef = runEffectFn2 writeRef_ --- writeRef :: forall a. Ref a -> a -> Effect Unit --- writeRef = runEffectFn2 writeRef_ +renderRef :: forall hooks a. Ref a -> Render hooks hooks a +renderRef ref = Render (readRef ref) --- useRef --- :: forall a --- . a --- -> Render (Ref a) --- useRef initialValue = Render do --- runEffectFn1 useRef_ initialValue +renderRefMaybe :: forall hooks a. Ref (Nullable a) -> Render hooks hooks (Maybe a) +renderRefMaybe a = Render (readRefMaybe a) -- | Keys represent values React uses to check for changes. -- | This is done using JavaScript's reference equality (`===`), -- | so complicated types may want to implement `ToKey` so that -- | it returns a primative like a `String`. A timestamp appended -- | to a unique ID, for example. Less strict cases can implement --- | `ToKey` using `unsafeCoerce`, while some extreme cases may +-- | `ToKey` using `unsafeToKey`, while some extreme cases may -- | need a hashing or stringifying mechanism. data Key class ToKey a where toKey :: a -> Key +unsafeToKey :: forall a. a -> Key +unsafeToKey = unsafeCoerce + instance trString :: ToKey String where - toKey = unsafeCoerce + toKey = unsafeToKey instance trInt :: ToKey Int where - toKey = unsafeCoerce + toKey = unsafeToKey instance trNumber :: ToKey Number where - toKey = unsafeCoerce + toKey = unsafeToKey instance trBoolean :: ToKey Boolean where - toKey = unsafeCoerce + toKey = unsafeToKey instance trRecord :: ToKey (Record a) where - toKey = unsafeCoerce + toKey = unsafeToKey instance trArray :: ToKey (Array a) where - toKey = unsafeCoerce + toKey = unsafeToKey instance trNullable :: ToKey (Nullable a) where - toKey = unsafeCoerce + toKey = unsafeToKey instance trMaybe :: ToKey (Maybe a) where toKey a = toKey (toNullable a) @@ -227,8 +240,8 @@ foreign import fragment :: Array JSX -> JSX -- | -- | __*See also:* `Component`, `elementKeyed`__ element - :: forall props - . Component {| props } + :: forall hooks props + . Component {| props } hooks -> {| props } -> JSX element (Component c) props = runFn2 element_ c props @@ -240,8 +253,8 @@ element (Component c) props = runFn2 element_ c props -- | -- | __*See also:* `Component`, `element`, React's documentation regarding the special `key` prop__ elementKeyed - :: forall props - . Component {| props } + :: forall hooks props + . Component {| props } hooks -> { key :: String | props } -> JSX elementKeyed = runFn2 elementKeyed_ @@ -251,8 +264,8 @@ elementKeyed = runFn2 elementKeyed_ -- | -- | __*See also:* `displayNameFromSelf`, `createComponent`__ foreign import displayName - :: forall props - . Component props + :: forall hooks props + . Component props hooks -> String @@ -262,8 +275,8 @@ foreign import displayName -- | foreign import unsafeSetDisplayName - :: forall props - . EffectFn2 String (Component props) (Component props) + :: forall hooks props + . EffectFn2 String (Component props hooks) (Component props hooks) foreign import useState_ :: forall state @@ -280,31 +293,30 @@ foreign import useEffect_ foreign import useReducer_ :: forall state action - . EffectFn4 + . EffectFn3 (forall a b. Fn2 a b (Tuple a b)) (Fn2 state action state) state - (Nullable action) (Tuple state (action -> Effect Unit)) --- foreign import readRef_ --- :: forall a --- . EffectFn1 --- (Ref a) --- a - --- foreign import writeRef_ --- :: forall a --- . EffectFn2 --- (Ref a) --- a --- Unit - --- foreign import useRef_ --- :: forall a --- . EffectFn1 --- a --- (Ref a) +foreign import readRef_ + :: forall a + . EffectFn1 + (Ref a) + a + +foreign import writeRef_ + :: forall a + . EffectFn2 + (Ref a) + a + Unit + +foreign import useRef_ + :: forall a + . EffectFn1 + a + (Ref a) foreign import keyed_ :: Fn2 String JSX JSX @@ -313,5 +325,5 @@ foreign import element_ . Fn2 (EffectFn1 { | props } JSX) { | props } JSX foreign import elementKeyed_ - :: forall props - . Fn2 (Component {| props }) { key :: String | props } JSX + :: forall hooks props + . Fn2 (Component {| props } hooks) { key :: String | props } JSX diff --git a/src/React/Basic/Compat.purs b/src/React/Basic/Compat.purs index 2768a7f..bd13c5e 100644 --- a/src/React/Basic/Compat.purs +++ b/src/React/Basic/Compat.purs @@ -1,5 +1,6 @@ module React.Basic.Compat - ( component + ( RenderStatefulComponent + , component , stateless , module React.Basic ) where @@ -11,6 +12,8 @@ import Effect.Unsafe (unsafePerformEffect) import React.Basic (Component, JSX, RenderEffect, RenderState, element, elementKeyed, empty, fragment, keyed) import React.Basic (Tuple(..), bind, discard, component, render, toKey, useEffect, useState) as React +type RenderStatefulComponent state = (RenderEffect (RenderState state Unit)) + -- | Supports a common subset of the v2 API to ease the upgrade process component :: forall props state @@ -19,7 +22,7 @@ component , receiveProps :: { props :: {| props }, state :: {| state }, setState :: ({| state } -> {| state }) -> Effect Unit } -> Effect Unit , render :: { props :: {| props }, state :: {| state }, setState :: ({| state } -> {| state }) -> Effect Unit } -> JSX } - -> Component {| props } + -> Component {| props } (RenderStatefulComponent {| state }) component { displayName, initialState, receiveProps, render } = unsafePerformEffect do React.component displayName \props -> React.do React.Tuple state setState <- React.useState initialState @@ -34,7 +37,7 @@ stateless . { displayName :: String , render :: {| props } -> JSX } - -> Component {| props } + -> Component {| props } Unit stateless { displayName, render } = unsafePerformEffect do React.component displayName \props -> React.do React.render (render props) diff --git a/src/React/Basic/DOM/Internal.purs b/src/React/Basic/DOM/Internal.purs index c3b6240..8c3ff52 100644 --- a/src/React/Basic/DOM/Internal.purs +++ b/src/React/Basic/DOM/Internal.purs @@ -1,8 +1,12 @@ module React.Basic.DOM.Internal where -import React.Basic (Component) +import Prelude + +import Data.Nullable (Nullable) +import React.Basic (Component, Ref) import React.Basic.Events (EventHandler) import Unsafe.Coerce (unsafeCoerce) +import Web.DOM (Node) -- | An abstract type representing records of CSS attributes. foreign import data CSS :: Type @@ -12,6 +16,9 @@ type SharedProps specific = -- | `key` is not really a DOM attribute - React intercepts it ( key :: String + -- | `ref` is not really a DOM attribute - React intercepts it + , ref :: Ref (Nullable Node) + , about :: String , acceptCharset :: String , accessKey :: String @@ -93,5 +100,5 @@ type SharedProps specific = | specific ) -unsafeCreateDOMComponent :: forall props. String -> Component props +unsafeCreateDOMComponent :: forall props. String -> Component props Unit unsafeCreateDOMComponent = unsafeCoerce From b026dc1b0ee7c4f5f551a7a41d3d71710f927d92 Mon Sep 17 00:00:00 2001 From: Madeline Trotter Date: Wed, 19 Dec 2018 22:50:55 -0700 Subject: [PATCH 10/12] Cleanup/rename --- examples/component/src/Container.purs | 5 +- examples/component/src/ToggleButton.purs | 6 +- .../controlled-input/src/ControlledInput.purs | 10 +- examples/counter/src/Counter.purs | 6 +- examples/legacy-v2/src/LegacyCounter.purs | 4 +- examples/reducer/src/Reducer.purs | 6 +- examples/refs/src/Refs.purs | 10 +- generated-docs/React/Basic.md | 123 +++++++++--------- generated-docs/React/Basic/Compat.md | 19 ++- src/React/Basic.purs | 105 ++++++++------- src/React/Basic/Compat.purs | 14 +- 11 files changed, 150 insertions(+), 158 deletions(-) diff --git a/examples/component/src/Container.purs b/examples/component/src/Container.purs index 7b6737e..e960282 100644 --- a/examples/component/src/Container.purs +++ b/examples/component/src/Container.purs @@ -2,7 +2,8 @@ module Container where import Prelude -import React.Basic (CreateComponent, component, element, render) +import React.Basic (CreateComponent, component, element) +import React.Basic as React import React.Basic.DOM as R import ToggleButton (mkToggleButton) @@ -11,7 +12,7 @@ mkToggleButtonContainer = do toggleButton <- mkToggleButton component "Container" \_ -> - render $ R.div + React.pure $ R.div { children: [ element toggleButton { label: "A" } , element toggleButton { label: "B" } diff --git a/examples/component/src/ToggleButton.purs b/examples/component/src/ToggleButton.purs index 96248a4..d3fea55 100644 --- a/examples/component/src/ToggleButton.purs +++ b/examples/component/src/ToggleButton.purs @@ -3,12 +3,12 @@ module ToggleButton where import Prelude import Effect.Console (log) -import React.Basic (CreateComponent, RenderEffect, RenderState, component, render, toKey, useEffect, useState, (/\)) +import React.Basic (CreateComponent, UseEffect, UseState, component, toKey, useEffect, useState, (/\)) import React.Basic as React import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) -mkToggleButton :: CreateComponent { label :: String } (RenderEffect (RenderState Boolean Unit)) +mkToggleButton :: CreateComponent { label :: String } (UseEffect (UseState Boolean Unit)) mkToggleButton = do component "ToggleButton" \{ label } -> React.do on /\ setOn <- useState false @@ -17,7 +17,7 @@ mkToggleButton = do log $ "State: " <> if on then "On" else "Off" pure (pure unit) - render $ R.button + React.pure $ R.button { onClick: capture_ $ setOn not , children: [ R.text label diff --git a/examples/controlled-input/src/ControlledInput.purs b/examples/controlled-input/src/ControlledInput.purs index cf134ec..845730e 100644 --- a/examples/controlled-input/src/ControlledInput.purs +++ b/examples/controlled-input/src/ControlledInput.purs @@ -4,19 +4,19 @@ import Prelude import Control.Applicative.Indexed (ipure) import Data.Maybe (Maybe(..), fromMaybe, maybe) -import React.Basic (CreateComponent, Render, RenderState, component, fragment, render, useState, (/\)) +import React.Basic (CreateComponent, Render, UseState, component, fragment, useState, (/\)) import React.Basic as React import React.Basic.DOM as R import React.Basic.DOM.Events (capture, targetValue, timeStamp) import React.Basic.Events (EventHandler, merge) -mkControlledInput :: CreateComponent {} (RenderInput () (RenderInput () Unit)) +mkControlledInput :: CreateComponent {} (UseInput () (UseInput () Unit)) mkControlledInput = component "ControlledInput" \props -> React.do firstName <- useInput "hello" lastName <- useInput "world" - render $ R.form_ + React.pure $ R.form_ [ renderInput firstName , renderInput lastName ] @@ -30,12 +30,12 @@ mkControlledInput = type InputState state = { value :: String, lastChanged :: Maybe Number | state } -type RenderInput state hooks = RenderState (InputState state) hooks +type UseInput state hooks = UseState (InputState state) hooks useInput :: forall hooks . String -> Render hooks - (RenderInput () hooks) + (UseInput () hooks) (InputState ( onChange :: EventHandler )) useInput initialValue = React.do { value, lastChanged } /\ replaceState <- useState { value: initialValue, lastChanged: Nothing } diff --git a/examples/counter/src/Counter.purs b/examples/counter/src/Counter.purs index 2b79715..9fa9a6e 100644 --- a/examples/counter/src/Counter.purs +++ b/examples/counter/src/Counter.purs @@ -3,12 +3,12 @@ module Counter where import Prelude import Effect (Effect) -import React.Basic (CreateComponent, RenderEffect, RenderState, component, render, toKey, useEffect, useState, (/\)) +import React.Basic (CreateComponent, UseEffect, UseState, component, toKey, useEffect, useState, (/\)) import React.Basic as React import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) -mkCounter :: CreateComponent {} (RenderEffect (RenderState Int Unit)) +mkCounter :: CreateComponent {} (UseEffect (UseState Int Unit)) mkCounter = do component "Counter" \props -> React.do counter /\ setCounter <- useState 0 @@ -17,7 +17,7 @@ mkCounter = do setDocumentTitle $ "Count: " <> show counter pure mempty - render $ R.button + React.pure $ R.button { onClick: capture_ $ setCounter (_ + 1) , children: [ R.text $ "Increment: " <> show counter ] } diff --git a/examples/legacy-v2/src/LegacyCounter.purs b/examples/legacy-v2/src/LegacyCounter.purs index 971f446..b578ccb 100644 --- a/examples/legacy-v2/src/LegacyCounter.purs +++ b/examples/legacy-v2/src/LegacyCounter.purs @@ -2,7 +2,7 @@ module LegacyCounter where import Prelude -import React.Basic.Compat (Component, RenderStatefulComponent, component, element, stateless) +import React.Basic.Compat (Component, UseStatefulComponent, component, element, stateless) import React.Basic.DOM as R import React.Basic.Events as Events @@ -11,7 +11,7 @@ type Props = } -- | checks `component` -legacyCounter :: Component Props (RenderStatefulComponent { counter :: Int }) +legacyCounter :: Component Props (UseStatefulComponent { counter :: Int }) legacyCounter = component { displayName: "LegacyCounter", initialState, receiveProps, render } where initialState = diff --git a/examples/reducer/src/Reducer.purs b/examples/reducer/src/Reducer.purs index f786ce3..36778bd 100644 --- a/examples/reducer/src/Reducer.purs +++ b/examples/reducer/src/Reducer.purs @@ -2,7 +2,7 @@ module Reducer where import Prelude -import React.Basic (CreateComponent, RenderReducer, component, fragment, render, useReducer, (/\)) +import React.Basic (CreateComponent, UseReducer, component, fragment, useReducer, (/\)) import React.Basic as React import React.Basic.DOM as R import React.Basic.DOM.Events (capture_) @@ -11,7 +11,7 @@ data Action = Increment | Decrement -mkReducer :: CreateComponent {} (RenderReducer { counter :: Int } Action Unit) +mkReducer :: CreateComponent {} (UseReducer { counter :: Int } Action Unit) mkReducer = do component "Reducer" \props -> React.do @@ -20,7 +20,7 @@ mkReducer = do Increment -> state { counter = state.counter + 1 } Decrement -> state { counter = state.counter - 1 } - render $ fragment + React.pure $ fragment [ R.button { onClick: capture_ $ dispatch Decrement , children: [ R.text $ "Decrement" ] diff --git a/examples/refs/src/Refs.purs b/examples/refs/src/Refs.purs index 0cf4c58..7fd4a3f 100644 --- a/examples/refs/src/Refs.purs +++ b/examples/refs/src/Refs.purs @@ -6,7 +6,7 @@ import Data.Int (round) import Data.Maybe (Maybe(..)) import Data.Nullable (Nullable, null) import Math (pow, sqrt) -import React.Basic (CreateComponent, Ref, Render, RenderEffect, RenderRef, RenderState, Tuple, component, fragment, readRefMaybe, render, useEffect, useRef, useState, (/\)) +import React.Basic (CreateComponent, Ref, Render, UseEffect, UseRef, UseState, Tuple, component, fragment, readRefMaybe, useEffect, useRef, useState, (/\)) import React.Basic as React import React.Basic.DOM as R import Unsafe.Coerce (unsafeCoerce) @@ -18,7 +18,7 @@ import Web.HTML.HTMLElement (getBoundingClientRect) import Web.HTML.HTMLElement as HTMLElement import Web.HTML.Window as Window -mkRefs :: CreateComponent {} (RenderNodeDistance (RenderNodeDistance (RenderNodeDistance Unit))) +mkRefs :: CreateComponent {} (UseNodeDistance (UseNodeDistance (UseNodeDistance Unit))) mkRefs = do component "Refs" \props -> React.do @@ -26,7 +26,7 @@ mkRefs = do mouseDistance2 /\ buttonRef2 <- useNodeDistanceFromMouse mouseDistance3 /\ buttonRef3 <- useNodeDistanceFromMouse - render $ fragment + React.pure $ fragment [ R.button { ref: buttonRef1 , children: [ R.text $ show mouseDistance1 <> "px" ] @@ -44,13 +44,13 @@ mkRefs = do } ] -type RenderNodeDistance hooks = RenderEffect (RenderState Int (RenderRef (Nullable Node) hooks)) +type UseNodeDistance hooks = UseEffect (UseState Int (UseRef (Nullable Node) hooks)) useNodeDistanceFromMouse :: forall hooks . Render hooks - (RenderNodeDistance hooks) + (UseNodeDistance hooks) (Tuple Int (Ref (Nullable Node))) useNodeDistanceFromMouse = React.do elementRef <- useRef null diff --git a/generated-docs/React/Basic.md b/generated-docs/React/Basic.md index 1d147dc..3a3a8ac 100644 --- a/generated-docs/React/Basic.md +++ b/generated-docs/React/Basic.md @@ -6,49 +6,7 @@ newtype Component props hooks ``` -#### `Render` - -``` purescript -newtype Render x y a -``` - -##### Instances -``` purescript -IxFunctor Render -IxApply Render -IxBind Render -IxApplicative Render -``` - -#### `bind` - -``` purescript -bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b -``` - -#### `discard` - -``` purescript -discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b -``` - -#### `pure` - -``` purescript -pure :: forall a x m. IxApplicative m => a -> m x x a -``` - -#### `RenderJSX` - -``` purescript -data RenderJSX :: Type -> Type -``` - -#### `render` - -``` purescript -render :: forall hooks. JSX -> Render hooks (RenderJSX hooks) JSX -``` +A React component #### `CreateComponent` @@ -56,6 +14,10 @@ render :: forall hooks. JSX -> Render hooks (RenderJSX hooks) JSX type CreateComponent props hooks = Effect (Component props hooks) ``` +Alias for convenience. Creating components is effectful because +React uses the function instance as the component's "identity" +or "type". + #### `JSX` ``` purescript @@ -84,52 +46,51 @@ Monoid JSX #### `component` ``` purescript -component :: forall hooks props. String -> (props -> Render Unit (RenderJSX hooks) JSX) -> CreateComponent props hooks +component :: forall hooks props. String -> (props -> Render Unit hooks JSX) -> CreateComponent props hooks ``` -#### `RenderState` +Create a React component given a display name and render function. + +#### `UseState` ``` purescript -data RenderState :: Type -> Type -> Type +data UseState :: Type -> Type -> Type ``` #### `useState` ``` purescript -useState :: forall hooks state. state -> Render hooks (RenderState state hooks) (Tuple state ((state -> state) -> Effect Unit)) +useState :: forall hooks state. state -> Render hooks (UseState state hooks) (Tuple state ((state -> state) -> Effect Unit)) ``` -#### `RenderEffect` +#### `UseEffect` ``` purescript -data RenderEffect :: Type -> Type +data UseEffect :: Type -> Type ``` #### `useEffect` ``` purescript -useEffect :: forall hooks. Array Key -> Effect (Effect Unit) -> Render hooks (RenderEffect hooks) Unit +useEffect :: forall hooks. Array Key -> Effect (Effect Unit) -> Render hooks (UseEffect hooks) Unit ``` -#### `RenderReducer` +#### `UseReducer` ``` purescript -data RenderReducer :: Type -> Type -> Type -> Type +data UseReducer :: Type -> Type -> Type -> Type ``` #### `useReducer` ``` purescript -useReducer :: forall hooks state action. ToKey state => state -> (state -> action -> state) -> Render hooks (RenderReducer state action hooks) (Tuple state (action -> Effect Unit)) +useReducer :: forall hooks state action. ToKey state => state -> (state -> action -> state) -> Render hooks (UseReducer state action hooks) (Tuple state (action -> Effect Unit)) ``` -useReducer -TODO: add note about conditionally updating state - -#### `RenderRef` +#### `UseRef` ``` purescript -data RenderRef :: Type -> Type -> Type +data UseRef :: Type -> Type -> Type ``` #### `Ref` @@ -171,7 +132,7 @@ renderRefMaybe :: forall hooks a. Ref (Nullable a) -> Render hooks hooks (Maybe #### `useRef` ``` purescript -useRef :: forall hooks a. a -> Render hooks (RenderRef a hooks) (Ref a) +useRef :: forall hooks a. a -> Render hooks (UseRef a hooks) (Ref a) ``` #### `Key` @@ -225,6 +186,43 @@ on the parent node. __*See also:* `JSX`, Monoid `guard`__ +#### `Render` + +``` purescript +newtype Render x y a +``` + +Render represents the effects allowed within a React component's +body, i.e. during "render". This includes hooks and ends with +returning JSX (see `pure`), but does not allow arbitrary side +effects. + +##### Instances +``` purescript +IxFunctor Render +IxApply Render +IxBind Render +IxApplicative Render +``` + +#### `bind` + +``` purescript +bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b +``` + +#### `discard` + +``` purescript +discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b +``` + +#### `pure` + +``` purescript +pure :: forall a x m. IxApplicative m => a -> m x x a +``` + #### `keyed` ``` purescript @@ -268,9 +266,6 @@ elementKeyed :: forall hooks props. Component { | props } hooks -> { key :: Str Create a `JSX` node from a `Component`, by providing the props and a key. -This function is for non-React-Basic React components, such as those -imported from FFI. - __*See also:* `Component`, `element`, React's documentation regarding the special `key` prop__ #### `displayName` @@ -279,10 +274,10 @@ __*See also:* `Component`, `element`, React's documentation regarding the specia displayName :: forall hooks props. Component props hooks -> String ``` -Retrieve the Display Name from a `ComponentSpec`. Useful for debugging and improving +Retrieve the Display Name from a `Component`. Useful for debugging and improving error messages in logs. -__*See also:* `displayNameFromSelf`, `createComponent`__ +__*See also:* `component`__ ### Re-exported from Data.Tuple: diff --git a/generated-docs/React/Basic/Compat.md b/generated-docs/React/Basic/Compat.md index 91c3c1a..3f32360 100644 --- a/generated-docs/React/Basic/Compat.md +++ b/generated-docs/React/Basic/Compat.md @@ -1,15 +1,15 @@ ## Module React.Basic.Compat -#### `RenderStatefulComponent` +#### `UseStatefulComponent` ``` purescript -type RenderStatefulComponent state = RenderEffect (RenderState state Unit) +type UseStatefulComponent state = UseEffect (UseState state Unit) ``` #### `component` ``` purescript -component :: forall props state. { displayName :: String, initialState :: { | state }, receiveProps :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> Effect Unit, render :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> JSX } -> Component { | props } (RenderStatefulComponent { | state }) +component :: forall props state. { displayName :: String, initialState :: { | state }, receiveProps :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> Effect Unit, render :: { props :: { | props }, state :: { | state }, setState :: ({ | state } -> { | state }) -> Effect Unit } -> JSX } -> Component { | props } (UseStatefulComponent { | state }) ``` Supports a common subset of the v2 API to ease the upgrade process @@ -25,16 +25,16 @@ Supports a common subset of the v2 API to ease the upgrade process ### Re-exported from React.Basic: -#### `RenderState` +#### `UseState` ``` purescript -data RenderState :: Type -> Type -> Type +data UseState :: Type -> Type -> Type ``` -#### `RenderEffect` +#### `UseEffect` ``` purescript -data RenderEffect :: Type -> Type +data UseEffect :: Type -> Type ``` #### `JSX` @@ -68,6 +68,8 @@ Monoid JSX newtype Component props hooks ``` +A React component + #### `keyed` ``` purescript @@ -110,9 +112,6 @@ elementKeyed :: forall hooks props. Component { | props } hooks -> { key :: Str Create a `JSX` node from a `Component`, by providing the props and a key. -This function is for non-React-Basic React components, such as those -imported from FFI. - __*See also:* `Component`, `element`, React's documentation regarding the special `key` prop__ #### `element` diff --git a/src/React/Basic.purs b/src/React/Basic.purs index 0a48b75..ffabcf0 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -1,21 +1,15 @@ module React.Basic ( Component - , Render - , bind - , discard - , pure - , RenderJSX - , render , CreateComponent , JSX , component - , RenderState + , UseState , useState - , RenderEffect + , UseEffect , useEffect - , RenderReducer + , UseReducer , useReducer - , RenderRef + , UseRef , Ref , readRef , readRefMaybe @@ -28,6 +22,10 @@ module React.Basic , toKey , unsafeToKey , empty + , Render + , bind + , discard + , pure , keyed , fragment , element @@ -53,89 +51,64 @@ import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, runEffect import Prelude (bind, pure) as Prelude import Unsafe.Coerce (unsafeCoerce) +-- | A React component newtype Component props hooks = Component (EffectFn1 props JSX) -newtype Render x y a = Render (Effect a) - -instance ixFunctorRender :: IxFunctor Render where - imap f (Render a) = Render (map f a) - -instance ixApplyRender :: IxApply Render where - iapply (Render f) (Render a) = Render (apply f a) - -instance ixBindRender :: IxBind Render where - ibind (Render m) f = Render (Prelude.bind m \a -> case f a of Render b -> b) - -bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b -bind = ibind - -discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b -discard = ibind - -pure :: forall a x m. IxApplicative m => a -> m x x a -pure = ipure - -instance ixApplicativeRender :: IxApplicative Render where - ipure a = Render (Prelude.pure a) - +-- | Alias for convenience. Creating components is effectful because +-- | React uses the function instance as the component's "identity" +-- | or "type". type CreateComponent props hooks = Effect (Component props hooks) +-- | Create a React component given a display name and render function. component :: forall hooks props . String - -> (props -> Render Unit (RenderJSX hooks) JSX) + -> (props -> Render Unit hooks JSX) -> CreateComponent props hooks component name renderFn = let c = Component (mkEffectFn1 (\props -> case renderFn props of Render a -> a)) in runEffectFn2 unsafeSetDisplayName name c -foreign import data RenderState :: Type -> Type -> Type +foreign import data UseState :: Type -> Type -> Type useState :: forall hooks state . state - -> Render hooks (RenderState state hooks) (Tuple state ((state -> state) -> Effect Unit)) + -> Render hooks (UseState state hooks) (Tuple state ((state -> state) -> Effect Unit)) useState initialState = Render do runEffectFn2 useState_ (mkFn2 Tuple) initialState -foreign import data RenderEffect :: Type -> Type +foreign import data UseEffect :: Type -> Type useEffect :: forall hooks . Array Key -> Effect (Effect Unit) - -> Render hooks (RenderEffect hooks) Unit + -> Render hooks (UseEffect hooks) Unit useEffect keys effect = Render (runEffectFn2 useEffect_ effect keys) -foreign import data RenderJSX :: Type -> Type - -render :: forall hooks. JSX -> Render hooks (RenderJSX hooks) JSX -render jsx = Render (Prelude.pure jsx) +foreign import data UseReducer :: Type -> Type -> Type -> Type -foreign import data RenderReducer :: Type -> Type -> Type -> Type - --- | useReducer --- | TODO: add note about conditionally updating state useReducer :: forall hooks state action . ToKey state => state -> (state -> action -> state) - -> Render hooks (RenderReducer state action hooks) (Tuple state (action -> Effect Unit)) + -> Render hooks (UseReducer state action hooks) (Tuple state (action -> Effect Unit)) useReducer initialState reducer = Render do runEffectFn3 useReducer_ (mkFn2 Tuple) (mkFn2 reducer) initialState -foreign import data RenderRef :: Type -> Type -> Type +foreign import data UseRef :: Type -> Type -> Type foreign import data Ref :: Type -> Type useRef :: forall hooks a . a - -> Render hooks (RenderRef a hooks) (Ref a) + -> Render hooks (UseRef a hooks) (Ref a) useRef initialValue = Render do runEffectFn1 useRef_ initialValue @@ -220,6 +193,33 @@ instance monoidJSX :: Monoid JSX where -- | __*See also:* `JSX`, Monoid `guard`__ foreign import empty :: JSX +-- | Render represents the effects allowed within a React component's +-- | body, i.e. during "render". This includes hooks and ends with +-- | returning JSX (see `pure`), but does not allow arbitrary side +-- | effects. +newtype Render x y a = Render (Effect a) + +instance ixFunctorRender :: IxFunctor Render where + imap f (Render a) = Render (map f a) + +instance ixApplyRender :: IxApply Render where + iapply (Render f) (Render a) = Render (apply f a) + +instance ixBindRender :: IxBind Render where + ibind (Render m) f = Render (Prelude.bind m \a -> case f a of Render b -> b) + +instance ixApplicativeRender :: IxApplicative Render where + ipure a = Render (Prelude.pure a) + +bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b +bind = ibind + +discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b +discard = ibind + +pure :: forall a x m. IxApplicative m => a -> m x x a +pure = ipure + -- | Apply a React key to a subtree. React-Basic usually hides React's warning about -- | using `key` props on components in an Array, but keys are still important for -- | any dynamic lists of child components. @@ -248,9 +248,6 @@ element (Component c) props = runFn2 element_ c props -- | Create a `JSX` node from a `Component`, by providing the props and a key. -- | --- | This function is for non-React-Basic React components, such as those --- | imported from FFI. --- | -- | __*See also:* `Component`, `element`, React's documentation regarding the special `key` prop__ elementKeyed :: forall hooks props @@ -259,10 +256,10 @@ elementKeyed -> JSX elementKeyed = runFn2 elementKeyed_ --- | Retrieve the Display Name from a `ComponentSpec`. Useful for debugging and improving +-- | Retrieve the Display Name from a `Component`. Useful for debugging and improving -- | error messages in logs. -- | --- | __*See also:* `displayNameFromSelf`, `createComponent`__ +-- | __*See also:* `component`__ foreign import displayName :: forall hooks props . Component props hooks diff --git a/src/React/Basic/Compat.purs b/src/React/Basic/Compat.purs index bd13c5e..94e951a 100644 --- a/src/React/Basic/Compat.purs +++ b/src/React/Basic/Compat.purs @@ -1,5 +1,5 @@ module React.Basic.Compat - ( RenderStatefulComponent + ( UseStatefulComponent , component , stateless , module React.Basic @@ -9,10 +9,10 @@ import Prelude import Effect (Effect) import Effect.Unsafe (unsafePerformEffect) -import React.Basic (Component, JSX, RenderEffect, RenderState, element, elementKeyed, empty, fragment, keyed) -import React.Basic (Tuple(..), bind, discard, component, render, toKey, useEffect, useState) as React +import React.Basic (Component, JSX, UseEffect, UseState, element, elementKeyed, empty, fragment, keyed) +import React.Basic (Tuple(..), bind, discard, pure, component, toKey, useEffect, useState) as React -type RenderStatefulComponent state = (RenderEffect (RenderState state Unit)) +type UseStatefulComponent state = (UseEffect (UseState state Unit)) -- | Supports a common subset of the v2 API to ease the upgrade process component @@ -22,14 +22,14 @@ component , receiveProps :: { props :: {| props }, state :: {| state }, setState :: ({| state } -> {| state }) -> Effect Unit } -> Effect Unit , render :: { props :: {| props }, state :: {| state }, setState :: ({| state } -> {| state }) -> Effect Unit } -> JSX } - -> Component {| props } (RenderStatefulComponent {| state }) + -> Component {| props } (UseStatefulComponent {| state }) component { displayName, initialState, receiveProps, render } = unsafePerformEffect do React.component displayName \props -> React.do React.Tuple state setState <- React.useState initialState React.useEffect [React.toKey props, React.toKey state] do receiveProps { props, state, setState } pure (pure unit) - React.render (render { props, state, setState }) + React.pure (render { props, state, setState }) -- | Supports a common subset of the v2 API to ease the upgrade process stateless @@ -40,4 +40,4 @@ stateless -> Component {| props } Unit stateless { displayName, render } = unsafePerformEffect do React.component displayName \props -> React.do - React.render (render props) + React.pure (render props) From be61ce5a4cf5a529ff232e5dd4d5382ce8cf2766 Mon Sep 17 00:00:00 2001 From: Madeline Trotter Date: Thu, 20 Dec 2018 22:33:05 -0700 Subject: [PATCH 11/12] Add useMemo, useLayoutEffect --- src/React/Basic.js | 25 +++++--------------- src/React/Basic.purs | 54 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/src/React/Basic.js b/src/React/Basic.js index 0b28dbb..3cd7f51 100644 --- a/src/React/Basic.js +++ b/src/React/Basic.js @@ -3,12 +3,6 @@ var React = require("react"); var Fragment = React.Fragment || "div"; -// foreign import useState_ -// :: forall state -// . EffectFn2 -// (forall a b. Fn2 a b (Tuple a b)) -// state -// (Tuple state ((state -> state) -> Effect Unit)) exports.useState_ = function(tuple, initialState) { var r = React.useState(initialState); var state = r[0]; @@ -20,21 +14,10 @@ exports.useState_ = function(tuple, initialState) { }); }; -// foreign import useEffect_ -// :: EffectFn2 -// (Effect (Effect Unit)) -// (Array Key) -// Unit exports.useEffect_ = React.useEffect; -// foreign import useReducer_ -// :: forall state action -// . EffectFn4 -// (forall a b. Fn2 a b (Tuple a b)) -// (Fn2 state action state) -// state -// (Nullable action) -// (Tuple state (action -> Effect Unit)) +exports.useLayoutEffect_ = React.useLayoutEffect; + exports.useReducer_ = function(tuple, reducer, initialState, initialAction) { var r = React.useReducer(reducer, initialState, initialAction); var state = r[0]; @@ -56,6 +39,10 @@ exports.writeRef_ = function(ref, a) { ref.current = a; }; +// exports.useContext_ = React.useContext; + +exports.useMemo_ = React.useMemo; + exports.empty = null; exports.keyed_ = function(key, child) { diff --git a/src/React/Basic.purs b/src/React/Basic.purs index ffabcf0..b16fd68 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -7,6 +7,8 @@ module React.Basic , useState , UseEffect , useEffect + , UseLayoutEffect + , useLayoutEffect , UseReducer , useReducer , UseRef @@ -17,6 +19,11 @@ module React.Basic , renderRef , renderRefMaybe , useRef + -- , UseContext + -- , Context + -- , useContext + , UseMemo + , useMemo , Key , class ToKey , toKey @@ -87,6 +94,15 @@ useEffect -> Render hooks (UseEffect hooks) Unit useEffect keys effect = Render (runEffectFn2 useEffect_ effect keys) +foreign import data UseLayoutEffect :: Type -> Type + +useLayoutEffect + :: forall hooks + . Array Key + -> Effect (Effect Unit) + -> Render hooks (UseLayoutEffect hooks) Unit +useLayoutEffect keys effect = Render (runEffectFn2 useLayoutEffect_ effect keys) + foreign import data UseReducer :: Type -> Type -> Type -> Type useReducer @@ -127,6 +143,25 @@ renderRef ref = Render (readRef ref) renderRefMaybe :: forall hooks a. Ref (Nullable a) -> Render hooks hooks (Maybe a) renderRefMaybe a = Render (readRefMaybe a) +-- foreign import data UseContext :: Type -> Type -> Type + +-- foreign import data Context :: Type -> Type + +-- useContext +-- :: forall hooks a +-- . Context a +-- -> Render hooks (UseContext a hooks) a +-- useContext context = Render (runEffectFn1 useContext_ context) + +foreign import data UseMemo :: Type -> Type -> Type + +useMemo + :: forall hooks a + . Array Key + -> (Unit -> a) + -> Render hooks (UseMemo a hooks) a +useMemo keys factory = Render (runEffectFn2 useMemo_ factory keys) + -- | Keys represent values React uses to check for changes. -- | This is done using JavaScript's reference equality (`===`), -- | so complicated types may want to implement `ToKey` so that @@ -288,6 +323,12 @@ foreign import useEffect_ (Array Key) Unit +foreign import useLayoutEffect_ + :: EffectFn2 + (Effect (Effect Unit)) + (Array Key) + Unit + foreign import useReducer_ :: forall state action . EffectFn3 @@ -315,6 +356,19 @@ foreign import useRef_ a (Ref a) +-- foreign import useContext_ +-- :: forall a +-- . EffectFn1 +-- (Context a) +-- a + +foreign import useMemo_ + :: forall a + . EffectFn2 + (Unit -> a) + (Array Key) + a + foreign import keyed_ :: Fn2 String JSX JSX foreign import element_ From acd9f40a68d243a55d4a86d9f36aae6d866a67ff Mon Sep 17 00:00:00 2001 From: Madeline Trotter Date: Fri, 21 Dec 2018 00:10:14 -0700 Subject: [PATCH 12/12] Add TodoApp example --- examples/component/README.md | 5 +- examples/controlled-input/README.md | 5 +- examples/counter/README.md | 5 +- examples/legacy-v2/README.md | 7 +- examples/reducer/README.md | 7 +- examples/refs/README.md | 7 +- examples/todo-app/.gitignore | 4 + examples/todo-app/Makefile | 8 ++ examples/todo-app/README.md | 11 ++ examples/todo-app/html/index.html | 10 ++ examples/todo-app/package.json | 10 ++ examples/todo-app/src/Main.purs | 24 +++++ examples/todo-app/src/TodoApp.purs | 159 ++++++++++++++++++++++++++++ src/React/Basic.js | 2 + src/React/Basic.purs | 13 +++ 15 files changed, 256 insertions(+), 21 deletions(-) create mode 100644 examples/todo-app/.gitignore create mode 100644 examples/todo-app/Makefile create mode 100644 examples/todo-app/README.md create mode 100644 examples/todo-app/html/index.html create mode 100644 examples/todo-app/package.json create mode 100644 examples/todo-app/src/Main.purs create mode 100644 examples/todo-app/src/TodoApp.purs diff --git a/examples/component/README.md b/examples/component/README.md index cc747f4..aef84a0 100644 --- a/examples/component/README.md +++ b/examples/component/README.md @@ -2,9 +2,8 @@ ## Building -``` -npm install -make all +```sh +make ``` This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. diff --git a/examples/controlled-input/README.md b/examples/controlled-input/README.md index 2004b87..a36ddf3 100644 --- a/examples/controlled-input/README.md +++ b/examples/controlled-input/README.md @@ -2,9 +2,8 @@ ## Building -``` -npm install -make all +```sh +make ``` This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. diff --git a/examples/counter/README.md b/examples/counter/README.md index f2418c0..88847e5 100644 --- a/examples/counter/README.md +++ b/examples/counter/README.md @@ -2,9 +2,8 @@ ## Building -``` -npm install -make all +```sh +make ``` This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. diff --git a/examples/legacy-v2/README.md b/examples/legacy-v2/README.md index f2418c0..7e1799b 100644 --- a/examples/legacy-v2/README.md +++ b/examples/legacy-v2/README.md @@ -1,10 +1,9 @@ -# Counter Example +# Legacy Example ## Building -``` -npm install -make all +```sh +make ``` This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. diff --git a/examples/reducer/README.md b/examples/reducer/README.md index f2418c0..8c2ad2e 100644 --- a/examples/reducer/README.md +++ b/examples/reducer/README.md @@ -1,10 +1,9 @@ -# Counter Example +# Reducer Example ## Building -``` -npm install -make all +```sh +make ``` This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. diff --git a/examples/refs/README.md b/examples/refs/README.md index f2418c0..baa86ae 100644 --- a/examples/refs/README.md +++ b/examples/refs/README.md @@ -1,10 +1,9 @@ -# Counter Example +# Refs Example ## Building -``` -npm install -make all +```sh +make ``` This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle. diff --git a/examples/todo-app/.gitignore b/examples/todo-app/.gitignore new file mode 100644 index 0000000..645684d --- /dev/null +++ b/examples/todo-app/.gitignore @@ -0,0 +1,4 @@ +output +html/index.js +package-lock.json +node_modules diff --git a/examples/todo-app/Makefile b/examples/todo-app/Makefile new file mode 100644 index 0000000..ecacfbe --- /dev/null +++ b/examples/todo-app/Makefile @@ -0,0 +1,8 @@ +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 + +node_modules: + npm install + diff --git a/examples/todo-app/README.md b/examples/todo-app/README.md new file mode 100644 index 0000000..55d0bc0 --- /dev/null +++ b/examples/todo-app/README.md @@ -0,0 +1,11 @@ +# 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/html/index.html b/examples/todo-app/html/index.html new file mode 100644 index 0000000..6b93b7c --- /dev/null +++ b/examples/todo-app/html/index.html @@ -0,0 +1,10 @@ + + + + react-basic example + + +
+ + + diff --git a/examples/todo-app/package.json b/examples/todo-app/package.json new file mode 100644 index 0000000..80aec4d --- /dev/null +++ b/examples/todo-app/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2", + "uuid": "3.3.2" + }, + "devDependencies": { + "browserify": "16.2.3" + } +} diff --git a/examples/todo-app/src/Main.purs b/examples/todo-app/src/Main.purs new file mode 100644 index 0000000..86e4dac --- /dev/null +++ b/examples/todo-app/src/Main.purs @@ -0,0 +1,24 @@ +module Main where + +import Prelude + +import TodoApp (mkTodoApp) +import Data.Maybe (Maybe(..)) +import Effect (Effect) +import Effect.Exception (throw) +import React.Basic (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 + todoApp <- mkTodoApp + let app = element todoApp {} + render app c diff --git a/examples/todo-app/src/TodoApp.purs b/examples/todo-app/src/TodoApp.purs new file mode 100644 index 0000000..2b34c5b --- /dev/null +++ b/examples/todo-app/src/TodoApp.purs @@ -0,0 +1,159 @@ +module TodoApp where + +import Prelude + +import Data.Array as Array +import Data.Foldable (traverse_) +import Data.Maybe (Maybe(..)) +import Effect (Effect) +import React.Basic (CreateComponent, UseReducer, UseState, component, element, elementKeyed, empty, memo, useReducer, useState, (/\)) +import React.Basic as React +import React.Basic.DOM as R +import React.Basic.DOM.Events (capture, capture_, targetValue) +import React.Basic.Events as Events + +data Action + = CreateTodo String + | ToggleTodo Int + | DeleteTodo Int + | SetFilter TodoFilter + +data TodoFilter + = All + | Complete + | Incomplete +derive instance eqTodoFilter :: Eq TodoFilter + +type Todo = + { task :: String + , isComplete :: Boolean + } + +type State = + { todos :: Array Todo + , filter :: TodoFilter + } + +reducer :: State -> Action -> State +reducer state = case _ of + CreateTodo task -> + state { todos = Array.cons { task, isComplete: false } state.todos } + + ToggleTodo index -> + case Array.modifyAt index (\todo -> todo { isComplete = not todo.isComplete }) state.todos of + Just todos -> + state { todos = todos } + Nothing -> + state + + DeleteTodo index -> + case Array.deleteAt index state.todos of + Just todos -> + state { todos = todos } + Nothing -> + state + + SetFilter filter -> + state { filter = filter } + +mkTodoApp :: CreateComponent {} (UseReducer State Action Unit) +mkTodoApp = do + let initialState = { todos: [], filter: All } + todoInput <- memo mkTodoInput + todoRow <- memo mkTodoRow + todoFilters <- memo mkTodoFilters + + component "TodoApp" \props -> React.do + state /\ dispatch <- useReducer initialState reducer + React.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 } + ] + , style: R.css + { maxWidth: "600px" + , margin: "auto" + , padding: "16px" + , fontFamily: "sans-serif" + , fontSize: "16px" + } + } + + where + todoAppEl = element $ R.unsafeCreateDOMComponent "todo-app" + +mkTodoInput :: CreateComponent { dispatch :: Action -> Effect Unit } (UseState String Unit) +mkTodoInput = do + component "TodoInput" \props -> React.do + value /\ setValue <- useState "" + React.pure $ R.form + { onSubmit: capture_ do + props.dispatch $ CreateTodo value + setValue $ const "" + , children: + [ R.input + { value + , onChange: capture 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%" } + } + +mkTodoRow :: CreateComponent { id :: Int, todo :: Todo, dispatch :: Action -> Effect Unit } Unit +mkTodoRow = component "Todo" \props -> React.do + React.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: capture_ $ props.dispatch $ DeleteTodo props.id + , style: R.css { cursor: "pointer" } + } + ] + , style: R.css + { display: "flex" + , flexFlow: "row" + , alignItems: "center" + } + } + +mkTodoFilters :: CreateComponent { filter :: TodoFilter, dispatch :: Action -> Effect Unit } Unit +mkTodoFilters = component "TodoFilters" \props -> React.do + let + filterLink f label = R.a + { children: [ R.text label ] + , onClick: capture_ $ props.dispatch $ SetFilter f + , style: if props.filter == f + then R.css { cursor: "pointer", fontWeight: "bold" } + else R.css { cursor: "pointer" } + } + React.pure $ R.div + { children: + [ 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/src/React/Basic.js b/src/React/Basic.js index 3cd7f51..3b56355 100644 --- a/src/React/Basic.js +++ b/src/React/Basic.js @@ -3,6 +3,8 @@ var React = require("react"); var Fragment = React.Fragment || "div"; +exports.memo_ = React.memo; + exports.useState_ = function(tuple, initialState) { var r = React.useState(initialState); var state = r[0]; diff --git a/src/React/Basic.purs b/src/React/Basic.purs index b16fd68..3a5438f 100644 --- a/src/React/Basic.purs +++ b/src/React/Basic.purs @@ -3,6 +3,7 @@ module React.Basic , CreateComponent , JSX , component + , memo , UseState , useState , UseEffect @@ -76,6 +77,12 @@ component name renderFn = let c = Component (mkEffectFn1 (\props -> case renderFn props of Render a -> a)) in runEffectFn2 unsafeSetDisplayName name c +memo + :: forall hooks props + . CreateComponent hooks props + -> CreateComponent hooks props +memo = flip Prelude.bind (runEffectFn1 memo_) + foreign import data UseState :: Type -> Type -> Type useState @@ -306,6 +313,12 @@ foreign import displayName -- | Internal utility or FFI functions -- | +foreign import memo_ + :: forall hooks props + . EffectFn1 + (Component props hooks) + (Component props hooks) + foreign import unsafeSetDisplayName :: forall hooks props . EffectFn2 String (Component props hooks) (Component props hooks)