├── .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 | 
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 |   [](http://packagequality.com/#?package=react-transmit)  
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 |
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 |
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 |
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 |
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('
9 |
10 |
11 | ') > -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 |