├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── develop.MD
├── dist
├── index.d.ts
└── index.js
├── package-lock.json
├── package.json
├── src
└── index.tsx
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Arjan Egges
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-apollo-autosave
2 | A React component that allows for editing and autosave behavior, combining an Apollo Query and Mutation. With the query, you retrieve the data that you want to edit from your backend. The mutation updates it. Internally, the component maintains a local copy of the data so that you can directly couple properties of the data to React props. As a result, you don't need to create variables inside your component state for this.
3 |
4 | ## Installation
5 | ```sh
6 | npm install react-apollo-autosave --save
7 | ```
8 |
9 | ## Example
10 | ```typescript
11 | import { EditorAutosave } from 'react-apollo-autosave';
12 |
13 | // GraphQL query and mutation for reading and updating a user's name
14 | const getUser = gql`
15 | query User($id: ID!) {
16 | user(id: $id) {
17 | id
18 | name
19 | }
20 | }
21 | `;
22 |
23 | const updateUser = gql`
24 | mutation UpdateUser($id: ID!, $input: UserInput!) {
25 | updateUser(id: $id, input: $input) {
26 | id
27 | name
28 | }
29 | }
30 | `;
31 |
32 | // A simple React component that shows an input where you can edit a user's name
33 | class MyReactComponent {
34 | render() {
35 | const id = "someUser";
36 | return
43 | {({ update, queryResult: { loading, error, data } }) => {
44 | // deal with loading and error cases
45 | if (loading) { return null; }
46 | if (error) { return error.message; }
47 |
48 | // retrieve the user information from the query result
49 | const user = data.user;
50 |
51 | // render the input
52 | return {
53 | // create the object containing the changed user name
54 | const input = {
55 | name: event.target.value
56 | };
57 |
58 | // only strings with length > 0 are a valid username
59 | const nameIsValid = input.name.length > 0;
60 |
61 | /* Call the update function. The first parameter contains the new local
62 | data (should be the same structure as the query result). The second parameter
63 | contains the information needed to perform the mutation. The third parameter
64 | overrides the mutateOnUpdate value. In this case, the mutation will not be
65 | performed if the user name is not valid, but the local data will be updated
66 | so that the contents of the input element changes when the user types
67 | something.
68 | */
69 | update({ user: input }, { variables: { id, input } }, nameIsValid);
70 | }}>;
71 | }
72 |
73 | }
74 | }
75 | ```
76 |
77 | ## Features
78 |
79 | ### Autosaving changes
80 | Whenever you change the value that the editor component controls, you call the `update` method, which updates the local data. The component then automatically calls the mutate function (resulting in the autosave behavior). The component uses the lodash `throttle` function to reduce the number of queries to the backend. You can set the throttle wait time and whether the throttle is leading or trailing by changing the `waitTime` (default 3000 ms) and `throttleType` (default "leading") properties.
81 |
82 | ### Explicit saving
83 | If you want control over the saving behavior (for example only save when the user presses a button), you can set the `mutateOnUpdate` prop to `false`. Whenever the input changes, call the `update` function:
84 | ```typescript
85 | update({ user: input });
86 | ```
87 |
88 | When the user presses the save button, call the `update` function again to trigger a save explicitly:
89 | ```typescript
90 | update(undefined, { variables: { id, input } }, true);
91 | ```
92 |
93 | ### Combining with validation
94 | In case of an invalid input, normally you don't want to sync the changes to the backend, but you do want to change the local data, otherwise the user will not be able to see what (s)he is typing/clicking on. To support validation, you can override whether a mutation should happen when you call the `update` function (see the code example in the previous section).
95 |
--------------------------------------------------------------------------------
/develop.MD:
--------------------------------------------------------------------------------
1 | Patching the version:
2 |
3 | `npm version patch -m "Version %s - "`
4 |
5 | Push the changes
6 |
7 | `git push && git push --tags`
8 |
9 | Publish the module
10 |
11 | `npm publish`
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | import React = require("react");
2 | import { DocumentNode } from "graphql";
3 | import { QueryProps, QueryResult, OperationVariables, MutationProps, MutationResult, MutationOptions } from "react-apollo";
4 | import { ApolloError } from "apollo-client";
5 | /** Signature of the update function which updates the local data and optionally commits the update to the backend. */
6 | export declare type UpdateFunction = (data?: any, options?: MutationOptions, mutate?: boolean) => void;
7 | /** Object of arguments passed to the editor render prop. */
8 | export interface EditorAutosaveRenderArgs {
9 | /** The result of the initial query. */
10 | queryResult: QueryResult;
11 | /** The result of any subsequent mutation. */
12 | mutationResult: MutationResult;
13 | /** Change handler that updates the local data and optionally commits the update to the backend (overriding the value of mutateOnUpdate). */
14 | update: UpdateFunction;
15 | }
16 | /** Editor component properties. */
17 | export interface EditorAutosaveProps {
18 | /** GraphQL query. This overrides the query field in the queryProps object. */
19 | query?: DocumentNode;
20 | /** Variables needed for the query. This overrides the variables field in the queryProps object. */
21 | queryVariables?: OperationVariables;
22 | /** Additional properties for the query. */
23 | queryProps?: Partial;
24 | /** GraphQL mutation. This overrides the mutation field in the mutationProps object. */
25 | mutation?: DocumentNode;
26 | /** Callback for when the mutation has completed successfully. This performs the same function as the onCompleted callback function in the mutationProps object. */
27 | mutationOnCompleted?: (data: any) => void;
28 | /** Callback for when the mutation resulted in an error. This performs the same function as the onError callback function in the mutationProps object. */
29 | mutationOnError?: (error: ApolloError) => void;
30 | /** Additional properties for the mutation. */
31 | mutationProps?: Partial;
32 | /** Render property, with results and update functions. */
33 | children: (args: EditorAutosaveRenderArgs) => React.ReactNode;
34 | /** The time to wait between mutations in ms (default 3000). */
35 | waitTime?: number;
36 | /** Whether to commit an update automatically to the backend when local data is changed (default true). */
37 | mutateOnUpdate?: boolean;
38 | /** Callback for local data changes. */
39 | onUpdate?: () => void;
40 | /** When to run the first save: immediately (default) or after the wait. */
41 | throttleType?: "leading" | "trailing";
42 | }
43 | /**
44 | * Component handling editing fields and syncing with the database, including an autosave using Apollo GraphQL queries and mutations.
45 | * @class EditorAutosave
46 | */
47 | export declare class EditorAutosave extends React.Component {
48 | /** Default prop values. */
49 | static defaultProps: {
50 | queryProps: {};
51 | mutationProps: {};
52 | waitTime: number;
53 | mutateOnUpdate: boolean;
54 | throttleType: string;
55 | };
56 | /** Local copy of the query result data. */
57 | private localData;
58 | /** Throttled version of mutation function. */
59 | private throttledMutate;
60 | /** Place to accumulate options while waiting */
61 | private mergedOptions;
62 | /** Creates the throttled mutation function if needed. */
63 | private initMutate;
64 | /** Merges two objects but overwrites arrays. */
65 | private merge;
66 | /** Updates the local data, triggers a render, and performs a mutation. */
67 | private update;
68 | /** Handles updating local data. */
69 | private handleUpdateLocalData;
70 | /** Handles performing a mutation. */
71 | private handleMutate;
72 | render(): JSX.Element;
73 | }
74 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __extends = (this && this.__extends) || (function () {
3 | var extendStatics = function (d, b) {
4 | extendStatics = Object.setPrototypeOf ||
5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
7 | return extendStatics(d, b);
8 | }
9 | return function (d, b) {
10 | extendStatics(d, b);
11 | function __() { this.constructor = d; }
12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
13 | };
14 | })();
15 | var __assign = (this && this.__assign) || function () {
16 | __assign = Object.assign || function(t) {
17 | for (var s, i = 1, n = arguments.length; i < n; i++) {
18 | s = arguments[i];
19 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
20 | t[p] = s[p];
21 | }
22 | return t;
23 | };
24 | return __assign.apply(this, arguments);
25 | };
26 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
27 | return new (P || (P = Promise))(function (resolve, reject) {
28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
31 | step((generator = generator.apply(thisArg, _arguments || [])).next());
32 | });
33 | };
34 | var __generator = (this && this.__generator) || function (thisArg, body) {
35 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
36 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
37 | function verb(n) { return function (v) { return step([n, v]); }; }
38 | function step(op) {
39 | if (f) throw new TypeError("Generator is already executing.");
40 | while (_) try {
41 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
42 | if (y = 0, t) op = [op[0] & 2, t.value];
43 | switch (op[0]) {
44 | case 0: case 1: t = op; break;
45 | case 4: _.label++; return { value: op[1], done: false };
46 | case 5: _.label++; y = op[1]; op = [0]; continue;
47 | case 7: op = _.ops.pop(); _.trys.pop(); continue;
48 | default:
49 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
50 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
51 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
52 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
53 | if (t[2]) _.ops.pop();
54 | _.trys.pop(); continue;
55 | }
56 | op = body.call(thisArg, _);
57 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
58 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
59 | }
60 | };
61 | Object.defineProperty(exports, "__esModule", { value: true });
62 | var React = require("react");
63 | var react_apollo_1 = require("react-apollo");
64 | var lodash_1 = require("lodash");
65 | /**
66 | * Component handling editing fields and syncing with the database, including an autosave using Apollo GraphQL queries and mutations.
67 | * @class EditorAutosave
68 | */
69 | var EditorAutosave = /** @class */ (function (_super) {
70 | __extends(EditorAutosave, _super);
71 | function EditorAutosave() {
72 | var _this = _super !== null && _super.apply(this, arguments) || this;
73 | /** Local copy of the query result data. */
74 | _this.localData = null;
75 | /** Throttled version of mutation function. */
76 | _this.throttledMutate = null;
77 | /** Place to accumulate options while waiting */
78 | _this.mergedOptions = {};
79 | /** Creates the throttled mutation function if needed. */
80 | _this.initMutate = function (mutate) {
81 | if (_this.throttledMutate) {
82 | return;
83 | }
84 | var _a = _this.props, waitTime = _a.waitTime, throttleType = _a.throttleType;
85 | // Define throttle type
86 | var throttleOptions = { trailing: true, leading: true };
87 | if (throttleType === "trailing") {
88 | throttleOptions.leading = false;
89 | }
90 | _this.throttledMutate = lodash_1.throttle(function (options) { return __awaiter(_this, void 0, void 0, function () {
91 | var result, error_1;
92 | return __generator(this, function (_a) {
93 | switch (_a.label) {
94 | case 0:
95 | _a.trys.push([0, 2, , 3]);
96 | return [4 /*yield*/, mutate(options)];
97 | case 1:
98 | result = _a.sent();
99 | // Clear accumulated options
100 | this.mergedOptions = {};
101 | return [2 /*return*/, result];
102 | case 2:
103 | error_1 = _a.sent();
104 | // Catch, log, and discard errors since there might not be an error handler due to the
105 | // throttled function call.
106 | console.log(error_1);
107 | return [3 /*break*/, 3];
108 | case 3: return [2 /*return*/];
109 | }
110 | });
111 | }); }, waitTime, throttleOptions);
112 | };
113 | /** Merges two objects but overwrites arrays. */
114 | _this.merge = function (obj1, obj2) {
115 | var customizer = function (objValue, srcValue) {
116 | if (lodash_1.isArray(objValue)) {
117 | return srcValue;
118 | }
119 | };
120 | return lodash_1.mergeWith(obj1, obj2, customizer);
121 | };
122 | /** Updates the local data, triggers a render, and performs a mutation. */
123 | _this.update = function (data, options, mutate) { return __awaiter(_this, void 0, void 0, function () {
124 | var shouldMutate;
125 | return __generator(this, function (_a) {
126 | // Handle updating local data
127 | if (data) {
128 | this.handleUpdateLocalData(data);
129 | }
130 | shouldMutate = mutate === undefined || mutate === null ? this.props.mutateOnUpdate : mutate;
131 | if (shouldMutate) {
132 | return [2 /*return*/, this.handleMutate(options)];
133 | }
134 | return [2 /*return*/];
135 | });
136 | }); };
137 | /** Handles updating local data. */
138 | _this.handleUpdateLocalData = function (data) {
139 | var onUpdate = _this.props.onUpdate;
140 | _this.merge(_this.localData, data);
141 | // Callback
142 | if (onUpdate) {
143 | onUpdate();
144 | }
145 | // Render
146 | _this.forceUpdate();
147 | };
148 | /** Handles performing a mutation. */
149 | _this.handleMutate = function (options) {
150 | if (options) {
151 | // Merge options with any previous calls to make sure every input is sent,
152 | // and not only the last one
153 | _this.merge(_this.mergedOptions, options);
154 | }
155 | if (!_this.throttledMutate) {
156 | // this should never happen, but the check is here for safety
157 | return;
158 | }
159 | return _this.throttledMutate(_this.mergedOptions);
160 | };
161 | return _this;
162 | }
163 | EditorAutosave.prototype.render = function () {
164 | var _this = this;
165 | var _a = this.props, query = _a.query, mutation = _a.mutation, mutationOnCompleted = _a.mutationOnCompleted, mutationOnError = _a.mutationOnError, queryVariables = _a.queryVariables, children = _a.children;
166 | var _b = this.props, queryProps = _b.queryProps, mutationProps = _b.mutationProps;
167 | // Override query and query variables
168 | queryProps = queryProps || {};
169 | queryProps.query = query || queryProps.query;
170 | queryProps.variables = queryVariables || queryProps.variables;
171 | // Override mutation and store onCompleted to call later
172 | mutationProps = mutationProps || {};
173 | mutationProps.mutation = mutation || mutationProps.mutation;
174 | return React.createElement(react_apollo_1.Query, __assign({}, queryProps), function (queryResult) {
175 | var loading = queryResult.loading, data = queryResult.data;
176 | if (_this.localData) {
177 | // Use local data instead of (cached) query result
178 | queryResult.data = _this.localData;
179 | }
180 | else if (!loading && data) {
181 | // First run: create the local copy
182 | _this.localData = lodash_1.cloneDeep(data);
183 | }
184 | return React.createElement(react_apollo_1.Mutation, __assign({}, mutationProps, { onCompleted: function (data) {
185 | // Call the onCompleted function provided by the user of the component
186 | if (mutationOnCompleted) {
187 | mutationOnCompleted(data);
188 | }
189 | // Call the original onCompleted method from the props
190 | if (mutationProps && mutationProps.onCompleted) {
191 | mutationProps.onCompleted(data);
192 | }
193 | }, onError: function (error) {
194 | // Reset the local data from the query
195 | _this.localData = lodash_1.cloneDeep(data);
196 | // Call the onError function provided by the user of the component
197 | if (mutationOnError) {
198 | mutationOnError(error);
199 | }
200 | // Call the original onError method from the props
201 | if (mutationProps && mutationProps.onError) {
202 | mutationProps.onError(error);
203 | }
204 | } }), function (mutate, mutationResult) {
205 | // Verify that the throttled mutate function was created
206 | _this.initMutate(mutate);
207 | // Call the render prop
208 | return children({
209 | queryResult: queryResult, mutationResult: mutationResult,
210 | update: _this.update
211 | });
212 | });
213 | });
214 | };
215 | /** Default prop values. */
216 | EditorAutosave.defaultProps = {
217 | queryProps: {},
218 | mutationProps: {},
219 | waitTime: 3000,
220 | mutateOnUpdate: true,
221 | throttleType: "leading"
222 | };
223 | return EditorAutosave;
224 | }(React.Component));
225 | exports.EditorAutosave = EditorAutosave;
226 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-apollo-autosave",
3 | "version": "0.0.9",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@types/async": {
8 | "version": "2.0.50",
9 | "resolved": "https://registry.npmjs.org/@types/async/-/async-2.0.50.tgz",
10 | "integrity": "sha512-VMhZMMQgV1zsR+lX/0IBfAk+8Eb7dPVMWiQGFAt3qjo5x7Ml6b77jUo0e1C3ToD+XRDXqtrfw+6AB0uUsPEr3Q==",
11 | "optional": true
12 | },
13 | "@types/graphql": {
14 | "version": "14.0.3",
15 | "resolved": "https://registry.npmjs.org/@types/graphql/-/graphql-14.0.3.tgz",
16 | "integrity": "sha512-TcFkpEjcQK7w8OcrQcd7iIBPjU0rdyi3ldj6d0iJ4PPSzbWqPBvXj9KSwO14hTOX2dm9RoiH7VuxksJLNYdXUQ=="
17 | },
18 | "@types/lodash": {
19 | "version": "4.14.117",
20 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.117.tgz",
21 | "integrity": "sha512-xyf2m6tRbz8qQKcxYZa7PA4SllYcay+eh25DN3jmNYY6gSTL7Htc/bttVdkqj2wfJGbeWlQiX8pIyJpKU+tubw=="
22 | },
23 | "@types/prop-types": {
24 | "version": "15.5.6",
25 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.6.tgz",
26 | "integrity": "sha512-ZBFR7TROLVzCkswA3Fmqq+IIJt62/T7aY/Dmz+QkU7CaW2QFqAitCE8Ups7IzmGhcN1YWMBT4Qcoc07jU9hOJQ=="
27 | },
28 | "@types/react": {
29 | "version": "16.4.18",
30 | "resolved": "https://registry.npmjs.org/@types/react/-/react-16.4.18.tgz",
31 | "integrity": "sha512-eFzJKEg6pdeaukVLVZ8Xb79CTl/ysX+ExmOfAAqcFlCCK5TgFDD9kWR0S18sglQ3EmM8U+80enjUqbfnUyqpdA==",
32 | "requires": {
33 | "@types/prop-types": "*",
34 | "csstype": "^2.2.0"
35 | }
36 | },
37 | "@types/zen-observable": {
38 | "version": "0.8.0",
39 | "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz",
40 | "integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg=="
41 | },
42 | "apollo-cache": {
43 | "version": "1.1.20",
44 | "resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.1.20.tgz",
45 | "integrity": "sha512-+Du0/4kUSuf5PjPx0+pvgMGV12ezbHA8/hubYuqRQoy/4AWb4faa61CgJNI6cKz2mhDd9m94VTNKTX11NntwkQ==",
46 | "requires": {
47 | "apollo-utilities": "^1.0.25"
48 | }
49 | },
50 | "apollo-client": {
51 | "version": "2.4.5",
52 | "resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.4.5.tgz",
53 | "integrity": "sha512-nUm06EGa4TP/IY68OzmC3lTD32TqkjLOQdb69uYo+lHl8NnwebtrAw3qFtsQtTEz6ueBp/Z/HasNZng4jwafVQ==",
54 | "requires": {
55 | "@types/async": "2.0.50",
56 | "@types/zen-observable": "^0.8.0",
57 | "apollo-cache": "1.1.20",
58 | "apollo-link": "^1.0.0",
59 | "apollo-link-dedup": "^1.0.0",
60 | "apollo-utilities": "1.0.25",
61 | "symbol-observable": "^1.0.2",
62 | "zen-observable": "^0.8.0"
63 | }
64 | },
65 | "apollo-link": {
66 | "version": "1.2.3",
67 | "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.3.tgz",
68 | "integrity": "sha512-iL9yS2OfxYhigme5bpTbmRyC+Htt6tyo2fRMHT3K1XRL/C5IQDDz37OjpPy4ndx7WInSvfSZaaOTKFja9VWqSw==",
69 | "requires": {
70 | "apollo-utilities": "^1.0.0",
71 | "zen-observable-ts": "^0.8.10"
72 | }
73 | },
74 | "apollo-link-dedup": {
75 | "version": "1.0.10",
76 | "resolved": "https://registry.npmjs.org/apollo-link-dedup/-/apollo-link-dedup-1.0.10.tgz",
77 | "integrity": "sha512-tpUI9lMZsidxdNygSY1FxflXEkUZnvKRkMUsXXuQUNoSLeNtEvUX7QtKRAl4k9ubLl8JKKc9X3L3onAFeGTK8w==",
78 | "requires": {
79 | "apollo-link": "^1.2.3"
80 | }
81 | },
82 | "apollo-utilities": {
83 | "version": "1.0.25",
84 | "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.0.25.tgz",
85 | "integrity": "sha512-AXvqkhni3Ir1ffm4SA1QzXn8k8I5BBl4PVKEyak734i4jFdp+xgfUyi2VCqF64TJlFTA/B73TRDUvO2D+tKtZg==",
86 | "requires": {
87 | "fast-json-stable-stringify": "^2.0.0"
88 | }
89 | },
90 | "asap": {
91 | "version": "2.0.6",
92 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
93 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
94 | },
95 | "core-js": {
96 | "version": "2.5.7",
97 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
98 | "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
99 | },
100 | "csstype": {
101 | "version": "2.5.7",
102 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.5.7.tgz",
103 | "integrity": "sha512-Nt5VDyOTIIV4/nRFswoCKps1R5CD1hkiyjBE9/thNaNZILLEviVw9yWQw15+O+CpNjQKB/uvdcxFFOrSflY3Yw=="
104 | },
105 | "encoding": {
106 | "version": "0.1.12",
107 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
108 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
109 | "requires": {
110 | "iconv-lite": "~0.4.13"
111 | }
112 | },
113 | "fast-json-stable-stringify": {
114 | "version": "2.0.0",
115 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
116 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
117 | },
118 | "fbjs": {
119 | "version": "1.0.0",
120 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-1.0.0.tgz",
121 | "integrity": "sha512-MUgcMEJaFhCaF1QtWGnmq9ZDRAzECTCRAF7O6UZIlAlkTs1SasiX9aP0Iw7wfD2mJ7wDTNfg2w7u5fSCwJk1OA==",
122 | "requires": {
123 | "core-js": "^2.4.1",
124 | "fbjs-css-vars": "^1.0.0",
125 | "isomorphic-fetch": "^2.1.1",
126 | "loose-envify": "^1.0.0",
127 | "object-assign": "^4.1.0",
128 | "promise": "^7.1.1",
129 | "setimmediate": "^1.0.5",
130 | "ua-parser-js": "^0.7.18"
131 | }
132 | },
133 | "fbjs-css-vars": {
134 | "version": "1.0.1",
135 | "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.1.tgz",
136 | "integrity": "sha512-IM+v/C40MNZWqsLErc32e0TyIk/NhkkQZL0QmjBh6zi1eXv0/GeVKmKmueQX7nn9SXQBQbTUcB8zuexIF3/88w=="
137 | },
138 | "graphql": {
139 | "version": "14.0.2",
140 | "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.0.2.tgz",
141 | "integrity": "sha512-gUC4YYsaiSJT1h40krG3J+USGlwhzNTXSb4IOZljn9ag5Tj+RkoXrWp+Kh7WyE3t1NCfab5kzCuxBIvOMERMXw==",
142 | "requires": {
143 | "iterall": "^1.2.2"
144 | }
145 | },
146 | "hoist-non-react-statics": {
147 | "version": "3.0.1",
148 | "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.0.1.tgz",
149 | "integrity": "sha512-1kXwPsOi0OGQIZNVMPvgWJ9tSnGMiMfJdihqEzrPEXlHOBh9AAHXX/QYmAJTXztnz/K+PQ8ryCb4eGaN6HlGbQ==",
150 | "requires": {
151 | "react-is": "^16.3.2"
152 | }
153 | },
154 | "iconv-lite": {
155 | "version": "0.4.24",
156 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
157 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
158 | "requires": {
159 | "safer-buffer": ">= 2.1.2 < 3"
160 | }
161 | },
162 | "invariant": {
163 | "version": "2.2.4",
164 | "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
165 | "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
166 | "requires": {
167 | "loose-envify": "^1.0.0"
168 | }
169 | },
170 | "is-stream": {
171 | "version": "1.1.0",
172 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
173 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
174 | },
175 | "isomorphic-fetch": {
176 | "version": "2.2.1",
177 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
178 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
179 | "requires": {
180 | "node-fetch": "^1.0.1",
181 | "whatwg-fetch": ">=0.10.0"
182 | }
183 | },
184 | "iterall": {
185 | "version": "1.2.2",
186 | "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.2.2.tgz",
187 | "integrity": "sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA=="
188 | },
189 | "js-tokens": {
190 | "version": "4.0.0",
191 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
192 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
193 | },
194 | "lodash": {
195 | "version": "4.17.11",
196 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
197 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
198 | },
199 | "lodash.flowright": {
200 | "version": "3.5.0",
201 | "resolved": "https://registry.npmjs.org/lodash.flowright/-/lodash.flowright-3.5.0.tgz",
202 | "integrity": "sha1-K1//OZcW1+fcVyT+k0n2cGUYTWc="
203 | },
204 | "lodash.isequal": {
205 | "version": "4.5.0",
206 | "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
207 | "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
208 | },
209 | "loose-envify": {
210 | "version": "1.4.0",
211 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
212 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
213 | "requires": {
214 | "js-tokens": "^3.0.0 || ^4.0.0"
215 | }
216 | },
217 | "node-fetch": {
218 | "version": "1.7.3",
219 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
220 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
221 | "requires": {
222 | "encoding": "^0.1.11",
223 | "is-stream": "^1.0.1"
224 | }
225 | },
226 | "object-assign": {
227 | "version": "4.1.1",
228 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
229 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
230 | },
231 | "promise": {
232 | "version": "7.3.1",
233 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
234 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
235 | "requires": {
236 | "asap": "~2.0.3"
237 | }
238 | },
239 | "prop-types": {
240 | "version": "15.6.2",
241 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
242 | "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
243 | "requires": {
244 | "loose-envify": "^1.3.1",
245 | "object-assign": "^4.1.1"
246 | }
247 | },
248 | "react": {
249 | "version": "16.6.0",
250 | "resolved": "https://registry.npmjs.org/react/-/react-16.6.0.tgz",
251 | "integrity": "sha512-zJPnx/jKtuOEXCbQ9BKaxDMxR0001/hzxXwYxG8septeyYGfsgAei6NgfbVgOhbY1WOP2o3VPs/E9HaN+9hV3Q==",
252 | "requires": {
253 | "loose-envify": "^1.1.0",
254 | "object-assign": "^4.1.1",
255 | "prop-types": "^15.6.2",
256 | "scheduler": "^0.10.0"
257 | }
258 | },
259 | "react-apollo": {
260 | "version": "2.2.4",
261 | "resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-2.2.4.tgz",
262 | "integrity": "sha512-haS5R30Qvteb65ZLfWomUZQh47VU4ld4Kof3zlqdbLOrYPt3/DdVZC8ZFPZSxd5zPeIJtZqpUfAxD1WHVoMPIA==",
263 | "requires": {
264 | "fbjs": "^1.0.0",
265 | "hoist-non-react-statics": "^3.0.0",
266 | "invariant": "^2.2.2",
267 | "lodash.flowright": "^3.5.0",
268 | "lodash.isequal": "^4.5.0",
269 | "prop-types": "^15.6.0"
270 | }
271 | },
272 | "react-is": {
273 | "version": "16.6.0",
274 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.0.tgz",
275 | "integrity": "sha512-q8U7k0Fi7oxF1HvQgyBjPwDXeMplEsArnKt2iYhuIF86+GBbgLHdAmokL3XUFjTd7Q363OSNG55FOGUdONVn1g=="
276 | },
277 | "safer-buffer": {
278 | "version": "2.1.2",
279 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
280 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
281 | },
282 | "scheduler": {
283 | "version": "0.10.0",
284 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.10.0.tgz",
285 | "integrity": "sha512-+TSTVTCBAA3h8Anei3haDc1IRwMeDmtI/y/o3iBe3Mjl2vwYF9DtPDt929HyRmV/e7au7CLu8sc4C4W0VOs29w==",
286 | "requires": {
287 | "loose-envify": "^1.1.0",
288 | "object-assign": "^4.1.1"
289 | }
290 | },
291 | "setimmediate": {
292 | "version": "1.0.5",
293 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
294 | "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
295 | },
296 | "symbol-observable": {
297 | "version": "1.2.0",
298 | "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
299 | "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
300 | },
301 | "typescript": {
302 | "version": "3.1.6",
303 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.1.6.tgz",
304 | "integrity": "sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA==",
305 | "dev": true
306 | },
307 | "ua-parser-js": {
308 | "version": "0.7.19",
309 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",
310 | "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
311 | },
312 | "whatwg-fetch": {
313 | "version": "3.0.0",
314 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
315 | "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
316 | },
317 | "zen-observable": {
318 | "version": "0.8.11",
319 | "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.11.tgz",
320 | "integrity": "sha512-N3xXQVr4L61rZvGMpWe8XoCGX8vhU35dPyQ4fm5CY/KDlG0F75un14hjbckPXTDuKUY6V0dqR2giT6xN8Y4GEQ=="
321 | },
322 | "zen-observable-ts": {
323 | "version": "0.8.10",
324 | "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.10.tgz",
325 | "integrity": "sha512-5vqMtRggU/2GhePC9OU4sYEWOdvmayp2k3gjPf4F0mXwB3CSbbNznfDUvDJx9O2ZTa1EIXdJhPchQveFKwNXPQ==",
326 | "requires": {
327 | "zen-observable": "^0.8.0"
328 | }
329 | }
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-apollo-autosave",
3 | "version": "0.0.9",
4 | "description": "React component that allows for editing and autosave behaviour, combining an Apollo Query and Mutation.",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "tsc",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/egges/react-apollo-autosave.git"
14 | },
15 | "keywords": [
16 | "react",
17 | "apollo",
18 | "autosave",
19 | "editor",
20 | "local",
21 | "data"
22 | ],
23 | "author": "Arjan Egges",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/egges/react-apollo-autosave/issues"
27 | },
28 | "homepage": "https://github.com/egges/react-apollo-autosave#readme",
29 | "devDependencies": {
30 | "typescript": "^3.1.6"
31 | },
32 | "dependencies": {
33 | "@types/graphql": "^14.0.3",
34 | "@types/lodash": "^4.14.117",
35 | "@types/react": "^16.4.18",
36 | "apollo-client": "^2.4.5",
37 | "graphql": "^14.0.2",
38 | "lodash": "^4.17.11",
39 | "react": "^16.6.0",
40 | "react-apollo": "^2.2.4"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React = require("react");
2 | import { DocumentNode } from "graphql";
3 | import {
4 | Query, QueryProps, QueryResult,
5 | OperationVariables, Mutation, MutationProps,
6 | MutationResult, MutationFn, MutationOptions
7 | } from "react-apollo";
8 | import { ApolloError } from "apollo-client";
9 | import { mergeWith, cloneDeep, throttle, isArray } from "lodash";
10 |
11 | /** Signature of the update function which updates the local data and optionally commits the update to the backend. */
12 | export type UpdateFunction = (data?: any, options?: MutationOptions, mutate?: boolean, replaceData?: boolean) => void;
13 |
14 | /** Object of arguments passed to the editor render prop. */
15 | export interface EditorAutosaveRenderArgs {
16 | /** The result of the initial query. */
17 | queryResult: QueryResult;
18 | /** The result of any subsequent mutation. */
19 | mutationResult: MutationResult;
20 | /** Change handler that updates the local data and optionally commits the update to the backend (overriding the value of mutateOnUpdate). */
21 | update: UpdateFunction;
22 | }
23 |
24 | /** Editor component properties. */
25 | export interface EditorAutosaveProps {
26 | /** GraphQL query. This overrides the query field in the queryProps object. */
27 | query?: DocumentNode;
28 | /** Variables needed for the query. This overrides the variables field in the queryProps object. */
29 | queryVariables?: TQueryVariables;
30 | /** Additional properties for the query. */
31 | queryProps?: QueryProps;
32 | /** GraphQL mutation. This overrides the mutation field in the mutationProps object. */
33 | mutation?: DocumentNode;
34 | /** Callback for when the mutation has completed successfully. This performs the same function as the onCompleted callback function in the mutationProps object. */
35 | mutationOnCompleted?: (data: any) => void;
36 | /** Callback for when the mutation resulted in an error. This performs the same function as the onError callback function in the mutationProps object. */
37 | mutationOnError?: (error: ApolloError) => void;
38 | /** Additional properties for the mutation. */
39 | mutationProps?: Partial>;
40 | /** Render property, with results and update functions. */
41 | children: (args: EditorAutosaveRenderArgs) => React.ReactNode;
42 | /** The time to wait between mutations in ms (default 3000). */
43 | waitTime?: number;
44 | /** Whether to commit an update automatically to the backend when local data is changed (default true). */
45 | mutateOnUpdate?: boolean;
46 | /** Callback for local data changes. */
47 | onUpdate?: () => void;
48 | /** When to run the first save: immediately (default) or after the wait. */
49 | throttleType?: "leading" | "trailing";
50 | }
51 |
52 | /**
53 | * Component handling editing fields and syncing with the database, including an autosave using Apollo GraphQL queries and mutations.
54 | * @class EditorAutosave
55 | */
56 | export class EditorAutosave
57 | extends React.Component> {
58 |
59 | /** Default prop values. */
60 | public static defaultProps = {
61 | queryProps: {},
62 | mutationProps: {},
63 | waitTime: 3000,
64 | mutateOnUpdate: true,
65 | throttleType: "leading"
66 | };
67 |
68 | /** Local copy of the query result data. */
69 | private localData: any = null;
70 | /** Throttled version of mutation function. */
71 | private throttledMutate: MutationFn | null = null;
72 | /** Place to accumulate options while waiting */
73 | private mergedOptions: MutationOptions = {};
74 |
75 | /** Creates the throttled mutation function if needed. */
76 | private initMutate = (mutate: MutationFn) => {
77 | if (this.throttledMutate) {
78 | return;
79 | }
80 | const { waitTime, throttleType } = this.props;
81 |
82 | // Define throttle type
83 | const throttleOptions = { trailing: true, leading: true };
84 | if (throttleType === "trailing") {
85 | throttleOptions.leading = false;
86 | }
87 |
88 | this.throttledMutate = throttle(async (options?: MutationOptions) => {
89 | try {
90 | // Run the mutation
91 | const result = await mutate(options);
92 | // Clear accumulated options
93 | this.mergedOptions = {};
94 | return result;
95 | } catch (error) {
96 | // Catch, log, and discard errors since there might not be an error handler due to the
97 | // throttled function call.
98 | console.log(error);
99 | }
100 | }, waitTime, throttleOptions);
101 | }
102 |
103 | /** Merges two objects but overwrites arrays. */
104 | private merge = (obj1: any, obj2: any) => {
105 | const customizer = (objValue: any, srcValue: any) => {
106 | if (isArray(objValue)) {
107 | return srcValue;
108 | }
109 | }
110 | return mergeWith(obj1, obj2, customizer);
111 | }
112 |
113 | /** Updates the local data, triggers a render, and performs a mutation. */
114 | private update = async (data?: any, options?: MutationOptions, mutate?: boolean, replaceData: boolean = false) => {
115 | // Handle updating local data
116 | if (data) {
117 | this.handleUpdateLocalData(data, replaceData);
118 | }
119 | // Handle mutation
120 | const shouldMutate = mutate === undefined || mutate === null ? this.props.mutateOnUpdate : mutate;
121 | if (shouldMutate) {
122 | return this.handleMutate(options, replaceData);
123 | }
124 | }
125 |
126 | /** Handles updating local data. */
127 | private handleUpdateLocalData = (data: any, replaceData?: boolean) => {
128 | const { onUpdate } = this.props;
129 | if (replaceData) {
130 | this.localData = data;
131 | } else {
132 | this.merge(this.localData, data);
133 | }
134 | // Callback
135 | if (onUpdate) { onUpdate(); }
136 | // Render
137 | this.forceUpdate();
138 | }
139 |
140 | /** Handles performing a mutation. */
141 | private handleMutate = (options?: MutationOptions, replaceData?: boolean) => {
142 | if (options) {
143 | if (replaceData) {
144 | this.mergedOptions = options;
145 | } else {
146 | // Merge options with any previous calls to make sure every input is sent,
147 | // and not only the last one
148 | this.merge(this.mergedOptions, options);
149 | }
150 |
151 | }
152 | if (!this.throttledMutate) {
153 | // this should never happen, but the check is here for safety
154 | return;
155 | }
156 | return this.throttledMutate(this.mergedOptions);
157 | }
158 |
159 | public render() {
160 | const { query, mutation, queryProps, mutationProps, mutationOnCompleted, mutationOnError, queryVariables, children } = this.props;
161 |
162 | // Override query and query variables
163 | if (queryProps) {
164 | queryProps.query = query || queryProps.query;
165 | queryProps.variables = queryVariables || queryProps.variables;
166 | }
167 |
168 | // Override mutation and store onCompleted to call later
169 | if (mutationProps) {
170 | mutationProps.mutation = mutation || mutationProps.mutation;
171 | }
172 |
173 | return
174 | {(queryResult) => {
175 | const { loading, data } = queryResult;
176 |
177 | if (this.localData) {
178 | // Use local data instead of (cached) query result
179 | queryResult.data = this.localData;
180 | } else if (!loading && data) {
181 | // First run: create the local copy
182 | this.localData = cloneDeep(data);
183 | }
184 |
185 | return {
188 | // Call the onCompleted function provided by the user of the component
189 | if (mutationOnCompleted) {
190 | mutationOnCompleted(mutationData);
191 | }
192 | // Call the original onCompleted method from the props
193 | if (mutationProps && mutationProps.onCompleted) {
194 | mutationProps.onCompleted(mutationData);
195 | }
196 | }}
197 | onError={(error) => {
198 | // Reset the local data from the query
199 | this.localData = cloneDeep(data);
200 | // Call the onError function provided by the user of the component
201 | if (mutationOnError) {
202 | mutationOnError(error);
203 | }
204 | // Call the original onError method from the props
205 | if (mutationProps && mutationProps.onError) {
206 | mutationProps.onError(error);
207 | }
208 | }}>
209 | {(mutate, mutationResult) => {
210 | // Verify that the throttled mutate function was created
211 | this.initMutate(mutate);
212 |
213 | // Call the render prop
214 | return children({
215 | queryResult, mutationResult,
216 | update: this.update
217 | });
218 | }}
219 | ;
220 | }}
221 | ;
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "jsx": "react",
5 | "module": "commonjs",
6 | "lib": ["dom", "es6", "esnext.asynciterable"],
7 | "declaration": true,
8 | "outDir": "./dist",
9 | "strict": true
10 | }
11 | }
--------------------------------------------------------------------------------