diff --git a/package-lock.json b/package-lock.json index 6002a549970..ef0509388d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8009,6 +8009,10 @@ "prettier": "^2.3.2" } }, + "node_modules/@mongodb-js/reflux-state-mixin": { + "resolved": "packages/reflux-state-mixin", + "link": true + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", @@ -38081,14 +38085,6 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=" }, - "node_modules/reflux-state-mixin": { - "version": "0.7.0", - "resolved": "git+ssh://git@github.com/mongodb-js/reflux-state-mixin.git#e050454cb3be029c3e7fd2ee6a08111e4d15161f", - "license": "ISC", - "peerDependencies": { - "reflux": ">=0.2.5 <=0.4.x" - } - }, "node_modules/reflux/node_modules/eventemitter3": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", @@ -44420,6 +44416,7 @@ "@mongodb-js/compass-workspaces": "^0.10.0", "@mongodb-js/explain-plan-helper": "^1.1.12", "@mongodb-js/my-queries-storage": "^0.8.1", + "@mongodb-js/reflux-state-mixin": "^1.0.0", "ag-grid-community": "^20.2.0", "ag-grid-react": "^20.2.0", "bson": "^6.7.0", @@ -44436,7 +44433,6 @@ "prop-types": "^15.7.2", "react": "^17.0.2", "reflux": "^0.4.1", - "reflux-state-mixin": "github:mongodb-js/reflux-state-mixin", "semver": "^7.6.2" }, "devDependencies": { @@ -46010,6 +46006,7 @@ "@mongodb-js/compass-logging": "^1.2.18", "@mongodb-js/compass-query-bar": "^8.30.0", "@mongodb-js/connection-storage": "^0.12.0", + "@mongodb-js/reflux-state-mixin": "^1.0.0", "bson": "^6.7.0", "compass-preferences-model": "^2.21.0", "d3": "^3.5.17", @@ -46028,8 +46025,7 @@ "react": "^17.0.2", "react-leaflet": "^2.4.0", "react-leaflet-draw": "^0.19.0", - "reflux": "^0.4.1", - "reflux-state-mixin": "github:mongodb-js/reflux-state-mixin" + "reflux": "^0.4.1" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.1.1", @@ -49143,6 +49139,27 @@ "mocha": "^10.2.0" } }, + "packages/reflux-state-mixin": { + "version": "1.0.0", + "license": "SSPL", + "dependencies": { + "reflux": "^0.4.1" + }, + "devDependencies": { + "@mongodb-js/eslint-config-compass": "^1.1.1", + "@mongodb-js/mocha-config-compass": "^1.3.9", + "@mongodb-js/prettier-config-compass": "^1.0.2", + "@mongodb-js/tsconfig-compass": "^1.0.4", + "@types/mocha": "^9.0.0", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.0", + "mocha": "^10.2.0", + "nyc": "^15.1.0", + "prettier": "^2.7.1", + "typescript": "^5.0.4" + } + }, "packages/reflux-store/node_modules/acorn": { "version": "5.7.4", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", @@ -55600,6 +55617,7 @@ "@mongodb-js/mocha-config-compass": "^1.3.9", "@mongodb-js/my-queries-storage": "^0.8.1", "@mongodb-js/prettier-config-compass": "^1.0.2", + "@mongodb-js/reflux-state-mixin": "^1.0.0", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", @@ -55631,7 +55649,6 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "reflux": "^0.4.1", - "reflux-state-mixin": "github:mongodb-js/reflux-state-mixin", "semver": "^7.6.2", "sinon": "^8.1.1", "typescript": "^5.0.4" @@ -56618,6 +56635,7 @@ "@mongodb-js/mocha-config-compass": "^1.3.9", "@mongodb-js/my-queries-storage": "^0.8.1", "@mongodb-js/prettier-config-compass": "^1.0.2", + "@mongodb-js/reflux-state-mixin": "^1.0.0", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", @@ -56653,7 +56671,6 @@ "react-leaflet": "^2.4.0", "react-leaflet-draw": "^0.19.0", "reflux": "^0.4.1", - "reflux-state-mixin": "github:mongodb-js/reflux-state-mixin", "sinon": "^9.2.3", "typescript": "^5.0.4", "xvfb-maybe": "^0.2.1" @@ -58757,6 +58774,24 @@ "integrity": "sha512-Zaw/H/QUzwnIpThiD8IYxTurC7sv7OLwVXx9msgMkBIB6ebYXLeSNVZ25Q+gDah/t8mRFtBbDhq/Uledg7dPSQ==", "dev": true }, + "@mongodb-js/reflux-state-mixin": { + "version": "file:packages/reflux-state-mixin", + "requires": { + "@mongodb-js/eslint-config-compass": "^1.1.1", + "@mongodb-js/mocha-config-compass": "^1.3.9", + "@mongodb-js/prettier-config-compass": "^1.0.2", + "@mongodb-js/tsconfig-compass": "^1.0.4", + "@types/mocha": "^9.0.0", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.0", + "mocha": "^10.2.0", + "nyc": "^15.1.0", + "prettier": "^2.7.1", + "reflux": "^0.4.1", + "typescript": "^5.0.4" + } + }, "@mongodb-js/saslprep": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", @@ -84259,10 +84294,6 @@ } } }, - "reflux-state-mixin": { - "version": "git+ssh://git@github.com/mongodb-js/reflux-state-mixin.git#e050454cb3be029c3e7fd2ee6a08111e4d15161f", - "from": "reflux-state-mixin@github:mongodb-js/reflux-state-mixin" - }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", diff --git a/packages/compass-crud/package.json b/packages/compass-crud/package.json index 5f2d167abc4..144ae662d3b 100644 --- a/packages/compass-crud/package.json +++ b/packages/compass-crud/package.json @@ -98,7 +98,7 @@ "prop-types": "^15.7.2", "react": "^17.0.2", "reflux": "^0.4.1", - "reflux-state-mixin": "github:mongodb-js/reflux-state-mixin", + "@mongodb-js/reflux-state-mixin": "^1.0.0", "semver": "^7.6.2" }, "is_compass_plugin": true diff --git a/packages/compass-crud/src/stores/crud-store.ts b/packages/compass-crud/src/stores/crud-store.ts index 0cf42823c37..c5d84b96a9d 100644 --- a/packages/compass-crud/src/stores/crud-store.ts +++ b/packages/compass-crud/src/stores/crud-store.ts @@ -3,8 +3,7 @@ import Reflux from 'reflux'; import toNS from 'mongodb-ns'; import { findIndex, isEmpty, isEqual } from 'lodash'; import semver from 'semver'; -// @ts-expect-error no types available -import StateMixin from 'reflux-state-mixin'; +import StateMixin from '@mongodb-js/reflux-state-mixin'; import type { Element } from 'hadron-document'; import { Document } from 'hadron-document'; import HadronDocument from 'hadron-document'; @@ -320,7 +319,7 @@ class CrudStoreImpl extends BaseRefluxStore implements CrudActions { - mixins = [StateMixin.store]; + mixins = [StateMixin.store()]; listenables: unknown[]; // Should this be readonly? The existence of setState would imply that... diff --git a/packages/compass-schema/package.json b/packages/compass-schema/package.json index 5652d8cd0ad..6667e0d1ac9 100644 --- a/packages/compass-schema/package.json +++ b/packages/compass-schema/package.json @@ -99,7 +99,7 @@ "react-leaflet": "^2.4.0", "react-leaflet-draw": "^0.19.0", "reflux": "^0.4.1", - "reflux-state-mixin": "github:mongodb-js/reflux-state-mixin" + "@mongodb-js/reflux-state-mixin": "^1.0.0" }, "is_compass_plugin": true } diff --git a/packages/compass-schema/src/stores/store.ts b/packages/compass-schema/src/stores/store.ts index 7c7126c3de7..955367199d8 100644 --- a/packages/compass-schema/src/stores/store.ts +++ b/packages/compass-schema/src/stores/store.ts @@ -1,6 +1,6 @@ import Reflux from 'reflux'; -// @ts-expect-error no types available -import StateMixin from 'reflux-state-mixin'; +import type { StoreWithStateMixin } from '@mongodb-js/reflux-state-mixin'; +import StateMixin from '@mongodb-js/reflux-state-mixin'; import type { LoggerAndTelemetry } from '@mongodb-js/compass-logging'; import type { InternalLayer } from '../modules/geo'; import { addLayer, generateGeoQuery } from '../modules/geo'; @@ -65,8 +65,6 @@ export type SchemaPluginServices = { }; type SchemaState = { - localAppRegistry: SchemaPluginServices['localAppRegistry']; - globalAppRegistry: SchemaPluginServices['globalAppRegistry']; analysisState: AnalysisState; errorMessage: string; schema: Schema | null; @@ -74,10 +72,7 @@ type SchemaState = { abortController: undefined | AbortController; }; -export type SchemaStore = Reflux.Store & { - state: Readonly; - setState(state: Partial): void; - +export type SchemaStore = StoreWithStateMixin & { localAppRegistry: SchemaPluginServices['localAppRegistry']; globalAppRegistry: SchemaPluginServices['globalAppRegistry']; fieldStoreService: SchemaPluginServices['fieldStoreService']; @@ -139,7 +134,7 @@ export function activateSchemaPlugin( * The reflux store for the schema. */ const store: SchemaStore = Reflux.createStore({ - mixins: [StateMixin.store], + mixins: [StateMixin.store()], listenables: actions, /** @@ -185,8 +180,6 @@ export function activateSchemaPlugin( */ getInitialState(this: SchemaStore): SchemaState { return { - localAppRegistry, - globalAppRegistry, analysisState: ANALYSIS_STATE_INITIAL, errorMessage: '', schema: null, diff --git a/packages/reflux-state-mixin/.depcheckrc b/packages/reflux-state-mixin/.depcheckrc new file mode 100644 index 00000000000..bd17bc9be84 --- /dev/null +++ b/packages/reflux-state-mixin/.depcheckrc @@ -0,0 +1,9 @@ +ignores: + - '@mongodb-js/prettier-config-compass' + - '@mongodb-js/tsconfig-compass' + - '@types/chai' + - '@types/sinon-chai' + - 'sinon' + - 'reflux' +ignore-patterns: + - 'dist' diff --git a/packages/reflux-state-mixin/.eslintignore b/packages/reflux-state-mixin/.eslintignore new file mode 100644 index 00000000000..85a8a75e68c --- /dev/null +++ b/packages/reflux-state-mixin/.eslintignore @@ -0,0 +1,2 @@ +.nyc-output +dist diff --git a/packages/reflux-state-mixin/.eslintrc.js b/packages/reflux-state-mixin/.eslintrc.js new file mode 100644 index 00000000000..e4cf824b6ac --- /dev/null +++ b/packages/reflux-state-mixin/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + root: true, + extends: ['@mongodb-js/eslint-config-compass'], + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig-lint.json'], + }, +}; diff --git a/packages/reflux-state-mixin/.mocharc.js b/packages/reflux-state-mixin/.mocharc.js new file mode 100644 index 00000000000..7e473d17b76 --- /dev/null +++ b/packages/reflux-state-mixin/.mocharc.js @@ -0,0 +1 @@ +module.exports = require('@mongodb-js/mocha-config-compass'); diff --git a/packages/reflux-state-mixin/.prettierignore b/packages/reflux-state-mixin/.prettierignore new file mode 100644 index 00000000000..4d28df6603a --- /dev/null +++ b/packages/reflux-state-mixin/.prettierignore @@ -0,0 +1,3 @@ +.nyc_output +dist +coverage diff --git a/packages/reflux-state-mixin/.prettierrc.json b/packages/reflux-state-mixin/.prettierrc.json new file mode 100644 index 00000000000..18853d1532e --- /dev/null +++ b/packages/reflux-state-mixin/.prettierrc.json @@ -0,0 +1 @@ +"@mongodb-js/prettier-config-compass" diff --git a/packages/reflux-state-mixin/LICENSE b/packages/reflux-state-mixin/LICENSE new file mode 100644 index 00000000000..5e0fd33cbbd --- /dev/null +++ b/packages/reflux-state-mixin/LICENSE @@ -0,0 +1,201 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "{}" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright {yyyy} {name of copyright owner} + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/reflux-state-mixin/README.md b/packages/reflux-state-mixin/README.md new file mode 100644 index 00000000000..61839a148e0 --- /dev/null +++ b/packages/reflux-state-mixin/README.md @@ -0,0 +1,173 @@ +## reflux-state-mixin + +A mixin to make Reflux Stores to have a state, similar to React components. + +### Installation + +```bash +$ npm install @mongodb-js/reflux-state-mixin --save +``` + +### in store: + +```javascript +// myStore.js +var Reflux = require('reflux'); +var StateMixin = require('reflux-state-mixin'); +var Actions = require('./../actions/AnimalsActions'); + +var AnimalStore = (module.exports = Reflux.createStore({ + mixins: [StateMixin.store], + listenables: Actions, //or any other way of listening... + + getInitialState: function () { + //that's a must! + return { + dogs: 5, + cats: 3, + }; + }, + + onNewDogBorn: function () { + this.setState({ dogs: this.state.dogs + 1 }); + //just like in a Component. + //this will `trigger()` this state, similar to `render()` in a Component + }, + + //you can use storeDidUpdate lifecycle in the store, which will get called with every change to the state + storeDidUpdate: function (prevState) { + if (this.state.dogs !== prevState.dogs) { + console.log('number of dogs has changed!'); + } + }, +})); +``` + +### in component: + +#### 1. the easy way - connect, with mixin or decorator: + +```javascript +// PetsComponent.js +var AnimalStore = require('./AnimalStore.js'); +var StateMixin = require('reflux-state-mixin'); + +var PetsComponent = React.createClass({ + mixins:[ + StateMixin.connect(AnimalStore, 'dogs') + StateMixin.connect(AnimalStore, 'cats') + //OR + StateMixin.connect(AnimalStore) //now PetsComponent.state includes AnimalStore.state + ], + + render: function () { + return (

