├── .editorconfig ├── .gitignore ├── .npmignore ├── DOCS.md ├── LICENSE.md ├── README.md ├── package.json ├── src ├── example.js ├── example │ ├── Like.js │ ├── Main.js │ ├── Newsfeed.js │ ├── Story.js │ └── githubRest.js └── lib │ ├── assign.js │ ├── assignProperty.js │ ├── createContainer.js │ ├── injectIntoMarkup.js │ ├── isContainer.js │ ├── isRootContainer.js │ ├── overrideCreateElement.js │ ├── promiseProxy.js │ ├── react-dom-server.js │ ├── react-dom.js │ ├── react-transmit.js │ ├── react.js │ ├── render.js │ ├── renderToString.js │ └── takeFromMarkup.js ├── static ├── favicon.ico └── index.html ├── webpack.client-watch.js └── webpack.client.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = crlf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = tab 11 | tab_width = 4 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | *.log 5 | *.map 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | *.log 5 | *.map 6 | .DS_Store 7 | 8 | static/ 9 | src/example/ 10 | src/example.* 11 | tmp/ 12 | webpack.*.js 13 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | ## API: `Transmit` 2 | 3 | Transmit API is available from the `react-transmit` or `react-transmit-native` package: 4 | 5 | ````js 6 | import Transmit from "react-transmit"; // ES6 imports are awseome! 7 | 8 | var Transmit = require("react-transmit"); // Still using require() aye? 9 | ```` 10 | 11 | ### Methods 12 | 13 | The methods are named after their React / Relay counterparts. Their functionality is mostly the same, but their arguments and/or return types might differ slightly. 14 | 15 | #### `createContainer(ReactClass, options) : ReactClass` 16 | 17 | * Creates a container that wraps the original ReactClass. 18 | * The container performs queries and passes query results as props to the original ReactClass. 19 | * Possible `options` are the `initialVariables`, `prepareVariables` function, `shouldContainerUpdate` function, and the `fragments` definitions. 20 | * [Example usage](https://github.com/RickWong/react-transmit/blob/c0266b061a2cfa7030500b932f3a88bf195e4465/src/example/Newsfeed.js#L50-L73) 21 | 22 | #### `render(ReactClass, optionalProps, targetDOMNode, completeCallback) : void` 23 | 24 | * For isomorphic apps, client-side. 25 | * Use it instead of `React.render()` when you're using Transmit's `renderToString()` on the server-side. 26 | * [Example usage](https://github.com/RickWong/react-isomorphic-starterkit/blob/2bf29c747770e79de06e130af325e0bdfb216bc9/src/client.js#L10) 27 | 28 | #### `renderToString(ReactClass [, optionalProps]) : Promise` 29 | 30 | * For isomorphic apps, server-side. 31 | * Use it on the server to render your React component tree and capture the Transmit query results. 32 | * Returns a Promise to a the rendered React-string and the captured query results. 33 | * Tip: catch errors by defining a `.catch()` handler on the returned Promise. 34 | * [Example usage](https://github.com/RickWong/react-isomorphic-starterkit/blob/2bf29c747770e79de06e130af325e0bdfb216bc9/src/server.js#L34-L52) 35 | 36 | #### `injectIntoMarkup(html, data, scripts) : string` 37 | 38 | * For isomorphic apps, server-side. 39 | * If you captured query results on the server with Transmit's `renderToString()` then you can inject that data into the final markup that's sent to the client. Doing this allows Transmit's `render()` on the client to re-use the data. 40 | * This method is actually copied from [react-async](https://github.com/andreypopp/react-async). Thanks [@andreypopp](https://github.com/andreypopp)! 41 | * [Example usage](https://github.com/RickWong/react-isomorphic-starterkit/blob/2bf29c747770e79de06e130af325e0bdfb216bc9/src/server.js#L52) 42 | 43 | #### `setPromiseConstructor(PromiseConstructor) : void` 44 | 45 | * Optional. Provide your preferred Promise implementation instead of using `global.Promise` by default. 46 | 47 | ## API: `Transmit.Container` (Higher-order component) 48 | 49 | Transmit's `createContainer()` method describes a new React component, a so-called Higher-order component that wraps the original ReactClass. Like any React component you can pass props to it. Below are the Transmit-specific props. Your own props are just passed onto the original ReactClass. 50 | 51 | ### PropTypes / specifiable props 52 | 53 | #### `onFetch(Promise) : function` 54 | 55 | * Optional. Pass this callback function to accept a Promise to the fetch results. 56 | * Don't use this to call `setState()`. That's not necessary. Only use it for caching or logging the query results. 57 | * Tip: catch errors by defining a `.catch()` handler on the accepted Promise. 58 | * [Example usage](https://github.com/RickWong/react-transmit/blob/c0266b061a2cfa7030500b932f3a88bf195e4465/src/example/Main.js#L16) 59 | 60 | #### `variables : object` 61 | 62 | * Optional. 63 | * Overwrites the default `initialVariables` defined with `createContainer()`. 64 | 65 | #### `renderLoading : ReactElement or Function` 66 | 67 | * Optional. The container will render this while the queries are not yet resolved. 68 | * Defaults to `null` (React) or `` (React Native). 69 | 70 | ### Static Methods 71 | 72 | #### `getFragment(fragmentName [, variables]) : Promise` 73 | 74 | * Retrieve a single fragment and returns a Promise. 75 | * This is useful to compose a parent query that resolves child components' fragments. 76 | * [Example usage](https://github.com/RickWong/react-transmit/blob/master/src/example/Newsfeed.js#L65-L69) 77 | 78 | 79 | ## API: Original ReactClass' `this.props` 80 | 81 | Transmit exposes a complemental API to the contained ReactClass via its `this.props` in the same way Relay does. Don't worry, your own props are also accessible via `this.props`. 82 | 83 | ### Transmit props 84 | 85 | #### ` : ` 86 | 87 | * For each declared query the original ReactClass will receive the query result from the container as a prop named exactly like the query. 88 | * The query results are simply the values resolved from the query's Promise. 89 | * If the query returns a function (that returns the query's Promise) then it's treated as deferred. The Promise results only become available in subsequent renders. Useful for isomorphic purposes as deferred queries only run client-side, NOT on the server. 90 | * [Example usage](https://github.com/RickWong/react-transmit/blob/c0266b061a2cfa7030500b932f3a88bf195e4465/src/example/Newsfeed.js#L14) 91 | 92 | #### `transmit.variables : object` 93 | 94 | * Currently used variables, read-only. 95 | * You can use mutate these values to by calling `this.props.transmit.forceFetch()` that will also re-perform the queries. 96 | * [Example usage](https://github.com/RickWong/react-transmit/blob/c0266b061a2cfa7030500b932f3a88bf195e4465/src/example/Newsfeed.js#L37) 97 | 98 | ### Methods 99 | 100 | #### `transmit.forceFetch(variables [, fragmentName|fragmentNames]) : Promise` 101 | 102 | * Call this method to perform all queries again with the new `variables`. 103 | * Optionally specify a string or string-array to only re-perform a specific query/queries. 104 | * Returns a Promise to the query results. The same Promise that's passed to `onFetch()`. 105 | * Tip: catch errors by defining a `.catch()` handler on the returned Promise. 106 | * [Example usage](https://github.com/RickWong/react-transmit/blob/c0266b061a2cfa7030500b932f3a88bf195e4465/src/example/Newsfeed.js#L35-L43) 107 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | 3 | Copyright © 2015, Rick Wong 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 3. Neither the name of the copyright holder nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](http://i.imgur.com/X3JE4Ev.png?1) 2 | 3 | [View live demo](https://edealer.nl/react-transmit/) 4 | 5 | # React Transmit 6 | 7 | [Relay](https://facebook.github.io/relay/)-inspired library based on Promises instead of GraphQL. 8 | 9 | Inspired by: [Building the Facebook Newsfeed with Relay](http://facebook.github.io/react/blog/2015/03/19/building-the-facebook-news-feed-with-relay.html) (React blog) 10 | 11 | ![version](https://img.shields.io/npm/v/react-transmit.svg) ![license](https://img.shields.io/npm/l/react-transmit.svg) [![Package Quality](http://npm.packagequality.com/shield/react-transmit.svg)](http://packagequality.com/#?package=react-transmit) ![npm installs](https://img.shields.io/npm/dt/react-transmit.svg) ![downloads](https://img.shields.io/github/downloads/RickWong/react-transmit/latest/total.svg) 12 | 13 | 14 | ## Features 15 | 16 | - API similar to the official Relay API, adapted for Promises. 17 | - Higher-order Component (HoC) syntax is great for functional-style React. 18 | - Composable Promise-based queries using fragments. 19 | - Isomorphic architecture supports server-side rendering. 20 | - Also works with React Native! 21 | 22 | ## Installation 23 | 24 | ```bash 25 | # For web or Node: 26 | npm install react-transmit 27 | 28 | # For React Native: 29 | npm install react-transmit-native 30 | ``` 31 | 32 | ## Usage 33 | 34 | **Newsfeed.js** (read the comments) 35 | 36 | ````js 37 | import React from "react"; 38 | import Transmit from "react-transmit"; // Import Transmit. 39 | import Story from "./Story"; 40 | 41 | const Newsfeed = React.createClass({ 42 | render () { 43 | const {stories} = this.props; // Fragments are guaranteed. 44 | 45 | return
{stories.map(story => )}
; 46 | } 47 | }); 48 | 49 | // Higher-order component that will fetch data for the above React component. 50 | export default Transmit.createContainer(Newsfeed, { 51 | initialVariables: { 52 | count: 10 // Default variable. 53 | }, 54 | fragments: { 55 | // Fragment names become the Transmit prop names. 56 | stories ({count}) { 57 | // This "stories" query returns a Promise composed of 3 other Promises. 58 | return Promise.all([ 59 | Story.getFragment("story", {storyId: 1}), 60 | Story.getFragment("story", {storyId: 2}), 61 | Story.getFragment("story", {storyId: 3}) 62 | ]); 63 | }, 64 | somethingDeferred () { 65 | // Return the promise wrapped in a function to mark this fragment as non-critical. 66 | return () => Promise.resolve(true); 67 | } 68 | } 69 | }); 70 | ```` 71 | **Story.js** (read the comments) 72 | 73 | ````js 74 | import React from "react"; 75 | import Transmit from "react-transmit"; // Import Transmit. 76 | 77 | const Story = React.createClass({ 78 | render () { 79 | const {story} = this.props; // Fragments are guaranteed. 80 | 81 | return

