├── .editorconfig
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── none-of-the-above.md
└── workflows
│ └── nodejs.yml
├── .gitignore
├── .prettierrc.js
├── LICENSE.md
├── README.md
├── cycle.png
├── demo
├── .eslintrc.json
├── .gitignore
├── .prettierrc.js
├── README.md
├── cypress.json
├── cypress
│ ├── fixtures
│ │ └── example.json
│ ├── integration
│ │ ├── bigTreePage.test.js
│ │ ├── productPage.test.js
│ │ └── todoMvcPage.test.js
│ ├── plugins
│ │ └── index.js
│ └── support
│ │ ├── commands.js
│ │ └── index.js
├── package-lock.json
├── package.json
├── public
│ ├── browser.html
│ ├── favicon.ico
│ └── index.html
└── src
│ ├── App.css
│ ├── App.js
│ ├── index.css
│ ├── index.js
│ ├── pages
│ ├── bigTree
│ │ ├── BigTree.js
│ │ ├── Item.js
│ │ ├── selectors.js
│ │ ├── updaters.js
│ │ └── utils.js
│ ├── products
│ │ ├── Product.js
│ │ ├── Product.module.css
│ │ ├── Products.js
│ │ ├── Products.module.css
│ │ ├── loadProducts.js
│ │ ├── makeData.js
│ │ └── propTypes
│ │ │ ├── ProductPagePropType.js
│ │ │ └── ProductPropType.js
│ ├── todomvc
│ │ ├── components
│ │ │ ├── Footer.js
│ │ │ ├── Header.js
│ │ │ ├── Link.js
│ │ │ ├── MainSection.js
│ │ │ ├── TodoItem.js
│ │ │ ├── TodoList.js
│ │ │ ├── TodoMvcPage.css
│ │ │ ├── TodoMvcPage.js
│ │ │ └── TodoTextInput.js
│ │ ├── propTypes
│ │ │ ├── TodoMvcPropType.js
│ │ │ └── TodoPropType.js
│ │ ├── selectors
│ │ │ ├── getVisibleTodos.js
│ │ │ └── loadTodoData.js
│ │ └── updaters
│ │ │ └── deleteTodo.js
│ └── updates
│ │ ├── Child.js
│ │ ├── GridItem.js
│ │ ├── GridItem.module.css
│ │ ├── Parent.js
│ │ ├── Updates.js
│ │ └── throttledUpdate.js
│ ├── propTypes
│ └── StorePropType.js
│ └── shared
│ ├── Theme.js
│ └── constants.js
├── index.cjs.js
├── index.esm.js
├── jest.config.js
├── jestSetup.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── collect.tsx
├── index.ts
├── proxyManager.ts
├── shared
│ ├── batchedUpdates.ts
│ ├── constants.ts
│ ├── debug.ts
│ ├── ls.ts
│ ├── paths.ts
│ ├── propTypes.ts
│ ├── pubSub.ts
│ ├── state.ts
│ ├── timeTravel.ts
│ ├── types.ts
│ └── utils.ts
├── store.ts
├── types.d.ts
└── updateManager.ts
├── tests
├── anti
│ └── hiddenProperties.test.tsx
├── integration
│ ├── README.md
│ ├── TaskListTest
│ │ ├── App.tsx
│ │ ├── Notifications.tsx
│ │ ├── Task.tsx
│ │ ├── TaskList.test.tsx
│ │ ├── TaskList.tsx
│ │ └── loadTasks.ts
│ ├── isolation.test.tsx
│ ├── listening.test.tsx
│ ├── newComponentsGetNewStore.test.tsx
│ ├── nodeJs.js
│ ├── propsInheritance.test.tsx
│ ├── readFromTwoStores.test.tsx
│ ├── readRemovedProperty.test.ts
│ ├── renderBatching.test.tsx
│ ├── setStoreTwiceInOnClick.test.tsx
│ ├── sharedStoreProps.test.tsx
│ ├── siblingIsolation.test.tsx
│ ├── storeVPlainObject.test.ts
│ ├── types.test.ts
│ ├── unCollectedChildren.test.tsx
│ └── updating.test.tsx
├── objectTypes
│ ├── array.test.tsx
│ ├── map.test.tsx
│ └── set.test.tsx
├── react
│ ├── basicClassComponent.test.tsx
│ ├── basicFunctionalComponent.test.tsx
│ ├── componentDidMount.test.tsx
│ ├── componentDidUpdate.test.tsx
│ ├── componentsInTheStore.test.tsx
│ ├── forwardRefClass.test.tsx
│ ├── forwardRefFc.test.tsx
│ ├── hooks.test.tsx
│ └── propTypes.test.tsx
├── testUtils.tsx
└── unit
│ ├── batch.test.tsx
│ ├── debug.test.tsx
│ ├── initStore.test.ts
│ ├── noWastedUpdates.test.ts
│ ├── nonReactStatics.test.tsx
│ ├── propTypes.test.tsx
│ ├── setting.test.ts
│ ├── timeTravel.test.tsx
│ ├── useProps.test.tsx
│ └── utils.test.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "project": "./tsconfig.json"
6 | },
7 | "plugins": [
8 | "@typescript-eslint"
9 | ],
10 | "extends": [
11 | "airbnb-typescript",
12 | "plugin:prettier/recommended",
13 | "prettier/@typescript-eslint",
14 | "prettier/react"
15 | ],
16 | "env": {
17 | "node": true,
18 | "browser": true,
19 | "jest": true,
20 | "es2020": false
21 | },
22 | "rules": {
23 | "import/prefer-default-export": "off",
24 | "jsx-a11y/label-has-associated-control": "off",
25 | "no-console": [
26 | "warn",
27 | {
28 | "allow": [
29 | "info",
30 | "warn",
31 | "error",
32 | "group",
33 | "groupCollapsed",
34 | "groupEnd",
35 | "time",
36 | "timeEnd"
37 | ]
38 | }
39 | ],
40 | "no-param-reassign": [
41 | "error",
42 | {
43 | "props": true,
44 | "ignorePropertyModificationsFor": [
45 | "mutableTarget",
46 | "task",
47 | "store"
48 | ]
49 | }
50 | ],
51 | "no-plusplus": "off",
52 | "no-underscore-dangle": [
53 | "error",
54 | {
55 | "allow": [
56 | "__RR__",
57 | "_isMounted",
58 | "_isMounting",
59 | "_isRendering",
60 | "_name"
61 | ]
62 | }
63 | ],
64 | "react/button-has-type": "off",
65 | "react/destructuring-assignment": "off",
66 | "react/jsx-props-no-spreading": "off",
67 | "react/prop-types": "off",
68 | "react/state-in-constructor": ["error", "never"],
69 | "react/static-property-placement": ["error", "static public field"]
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Recollect version**
11 |
12 | **Describe the bug**
13 |
14 |
15 | If possible, replicate the issue in a fork of this CodeSandbox: https://codesandbox.io/s/react-recollect-demo-simple-70x9i
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/none-of-the-above.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: None of the above
3 | about: Not a bug or feature request
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | strategy:
11 | matrix:
12 | node-version: [10, 12, 13]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - run: npm install
21 | - run: npm run check:all
22 | env:
23 | CI: true
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cache
2 | .eslintcache
3 | .idea
4 | node_modules
5 | dist
6 | /private/
7 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | proseWrap: 'always',
3 | singleQuote: true,
4 | };
5 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 David Gilbertson
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 |
--------------------------------------------------------------------------------
/cycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/react-recollect/388e96311c19b136ed81810ffc856158053c19d5/cycle.png
--------------------------------------------------------------------------------
/demo/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": [
4 | "react-app",
5 | "airbnb",
6 | "plugin:prettier/recommended",
7 | "prettier/react",
8 | "plugin:cypress/recommended"
9 | ],
10 | "env": {
11 | "browser": true,
12 | "jest": true
13 | },
14 | "rules": {
15 | "no-plusplus": "off",
16 | "import/prefer-default-export": "off",
17 | "jsx-a11y/anchor-is-valid": "off",
18 | "jsx-a11y/click-events-have-key-events": "off",
19 | "jsx-a11y/control-has-associated-label": "off",
20 | "jsx-a11y/interactive-supports-focus": "off",
21 | "jsx-a11y/label-has-associated-control": "off",
22 | "jsx-a11y/no-autofocus": "off",
23 | "jsx-a11y/no-noninteractive-element-interactions": "off",
24 | "jsx-a11y/no-static-element-interactions": "off",
25 | "no-param-reassign": "off",
26 | "react/button-has-type": "off",
27 | "react/destructuring-assignment": "off",
28 | "react/jsx-filename-extension": "off",
29 | "react/require-default-props": "off",
30 | "react/state-in-constructor": ["error", "never"]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .env
26 | /debug.log
27 | /cypress/videos/
28 |
--------------------------------------------------------------------------------
/demo/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | proseWrap: 'always',
3 | singleQuote: true,
4 | };
5 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # A demo site for React Recollect
2 |
3 | ## Running locally
4 |
5 | `cd demo`, `npm i`, `npm start`.
6 |
7 | ## Developing locally
8 |
9 | To use this site when developing `react-recollect` locally, in the root of the
10 | `react-recollect` repo, run:
11 |
12 | ```
13 | npm run build:watch
14 | ```
15 |
16 | Then in the `/demo` site directory...
17 |
18 | ```
19 | cd demo
20 | ```
21 |
22 | Create a symlink for `react-recollect`:
23 |
24 | ```
25 | npm link ../
26 | ```
27 |
28 | Now imports of `'react-recollect'` in the demo site will load from
29 | `react-recollect/dist`.
30 |
31 | Running `npm i` will undo this link, in which case you'll need to do it again to
32 | relink.
33 |
34 | Beware! Since this directory will have a `node_modules` directory, `react-dom`
35 | may be loaded from there, rather than the `demo/node_modules` directory. To be
36 | 100% certain you're replicating what users get, you'll want to release as a
37 | prerelease and install it from npm. This is also required to test that when the
38 | package is installed, it doesn't install any `node_modules` when it shouldn't
39 | (e.g. it should share hoistNonReactStatics with material-ui).
40 |
41 | Unlink with `npm i react-recollect`.
42 |
43 | Start the demo site:
44 |
45 | ```
46 | npm start
47 | ```
48 |
49 | And you're in business.
50 |
51 | If your editor complains about ESLint rules, it might be struggling with nested
52 | projects, if so, open `react-recollect/demo` as its own project. That's better
53 | for searching/etc. anyway.
54 |
55 | ## Test the UMD build
56 |
57 | To test loading Recollect via a script tag, run:
58 |
59 | ```
60 | npm run serve:root
61 | ```
62 |
63 | And go to http://localhost:3000/demo/public/browser.html
64 |
65 | ## Open in CodeSandbox
66 |
67 | https://codesandbox.io/s/github/davidgilbertson/react-recollect/tree/master/demo
68 |
--------------------------------------------------------------------------------
/demo/cypress.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/demo/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/demo/cypress/integration/bigTreePage.test.js:
--------------------------------------------------------------------------------
1 | it('Product page should work', () => {
2 | cy.viewport(1000, 2000);
3 |
4 | cy.visit('http://localhost:3000');
5 |
6 | // Go to the Big tree page
7 | cy.contains('Big tree').click();
8 |
9 | // Names are in the format:
10 | // A [parent type] with [children type] [(id)] [[renderCount]]
11 | cy.findByText('An object prop with Array children (100) [1]');
12 |
13 | cy.findByTitle('Turn node 100 on').click();
14 | cy.findByTitle('Turn node 100 off');
15 |
16 | cy.findByText('An object prop with Array children (100) [2]');
17 |
18 | cy.findByTitle('Add Object child to node 100').click();
19 | cy.findByTitle('Add Array child to node 100').click();
20 | cy.findByTitle('Add Map child to node 100').click();
21 | cy.findByTitle('Add Set child to node 100').click();
22 |
23 | // Root level now rendered 6 times
24 | cy.findByText('An object prop with Array children (100) [6]');
25 |
26 | // Each item rendered once when added, then once as each new one was added
27 | cy.findByText('An array item with Object children (101) [4]');
28 | cy.findByText('An array item with Array children (102) [3]');
29 | cy.findByText('An array item with Map children (103) [2]');
30 | cy.findByText('An array item with Set children (104) [1]');
31 |
32 | // Add children to two siblings
33 | cy.findByTitle('Add Map child to node 101').click();
34 | cy.findByTitle('Add Map child to node 102').click();
35 |
36 | // Everyone renders twice more
37 | cy.findByText('An object prop with Array children (100) [8]');
38 | cy.findByText('An array item with Object children (101) [6]');
39 | cy.findByText('An object prop with Map children (105) [2]'); // New
40 | cy.findByText('An array item with Array children (102) [5]');
41 | cy.findByText('An array item with Map children (106) [1]'); // New
42 | cy.findByText('An array item with Map children (103) [4]');
43 | cy.findByText('An array item with Set children (104) [3]');
44 |
45 | cy.findByTitle('Turn node 105 on').click();
46 |
47 | // Re-rendered items will be all the ancestors of the changed item, plus
48 | // the immediate children of any changed item
49 | cy.findByText('An object prop with Array children (100) [9]');
50 | cy.findByText('An array item with Object children (101) [7]');
51 | cy.findByText('An object prop with Map children (105) [3]');
52 | cy.findByText('An array item with Array children (102) [6]');
53 | cy.findByText('An array item with Map children (106) [1]'); // No re-render
54 | cy.findByText('An array item with Map children (103) [5]');
55 | cy.findByText('An array item with Set children (104) [4]');
56 |
57 | cy.findByPlaceholderText('Notes for node 106').type('0123456789');
58 |
59 | cy.findByText('An object prop with Array children (100) [19]');
60 | cy.findByText('An array item with Object children (101) [17]');
61 | cy.findByText('An object prop with Map children (105) [3]'); // No re-render
62 | cy.findByText('An array item with Array children (102) [16]');
63 | cy.findByText('An array item with Map children (106) [11]');
64 | cy.findByText('An array item with Map children (103) [15]');
65 | cy.findByText('An array item with Set children (104) [14]');
66 |
67 | cy.findByTitle('Delete node 105').click();
68 | cy.findByTitle('Delete node 106').click();
69 |
70 | // Every type as a child of every other type
71 | cy.findByTitle('Add Object child to node 101').click();
72 | cy.findByTitle('Add Array child to node 101').click();
73 | cy.findByTitle('Add Map child to node 101').click();
74 | cy.findByTitle('Add Set child to node 101').click();
75 | cy.findByTitle('Add Object child to node 102').click();
76 | cy.findByTitle('Add Array child to node 102').click();
77 | cy.findByTitle('Add Map child to node 102').click();
78 | cy.findByTitle('Add Set child to node 102').click();
79 | cy.findByTitle('Add Object child to node 103').click();
80 | cy.findByTitle('Add Array child to node 103').click();
81 | cy.findByTitle('Add Map child to node 103').click();
82 | cy.findByTitle('Add Set child to node 103').click();
83 | cy.findByTitle('Add Object child to node 104').click();
84 | cy.findByTitle('Add Array child to node 104').click();
85 | cy.findByTitle('Add Map child to node 104').click();
86 | cy.findByTitle('Add Set child to node 104').click();
87 |
88 | // Sets can't be modified, so just the others
89 | cy.findByTitle('Turn node 101 on').click();
90 | cy.findByTitle('Turn node 102 on').click();
91 | cy.findByTitle('Turn node 103 on').click();
92 | cy.findByTitle('Turn node 104 on').click();
93 | cy.findByTitle('Turn node 107 on').click();
94 | cy.findByTitle('Turn node 108 on').click();
95 | cy.findByTitle('Turn node 109 on').click();
96 | cy.findByTitle('Turn node 110 on').click();
97 | cy.findByTitle('Turn node 111 on').click();
98 | cy.findByTitle('Turn node 112 on').click();
99 | cy.findByTitle('Turn node 113 on').click();
100 | cy.findByTitle('Turn node 114 on').click();
101 | cy.findByTitle('Turn node 115 on').click();
102 | cy.findByTitle('Turn node 116 on').click();
103 | cy.findByTitle('Turn node 117 on').click();
104 | cy.findByTitle('Turn node 118 on').click();
105 |
106 | cy.findByPlaceholderText('Notes for node 101').type('101!');
107 | cy.findByPlaceholderText('Notes for node 102').type('102!');
108 | cy.findByPlaceholderText('Notes for node 103').type('103!');
109 | cy.findByPlaceholderText('Notes for node 104').type('104!');
110 | cy.findByPlaceholderText('Notes for node 107').type('107!');
111 | cy.findByPlaceholderText('Notes for node 108').type('108!');
112 | cy.findByPlaceholderText('Notes for node 109').type('109!');
113 | cy.findByPlaceholderText('Notes for node 110').type('110!');
114 | cy.findByPlaceholderText('Notes for node 111').type('111!');
115 | cy.findByPlaceholderText('Notes for node 112').type('112!');
116 | cy.findByPlaceholderText('Notes for node 113').type('113!');
117 | cy.findByPlaceholderText('Notes for node 114').type('114!');
118 | cy.findByPlaceholderText('Notes for node 115').type('115!');
119 | cy.findByPlaceholderText('Notes for node 116').type('116!');
120 | cy.findByPlaceholderText('Notes for node 117').type('117!');
121 | cy.findByPlaceholderText('Notes for node 118').type('118!');
122 |
123 | cy.findByTitle('Delete node 107').click();
124 | cy.findByTitle('Delete node 108').click();
125 | cy.findByTitle('Delete node 109').click();
126 | cy.findByTitle('Delete node 110').click();
127 | cy.findByTitle('Delete node 111').click();
128 | cy.findByTitle('Delete node 112').click();
129 | cy.findByTitle('Delete node 113').click();
130 | cy.findByTitle('Delete node 114').click();
131 | cy.findByTitle('Delete node 115').click();
132 | cy.findByTitle('Delete node 116').click();
133 | cy.findByTitle('Delete node 117').click();
134 | cy.findByTitle('Delete node 118').click();
135 |
136 | // Delete the top 4 last
137 | cy.findByTitle('Delete node 101').click();
138 | cy.findByTitle('Delete node 102').click();
139 | cy.findByTitle('Delete node 103').click();
140 | cy.findByTitle('Delete node 104').click();
141 |
142 | cy.findByTitle('Turn node 100 off').click();
143 | });
144 |
--------------------------------------------------------------------------------
/demo/cypress/integration/productPage.test.js:
--------------------------------------------------------------------------------
1 | it('Product page should work', () => {
2 | cy.visit('http://localhost:3000');
3 |
4 | // Go to the Products page
5 | cy.contains('Products').click();
6 | });
7 |
--------------------------------------------------------------------------------
/demo/cypress/integration/todoMvcPage.test.js:
--------------------------------------------------------------------------------
1 | it('Todo MVC page does the things it should do', () => {
2 | cy.visit('http://localhost:3000');
3 |
4 | // Go to the TodoMVC page
5 | cy.contains('Todo MVC').click();
6 |
7 | // Data from the API
8 | const task1Name = 'delectus aut autem';
9 | const task2Name = 'quis ut nam facilis et officia qui';
10 | const task3Name = 'fugiat veniam minus';
11 | const task4Name = 'et porro tempora';
12 | const task5Name =
13 | 'laboriosam mollitia et enim quasi adipisci quia provident illum';
14 |
15 | cy.findByText(task1Name).should('exist');
16 | cy.findByText(task2Name).should('exist');
17 | cy.findByText(task3Name).should('exist');
18 | cy.findByText(task4Name).should('exist');
19 | cy.findByText(task5Name).should('exist');
20 |
21 | cy.contains('4 items left');
22 |
23 | cy.findByText('Active').click();
24 | cy.findByText(task1Name).should('exist');
25 | cy.findByText(task2Name).should('exist');
26 | cy.findByText(task3Name).should('exist');
27 | cy.findByText(task4Name).should('not.exist');
28 | cy.findByText(task5Name).should('exist');
29 |
30 | cy.findByText('Completed').click();
31 | cy.findByText(task1Name).should('not.exist');
32 | cy.findByText(task2Name).should('not.exist');
33 | cy.findByText(task3Name).should('not.exist');
34 | cy.findByText(task4Name).should('exist');
35 | cy.findByText(task5Name).should('not.exist');
36 |
37 | cy.findByText('All').click();
38 | cy.findByText(task1Name).should('exist');
39 | cy.findByText(task2Name).should('exist');
40 | cy.findByText(task3Name).should('exist');
41 | cy.findByText(task4Name).should('exist');
42 | cy.findByText(task5Name).should('exist');
43 |
44 | // Mark them all complete
45 | cy.findByTestId('toggle-all').click();
46 | cy.contains('No items left');
47 |
48 | // Mark them all incomplete
49 | cy.findByTestId('toggle-all').click();
50 | cy.contains('5 items left');
51 |
52 | // Mark them all complete again
53 | cy.findByTestId('toggle-all').click();
54 |
55 | cy.findByText('Clear completed').click();
56 | cy.findByText(task1Name).should('not.exist');
57 | cy.findByText(task2Name).should('not.exist');
58 | cy.findByText(task3Name).should('not.exist');
59 | cy.findByText(task4Name).should('not.exist');
60 | cy.findByText(task5Name).should('not.exist');
61 |
62 | // Enter some new tasks
63 | cy.findByPlaceholderText('What needs to be done?').type('Task one{enter}');
64 | // Auto focus seems not to work, so we select the input again
65 | cy.findByPlaceholderText('What needs to be done?').type('Task two{enter}');
66 |
67 | cy.contains('2 items left');
68 |
69 | // Enter nothing
70 | cy.findByPlaceholderText('What needs to be done?').type('{enter}');
71 | cy.contains('2 items left');
72 |
73 | // Finish task one
74 | cy.findByText('Task one').click();
75 | cy.contains('1 item left');
76 |
77 | // Edit task two
78 | cy.findByText('Task two').dblclick();
79 | cy.findByDisplayValue('Task two').type(
80 | '{selectall}A new name for task two{enter}'
81 | );
82 |
83 | // The delete button only shows on hover, so we need to force that
84 | cy.findByTitle(`Delete 'A new name for task two'`).invoke('show').click();
85 | cy.findByText('A new name for task two').should('not.exist');
86 | cy.findByTitle(`Delete 'Task one'`).invoke('show').click();
87 | cy.findByText('Task one').should('not.exist');
88 | });
89 |
--------------------------------------------------------------------------------
/demo/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // ***********************************************************
4 | // This example plugins/index.js can be used to load plugins
5 | //
6 | // You can change the location of this file or turn off loading
7 | // the plugins file with the 'pluginsFile' configuration option.
8 | //
9 | // You can read more here:
10 | // https://on.cypress.io/plugins-guide
11 | // ***********************************************************
12 |
13 | // This function is called when a project is opened or re-opened (e.g. due to
14 | // the project's config changing)
15 |
16 | /**
17 | * @type {Cypress.PluginConfig}
18 | */
19 | module.exports = (on, config) => {
20 | // `on` is used to hook into various events Cypress emits
21 | // `config` is the resolved Cypress config
22 | };
23 |
--------------------------------------------------------------------------------
/demo/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/cypress/add-commands';
2 |
--------------------------------------------------------------------------------
/demo/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | import './commands';
2 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "react-scripts build",
7 | "lint": "eslint src --fix --cache --max-warnings=0",
8 | "serve": "ws --compress --port=3000 --directory=./build",
9 | "serve:root": "ws --compress --port=3000 --directory=../",
10 | "start": "react-scripts start",
11 | "test": "cypress open"
12 | },
13 | "browserslist": {
14 | "production": [
15 | ">0.2%",
16 | "not dead",
17 | "not op_mini all"
18 | ],
19 | "development": [
20 | "last 1 chrome version",
21 | "last 1 firefox version",
22 | "last 1 safari version"
23 | ]
24 | },
25 | "eslintConfig": {
26 | "extends": "react-app"
27 | },
28 | "dependencies": {
29 | "@material-ui/core": "4.9.7",
30 | "@material-ui/icons": "^4.9.1",
31 | "@material-ui/lab": "^4.0.0-alpha.46",
32 | "classnames": "2.2.6",
33 | "react": "16.13.1",
34 | "react-dom": "16.13.1",
35 | "react-recollect": "^5.2.2",
36 | "react-scripts": "3.4.1"
37 | },
38 | "devDependencies": {
39 | "@testing-library/cypress": "^5.3.0",
40 | "@testing-library/jest-dom": "5.1.1",
41 | "@testing-library/react": "10.0.1",
42 | "@testing-library/user-event": "10.0.0",
43 | "@types/testing-library__cypress": "^5.0.3",
44 | "cypress": "^4.1.0",
45 | "eslint": "6.8.0",
46 | "eslint-config-airbnb": "18.1.0",
47 | "eslint-config-prettier": "6.10.1",
48 | "eslint-plugin-cypress": "^2.10.3",
49 | "eslint-plugin-import": "2.20.1",
50 | "eslint-plugin-prettier": "3.1.2",
51 | "local-web-server": "^4.0.0",
52 | "prettier": "2.0.1"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/demo/public/browser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Recollect test
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/react-recollect/388e96311c19b136ed81810ffc856158053c19d5/demo/public/favicon.ico
--------------------------------------------------------------------------------
/demo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React Recollect Demo Site
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | height: 100%;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | .App-header {
8 | padding: 8px;
9 | flex: none;
10 | background: #455a64;
11 | color: white;
12 | font-size: 24px;
13 | text-align: center;
14 | font-weight: 300;
15 | }
16 |
17 | .App__page-wrapper {
18 | flex: 1;
19 | }
20 |
--------------------------------------------------------------------------------
/demo/src/App.js:
--------------------------------------------------------------------------------
1 | import AppBar from '@material-ui/core/AppBar';
2 | import Box from '@material-ui/core/Box';
3 | import ScopedCssBaseline from '@material-ui/core/ScopedCssBaseline';
4 | import Tab from '@material-ui/core/Tab';
5 | import Tabs from '@material-ui/core/Tabs';
6 | import Toolbar from '@material-ui/core/Toolbar';
7 | import Typography from '@material-ui/core/Typography';
8 | import React from 'react';
9 | import { collect } from 'react-recollect';
10 | import './App.css';
11 | import Updates from './pages/updates/Updates';
12 | import BigTree from './pages/bigTree/BigTree';
13 | import Products from './pages/products/Products';
14 | import TodoMvcPage from './pages/todomvc/components/TodoMvcPage';
15 | import { PAGES } from './shared/constants';
16 | import StorePropType from './propTypes/StorePropType';
17 |
18 | const App = ({ store }) => (
19 |
20 |
21 |
22 |
23 | React Recollect demo site
24 |
25 |
26 |
27 | {
30 | store.currentPage = value;
31 | }}
32 | >
33 | {Object.values(PAGES).map((page) => (
34 |
35 | ))}
36 |
37 |
38 |
39 |
40 |
41 | {store.currentPage === PAGES.UPDATES &&
}
42 |
43 | {store.currentPage === PAGES.PRODUCTS &&
}
44 |
45 | {store.currentPage === PAGES.BIG_TREE && (
46 |
47 |
48 |
49 | )}
50 |
51 | {store.currentPage === PAGES.TODO_MVC && (
52 |
53 | )}
54 |
55 | );
56 |
57 | App.propTypes = {
58 | store: StorePropType.isRequired,
59 | };
60 |
61 | export default collect(App);
62 |
--------------------------------------------------------------------------------
/demo/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --gold--500: #ff9800;
3 | --gray--100: #f5f5f5;
4 | --gray--050: #fafafa;
5 | --gray--300: #e0e0e0;
6 | }
7 |
8 | html {
9 | box-sizing: border-box;
10 | }
11 |
12 | *,
13 | *::before,
14 | *::after {
15 | box-sizing: inherit;
16 | }
17 |
18 | html,
19 | body,
20 | #root {
21 | height: 100%;
22 | }
23 |
24 | body {
25 | margin: 0;
26 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
27 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
28 | sans-serif;
29 | -webkit-font-smoothing: antialiased;
30 | -moz-osx-font-smoothing: grayscale;
31 | overflow-y: scroll;
32 | background: var(--gray--050);
33 | }
34 |
--------------------------------------------------------------------------------
/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { afterChange, initStore } from 'react-recollect';
4 | import App from './App';
5 | import './index.css';
6 | import { makeItem } from './pages/bigTree/utils';
7 | import loadProducts from './pages/products/loadProducts';
8 | import {
9 | LOAD_STATUSES,
10 | PAGES,
11 | PRODUCT_FILTER,
12 | TYPES,
13 | VISIBILITY_FILTERS,
14 | } from './shared/constants';
15 | import Theme from './shared/Theme';
16 |
17 | const currentPage = Object.values(PAGES).includes(localStorage.currentPage)
18 | ? localStorage.currentPage
19 | : PAGES.UPDATES;
20 |
21 | initStore({
22 | currentPage,
23 | loading: false,
24 | productPage: {
25 | filter: PRODUCT_FILTER.ALL,
26 | products: [],
27 | searchQuery: '',
28 | },
29 | batchUpdatePage: {
30 | text: '×',
31 | grid: {
32 | 100: { x: 0, y: 0 },
33 | 101: { x: 0, y: 0 },
34 | 102: { x: 0, y: 0 },
35 | 103: { x: 0, y: 0 },
36 | 104: { x: 0, y: 0 },
37 | 105: { x: 0, y: 0 },
38 | 106: { x: 0, y: 0 },
39 | 107: { x: 0, y: 0 },
40 | 108: { x: 0, y: 0 },
41 | 109: { x: 0, y: 0 },
42 | 110: { x: 0, y: 0 },
43 | 111: { x: 0, y: 0 },
44 | 112: { x: 0, y: 0 },
45 | 113: { x: 0, y: 0 },
46 | 114: { x: 0, y: 0 },
47 | 115: { x: 0, y: 0 },
48 | },
49 | },
50 | todoMvcPage: {
51 | loadStatus: LOAD_STATUSES.NOT_STARTED,
52 | todos: [],
53 | visibilityFilter: VISIBILITY_FILTERS.SHOW_ALL,
54 | },
55 | bigTreePage: {
56 | tree: makeItem(TYPES.OBJ, TYPES.ARR),
57 | expandedNodeIds: new Set(),
58 | },
59 | });
60 |
61 | loadProducts();
62 |
63 | afterChange((e) => {
64 | // When the currentPage changes, update localStorage
65 | if (e.changedProps.includes('currentPage')) {
66 | localStorage.currentPage = e.store.currentPage;
67 | }
68 | });
69 |
70 | ReactDOM.render(
71 |
72 |
73 | ,
74 | document.getElementById('root')
75 | );
76 |
--------------------------------------------------------------------------------
/demo/src/pages/bigTree/BigTree.js:
--------------------------------------------------------------------------------
1 | import Box from '@material-ui/core/Box';
2 | import Container from '@material-ui/core/Container';
3 | import Typography from '@material-ui/core/Typography';
4 | import ChevronRightIcon from '@material-ui/icons/ChevronRight';
5 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
6 | import TreeView from '@material-ui/lab/TreeView';
7 | import React from 'react';
8 | import { collect } from 'react-recollect';
9 | import StorePropType from '../../propTypes/StorePropType';
10 | import Item from './Item';
11 | import { stringifyPlus } from './utils';
12 |
13 | const BigTree = (props) => {
14 | const { expandedNodeIds, tree } = props.store.bigTreePage;
15 |
16 | return (
17 |
18 |
19 |
20 | This page is for internal testing and won‘t make a lot of sense to the
21 | casual observer.
22 |
23 |
24 |
25 | }
27 | defaultExpandIcon={}
28 | expanded={Array.from(expandedNodeIds)}
29 | >
30 |
31 |
32 |
33 |
34 |
40 | {stringifyPlus(tree)}
41 |
42 |
43 | );
44 | };
45 |
46 | BigTree.propTypes = {
47 | store: StorePropType.isRequired,
48 | };
49 |
50 | export default collect(BigTree);
51 |
--------------------------------------------------------------------------------
/demo/src/pages/bigTree/Item.js:
--------------------------------------------------------------------------------
1 | import Box from '@material-ui/core/Box';
2 | import Button from '@material-ui/core/Button';
3 | import ButtonGroup from '@material-ui/core/ButtonGroup';
4 | import IconButton from '@material-ui/core/IconButton';
5 | import Switch from '@material-ui/core/Switch';
6 | import { Delete } from '@material-ui/icons';
7 | import TreeItem from '@material-ui/lab/TreeItem';
8 | import React, { useRef } from 'react';
9 | import { batch, PropTypes } from 'react-recollect';
10 | import { TYPES } from '../../shared/constants';
11 | import { getChildrenAsArray } from './selectors';
12 | import { deleteChild } from './updaters';
13 | import { makeItem } from './utils';
14 |
15 | const Item = (props) => {
16 | const { item, parent } = props;
17 |
18 | // Items will re-render when the item updates or the parent item updates.
19 | const renderCountRef = useRef(0);
20 | renderCountRef.current++;
21 |
22 | const nodeId = item.id.toString();
23 | const isSetItem = item.parentType === TYPES.SET;
24 |
25 | const addChild = (newItem) => {
26 | if (item.childrenType === TYPES.OBJ) {
27 | item.children[newItem.id] = newItem;
28 | } else if (item.childrenType === TYPES.ARR) {
29 | item.children.push(newItem);
30 | } else if (item.childrenType === TYPES.MAP) {
31 | item.children.set(newItem.id, newItem);
32 | } else if (item.childrenType === TYPES.SET) {
33 | item.children.add(newItem);
34 | }
35 | };
36 |
37 | return (
38 |
42 |
43 | {
48 | // Don't expand/collapse if there's nothing to expand/collapse
49 | if (item.children) {
50 | if (props.expandedNodeIds.has(nodeId)) {
51 | props.expandedNodeIds.delete(nodeId);
52 | } else {
53 | props.expandedNodeIds.add(nodeId);
54 | }
55 | }
56 | }}
57 | >
58 | {props.item.name} [{renderCountRef.current}]
59 |
60 |
61 |
62 |
63 |
64 | {Object.entries(TYPES).map(([typeCode, typeString]) => (
65 |
80 | ))}
81 |
82 |
83 | {
90 | props.item.switchedOn = e.target.checked;
91 | }}
92 | />
93 |
94 | {
107 | props.item.notes = e.target.value;
108 | }}
109 | onClick={(e) => {
110 | // material-ui is stealing focus after the input is clicked.
111 | // This fix is extremely dodgy,
112 | // but for a test site it's good enough
113 | e.persist();
114 | setTimeout(() => {
115 | e.target.focus();
116 | });
117 | }}
118 | />
119 |
120 | props.onDeleteChild(parent, item)}
123 | title={`Delete node ${nodeId}`}
124 | >
125 |
126 |
127 |
128 |
129 | }
130 | >
131 | {item.children &&
132 | getChildrenAsArray(item).map((child) => (
133 |
140 | ))}
141 |
142 | );
143 | };
144 |
145 | const ItemPropType = PropTypes.shape({
146 | id: PropTypes.number.isRequired,
147 | name: PropTypes.string.isRequired,
148 | childrenType: PropTypes.oneOf(Object.values(TYPES)).isRequired,
149 | parentType: PropTypes.oneOf(Object.values(TYPES)).isRequired,
150 | children: PropTypes.any.isRequired,
151 | switchedOn: PropTypes.bool.isRequired,
152 | notes: PropTypes.string.isRequired,
153 | });
154 |
155 | Item.propTypes = {
156 | item: ItemPropType.isRequired,
157 | parent: ItemPropType,
158 | onDeleteChild: PropTypes.func,
159 | expandedNodeIds: PropTypes.instanceOf(Set).isRequired,
160 | };
161 |
162 | const ItemMemo = React.memo(Item);
163 |
164 | export default Item;
165 |
--------------------------------------------------------------------------------
/demo/src/pages/bigTree/selectors.js:
--------------------------------------------------------------------------------
1 | import { TYPES } from '../../shared/constants';
2 |
3 | export const getChildrenAsArray = (item) => {
4 | if (item.childrenType === TYPES.OBJ) {
5 | return Array.from(Object.values(item.children));
6 | }
7 | if (item.childrenType === TYPES.ARR) {
8 | return item.children;
9 | }
10 | if (item.childrenType === TYPES.MAP) {
11 | return Array.from(item.children.values());
12 | }
13 | if (item.childrenType === TYPES.SET) {
14 | return Array.from(item.children);
15 | }
16 |
17 | throw Error('Unknown type');
18 | };
19 |
--------------------------------------------------------------------------------
/demo/src/pages/bigTree/updaters.js:
--------------------------------------------------------------------------------
1 | import { TYPES } from '../../shared/constants';
2 |
3 | export const deleteChild = (parent, child) => {
4 | if (parent.childrenType === TYPES.OBJ) {
5 | delete parent.children[child.id];
6 | } else if (parent.childrenType === TYPES.ARR) {
7 | parent.children = parent.children.filter(
8 | (childItem) => childItem.id !== child.id
9 | );
10 | } else if (parent.childrenType === TYPES.MAP) {
11 | parent.children.delete(child.id);
12 | } else if (parent.childrenType === TYPES.SET) {
13 | parent.children.delete(child);
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/demo/src/pages/bigTree/utils.js:
--------------------------------------------------------------------------------
1 | import { TYPES } from '../../shared/constants';
2 |
3 | let id = 99;
4 |
5 | export const makeItem = (parentType, childrenType) => {
6 | id++;
7 |
8 | const item = {
9 | id,
10 | name: null,
11 | childrenType,
12 | parentType,
13 | switchedOn: false,
14 | notes: '',
15 | };
16 |
17 | if (parentType === TYPES.OBJ) {
18 | item.name = `An object prop with ${childrenType} children (${id})`;
19 | } else if (parentType === TYPES.ARR) {
20 | item.name = `An array item with ${childrenType} children (${id})`;
21 | } else if (parentType === TYPES.MAP) {
22 | item.name = `A Map entry with ${childrenType} children (${id})`;
23 | } else if (parentType === TYPES.SET) {
24 | item.name = `A Set entry with ${childrenType} children (${id})`;
25 | }
26 |
27 | if (childrenType === TYPES.OBJ) {
28 | item.children = {};
29 | } else if (childrenType === TYPES.ARR) {
30 | item.children = [];
31 | } else if (childrenType === TYPES.MAP) {
32 | item.children = new Map();
33 | } else if (childrenType === TYPES.SET) {
34 | item.children = new Set();
35 | }
36 |
37 | return item;
38 | };
39 |
40 | export const stringifyPlus = (data) =>
41 | JSON.stringify(
42 | data,
43 | (key, value) => {
44 | if (value instanceof Map) {
45 | return {
46 | '