diff --git a/bower.json b/bower.json index 690f7b3..ffb30d8 100644 --- a/bower.json +++ b/bower.json @@ -22,7 +22,8 @@ "purescript-unsafe-coerce": "^3.0.0", "purescript-exceptions": "^3.1.0", "purescript-maybe": "^3.0.0", - "purescript-nullable": "^3.0.0" + "purescript-nullable": "^3.0.0", + "purescript-typelevel-prelude": "^2.5.0" }, "devDependencies": { "purescript-console": "^3.0.0", diff --git a/package.json b/package.json index 8930ed5..fe70c4c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "purescript-react", "files": [], "peerDependencies": { - "react": "^16.0.0", - "create-react-class": "^15.6.0" + "react": "^16.0.0" } } diff --git a/src/React.js b/src/React.js index 84da9c5..b1fec0b 100644 --- a/src/React.js +++ b/src/React.js @@ -2,71 +2,74 @@ "use strict"; var React = require("react"); -var createReactClass = require("create-react-class"); -function getProps(this_) { - return function(){ - return this_.props; - }; -} -exports.getProps = getProps; +function createClass(baseClass) { + function bindProperty(instance, prop, value) { + switch (prop) { + case 'componentDidMount': + case 'componentDidUpdate': + case 'componentWillMount': + case 'componentWillUnmount': + case 'render': + case 'state': + instance[prop] = value; + break; + + case 'componentWillReceiveProps': + instance[prop] = function (a) { return value(a)(); }; + break; + + case 'componentDidCatch': + case 'componentWillUpdate': + case 'shouldComponentUpdate': + instance[prop] = function (a, b) { return value(a)(b)(); }; + break; + + default: + throw new Error('Not a component property: ' + prop); + } + } -function getRefs(this_) { - return function(){ - return this_.refs; + return function (displayName) { + return function (ctrFn) { + var Constructor = function (props) { + baseClass.call(this, props); + var spec = ctrFn(this)(); + for (var k in spec) { + bindProperty(this, k, spec[k]); + } + }; + + Constructor.displayName = displayName; + Constructor.prototype = Object.create(baseClass.prototype); + Constructor.prototype.constructor = Constructor; + + return Constructor; + }; }; } -exports.getRefs = getRefs; -function childrenToArray(children) { - var result = []; +exports.componentImpl = createClass(React.Component); - React.Children.forEach(children, function(child){ - result.push(child); - }); +exports.pureComponentImpl = createClass(React.PureComponent); - return result; -} -exports.childrenToArray = childrenToArray; +exports.statelessComponent = function(x) { return x; }; -function getChildren(this_) { +function getProps(this_) { return function(){ - var children = this_.props.children; - - var result = childrenToArray(children); - - return result; + return this_.props; }; } -exports.getChildren = getChildren; +exports.getProps = getProps; -function readRefImpl (this_) { - return function(name) { - return function() { - return this_[name]; - } - } -} -exports.readRefImpl = readRefImpl; +exports.childrenToArray = React.Children.toArray -function writeRef(this_) { - return function(name) { - return function(node) { - return function() { - this_[name] = node; - return {}; - } - } - } -} -exports.writeRef = writeRef; +exports.childrenCount = React.Children.count; function writeState(this_) { return function(state){ return function(){ - this_.setState({ - state: state - }); + this_.setState(state); return state; }; }; @@ -77,9 +80,7 @@ function writeStateWithCallback(this_, cb) { return function(state){ return function(cb){ return function() { - this_.setState({ - state: state - }, cb); + this_.setState(state, cb); return state; }; }; @@ -89,7 +90,7 @@ exports.writeStateWithCallback = writeStateWithCallback; function readState(this_) { return function(){ - return this_.state.state; + return this_.state; }; } exports.readState = readState; @@ -98,71 +99,13 @@ function transformState(this_){ return function(update){ return function(){ this_.setState(function(old, props){ - return {state: update(old.state)}; + return update(old); }); }; }; } exports.transformState = transformState; -function createClass(toNullable, spec) { - var didCatch = toNullable(spec.componentDidCatch) - - var result = { - displayName: spec.displayName, - render: function(){ - return spec.render(this)(); - }, - getInitialState: function(){ - return { - state: spec.getInitialState(this)() - }; - }, - componentWillMount: function(){ - return spec.componentWillMount(this)(); - }, - componentDidMount: function(){ - return spec.componentDidMount(this)(); - }, - componentDidCatch: didCatch - ? function(error, info) {return didCatch(this)(error)(info)(); } - : undefined, - componentWillReceiveProps: function(nextProps){ - return spec.componentWillReceiveProps(this)(nextProps)(); - }, - shouldComponentUpdate: function(nextProps, nextState){ - return spec.shouldComponentUpdate(this)(nextProps)(nextState.state)(); - }, - componentWillUpdate: function(nextProps, nextState){ - return spec.componentWillUpdate(this)(nextProps)(nextState.state)(); - }, - componentDidUpdate: function(prevProps, prevState){ - return spec.componentDidUpdate(this)(prevProps)(prevState.state)(); - }, - componentWillUnmount: function(){ - return spec.componentWillUnmount(this)(); - } - }; - - return createReactClass(result); -} -exports["createClass'"] = createClass; - -function capitalize(s) { - if (!s) - return s; - return s.charAt(0).toUpperCase() + s.slice(1); -}; - -function createClassStateless(dict) { - return function (f) { - if (!f.displayName) - f.displayName = capitalize(f.name); - return f; - }; -}; -exports.createClassStateless = createClassStateless; - function forceUpdateCbImpl(this_, cb) { this_.forceUpdate(function() { return cb(); @@ -185,9 +128,16 @@ function createElement(class_) { }; }; } -exports.createElement = createElement; +exports.createElementImpl = createElement; exports.createElementTagName = createElement; +function createLeafElement(class_) { + return function(props) { + return React.createElement(class_, props); + }; +} +exports.createLeafElementImpl = createLeafElement; + function createElementDynamic(class_) { return function(props) { return function(children){ @@ -195,14 +145,9 @@ function createElementDynamic(class_) { }; }; }; -exports.createElementDynamic = createElementDynamic; +exports.createElementDynamicImpl = createElementDynamic; exports.createElementTagNameDynamic = createElementDynamic; -function createFactory(class_) { - return React.createFactory(class_); -} -exports.createFactory = createFactory; - function preventDefault(event) { return function() { event.preventDefault(); diff --git a/src/React.purs b/src/React.purs index 1829cae..e13d878 100644 --- a/src/React.purs +++ b/src/React.purs @@ -17,13 +17,8 @@ module React , ReactState , ReactProps - , ReactRefs - - , Refs - , Ref , Render - , GetInitialState , ComponentWillMount , ComponentDidMount , ComponentDidCatch @@ -33,8 +28,8 @@ module React , ComponentDidUpdate , ComponentWillUnmount - , ReactSpec , ReactClass + , Ref , Event , MouseEvent @@ -42,13 +37,11 @@ module React , EventHandlerContext - , spec, spec' + , component + , pureComponent + , statelessComponent , getProps - , getRefs - , readRef - , writeRef - , getChildren , readState , writeState @@ -62,28 +55,25 @@ module React , preventDefault , stopPropagation - , createClass - , createClassStateless - , createClassStateless' , createElement , createElementDynamic , createElementTagName , createElementTagNameDynamic - , createFactory + , createLeafElement , Children , childrenToArray + , childrenCount + + , class ReactPropFields ) where import Prelude import Control.Monad.Eff (kind Effect, Eff) import Control.Monad.Eff.Exception (Error) -import Data.Function.Uncurried (Fn2, runFn2) -import Data.Maybe (Maybe(Nothing)) -import Data.Nullable (Nullable, toMaybe, toNullable) import Control.Monad.Eff.Uncurried (EffFn2, runEffFn2) -import Unsafe.Coerce (unsafeCoerce) +import Data.Nullable (Nullable) -- | Name of a tag. type TagName = String @@ -123,14 +113,6 @@ foreign import data ReactState :: # Effect -> Effect -- | This effect indicates that a computation may read the component props. foreign import data ReactProps :: Effect --- | This effect indicates that a computation may read the component refs. --- | --- | The first type argument is a row of access types (`Read`, `Write`). -foreign import data ReactRefs :: # Effect -> Effect - --- | The type of refs objects. -foreign import data Refs :: Type - -- | The type of DOM events. foreign import data Event :: Type @@ -159,7 +141,6 @@ type KeyboardEvent = type EventHandlerContext eff props state result = Eff ( props :: ReactProps - , refs :: ReactRefs ReadOnly , state :: ReactState ReadWrite | eff ) result @@ -177,196 +158,170 @@ instance intReactRender :: ReactRender Int instance numberReactRender :: ReactRender Number -- | A render function. -type Render props state render eff = - ReactThis props state -> +type Render render eff = Eff ( props :: ReactProps - , refs :: ReactRefs Disallowed , state :: ReactState ReadOnly | eff ) render --- | A get initial state function. -type GetInitialState props state eff = - ReactThis props state -> - Eff - ( props :: ReactProps - , state :: ReactState Disallowed - , refs :: ReactRefs Disallowed - | eff - ) state - -- | A component will mount function. -type ComponentWillMount props state eff = - ReactThis props state -> +type ComponentWillMount eff = Eff ( props :: ReactProps , state :: ReactState ReadWrite - , refs :: ReactRefs Disallowed | eff ) Unit -- | A component did mount function. -type ComponentDidMount props state eff = - ReactThis props state -> +type ComponentDidMount eff = Eff ( props :: ReactProps , state :: ReactState ReadWrite - , refs :: ReactRefs ReadOnly | eff ) Unit -type ComponentDidCatch props state eff = - ReactThis props state -> +type ComponentDidCatch eff = Error -> { componentStack :: String } -> Eff ( props :: ReactProps , state :: ReactState ReadWrite - , refs :: ReactRefs ReadOnly | eff ) Unit - -- | A component will receive props function. -type ComponentWillReceiveProps props state eff = - ReactThis props state -> +type ComponentWillReceiveProps props eff = props -> Eff ( props :: ReactProps , state :: ReactState ReadWrite - , refs :: ReactRefs ReadOnly | eff ) Unit -- | A should component update function. type ShouldComponentUpdate props state eff = - ReactThis props state -> props -> state -> Eff ( props :: ReactProps , state :: ReactState ReadWrite - , refs :: ReactRefs ReadOnly | eff ) Boolean -- | A component will update function. type ComponentWillUpdate props state eff = - ReactThis props state -> props -> state -> Eff ( props :: ReactProps , state :: ReactState ReadWrite - , refs :: ReactRefs ReadOnly | eff ) Unit -- | A component did update function. type ComponentDidUpdate props state eff = - ReactThis props state -> props -> state -> Eff ( props :: ReactProps , state :: ReactState ReadOnly - , refs :: ReactRefs ReadOnly | eff ) Unit -- | A component will unmount function. -type ComponentWillUnmount props state eff = - ReactThis props state -> +type ComponentWillUnmount eff = Eff ( props :: ReactProps , state :: ReactState ReadOnly - , refs :: ReactRefs ReadOnly | eff ) Unit --- | A specification of a component. -type ReactSpec props state render eff = - { render :: Render props state render eff - , displayName :: String - , getInitialState :: GetInitialState props state eff - , componentWillMount :: ComponentWillMount props state eff - , componentDidMount :: ComponentDidMount props state eff - , componentDidCatch :: Maybe (ComponentDidCatch props state eff) - , componentWillReceiveProps :: ComponentWillReceiveProps props state eff - , shouldComponentUpdate :: ShouldComponentUpdate props state eff +-- | Required fields for constructing a ReactClass. +type ReactSpecRequired state render eff r = + ( state :: state + , render :: Render render eff + | r + ) + +-- | Optional fields for constructing a ReactClass. +type ReactSpecOptional props state eff r = + ( componentWillMount :: ComponentWillMount eff + , componentDidMount :: ComponentDidMount eff + , componentDidCatch :: ComponentDidCatch eff + , componentWillReceiveProps :: ComponentWillReceiveProps props eff , componentWillUpdate :: ComponentWillUpdate props state eff , componentDidUpdate :: ComponentDidUpdate props state eff - , componentWillUnmount :: ComponentWillUnmount props state eff - } - --- | Create a component specification with a provided state. -spec :: forall props state render eff. - ReactRender render => - state -> Render props state render eff -> ReactSpec props state render eff -spec state = spec' \_ -> pure state - --- | Create a component specification with a get initial state function. -spec' :: forall props state render eff. - ReactRender render => - GetInitialState props state eff -> - Render props state render eff -> - ReactSpec props state render eff -spec' getInitialState renderFn = - { render: renderFn - , displayName: "" - , getInitialState: getInitialState - , componentWillMount: \_ -> pure unit - , componentDidMount: \_ -> pure unit - , componentDidCatch: Nothing - , componentWillReceiveProps: \_ _ -> pure unit - , shouldComponentUpdate: \_ _ _ -> pure true - , componentWillUpdate: \_ _ _ -> pure unit - , componentDidUpdate: \_ _ _ -> pure unit - , componentWillUnmount: \_ -> pure unit - } - --- | React class for components. -foreign import data ReactClass :: Type -> Type + , componentWillUnmount :: ComponentWillUnmount eff + | r + ) --- | Read the component props. -foreign import getProps :: forall props state eff. - ReactThis props state -> - Eff (props :: ReactProps | eff) props +type ReactSpecShouldComponentUpdate props state eff = + ( shouldComponentUpdate :: ShouldComponentUpdate props state eff + ) --- | Read the component refs. -foreign import getRefs :: forall props state access eff. - ReactThis props state -> - Eff (refs :: ReactRefs (read :: Read | access) | eff) Refs +type ReactSpecAll props state render eff = + ReactSpecRequired state render eff (ReactSpecOptional props state eff (ReactSpecShouldComponentUpdate props state eff)) --- | Ref type. You can store `Ref` types on `Refs` object (which in --- | corresponds to `this.refs`). Use `ReactDOM.refToNode` if you want to --- | store a `DOM.Node.Types.Node` -foreign import data Ref :: Type +type ReactSpecPure props state render eff = + ReactSpecRequired state render eff (ReactSpecOptional props state eff ()) -foreign import readRefImpl :: forall props state access eff. +-- | The signature for a ReactClass constructor. A constructor takes the +-- | `ReactThis` context and returns a record with appropriate lifecycle +-- | methods. +type ReactClassConstructor props state render eff r = ReactThis props state -> + Eff + ( props :: ReactProps + , state :: ReactState Disallowed + | eff + ) (Record (ReactSpecRequired state render eff r)) + +-- | Creates a `ReactClass`` inherited from `React.Component`. +component + :: forall props state render eff r x + . Union (ReactSpecRequired (Record state) render eff r) x (ReactSpecAll (Record props) (Record state) render eff) + => ReactRender render + => String + -> ReactClassConstructor (Record props) (Record state) render eff r + -> ReactClass (Record props) +component = componentImpl + +-- | Creates a `ReactClass`` inherited from `React.PureComponent`. +pureComponent + :: forall props state render eff r x + . Union (ReactSpecRequired (Record state) render eff r) x (ReactSpecPure (Record props) (Record state) render eff) + => ReactRender render + => String + -> ReactClassConstructor (Record props) (Record state) render eff r + -> ReactClass (Record props) +pureComponent = pureComponentImpl + +foreign import componentImpl :: forall this props eff r. String -> - Eff (refs :: ReactRefs (read :: Read | access) | eff) (Nullable Ref) + (this -> Eff eff r) -> + ReactClass props --- | Read named ref from `Refs`. -readRef :: forall props state access eff. - ReactThis props state -> +foreign import pureComponentImpl :: forall this props eff r. String -> - Eff (refs :: ReactRefs (read :: Read | access) | eff) (Maybe Ref) -readRef this name = toMaybe <$> readRefImpl this name + (this -> Eff eff r) -> + ReactClass props --- | Write a `Ref` to `Refs` -foreign import writeRef :: forall props state access eff. - ReactThis props state -> - String -> - Nullable Ref -> - Eff (refs :: ReactRefs (write :: Write | access) | eff) Unit +foreign import statelessComponent :: forall props. + (Record props -> ReactElement) -> + ReactClass (Record props) --- | Read the component children property. -foreign import getChildren :: forall props state eff. +-- | React class for components. +foreign import data ReactClass :: Type -> Type + +-- | Type for React refs. This type is opaque, but you can use `Data.Foreign` +-- | and `DOM` to validate the underlying representation. +foreign import data Ref :: Type + +-- | Read the component props. +foreign import getProps :: forall props state eff. ReactThis props state -> - Eff (props :: ReactProps | eff) (Array ReactElement) + Eff (props :: ReactProps | eff) props -- | Write the component state. foreign import writeState :: forall props state access eff. @@ -375,7 +330,10 @@ foreign import writeState :: forall props state access eff. Eff (state :: ReactState (write :: Write | access) | eff) state -- | Write the component state with a callback. -foreign import writeStateWithCallback :: forall props state access eff. ReactThis props state -> state -> Eff (state :: ReactState (write :: Write | access) | eff) Unit -> Eff (state :: ReactState (write :: Write | access) | eff) state +foreign import writeStateWithCallback :: forall props state access eff. + ReactThis props state -> + state -> + Eff (state :: ReactState (write :: Write | access) | eff) Unit -> Eff (state :: ReactState (write :: Write | access) | eff) state -- | Read the component state. foreign import readState :: forall props state access eff. @@ -388,37 +346,6 @@ foreign import transformState :: forall props state eff. (state -> state) -> Eff (state :: ReactState ReadWrite | eff) Unit --- | Create a React class from a specification. -foreign import createClass' :: forall props state render eff. - Fn2 - (forall a. Maybe a -> Nullable a) - (ReactSpec props state render eff) - (ReactClass props) - -createClass :: forall props state render eff. - ReactSpec props state render eff -> ReactClass props -createClass spc = runFn2 createClass' toNullable spc - --- | Create a stateless React class. When using a non anonymous function the --- | displayName will be the capitalized name of the function, e.g. --- | ``` purescript --- | helloWorld = createClassStatelesss helloWorldCls --- | where --- | helloWorldCls props = ... --- | ``` --- | Then the `displayName` will be set up to `HelloWorldCls` -foreign import createClassStateless :: forall props render. - ReactRender render => - (props -> render) -> ReactClass props - --- | Create a stateless React class with children access. -createClassStateless' :: forall props render. - ReactRender render => - (props -> Array ReactElement -> render) -> ReactClass props -createClassStateless' k = - createClassStateless \props -> - k props (childrenToArray (unsafeCoerce props).children) - -- | Force render of a react component. forceUpdate :: forall eff props state. ReactThis props state -> Eff eff Unit @@ -439,13 +366,54 @@ forceUpdateCb this m = runEffFn2 forceUpdateCbImpl this m foreign import handle :: forall eff ev props state result. (ev -> EventHandlerContext eff props state result) -> EventHandler ev +class ReactPropFields (required :: # Type) (given :: # Type) + +type ReservedReactPropFields r = + ( key :: String + , ref :: EventHandler (Nullable Ref) + | r + ) + +instance reactPropFields :: + ( Union given optional (ReservedReactPropFields required) + , Union optional leftover (ReservedReactPropFields ()) + ) => + ReactPropFields required given + -- | Create an element from a React class spreading the children array. Used when the children are known up front. -foreign import createElement :: forall props. - ReactClass props -> props -> Array ReactElement -> ReactElement +createElement :: forall required given. + ReactPropFields required given => + ReactClass { children :: Children | required } -> + { | given } -> + Array ReactElement -> + ReactElement +createElement = createElementImpl -- | Create an element from a React class passing the children array. Used for a dynamic array of children. -foreign import createElementDynamic :: forall props. - ReactClass props -> props -> Array ReactElement -> ReactElement +createElementDynamic :: forall required given. + ReactPropFields required given => + ReactClass { children :: Children | required } -> + { | given } -> + Array ReactElement -> + ReactElement +createElementDynamic = createElementDynamicImpl + +foreign import createElementImpl :: forall required given children. + ReactClass required -> given -> Array children -> ReactElement + +foreign import createElementDynamicImpl :: forall required given children. + ReactClass required -> given -> Array children -> ReactElement + +-- | Create an element from a React class that does not require children. +createLeafElement :: forall required given. + ReactPropFields required given => + ReactClass { | required } -> + { | given } -> + ReactElement +createLeafElement = createLeafElementImpl + +foreign import createLeafElementImpl :: forall required given. + ReactClass required -> given -> ReactElement -- | Create an element from a tag name spreading the children array. Used when the children are known up front. foreign import createElementTagName :: forall props. @@ -455,16 +423,15 @@ foreign import createElementTagName :: forall props. foreign import createElementTagNameDynamic :: forall props. TagName -> props -> Array ReactElement -> ReactElement --- | Create a factory from a React class. -foreign import createFactory :: forall props. - ReactClass props -> props -> ReactElement - -- | Internal representation for the children elements passed to a component foreign import data Children :: Type -- | Internal conversion function from children elements to an array of React elements foreign import childrenToArray :: Children -> Array ReactElement +-- | Returns the number of children. +foreign import childrenCount :: Children -> Int + foreign import preventDefault :: forall eff. Event -> Eff eff Unit foreign import stopPropagation :: forall eff. Event -> Eff eff Unit diff --git a/src/React/DOM/Props.purs b/src/React/DOM/Props.purs index 4af5349..6c554ee 100644 --- a/src/React/DOM/Props.purs +++ b/src/React/DOM/Props.purs @@ -1,10 +1,7 @@ module React.DOM.Props where -import Control.Monad.Eff (Eff) -import Control.Monad.Eff.Unsafe (unsafePerformEff) import Data.Nullable (Nullable) -import Prelude (Unit, (<<<)) -import React (Event, EventHandlerContext, KeyboardEvent, MouseEvent, ReactRefs, Ref, Write, handle) +import React (Event, EventHandlerContext, KeyboardEvent, MouseEvent, Ref, handle) foreign import data Props :: Type @@ -301,22 +298,6 @@ radioGroup = unsafeMkProps "radioGroup" readOnly :: Boolean -> Props readOnly = unsafeMkProps "readOnly" --- | You can use `ref` to store a reference on `this.refs`. --- | To access the stored reference, `getRefs` can then be used. -ref :: String -> Props -ref = unsafeMkProps "ref" - --- | You can use `writeRef` to store a reference on `this`. --- | ```purescript --- | div [ withRef (writeRef this "inputElement") ] [...] --- | ``` --- | To access the stored reference, `readRef` can then be used. -withRef - :: forall access eff - . (Nullable Ref -> Eff (refs :: ReactRefs (write :: Write | access) | eff) Unit) - -> Props -withRef cb = unsafeMkProps "ref" (unsafePerformEff <<< cb) - rel :: String -> Props rel = unsafeMkProps "rel" @@ -638,6 +619,10 @@ onWheel :: forall eff props state result. (Event -> EventHandlerContext eff props state result) -> Props onWheel f = unsafeMkProps "onWheel" (handle f) +ref :: forall eff props state result. + (Nullable Ref -> EventHandlerContext eff props state result) -> Props +ref f = unsafeMkProps "ref" (handle f) + suppressContentEditableWarning :: Boolean -> Props suppressContentEditableWarning = unsafeMkProps "suppressContentEditableWarning"