{story.content}

; 82 | } 83 | }); 84 | 85 | export default Transmit.createContainer(Story, { 86 | fragments: { 87 | // This "story" fragment returns a Fetch API promise. 88 | story ({storyId}) { 89 | return fetch("https://some.api/stories/" + storyId).then(res => res.json()); 90 | } 91 | } 92 | }); 93 | ```` 94 | 95 | ## Documentation 96 | 97 | See [DOCS.md](https://github.com/RickWong/react-transmit/blob/master/DOCS.md) 98 | 99 | ## Community 100 | 101 | Let's start one together! After you ★Star this project, follow me [@Rygu](https://twitter.com/rygu) 102 | on Twitter. 103 | 104 | ## License 105 | 106 | BSD 3-Clause license. Copyright © 2015, Rick Wong. All rights reserved. 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-transmit", 3 | "description": "Relay-inspired library based on Promises instead of GraphQL.", 4 | "version": "3.2.0", 5 | "license": "BSD-3-Clause", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/RickWong/react-transmit.git" 9 | }, 10 | "homepage": "https://github.com/RickWong/react-transmit", 11 | "keywords": [ 12 | "react", 13 | "transmit", 14 | "relay", 15 | "react-component" 16 | ], 17 | "main": "src/lib/react-transmit.js", 18 | "scripts": { 19 | "localhost": "sleep 3; which open && open http://localhost:8080", 20 | "build": "webpack --verbose --colors --display-error-details --config webpack.client.js", 21 | "watch-client": "webpack --verbose --colors --display-error-details --config webpack.client-watch.js && webpack-dev-server --config webpack.client-watch.js", 22 | "watch": "concurrently --kill-others 'npm run watch-client' 'npm run localhost'" 23 | }, 24 | "dependencies": { 25 | "ascii-json": ">= 0.2.0", 26 | "object-assign": ">= 4.1.0" 27 | }, 28 | "devDependencies": { 29 | "babel": "^6.5.2", 30 | "babel-core": "^6.14.0", 31 | "babel-loader": "^6.2.5", 32 | "babel-preset-es2015": "^6.14.0", 33 | "babel-preset-react": "^6.11.1", 34 | "concurrently": "^2.2.0", 35 | "fetch-plus": "^3.10.4", 36 | "fetch-plus-json": "^3.10.4", 37 | "isomorphic-fetch": "^2.2.1", 38 | "json-loader": "^0.5.4", 39 | "react": "^15.3.2", 40 | "react-dom": "^15.3.2", 41 | "react-hot-loader": "^1.3.0", 42 | "react-inline-css": "^2.3.0", 43 | "webpack": "^1.13.2", 44 | "webpack-dev-server": "^1.16.1" 45 | }, 46 | "engines": { 47 | "node": ">=0.10.32" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/example.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Transmit from "lib/react-transmit"; 3 | import Main from "example/Main"; 4 | 5 | /** 6 | * Transmit.render() will automatically render with pre-queried data. 7 | */ 8 | try { 9 | Transmit.render(Main, {}, document.getElementById("react-root")); 10 | } 11 | catch (error) { 12 | console.warn(error); 13 | } 14 | -------------------------------------------------------------------------------- /src/example/Like.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import InlineCss from "react-inline-css"; 3 | import Transmit from "lib/react-transmit"; 4 | 5 | /** 6 | * @class Like 7 | */ 8 | const Like = React.createClass({ 9 | render () { 10 | /** 11 | * This prop is guaranteed by Transmit. 12 | */ 13 | const like = this.props.like; 14 | 15 | return ( 16 |
  • 17 | 18 |

    {like.name}

    19 | likes this. 20 |
  • 21 | ); 22 | } 23 | }); 24 | 25 | /** 26 | * Higher-order component that will fetch data for the above React component. 27 | */ 28 | export default Transmit.createContainer(Like, { 29 | fragments: { 30 | /** 31 | * The "like" fragment maps a GitHub user to a like. 32 | */ 33 | like ({stargazer}) { 34 | return Promise.resolve({ 35 | name: stargazer.login, 36 | uri: stargazer.html_url, 37 | profile_picture: { 38 | uri: `${stargazer.avatar_url}&s=20` 39 | } 40 | }); 41 | } 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /src/example/Main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import InlineCss from "react-inline-css"; 3 | import Transmit from "lib/react-transmit"; 4 | import Newsfeed from "example/Newsfeed"; 5 | 6 | /** 7 | * @class Main 8 | */ 9 | const Main = React.createClass({ 10 | render () { 11 | return ( 12 | 13 | 14 | Fork me on GitHub 15 | 16 | 17 | 18 | ); 19 | }, 20 | /** 21 | * This is optional. It allows this parent component to capture the fetched fragments. 22 | */ 23 | onFetch (promise) { 24 | promise.then((fetchedFragments) => { 25 | console.log("Main onFetch: ", fetchedFragments); 26 | }); 27 | } 28 | }); 29 | 30 | export default Main; 31 | 32 | /** 33 | * Style this example app like a Facebook feed. 34 | */ 35 | Main.css = function () { 36 | return ` 37 | * { 38 | box-sizing: border-box; 39 | } 40 | body { 41 | background: #E9EAED; 42 | font-family: Helvetica, sans-serif; 43 | } 44 | a { 45 | color: #3B5998; 46 | text-decoration: none; 47 | } 48 | a:hover { 49 | text-decoration: underline; 50 | } 51 | & .github { 52 | position: fixed; 53 | top: 0; 54 | right: 0; 55 | border: 0; 56 | } 57 | `; 58 | }; 59 | -------------------------------------------------------------------------------- /src/example/Newsfeed.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import InlineCss from "react-inline-css"; 3 | import Transmit from "lib/react-transmit"; 4 | import Story from "example/Story"; 5 | 6 | /** 7 | * @class Newsfeed 8 | */ 9 | const Newsfeed = React.createClass({ 10 | render () { 11 | /** 12 | * This prop could be deferred, see explanation below. 13 | */ 14 | const newsfeed = this.props.newsfeed || []; 15 | 16 | return ( 17 | 18 |
    19 | {newsfeed.map((story, key) => { 20 | return ; 21 | })} 22 |
    23 |
    24 | 27 |
    28 |
    29 | ); 30 | }, 31 | onLoadMore () { 32 | /** 33 | * Call this.props.transmit.forceFetch() to re-fetch fragments with new variables. 34 | */ 35 | this.props.transmit.forceFetch({ 36 | existingNewsfeed: this.props.newsfeed, 37 | nextStoryId: this.props.transmit.variables.nextStoryId + 1 38 | }).then((fetchedFragments) => { 39 | /** 40 | * Optional. Like onFetch() you can capture the fetched data or handle any errors. 41 | */ 42 | console.log("Newsfeed forceFetch: ", fetchedFragments); 43 | }); 44 | } 45 | }); 46 | 47 | /** 48 | * Higher-order component that will fetch data for the above React component. 49 | */ 50 | export default Transmit.createContainer(Newsfeed, { 51 | /** 52 | * Default variables. 53 | */ 54 | initialVariables: { 55 | existingNewsfeed: [], 56 | nextStoryId: 1 57 | }, 58 | fragments: { 59 | /** 60 | * The "newsfeed" fragment fetches the next Story, and returns a Promise to a newsfeed that 61 | * is the existing newsfeed concatenated with the next Story. 62 | * 63 | * Actually this Promise is marked as deferred, since it's wrapped in a function. 64 | */ 65 | newsfeed ({existingNewsfeed, nextStoryId}) { 66 | return () => Story.getFragment( 67 | "story", {storyId: nextStoryId} 68 | ).then((nextStory) => { 69 | return existingNewsfeed.concat([nextStory]); 70 | }); 71 | } 72 | }, 73 | shouldContainerUpdate (nextVariables) { 74 | return this.variables.nextStoryId < nextVariables.nextStoryId; 75 | } 76 | }); 77 | 78 | /** 79 | * Style this example app like a Facebook feed. 80 | */ 81 | Newsfeed.css = function () { 82 | return ` 83 | & { 84 | width: 500px; 85 | margin: 0 auto; 86 | } 87 | & > footer { 88 | text-align: center; 89 | margin: 20px; 90 | } 91 | & button { 92 | border: 1px solid #ccc; 93 | background: #f4f4f4; 94 | padding: 5px 15px; 95 | border-radius: 3px; 96 | cursor: pointer; 97 | outline: 0; 98 | } 99 | `; 100 | }; 101 | -------------------------------------------------------------------------------- /src/example/Story.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import InlineCss from "react-inline-css"; 3 | import Transmit from "lib/react-transmit"; 4 | import Like from "example/Like"; 5 | import githubRest from "./githubRest"; 6 | 7 | /** 8 | * @class Story 9 | */ 10 | const Story = React.createClass({ 11 | render () { 12 | /** 13 | * This prop is guaranteed. 14 | */ 15 | const story = this.props.story; 16 | 17 | return ( 18 | 19 |
    20 |

    21 | 22 | {story.title} 23 |

    24 |

    25 | {story.text} 26 |

    27 | Like 28 | · 29 | Share 30 |
    31 |
      32 | {story.likes.map((like, key) => )} 33 |
    34 |
    35 |
    36 | ); 37 | } 38 | }); 39 | 40 | /** 41 | * Higher-order component that will fetch data for the above React component. 42 | */ 43 | export default Transmit.createContainer(Story, { 44 | fragments: { 45 | /** 46 | * The "story" fragment will fetch some GitHub users as likers, and returns 47 | * a Promise to the new Story object. 48 | */ 49 | story ({storyId}) { 50 | if (!storyId) { 51 | throw new Error("storyId required"); 52 | } 53 | 54 | return ( 55 | githubRest.browse( 56 | ["repos", "RickWong/react-transmit", "stargazers"], 57 | {query: {per_page: 60, page: storyId}} 58 | ).then((stargazers) => { 59 | /** 60 | * Chain a promise that maps GitHub users into likers. 61 | */ 62 | return Promise.all( 63 | stargazers.map((stargazer) => Like.getFragment("like", {stargazer})) 64 | ); 65 | }).then((likes) => { 66 | /** 67 | * Just the same story everytime but with different likes :) 68 | */ 69 | return { 70 | title: "React Transmit", 71 | text: "Relay-inspired library based on Promises instead of GraphQL.", 72 | url: "https://github.com/RickWong/react-transmit", 73 | likes: likes 74 | }; 75 | }) 76 | ); 77 | } 78 | } 79 | }); 80 | 81 | /** 82 | * Style this example app like a Facebook feed. 83 | */ 84 | Story.css = function () { 85 | return ` 86 | & { 87 | margin-top: 10px; 88 | } 89 | & > section { 90 | padding: 10px 12px; 91 | background: #fff; 92 | border: 1px solid #e1e1e1; 93 | border-radius: 3px 3px 0 0; 94 | font-size: 14px; 95 | } 96 | & > section img { 97 | float: left; 98 | width: 40px; 99 | height: 40px; 100 | margin-right: 8px; 101 | border: 1px solid #e1e1e1; 102 | } 103 | & > section h3 { 104 | margin: 0 0 10px 0; 105 | float: left; 106 | line-height: 40px; 107 | } 108 | & > section p { 109 | clear: both; 110 | } 111 | & > section > a { 112 | color: #6d84b4; 113 | font-size: 13px; 114 | } 115 | & > ul { 116 | margin: 0; 117 | padding: 0; 118 | list-style: none; 119 | border: 1px solid #e1e1e1; 120 | border-width: 0 1px 1px; 121 | background: #f6f7f8; 122 | float: left; 123 | width: 100%; 124 | border-radius: 0 0 3px 3px; 125 | } 126 | & > ul li { 127 | padding: 2px 12px; 128 | float: left; 129 | width: 50%; 130 | font-size: 12px; 131 | } 132 | & > ul img { 133 | float: left; 134 | border: 1px solid #e1e1e1; 135 | margin-right: 6px; 136 | width: 20px; 137 | height: 20px; 138 | } 139 | & > ul h4 { 140 | display: inline-block; 141 | margin: 4px 0; 142 | } 143 | & > hr { 144 | border: none; 145 | clear: both; 146 | } 147 | `; 148 | }; 149 | -------------------------------------------------------------------------------- /src/example/githubRest.js: -------------------------------------------------------------------------------- 1 | import fetch from "isomorphic-fetch"; 2 | import {connectEndpoint} from "fetch-plus"; 3 | import jsonMiddleware from "fetch-plus-json"; 4 | 5 | /** 6 | * Return GitHub API client with built-in JSON support. 7 | */ 8 | export default connectEndpoint( 9 | "https://api.github.com", {}, [jsonMiddleware()] 10 | ); 11 | -------------------------------------------------------------------------------- /src/lib/assign.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | module.exports = require("object-assign"); 7 | -------------------------------------------------------------------------------- /src/lib/assignProperty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | function assignProperty (object, key, value) { 7 | if (typeof object === "object") { 8 | object[key] = value; 9 | } 10 | 11 | return object; 12 | } 13 | 14 | module.exports = assignProperty; 15 | -------------------------------------------------------------------------------- /src/lib/createContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | var isRootContainer = require("./isRootContainer"); 7 | var promiseProxy = require("./promiseProxy"); 8 | var React = require("./react"); 9 | var assign = require("./assign"); 10 | var assignProperty = require("./assignProperty"); 11 | 12 | /** 13 | * @function createContainer 14 | * @returns {ReactClass} 15 | */ 16 | module.exports = function (Component, options) { 17 | options = arguments[1] || {}; 18 | 19 | var Container = React.createClass({ 20 | displayName: (Component.displayName || Component.name) + "TransmitContainer", 21 | propTypes: { 22 | variables: React.PropTypes.object, 23 | onFetch: React.PropTypes.func, 24 | renderLoading: React.PropTypes.oneOfType([ 25 | React.PropTypes.element, 26 | React.PropTypes.func 27 | ]) 28 | }, 29 | statics: { 30 | isRootContainer: !!options.initialVariables, 31 | variables: options.initialVariables || {}, 32 | prepareVariables: options.prepareVariables || function (v) { return v; }, 33 | fragments: options.fragments || {}, 34 | /** 35 | * @returns {Promise} 36 | */ 37 | getFragment: function (fragmentName, variables) { 38 | if (!Container.fragments[fragmentName]) { 39 | throw new Error(Component.displayName + " has no '" + fragmentName +"' fragment") 40 | } 41 | 42 | variables = assign({}, Container.variables, variables || {}); 43 | 44 | var promise = Container.fragments[fragmentName](variables); 45 | 46 | if (typeof promise === "function" && isRootContainer(Container)) { 47 | return promiseProxy.Promise.resolve(promise); 48 | } 49 | 50 | return promise; 51 | }, 52 | /** 53 | * @returns {Promise} 54 | */ 55 | getAllFragments: function (variables, optionalFragmentNames) { 56 | var promises = []; 57 | 58 | optionalFragmentNames = optionalFragmentNames || []; 59 | 60 | if (typeof optionalFragmentNames === "string") { 61 | optionalFragmentNames = [optionalFragmentNames]; 62 | } 63 | 64 | Object.keys(Container.fragments).forEach(function (fragmentName) { 65 | if (optionalFragmentNames.length && optionalFragmentNames.indexOf(fragmentName) < 0) { 66 | return; 67 | } 68 | 69 | var promise = Container.getFragment( 70 | fragmentName, variables 71 | ).then(function (fragmentResult) { 72 | return assignProperty({}, fragmentName, fragmentResult); 73 | }); 74 | 75 | promises.push(promise); 76 | }); 77 | 78 | if (!promises.length) { 79 | promises.push(promiseProxy.Promise.resolve(true)); 80 | } 81 | 82 | return promiseProxy.Promise.all( 83 | promises 84 | ).then(function (fetchedFragments) { 85 | return assign.apply(null, fetchedFragments); 86 | }); 87 | }, 88 | getComponent: function () { 89 | return Component; 90 | } 91 | }, 92 | componentDidMount: function () { 93 | // Keep track of the mounted state manually, because the official isMounted() method 94 | // returns true when using renderToString() from react-dom/server. 95 | this._mounted = true; 96 | 97 | if (isRootContainer(Container)) { 98 | var promise = this.fetching || Promise.resolve(null); 99 | var _this = this; 100 | 101 | promise.then(function () { 102 | var deferredFragments = _this.missingFragments(false); 103 | 104 | if (deferredFragments.length) { 105 | _this.forceFetch({}, deferredFragments); 106 | } 107 | }); 108 | } 109 | }, 110 | componentWillUnmount: function () { 111 | this._mounted = false; 112 | }, 113 | _isMounted: function () { 114 | // See the official `isMounted` discussion at https://github.com/facebook/react/issues/2787 115 | return !!this._mounted; 116 | }, 117 | /** 118 | * @returns {Promise|Boolean} 119 | */ 120 | forceFetch: function (nextVariables, optionalFragmentNames, skipDeferred) { 121 | var _this = this; 122 | nextVariables = nextVariables || {}; 123 | 124 | if (!isRootContainer(Container)) { 125 | throw new Error("Only root Transmit Containers should fetch fragments"); 126 | } 127 | 128 | if (options.shouldContainerUpdate && Object.keys(nextVariables).length) { 129 | if (!options.shouldContainerUpdate.call(this, nextVariables)) { 130 | return false; 131 | } 132 | } 133 | 134 | assign(_this.variables, nextVariables); 135 | var fetchPromise = Container.getAllFragments(_this.variables, optionalFragmentNames); 136 | 137 | fetchPromise.then(function (fetchedFragments) { 138 | var deferredFragments = {}; 139 | 140 | Object.keys(fetchedFragments).forEach(function (key) { 141 | if (typeof fetchedFragments[key] !== "function") { 142 | return; 143 | } 144 | 145 | if (skipDeferred) { 146 | // Set deferred fragment to null so component will be rendered. 147 | fetchedFragments[key] = null; 148 | } 149 | else { 150 | // Remember and then delete the deferred fragment. 151 | assignProperty(deferredFragments, key, fetchedFragments[key]); 152 | delete fetchedFragments[key]; 153 | } 154 | }); 155 | 156 | _this.safeguardedSetState(fetchedFragments); 157 | 158 | if (!skipDeferred) { 159 | Object.keys(deferredFragments).forEach(function (key) { 160 | var fetchPromise = deferredFragments[key]().then(function (deferredFragment) { 161 | var fetchedDeferred = assignProperty({}, key, deferredFragment); 162 | 163 | _this.safeguardedSetState(fetchedDeferred); 164 | 165 | return fetchedDeferred; 166 | }); 167 | 168 | _this.callOnFetchHandler(fetchPromise); 169 | }); 170 | } 171 | 172 | return fetchedFragments; 173 | }); 174 | 175 | _this.callOnFetchHandler(fetchPromise); 176 | 177 | return fetchPromise; 178 | }, 179 | callOnFetchHandler: function (fetchPromise) { 180 | if (this.props && this.props.onFetch) { 181 | this.props.onFetch.call(this, fetchPromise); 182 | } 183 | }, 184 | safeguardedSetState: function (stateChanges) { 185 | if (!this._isMounted()) { 186 | return; 187 | } 188 | 189 | if (!Object.keys(stateChanges).length) { 190 | return; 191 | } 192 | 193 | try { 194 | this.setState(stateChanges); 195 | } 196 | catch (error) { 197 | // Call to setState may fail if renderToString() was used. 198 | if (!error.message || !error.message.match(/^document/)) { 199 | throw error; 200 | } 201 | } 202 | }, 203 | /** 204 | * @returns {Array} Names of fragments with missing data. 205 | */ 206 | missingFragments: function (nullAllowed) { 207 | var state = this.state || {}; 208 | var props = this.props || {}; 209 | 210 | if (!Object.keys(Container.fragments).length) { 211 | return []; 212 | } 213 | 214 | var missing = []; 215 | 216 | for (var fragmentName in Container.fragments) { 217 | if (!Container.fragments.hasOwnProperty(fragmentName) || 218 | props.hasOwnProperty(fragmentName) || 219 | state.hasOwnProperty(fragmentName)) { 220 | if (nullAllowed) { 221 | continue; 222 | } 223 | 224 | if (props[fragmentName] || state[fragmentName]) { 225 | continue; 226 | } 227 | } 228 | 229 | missing.push(fragmentName); 230 | } 231 | 232 | return missing; 233 | }, 234 | /** 235 | */ 236 | componentWillMount: function () { 237 | var externalVariables = this.props && this.props.variables || {}; 238 | 239 | this.variables = assign({}, Container.variables, externalVariables); 240 | this.variables = Container.prepareVariables(this.variables); 241 | 242 | if (isRootContainer(Container)) { 243 | var missingFragments = this.missingFragments(true); 244 | 245 | if (missingFragments.length) { 246 | var _this = this; 247 | this.fetching = this.forceFetch({}, missingFragments, true).then(function () { 248 | _this.fetching = false; 249 | }); 250 | } 251 | else { 252 | this.callOnFetchHandler(promiseProxy.Promise.resolve({})); 253 | } 254 | } 255 | }, 256 | /** 257 | * 258 | */ 259 | componentWillReceiveProps: function (nextProps) { 260 | if (isRootContainer(Container)) { 261 | this.forceFetch(nextProps.variables); 262 | } 263 | }, 264 | /** 265 | * @returns {ReactElement} or null 266 | */ 267 | render: function () { 268 | var state = this.state || {}; 269 | var props = this.props || {}; 270 | var transmit = { 271 | variables: this.variables, 272 | forceFetch: this.forceFetch, 273 | onFetch: undefined 274 | }; 275 | 276 | // Don't render without data. 277 | if (this.missingFragments(true).length) { 278 | return (typeof props.renderLoading === "function") ? 279 | props.renderLoading() : props.renderLoading || null; 280 | } 281 | 282 | return React.createElement( 283 | Component, 284 | assign({}, props, state, {transmit: transmit}) 285 | ); 286 | } 287 | }); 288 | 289 | return Container; 290 | }; 291 | -------------------------------------------------------------------------------- /src/lib/injectIntoMarkup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: This file is copied from `react-async`. Thanks Andrey Popp! 3 | * 4 | * @see https://github.com/andreypopp/react-async 5 | */ 6 | "use strict"; 7 | 8 | var asciiJSON = require('ascii-json'); 9 | 10 | /** 11 | * Inject data and optional client scripts into markup. 12 | * 13 | * @param {String} markup 14 | * @param {Object} data 15 | * @param {?Array} scripts 16 | */ 17 | function injectIntoMarkup(markup, data, scripts) { 18 | var escapedJson = asciiJSON.stringify(data).replace(/<\//g, '<\\/'); 19 | var injected = ''; 20 | 21 | if (scripts) { 22 | injected += scripts.map(function(script) { 23 | return ''; 24 | }).join(''); 25 | } 26 | 27 | if (markup.indexOf('') > -1) { 28 | return markup.replace('', injected + '$&'); 29 | } else { 30 | return markup + injected; 31 | } 32 | } 33 | 34 | module.exports = injectIntoMarkup; 35 | -------------------------------------------------------------------------------- /src/lib/isContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | /** 7 | * @function isContainer 8 | */ 9 | module.exports = function (Container) { 10 | return !!(Container && 11 | Container.getFragment && 12 | Container.getAllFragments); 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/isRootContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | var isContainer = require("./isContainer"); 7 | 8 | /** 9 | * @function isContainer 10 | */ 11 | module.exports = function (Container) { 12 | return !!(isContainer(Container) && 13 | Container.isRootContainer); 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/overrideCreateElement.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | var React = require("./react"); 7 | 8 | /** 9 | * @function overrideCreateElement 10 | */ 11 | module.exports = function (replacement, callback) { 12 | var originalCreateElement = React.createElement; 13 | 14 | React.createElement = function (t, p, c) { 15 | var args = [].slice.call(arguments); 16 | return replacement.apply(null, [originalCreateElement].concat(args)); 17 | }; 18 | 19 | callback(); 20 | 21 | React.createElement = originalCreateElement; 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/promiseProxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | module.exports = { 7 | Promise: global.Promise || function () { 8 | throw new Error("Missing ES6 Promise implementation"); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/react-dom-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | module.exports = require("react-dom/server"); 7 | -------------------------------------------------------------------------------- /src/lib/react-dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | module.exports = require("react-dom"); 7 | -------------------------------------------------------------------------------- /src/lib/react-transmit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | module.exports = { 7 | createContainer: require("./createContainer"), 8 | injectIntoMarkup: require("./injectIntoMarkup"), 9 | isContainer: require("./isContainer"), 10 | isRootContainer: require("./isRootContainer"), 11 | render: require("./render"), 12 | renderToString: require("./renderToString"), 13 | takeFromMarkup: require("./takeFromMarkup"), 14 | setPromiseConstructor: function (PromiseConstructor) { 15 | require("./promiseProxy").Promise = PromiseConstructor; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | module.exports = require("react"); 7 | -------------------------------------------------------------------------------- /src/lib/render.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | var assign = require("./assign"); 7 | var isRootContainer = require("./isRootContainer"); 8 | var overrideCreateElement = require("./overrideCreateElement"); 9 | var React = require("./react"); 10 | var ReactDOM = require("./react-dom"); 11 | var takeFromMarkup = require("./takeFromMarkup"); 12 | 13 | var reactData = takeFromMarkup(); 14 | 15 | /** 16 | * @function render 17 | */ 18 | module.exports = function (Component, props, targetDOMNode, callback) { 19 | var fetchedFragments = reactData; 20 | 21 | overrideCreateElement( 22 | function (originalCreateElement, type, props, children) { 23 | var args = [].slice.call(arguments, 1); 24 | 25 | if (isRootContainer(type) && fetchedFragments.length) { 26 | assign(props, fetchedFragments.pop()); 27 | } 28 | 29 | return originalCreateElement.apply(null, args); 30 | }, 31 | function () { 32 | assign(props, {createElement: React.createElement}); 33 | ReactDOM.render(React.createElement(Component, props), targetDOMNode, callback); 34 | } 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/lib/renderToString.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | var assign = require("./assign"); 7 | var isRootContainer = require("./isRootContainer"); 8 | var overrideCreateElement = require("./overrideCreateElement"); 9 | var promiseProxy = require("./promiseProxy"); 10 | var React = require("./react"); 11 | var ReactDOM = require("./react-dom-server"); 12 | 13 | /** 14 | * @function renderToString 15 | */ 16 | module.exports = function (Component, props) { 17 | props = props || {}; 18 | 19 | return new promiseProxy.Promise(function (resolve, reject) { 20 | var promises = []; 21 | var myProps = assign({}, props); 22 | 23 | var reactString; 24 | 25 | overrideCreateElement( 26 | function (originalCreateElement, type, props, children) { 27 | var args = [].slice.call(arguments, 1); 28 | 29 | if (isRootContainer(type)) { 30 | props.onFetch = function (promise) { 31 | promises.push(promise); 32 | }; 33 | } 34 | 35 | return originalCreateElement.apply(null, args); 36 | }, 37 | function () { 38 | assign(myProps, {createElement: React.createElement}); 39 | reactString = ReactDOM.renderToString(React.createElement(Component, myProps)); 40 | } 41 | ); 42 | 43 | if (!promises.length) { 44 | resolve({reactString: reactString, reactData: []}); 45 | } 46 | else { 47 | promiseProxy.Promise.all(promises).then(function (fetchedFragments) { 48 | var reactString; 49 | var reactData = fetchedFragments.slice(0); 50 | 51 | overrideCreateElement( 52 | function (originalCreateElement, type, props, children) { 53 | var args = [].slice.call(arguments, 1); 54 | 55 | if (isRootContainer(type) && fetchedFragments.length) { 56 | assign(props, fetchedFragments.pop()); 57 | } 58 | 59 | return originalCreateElement.apply(null, args); 60 | }, 61 | function () { 62 | assign(myProps, {createElement: React.createElement}); 63 | reactString = ReactDOM.renderToString(React.createElement(Component, myProps)); 64 | } 65 | ); 66 | 67 | resolve({ 68 | reactString: reactString, 69 | reactData: reactData 70 | }); 71 | }).catch(function (error) { 72 | reject(error); 73 | }); 74 | } 75 | }); 76 | }; 77 | -------------------------------------------------------------------------------- /src/lib/takeFromMarkup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright © 2015, Rick Wong. All rights reserved. 3 | */ 4 | "use strict"; 5 | 6 | /** 7 | * @function takeFromMarkup 8 | */ 9 | module.exports = function () { 10 | var packet = []; 11 | 12 | if (typeof window !== "undefined" && window.__reactTransmitPacket instanceof Array) { 13 | packet = window.__reactTransmitPacket.slice(0); 14 | delete window.__reactTransmitPacket; 15 | } 16 | 17 | return packet; 18 | }; 19 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RickWong/react-transmit/8742e072e193bca278da4f20200d09a163acf7f2/static/favicon.ico -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | react-transmit 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /webpack.client-watch.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var config = require("./webpack.client.js"); 3 | 4 | config.cache = true; 5 | config.debug = false; 6 | config.devtool = "cheap-eval-source-map"; 7 | 8 | config.entry.unshift( 9 | "webpack-dev-server/client?http://localhost:8080", 10 | "webpack/hot/only-dev-server" 11 | ); 12 | 13 | config.output.publicPath = "http://localhost:8080/dist/"; 14 | config.output.hotUpdateMainFilename = "update/[hash]/update.json"; 15 | config.output.hotUpdateChunkFilename = "update/[hash]/[id].update.js"; 16 | 17 | config.plugins = [ 18 | new webpack.HotModuleReplacementPlugin() 19 | ]; 20 | 21 | config.module.postLoaders = [ 22 | {test: /\.js$/, loaders: ["react-hot"], exclude: /node_modules/} 23 | ]; 24 | 25 | config.devServer = { 26 | publicPath: "http://localhost:8080/dist/", 27 | contentBase: "./static", 28 | hot: true, 29 | inline: true, 30 | lazy: false, 31 | quiet: true, 32 | noInfo: false, 33 | headers: {"Access-Control-Allow-Origin": "*"}, 34 | stats: {colors: true} 35 | }; 36 | 37 | module.exports = config; 38 | -------------------------------------------------------------------------------- /webpack.client.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var path = require("path"); 3 | 4 | module.exports = { 5 | target: "web", 6 | cache: false, 7 | context: __dirname, 8 | devtool: false, 9 | entry: ["./src/example"], 10 | output: { 11 | path: path.join(__dirname, "static/dist"), 12 | filename: "client.js", 13 | chunkFilename: "[name].[id].js", 14 | publicPath: "dist/" 15 | }, 16 | plugins: [ 17 | new webpack.DefinePlugin({"process.env": {NODE_ENV: '"production"'}}), 18 | new webpack.optimize.DedupePlugin(), 19 | new webpack.optimize.OccurenceOrderPlugin(), 20 | new webpack.optimize.UglifyJsPlugin() 21 | ], 22 | module: { 23 | loaders: [ 24 | {test: /\.json$/, loaders: ["json"]}, 25 | {test: /\.js$/, loaders: ["babel?cacheDirectory&presets[]=es2015&presets[]=react"], exclude: /node_modules/} 26 | ], 27 | noParse: /\.min\.js$/ 28 | }, 29 | resolve: { 30 | alias: { 31 | react: path.join(__dirname, "node_modules/react") 32 | }, 33 | modulesDirectories: [ 34 | "src", 35 | "node_modules", 36 | "web_modules" 37 | ], 38 | extensions: ["", ".json", ".js"] 39 | }, 40 | node: { 41 | __dirname: true, 42 | fs: 'empty' 43 | } 44 | }; 45 | --------------------------------------------------------------------------------