From 9ce8fd1db0e181ee5e8c28296a00c2205138d88f Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Thu, 8 Oct 2020 16:52:41 -0700 Subject: [PATCH 1/5] Update for Contributors library guidelines --- .editorconfig | 13 ++ .eslintrc.json | 29 +++ .github/ISSUE_TEMPLATE/bug-report.md | 19 ++ .github/ISSUE_TEMPLATE/change-request.md | 21 ++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/PULL_REQUEST_TEMPLATE.md | 11 + .github/workflows/ci.yml | 51 +++++ .gitignore | 20 +- CHANGELOG.md | 194 ++++++++++++++++ CONTRIBUTING.md | 5 + README.md | 279 +++-------------------- docs/README.md | 254 +++++++++++++++++++++ package.json | 11 +- packages.dhall | 4 + psc-package.json | 10 - spago.dhall | 15 ++ test/Main.purs | 11 + 17 files changed, 681 insertions(+), 274 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintrc.json create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/change-request.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/README.md create mode 100644 packages.dhall delete mode 100644 psc-package.json create mode 100644 spago.dhall create mode 100644 test/Main.purs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7c68b07 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# https://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..17f167d --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "env": { "browser": true, "commonjs": true }, + "extends": "eslint:recommended", + "parserOptions": { "ecmaVersion": 5 }, + "rules": { + "block-scoped-var": "error", + "consistent-return": "error", + "eqeqeq": "error", + "guard-for-in": "error", + "no-bitwise": "error", + "no-caller": "error", + "no-extra-parens": "off", + "no-extend-native": "error", + "no-loop-func": "error", + "no-new": "error", + "no-param-reassign": "error", + "no-return-assign": "error", + "no-sequences": "error", + "no-unused-expressions": "error", + "no-use-before-define": "error", + "no-undef": "error", + "no-eq-null": "error", + "radix": ["error", "always"], + "indent": ["error", 2, { "SwitchCase": 1 }], + "quotes": ["error", "double"], + "semi": ["error", "always"], + "strict": ["error", "global"] + } +} diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..b79b995 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,19 @@ +--- +name: Bug report +about: Report an issue +title: "" +labels: bug +assignees: "" +--- + +**Describe the bug** +A clear and concise description of the bug. + +**To Reproduce** +A minimal code example (preferably a runnable example on [Try PureScript](https://try.purescript.org)!) or steps to reproduce the issue. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/change-request.md b/.github/ISSUE_TEMPLATE/change-request.md new file mode 100644 index 0000000..a2ee685 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/change-request.md @@ -0,0 +1,21 @@ +--- +name: Change request +about: Propose an improvement to this library +title: "" +labels: "" +assignees: "" +--- + +**Is your change request related to a problem? Please describe.** +A clear and concise description of the problem. + +Examples: + +- It's frustrating to have to [...] +- I was looking for a function to [...] + +**Describe the solution you'd like** +A clear and concise description of what a good solution to you looks like, including any solutions you've already considered. + +**Additional context** +Add any other context about the change request here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..c47a263 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: PureScript Discourse + url: https://discourse.purescript.org/ + about: Ask and answer questions here. + - name: Functional Programming Slack + url: https://functionalprogramming.slack.com + about: For casual chat and questions (use https://fpchat-invite.herokuapp.com to join). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d8780f7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +**Description of the change** +Clearly and concisely describe the purpose of the pull request. If this PR relates to an existing issue or change proposal, please link to it. Include any other background context that would help reviewers understand the motivation for this PR. + +--- + +**Checklist:** + +- [ ] Added the change to the changelog's "Unreleased" section with a link to this PR and your username +- [ ] Linked any existing issues or proposals that this pull request should close +- [ ] Updated or added relevant documentation in the README and/or documentation directory +- [ ] Added a test for the contribution (if applicable) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0b3ca67 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up PureScript toolchain + uses: purescript-contrib/setup-purescript@main + + - name: Cache PureScript dependencies + uses: actions/cache@v2 + with: + key: ${{ runner.os }}-spago-${{ hashFiles('**/*.dhall') }} + path: | + .spago + output + + - name: Set up Node toolchain + uses: actions/setup-node@v1 + with: + node-version: "12.x" + + - name: Cache NPM dependencies + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Install NPM dependencies + run: npm install + + - name: Build the project + run: npm run build + + - name: Run tests + run: npm run test diff --git a/.gitignore b/.gitignore index e284bdc..5a54e2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,13 @@ -.psci* -bower_components/ -node_modules/ -output/ -yarn-error.log -.psc-package -.psc-ide-port +.* +!.gitignore +!.github +!.editorconfig +!.eslintrc.json + +output +generated-docs +bower_components + +node_modules +package-lock.json +*.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a923ca4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,194 @@ +# Changelog + +Notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +Breaking changes (😱!!!): + +New features: + +Bugfixes: + +Other improvements: + +## [v8.0.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v8.0.0) - 2019-08-10 + +**Breaking changes** + + - #172 add `createRef` support @elliotdavies + +## [v7.0.1](https://github.com/purescript-contrib/purescript-react/releases/tag/v7.0.1) - 2019-05-27 + +- Relax upper bound on `purescript-typelevel-prelude` (@hdgarrood) + +## [v7.0.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v7.0.0) - 2019-05-17 + +**Breaking Changes** + +- #169 Bump dependency (@bbarker) + +**Documentation** + +- #165 Update documentation (@athanclark) + +## [v6.1.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v6.1.0) - 2018-08-24 + +**Features** + - #155 export react types (@tellnobody1) + +## [v6.0.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v6.0.0) - 2018-06-09 + +**Breaking Changes** +- Alternative class construction #129 (@natefaubion) +- Replace ReactRender with IsReactElement #137 (@natefaubion) +- Event refactoring #144 (@ethul) +- Remove children for void DOM elements #145, #146 (@ethul) +- Updates for PureScript 0.12, including the Context API, and unsafe createElement variants #149 (@natefaubion) + +**Features** +- Add onError #133 (@safareli) + +## [v5.1.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v5.1.0) - 2017-12-16 + +**Features** + + - #130 Adds value array for multiselect (@tellnobody1) + +## [v5.0.1](https://github.com/purescript-contrib/purescript-react/releases/tag/v5.0.1) - 2017-11-21 + +**Fixes** + + - #125 `writeRef` writes directly to `this`. + +## [v5.0.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v5.0.0) - 2017-11-02 + +**Breaking** + +- #109 React 16 (@coot) +- #121 Fix event type functions (@spicydonuts) + +## [v4.4.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v4.4.0) - 2017-10-11 + +**Features** + + - #100 refs (@coot) + +## [v4.3.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v4.3.0) - 2017-09-06 + +**Features** + + - #114 Add common SVG elements (@evenchange4) + +**Documentation** + + - #115 Update maintainers (@paf31) + +## [v4.2.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v4.2.0) - 2017-08-29 + +**Features** + + - #112 Add SVG element `foreignObject` (@paulyoung) + - #113 Add SVG attributes `x` and `y` (@paulyoung) + +**Fixes** + + - #110 Update badge version in README (@coot) + - #111 Correct documentation link (@nwolverson) + +## [v4.1.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v4.1.0) - 2017-08-10 + +**Features** + +- #103 Force update (@coot) +- #107 Export `childrenToArray` (@coot) + +## [v4.0.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v4.0.0) - 2017-06-27 + +Updates for React 15 (@coot) + +## [v3.0.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v3.0.0) - 2017-03-29 + +Updates for 0.11.1 compiler release. + +## [v1.3.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v1.3.0) - 2016-09-17 + +Add simpler props-free versions of SVG functions (@joshuahhh) + +## [v1.2.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v1.2.0) - 2016-09-11 + +Add `preventDefault` and `stopPropagation` (@teh). + +## [v1.1.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v1.1.0) - 2016-06-12 + +Add a variant of `writeState` with a callback (@pkamenarsky) + +## [v1.0.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v1.0.0) - 2016-06-11 + +- Updates for 1.0 core libraries and 0.9.1 compiler. This library will not work with compiler versions < 0.9.1. (@spicydonuts) + +## [v0.7.1](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.7.1) - 2016-03-14 + +**Bug Fixes** + +#70 - Fixes `aria` and `data` props + +## [v0.7.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.7.0) - 2016-02-29 + +**Features** + +#62 - Fix transform state (@spencerjanssen) +#63 - Stateless components with children (@ethul) + +## [v0.6.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.6.0) - 2016-01-25 + +**Features** + +#54 - Dynamic children support +#56 - Bindings for React 0.14 + +**Breaking Changes** + +The `react` library is now explicitly required in the FFI code. `purescript-react` no longer assumes that `React` is globally available. Also, with the support for 0.14 bindings of React, the DOM bindings have been split out into a separate repository [purescript-react-dom](https://github.com/purescript-contrib/purescript-react-dom). + +## [v0.5.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.5.0) - 2015-11-19 + +- Simplify effect types for read/write access. + +## [v0.4.3](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.4.3) - 2015-10-18 + +Add `GetInitialState` argument in `spec'` (@ethul) + +## [v0.4.2](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.4.2) - 2015-10-01 + +Fix inline styling error (@robkuz) + +## [v0.4.1](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.4.1) - 2015-09-24 + +Fix a bug in `getChildren` (@ethul) + +## [v0.4.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.4.0) - 2015-09-04 + +Add state and props to `ReactThis` (@ethul) + +## [v0.3.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.3.0) - 2015-09-01 + +Support React 0.13.\* (@ethul) + +## [v0.2.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.2.0) - 2015-08-31 + +Add additional arguments to lifecycle methods (@ethul) + +## [v0.1.2](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.1.2) - 2015-08-12 + +Support `displayName` (@ethul) + +## [v0.1.1](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.1.1) - 2015-07-29 + +Fix `bower.json` file. + +## [v0.1.0](https://github.com/purescript-contrib/purescript-react/releases/tag/v0.1.0) - 2015-07-02 + +This release works with versions 0.7.\* of the PureScript compiler. It will not work with older versions. If you are using an older version, you should require an older, compatible version of this library. +- Break up `React.DOM` module +- Make `this` reference explicit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dc4f0ca --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +# Contributing to React + +Thanks for your interest in contributing to `react`! We welcome new contributions regardless of your level of experience or familiarity with PureScript. + +Every library in the Contributors organization shares a simple handbook that helps new contributors get started. With that in mind, please [read the short contributing guide on purescript-contrib/governance](https://github.com/purescript-contrib/governance/blob/main/contributing.md) before contributing to this library. diff --git a/README.md b/README.md index b15c233..b51ecec 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ -# purescript-react +# React -[![Maintainer: ethul](https://img.shields.io/badge/maintainer-ethul-lightgrey.svg)](http://github.com/ethul) -![React: 16](https://img.shields.io/badge/react-16-lightgrey.svg) +[![CI](https://github.com/purescript-contrib/purescript-react/workflows/CI/badge.svg?branch=main)](https://github.com/purescript-contrib/purescript-react/actions?query=workflow%3ACI+branch%3Amain) +[![Release](https://img.shields.io/github/release/purescript-contrib/purescript-react.svg)](https://github.com/purescript-contrib/purescript-react/releases) +[![Pursuit](https://pursuit.purescript.org/packages/purescript-react/badge)](https://pursuit.purescript.org/packages/purescript-react) +[![Maintainer: ethul](https://img.shields.io/badge/maintainer-ethul-teal.svg)](https://github.com/ethul) -Low-level React bindings for PureScript. For a more high-level set of bindings, you might like to look at [`purescript-react-basic`](https://github.com/lumihq/purescript-react-basic). +Low-level React bindings for PureScript. For a higher-level library for working with React, please see [`react-basic`](https://github.com/lumihq/purescript-react-basic) repository (which includes a package for working with React Hooks). -- [Module Documentation](https://pursuit.purescript.org/packages/purescript-react/) +You may also be interested in the low-level [React DOM bindings](https://github.com/purescript-contrib/purescript-react-dom). + +## Installation + +Install `react` with [Spago](https://github.com/purescript/spago): ```sh spago install react @@ -17,262 +23,29 @@ This library requires the `react` module. This dependency may be satisfied by in npm install react ``` -## Related Modules - -- [React DOM](https://github.com/purescript-contrib/purescript-react-dom) - -## Example - -Please refer to [purescript-react-example](https://github.com/ethul/purescript-react-example) - -## Troubleshooting - -#### How to use JavaScript components? - -To use a React component that is published as a JavaScript module, one -can leverage PureScript's FFI to define a type for the component and its -props. Consider the following example. - -```purescript -module Clock (clockComponent) where - -import React (ReactClass, SyntheticEventHandler, Children) -import React.SyntheticEvent (SyntheticEvent) - -foreign import clockComponent - :: ReactClass - { children :: Children - , format :: String - , className :: String - , onTick :: SyntheticEventHandler SyntheticEvent - } -``` - -Rendering the `clockComponent` can be done as follows. - -```purescript -module Component where - -import Prelude - -import Effect.Uncurried (mkEffectFn1) - -import React as React -import React.SyntheticEvent as Event - -import Clock as Clock - -clock :: React.ReactElement -clock = - React.createElement Clock.clockComponent - { format: "HH:mm:ss" - , className: "test-class-name" - , onTick: mkEffectFn1 $ \event -> do - Event.preventDefault event - -- etc. - pure unit - } [] -``` - -A consideration when defining a type for an external component is that -some components pass their props through to a DOM element. In a case -such as this, it can be helpful to leverage the props defined in the -`React.DOM.Props` module. One way to accomplish this is to define the -external component as follows. +## Quick start -```purescript -module Clock - ( clockComponent - , format - , onTick - ) where +The quick start hasn't been written yet (contributions are welcome!). The quick start covers a common, minimal use case for the library, whereas longer examples and tutorials are kept in the [docs directory](./docs). -import Prelude +## Documentation -import Effect (Effect) -import Effect.Uncurried (mkEffectFn1) +`react` documentation is stored in a few places: -import React (ReactClass, ReactElement, Children, createElement) -import React.SyntheticEvent (SyntheticEvent) -import React.DOM.Props (Props, unsafeFromPropsArray, unsafeMkProps) +1. Module documentation is [published on Pursuit](https://pursuit.purescript.org/packages/purescript-react). +2. Written documentation is kept in the [docs directory](./docs). +3. The [react-example](https://github.com/ethul/purescript-react-example) repository demonstrates these bindings in action. -clockComponent :: Array Props -> Array ReactElement -> ReactElement -clockComponent props children = createElement clockComponent_ (unsafeFromPropsArray props :: {}) children +If you get stuck, there are several ways to get help: -format :: String -> Props -format = unsafeMkProps "format" +- [Open an issue](https://github.com/purescript-contrib/purescript-react/issues) if you have encountered a bug or problem. +- [Search or start a thread on the PureScript Discourse](https://discourse.purescript.org) if you have general questions. You can also ask questions in the `#purescript` and `#purescript-beginners` channels on the [Functional Programming Slack](https://functionalprogramming.slack.com) ([invite link](https://fpchat-invite.herokuapp.com/)). -onTick :: (SyntheticEvent -> Effect Unit) -> Props -onTick k = unsafeMkProps "onTick" (mkEffectFn1 k) - -foreign import clockComponent_ - :: ReactClass - { children :: Children - } -``` - -Rendering the `clockComponent` can be done as follows. - -```purescript -module Component where - -import Prelude - -import React as React -import React.SyntheticEvent as Event -import React.DOM.Props as Props - -import Clock as Clock - -clock :: React.ReactElement -clock = - Clock.clockComponent - [ Clock.format "HH:mm:ss" - , Clock.onTick $ \event -> do - Event.preventDefault event - -- etc. - pure unit - , Props.className "test-class-name" - , Props.style - { fontWeight: "bold" - , color: "blue" - } - -- additional Props.* - ] [] -``` +## Contributing -#### Components with type class constraints re-mount on every render? +You can contribute to `react` in several ways: -Consider the following example where an ordered list component is -defined for any item of type `a`, where `a` is constrained to have an -`Ord` type class instance. +1. If you encounter a problem or have a question, please [open an issue](https://github.com/purescript-contrib/purescript-react/issues). We'll do our best to work with you to resolve or answer it. -```purescript -module OrderedList where +2. If you would like to contribute code, tests, or documentation, please [read the contributor guide](./CONTRIBUTING.md). It's a short, helpful introduction to contributing to this library, including development instructions. -import Prelude - -import Data.Array (sort) - -import React as React -import React.DOM as DOM -import Debug.Trace as Trace - -type OrderedListProps a - = { items :: Array a - , renderItem :: a -> React.ReactElement - } - -orderedList :: forall a. Ord a => React.ReactClass (OrderedListProps a) -orderedList = React.component "OrderedList" component - where - component this = - pure { state: {} - , componentDidMount: do - _ <- pure $ Trace.spy "OrderedList.componentDidMount" - pure unit - , render: render <$> React.getProps this - } - where - render - { items - , renderItem - } = - DOM.ol [ ] $ - renderItem' <$> sort items - where - renderItem' a = - DOM.li - [ ] - [ renderItem a ] - --- This would be defined where the type parameter `a` is known. - -orderedListInt :: React.ReactClass (OrderedListProps Int) -orderedListInt = orderedList -``` - -If the component `orderedList` above were to be rendered, the debugging -statement `OrderedList.componentDidMount` is printed to the console each -time the parent component is rendered. The reason for this is due to how -the `orderedList` component is compiled to JavaScript. - -```javascript -var orderedList = function (dictOrd) { - var component = function ($$this) { - // ... - }; - return React.component(React.reactComponentSpec()())("OrderedList")(component); -}; -``` - -Above, the component creation is wrapped by the function with the -`dictOrd` parameter. This means that a new component is being created on -each render of the component using `orderedList`. This may not be ideal -in all cases; e.g., if `orderedList` had needed to store state. - -To avoid `orderedList` from being recreated each time, a function can be -defined that specifies the type parameter with the type class contraint. -If the component using the ordered list knows that the items are of type -`Int`, the component can define `orderedListInt` as shown above, and use -it to render the ordered list instead of `orderedList`. - - -#### Understanding `Children` - - -In React, we see the `children` prop type from time to time, especially -when using `createElement`. This is an opaque data type, in which we can -coerce into an `Array`, but we cannot create. Usually, when you see a -`ReactClass` that features a `children :: Children` prop type, this -means that the component itself expects children to be supplied as an -argument to `createElement`, in the form of an `Array ReactElement`. - -However, in some circumstances (like a `ContextConsumer`), the `children` -prop type might look different, like `children :: a -> ReactElement`. -In this case, it would be better to use `createLeafElement`, to supply -the children _directly through the props_, rather than as a separate -argument. - -This also means that any leaf-like components should _not_ define a -`children :: Children` prop - this prop should be treated as the -_expectation_ of a children argument. In the clock example above, a -more proper specification might look like the following: - -```purescript -module Clock (clockComponent) where - -import React (ReactClass, SyntheticEventHandler) -import React.SyntheticEvent (SyntheticEvent) - -foreign import clockComponent - :: ReactClass - { format :: String - , className :: String - , onTick :: SyntheticEventHandler SyntheticEvent - } -``` - -```purescript -module Component where - -import Prelude - -import Effect.Uncurried (mkEffectFn1) - -import React as React -import React.SyntheticEvent as Event - -import Clock as Clock - -clock :: React.ReactElement -clock = - React.createLeafElement Clock.clockComponent - { format: "HH:mm:ss" - , className: "test-class-name" - , onTick: mkEffectFn1 $ \event -> do - Event.preventDefault event - -- etc. - pure unit - } -``` +3. If you have written a library, tutorial, guide, or other resource based on this package, please share it on the [PureScript Discourse](https://discourse.purescript.org)! Writing libraries and learning resources are a great way to help this library succeed. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..514d812 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,254 @@ +# React Documentation + +This directory contains documentation for `react`. If you are interested in contributing new documentation, please read the [contributor guidelines](../CONTRIBUTING.md) and [What Nobody Tells You About Documentation](https://documentation.divio.com) for help getting started. + +## Frequently Asked Questions + +### How to use JavaScript components? + +To use a React component that is published as a JavaScript module, one +can leverage PureScript's FFI to define a type for the component and its +props. Consider the following example. + +```purescript +module Clock (clockComponent) where + +import React (ReactClass, SyntheticEventHandler, Children) +import React.SyntheticEvent (SyntheticEvent) + +foreign import clockComponent + :: ReactClass + { children :: Children + , format :: String + , className :: String + , onTick :: SyntheticEventHandler SyntheticEvent + } +``` + +Rendering the `clockComponent` can be done as follows. + +```purescript +module Component where + +import Prelude + +import Effect.Uncurried (mkEffectFn1) + +import React as React +import React.SyntheticEvent as Event + +import Clock as Clock + +clock :: React.ReactElement +clock = + React.createElement Clock.clockComponent + { format: "HH:mm:ss" + , className: "test-class-name" + , onTick: mkEffectFn1 $ \event -> do + Event.preventDefault event + -- etc. + pure unit + } [] +``` + +A consideration when defining a type for an external component is that +some components pass their props through to a DOM element. In a case +such as this, it can be helpful to leverage the props defined in the +`React.DOM.Props` module. One way to accomplish this is to define the +external component as follows. + +```purescript +module Clock + ( clockComponent + , format + , onTick + ) where + +import Prelude + +import Effect (Effect) +import Effect.Uncurried (mkEffectFn1) + +import React (ReactClass, ReactElement, Children, createElement) +import React.SyntheticEvent (SyntheticEvent) +import React.DOM.Props (Props, unsafeFromPropsArray, unsafeMkProps) + +clockComponent :: Array Props -> Array ReactElement -> ReactElement +clockComponent props children = createElement clockComponent_ (unsafeFromPropsArray props :: {}) children + +format :: String -> Props +format = unsafeMkProps "format" + +onTick :: (SyntheticEvent -> Effect Unit) -> Props +onTick k = unsafeMkProps "onTick" (mkEffectFn1 k) + +foreign import clockComponent_ + :: ReactClass + { children :: Children + } +``` + +Rendering the `clockComponent` can be done as follows. + +```purescript +module Component where + +import Prelude + +import React as React +import React.SyntheticEvent as Event +import React.DOM.Props as Props + +import Clock as Clock + +clock :: React.ReactElement +clock = + Clock.clockComponent + [ Clock.format "HH:mm:ss" + , Clock.onTick $ \event -> do + Event.preventDefault event + -- etc. + pure unit + , Props.className "test-class-name" + , Props.style + { fontWeight: "bold" + , color: "blue" + } + -- additional Props.* + ] [] +``` + +### Components with type class constraints re-mount on every render? + +Consider the following example where an ordered list component is +defined for any item of type `a`, where `a` is constrained to have an +`Ord` type class instance. + +```purescript +module OrderedList where + +import Prelude + +import Data.Array (sort) + +import React as React +import React.DOM as DOM +import Debug.Trace as Trace + +type OrderedListProps a + = { items :: Array a + , renderItem :: a -> React.ReactElement + } + +orderedList :: forall a. Ord a => React.ReactClass (OrderedListProps a) +orderedList = React.component "OrderedList" component + where + component this = + pure { state: {} + , componentDidMount: do + _ <- pure $ Trace.spy "OrderedList.componentDidMount" + pure unit + , render: render <$> React.getProps this + } + where + render + { items + , renderItem + } = + DOM.ol [ ] $ + renderItem' <$> sort items + where + renderItem' a = + DOM.li + [ ] + [ renderItem a ] + +-- This would be defined where the type parameter `a` is known. + +orderedListInt :: React.ReactClass (OrderedListProps Int) +orderedListInt = orderedList +``` + +If the component `orderedList` above were to be rendered, the debugging +statement `OrderedList.componentDidMount` is printed to the console each +time the parent component is rendered. The reason for this is due to how +the `orderedList` component is compiled to JavaScript. + +```javascript +var orderedList = function (dictOrd) { + var component = function ($$this) { + // ... + }; + return React.component(React.reactComponentSpec()())("OrderedList")(component); +}; +``` + +Above, the component creation is wrapped by the function with the +`dictOrd` parameter. This means that a new component is being created on +each render of the component using `orderedList`. This may not be ideal +in all cases; e.g., if `orderedList` had needed to store state. + +To avoid `orderedList` from being recreated each time, a function can be +defined that specifies the type parameter with the type class contraint. +If the component using the ordered list knows that the items are of type +`Int`, the component can define `orderedListInt` as shown above, and use +it to render the ordered list instead of `orderedList`. + + +### Understanding `Children` + +In React, we see the `children` prop type from time to time, especially +when using `createElement`. This is an opaque data type, in which we can +coerce into an `Array`, but we cannot create. Usually, when you see a +`ReactClass` that features a `children :: Children` prop type, this +means that the component itself expects children to be supplied as an +argument to `createElement`, in the form of an `Array ReactElement`. + +However, in some circumstances (like a `ContextConsumer`), the `children` +prop type might look different, like `children :: a -> ReactElement`. +In this case, it would be better to use `createLeafElement`, to supply +the children _directly through the props_, rather than as a separate +argument. + +This also means that any leaf-like components should _not_ define a +`children :: Children` prop - this prop should be treated as the +_expectation_ of a children argument. In the clock example above, a +more proper specification might look like the following: + +```purescript +module Clock (clockComponent) where + +import React (ReactClass, SyntheticEventHandler) +import React.SyntheticEvent (SyntheticEvent) + +foreign import clockComponent + :: ReactClass + { format :: String + , className :: String + , onTick :: SyntheticEventHandler SyntheticEvent + } +``` + +```purescript +module Component where + +import Prelude + +import Effect.Uncurried (mkEffectFn1) + +import React as React +import React.SyntheticEvent as Event + +import Clock as Clock + +clock :: React.ReactElement +clock = + React.createLeafElement Clock.clockComponent + { format: "HH:mm:ss" + , className: "test-class-name" + , onTick: mkEffectFn1 $ \event -> do + Event.preventDefault event + -- etc. + pure unit + } +``` \ No newline at end of file diff --git a/package.json b/package.json index cd9473b..ad3ecca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,10 @@ { - "name": "purescript-react", - "files": [], - "peerDependencies": { - "react": "^16.3.0" + "private": true, + "scripts": { + "build": "eslint src && spago build --purs-args '--censor-lib --strict'", + "test": "spago test --no-install" + }, + "devDependencies": { + "eslint": "^7.6.0" } } diff --git a/packages.dhall b/packages.dhall new file mode 100644 index 0000000..80f5fe6 --- /dev/null +++ b/packages.dhall @@ -0,0 +1,4 @@ +let upstream = + https://github.com/purescript/package-sets/releases/download/psc-0.13.8-20201007/packages.dhall sha256:35633f6f591b94d216392c9e0500207bb1fec42dd355f4fecdfd186956567b6b + +in upstream diff --git a/psc-package.json b/psc-package.json deleted file mode 100644 index fc9f3a6..0000000 --- a/psc-package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "react", - "source": "https://github.com/purescript/package-sets.git", - "set": "psc-0.10.1", - "depends": [ - "eff", - "prelude", - "unsafe-coerce" - ] -} diff --git a/spago.dhall b/spago.dhall new file mode 100644 index 0000000..4e5b7a6 --- /dev/null +++ b/spago.dhall @@ -0,0 +1,15 @@ +{ name = "react" +, dependencies = + [ "console" + , "effect" + , "exceptions" + , "maybe" + , "nullable" + , "prelude" + , "psci-support" + , "typelevel-prelude" + , "unsafe-coerce" + ] +, packages = ./packages.dhall +, sources = [ "src/**/*.purs", "test/**/*.purs" ] +} diff --git a/test/Main.purs b/test/Main.purs new file mode 100644 index 0000000..f91f98c --- /dev/null +++ b/test/Main.purs @@ -0,0 +1,11 @@ +module Test.Main where + +import Prelude + +import Effect (Effect) +import Effect.Class.Console (log) + +main :: Effect Unit +main = do + log "🍝" + log "You should add some tests." From 41619266f5675a90ccb0f757c10a5cda2329f6a3 Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Thu, 8 Oct 2020 17:01:20 -0700 Subject: [PATCH 2/5] Updates for eslint --- package.json | 3 ++- src/React.js | 54 +++++++++++++++++++------------------ src/React/DOM/Props.js | 11 +++----- src/React/Ref.js | 4 +-- src/React/SyntheticEvent.js | 2 +- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index ad3ecca..d34c856 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "test": "spago test --no-install" }, "devDependencies": { - "eslint": "^7.6.0" + "eslint": "^7.6.0", + "purescript-psa": "^0.8.0" } } diff --git a/src/React.js b/src/React.js index 91d4c45..7cc5a0e 100644 --- a/src/React.js +++ b/src/React.js @@ -1,4 +1,3 @@ -/* global exports */ "use strict"; var React = require("react"); @@ -6,38 +5,38 @@ var React = require("react"); function createClass(baseClass) { function bindProperty(instance, prop, value) { switch (prop) { - case 'state': - case 'render': - case 'componentDidMount': - case 'componentWillUnmount': + case "state": + case "render": + case "componentDidMount": + case "componentWillUnmount": instance[prop] = value; break; - case 'componentDidCatch': - case 'componentWillUpdate': - case 'shouldComponentUpdate': - case 'getSnapshotBeforeUpdate': + case "componentDidCatch": + case "componentWillUpdate": + case "shouldComponentUpdate": + case "getSnapshotBeforeUpdate": instance[prop] = function (a, b) { return value(a)(b)(); }; break; - case 'componentDidUpdate': + case "componentDidUpdate": instance[prop] = function (a, b, c) { return value(a)(b)(c)(); }; break; - case 'unsafeComponentWillMount': - instance['UNSAFE_componentWillMount'] = value; + case "unsafeComponentWillMount": + instance["UNSAFE_componentWillMount"] = value; break; - case 'unsafeComponentWillReceiveProps': - instance['UNSAFE_componentWillReceiveProps'] = function (a) { return value(a)(); }; + case "unsafeComponentWillReceiveProps": + instance["UNSAFE_componentWillReceiveProps"] = function (a) { return value(a)(); }; break; - case 'unsafeComponentWillUpdate': - instance['UNSAFE_componentWillUpdate'] = function (a, b) { return value(a)(b)(); }; + case "unsafeComponentWillUpdate": + instance["UNSAFE_componentWillUpdate"] = function (a, b) { return value(a)(b)(); }; break; default: - throw new Error('[purescript-react] Not a component property: ' + prop); + throw new Error("[purescript-react] Not a component property: " + prop); } } @@ -47,7 +46,9 @@ function createClass(baseClass) { baseClass.call(this, props); var spec = ctrFn(this)(); for (var k in spec) { - bindProperty(this, k, spec[k]); + if (Object.prototype.hasOwnProperty.call(spec, k)) { + bindProperty(this, k, spec[k]); + } } }; @@ -60,7 +61,13 @@ function createClass(baseClass) { }; } -function createClassWithDerivedState(classCtr) { +var componentImpl = createClass(React.Component); +exports.componentImpl = componentImpl; + +var pureComponentImpl = createClass(React.PureComponent); +exports.pureComponentImpl = pureComponentImpl; + +function createClassWithDerivedState() { return function(displayName) { return function(getDerivedStateFromProps) { return function(ctrFn) { @@ -72,12 +79,7 @@ function createClassWithDerivedState(classCtr) { }; } -var componentImpl = createClass(React.Component); -exports.componentImpl = componentImpl; exports.componentWithDerivedStateImpl = createClassWithDerivedState(componentImpl); - -var pureComponentImpl = createClass(React.PureComponent); -exports.pureComponentImpl = pureComponentImpl; exports.pureComponentWithDerivedStateImpl = createClassWithDerivedState(pureComponentImpl); exports.statelessComponent = function(x) { return x; }; @@ -118,7 +120,7 @@ exports.setStateWithCallbackImpl = setStateWithCallbackImpl; function getState(this_) { return function(){ if (!this_.state) { - throw new Error('[purescript-react] Cannot get state within constructor'); + throw new Error("[purescript-react] Cannot get state within constructor"); } return this_.state; }; @@ -157,7 +159,7 @@ function createElementDynamic(class_) { return React.createElement(class_, props, children); }; }; -}; +} exports.createElementDynamicImpl = createElementDynamic; exports.createElementTagNameDynamic = createElementDynamic; diff --git a/src/React/DOM/Props.js b/src/React/DOM/Props.js index 68e4a39..0657b69 100644 --- a/src/React/DOM/Props.js +++ b/src/React/DOM/Props.js @@ -1,8 +1,5 @@ -/* global exports */ "use strict"; -var React = require("react"); - function unsafeMkProps(key) { return function(value){ var result = {}; @@ -19,7 +16,7 @@ function unsafeUnfoldProps(key) { props[key] = result; for (var subprop in value) { - if (value.hasOwnProperty(subprop)) { + if (Object.prototype.hasOwnProperty.call(value, subprop)) { result[subprop] = value[subprop]; } } @@ -34,7 +31,7 @@ function unsafePrefixProps(prefix) { var result = {}; for (var prop in value) { - if (value.hasOwnProperty(prop)) { + if (Object.prototype.hasOwnProperty.call(value, prop)) { result[prefix + prop] = value[prop]; } } @@ -51,12 +48,12 @@ function unsafeFromPropsArray(props) { var prop = props[i]; for (var key in prop) { - if (prop.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(prop, key)) { result[key] = prop[key]; } } } return result; -}; +} exports.unsafeFromPropsArray = unsafeFromPropsArray; diff --git a/src/React/Ref.js b/src/React/Ref.js index a08fe02..a0ec3ee 100644 --- a/src/React/Ref.js +++ b/src/React/Ref.js @@ -6,8 +6,8 @@ exports.createRef = React.createRef; exports.liftCallbackRef = function(ref) { return { current: ref }; -} +}; exports.getCurrentRef_ = function(ref) { return ref.current; -} +}; diff --git a/src/React/SyntheticEvent.js b/src/React/SyntheticEvent.js index 6d39ee0..d52011e 100644 --- a/src/React/SyntheticEvent.js +++ b/src/React/SyntheticEvent.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; exports.preventDefault = function preventDefault(event) { return function() { From 0727ea512a6c35a91b253691b5164991b4f7fc25 Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Thu, 8 Oct 2020 19:08:38 -0700 Subject: [PATCH 3/5] Revert "Updates for eslint" This reverts commit 41619266f5675a90ccb0f757c10a5cda2329f6a3. --- package.json | 3 +-- src/React.js | 54 ++++++++++++++++++------------------- src/React/DOM/Props.js | 11 +++++--- src/React/Ref.js | 4 +-- src/React/SyntheticEvent.js | 2 +- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index d34c856..ad3ecca 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "test": "spago test --no-install" }, "devDependencies": { - "eslint": "^7.6.0", - "purescript-psa": "^0.8.0" + "eslint": "^7.6.0" } } diff --git a/src/React.js b/src/React.js index 7cc5a0e..91d4c45 100644 --- a/src/React.js +++ b/src/React.js @@ -1,3 +1,4 @@ +/* global exports */ "use strict"; var React = require("react"); @@ -5,38 +6,38 @@ var React = require("react"); function createClass(baseClass) { function bindProperty(instance, prop, value) { switch (prop) { - case "state": - case "render": - case "componentDidMount": - case "componentWillUnmount": + case 'state': + case 'render': + case 'componentDidMount': + case 'componentWillUnmount': instance[prop] = value; break; - case "componentDidCatch": - case "componentWillUpdate": - case "shouldComponentUpdate": - case "getSnapshotBeforeUpdate": + case 'componentDidCatch': + case 'componentWillUpdate': + case 'shouldComponentUpdate': + case 'getSnapshotBeforeUpdate': instance[prop] = function (a, b) { return value(a)(b)(); }; break; - case "componentDidUpdate": + case 'componentDidUpdate': instance[prop] = function (a, b, c) { return value(a)(b)(c)(); }; break; - case "unsafeComponentWillMount": - instance["UNSAFE_componentWillMount"] = value; + case 'unsafeComponentWillMount': + instance['UNSAFE_componentWillMount'] = value; break; - case "unsafeComponentWillReceiveProps": - instance["UNSAFE_componentWillReceiveProps"] = function (a) { return value(a)(); }; + case 'unsafeComponentWillReceiveProps': + instance['UNSAFE_componentWillReceiveProps'] = function (a) { return value(a)(); }; break; - case "unsafeComponentWillUpdate": - instance["UNSAFE_componentWillUpdate"] = function (a, b) { return value(a)(b)(); }; + case 'unsafeComponentWillUpdate': + instance['UNSAFE_componentWillUpdate'] = function (a, b) { return value(a)(b)(); }; break; default: - throw new Error("[purescript-react] Not a component property: " + prop); + throw new Error('[purescript-react] Not a component property: ' + prop); } } @@ -46,9 +47,7 @@ function createClass(baseClass) { baseClass.call(this, props); var spec = ctrFn(this)(); for (var k in spec) { - if (Object.prototype.hasOwnProperty.call(spec, k)) { - bindProperty(this, k, spec[k]); - } + bindProperty(this, k, spec[k]); } }; @@ -61,13 +60,7 @@ function createClass(baseClass) { }; } -var componentImpl = createClass(React.Component); -exports.componentImpl = componentImpl; - -var pureComponentImpl = createClass(React.PureComponent); -exports.pureComponentImpl = pureComponentImpl; - -function createClassWithDerivedState() { +function createClassWithDerivedState(classCtr) { return function(displayName) { return function(getDerivedStateFromProps) { return function(ctrFn) { @@ -79,7 +72,12 @@ function createClassWithDerivedState() { }; } +var componentImpl = createClass(React.Component); +exports.componentImpl = componentImpl; exports.componentWithDerivedStateImpl = createClassWithDerivedState(componentImpl); + +var pureComponentImpl = createClass(React.PureComponent); +exports.pureComponentImpl = pureComponentImpl; exports.pureComponentWithDerivedStateImpl = createClassWithDerivedState(pureComponentImpl); exports.statelessComponent = function(x) { return x; }; @@ -120,7 +118,7 @@ exports.setStateWithCallbackImpl = setStateWithCallbackImpl; function getState(this_) { return function(){ if (!this_.state) { - throw new Error("[purescript-react] Cannot get state within constructor"); + throw new Error('[purescript-react] Cannot get state within constructor'); } return this_.state; }; @@ -159,7 +157,7 @@ function createElementDynamic(class_) { return React.createElement(class_, props, children); }; }; -} +}; exports.createElementDynamicImpl = createElementDynamic; exports.createElementTagNameDynamic = createElementDynamic; diff --git a/src/React/DOM/Props.js b/src/React/DOM/Props.js index 0657b69..68e4a39 100644 --- a/src/React/DOM/Props.js +++ b/src/React/DOM/Props.js @@ -1,5 +1,8 @@ +/* global exports */ "use strict"; +var React = require("react"); + function unsafeMkProps(key) { return function(value){ var result = {}; @@ -16,7 +19,7 @@ function unsafeUnfoldProps(key) { props[key] = result; for (var subprop in value) { - if (Object.prototype.hasOwnProperty.call(value, subprop)) { + if (value.hasOwnProperty(subprop)) { result[subprop] = value[subprop]; } } @@ -31,7 +34,7 @@ function unsafePrefixProps(prefix) { var result = {}; for (var prop in value) { - if (Object.prototype.hasOwnProperty.call(value, prop)) { + if (value.hasOwnProperty(prop)) { result[prefix + prop] = value[prop]; } } @@ -48,12 +51,12 @@ function unsafeFromPropsArray(props) { var prop = props[i]; for (var key in prop) { - if (Object.prototype.hasOwnProperty.call(prop, key)) { + if (prop.hasOwnProperty(key)) { result[key] = prop[key]; } } } return result; -} +}; exports.unsafeFromPropsArray = unsafeFromPropsArray; diff --git a/src/React/Ref.js b/src/React/Ref.js index a0ec3ee..a08fe02 100644 --- a/src/React/Ref.js +++ b/src/React/Ref.js @@ -6,8 +6,8 @@ exports.createRef = React.createRef; exports.liftCallbackRef = function(ref) { return { current: ref }; -}; +} exports.getCurrentRef_ = function(ref) { return ref.current; -}; +} diff --git a/src/React/SyntheticEvent.js b/src/React/SyntheticEvent.js index d52011e..6d39ee0 100644 --- a/src/React/SyntheticEvent.js +++ b/src/React/SyntheticEvent.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; exports.preventDefault = function preventDefault(event) { return function() { From 4772e2802bd3042fb5da4cbb135935cedb2858f2 Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Thu, 8 Oct 2020 19:09:24 -0700 Subject: [PATCH 4/5] Update package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ad3ecca..ca79d85 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "test": "spago test --no-install" }, "devDependencies": { - "eslint": "^7.6.0" + "eslint": "^7.10.0", + "purescript-psa": "^0.8.0" } } From 6ed7babe1f527bb00f2555ff2ac00b2a4e27505f Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Thu, 8 Oct 2020 19:11:26 -0700 Subject: [PATCH 5/5] Disable eslint --- .eslintrc.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 17f167d..7f8e075 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,5 @@ { - "env": { "browser": true, "commonjs": true }, + "env": { "commonjs": true }, "extends": "eslint:recommended", "parserOptions": { "ecmaVersion": 5 }, "rules": { diff --git a/package.json b/package.json index ca79d85..acf39bb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "scripts": { - "build": "eslint src && spago build --purs-args '--censor-lib --strict'", + "build": "spago build --purs-args '--censor-lib --strict'", "test": "spago test --no-install" }, "devDependencies": {