We have {this.state.dogs + this.state.cats} pets

); + } +}) + +``` + +and if you use React's es6 classes, use the es7 `connector` decorator: + +```javascript +import { connector } from 'reflux-state-mixin'; +import { AnimalStore } from './AnimalStore'; + +//@viewPortDecorator // make sure other decorators that returns a Component (usually those who provide props) are above `connector` (since it controls state). +@connector(AnimalStore, 'cats') +@connector(AnimalStore, 'dogs') +//or the entire store +@connector(AnimalStore) +//@autobind //other decorators could be anywhere +class PetsComponent extends React.Component { + render() { + let { dogs, cats } = this.state; + return
We have {dogs + cats} pets
; + } +} +``` + +#### or if you want to take the long way: + +##### listening to a specific property of Store's state + +```javascript +var AnimalStore = require('./AnimalStore.js'); + +var DogsComponent = React.createClass({ + mixins: [Reflux.ListenerMixin], + getInitialState: function () { + return { + dogs: AnimalStore.state.dogs, + //Treat store.state as immutable data - same as you treat component.state - + //you could use it inside a component, but never change it's value - only with setState() + }; + }, + componentDidMount: function () { + this.listenTo(AnimalStore.dogs, this.updateDogs); + //this Component has no interest in `cats` or any other animal, so it listens to `dogs` changes only + }, + updateDogs: function (dogs) { + this.setState({ dogs: dogs }); + //now we have auto-synchronization with `dogs`, with no need for specific logic for that + }, + render: function () { + return ( +
+

We have {this.state.dogs} dogs

+
+ ); + }, +}); +``` + +##### listen to an entire store: + +```javascript +var AnimalStore = require('./AnimalStore.js'); + +var PetsComponent = React.createClass({ + mixins: [Reflux.ListenerMixin], + getInitialState: function () { + return { + dogs: AnimalStore.state.dogs, + cats: AnimalStore.state.cats, + }; + }, + componentDidMount: function () { + this.listenTo(AnimalStore, (state) => { + this.setState({ + dogs: state.dogs, + cats: state.cats, + }); + }); + //this way the component can easily decide what parts of the store-state are interesting + }, + + render: function () { + return ( +
+

We have {this.state.dogs + this.state.cats} pets

+
+ ); + }, +}); +``` + +## some details + +`GetInitialState()` in store should have all of the states from the beginning. +Store should not have any method that are declared in state, since you can listen to MyStore.dogs +`setState()` is checking to see if there is a difference between new `state.key` from current `state.key`. If not, this specific `state.key` it's not triggering. +For any `setState()` the entire store is triggering (regardless of changes), allowing any Component or other Store to listen to the entire Store's state. + +## acknowledgments + +Original source from [yonatanmn/Super-Simple-Reflux](https://github.com/yonatanmn/Super-Simple-Flux). + +This mixin was inspired by (a.k.a shamelessly stole from) - +[triggerables-mixin](https://github.com/jesstelford/reflux-triggerable-mixin). Also see [this](https://github.com/spoike/refluxjs/issues/158) for details. +[reflux-provides-store](https://github.com/brigand/reflux-provides-store) +And [state-mixin](https://github.com/spoike/refluxjs/issues/290). + +I thank @jehoshua02 @brigand and @jesstelford for their valuable code. diff --git a/packages/reflux-state-mixin/old_readme.md b/packages/reflux-state-mixin/old_readme.md new file mode 100644 index 00000000000..096845a1646 --- /dev/null +++ b/packages/reflux-state-mixin/old_readme.md @@ -0,0 +1,145 @@ +# reflux-state-mixin + +Mixin for [reflux](https://www.npmjs.com/packages/reflux) stores to enable them to have `state`, `setState()`, `storeDidUpdate()` and `getInitialState()`, similar to React components. + +## Usage + +### in store: + +```javascript +// myStore.js +var Reflux = require('reflux'); +var StateMixin = require('reflux-state-mixin')(Reflux); //call this mixin like that +var Actions = require('./../actions/AnimalsActions'); + +var AnimalStore = (module.exports = Reflux.createStore({ + mixins: [StateMixin], + listenables: Actions, //or any other way of listening... + + getInitialState() { + return { + dogs: 5, + cats: 3, + }; + }, + + onNewDogBorn: function () { + this.setState({ dogs: this.state.dogs + 1 }); + //just like in a Component. + //this will `trigger()` this state, similar to `render()` in a Component + }, + + //you can use storeDidUpdate lifecycle in the store, which will get called with every change to the state + storeDidUpdate: function (prevState) { + if (this.state.dogs !== prevState.dogs) { + console.log('number of dogs has changed!'); + } + }, +})); +``` + +### in component: + +#### 1: + +```javascript +var AnimalStore = require('./AnimalStore.js'); + +var DogsComponent = React.createClass({ + mixins: [Reflux.ListenerMixin], + getInitialState: function () { + return { + dogs: AnimalStore.state.dogs, + //Treat store.state as immutable data - same as you treat component.state - + //you could use it inside a component, but never change it's value - only with setState() + }; + }, + componentDidMount: function () { + this.listenTo(AnimalStore.dogs, this.updateDogs); + //this Component has no interest in `cats` or any other animal, so it listens to `dogs` changes only + }, + updateDogs: function (dogs) { + this.setState({ dogs: dogs }); + //now we have auto-synchronization with `dogs`, with no need for specific logic for that + }, + render: function () { + return ( +
+

