Repository: RoyalIcing/react-organism Branch: master Commit: 4879bc0a0066 Files: 42 Total size: 76.9 KB Directory structure: gitextract_z5_js_hy/ ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── demo/ │ └── src/ │ ├── components/ │ │ ├── Calculator.js │ │ ├── Counter.js │ │ ├── FriendsList.js │ │ ├── Items.js │ │ ├── Notifications.js │ │ ├── PhotosList.js │ │ └── Row.js │ ├── index.js │ ├── organisms/ │ │ ├── Calculator.js │ │ ├── Counter.js │ │ ├── Counter2.js │ │ ├── Counter3.js │ │ ├── Counter4.js │ │ ├── Items.js │ │ ├── ItemsChoice.js │ │ └── Social.js │ └── state/ │ ├── counter.js │ ├── friends.js │ ├── photos.js │ ├── placeholderAPI.js │ └── selection.js ├── nwb.config.js ├── package.json ├── packages/ │ └── create-react-organism/ │ ├── .gitignore │ ├── README.md │ ├── bin/ │ │ └── create-react-organism.js │ └── package.json ├── src/ │ ├── adjustArgs/ │ │ └── extractFromDOM.js │ ├── index.d.ts │ ├── index.js │ ├── multi.js │ └── nextFrame.js ├── tests/ │ ├── .eslintrc │ ├── extractFromDOM-test.js │ ├── index-test.js │ └── multi-test.js └── umd/ └── react-organism.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /coverage /demo/dist /es /lib /node_modules npm-debug.log* ================================================ FILE: .travis.yml ================================================ sudo: false language: node_js node_js: - 4 - 6 - 7 - 8 before_install: - npm install codecov.io coveralls after_success: - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js branches: only: - master ================================================ FILE: CONTRIBUTING.md ================================================ ## Prerequisites [Node.js](http://nodejs.org/) >= v4 must be installed. ## Installation - Running `npm install` in the components's root directory will install everything you need for development. ## Demo Development Server - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. ## Running Tests - `npm test` will run the tests once. - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. - `npm run test:watch` will run the tests on every change. ## Building - `npm run build` will build the component for publishing to npm and also bundle the demo app. - `npm run clean` will delete built resources. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Patrick Smith Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # React Organism [![Travis][build-badge]][build] [![npm package][npm-badge]][npm] [![Coveralls][coveralls-badge]][coveralls] **Dead simple React/Preact state management to bring pure components alive** - Supports `async`/`await` and easy loading (e.g. `fetch()`) - Reload when particular props change - Animate using generator functions: just `yield` the new state for each frame - Tiny: 1.69 KB gzipped (3.49 KB uncompressed) - Embraces the existing functional `setState` while avoiding boilerplate (no writing `this.setState()` or `.bind` again) - Easy to unit test #### Table of contents - [Installation](#installation) - [Demos](#demos) - [Usage](#usage) - [Basic](#basic) - [Using props](#using-props) - [Async & promises](#async) - [Handling events](#handling-events) - [Animation](#animation) - [Serialization: Local storage](#serialization-local-storage) - [Separate and reuse state handlers](#separate-and-reuse-state-handlers) - [Multicelled organisms](#multicelled-organisms) - [API](#api) - [`makeOrganism(PureComponent, StateFunctions, options)`](#makeorganismpurecomponent-statefunctions-options) - [State functions](#state-functions) - [Argument enhancers](#argument-enhancers) - [Why instead of Redux?](#why-instead-of-redux) ## Installation ``` npm i react-organism --save ``` ## Demos - [Animated counter](https://codesandbox.io/s/2vx12v3qmn) - [Dynamic loading with `import()`](https://codesandbox.io/s/X6mLEwG7W) - [Live form error validation with Yup](https://codesandbox.io/s/4xQpKRRWx) - [Multicelled component — using multiple states](https://codesandbox.io/s/Yv7j1xLqM) - [Todo List](https://codesandbox.io/s/yME5Y3Yz) - [Inputs, forms, animation, fetch](https://react-organism.now.sh) · [code](https://github.com/BurntCaramel/react-organism/tree/master/demo/src) - [User Stories Maker](https://codesandbox.io/s/xkZ5ZONl) - [React Cheat Sheet](https://react-cheat.now.sh/) · [code](https://github.com/BurntCaramel/react-cheat) ## Usage ### Basic ```js // organisms/Counter.js import makeOrganism from 'react-organism' import Counter from './components/Counter' export default makeOrganism(Counter, { initial: () => ({ count: 0 }), increment: () => ({ count }) => ({ count: count + 1 }), decrement: () => ({ count }) => ({ count: count - 1 }) }) ``` ```js // components/Counter.js import React, { Component } from 'react' export default function Counter({ count, handlers: { increment, decrement } }) { return (
) } ``` ### Using props The handlers can easily use props, which are always passed as the first argument ```js // organisms/Counter.js import makeOrganism from 'react-organism' import Counter from './components/Counter' export default makeOrganism(Counter, { initial: ({ initialCount = 0 }) => ({ count: initialCount }), increment: ({ stride = 1 }) => ({ count }) => ({ count: count + stride }), decrement: ({ stride = 1 }) => ({ count }) => ({ count: count - stride }) }) // Render passing prop: ``` ### Async Asynchronous code to load from an API is easy: ```js // components/Items.js import React, { Component } from 'react' export default function Items({ items, collectionName, handlers: { load } }) { return (
{ !!items ? ( `${items.length} ${collectionName}` ) : ( 'Loading…' ) }
) } ``` ```js // organisms/Items.js import makeOrganism from 'react-organism' import Items from '../components/Items' const baseURL = 'https://jsonplaceholder.typicode.com' const fetchAPI = (path) => fetch(baseURL + path).then(r => r.json()) export default makeOrganism(Items, { initial: () => ({ items: null }), load: async ({ path }, prevProps) => { if (!prevProps || path !== prevProps.path) { return { items: await fetchAPI(path) } } } }) ``` ```js
``` ### Handling events Handlers can easily accept arguments such as events. ```js // components/Calculator.js import React, { Component } from 'react' export default function Calculator({ value, handlers: { changeValue, double, add3, initial } }) { return (
) } ``` ```js // organisms/Calculator.js import makeOrganism from 'react-organism' import Calculator from '../components/Calculator' export default makeOrganism(Calculator, { initial: ({ initialValue = 0 }) => ({ value: initialValue }), // Destructure event to get target changeValue: (props, { target }) => ({ value }) => ({ value: parseInt(target.value, 10) }), double: () => ({ value }) => ({ value: value * 2 }), add3: () => ({ value }) => ({ value: value + 3 }) }) ``` ### Animation ```js import makeOrganism from 'react-organism' import Counter from '../components/Counter' export default makeOrganism(Counter, { initial: ({ initialCount = 0 }) => ({ count: initialCount }), increment: function * ({ stride = 20 }) { while (stride > 0) { yield ({ count }) => ({ count: count + 1 }) stride -= 1 } }, decrement: function * ({ stride = 20 }) { while (stride > 0) { yield ({ count }) => ({ count: count - 1 }) stride -= 1 } } }) ``` ### Automatically extract from `data-` attributes and `` Example coming soon ### Serialization: Local storage ```js // organisms/Counter.js import makeOrganism from 'react-organism' import Counter from '../components/Counter' const localStorageKey = 'counter' export default makeOrganism(Counter, { initial: ({ initialCount = 0 }) => ({ count: initialCount }), load: async (props, prevProps) => { if (!prevProps) { // Try commenting out: /* throw (new Error('Oops!')) */ // Load previously stored state, if present return await JSON.parse(localStorage.getItem(localStorageKey)) } }, increment: ({ stride = 1 }) => ({ count }) => ({ count: count + stride }), decrement: ({ stride = 1 }) => ({ count }) => ({ count: count - stride }) }, { onChange(state) { // When state changes, save in local storage localStorage.setItem(localStorageKey, JSON.stringify(state)) } }) ``` ### Separate and reuse state handlers React Organism supports separating state handlers and the component into their own files. This means state handlers could be reused by multiple smart components. Here’s an example of separating state: ```js // state/counter.js export const initial = () => ({ count: 0 }) export const increment = () => ({ count }) => ({ count: count + 1 }) export const decrement = () => ({ count }) => ({ count: count - 1 }) ``` ```js // organisms/Counter.js import makeOrganism from 'react-organism' import Counter from './components/Counter' import * as counterState from './state/counter' export default makeOrganism(Counter, counterState) ``` ```js // App.js import React from 'react' import CounterOrganism from './organisms/Counter' class App extends React.Component { render() { return (
) } } ``` ### Multicelled Organisms Example coming soon. ## API ### `makeOrganism(PureComponent, StateFunctions, options?)` ```js import makeOrganism from 'react-organism' ``` Creates a smart component, rendering using React component `PureComponent`, and managing state using `StateFunctions`. #### `PureComponent` A React component, usually a pure functional component. This component is passed as its props: - The props passed to the smart component, combined with - The current state, combined with - `handlers` which correspond to each function in `StateFunctions` and are ready to be passed to e.g. `onClick`, `onChange`, etc. - `loadError?`: Error produced by the `load` handler - `handlerError?`: Error produced by any other handler #### `StateFunctions` Object with functional handlers. See [state functions below](#state-functions). Either pass a object directly with each function, or create a separate file with each handler function `export`ed out, and then bring in using `import * as StateFunctions from '...'`. #### `options` ##### `adjustArgs?(args: array) => newArgs: array` Used to enhance handlers. See [built-in handlers below](#argument-enhancers). ##### `onChange?(state)` Called after the state has changed, making it ideal for saving the state somewhere (e.g. Local Storage). ### State functions Your state is handled by a collection of functions. Each function is pure: they can only rely on the props and state passed to them. Functions return the new state, either immediately or asynchronously. Each handler is passed the current props first, followed by the called arguments: - `(props, event)`: most event handlers, e.g. `onClick`, `onChange` - `(props, first, second)`: e.g. `handler(first, second)` - `(props, ...args)`: get all arguments passed - `(props)`: ignore any arguments - `()`: ignore props and arguments Handlers must return one of the following: - An object with new state changes, a la React’s `setState(changes)`. - A function accepting the previous state and current props, and returns the new state, a la React’s `setState((prevState, props) => changes)`. - A promise resolving to any of the above (object / function), which will then be used to update the state. Uncaught errors are stored in state under the key `handlerError`. Alternatively, your handler can use the `async`/`await` syntax. - An iterator, such as one made by using a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function%2A). Each object passed to `yield` may be one of the above (object / function / promise). - An array of any of the above (object / function / promise / iterator). - Or optionally, nothing. There are some handlers for special tasks, specifically: #### `initial(props) => object` (required) Return initial state to start off with, a la React’s `initialState`. Passed props. #### `load(props: object, prevProps: object?, { handlers: object }) => object | Promise | void` (optional) Passed the current props and the previous props. Return new state, a Promise returning new state, or nothing. You may also use a generator function (`function * load(props, prevProps)`) and `yield` state changes. If this is the first time loaded or if being reloaded, then `prevProps` is `null`. Usual pattern is to check for either `prevProps` being `null` or if the prop of interest has changed from its previous value: ```js export const load = async ({ id }, prevProps) => { if (!prevProps || id !== prevProps.id) { return { item: await loadItem(id) } } } ``` Your `load` handler will be called in React’s lifecycle: `componentDidMount` and `componentWillReceiveProps`. ### Argument enhancers Handler arguments can be adjusted, to cover many common cases. Pass them to the `adjustArgs` option. The following enhancers are built-in: #### `extractFromDOM(args: array) => newArgs: array` ```js import extractFromDOM from 'react-organism/lib/adjustArgs/extractFromDOM' ``` Extract values from DOM, specifically: - For events as the first argument, extracts `value`, `checked`, and `name` from `event.target`. Additionally, if target has `data-` attributes, these will also be extracted in camelCase from its [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset). Suffixing `data-` attributes with `_number` will convert value to a number (instead of string) using `parseFloat`, and drop the suffix. Handler will receive these extracted values in an object as the first argument, followed by the original arguments. - For `submit` events, extracts values of `` fields in a `
`. Handler will receive the values keyed by the each input’s `name` attribute, followed by the original arguments. Pass the handler to the `onSubmit` prop of the ``. Form must have `data-extract` attribute present. To clear the form after submit, add `data-reset` to the form. ## Why instead of Redux? - Like Redux, separate your state management from rendering - Unlike Redux, avoid loose strings for identifying actions - Redux encourages having state in one bundle, whereas dynamic `import()` encourages breaking apps into sections - Easier to reuse functionality, as action handlers are totally encapsulated - No ability to reach across to the other side of your state tree - Encourages composition of components - Supports `async` and `await` in any action - Supports generator functions to allow multiple state changes — great for animation - No `switch` statements - No boilerplate or additional helper libraries needed [build-badge]: https://img.shields.io/travis/RoyalIcing/react-organism/master.png?style=flat-square [build]: https://travis-ci.org/RoyalIcing/react-organism [npm-badge]: https://img.shields.io/npm/v/react-organism.png?style=flat-square [npm]: https://www.npmjs.org/package/react-organism [coveralls-badge]: https://img.shields.io/coveralls/RoyalIcing/react-organism/master.png?style=flat-square [coveralls]: https://coveralls.io/github/RoyalIcing/react-organism ================================================ FILE: demo/src/components/Calculator.js ================================================ import React, { Component } from 'react' export default function Calculator({ value, handlers: { changeValue, double, add3, initial } }) { return (
) } ================================================ FILE: demo/src/components/Counter.js ================================================ import React, { Component } from 'react' export default function Counter({ count, handlers: { increment, decrement, initial } }) { return (
) } ================================================ FILE: demo/src/components/FriendsList.js ================================================ import React, { Component } from 'react' export default function FriendsList({ friendsList, selectedIndex, onSelectAtIndex, handlers: { addRandomFriend } }) { return (
{ !!friendsList ? ( friendsList.map((friend, index) => (
Name: { friend.name }
)) ) : ( 'Loading…' ) }
) } ================================================ FILE: demo/src/components/Items.js ================================================ import React, { Component } from 'react' export default function Counter({ items, collectionName, handlers: { load } }) { return (
{ !!items ? ( `${items.length} ${collectionName}` ) : ( 'Loading…' ) }
) } ================================================ FILE: demo/src/components/Notifications.js ================================================ import React from 'react' export default function Notifications({ friends: { friendsList }, photos: { photosList } }) { return (
{ `${friendsList.length} friends` } { ' | ' } { `${photosList.length} photos` }
) } ================================================ FILE: demo/src/components/PhotosList.js ================================================ import React, { Component } from 'react' export default function PhotosList({ photosList, handlers: { addRandomPhoto, addPhoto } }) { return (
{ !!photosList ? ( photosList.map((photo, index) => (
)) ) : ( 'Loading…' ) }
) } ================================================ FILE: demo/src/components/Row.js ================================================ import React from 'react' const style = { display: 'flex', flexDirection: 'row' } export default function Row({ children }) { return (
) } ================================================ FILE: demo/src/index.js ================================================ import React, { Component } from 'react' import { render } from 'react-dom' import CounterOrganism from './organisms/Counter' import Counter2Organism from './organisms/Counter2' import Counter3Organism from './organisms/Counter3' import Counter4Organism from './organisms/Counter4' import ItemsOrganism from './organisms/Items' import ItemsChoiceOrganism from './organisms/ItemsChoice' import CalculatorOrganism from './organisms/Calculator' import SocialOrganism from './organisms/Social' class Demo extends Component { render() { return

react-organism

Simple counter:


Using props to customize:


Local storage (change and reload page):


Async animated:


Load data from API:




Handling prop changes:


Event handlers with calculator:


Multi-celled organism

} } render(, document.querySelector('#demo')) ================================================ FILE: demo/src/organisms/Calculator.js ================================================ import makeOrganism from '../../../src' import extractFromDOM from '../../../src/adjustArgs/extractFromDOM' import Calculator from '../components/Calculator' export default makeOrganism(Calculator, { initial: ({ initialValue = 0 }) => ({ value: initialValue }), changeValue: (props, { value }) => ({ value: parseInt(value, 10) || '' }), // Or more robust number input handling with fallback to previous value: // changeValue: (props, { target: { value: newValue } }) => ({ value: previousValue }) => ({ // value: newValue && (parseInt(newValue, 10) || previousValue) // }), double: () => ({ value }) => ({ value: (value || 0) * 2 }), add3: () => ({ value }) => ({ value: (value || 0) + 3 }) }, { adjustArgs: extractFromDOM }) ================================================ FILE: demo/src/organisms/Counter.js ================================================ import makeOrganism from '../../../src' import * as counterState from '../state/counter' import Counter from '../components/Counter' //export default makeOrganism(Counter, counterState) export default makeOrganism(Counter, { initial: () => ({ count: 0 }), increment: () => ({ count }) => ({ count: count + 1 }), decrement: () => ({ count }) => ({ count: count - 1 }) }) ================================================ FILE: demo/src/organisms/Counter2.js ================================================ import makeOrganism from '../../../src' import Counter from '../components/Counter' export default makeOrganism(Counter, { initial: ({ initialCount = 0 }) => ({ count: initialCount }), increment: () => ({ count }, { stride = 1 }) => ({ count: count + stride }), decrement: () => ({ count }, { stride = 1 }) => ({ count: count - stride }) }) ================================================ FILE: demo/src/organisms/Counter3.js ================================================ import makeOrganism from '../../../src' import Counter from '../components/Counter' const localStorageKey = 'counter3' export default makeOrganism(Counter, { initial: ({ initialCount = 13 }) => ({ count: initialCount }), load: async (props, prevProps) => { if (!prevProps) { // Try commenting out: /* throw (new Error('Oops!')) */ // Load previously stored state, if present return await JSON.parse(localStorage.getItem(localStorageKey)) } }, increment: ({ stride = 1 }) => ({ count }) => ({ count: count + stride }), decrement: ({ stride = 1 }) => ({ count }) => ({ count: count - stride }) }, { onChange(state) { // When state changes, save in local storage localStorage.setItem(localStorageKey, JSON.stringify(state)) } }) ================================================ FILE: demo/src/organisms/Counter4.js ================================================ import makeOrganism from '../../../src' import Counter from '../components/Counter' export default makeOrganism(Counter, { initial: ({ initialCount = 0 }) => ({ count: initialCount }), increment: function * ({ stride = 20 }) { while (stride > 0) { yield ({ count }) => ({ count: count + 1 }) stride -= 1 } }, decrement: function * ({ stride = 20 }) { while (stride > 0) { yield ({ count }) => ({ count: count - 1 }) stride -= 1 } } }) ================================================ FILE: demo/src/organisms/Items.js ================================================ import makeOrganism from '../../../src' import * as loadItemsState from '../state/placeholderAPI' import Items from '../components/Items' // export default makeOrganism(Items, loadItemsState) const baseURL = 'https://jsonplaceholder.typicode.com' const fetchAPI = (path) => fetch(baseURL + path).then(r => r.json()) export default makeOrganism(Items, { initial: () => ({ items: null }), load: function * ({ path }, prevProps) { if (!prevProps || path !== prevProps.path) { yield { items: null } // Clear so 'loading…' text appears yield fetchAPI(path).then(items => ({ items })) } } }) ================================================ FILE: demo/src/organisms/ItemsChoice.js ================================================ import React from 'react' import makeOrganism from '../../../src' import ItemsOrganism from './Items' export default makeOrganism(({ collection, handlers: { selectPosts, selectPhotos, selectTodos } }) => (
), { initial: () => ({ collection: 'posts' }), selectPosts: () => ({ collection: 'posts' }), selectPhotos: () => ({ collection: 'photos' }), selectTodos: () => ({ collection: 'todos' }) }) ================================================ FILE: demo/src/organisms/Social.js ================================================ import React from 'react' import makeMultiCelledOrganism from '../../../src/multi' import extractFromDOM from '../../../src/adjustArgs/extractFromDOM' import PhotosList from '../components/PhotosList' import FriendsList from '../components/FriendsList' import Notifications from '../components/Notifications' import Row from '../components/Row' import * as friends from '../state/friends' import * as photos from '../state/photos' import * as selection from '../state/selection' const styles = { light: { color: '#111', backgroundColor: '#f6f6f6' }, dark: { color: '#f6f6f6', backgroundColor: '#222' } } function Social({ darkMode = false, cells }) { return (
) } export default makeMultiCelledOrganism(Social, { friends, photos, selection }, { adjustArgs: extractFromDOM }) ================================================ FILE: demo/src/state/counter.js ================================================ export const initial = () => ({ count: 0 }) export const increment = () => ({ count }) => ({ count: count + 1 }) export const decrement = () => ({ count }) => ({ count: count - 1 }) ================================================ FILE: demo/src/state/friends.js ================================================ export const initial = () => ({ friendsList: [] }) const convertUserToFriend = (user) => ({ name: `${user.first} ${user.last}` }) const fetchRandomFriends = () => fetch('https://randomapi.com/api/6de6abfedb24f889e0b5f675edc50deb?fmt=raw&sole') .then(res => res.json()) .then(users => users.slice(0, 10)) .then(users => users.map(convertUserToFriend)) export const load = async (props, prevProps) => { if (!prevProps) { const newFriends = await fetchRandomFriends() return ({ friendsList }) => ({ friendsList: friendsList.concat(newFriends) }) } } export const addFriend = (props, { name }) => ({ friendsList }) => ({ friendsList: friendsList.concat({ name }) }) export const addRandomFriend = async ({ handlers }) => { const [ newFriend ] = await fetchRandomFriends() handlers.addFriend(newFriend) } ================================================ FILE: demo/src/state/photos.js ================================================ export const initial = () => ({ photosList: [] }) const fetchRandomPhotoURL = () => fetch( 'https://source.unsplash.com/random/800x600', { method: 'HEAD', cache: 'no-cache' } ) .then(res => res.url) export const load = async (props, prevProps) => { if (!prevProps) { const url = await fetchRandomPhotoURL() return ({ photosList }) => ({ photosList: photosList.concat({ url }) }) } } export const addPhoto = (props, { url }) => ({ photosList }) => ({ photosList: photosList.concat({ url }) }) export const addRandomPhoto = async (props) => { const url = await fetchRandomPhotoURL() return addPhoto({}, { url }) } ================================================ FILE: demo/src/state/placeholderAPI.js ================================================ const baseURL = 'https://jsonplaceholder.typicode.com' const fetchAPI = (path) => fetch(baseURL + path).then(r => r.json()) export const initial = () => ({ items: null }) export const load = async ({ path }, prevProps) => { if (!prevProps || path !== prevProps.path) { console.log('load', path) return { items: await fetchAPI(path) } } } ================================================ FILE: demo/src/state/selection.js ================================================ export const initial = () => ({ selectedFriendIndex: null, selectedPhotoIndex: null }) export const selectFriendAtIndex = (props, { index }) => ({ selectedFriendIndex: index }) export const selectPhotoAtIndex = (props, { index }) => ({ selectedPhotoIndex: index }) ================================================ FILE: nwb.config.js ================================================ module.exports = { type: 'react-component', webpack: { hoisting: true }, npm: { esModules: true, umd: { global: 'makeOrganism', externals: { 'react': 'React' } } } } ================================================ FILE: package.json ================================================ { "name": "react-organism", "version": "0.3.8", "description": "Separate React state in a dead simple functional way", "main": "lib/index.js", "module": "es/index.js", "types": "src/index.d.ts", "files": [ "css", "es", "lib", "umd" ], "scripts": { "build": "nwb build-react-component", "clean": "nwb clean-module && nwb clean-demo", "dev": "nwb serve-react-demo", "test": "nwb test-react", "test:coverage": "nwb test-react --coverage", "test:watch": "nwb test-react --server", "now:deploy": "yarn run build && cd demo/dist && now deploy", "explore-bundle": "source-map-explorer", "prepublishOnly": "npm run test && npm run build" }, "dependencies": { "awareness": "^1.1.4" }, "peerDependencies": { "react": "^15.0.0-0 || ^16.0.0-0" }, "devDependencies": { "nwb": "^0.21.5", "react": "^16.0.0-0", "react-dom": "^16.0.0-0", "source-map-explorer": "^1.3.3" }, "author": "", "homepage": "", "license": "MIT", "repository": "", "keywords": [ "react-component" ] } ================================================ FILE: packages/create-react-organism/.gitignore ================================================ node_modules ================================================ FILE: packages/create-react-organism/README.md ================================================ # create-react-organism Easily create [react-organism](https://github.com/RoyalIcing/react-organism) smart components. ## Usage ```sh yarn create react-organism CustomName # or npm i -g create-react-organism create-react-organism CustomName ``` Creates an **organisms** directory in your project, and a directory for your organism inside there: **[/src]/organisms/CustomName/** Three files are created inside: - **[CustomName].js:** a pure React component that renders the given state as props, as well as call action handlers defined in **state.js** - **state.js:** a list of exported functions that handle the progression of state, from its `initial` form, to `load` data in asynchronously, to other action handlers that are called in response to events (e.g. onClick, onChange, etc). - **index.js:** connects the pure component and state together and exports it for easy use. To use the organism, simply import it: ```javascript // pages/example.js import CustomName from '../organisms/CustomName' export default () => (
) ``` ================================================ FILE: packages/create-react-organism/bin/create-react-organism.js ================================================ #!/usr/bin/env node const Path = require('path') const FS = require('fs') const Spawn = require('cross-spawn') const { resolve, coroutine, runNode } = require('creed') const _ = require('lodash') const accessibleFile = (path) => runNode(FS.access, path).map(() => path).catch(() => null) const ensureDir = (path) => runNode(FS.mkdir, path).catch(e => { if (e.code === 'EEXIST') { return } throw e }) const installPackage = coroutine(function * installPackage(projectPath, packageName) { const appPackage = require(Path.join(projectPath, 'package.json')) const dependencies = appPackage.dependencies || {} const hasInstalled = !!dependencies[packageName] if (hasInstalled) { return } const useYarn = !!(yield accessibleFile(Path.join(projectPath, 'yarn.lock'))) const command = useYarn ? 'yarnpkg' : 'npm' let args = useYarn ? ['add'] : ['install', '--save'] args.push(packageName) const proc = Spawn.sync(command, args, { stdio: 'inherit' }) if (proc.status !== 0) { throw new Error(`\`${command} ${args.join(' ')}\` failed with status ${proc.status}`) } }) function * run([ inputName, ...args ]) { //const fileName = _.kebabCase(inputName) const componentName = _.upperFirst(_.camelCase(inputName)) let projectPath = process.cwd() let srcPath = yield accessibleFile(Path.resolve(projectPath, 'src')) const codePath = srcPath || projectPath // Add react-organism dependency yield installPackage(projectPath, 'react-organism') // organisms/ const organismsDirPath = Path.resolve(codePath, 'organisms') yield ensureDir(organismsDirPath) // organisms/:fileName const organismPath = Path.join(organismsDirPath, componentName) yield ensureDir(organismPath) // organisms/:fileName/component.js yield runNode(FS.writeFile, Path.join(organismPath, 'component.js'), makeComponentJS(componentName)) // organisms/:fileName/state.js yield runNode(FS.writeFile, Path.join(organismPath, 'state.js'), makeStateJS(componentName)) // organisms/:fileName/index.js yield runNode(FS.writeFile, Path.join(organismPath, 'index.js'), makeIndexJS(componentName)) } function makeStateJS(componentName) { return ` export const initial = () => ({ // TODO: initial state properties }) export const example = (props, ...args) => (prevState) => { // TODO: return changed state return prevState } `.trim() } function makeComponentJS(componentName) { return ` import React from 'react' export default function ${componentName}({ // TODO: props handlers: { example // TODO: state handlers } }) { return (
) } `.trim() } function makeIndexJS(componentName) { return ` import makeOrganism from 'react-organism' import ${componentName} from './component' import * as state from './state' export default makeOrganism(${componentName}, state) `.trim() } coroutine(run)(process.argv.slice(2)) .catch(error => { console.error(error.message) }) ================================================ FILE: packages/create-react-organism/package.json ================================================ { "name": "create-react-organism", "version": "0.2.0", "description": "Tool to easily create react-organism smart components", "engines": { "node": ">=6" }, "bin": { "create-react-organism": "./bin/create-react-organism.js" }, "main": "index.js", "repository": "https://github.com/RoyalIcing/react-organism", "author": "Patrick Smith ", "license": "MIT", "dependencies": { "creed": "^1.2.1", "cross-spawn": "^5.1.0", "lodash": "^4.17.4" } } ================================================ FILE: src/adjustArgs/extractFromDOM.js ================================================ import extractValuesFromDOMEvent from 'awareness/lib/extractValuesFromDOMEvent' export default function extractFromDOM(args) { if (args[0]) { const values = extractValuesFromDOMEvent(args[0]) // Place extracted dataset values first, followed by original arguments args = [values].concat(args) } return args } ================================================ FILE: src/index.d.ts ================================================ declare module 'react-organism' { import React from 'react' export interface ReceiverProps { handlers: HandlersOut } function makeOrganism( Pure: | React.ComponentClass> | React.StatelessComponent>, handlersIn: HandlersIn, options?: { onChange: (newState: State) => {} adjustArgs: (args: any[]) => any[] } ): React.ComponentClass export default makeOrganism } ================================================ FILE: src/index.js ================================================ import React, { PureComponent } from 'react' import makeAwareness from 'awareness' // Returns a new stateful component, given the specified state handlers and a pure component to render with export default ( Pure, handlersIn, { onChange, adjustArgs } = {} ) => class Organism extends PureComponent { static initialStateForProps(props) { return handlersIn.initial(props) } get currentState() { return this.props.getState ? this.props.getState() : this.state } alterState = (stateChanger) => { // Can either be a plain object or a callback to transform the existing state (this.props.setState || this.setState).call( this, stateChanger, // Call onChange once updated with current version of state onChange ? () => { onChange(this.currentState) } : undefined ) } awareness = makeAwareness(this.alterState, handlersIn, { getProps: () => this.props, adjustArgs }) state = this.awareness.state componentDidMount() { this.awareness.loadAsync(this.props, null, this.currentState) } componentDidUpdate(prevProps, prevState) { this.awareness.loadAsync(this.props, prevProps, this.currentState) } render() { // Render the pure component, passing both props and state, plus handlers bundled together return } } ================================================ FILE: src/multi.js ================================================ import React from 'react' import makeAwareness from 'awareness' import nextFrame from './nextFrame' function cellStateChangerCatchingError(cellKey, stateChanger, errorKey) { return (prevState, props) => { let cellChanges = {} // Check if stateChanger is a function if (typeof(stateChanger) === typeof(stateChanger.call)) { try { // Call state changer cellChanges = stateChanger(prevState[cellKey], props) } // State changer may throw catch (error) { // Store error in state return { [errorKey]: error } } } // Else just an object with changes else { cellChanges = stateChanger } return { [cellKey]: Object.assign({}, prevState[cellKey], cellChanges) } } } function processStateChanger(changeState, stateChanger, storeError) { if (!stateChanger) { return; } // Check if thenable (i.e. a Promise) if (typeof stateChanger.then === typeof Object.assign) { return stateChanger.then(stateChanger => ( stateChanger && changeState(stateChanger) )) .catch(storeError) } // Check if iterator else if (typeof stateChanger.next === typeof Object.assign) { return processIterator(changeState, stateChanger, storeError) } // Otherwise, change state immediately // Required for things like