├── .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 | } --------------------------------------------------------------------------------