We have {this.state.dogs} dogs

+
+ ); + }, +}); +``` + +#### 2. listen to an entire store: + +```javascript +var AnimalStore = require('./AnimalStore.js'); + +var PetsComponent = React.createClass({ + mixins: [Reflux.ListenerMixin], + getInitialState: function () { + return { + dogs: AnimalStore.state.dogs, + cats: AnimalStore.state.cats, + }; + }, + componentDidMount: function () { + this.listenTo(AnimalStore, (state) => { + this.setState({ + dogs: state.dogs, + cats: state.cats, + }); + }); + //this way the component can easily decide what parts of the store-state are interesting + }, + + render: function () { + return ( +
+

We have {this.state.dogs + this.state.cats} pets

+
+ ); + }, +}); +``` + +#### 3. connect: + +```javascript +var AnimalStore = require('./AnimalStore.js'); +var StateMixin = require('reflux-state-mixin')(Reflux); + +var PetsComponent = React.createClass({ + mixins:[ + StateMixin.connect(AnimalStore, 'dogs') + //OR + StateMixin.connect(AnimalStore) //now PetsComponent.state === AnimalStore.state + ], + + render: function () { + return (

We have {this.state.dogs} dogs

); + } +}) +``` + +## Installation + +```bash +$ npm install reflux-state-mixin --save +``` + +## some details + +`GetInitialState()` in store should have all of the states from the beginning. +`setState()` is checking to see if there is a difference between new `state.key` from current `state.key`. If not, this specific `state.key` it's not triggering. +For any `setState()` the entire store is triggering (regardless of changes), allowing any Component or other Store to listen to the entire Store's state. + +## acknowledgments + +This mixin is combination of two other mixins - +[triggerables-mixin](https://github.com/jesstelford/reflux-triggerable-mixin), a really useful mixin for controlling the trigger of the stores. Also see [this](https://github.com/spoike/refluxjs/issues/158) for details. +And [state-mixin](https://github.com/spoike/refluxjs/issues/290) + +I thank @jehoshua02 and @jesstelford for their valuable code. diff --git a/packages/reflux-state-mixin/package.json b/packages/reflux-state-mixin/package.json new file mode 100644 index 00000000000..11e75183132 --- /dev/null +++ b/packages/reflux-state-mixin/package.json @@ -0,0 +1,69 @@ +{ + "name": "@mongodb-js/reflux-state-mixin", + "description": "Reflux stores mixin adding 'state' syntax similar to React components", + "author": { + "name": "MongoDB Inc", + "email": "compass@mongodb.com" + }, + "publishConfig": { + "access": "public" + }, + "bugs": { + "url": "https://jira.mongodb.org/projects/COMPASS/issues", + "email": "compass@mongodb.com" + }, + "homepage": "https://github.com/mongodb-js/compass", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "https://github.com/mongodb-js/compass.git" + }, + "files": [ + "dist" + ], + "license": "SSPL", + "main": "dist/index.js", + "compass:main": "src/index.ts", + "exports": { + "import": "./dist/.esm-wrapper.mjs", + "require": "./dist/index.js" + }, + "compass:exports": { + ".": "./src/index.ts" + }, + "types": "./dist/index.d.ts", + "scripts": { + "bootstrap": "npm run compile", + "prepublishOnly": "npm run compile && compass-scripts check-exports-exist", + "compile": "tsc -p tsconfig.json && gen-esm-wrapper . ./dist/.esm-wrapper.mjs", + "typecheck": "tsc -p tsconfig-lint.json --noEmit", + "eslint": "eslint", + "prettier": "prettier", + "lint": "npm run eslint . && npm run prettier -- --check .", + "depcheck": "compass-scripts check-peer-deps && depcheck", + "check": "npm run typecheck && npm run lint && npm run depcheck", + "check-ci": "npm run check", + "test": "mocha", + "test-cov": "nyc --compact=false --produce-source-map=false -x \"**/*.spec.*\" --reporter=lcov --reporter=text --reporter=html npm run test", + "test-watch": "npm run test -- --watch", + "test-ci": "npm run test-cov", + "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." + }, + "dependencies": { + "reflux": "^0.4.1" + }, + "devDependencies": { + "@mongodb-js/eslint-config-compass": "^1.1.1", + "@mongodb-js/mocha-config-compass": "^1.3.9", + "@mongodb-js/prettier-config-compass": "^1.0.2", + "@mongodb-js/tsconfig-compass": "^1.0.4", + "@types/mocha": "^9.0.0", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.0", + "mocha": "^10.2.0", + "nyc": "^15.1.0", + "prettier": "^2.7.1", + "typescript": "^5.0.4" + } +} diff --git a/packages/reflux-state-mixin/src/index.spec.tsx b/packages/reflux-state-mixin/src/index.spec.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/reflux-state-mixin/src/index.ts b/packages/reflux-state-mixin/src/index.ts new file mode 100644 index 00000000000..5800bcf250a --- /dev/null +++ b/packages/reflux-state-mixin/src/index.ts @@ -0,0 +1,3 @@ +import storeMixin from './store-mixin'; +export default { store: storeMixin }; +export type { StoreWithStateMixin } from './store-mixin'; diff --git a/packages/reflux-state-mixin/src/store-mixin.ts b/packages/reflux-state-mixin/src/store-mixin.ts new file mode 100644 index 00000000000..19502a98dc0 --- /dev/null +++ b/packages/reflux-state-mixin/src/store-mixin.ts @@ -0,0 +1,57 @@ +import Reflux from 'reflux'; + +export type StoreWithStateMixin> = + Reflux.Store & { + readonly state: Readonly; + setState(newState: Partial): void; + storeDidUpdate?(prevState: T): void; + getInitialState(): T; + } & { + [k in keyof T]: { trigger(value: T[k]): void }; + }; + +function attachAction>( + this: StoreWithStateMixin, + actionName: string & keyof T +) { + if (this[actionName]) { + // eslint-disable-next-line no-console + console.warn('Not attaching event ' + actionName + '; key already exists'); + return; + } + this[actionName] = Reflux.createAction(); +} + +export default >() => ({ + setState: function (this: StoreWithStateMixin, state: Partial) { + let changed = false; + const prevState = { ...this.state }; + + for (const key of Object.keys(state) as (keyof T)[]) { + if (this.state[key] !== state[key]) { + this.state[key] = state[key]!; + this[key].trigger(state[key] as any); + changed = true; + } + } + + if (changed) { + //this.state = extend(this.state, state); + + if (typeof this.storeDidUpdate === 'function') { + this.storeDidUpdate(prevState); + } + + this.trigger(this.state); + } + }, + + init: function (this: StoreWithStateMixin) { + if (typeof this.getInitialState === 'function') { + this.state = this.getInitialState(); + for (const key of Object.keys(this.state)) { + attachAction.call(this, key); + } + } + }, +}); diff --git a/packages/reflux-state-mixin/tsconfig-lint.json b/packages/reflux-state-mixin/tsconfig-lint.json new file mode 100644 index 00000000000..6bdef84f322 --- /dev/null +++ b/packages/reflux-state-mixin/tsconfig-lint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/reflux-state-mixin/tsconfig.json b/packages/reflux-state-mixin/tsconfig.json new file mode 100644 index 00000000000..ecd0a14474a --- /dev/null +++ b/packages/reflux-state-mixin/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@mongodb-js/tsconfig-compass/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["./src/**/*.spec.*"] +}