(
(collection) => ({
key: 'my-collection',
primaryKey: 'key',
@@ -102,7 +114,7 @@ export const externalCreatedItem = new Item(MY_COLLECTION, {
console.log('Initial: myCollection ', clone(MY_COLLECTION));
-export const MY_EVENT = new Event<{ name: string }>(App, {
+export const MY_EVENT = createEvent<{ name: string }>({
delay: 3000,
key: 'myEvent',
});
diff --git a/examples/react/develop/functional-component-ts/yarn.lock b/examples/react/develop/functional-component-ts/yarn.lock
index b44f5283..d760f47c 100644
--- a/examples/react/develop/functional-component-ts/yarn.lock
+++ b/examples/react/develop/functional-component-ts/yarn.lock
@@ -3,36 +3,36 @@
"@agile-ts/api@file:.yalc/@agile-ts/api":
- version "0.0.19"
+ version "0.0.21"
dependencies:
- "@agile-ts/utils" "^0.0.5"
+ "@agile-ts/utils" "^0.0.7"
"@agile-ts/core@file:.yalc/@agile-ts/core":
- version "0.1.0"
+ version "0.2.0-alpha.4"
dependencies:
- "@agile-ts/utils" "^0.0.5"
+ "@agile-ts/utils" "^0.0.7"
"@agile-ts/event@file:.yalc/@agile-ts/event":
- version "0.0.8"
+ version "0.0.10"
"@agile-ts/logger@file:.yalc/@agile-ts/logger":
- version "0.0.5"
+ version "0.0.7"
dependencies:
- "@agile-ts/utils" "^0.0.5"
+ "@agile-ts/utils" "^0.0.7"
"@agile-ts/multieditor@file:.yalc/@agile-ts/multieditor":
- version "0.0.18"
+ version "0.0.20"
"@agile-ts/proxytree@file:.yalc/@agile-ts/proxytree":
- version "0.0.4"
+ version "0.0.5"
"@agile-ts/react@file:.yalc/@agile-ts/react":
- version "0.1.0"
+ version "0.2.0-alpha.1"
-"@agile-ts/utils@^0.0.5":
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.5.tgz#23cc83e60eb6b15734247fac1d77f1fd629ffdb6"
- integrity sha512-R86X9MjMty14eoQ4djulZSdHf9mIF9dPcj4g+SABqdA6AqbewS0/BQGNGR5p6gXhqc4+mT8rzkutywdPnMUNfA==
+"@agile-ts/utils@^0.0.7":
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.7.tgz#3dd1add6b9f63d0a5bf35e71f54ac46448ae047f"
+ integrity sha512-OviTDC+ZbfyiUx8Gy8veS6YymC/tT6UeP23nT8V0EQV4F2MmuWqZ2yiKk+AYxZx8h74Ey8BVEUX6/ntpxhSNPw==
"@babel/code-frame@7.8.3":
version "7.8.3"
diff --git a/examples/react/develop/multieditor-ts/src/core/agile.ts b/examples/react/develop/multieditor-ts/src/core/agile.ts
index e212a41d..d0e37e07 100644
--- a/examples/react/develop/multieditor-ts/src/core/agile.ts
+++ b/examples/react/develop/multieditor-ts/src/core/agile.ts
@@ -1,10 +1,6 @@
import { Agile, globalBind } from '@agile-ts/core';
-const App = new Agile({
- logConfig: {
- active: true,
- },
-});
+const App = new Agile();
export default App;
diff --git a/examples/react/develop/simple-counter/package.json b/examples/react/develop/simple-counter/package.json
index 8113e04e..0a89113d 100644
--- a/examples/react/develop/simple-counter/package.json
+++ b/examples/react/develop/simple-counter/package.json
@@ -2,25 +2,35 @@
"name": "counter",
"version": "0.1.0",
"private": true,
- "dependencies": {
- "@agile-ts/core": "^0.0.17",
- "@agile-ts/react": "^0.1.0",
- "react": "17.0.2",
- "react-dom": "17.0.2",
- "react-scripts": "4.0.0"
- },
- "devDependencies": {
- "source-map-explorer": "^2.5.2"
- },
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
- "analyze": "source-map-explorer 'build/static/js/*.js'",
+ "analyze": "yarn run build && source-map-explorer 'build/static/js/*.js'",
+ "analyze:webpack": "node scripts/analyze.js",
"install:dev:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/logger & yarn install",
"install:prod:agile": "yarn add @agile-ts/core @agile-ts/react @agile-ts/logger & yarn install"
},
+ "dependencies": {
+ "@agile-ts/core": "file:.yalc/@agile-ts/core",
+ "@agile-ts/logger": "file:.yalc/@agile-ts/logger",
+ "@agile-ts/react": "file:.yalc/@agile-ts/react",
+ "@hookstate/core": "^3.0.8",
+ "@reduxjs/toolkit": "^1.6.1",
+ "dotenv": "^10.0.0",
+ "jotai": "^1.2.2",
+ "nanostores": "^0.4.1",
+ "react": "17.0.2",
+ "react-dom": "17.0.2",
+ "react-redux": "^7.2.4",
+ "react-scripts": "4.0.0",
+ "recoil": "^0.4.0"
+ },
+ "devDependencies": {
+ "source-map-explorer": "^2.5.2",
+ "webpack-bundle-analyzer": "^4.4.2"
+ },
"browserslist": {
"production": [
">0.2%",
diff --git a/examples/react/develop/simple-counter/scripts/analyze.js b/examples/react/develop/simple-counter/scripts/analyze.js
new file mode 100644
index 00000000..59a7bb13
--- /dev/null
+++ b/examples/react/develop/simple-counter/scripts/analyze.js
@@ -0,0 +1,38 @@
+// https://medium.com/@hamidihamza/optimize-react-web-apps-with-webpack-bundle-analyzer-6ecb9f162c76
+// Note: Webpack Bundle Analyzer doesn't show accurately which bundles were tree shaken
+// (See: https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/161)
+
+const dotenv = require('dotenv');
+
+// Loads environment variables from the '.env' file
+dotenv.config();
+
+// https://nodejs.org/docs/latest/api/process.html#process_process_argv
+const isDev = process.argv.includes('--dev') || process.env.DEV === 'true';
+
+console.log(
+ `Start bundling a '${isDev ? 'development' : 'production'}' build!`
+);
+
+process.env.NODE_ENV = isDev ? 'development' : 'production';
+
+const webpack = require('webpack');
+const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
+ .BundleAnalyzerPlugin;
+const webpackConfigProd = require('react-scripts/config/webpack.config')(
+ 'production'
+);
+const webpackConfigDev = require('react-scripts/config/webpack.config')(
+ 'development'
+);
+
+// Add Bundle Analyzer Plugin to React webpack config
+webpackConfigProd.plugins.push(new BundleAnalyzerPlugin());
+webpackConfigDev.plugins.push(new BundleAnalyzerPlugin());
+
+// Build project with webpack
+webpack(isDev ? webpackConfigDev : webpackConfigProd, (err, stats) => {
+ if (err || stats.hasErrors()) {
+ console.error(err);
+ }
+});
diff --git a/examples/react/develop/simple-counter/src/index.js b/examples/react/develop/simple-counter/src/index.js
index dd1c38f9..6e17f3fd 100644
--- a/examples/react/develop/simple-counter/src/index.js
+++ b/examples/react/develop/simple-counter/src/index.js
@@ -1,11 +1,21 @@
import React from 'react';
import ReactDOM from 'react-dom';
-import App from './App';
+import * as Agile from './state-manager/Agile';
+import * as Jotai from './state-manager/Jotai';
+import * as NanoStores from './state-manager/NanoStores';
+import * as Recoil from './state-manager/Recoil';
+import * as ReduxToolkit from './state-manager/ReduxToolkit';
+import * as Hookstate from './state-manager/Hookstate';
ReactDOM.render(
-
+
+
+
+
+
+
,
document.getElementById('root')
);
diff --git a/examples/react/develop/simple-counter/src/App.js b/examples/react/develop/simple-counter/src/state-manager/Agile.js
similarity index 65%
rename from examples/react/develop/simple-counter/src/App.js
rename to examples/react/develop/simple-counter/src/state-manager/Agile.js
index 36c4a037..8ec4ad49 100644
--- a/examples/react/develop/simple-counter/src/App.js
+++ b/examples/react/develop/simple-counter/src/state-manager/Agile.js
@@ -1,11 +1,12 @@
-import { Agile } from '@agile-ts/core';
+import React from 'react';
+import { createLightState } from '@agile-ts/core';
import { useAgile } from '@agile-ts/react';
-const AgileApp = new Agile();
-
-const COUNTER_A = AgileApp.createState(1);
-const COUNTER_B = AgileApp.createState(2);
-const COUNTER_C = AgileApp.createState(3);
+// registerStorageManager(createStorageManager({ localStorage: true }));
+// const COUNTER_A = createState(1).persist('persistKey');
+const COUNTER_A = createLightState(1);
+const COUNTER_B = createLightState(2);
+const COUNTER_C = createLightState(3);
const CounterA = () => {
const count = useAgile(COUNTER_A);
@@ -34,8 +35,9 @@ const CounterC = () => {
);
};
-const App = () => (
+export const App = () => (
+
Agile
@@ -44,5 +46,3 @@ const App = () => (
);
-
-export default App;
diff --git a/examples/react/develop/simple-counter/src/state-manager/Hookstate.js b/examples/react/develop/simple-counter/src/state-manager/Hookstate.js
new file mode 100644
index 00000000..0d9b86d4
--- /dev/null
+++ b/examples/react/develop/simple-counter/src/state-manager/Hookstate.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import { createState, useState } from '@hookstate/core';
+
+const COUNTER_A = createState(1);
+const COUNTER_B = createState(2);
+const COUNTER_C = createState(3);
+
+const CounterA = () => {
+ const count = useState(COUNTER_A);
+ return (
+
+ A: {count.get()}{' '}
+
+
+ );
+};
+
+const CounterB = () => {
+ const count = useState(COUNTER_B);
+ return (
+
+ B: {count.get()}{' '}
+
+
+ );
+};
+
+const CounterC = () => {
+ const count = useState(COUNTER_C);
+ return (
+
+ C: {count.get()}{' '}
+
+
+ );
+};
+
+export const App = () => (
+
+
Hookstate
+
+
+
+
+
+
+
+);
diff --git a/examples/react/develop/simple-counter/src/state-manager/Jotai.js b/examples/react/develop/simple-counter/src/state-manager/Jotai.js
new file mode 100644
index 00000000..093bb7b1
--- /dev/null
+++ b/examples/react/develop/simple-counter/src/state-manager/Jotai.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import { atom, useAtom } from 'jotai';
+
+const COUNTER_A = atom(1);
+const COUNTER_B = atom(2);
+const COUNTER_C = atom(3);
+
+const CounterA = () => {
+ const [count, setCount] = useAtom(COUNTER_A);
+ return (
+
+ A: {count}
+
+ );
+};
+
+const CounterB = () => {
+ const [count, setCount] = useAtom(COUNTER_B);
+ return (
+
+ B: {count}
+
+ );
+};
+
+const CounterC = () => {
+ const [count, setCount] = useAtom(COUNTER_C);
+ return (
+
+ C: {count}
+
+ );
+};
+
+export const App = () => (
+
+);
diff --git a/examples/react/develop/simple-counter/src/state-manager/NanoStores.js b/examples/react/develop/simple-counter/src/state-manager/NanoStores.js
new file mode 100644
index 00000000..f62e8067
--- /dev/null
+++ b/examples/react/develop/simple-counter/src/state-manager/NanoStores.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import { createStore, update } from 'nanostores';
+import { useStore } from 'nanostores/react';
+
+const COUNTER_A = createStore(() => {
+ COUNTER_A.set(1);
+});
+const COUNTER_B = createStore(() => {
+ COUNTER_B.set(1);
+});
+const COUNTER_C = createStore(() => {
+ COUNTER_C.set(1);
+});
+
+const CounterA = () => {
+ const count = useStore(COUNTER_A);
+ return (
+
+ A: {count}{' '}
+
+
+ );
+};
+
+const CounterB = () => {
+ const count = useStore(COUNTER_B);
+ return (
+
+ B: {count}{' '}
+
+
+ );
+};
+
+const CounterC = () => {
+ const count = useStore(COUNTER_C);
+ return (
+
+ C: {count}{' '}
+
+
+ );
+};
+
+export const App = () => (
+
+
Nano Stores
+
+
+
+
+
+
+
+);
diff --git a/examples/react/develop/simple-counter/src/state-manager/Recoil.js b/examples/react/develop/simple-counter/src/state-manager/Recoil.js
new file mode 100644
index 00000000..04af2cd9
--- /dev/null
+++ b/examples/react/develop/simple-counter/src/state-manager/Recoil.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import { atom, RecoilRoot, useRecoilState } from 'recoil';
+
+const COUNTER_A = atom({
+ key: 'counterA',
+ default: 1,
+});
+const COUNTER_B = atom({ key: 'counterB', default: 2 });
+const COUNTER_C = atom({ key: 'counterC', default: 3 });
+
+const CounterA = () => {
+ const [count, setCount] = useRecoilState(COUNTER_A);
+ return (
+
+ A: {count}
+
+ );
+};
+
+const CounterB = () => {
+ const [count, setCount] = useRecoilState(COUNTER_B);
+ return (
+
+ B: {count}
+
+ );
+};
+
+const CounterC = () => {
+ const [count, setCount] = useRecoilState(COUNTER_C);
+ return (
+
+ C: {count}
+
+ );
+};
+
+export const App = () => (
+
+ Recoil
+
+
+
+
+
+
+
+);
diff --git a/examples/react/develop/simple-counter/src/state-manager/ReduxToolkit.js b/examples/react/develop/simple-counter/src/state-manager/ReduxToolkit.js
new file mode 100644
index 00000000..ae022656
--- /dev/null
+++ b/examples/react/develop/simple-counter/src/state-manager/ReduxToolkit.js
@@ -0,0 +1,98 @@
+import React from 'react';
+import { configureStore, createSlice } from '@reduxjs/toolkit';
+import { Provider, useSelector, useDispatch } from 'react-redux';
+
+const counterSlice_A = createSlice({
+ name: 'counterA',
+ initialState: {
+ value: 0,
+ },
+ reducers: {
+ increment: (state) => {
+ state.value += 1;
+ },
+ },
+});
+
+const counterSlice_B = createSlice({
+ name: 'counterB',
+ initialState: {
+ value: 0,
+ },
+ reducers: {
+ increment: (state) => {
+ state.value += 1;
+ },
+ },
+});
+
+const counterSlice_C = createSlice({
+ name: 'counterC',
+ initialState: {
+ value: 0,
+ },
+ reducers: {
+ increment: (state) => {
+ state.value += 1;
+ },
+ },
+});
+
+const store = configureStore({
+ reducer: {
+ counterA: counterSlice_A.reducer,
+ counterB: counterSlice_B.reducer,
+ counterC: counterSlice_C.reducer,
+ },
+});
+
+const CounterA = () => {
+ const count = useSelector((state) => state.counterA?.value);
+ const dispatch = useDispatch();
+ return (
+
+ A: {count}{' '}
+
+
+ );
+};
+
+const CounterB = () => {
+ const count = useSelector((state) => state.counterB?.value);
+ const dispatch = useDispatch();
+ return (
+
+ B: {count}{' '}
+
+
+ );
+};
+
+const CounterC = () => {
+ const count = useSelector((state) => state.counterC?.value);
+ const dispatch = useDispatch();
+ return (
+
+ C: {count}{' '}
+
+
+ );
+};
+
+export const App = () => (
+
+ Redux Toolkit
+
+
+
+
+
+
+
+);
diff --git a/examples/react/develop/simple-counter/yarn.lock b/examples/react/develop/simple-counter/yarn.lock
index e0c5ba2c..2505ba28 100644
--- a/examples/react/develop/simple-counter/yarn.lock
+++ b/examples/react/develop/simple-counter/yarn.lock
@@ -2,37 +2,23 @@
# yarn lockfile v1
-"@agile-ts/core@^0.0.17":
- version "0.0.17"
- resolved "https://registry.yarnpkg.com/@agile-ts/core/-/core-0.0.17.tgz#410ab31ff6279567ff0266a5a55818744598f2c2"
- integrity sha512-EVWqf6PkBwDS/gdTTVwo9juyGPrnnKlq8dna3diXGZdDpwEzMc09nGCmLThYM5sEkDQGzir6enn3Oo2l+7Zp2Q==
+"@agile-ts/core@file:.yalc/@agile-ts/core":
+ version "0.2.0-alpha.3"
dependencies:
- "@agile-ts/logger" "^0.0.4"
- "@agile-ts/utils" "^0.0.4"
+ "@agile-ts/utils" "^0.0.7"
-"@agile-ts/logger@^0.0.4":
- version "0.0.4"
- resolved "https://registry.yarnpkg.com/@agile-ts/logger/-/logger-0.0.4.tgz#7f4d82ef8f03b13089af0878c360575c43f0962d"
- integrity sha512-qm0obAKqJMaPKM+c76gktRXyw3OL1v39AnhMZ0FBGwJqHWU+fLRkCzlQwjaROCr3F1XP01Lc/Ls3efF0WzyEPw==
+"@agile-ts/logger@file:.yalc/@agile-ts/logger":
+ version "0.0.7"
dependencies:
- "@agile-ts/utils" "^0.0.4"
-
-"@agile-ts/proxytree@^0.0.3":
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/@agile-ts/proxytree/-/proxytree-0.0.3.tgz#e3dacab123a311f2f0d4a0369793fe90fdab7569"
- integrity sha512-auO6trCo7ivLJYuLjxrnK4xuUTangVPTq8UuOMTlGbJFjmb8PLEkaXuRoVGSzv9jsT2FeS7KsP7Fs+yvv0WPdg==
+ "@agile-ts/utils" "^0.0.7"
-"@agile-ts/react@^0.0.18":
- version "0.0.18"
- resolved "https://registry.yarnpkg.com/@agile-ts/react/-/react-0.0.18.tgz#db1a617ad535f7a70254d62980d97350d4a85718"
- integrity sha512-K2FO3Odqaw/XkU3DO/mWSLkxLn45W7pXk/UlZl5E/CQPFFWlWsjuxtH/C/kfK+E6rnaNoToTjGscmcoeN/bLjQ==
- dependencies:
- "@agile-ts/proxytree" "^0.0.3"
+"@agile-ts/react@file:.yalc/@agile-ts/react":
+ version "0.1.2"
-"@agile-ts/utils@^0.0.4":
- version "0.0.4"
- resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.4.tgz#66e9536e561796489a37155da6b74ce2dc482697"
- integrity sha512-GiZyTYmCm4j2N57oDjeMuPpfQdgn9clb0Cxpfuwi2Bq5T/KPXlaROLsVGwHLjwwT+NX7xxr5qNJH8pZTnHnYRQ==
+"@agile-ts/utils@^0.0.7":
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.7.tgz#3dd1add6b9f63d0a5bf35e71f54ac46448ae047f"
+ integrity sha512-OviTDC+ZbfyiUx8Gy8veS6YymC/tT6UeP23nT8V0EQV4F2MmuWqZ2yiKk+AYxZx8h74Ey8BVEUX6/ntpxhSNPw==
"@babel/code-frame@7.10.4":
version "7.10.4"
@@ -1664,6 +1650,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2":
+ version "7.14.8"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446"
+ integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.3.3":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327"
@@ -1790,6 +1783,11 @@
dependencies:
"@hapi/hoek" "^8.3.0"
+"@hookstate/core@^3.0.8":
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-3.0.8.tgz#d6838153d6d43c2f35cfca475c31248192564e62"
+ integrity sha512-blQagGIVIbNoUiNCRrvaXqFmUe7WGMY35ok/LENfl2pcIsLBjkreYIZiaSFi83tkycwq7ZOmcQz/R1nvLKhH8w==
+
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -2018,6 +2016,21 @@
schema-utils "^2.6.5"
source-map "^0.7.3"
+"@polka/url@^1.0.0-next.20":
+ version "1.0.0-next.20"
+ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.20.tgz#111b5db0f501aa89b05076fa31f0ea0e0c292cd3"
+ integrity sha512-88p7+M0QGxKpmnkfXjS4V26AnoC/eiqZutE8GLdaI5X12NY75bXSdTY9NkmYb2Xyk1O+MmkuO6Frmsj84V6I8Q==
+
+"@reduxjs/toolkit@^1.6.1":
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.1.tgz#7bc83b47352a663bf28db01e79d17ba54b98ade9"
+ integrity sha512-pa3nqclCJaZPAyBhruQtiRwtTjottRrVJqziVZcWzI73i6L3miLTtUyWfauwv08HWtiXLx1xGyGt+yLFfW/d0A==
+ dependencies:
+ immer "^9.0.1"
+ redux "^4.1.0"
+ redux-thunk "^2.3.0"
+ reselect "^4.0.0"
+
"@rollup/plugin-node-resolve@^7.1.1":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz#80de384edfbd7bfc9101164910f86078151a3eca"
@@ -2242,6 +2255,14 @@
dependencies:
"@types/node" "*"
+"@types/hoist-non-react-statics@^3.3.0":
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
+ integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+ dependencies:
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+
"@types/html-minifier-terser@^5.0.0":
version "5.1.1"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#3c9ee980f1a10d6021ae6632ca3e79ca2ec4fb50"
@@ -2301,11 +2322,35 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.1.tgz#374e31645d58cb18a07b3ecd8e9dede4deb2cccd"
integrity sha512-DxZZbyMAM9GWEzXL+BMZROWz9oo6A9EilwwOMET2UVu2uZTqMWS5S69KVtuVKaRjCUpcrOXRalet86/OpG4kqw==
+"@types/prop-types@*":
+ version "15.7.4"
+ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
+ integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
+
"@types/q@^1.5.1":
version "1.5.4"
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
+"@types/react-redux@^7.1.16":
+ version "7.1.18"
+ resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.18.tgz#2bf8fd56ebaae679a90ebffe48ff73717c438e04"
+ integrity sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ==
+ dependencies:
+ "@types/hoist-non-react-statics" "^3.3.0"
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+ redux "^4.0.0"
+
+"@types/react@*":
+ version "17.0.15"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.15.tgz#c7533dc38025677e312606502df7656a6ea626d0"
+ integrity sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/scheduler" "*"
+ csstype "^3.0.2"
+
"@types/resolve@0.0.8":
version "0.0.8"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
@@ -2313,6 +2358,11 @@
dependencies:
"@types/node" "*"
+"@types/scheduler@*":
+ version "0.16.2"
+ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
+ integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
+
"@types/source-list-map@*":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
@@ -2661,6 +2711,11 @@ acorn-walk@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
+acorn-walk@^8.0.0:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.1.1.tgz#3ddab7f84e4a7e2313f6c414c5b7dac85f4e3ebc"
+ integrity sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==
+
acorn@^6.4.1:
version "6.4.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
@@ -2671,6 +2726,11 @@ acorn@^7.1.0, acorn@^7.1.1, acorn@^7.4.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
+acorn@^8.0.4:
+ version "8.4.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
+ integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
+
address@1.1.2, address@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6"
@@ -3916,6 +3976,11 @@ commander@^4.1.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
+commander@^6.2.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
+ integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
+
common-tags@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937"
@@ -4396,6 +4461,11 @@ cssstyle@^2.2.0:
dependencies:
cssom "~0.3.6"
+csstype@^3.0.2:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
+ integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
+
cyclist@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
@@ -4724,6 +4794,11 @@ dotenv@8.2.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
+dotenv@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
+ integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
+
duplexer@^0.1.1, duplexer@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
@@ -5897,6 +5972,11 @@ gzip-size@^6.0.0:
dependencies:
duplexer "^0.1.2"
+hamt_plus@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/hamt_plus/-/hamt_plus-1.0.2.tgz#e21c252968c7e33b20f6a1b094cd85787a265601"
+ integrity sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE=
+
handle-thing@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
@@ -6009,6 +6089,13 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
+ integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+ dependencies:
+ react-is "^16.7.0"
+
hoopy@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d"
@@ -6227,6 +6314,11 @@ immer@8.0.1:
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
+immer@^9.0.1:
+ version "9.0.5"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.5.tgz#a7154f34fe7064f15f00554cc94c66cc0bf453ec"
+ integrity sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ==
+
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -7192,6 +7284,11 @@ jest@26.6.0:
import-local "^3.0.2"
jest-cli "^26.6.0"
+jotai@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.2.2.tgz#631fd7ad44e9ac26cdf9874d52282c1cfe032807"
+ integrity sha512-iqkkUdWsH2Mk4HY1biba/8kA77+8liVBy8E0d8Nce29qow4h9mzdDhpTasAruuFYPycw6JvfZgL5RB0JJuIZjw==
+
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -7721,7 +7818,7 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-mime@^2.4.4:
+mime@^2.3.1, mime@^2.4.4:
version "2.5.2"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
@@ -7907,6 +8004,11 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
+nanostores@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/nanostores/-/nanostores-0.4.1.tgz#61c7a4aadca063bd1992e80f9a0e8b20d55dee0f"
+ integrity sha512-GfrWjngWVTBa3YSPijFGrxyYYEE017ONA/t6d6X6G99ccfED+eZEIciH+KKCqbvZhvRbqGH0aHDhRYNBwFw8hg==
+
native-url@^0.2.6:
version "0.2.6"
resolved "https://registry.yarnpkg.com/native-url/-/native-url-0.2.6.tgz#ca1258f5ace169c716ff44eccbddb674e10399ae"
@@ -8225,6 +8327,11 @@ open@^7.0.2, open@^7.3.1:
is-docker "^2.0.0"
is-wsl "^2.1.1"
+opener@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
+ integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
+
opn@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
@@ -9570,7 +9677,7 @@ react-error-overlay@^6.0.9:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
-react-is@^16.8.1:
+react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -9580,6 +9687,18 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
+react-redux@^7.2.4:
+ version "7.2.4"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
+ integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==
+ dependencies:
+ "@babel/runtime" "^7.12.1"
+ "@types/react-redux" "^7.1.16"
+ hoist-non-react-statics "^3.3.2"
+ loose-envify "^1.4.0"
+ prop-types "^15.7.2"
+ react-is "^16.13.1"
+
react-refresh@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
@@ -9732,6 +9851,13 @@ readdirp@~3.5.0:
dependencies:
picomatch "^2.2.1"
+recoil@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.4.0.tgz#2a933ba7c043cbf6f50f52da0828a879c7cd7d69"
+ integrity sha512-FZ2ljI4ldZU820V0APbKOtS4bPwPJHvpDBQEl+Cf47DMaM35wuLXl2u37E0TSgdvKAZBOUsIwcBnzE+ncODRxQ==
+ dependencies:
+ hamt_plus "1.0.2"
+
recursive-readdir@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
@@ -9739,6 +9865,18 @@ recursive-readdir@2.2.2:
dependencies:
minimatch "3.0.4"
+redux-thunk@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
+ integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
+
+redux@^4.0.0, redux@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.1.tgz#76f1c439bb42043f985fbd9bf21990e60bd67f47"
+ integrity sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==
+ dependencies:
+ "@babel/runtime" "^7.9.2"
+
regenerate-unicode-properties@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
@@ -9911,6 +10049,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+reselect@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
+ integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
+
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@@ -10393,6 +10536,15 @@ simple-swizzle@^0.2.2:
dependencies:
is-arrayish "^0.3.1"
+sirv@^1.0.7:
+ version "1.0.17"
+ resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.17.tgz#86e2c63c612da5a1dace1c16c46f524aaa26ac45"
+ integrity sha512-qx9go5yraB7ekT7bCMqUHJ5jEaOC/GXBxUWv+jeWnb7WzHUFdcQPGWk7YmAwFBaQBrogpuSqd/azbC2lZRqqmw==
+ dependencies:
+ "@polka/url" "^1.0.0-next.20"
+ mime "^2.3.1"
+ totalist "^1.0.0"
+
sisteransi@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@@ -11112,6 +11264,11 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+totalist@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
+ integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==
+
tough-cookie@^2.3.3, tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
@@ -11557,6 +11714,21 @@ webidl-conversions@^6.1.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
+webpack-bundle-analyzer@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.2.tgz#39898cf6200178240910d629705f0f3493f7d666"
+ integrity sha512-PIagMYhlEzFfhMYOzs5gFT55DkUdkyrJi/SxJp8EF3YMWhS+T9vvs2EoTetpk5qb6VsCq02eXTlRDOydRhDFAQ==
+ dependencies:
+ acorn "^8.0.4"
+ acorn-walk "^8.0.0"
+ chalk "^4.1.0"
+ commander "^6.2.0"
+ gzip-size "^6.0.0"
+ lodash "^4.17.20"
+ opener "^1.5.2"
+ sirv "^1.0.7"
+ ws "^7.3.1"
+
webpack-dev-middleware@^3.7.2:
version "3.7.3"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5"
@@ -11954,6 +12126,11 @@ ws@^7.2.3:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd"
integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==
+ws@^7.3.1:
+ version "7.5.4"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.4.tgz#56bfa20b167427e138a7795de68d134fe92e21f9"
+ integrity sha512-zP9z6GXm6zC27YtspwH99T3qTG7bBFv2VIkeHstMLrLlDJuzA7tQ5ls3OJ1hOGGCzTQPniNJoHXIAOS0Jljohg==
+
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
diff --git a/examples/react/develop/simple-todo-list/.env b/examples/react/develop/simple-todo-list/.env
new file mode 100644
index 00000000..6f809cc2
--- /dev/null
+++ b/examples/react/develop/simple-todo-list/.env
@@ -0,0 +1 @@
+SKIP_PREFLIGHT_CHECK=true
diff --git a/examples/react/develop/simple-todo-list/.gitignore b/examples/react/develop/simple-todo-list/.gitignore
new file mode 100644
index 00000000..4d29575d
--- /dev/null
+++ b/examples/react/develop/simple-todo-list/.gitignore
@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/examples/react/develop/simple-todo-list/README.md b/examples/react/develop/simple-todo-list/README.md
new file mode 100644
index 00000000..02aac3f6
--- /dev/null
+++ b/examples/react/develop/simple-todo-list/README.md
@@ -0,0 +1,70 @@
+# Getting Started with Create React App
+
+This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `yarn start`
+
+Runs the app in the development mode.\
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+
+The page will reload if you make edits.\
+You will also see any lint errors in the console.
+
+### `yarn test`
+
+Launches the test runner in the interactive watch mode.\
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+
+### `yarn build`
+
+Builds the app for production to the `build` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+
+### `yarn eject`
+
+**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
+
+If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+
+Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
+
+You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
+
+## Learn More
+
+You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+
+To learn React, check out the [React documentation](https://reactjs.org/).
+
+### Code Splitting
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
+
+### Analyzing the Bundle Size
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
+
+### Making a Progressive Web App
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
+
+### Advanced Configuration
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
+
+### Deployment
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
+
+### `yarn build` fails to minify
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
diff --git a/examples/react/develop/simple-todo-list/package.json b/examples/react/develop/simple-todo-list/package.json
new file mode 100644
index 00000000..c034b229
--- /dev/null
+++ b/examples/react/develop/simple-todo-list/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "simple-todo-list",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@agile-ts/core": "file:.yalc/@agile-ts/core",
+ "@agile-ts/logger": "file:.yalc/@agile-ts/logger",
+ "@agile-ts/react": "file:.yalc/@agile-ts/react",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2",
+ "react-scripts": "4.0.3"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject",
+ "install:dev:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/logger & yarn install",
+ "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react @agile-ts/logger & yarn install"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/examples/react/develop/simple-todo-list/public/favicon.ico b/examples/react/develop/simple-todo-list/public/favicon.ico
new file mode 100644
index 00000000..a11777cc
Binary files /dev/null and b/examples/react/develop/simple-todo-list/public/favicon.ico differ
diff --git a/examples/react/develop/simple-todo-list/public/index.html b/examples/react/develop/simple-todo-list/public/index.html
new file mode 100644
index 00000000..aa069f27
--- /dev/null
+++ b/examples/react/develop/simple-todo-list/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/examples/react/develop/simple-todo-list/public/logo192.png b/examples/react/develop/simple-todo-list/public/logo192.png
new file mode 100644
index 00000000..fc44b0a3
Binary files /dev/null and b/examples/react/develop/simple-todo-list/public/logo192.png differ
diff --git a/examples/react/develop/simple-todo-list/public/logo512.png b/examples/react/develop/simple-todo-list/public/logo512.png
new file mode 100644
index 00000000..a4e47a65
Binary files /dev/null and b/examples/react/develop/simple-todo-list/public/logo512.png differ
diff --git a/examples/react/develop/simple-todo-list/public/manifest.json b/examples/react/develop/simple-todo-list/public/manifest.json
new file mode 100644
index 00000000..080d6c77
--- /dev/null
+++ b/examples/react/develop/simple-todo-list/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/examples/react/develop/simple-todo-list/public/robots.txt b/examples/react/develop/simple-todo-list/public/robots.txt
new file mode 100644
index 00000000..e9e57dc4
--- /dev/null
+++ b/examples/react/develop/simple-todo-list/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/examples/react/develop/simple-todo-list/src/App.js b/examples/react/develop/simple-todo-list/src/App.js
new file mode 100644
index 00000000..7844b8c2
--- /dev/null
+++ b/examples/react/develop/simple-todo-list/src/App.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import { generateId } from '@agile-ts/core';
+import { useAgile } from '@agile-ts/react';
+import { TODOS } from './core';
+
+const App = () => {
+ // With the 'useAgile' Hook we bind our first Collection to the 'RandomComponent' for reactivity
+ const todos = useAgile(TODOS);
+
+ // Current Input of Name Form
+ const [currentInput, setCurrentInput] = React.useState('');
+
+ return (
+
+
Simple TODOS
+
{
+ setCurrentInput(event.target.value);
+ }}
+ />
+
+ {todos.map((value) => (
+
+
{value.name}
+
+
+ ))}
+
+ );
+};
+
+export default App;
diff --git a/examples/react/develop/simple-todo-list/src/core.js b/examples/react/develop/simple-todo-list/src/core.js
new file mode 100644
index 00000000..881290a6
--- /dev/null
+++ b/examples/react/develop/simple-todo-list/src/core.js
@@ -0,0 +1,11 @@
+import { createCollection, globalBind } from '@agile-ts/core';
+import { assignSharedAgileLoggerConfig, Logger } from '@agile-ts/logger';
+
+assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG });
+
+// Create Collection
+export const TODOS = createCollection({
+ initialData: [{ id: 1, name: 'Clean Bathroom' }],
+}).persist('todos'); // persist does store the Collection in the Local Storage
+
+globalBind('__core__', { TODOS });
diff --git a/examples/react/develop/simple-todo-list/src/index.js b/examples/react/develop/simple-todo-list/src/index.js
new file mode 100644
index 00000000..c1f31c5f
--- /dev/null
+++ b/examples/react/develop/simple-todo-list/src/index.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById('root')
+);
diff --git a/examples/react/develop/tree-shaking/package.json b/examples/react/develop/tree-shaking/package.json
new file mode 100644
index 00000000..05202eaf
--- /dev/null
+++ b/examples/react/develop/tree-shaking/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "tree-shaking",
+ "version": "1.0.0",
+ "main": "src/index.js",
+ "license": "MIT",
+ "scripts": {
+ "build": "npx webpack --config webpack.config.js",
+ "install:dev:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/logger & yarn install",
+ "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react @agile-ts/logger & yarn install"
+ },
+ "devDependencies": {
+ "babel-core": "^6.26.3",
+ "babel-loader": "^8.2.2",
+ "babel-preset-env": "^1.7.0",
+ "babel-preset-react": "^6.24.1",
+ "webpack": "^5.51.1",
+ "webpack-cli": "^4.8.0"
+ },
+ "dependencies": {
+ "@agile-ts/core": "file:.yalc/@agile-ts/core",
+ "@agile-ts/logger": "file:.yalc/@agile-ts/logger",
+ "@agile-ts/react": "file:.yalc/@agile-ts/react",
+ "react": "^17.0.2"
+ }
+}
diff --git a/examples/react/develop/tree-shaking/src/App.jsx b/examples/react/develop/tree-shaking/src/App.jsx
new file mode 100644
index 00000000..ac5157ce
--- /dev/null
+++ b/examples/react/develop/tree-shaking/src/App.jsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+export const FooComponent = ({ name }) => (
+ Hello from FooComponent, {name ?? 'unknown'}!
+);
+
+export const BarComponent = ({ name }) => (
+ Hello from BarComponent, {name ?? 'unknown'}!
+);
diff --git a/examples/react/develop/tree-shaking/src/core.js b/examples/react/develop/tree-shaking/src/core.js
new file mode 100644
index 00000000..6dcfb7c0
--- /dev/null
+++ b/examples/react/develop/tree-shaking/src/core.js
@@ -0,0 +1,3 @@
+import { createLightState } from '@agile-ts/core';
+
+export const MY_STATE = createLightState('jeff');
diff --git a/examples/react/develop/tree-shaking/src/index.js b/examples/react/develop/tree-shaking/src/index.js
new file mode 100644
index 00000000..7fb253f5
--- /dev/null
+++ b/examples/react/develop/tree-shaking/src/index.js
@@ -0,0 +1,9 @@
+import { BarComponent } from './App';
+import { MY_STATE } from './core';
+
+MY_STATE.set('jeff');
+
+// we could do something with BarComponent here,
+// like ReactDOM.render, but let's just dump it to
+// console for simplicity
+console.log(BarComponent);
diff --git a/examples/react/develop/tree-shaking/webpack.config.js b/examples/react/develop/tree-shaking/webpack.config.js
new file mode 100644
index 00000000..1b3fb79c
--- /dev/null
+++ b/examples/react/develop/tree-shaking/webpack.config.js
@@ -0,0 +1,31 @@
+const path = require('path');
+
+module.exports = {
+ mode: 'development',
+ entry: './src/index.js',
+ output: {
+ path: path.resolve(__dirname, 'dist'),
+ filename: 'app.js',
+ },
+ resolve: { extensions: ['.js', '.jsx'] },
+ module: {
+ rules: [
+ {
+ test: /\.jsx?$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: [['@babel/env', { modules: false }], '@babel/react'],
+ },
+ },
+ },
+ ],
+ },
+ optimization: {
+ usedExports: true,
+ innerGraph: true,
+ sideEffects: true,
+ },
+ devtool: false,
+};
diff --git a/examples/react/release/boxes/package.json b/examples/react/release/boxes/package.json
index ff3c03f1..078a67a8 100644
--- a/examples/react/release/boxes/package.json
+++ b/examples/react/release/boxes/package.json
@@ -3,10 +3,10 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@agile-ts/core": "^0.1.2",
- "@agile-ts/logger": "^0.0.7",
- "@agile-ts/proxytree": "^0.0.5",
- "@agile-ts/react": "^0.1.2",
+ "@agile-ts/core": "file:.yalc/@agile-ts/core",
+ "@agile-ts/logger": "file:.yalc/@agile-ts/logger",
+ "@agile-ts/proxytree": "file:.yalc/@agile-ts/proxytree",
+ "@agile-ts/react": "file:.yalc/@agile-ts/react",
"lodash": "^4.17.21",
"react": "^17.0.2",
"react-dom": "^17.0.2",
diff --git a/examples/react/release/boxes/src/core/entities/ui/ui.actions.ts b/examples/react/release/boxes/src/core/entities/ui/ui.actions.ts
index c3d537cd..2fe86360 100644
--- a/examples/react/release/boxes/src/core/entities/ui/ui.actions.ts
+++ b/examples/react/release/boxes/src/core/entities/ui/ui.actions.ts
@@ -8,7 +8,6 @@ import {
SCREEN,
} from './ui.controller';
import core from '../../index';
-import { copy } from '@agile-ts/utils';
export const addDefaultElement = (image: boolean = false) => {
if (image) addElement(defaultElementStyle, getRandomImage());
diff --git a/examples/react/release/boxes/src/core/entities/ui/ui.controller.ts b/examples/react/release/boxes/src/core/entities/ui/ui.controller.ts
index c92dcf93..071de3b3 100644
--- a/examples/react/release/boxes/src/core/entities/ui/ui.controller.ts
+++ b/examples/react/release/boxes/src/core/entities/ui/ui.controller.ts
@@ -1,22 +1,22 @@
-import { App } from '../../app';
import {
CanvasInterface,
ElementInterface,
ScreenInterface,
} from './ui.interfaces';
+import { createCollection, createState } from '@agile-ts/core';
export const defaultElementStyle = {
position: { top: 0, left: 0 },
size: { width: 200, height: 200 },
};
-export const CANVAS = App.createState({
+export const CANVAS = createState({
width: 5000,
height: 5000,
});
-export const SCREEN = App.createState({ width: 0, height: 0 });
+export const SCREEN = createState({ width: 0, height: 0 });
-export const ELEMENTS = App.createCollection();
+export const ELEMENTS = createCollection();
export const SELECTED_ELEMENT = ELEMENTS.createSelector(
'selectedElement',
diff --git a/examples/react/release/boxes/yarn.lock b/examples/react/release/boxes/yarn.lock
index 18e63613..07d40055 100644
--- a/examples/react/release/boxes/yarn.lock
+++ b/examples/react/release/boxes/yarn.lock
@@ -2,29 +2,21 @@
# yarn lockfile v1
-"@agile-ts/core@^0.1.2":
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/@agile-ts/core/-/core-0.1.2.tgz#5a3974ba0c57a51a19bcdf81b2055e091c884f5e"
- integrity sha512-9031MGUrPpg/ZL1ErpwUlHX751HKEtOfbc5Ae7W7x/POGH89Gka09hMAhqQlDrKF2+olVs3sf6PAsAHRv6paGw==
+"@agile-ts/core@file:.yalc/@agile-ts/core":
+ version "0.2.0-alpha.4"
dependencies:
"@agile-ts/utils" "^0.0.7"
-"@agile-ts/logger@^0.0.7":
+"@agile-ts/logger@file:.yalc/@agile-ts/logger":
version "0.0.7"
- resolved "https://registry.yarnpkg.com/@agile-ts/logger/-/logger-0.0.7.tgz#9e89e8d80f80a46901a508432696860f88d5e878"
- integrity sha512-6N9qyooo/a7ibyl9L7HnBX0LyMlSwaEYgObYs58KzR19JGF00PX/sUFfQAVplXXsMfT/8HvLyI+4TssmyI6DdQ==
dependencies:
"@agile-ts/utils" "^0.0.7"
-"@agile-ts/proxytree@^0.0.5":
+"@agile-ts/proxytree@file:.yalc/@agile-ts/proxytree":
version "0.0.5"
- resolved "https://registry.yarnpkg.com/@agile-ts/proxytree/-/proxytree-0.0.5.tgz#81c40970707271822a176ee59f93b9230df6311d"
- integrity sha512-KODknVD30ld9xPCyt0UCf0yGcroy/0CHEncAdmTFwEvDSMipMaqFQRsAYZ0tgB4bMfFzab40aUmYTK8XDkwdHw==
-"@agile-ts/react@^0.1.2":
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/@agile-ts/react/-/react-0.1.2.tgz#d07f6b935d9322cd60d2e9e3871da554b04460af"
- integrity sha512-W4u2+X6KCeXPdkjit/NsMJG5nBsa7dNFaEzyfTsp5Cqbs99zLqY6dO8LUIYyhRt/+HBvEW9o64i/6Kqd59WM1Q==
+"@agile-ts/react@file:.yalc/@agile-ts/react":
+ version "0.2.0-alpha.1"
"@agile-ts/utils@^0.0.7":
version "0.0.7"
diff --git a/examples/react/release/stopwatch-query-url/src/core/index.ts b/examples/react/release/stopwatch-query-url/src/core/index.ts
index b9e3cf5e..8ec09cb5 100644
--- a/examples/react/release/stopwatch-query-url/src/core/index.ts
+++ b/examples/react/release/stopwatch-query-url/src/core/index.ts
@@ -1,4 +1,9 @@
-import { createState, globalBind, shared } from '@agile-ts/core';
+import {
+ createState,
+ globalBind,
+ createStorage,
+ getStorageManager,
+} from '@agile-ts/core';
import queryString from 'query-string';
export type StopwatchStateType =
@@ -7,7 +12,7 @@ export type StopwatchStateType =
| 'initial'; // Stopwatch is reset
// Create Query Storage to store the State in the query (url)
-const queryUrlStorage = shared.createStorage({
+const queryUrlStorage = createStorage({
key: 'query-url',
methods: {
set: (key, value) => {
@@ -30,7 +35,7 @@ const queryUrlStorage = shared.createStorage({
});
// Register Query Storage to the shared Agile Instance and set it as default
-shared.registerStorage(queryUrlStorage, { default: true });
+getStorageManager().register(queryUrlStorage, { default: true });
// State to keep track of the current time of the Stopwatch
const TIME = createState(
diff --git a/examples/vue/develop/my-project/src/core.js b/examples/vue/develop/my-project/src/core.js
index 0512ec9b..8606056e 100644
--- a/examples/vue/develop/my-project/src/core.js
+++ b/examples/vue/develop/my-project/src/core.js
@@ -1,24 +1,24 @@
-import { Agile, assignSharedAgileInstance, globalBind } from '@agile-ts/core';
+import {
+ globalBind,
+ createState,
+ createComputed,
+ createCollection,
+} from '@agile-ts/core';
import { Logger, assignSharedAgileLoggerConfig } from '@agile-ts/logger';
import '@agile-ts/vue';
assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG });
-// Create Agile Instance
-export const App = new Agile({ localStorage: true });
-assignSharedAgileInstance(App);
-
// console.debug('hi'); // Doesn't work here idk why
// Create State
-export const MY_STATE = App.createState('World', {
+export const MY_STATE = createState('World', {
key: 'my-state',
-})
- .computeValue((v) => {
- return `Hello ${v}`;
- });
+}).computeValue((v) => {
+ return `Hello ${v}`;
+});
-export const MY_COMPUTED = App.createComputed(
+export const MY_COMPUTED = createComputed(
async () => {
await new Promise((resolve) => setTimeout(resolve, 3000));
return `${MY_STATE.value} Frank`;
@@ -27,7 +27,7 @@ export const MY_COMPUTED = App.createComputed(
);
// Create Collection
-export const TODOS = App.createCollection({
+export const TODOS = createCollection({
initialData: [{ id: 1, name: 'Clean Bathroom' }],
selectors: [1],
}).persist('todos');
diff --git a/package.json b/package.json
index 7511c703..7a77e31f 100644
--- a/package.json
+++ b/package.json
@@ -35,12 +35,12 @@
"release": "lerna run release && changeset publish",
"prettier": "prettier --config .prettierrc --write \"**/*.{js,ts}\"",
"lint": "eslint --cache \"**/*.{js,jsx,ts,tsx}\"",
- "pack": "lerna run prepare && lerna run preview",
- "pack:core": "cd packages/core && yarn run prepare && yarn run preview",
- "pack:react": "cd packages/react && yarn run prepare && yarn run preview",
- "pack:multieditor": "cd packages/multieditor && yarn run prepare && yarn run preview",
- "pack:api": "cd packages/api && yarn run prepare && yarn run preview",
- "pack:event": "cd packages/event && yarn run prepare && yarn run preview"
+ "pack": "lerna run prepare && lerna run pack",
+ "pack:core": "cd packages/core && yarn run prepare && yarn run pack",
+ "pack:react": "cd packages/react && yarn run prepare && yarn run pack",
+ "pack:multieditor": "cd packages/multieditor && yarn run prepare && yarn run pack",
+ "pack:api": "cd packages/api && yarn run prepare && yarn run pack",
+ "pack:event": "cd packages/event && yarn run prepare && yarn run pack"
},
"repository": {
"type": "git",
diff --git a/packages/api/package.json b/packages/api/package.json
index 8fcfd3eb..c223a02a 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -13,16 +13,20 @@
"agile-ts"
],
"main": "dist/index.js",
+ "module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"scripts": {
- "build": "tsc",
- "prepare": "tsc && tsc -p ./tsconfig.production.json",
+ "build": "yarn run build:esm && yarn run build:cjs",
+ "prepare": "yarn run build",
+ "build:esm": "tsc -p ./tsconfig.esm.json",
+ "build:cjs": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.production.json",
"dev:publish": "yalc publish",
"dev:push": "yalc push",
"watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"",
"watch": "tsc -w",
"release": "yarn run prepare",
- "preview": "npm pack",
+ "release:manual": "yarn run prepare && yarn run release && npm publish",
+ "pack": "npm pack",
"test": "jest",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*"
@@ -48,5 +52,6 @@
"LICENSE",
"README.md",
"CHANGELOG.md"
- ]
+ ],
+ "sideEffects": false
}
diff --git a/packages/api/tsconfig.esm.json b/packages/api/tsconfig.esm.json
new file mode 100644
index 00000000..a00a08bf
--- /dev/null
+++ b/packages/api/tsconfig.esm.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "module": "ES2015",
+ "outDir": "dist/esm",
+ "declaration": false, // already generated via 'tsconfig.json' in the root dist folder
+ "removeComments": true
+ }
+}
diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json
index 600cd205..791f587c 100644
--- a/packages/api/tsconfig.json
+++ b/packages/api/tsconfig.json
@@ -1,10 +1,11 @@
{
"extends": "../tsconfig.default.json",
"compilerOptions": {
+ "module": "commonjs",
"rootDir": "src",
"outDir": "dist"
},
"include": [
"./src/**/*" // Only include what is in src (-> dist, tests, .. will be excluded)
]
-}
\ No newline at end of file
+}
diff --git a/packages/api/tsconfig.production.json b/packages/api/tsconfig.production.json
index 4b5c4d12..b8ec8c38 100644
--- a/packages/api/tsconfig.production.json
+++ b/packages/api/tsconfig.production.json
@@ -1,7 +1,9 @@
+// Use File: Overwrites already generated js files with new js files that have no comments
+// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments too
{
"extends": "./tsconfig.json",
"compilerOptions": {
- "declaration": false,
+ "declaration": false, // '.d.ts' files should have been generated before
"removeComments": true
}
-}
\ No newline at end of file
+}
diff --git a/packages/core/package.json b/packages/core/package.json
index 833a6df5..88d0e1e0 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@agile-ts/core",
- "version": "0.1.3",
+ "version": "0.2.0-alpha.5",
"author": "BennoDev",
"license": "MIT",
"homepage": "https://agile-ts.org/",
@@ -24,16 +24,20 @@
"agile-ts"
],
"main": "dist/index.js",
+ "module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"scripts": {
- "build": "tsc",
- "prepare": "tsc && tsc -p ./tsconfig.production.json",
+ "build": "yarn run build:esm && yarn run build:cjs",
+ "prepare": "yarn run build",
+ "build:esm": "tsc -p ./tsconfig.esm.json",
+ "build:cjs": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.production.json",
"dev:publish": "yalc publish",
"dev:push": "yalc push",
"watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"",
"watch": "tsc -w",
"release": "node ./scripts/prepublish.js && yarn run prepare",
- "preview": "npm pack",
+ "release:manual": "yarn run prepare && yarn run release && npm publish && git checkout README.md",
+ "pack": "npm pack",
"test": "jest",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*",
@@ -69,5 +73,6 @@
"LICENSE",
"README.md",
"CHANGELOG.md"
- ]
+ ],
+ "sideEffects": false
}
diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts
index 6a8d34fa..82fe3bab 100644
--- a/packages/core/src/agile.ts
+++ b/packages/core/src/agile.ts
@@ -1,28 +1,10 @@
import {
Runtime,
Integration,
- State,
- Storage,
- Collection,
- CollectionConfig,
- DefaultItem,
- Computed,
Integrations,
SubController,
globalBind,
- Storages,
- CreateStorageConfigInterface,
- RegisterConfigInterface,
- StateConfigInterface,
- flatMerge,
LogCodeManager,
- DependableAgileInstancesType,
- CreateComputedConfigInterface,
- ComputeFunctionType,
- createStorage,
- createState,
- createCollection,
- createComputed,
IntegrationsConfigInterface,
defineConfig,
} from './internal';
@@ -37,8 +19,6 @@ export class Agile {
public runtime: Runtime;
// Manages and simplifies the subscription to UI-Components
public subController: SubController;
- // Handles the permanent persistence of Agile Classes
- public storages: Storages;
// Integrations (UI-Frameworks) that are integrated into the Agile Instance
public integrations: Integrations;
@@ -62,7 +42,6 @@ export class Agile {
* changes in the Runtime to prevent race conditions
* - update/rerender subscribed UI-Components through the provided Integrations
* such as the [React Integration](https://agile-ts.org/docs/react)
- * - integrate with the persistent [Storage](https://agile-ts.org/docs/core/storage)
* - provide configuration object
*
* Each Agile Sub Instance requires an Agile Instance to be instantiated and function properly.
@@ -90,9 +69,6 @@ export class Agile {
});
this.runtime = new Runtime(this);
this.subController = new SubController(this);
- this.storages = new Storages(this, {
- localStorage: config.localStorage,
- });
LogCodeManager.log('10:00:00', [], this);
@@ -105,143 +81,6 @@ export class Agile {
}
}
- /**
- * Returns a newly created Storage.
- *
- * A Storage Class serves as an interface to external storages,
- * such as the [Async Storage](https://github.com/react-native-async-storage/async-storage) or
- * [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp).
- *
- * It creates the foundation to easily [`persist()`](https://agile-ts.org/docs/core/state/methods#persist) [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance)
- * (like States or Collections) in nearly any external storage.
- *
- * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstorage)
- *
- * @public
- * @param config - Configuration object
- */
- public createStorage(config: CreateStorageConfigInterface): Storage {
- return createStorage(config);
- }
-
- /**
- * Returns a newly created State.
- *
- * A State manages a piece of Information
- * that we need to remember globally at a later point in time.
- * While providing a toolkit to use and mutate this piece of Information.
- *
- * You can create as many global States as you need.
- *
- * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstate)
- *
- * @public
- * @param initialValue - Initial value of the State.
- * @param config - Configuration object
- */
- public createState(
- initialValue: ValueType,
- config: StateConfigInterface = {}
- ): State {
- return createState(
- initialValue,
- defineConfig(config, {
- agileInstance: this,
- })
- );
- }
-
- /**
- * Returns a newly created Collection.
- *
- * A Collection manages a reactive set of Information
- * that we need to remember globally at a later point in time.
- * While providing a toolkit to use and mutate this set of Information.
- *
- * It is designed for arrays of data objects following the same pattern.
- *
- * Each of these data object must have a unique `primaryKey` to be correctly identified later.
- *
- * You can create as many global Collections as you need.
- *
- * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcollection)
- *
- * @public
- * @param config - Configuration object
- */
- public createCollection(
- config?: CollectionConfig
- ): Collection {
- return createCollection(config, this);
- }
-
- /**
- * Returns a newly created Computed.
- *
- * A Computed is an extension of the State Class
- * that computes its value based on a specified compute function.
- *
- * The computed value will be cached to avoid unnecessary recomputes
- * and is only recomputed when one of its direct dependencies changes.
- *
- * Direct dependencies can be States and Collections.
- * So when, for example, a dependent State value changes, the computed value is recomputed.
- *
- * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstate)
- *
- * @public
- * @param computeFunction - Function to compute the computed value.
- * @param config - Configuration object
- */
- public createComputed(
- computeFunction: ComputeFunctionType,
- config?: CreateComputedConfigInterface
- ): Computed;
- /**
- * Returns a newly created Computed.
- *
- * A Computed is an extension of the State Class
- * that computes its value based on a specified compute function.
- *
- * The computed value will be cached to avoid unnecessary recomputes
- * and is only recomputed when one of its direct dependencies changes.
- *
- * Direct dependencies can be States and Collections.
- * So when, for example, a dependent State value changes, the computed value is recomputed.
- *
- * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcomputed)
- *
- * @public
- * @param computeFunction - Function to compute the computed value.
- * @param deps - Hard-coded dependencies on which the Computed Class should depend.
- */
- public createComputed(
- computeFunction: ComputeFunctionType,
- deps?: Array
- ): Computed;
- public createComputed(
- computeFunction: ComputeFunctionType,
- configOrDeps?:
- | CreateComputedConfigInterface
- | Array
- ): Computed {
- let _config: CreateComputedConfigInterface = {};
-
- if (Array.isArray(configOrDeps)) {
- _config = defineConfig(_config, {
- computedDeps: configOrDeps,
- agileInstance: this,
- });
- } else {
- if (configOrDeps)
- _config = defineConfig(configOrDeps, {
- agileInstance: this,
- });
- }
-
- return createComputed(computeFunction, _config);
- }
-
/**
* Registers the specified Integration with AgileTs.
*
@@ -259,27 +98,6 @@ export class Agile {
return this;
}
- /**
- * Registers the specified Storage with AgileTs.
- *
- * After a successful registration,
- * [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) such as States
- * can be persisted in the external Storage.
- *
- * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#registerstorage)
- *
- * @public
- * @param storage - Storage to be registered.
- * @param config - Configuration object
- */
- public registerStorage(
- storage: Storage,
- config: RegisterConfigInterface = {}
- ): this {
- this.storages.register(storage, config);
- return this;
- }
-
/**
* Returns a boolean indicating whether any Integration
* has been registered with AgileTs or not.
@@ -291,18 +109,6 @@ export class Agile {
public hasIntegration(): boolean {
return this.integrations.hasIntegration();
}
-
- /**
- * Returns a boolean indicating whether any Storage
- * has been registered with AgileTs or not.
- *
- * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#hasstorage)
- *
- * @public
- */
- public hasStorage(): boolean {
- return this.storages.hasStorage();
- }
}
export type AgileKey = string | number;
@@ -315,11 +121,6 @@ export interface CreateAgileConfigInterface
* @default true
*/
waitForMount?: boolean;
- /**
- * Whether the Local Storage should be registered as a Agile Storage by default.
- * @default false
- */
- localStorage?: boolean;
/**
* Whether the Agile Instance should be globally bound (globalThis)
* and thus be globally available.
diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts
index 2f2b7136..0cbf3987 100644
--- a/packages/core/src/collection/collection.persistent.ts
+++ b/packages/core/src/collection/collection.persistent.ts
@@ -11,6 +11,7 @@ import {
Persistent,
PersistentKey,
StorageKey,
+ getStorageManager,
} from '../internal';
export class CollectionPersistent<
@@ -35,10 +36,10 @@ export class CollectionPersistent<
config: CreatePersistentConfigInterface = {}
) {
super(collection.agileInstance(), {
- instantiate: false,
+ loadValue: false,
});
config = defineConfig(config, {
- instantiate: true,
+ loadValue: true,
storageKeys: [],
defaultStorageKey: null as any,
});
@@ -50,7 +51,7 @@ export class CollectionPersistent<
});
// Load/Store persisted value/s for the first time
- if (this.ready && config.instantiate) this.initialLoading();
+ if (this.ready && config.loadValue) this.initialLoading();
}
/**
@@ -84,7 +85,7 @@ export class CollectionPersistent<
// Check if Collection is already persisted
// (indicated by the persistence of 'true' at '_storageItemKey')
- const isPersisted = await this.agileInstance().storages.get(
+ const isPersisted = await getStorageManager()?.get(
_storageItemKey,
this.config.defaultStorageKey as any
);
@@ -103,6 +104,7 @@ export class CollectionPersistent<
// Persist default Group and load its value manually to be 100% sure
// that it was loaded completely
+ defaultGroup.loadedInitialValue = false;
defaultGroup.persist(defaultGroupStorageKey, {
loadValue: false,
defaultStorageKey: this.config.defaultStorageKey || undefined,
@@ -112,14 +114,6 @@ export class CollectionPersistent<
if (defaultGroup.persistent?.ready)
await defaultGroup.persistent.initialLoading();
- // TODO rebuild the default Group once at the end when all Items were loaded into the Collection
- // because otherwise it rebuilds the Group for each loaded Item
- // (-> warnings are printed for all not yet loaded Items when rebuilding the Group)
- // or rethink the whole Group rebuild process by adding a 'addItem()', 'removeItem()' and 'updateItem()' function
- // so that there is no need for rebuilding the whole Group when for example only Item B changed or Item C was added
- //
- // See Issue by starting the vue develop example app and adding some todos to the _todo_ list
-
// Persist Items found in the default Group's value
for (const itemKey of defaultGroup._value) {
const item = this.collection().getItem(itemKey);
@@ -155,13 +149,14 @@ export class CollectionPersistent<
followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above
});
if (placeholderItem?.persistent?.ready) {
- const loadedPersistedValueIntoItem = await placeholderItem.persistent.loadPersistedValue(); // TODO FIRST GROUP REBUILD (by assigning loaded value to Item)
+ const loadedPersistedValueIntoItem = await placeholderItem.persistent.loadPersistedValue();
// If successfully loaded Item value, assign Item to Collection
if (loadedPersistedValueIntoItem) {
this.collection().assignItem(placeholderItem, {
overwrite: true, // Overwrite to overwrite the existing placeholder Item, if one exists
- }); // TODO SECOND GROUP REBUILD (by calling rebuildGroupThatInclude() in the assignItem() method)
+ rebuildGroups: false, // Not necessary since the Groups that include the to assign Item were already rebuild while assigning the loaded value to the Item via 'loadPersistedValue()'
+ });
placeholderItem.isPersisted = true;
@@ -175,6 +170,7 @@ export class CollectionPersistent<
}
}
+ defaultGroup.loadedInitialValue = true;
return true;
};
const success = await loadValuesIntoCollection();
@@ -207,7 +203,7 @@ export class CollectionPersistent<
);
// Set flag in Storage to indicate that the Collection is persisted
- this.agileInstance().storages.set(_storageItemKey, true, this.storageKeys);
+ getStorageManager()?.set(_storageItemKey, true, this.storageKeys);
// Persist default Group
defaultGroup.persist(defaultGroupStorageKey, {
@@ -282,7 +278,7 @@ export class CollectionPersistent<
);
// Remove Collection is persisted indicator flag from Storage
- this.agileInstance().storages.remove(_storageItemKey, this.storageKeys);
+ getStorageManager()?.remove(_storageItemKey, this.storageKeys);
// Remove default Group from the Storage
defaultGroup.persistent?.removePersistedValue(defaultGroupStorageKey);
diff --git a/packages/core/src/collection/collection.ts b/packages/core/src/collection/collection.ts
new file mode 100644
index 00000000..60535136
--- /dev/null
+++ b/packages/core/src/collection/collection.ts
@@ -0,0 +1,1711 @@
+import {
+ Agile,
+ CollectionPersistent,
+ ComputedTracker,
+ copy,
+ defineConfig,
+ generateId,
+ Group,
+ GroupAddConfigInterface,
+ GroupConfigInterface,
+ GroupIngestConfigInterface,
+ GroupKey,
+ isFunction,
+ isValidObject,
+ Item,
+ LogCodeManager,
+ normalizeArray,
+ PatchOptionConfigInterface,
+ removeProperties,
+ Selector,
+ SelectorConfigInterface,
+ SelectorKey,
+ StorageKey,
+ TrackedChangeMethod,
+} from '../internal';
+
+export class Collection<
+ DataType extends Object = DefaultItem,
+ GroupValueType = Array // To extract the Group Type Value in Integration methods like 'useAgile()'
+> {
+ // Agile Instance the Collection belongs to
+ public agileInstance: () => Agile;
+
+ public config: CollectionConfigInterface;
+ private initialConfig: CreateCollectionConfigInterface;
+
+ // Key/Name identifier of the Collection
+ public _key?: CollectionKey;
+ // Amount of the Items stored in the Collection
+ public size = 0;
+ // Items stored in the Collection
+ public data: { [key: string]: Item } = {};
+ // Whether the Collection is persisted in an external Storage
+ public isPersisted = false;
+ // Manages the permanent persistent in external Storages
+ public persistent: CollectionPersistent | undefined;
+
+ // Registered Groups of Collection
+ public groups: { [key: string]: Group } = {};
+ // Registered Selectors of Collection
+ public selectors: { [key: string]: Selector } = {};
+
+ // Whether the Collection was instantiated correctly
+ public isInstantiated = false;
+
+ // Helper property to check whether an unknown instance is a Collection,
+ // without importing the Collection itself for using 'instanceof' (Treeshaking support)
+ public isCollection = true;
+
+ /**
+ * A Collection manages a reactive set of Information
+ * that we need to remember globally at a later point in time.
+ * While providing a toolkit to use and mutate this set of Information.
+ *
+ * It is designed for arrays of data objects following the same pattern.
+ *
+ * Each of these data object must have a unique `primaryKey` to be correctly identified later.
+ *
+ * You can create as many global Collections as you need.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/)
+ *
+ * @public
+ * @param agileInstance - Instance of Agile the Collection belongs to.
+ * @param config - Configuration object
+ */
+ constructor(agileInstance: Agile, config: CollectionConfig = {}) {
+ this.agileInstance = () => agileInstance;
+ let _config = typeof config === 'function' ? config(this) : config;
+ _config = defineConfig(_config, {
+ primaryKey: 'id',
+ groups: {},
+ selectors: {},
+ defaultGroupKey: 'default',
+ });
+ this._key = _config.key;
+ this.config = {
+ defaultGroupKey: _config.defaultGroupKey as any,
+ primaryKey: _config.primaryKey as any,
+ };
+ this.initialConfig = _config;
+
+ this.initGroups(_config.groups as any);
+ this.initSelectors(_config.selectors as any);
+
+ this.isInstantiated = true;
+
+ // Add 'initialData' to Collection
+ // (after 'isInstantiated' to add them properly to the Collection)
+ if (_config.initialData) this.collect(_config.initialData);
+
+ // Reselect Selector Items
+ // Necessary because the selection of an Item
+ // hasn't worked with a not correctly 'instantiated' Collection before
+ for (const key in this.selectors) this.selectors[key].reselect();
+
+ // Rebuild of Groups
+ // Not necessary because if Items are added to the Collection,
+ // (after 'isInstantiated = true')
+ // the Groups which contain these added Items get rebuilt.
+ // for (const key in this.groups) this.groups[key].rebuild();
+ }
+
+ /**
+ * Updates the key/name identifier of the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key)
+ *
+ * @public
+ * @param value - New key/name identifier.
+ */
+ public set key(value: CollectionKey | undefined) {
+ this.setKey(value);
+ }
+
+ /**
+ * Returns the key/name identifier of the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key)
+ *
+ * @public
+ */
+ public get key(): CollectionKey | undefined {
+ return this._key;
+ }
+
+ /**
+ * Updates the key/name identifier of the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#setkey)
+ *
+ * @public
+ * @param value - New key/name identifier.
+ */
+ public setKey(value: CollectionKey | undefined) {
+ const oldKey = this._key;
+
+ // Update Collection key
+ this._key = value;
+
+ // Update key in Persistent (only if oldKey is equal to persistentKey
+ // because otherwise the persistentKey is detached from the Collection key
+ // -> not managed by Collection anymore)
+ if (value != null && this.persistent?._key === oldKey)
+ this.persistent?.setKey(value);
+
+ return this;
+ }
+
+ /**
+ * Creates a new Group without associating it to the Collection.
+ *
+ * This way of creating a Group is intended for use in the Collection configuration object,
+ * where the `constructor()` takes care of the binding.
+ *
+ * After a successful initiation of the Collection we recommend using `createGroup()`,
+ * because it automatically connects the Group to the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#group)
+ *
+ * @public
+ * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group.
+ * @param config - Configuration object
+ */
+ public Group(
+ initialItems?: Array,
+ config: GroupConfigInterface = {}
+ ): Group {
+ if (this.isInstantiated) {
+ const key = config.key ?? generateId();
+ LogCodeManager.log('1B:02:00');
+ return this.createGroup(key, initialItems);
+ }
+
+ return new Group(this, initialItems, config);
+ }
+
+ /**
+ * Creates a new Selector without associating it to the Collection.
+ *
+ * This way of creating a Selector is intended for use in the Collection configuration object,
+ * where the `constructor()` takes care of the binding.
+ *
+ * After a successful initiation of the Collection we recommend using `createSelector()`,
+ * because it automatically connects the Group to the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#selector)
+ *
+ * @public
+ * @param initialKey - Key/Name identifier of the Item to be represented by the Selector.
+ * @param config - Configuration object
+ */
+ public Selector(
+ initialKey: ItemKey | null,
+ config: SelectorConfigInterface = {}
+ ): Selector {
+ if (this.isInstantiated) {
+ const key = config.key ?? generateId();
+ LogCodeManager.log('1B:02:01');
+ return this.createSelector(key, initialKey);
+ }
+
+ return new Selector(this, initialKey, config);
+ }
+
+ /**
+ * Sets up the specified Groups or Group keys
+ * and assigns them to the Collection if they are valid.
+ *
+ * It also instantiates and assigns the default Group to the Collection.
+ * The default Group reflects the default pattern of the Collection.
+ *
+ * @internal
+ * @param groups - Entire Groups or Group keys to be set up.
+ */
+ public initGroups(groups: { [key: string]: Group } | string[]): void {
+ if (!groups) return;
+ let groupsObject: { [key: string]: Group } = {};
+
+ // If groups is Array of Group keys/names, create the Groups based on these keys
+ if (Array.isArray(groups)) {
+ groups.forEach((groupKey) => {
+ groupsObject[groupKey] = new Group(this, [], {
+ key: groupKey,
+ });
+ });
+ } else groupsObject = groups;
+
+ // Add default Group
+ groupsObject[this.config.defaultGroupKey] = new Group(this, [], {
+ key: this.config.defaultGroupKey,
+ });
+
+ // Assign missing key/name to Group based on the property key
+ for (const key in groupsObject)
+ if (groupsObject[key]._key == null) groupsObject[key].setKey(key);
+
+ this.groups = groupsObject;
+ }
+
+ /**
+ * Sets up the specified Selectors or Selector keys
+ * and assigns them to the Collection if they are valid.
+ *
+ * @internal
+ * @param selectors - Entire Selectors or Selector keys to be set up.
+ */
+ public initSelectors(selectors: { [key: string]: Selector } | string[]) {
+ if (!selectors) return;
+ let selectorsObject: { [key: string]: Selector } = {};
+
+ // If selectors is Array of Selector keys/names, create the Selectors based on these keys
+ if (Array.isArray(selectors)) {
+ selectors.forEach((selectorKey) => {
+ selectorsObject[selectorKey] = new Selector(
+ this,
+ selectorKey,
+ {
+ key: selectorKey,
+ }
+ );
+ });
+ } else selectorsObject = selectors;
+
+ // Assign missing key/name to Selector based on the property key
+ for (const key in selectorsObject)
+ if (selectorsObject[key]._key == null) selectorsObject[key].setKey(key);
+
+ this.selectors = selectorsObject;
+ }
+
+ /**
+ * Appends new data objects following the same pattern to the end of the Collection.
+ *
+ * Each collected `data object` requires a unique identifier at the primaryKey property (by default 'id')
+ * to be correctly identified later.
+ *
+ * For example, if we collect some kind of user object,
+ * it must contain such unique identifier at 'id'
+ * to be added to the Collection.
+ * ```
+ * MY_COLLECTION.collect({id: '1', name: 'jeff'}); // valid
+ * MY_COLLECTION.collect({name: 'frank'}); // invalid
+ * ```
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#collect)
+ *
+ * @public
+ * @param data - Data objects or entire Items to be added.
+ * @param groupKeys - Group/s to which the specified data objects or Items are to be added.
+ * @param config - Configuration object
+ */
+ public collect(
+ data: DataType | Item | Array>,
+ groupKeys?: GroupKey | Array,
+ config: CollectConfigInterface = {}
+ ): this {
+ const _data = normalizeArray>(data);
+ const _groupKeys = normalizeArray(groupKeys);
+ const defaultGroupKey = this.config.defaultGroupKey;
+ const primaryKey = this.config.primaryKey;
+ config = defineConfig(config, {
+ method: 'push',
+ background: false,
+ patch: false,
+ select: false,
+ });
+
+ // Add default groupKey, since all Items are added to the default Group
+ if (!_groupKeys.includes(defaultGroupKey)) _groupKeys.push(defaultGroupKey);
+
+ // Create not existing Groups
+ _groupKeys.forEach(
+ (key) => this.groups[key] == null && this.createGroup(key)
+ );
+
+ _data.forEach((data, index) => {
+ let itemKey;
+ let success = false;
+
+ // Assign Data or Item to Collection
+ if (data instanceof Item) {
+ success = this.assignItem(data, {
+ background: config.background,
+ });
+ itemKey = data._key;
+ } else {
+ success = this.assignData(data, {
+ patch: config.patch,
+ background: config.background,
+ });
+ itemKey = data[primaryKey];
+ }
+
+ // Add itemKey to provided Groups and create corresponding Selector
+ if (success) {
+ _groupKeys.forEach((groupKey) => {
+ this.getGroup(groupKey)?.add(itemKey, {
+ method: config.method,
+ background: config.background,
+ });
+ });
+
+ if (config.select) this.createSelector(itemKey, itemKey);
+ }
+
+ if (config.forEachItem) config.forEachItem(data, itemKey, success, index);
+ });
+
+ return this;
+ }
+
+ /**
+ * Updates the Item `data object` with the specified `object with changes`, if the Item exists.
+ * By default the `object with changes` is merged into the Item `data object` at top level.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#update)
+ *
+ * @public
+ * @param itemKey - Key/Name identifier of the Item to be updated.
+ * @param changes - Object with changes to be merged into the Item data object.
+ * @param config - Configuration object
+ */
+ public update(
+ itemKey: ItemKey,
+ changes: DefaultItem | DataType,
+ config: UpdateConfigInterface = {}
+ ): Item | undefined {
+ const item = this.getItem(itemKey, { notExisting: true });
+ const primaryKey = this.config.primaryKey;
+ config = defineConfig(config, {
+ patch: true,
+ background: false,
+ });
+
+ // Check if the given conditions are suitable for a update action
+ if (item == null) {
+ LogCodeManager.log('1B:03:00', [itemKey, this._key]);
+ return undefined;
+ }
+ if (!isValidObject(changes)) {
+ LogCodeManager.log('1B:03:01', [itemKey, this._key]);
+ return undefined;
+ }
+
+ const oldItemKey = item._value[primaryKey];
+ const newItemKey = changes[primaryKey] || oldItemKey;
+
+ // Update itemKey if the new itemKey differs from the old one
+ if (oldItemKey !== newItemKey)
+ this.updateItemKey(oldItemKey, newItemKey, {
+ background: config.background,
+ });
+
+ // Patch changes into Item data object
+ if (config.patch) {
+ // Delete primaryKey property from 'changes object' because if it has changed,
+ // it is correctly updated in the above called 'updateItemKey()' method
+ if (changes[primaryKey]) delete changes[primaryKey];
+
+ let patchConfig: { addNewProperties?: boolean } =
+ typeof config.patch === 'object' ? config.patch : {};
+ patchConfig = {
+ addNewProperties: true,
+ ...patchConfig,
+ };
+
+ item.patch(changes as any, {
+ background: config.background,
+ addNewProperties: patchConfig.addNewProperties,
+ });
+ }
+ // Apply changes to Item data object
+ else {
+ // Ensure that the current Item identifier isn't different from the 'changes object' itemKey
+ if (changes[this.config.primaryKey] !== itemKey) {
+ changes[this.config.primaryKey] = itemKey;
+ LogCodeManager.log('1B:02:02', [], changes);
+ }
+
+ item.set(changes as any, {
+ background: config.background,
+ });
+ }
+
+ return item;
+ }
+
+ /**
+ * Creates a new Group and associates it to the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createGroup)
+ *
+ * @public
+ * @param groupKey - Unique identifier of the Group to be created.
+ * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group.
+ */
+ public createGroup(
+ groupKey: GroupKey,
+ initialItems: Array = []
+ ): Group {
+ let group = this.getGroup(groupKey, { notExisting: true });
+ if (!this.isInstantiated) LogCodeManager.log('1B:02:03');
+
+ // Check if Group already exists
+ if (group != null) {
+ if (!group.isPlaceholder) {
+ LogCodeManager.log('1B:03:02', [groupKey]);
+ return group;
+ }
+ group.set(initialItems, { overwrite: true });
+ return group;
+ }
+
+ // Create new Group
+ group = new Group(this, initialItems, { key: groupKey });
+ this.groups[groupKey] = group;
+
+ return group;
+ }
+
+ /**
+ * Returns a boolean indicating whether a Group with the specified `groupKey`
+ * exists in the Collection or not.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasgroup)
+ *
+ * @public
+ * @param groupKey - Key/Name identifier of the Group to be checked for existence.
+ * @param config - Configuration object
+ */
+ public hasGroup(
+ groupKey: GroupKey | undefined,
+ config: HasConfigInterface = {}
+ ): boolean {
+ return !!this.getGroup(groupKey, config);
+ }
+
+ /**
+ * Retrieves a single Group with the specified key/name identifier from the Collection.
+ *
+ * If the to retrieve Group doesn't exist, `undefined` is returned.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroup)
+ *
+ * @public
+ * @param groupKey - Key/Name identifier of the Group.
+ * @param config - Configuration object
+ */
+ public getGroup(
+ groupKey: GroupKey | undefined | null,
+ config: HasConfigInterface = {}
+ ): Group | undefined {
+ config = defineConfig(config, {
+ notExisting: false,
+ });
+
+ // Retrieve Group
+ const group = groupKey ? this.groups[groupKey] : undefined;
+
+ // Check if retrieved Group exists
+ if (group == null || (!config.notExisting && !group.exists))
+ return undefined;
+
+ ComputedTracker.tracked(group.observers['value']);
+ return group;
+ }
+
+ /**
+ * Retrieves the default Group from the Collection.
+ *
+ * Every Collection should have a default Group,
+ * which represents the default pattern of the Collection.
+ *
+ * If the default Group, for what ever reason, doesn't exist, `undefined` is returned.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getdefaultgroup)
+ *
+ * @public
+ */
+ public getDefaultGroup(): Group | undefined {
+ return this.getGroup(this.config.defaultGroupKey);
+ }
+
+ /**
+ * Retrieves a single Group with the specified key/name identifier from the Collection.
+ *
+ * If the to retrieve Group doesn't exist, a reference Group is returned.
+ * This has the advantage that Components that have the reference Group bound to themselves
+ * are rerenderd when the original Group is created.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupwithreference)
+ *
+ * @public
+ * @param groupKey - Key/Name identifier of the Group.
+ */
+ public getGroupWithReference(groupKey: GroupKey): Group {
+ let group = this.getGroup(groupKey, { notExisting: true });
+
+ // Create dummy Group to hold reference
+ if (group == null) {
+ group = new Group(this, [], {
+ key: groupKey,
+ isPlaceholder: true,
+ });
+ this.groups[groupKey] = group;
+ }
+
+ ComputedTracker.tracked(group.observers['value']);
+ return group;
+ }
+
+ /**
+ * Removes a Group with the specified key/name identifier from the Collection,
+ * if it exists in the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removegroup)
+ *
+ * @public
+ * @param groupKey - Key/Name identifier of the Group to be removed.
+ */
+ public removeGroup(groupKey: GroupKey): this {
+ if (this.groups[groupKey] != null) delete this.groups[groupKey];
+ return this;
+ }
+
+ /**
+ * Returns the count of registered Groups in the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupcount)
+ *
+ * @public
+ */
+ public getGroupCount(): number {
+ let size = 0;
+ Object.keys(this.groups).map(() => size++);
+ return size;
+ }
+
+ /**
+ * Creates a new Selector and associates it to the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createSelector)
+ *
+ * @public
+ * @param selectorKey - Unique identifier of the Selector to be created.
+ * @param itemKey - Key/Name identifier of the Item to be represented by the Selector.
+ */
+ public createSelector(
+ selectorKey: SelectorKey,
+ itemKey: ItemKey | null
+ ): Selector {
+ let selector = this.getSelector(selectorKey, { notExisting: true });
+ if (!this.isInstantiated) LogCodeManager.log('1B:02:04');
+
+ // Check if Selector already exists
+ if (selector != null) {
+ if (!selector.isPlaceholder) {
+ LogCodeManager.log('1B:03:03', [selectorKey]);
+ return selector;
+ }
+ selector.select(itemKey, { overwrite: true });
+ return selector;
+ }
+
+ // Create new Selector
+ selector = new Selector(this, itemKey, {
+ key: selectorKey,
+ });
+ this.selectors[selectorKey] = selector;
+
+ return selector;
+ }
+
+ /**
+ * Creates a new Selector and associates it to the Collection.
+ *
+ * The specified `itemKey` is used as the unique identifier key of the new Selector.
+ * ```
+ * MY_COLLECTION.select('1');
+ * // is equivalent to
+ * MY_COLLECTION.createSelector('1', '1');
+ * ```
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#select)
+ *
+ * @public
+ * @param itemKey - Key/Name identifier of the Item to be represented by the Selector
+ * and used as unique identifier of the Selector.
+ */
+ public select(itemKey: ItemKey): Selector {
+ return this.createSelector(itemKey, itemKey);
+ }
+
+ /**
+ * Returns a boolean indicating whether a Selector with the specified `selectorKey`
+ * exists in the Collection or not.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasselector)
+ *
+ * @public
+ * @param selectorKey - Key/Name identifier of the Selector to be checked for existence.
+ * @param config - Configuration object
+ */
+ public hasSelector(
+ selectorKey: SelectorKey | undefined,
+ config: HasConfigInterface = {}
+ ): boolean {
+ return !!this.getSelector(selectorKey, config);
+ }
+
+ /**
+ * Retrieves a single Selector with the specified key/name identifier from the Collection.
+ *
+ * If the to retrieve Selector doesn't exist, `undefined` is returned.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselector)
+ *
+ * @public
+ * @param selectorKey - Key/Name identifier of the Selector.
+ * @param config - Configuration object
+ */
+ public getSelector(
+ selectorKey: SelectorKey | undefined | null,
+ config: HasConfigInterface = {}
+ ): Selector | undefined {
+ config = defineConfig(config, {
+ notExisting: false,
+ });
+
+ // Get Selector
+ const selector = selectorKey ? this.selectors[selectorKey] : undefined;
+
+ // Check if Selector exists
+ if (selector == null || (!config.notExisting && !selector.exists))
+ return undefined;
+
+ ComputedTracker.tracked(selector.observers['value']);
+ return selector;
+ }
+
+ /**
+ * Retrieves a single Selector with the specified key/name identifier from the Collection.
+ *
+ * If the to retrieve Selector doesn't exist, a reference Selector is returned.
+ * This has the advantage that Components that have the reference Selector bound to themselves
+ * are rerenderd when the original Selector is created.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorwithreference)
+ *
+ * @public
+ * @param selectorKey - Key/Name identifier of the Selector.
+ */
+ public getSelectorWithReference(
+ selectorKey: SelectorKey
+ ): Selector {
+ let selector = this.getSelector(selectorKey, { notExisting: true });
+
+ // Create dummy Selector to hold reference
+ if (selector == null) {
+ selector = new Selector(this, null, {
+ key: selectorKey,
+ isPlaceholder: true,
+ });
+ this.selectors[selectorKey] = selector;
+ }
+
+ ComputedTracker.tracked(selector.observers['value']);
+ return selector;
+ }
+
+ /**
+ * Removes a Selector with the specified key/name identifier from the Collection,
+ * if it exists in the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeselector)
+ *
+ * @public
+ * @param selectorKey - Key/Name identifier of the Selector to be removed.
+ */
+ public removeSelector(selectorKey: SelectorKey): this {
+ if (this.selectors[selectorKey] != null) {
+ this.selectors[selectorKey].unselect();
+ delete this.selectors[selectorKey];
+ }
+ return this;
+ }
+
+ /**
+ * Returns the count of registered Selectors in the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorcount)
+ *
+ * @public
+ */
+ public getSelectorCount(): number {
+ let size = 0;
+ Object.keys(this.selectors).map(() => size++);
+ return size;
+ }
+
+ /**
+ * Returns a boolean indicating whether a Item with the specified `itemKey`
+ * exists in the Collection or not.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasitem)
+ *
+ * @public
+ * @param itemKey - Key/Name identifier of the Item.
+ * @param config - Configuration object
+ */
+ public hasItem(
+ itemKey: ItemKey | undefined,
+ config: HasConfigInterface = {}
+ ): boolean {
+ return !!this.getItem(itemKey, config);
+ }
+
+ /**
+ * Retrieves a single Item with the specified key/name identifier from the Collection.
+ *
+ * If the to retrieve Item doesn't exist, `undefined` is returned.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitem)
+ *
+ * @public
+ * @param itemKey - Key/Name identifier of the Item.
+ * @param config - Configuration object
+ */
+ public getItem(
+ itemKey: ItemKey | undefined | null,
+ config: HasConfigInterface = {}
+ ): Item | undefined {
+ config = defineConfig(config, {
+ notExisting: false,
+ });
+
+ // Get Item
+ const item = itemKey != null ? this.data[itemKey] : undefined;
+
+ // Check if Item exists
+ if (item == null || (!config.notExisting && !item.exists)) return undefined;
+
+ ComputedTracker.tracked(item.observers['value']);
+ return item;
+ }
+
+ /**
+ * Retrieves a single Item with the specified key/name identifier from the Collection.
+ *
+ * If the to retrieve Item doesn't exist, a reference Item is returned.
+ * This has the advantage that Components that have the reference Item bound to themselves
+ * are rerenderd when the original Item is created.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemwithreference)
+ *
+ * @public
+ * @param itemKey - Key/Name identifier of the Item.
+ */
+ public getItemWithReference(itemKey: ItemKey): Item {
+ let item = this.getItem(itemKey, { notExisting: true });
+
+ // Create dummy Item to hold reference
+ if (item == null) item = this.createPlaceholderItem(itemKey, true);
+
+ ComputedTracker.tracked(item.observers['value']);
+ return item;
+ }
+
+ /**
+ * Creates a placeholder Item
+ * that can be used to hold a reference to a not existing Item.
+ *
+ * @internal
+ * @param itemKey - Unique identifier of the to create placeholder Item.
+ * @param addToCollection - Whether to add the Item to be created to the Collection.
+ */
+ public createPlaceholderItem(
+ itemKey: ItemKey,
+ addToCollection = false
+ ): Item {
+ // Create placeholder Item
+ const item = new Item(
+ this,
+ {
+ [this.config.primaryKey]: itemKey, // Setting primaryKey of the Item to passed itemKey
+ dummy: 'item',
+ } as any,
+ { isPlaceholder: true }
+ );
+
+ // Add placeholder Item to Collection
+ if (
+ addToCollection &&
+ !Object.prototype.hasOwnProperty.call(this.data, itemKey)
+ )
+ this.data[itemKey] = item;
+
+ ComputedTracker.tracked(item.observers['value']);
+ return item;
+ }
+
+ /**
+ * Retrieves the value (data object) of a single Item
+ * with the specified key/name identifier from the Collection.
+ *
+ * If the to retrieve Item containing the value doesn't exist, `undefined` is returned.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemvalue)
+ *
+ * @public
+ * @param itemKey - Key/Name identifier of the Item.
+ * @param config - Configuration object
+ */
+ public getItemValue(
+ itemKey: ItemKey | undefined,
+ config: HasConfigInterface = {}
+ ): DataType | undefined {
+ const item = this.getItem(itemKey, config);
+ if (item == null) return undefined;
+ return item.value;
+ }
+
+ /**
+ * Retrieves all Items from the Collection.
+ * ```
+ * MY_COLLECTION.getAllItems();
+ * // is equivalent to
+ * MY_COLLECTION.getDefaultGroup().items;
+ * ```
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getallitems)
+ *
+ * @public
+ * @param config - Configuration object
+ */
+ public getAllItems(config: HasConfigInterface = {}): Array- > {
+ config = defineConfig(config, {
+ notExisting: false,
+ });
+
+ const defaultGroup = this.getDefaultGroup();
+ let items: Array
- > = [];
+
+ // If config.notExisting transform the data object into array since it contains all Items,
+ // otherwise return the default Group Items
+ if (config.notExisting) {
+ for (const key in this.data) items.push(this.data[key]);
+ } else {
+ // Why default Group Items and not all '.exists === true' Items?
+ // Because the default Group keeps track of all existing Items.
+ // It also does control the Collection output in binding methods like 'useAgile()'
+ // and therefore should do it here too.
+ items = defaultGroup?.getItems() || [];
+ }
+
+ return items;
+ }
+
+ /**
+ * Retrieves the values (data objects) of all Items from the Collection.
+ * ```
+ * MY_COLLECTION.getAllItemValues();
+ * // is equivalent to
+ * MY_COLLECTION.getDefaultGroup().output;
+ * ```
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getallitemvalues)
+ *
+ * @public
+ * @param config - Configuration object
+ */
+ public getAllItemValues(config: HasConfigInterface = {}): Array {
+ const items = this.getAllItems(config);
+ return items.map((item) => item.value);
+ }
+
+ /**
+ * Preserves the Collection `value` in the corresponding external Storage.
+ *
+ * The Collection key/name is used as the unique identifier for the Persistent.
+ * If that is not desired or the Collection has no unique identifier,
+ * please specify a separate unique identifier for the Persistent.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#persist)
+ *
+ * @public
+ * @param config - Configuration object
+ */
+ public persist(config?: CollectionPersistentConfigInterface): this;
+ /**
+ * Preserves the Collection `value` in the corresponding external Storage.
+ *
+ * The specified key is used as the unique identifier for the Persistent.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#persist)
+ *
+ * @public
+ * @param key - Key/Name identifier of Persistent.
+ * @param config - Configuration object
+ */
+ public persist(
+ key?: StorageKey,
+ config?: CollectionPersistentConfigInterface
+ ): this;
+ public persist(
+ keyOrConfig: StorageKey | CollectionPersistentConfigInterface = {},
+ config: CollectionPersistentConfigInterface = {}
+ ): this {
+ let _config: CollectionPersistentConfigInterface;
+ let key: StorageKey | undefined;
+
+ if (isValidObject(keyOrConfig)) {
+ _config = keyOrConfig as CollectionPersistentConfigInterface;
+ key = this._key;
+ } else {
+ _config = config || {};
+ key = keyOrConfig as StorageKey;
+ }
+
+ _config = defineConfig(_config, {
+ loadValue: true,
+ storageKeys: [],
+ defaultStorageKey: null as any,
+ });
+
+ // Check if Collection is already persisted
+ if (this.persistent != null && this.isPersisted) return this;
+
+ // Create Persistent (-> persist value)
+ this.persistent = new CollectionPersistent(this, {
+ loadValue: _config.loadValue,
+ storageKeys: _config.storageKeys,
+ key: key,
+ defaultStorageKey: _config.defaultStorageKey,
+ });
+
+ return this;
+ }
+
+ /**
+ * Fires immediately after the persisted `value`
+ * is loaded into the Collection from a corresponding external Storage.
+ *
+ * Registering such callback function makes only sense
+ * when the Collection is [persisted](https://agile-ts.org/docs/core/collection/methods/#persist) in an external Storage.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#onload)
+ *
+ * @public
+ * @param callback - A function to be executed after the externally persisted `value` was loaded into the Collection.
+ */
+ public onLoad(callback: (success: boolean) => void): this {
+ if (!this.persistent) return this;
+ if (!isFunction(callback)) {
+ LogCodeManager.log('00:03:01', ['OnLoad Callback', 'function']);
+ return this;
+ }
+
+ // Register specified callback
+ this.persistent.onLoad = callback;
+
+ // If Collection is already persisted ('isPersisted') fire specified callback immediately
+ if (this.isPersisted) callback(true);
+
+ return this;
+ }
+
+ /**
+ * Removes all Items from the Collection
+ * and resets all Groups and Selectors of the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#reset)
+ *
+ * @public
+ */
+ public reset(): this {
+ // Reset data
+ this.data = {};
+ this.size = 0;
+
+ // Reset Groups
+ for (const key in this.groups) this.getGroup(key)?.reset();
+
+ // Reset Selectors
+ for (const key in this.selectors) this.getSelector(key)?.reset();
+
+ return this;
+ }
+
+ /**
+ * Puts `itemKeys/s` into Group/s.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#put)
+ *
+ * @public
+ * @param itemKeys - `itemKey/s` to be put into the specified Group/s.
+ * @param groupKeys - Key/Name Identifier/s of the Group/s the specified `itemKey/s` are to put in.
+ * @param config - Configuration object
+ */
+ public put(
+ itemKeys: ItemKey | Array,
+ groupKeys: GroupKey | Array,
+ config: GroupAddConfigInterface = {}
+ ): this {
+ const _itemKeys = normalizeArray(itemKeys);
+ const _groupKeys = normalizeArray(groupKeys);
+
+ // Assign itemKeys to Groups
+ _groupKeys.forEach((groupKey) => {
+ this.getGroup(groupKey)?.add(_itemKeys, config);
+ });
+
+ return this;
+ }
+
+ /**
+ * Moves specified `itemKey/s` from one Group to another Group.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#move)
+ *
+ * @public
+ * @param itemKeys - `itemKey/s` to be moved.
+ * @param oldGroupKey - Key/Name Identifier of the Group the `itemKey/s` are moved from.
+ * @param newGroupKey - Key/Name Identifier of the Group the `itemKey/s` are moved in.
+ * @param config - Configuration object
+ */
+ public move(
+ itemKeys: ItemKey | Array,
+ oldGroupKey: GroupKey,
+ newGroupKey: GroupKey,
+ config: GroupAddConfigInterface = {}
+ ): this {
+ const _itemKeys = normalizeArray(itemKeys);
+
+ // Remove itemKeys from old Group
+ this.getGroup(oldGroupKey)?.remove(
+ _itemKeys,
+ removeProperties(config, ['method', 'overwrite'])
+ );
+
+ // Assign itemKeys to new Group
+ this.getGroup(newGroupKey)?.add(_itemKeys, config);
+
+ return this;
+ }
+
+ /**
+ * Updates the key/name identifier of the Item
+ * and returns a boolean indicating
+ * whether the Item identifier was updated successfully.
+ *
+ * @internal
+ * @param oldItemKey - Old key/name Item identifier.
+ * @param newItemKey - New key/name Item identifier.
+ * @param config - Configuration object
+ */
+ public updateItemKey(
+ oldItemKey: ItemKey,
+ newItemKey: ItemKey,
+ config: UpdateItemKeyConfigInterface = {}
+ ): boolean {
+ const item = this.getItem(oldItemKey, { notExisting: true });
+ config = defineConfig(config, {
+ background: false,
+ });
+
+ if (item == null || oldItemKey === newItemKey) return false;
+
+ // Check if Item with newItemKey already exists
+ if (this.hasItem(newItemKey)) {
+ LogCodeManager.log('1B:03:04', [oldItemKey, newItemKey, this._key]);
+ return false;
+ }
+
+ // Update itemKey in data object
+ delete this.data[oldItemKey];
+ this.data[newItemKey] = item;
+
+ // Update key/name of the Item
+ item.setKey(newItemKey, {
+ background: config.background,
+ });
+
+ // Update Persistent key of the Item if it follows the Item Storage Key pattern
+ // and therefore differs from the actual Item key
+ // (-> isn't automatically updated when the Item key is updated)
+ if (
+ item.persistent != null &&
+ item.persistent._key ===
+ CollectionPersistent.getItemStorageKey(oldItemKey, this._key)
+ )
+ item.persistent?.setKey(
+ CollectionPersistent.getItemStorageKey(newItemKey, this._key)
+ );
+
+ // Update itemKey in Groups
+ for (const groupKey in this.groups) {
+ const group = this.getGroup(groupKey, { notExisting: true });
+ if (group == null || !group.has(oldItemKey)) continue;
+ group.replace(oldItemKey, newItemKey, { background: config.background });
+ }
+
+ // Update itemKey in Selectors
+ for (const selectorKey in this.selectors) {
+ const selector = this.getSelector(selectorKey, { notExisting: true });
+ if (selector == null) continue;
+
+ // Reselect Item in Selector that has selected the newItemKey.
+ // Necessary because potential reference placeholder Item got overwritten
+ // with the new (renamed) Item
+ // -> has to find the new Item at selected itemKey
+ // since the placeholder Item got overwritten
+ if (selector.hasSelected(newItemKey, false)) {
+ selector.reselect({
+ force: true, // Because itemKeys are the same (but not the Items at this itemKey anymore)
+ background: config.background,
+ });
+ }
+
+ // Select newItemKey in Selector that has selected the oldItemKey
+ if (selector.hasSelected(oldItemKey, false))
+ selector.select(newItemKey, {
+ background: config.background,
+ });
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns all key/name identifiers of the Group/s containing the specified `itemKey`.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupkeysthathaveitemkey)
+ *
+ * @public
+ * @param itemKey - `itemKey` to be contained in Group/s.
+ */
+ public getGroupKeysThatHaveItemKey(itemKey: ItemKey): Array {
+ const groupKeys: Array = [];
+ for (const groupKey in this.groups) {
+ const group = this.groups[groupKey];
+ if (group?.has(itemKey)) groupKeys.push(groupKey);
+ }
+ return groupKeys;
+ }
+
+ /**
+ * Removes Item/s from:
+ *
+ * - `.everywhere()`:
+ * Removes Item/s from the entire Collection and all its Groups and Selectors (i.e. from everywhere)
+ * ```
+ * MY_COLLECTION.remove('1').everywhere();
+ * // is equivalent to
+ * MY_COLLECTION.removeItems('1');
+ * ```
+ * - `.fromGroups()`:
+ * Removes Item/s only from specified Groups.
+ * ```
+ * MY_COLLECTION.remove('1').fromGroups(['1', '2']);
+ * // is equivalent to
+ * MY_COLLECTION.removeFromGroups('1', ['1', '2']);
+ * ```
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#remove)
+ *
+ * @public
+ * @param itemKeys - Item/s with identifier/s to be removed.
+ */
+ public remove(
+ itemKeys: ItemKey | Array
+ ): {
+ fromGroups: (groups: Array | ItemKey) => Collection;
+ everywhere: (config?: RemoveItemsConfigInterface) => Collection;
+ } {
+ return {
+ fromGroups: (groups: Array | ItemKey) =>
+ this.removeFromGroups(itemKeys, groups),
+ everywhere: (config) => this.removeItems(itemKeys, config || {}),
+ };
+ }
+
+ /**
+ * Remove Item/s from specified Group/s.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removefromgroups)
+ *
+ * @public
+ * @param itemKeys - Key/Name Identifier/s of the Item/s to be removed from the Group/s.
+ * @param groupKeys - Key/Name Identifier/s of the Group/s the Item/s are to remove from.
+ */
+ public removeFromGroups(
+ itemKeys: ItemKey | Array,
+ groupKeys: GroupKey | Array
+ ): this {
+ const _itemKeys = normalizeArray(itemKeys);
+ const _groupKeys = normalizeArray(groupKeys);
+
+ _itemKeys.forEach((itemKey) => {
+ let removedFromGroupsCount = 0;
+
+ // Remove itemKey from the Groups
+ _groupKeys.forEach((groupKey) => {
+ const group = this.getGroup(groupKey, { notExisting: true });
+ if (!group?.has(itemKey)) return;
+ group.remove(itemKey);
+ removedFromGroupsCount++;
+ });
+
+ // If the Item was removed from each Group representing the Item,
+ // remove it completely
+ if (
+ removedFromGroupsCount >=
+ this.getGroupKeysThatHaveItemKey(itemKey).length
+ )
+ this.removeItems(itemKey);
+ });
+
+ return this;
+ }
+
+ /**
+ * Removes Item/s from the entire Collection and all the Collection's Groups and Selectors.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeitems)
+ *
+ * @public
+ * @param itemKeys - Key/Name identifier/s of the Item/s to be removed from the entire Collection.
+ * @param config - Configuration object
+ */
+ public removeItems(
+ itemKeys: ItemKey | Array,
+ config: RemoveItemsConfigInterface = {}
+ ): this {
+ config = defineConfig(config, {
+ notExisting: false,
+ removeSelector: false,
+ });
+ const _itemKeys = normalizeArray(itemKeys);
+
+ _itemKeys.forEach((itemKey) => {
+ const item = this.getItem(itemKey, { notExisting: config.notExisting });
+ if (item == null) return;
+ const wasPlaceholder = item.isPlaceholder;
+
+ // Remove Item from the Groups
+ for (const groupKey in this.groups) {
+ const group = this.getGroup(groupKey, { notExisting: true });
+ if (group?.has(itemKey)) group?.remove(itemKey);
+ }
+
+ // Remove Item from Storage
+ item.persistent?.removePersistedValue();
+
+ // Remove Item from Collection
+ delete this.data[itemKey];
+
+ // Reselect or remove Selectors which have represented the removed Item
+ for (const selectorKey in this.selectors) {
+ const selector = this.getSelector(selectorKey, { notExisting: true });
+ if (selector != null && selector.hasSelected(itemKey, false)) {
+ if (config.removeSelector) {
+ // Remove Selector
+ this.removeSelector(selector._key ?? 'unknown');
+ } else {
+ // Reselect Item in Selector
+ // in order to create a new dummyItem
+ // to hold a reference to the now not existing Item
+ selector.reselect({ force: true });
+ }
+ }
+ }
+
+ if (!wasPlaceholder) this.size--;
+ });
+
+ return this;
+ }
+
+ /**
+ * Assigns the provided `data` object to an already existing Item
+ * with specified key/name identifier found in the `data` object.
+ * If the Item doesn't exist yet, a new Item with the `data` object as value
+ * is created and assigned to the Collection.
+ *
+ * Returns a boolean indicating
+ * whether the `data` object was assigned/updated successfully.
+ *
+ * @internal
+ * @param data - Data object
+ * @param config - Configuration object
+ */
+ public assignData(
+ data: DataType,
+ config: AssignDataConfigInterface = {}
+ ): boolean {
+ config = defineConfig(config, {
+ patch: false,
+ background: false,
+ });
+ const _data = copy(data); // Copy data object to get rid of reference
+ const primaryKey = this.config.primaryKey;
+
+ if (!isValidObject(_data)) {
+ LogCodeManager.log('1B:03:05', [this._key]);
+ return false;
+ }
+
+ // Check if data object contains valid itemKey,
+ // otherwise add random itemKey to Item
+ if (!Object.prototype.hasOwnProperty.call(_data, primaryKey)) {
+ LogCodeManager.log('1B:02:05', [this._key, primaryKey]);
+ _data[primaryKey] = generateId();
+ }
+
+ const itemKey = _data[primaryKey];
+ const item = this.getItem(itemKey, { notExisting: true });
+ const wasPlaceholder = item?.isPlaceholder || false;
+
+ // Create new Item or update existing Item
+ if (item != null) {
+ if (config.patch) {
+ item.patch(_data, { background: config.background });
+ } else {
+ item.set(_data, { background: config.background });
+ }
+ } else {
+ this.assignItem(new Item(this, _data), {
+ background: config.background,
+ });
+ }
+
+ // Increase size of Collection if Item was previously a placeholder
+ // (-> hasn't officially existed in Collection before)
+ if (wasPlaceholder) this.size++;
+
+ return true;
+ }
+
+ /**
+ * Assigns the specified Item to the Collection
+ * at the key/name identifier of the Item.
+ *
+ * And returns a boolean indicating
+ * whether the Item was assigned successfully.
+ *
+ * @internal
+ * @param item - Item to be added.
+ * @param config - Configuration object
+ */
+ public assignItem(
+ item: Item,
+ config: AssignItemConfigInterface = {}
+ ): boolean {
+ config = defineConfig(config, {
+ overwrite: false,
+ background: false,
+ rebuildGroups: true,
+ });
+ const primaryKey = this.config.primaryKey;
+ let itemKey = item._value[primaryKey];
+ let increaseCollectionSize = true;
+
+ // Check if Item has valid itemKey,
+ // otherwise add random itemKey to Item
+ if (!Object.prototype.hasOwnProperty.call(item._value, primaryKey)) {
+ LogCodeManager.log('1B:02:05', [this._key, primaryKey]);
+ itemKey = generateId();
+ item.patch(
+ { [this.config.primaryKey]: itemKey },
+ { background: config.background }
+ );
+ item._key = itemKey;
+ }
+
+ // Check if Item belongs to this Collection
+ if (item.collection() !== this) {
+ LogCodeManager.log('1B:03:06', [this._key, item.collection()._key]);
+ return false;
+ }
+
+ // Check if Item already exists
+ if (this.getItem(itemKey, { notExisting: true }) != null) {
+ if (!config.overwrite) {
+ this.assignData(item._value);
+ return true;
+ } else increaseCollectionSize = false;
+ }
+
+ // Assign/add Item to Collection
+ this.data[itemKey] = item;
+
+ // Rebuild Groups that include itemKey
+ // after adding Item with itemKey to the Collection
+ // (because otherwise it can't find the Item as it isn't added yet)
+ if (config.rebuildGroups)
+ this.rebuildGroupsThatIncludeItemKey(itemKey, {
+ background: config.background,
+ });
+
+ if (increaseCollectionSize) this.size++;
+
+ return true;
+ }
+
+ /**
+ * Rebuilds all Groups that contain the specified `itemKey`.
+ *
+ * @internal
+ * @itemKey - `itemKey` Groups must contain to be rebuilt.
+ * @config - Configuration object
+ */
+ public rebuildGroupsThatIncludeItemKey(
+ itemKey: ItemKey,
+ config: GroupIngestConfigInterface = {}
+ ): void {
+ // Rebuild Groups that include itemKey
+ for (const groupKey of Object.keys(this.groups)) {
+ const group = this.getGroup(groupKey);
+ if (group != null && group.has(itemKey)) {
+ const index = group._preciseItemKeys.findIndex((ik) => itemKey === ik);
+
+ // Update Group output at index
+ if (index !== -1) {
+ group.rebuild(
+ [
+ {
+ key: itemKey,
+ index: index,
+ method: TrackedChangeMethod.UPDATE,
+ },
+ ],
+ config
+ );
+ }
+ // Add Item to the Group output if it isn't yet represented there to be updated
+ else {
+ const indexOfBeforeItemKey =
+ group.nextStateValue.findIndex((ik) => itemKey === ik) - 1;
+
+ group.rebuild(
+ [
+ {
+ key: itemKey,
+ index:
+ indexOfBeforeItemKey >= 0
+ ? group._preciseItemKeys.findIndex(
+ (ik) =>
+ group.nextStateValue[indexOfBeforeItemKey] === ik
+ ) + 1
+ : 0,
+ method: TrackedChangeMethod.ADD,
+ },
+ ],
+ config
+ );
+ }
+ }
+ }
+ }
+}
+
+export type DefaultItem = Record; // same as { [key: string]: any };
+export type CollectionKey = string | number;
+export type ItemKey = string | number;
+
+export interface CreateCollectionConfigInterface {
+ /**
+ * Initial Groups of the Collection.
+ * @default []
+ */
+ groups?: { [key: string]: Group } | string[];
+ /**
+ * Initial Selectors of the Collection
+ * @default []
+ */
+ selectors?: { [key: string]: Selector } | string[];
+ /**
+ * Key/Name identifier of the Collection.
+ * @default undefined
+ */
+ key?: CollectionKey;
+ /**
+ * Key/Name of the property
+ * which represents the unique Item identifier
+ * in collected data objects.
+ * @default 'id'
+ */
+ primaryKey?: string;
+ /**
+ * Key/Name identifier of the default Group that is created shortly after instantiation.
+ * The default Group represents the default pattern of the Collection.
+ * @default 'default'
+ */
+ defaultGroupKey?: GroupKey;
+ /**
+ * Initial data objects of the Collection.
+ * @default []
+ */
+ initialData?: Array;
+}
+
+export type CollectionConfig =
+ | CreateCollectionConfigInterface
+ | ((
+ collection: Collection
+ ) => CreateCollectionConfigInterface);
+
+export interface CollectionConfigInterface {
+ /**
+ * Key/Name of the property
+ * which represents the unique Item identifier
+ * in collected data objects.
+ * @default 'id'
+ */
+ primaryKey: string;
+ /**
+ * Key/Name identifier of the default Group that is created shortly after instantiation.
+ * The default Group represents the default pattern of the Collection.
+ * @default 'default'
+ */
+ defaultGroupKey: ItemKey;
+}
+
+export interface CollectConfigInterface
+ extends AssignDataConfigInterface {
+ /**
+ * In which way the collected data should be added to the Collection.
+ * - 'push' = at the end
+ * - 'unshift' = at the beginning
+ * https://www.tutorialspoint.com/what-are-the-differences-between-unshift-and-push-methods-in-javascript
+ * @default 'push'
+ */
+ method?: 'push' | 'unshift';
+ /**
+ * Performs the specified action for each collected data object.
+ * @default undefined
+ */
+ forEachItem?: (
+ data: DataType | Item,
+ key: ItemKey,
+ success: boolean,
+ index: number
+ ) => void;
+ /**
+ * Whether to create a Selector for each collected data object.
+ * @default false
+ */
+ select?: boolean;
+}
+
+export interface UpdateConfigInterface {
+ /**
+ * Whether to merge the data object with changes into the existing Item data object
+ * or overwrite the existing Item data object entirely.
+ * @default true
+ */
+ patch?: boolean | PatchOptionConfigInterface;
+ /**
+ * Whether to update the data object in background.
+ * So that the UI isn't notified of these changes and thus doesn't rerender.
+ * @default false
+ */
+ background?: boolean;
+}
+
+export interface UpdateItemKeyConfigInterface {
+ /**
+ * Whether to update the Item key/name identifier in background
+ * So that the UI isn't notified of these changes and thus doesn't rerender.
+ * @default false
+ */
+ background?: boolean;
+}
+
+export interface HasConfigInterface {
+ /**
+ * Whether Items that do not officially exist,
+ * such as placeholder Items, can be found
+ * @default true
+ */
+ notExisting?: boolean;
+}
+
+export interface CollectionPersistentConfigInterface {
+ /**
+ * Whether the Persistent should automatically load
+ * the persisted value into the Collection after its instantiation.
+ * @default true
+ */
+ loadValue?: boolean;
+ /**
+ * Key/Name identifier of Storages
+ * in which the Collection value should be or is persisted.
+ * @default [`defaultStorageKey`]
+ */
+ storageKeys?: StorageKey[];
+ /**
+ * Key/Name identifier of the default Storage of the specified Storage keys.
+ *
+ * The Collection value is loaded from the default Storage by default
+ * and is only loaded from the remaining Storages (`storageKeys`)
+ * if the loading from the default Storage failed.
+ *
+ * @default first index of the specified Storage keys or the AgileTs default Storage key
+ */
+ defaultStorageKey?: StorageKey;
+}
+
+export interface RemoveItemsConfigInterface {
+ /**
+ * Whether to remove not officially existing Items (such as placeholder Items).
+ * Keep in mind that sometimes it won't remove an Item entirely
+ * as another Instance (like a Selector) might need to keep reference to it.
+ * https://github.com/agile-ts/agile/pull/152
+ * @default false
+ */
+ notExisting?: boolean;
+ /**
+ * Whether to remove Selectors that have selected an Item to be removed.
+ * @default false
+ */
+ removeSelector?: boolean;
+}
+
+export interface AssignDataConfigInterface {
+ /**
+ * When the Item identifier of the to assign data object already exists in the Collection,
+ * whether to merge the newly assigned data into the existing one
+ * or overwrite the existing one entirely.
+ * @default true
+ */
+ patch?: boolean;
+ /**
+ * Whether to assign the data object to the Collection in background.
+ * So that the UI isn't notified of these changes and thus doesn't rerender.
+ * @default false
+ */
+ background?: boolean;
+}
+
+export interface AssignItemConfigInterface {
+ /**
+ * If an Item with the Item identifier already exists,
+ * whether to overwrite it entirely with the new one.
+ * @default false
+ */
+ overwrite?: boolean;
+ /**
+ * Whether to assign the Item to the Collection in background.
+ * So that the UI isn't notified of these changes and thus doesn't rerender.
+ * @default false
+ */
+ background?: boolean;
+ /**
+ * Whether to rebuild all Groups that include the itemKey of the to assign Item.
+ * @default true
+ */
+ rebuildGroups?: boolean;
+}
diff --git a/packages/core/src/collection/group/group.observer.ts b/packages/core/src/collection/group/group.observer.ts
index 4c0e860c..3e5d490a 100644
--- a/packages/core/src/collection/group/group.observer.ts
+++ b/packages/core/src/collection/group/group.observer.ts
@@ -6,10 +6,10 @@ import {
equal,
generateId,
RuntimeJob,
- Item,
IngestConfigInterface,
CreateRuntimeJobConfigInterface,
defineConfig,
+ removeProperties,
} from '../../internal';
export class GroupObserver extends Observer {
@@ -45,14 +45,14 @@ export class GroupObserver extends Observer {
* into the runtime wrapped into a Runtime-Job
* where it is executed accordingly.
*
- * During the execution the runtime applies the rebuilt `nextGroupOutput` to the Group,
+ * During the execution the runtime applies the `nextGroupOutput` to the Group,
* updates its dependents and re-renders the UI-Components it is subscribed to.
*
* @internal
* @param config - Configuration object
*/
public ingest(config: GroupIngestConfigInterface = {}): void {
- this.group().rebuild(config);
+ this.ingestOutput(this.group().nextGroupOutput, config);
}
/**
@@ -63,23 +63,17 @@ export class GroupObserver extends Observer {
* updates its dependents and re-renders the UI-Components it is subscribed to.
*
* @internal
- * @param newGroupItems - New Group Items to be applied to the Group.
+ * @param newGroupOutput - New Group Output to be applied to the Group.
* @param config - Configuration object.
*/
- public ingestItems(
- newGroupItems: Item[],
+ public ingestOutput(
+ newGroupOutput: DataType[],
config: GroupIngestConfigInterface = {}
): void {
const group = this.group();
config = defineConfig(config, {
perform: true,
- background: false,
- sideEffects: {
- enabled: true,
- exclude: [],
- },
force: false,
- maxTriesToUpdate: 3,
});
// Force overwriting the Group value if it is a placeholder.
@@ -89,24 +83,19 @@ export class GroupObserver extends Observer {
}
// Assign next Group output to Observer
- this.nextGroupOutput = copy(
- newGroupItems.map((item) => {
- return item._value;
- })
- );
+ this.nextGroupOutput = copy(newGroupOutput);
// Check if current Group output and to assign Group output are equal
if (equal(group._output, this.nextGroupOutput) && !config.force) return;
// Create Runtime-Job
const job = new RuntimeJob(this, {
- sideEffects: config.sideEffects,
- force: config.force,
- background: config.background,
- key:
- config.key ??
- `${this._key != null ? this._key + '_' : ''}${generateId()}_output`,
- maxTriesToUpdate: config.maxTriesToUpdate,
+ ...removeProperties(config, ['perform']),
+ ...{
+ key:
+ config.key ??
+ `${this._key != null ? this._key + '_' : ''}${generateId()}_output`,
+ },
});
// Pass created Job into the Runtime
@@ -130,6 +119,7 @@ export class GroupObserver extends Observer {
// Assign new Group output
group._output = copy(observer.nextGroupOutput);
+ group.nextGroupOutput = copy(observer.nextGroupOutput);
// Assign new public output to the Observer (output used by the Integrations)
job.observer.previousValue = copy(job.observer.value);
diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts
index 44aef2c5..c404b7db 100644
--- a/packages/core/src/collection/group/index.ts
+++ b/packages/core/src/collection/group/index.ts
@@ -1,5 +1,5 @@
import {
- State,
+ EnhancedState,
Collection,
DefaultItem,
ItemKey,
@@ -18,26 +18,36 @@ import {
GroupObserver,
StateObserver,
defineConfig,
+ GroupIngestConfigInterface,
} from '../../internal';
export class Group<
DataType extends Object = DefaultItem,
ValueType = Array // To extract the Group Type Value in Integration methods like 'useAgile()'
-> extends State> {
+> extends EnhancedState> {
// Collection the Group belongs to
collection: () => Collection;
static rebuildGroupSideEffectKey = 'rebuildGroup';
// Item values represented by the Group
- _output: Array = [];
+ public _output: Array = [];
+ // Next output of the Group (which can be used for dynamic Group updates)
+ public nextGroupOutput: Array = [];
+ // Precise itemKeys of the Group only include itemKeys
+ // that actually exist in the corresponding Collection
+ public _preciseItemKeys: Array = [];
// Manages dependencies to other States and subscriptions of UI-Components.
// It also serves as an interface to the runtime.
public observers: GroupObservers = {} as any;
// Keeps track of all Item identifiers for Items that couldn't be found in the Collection
- notFoundItemKeys: Array = [];
+ public notFoundItemKeys: Array = [];
+
+ // Whether the initial value was loaded from the corresponding Persistent
+ // https://github.com/agile-ts/agile/issues/155
+ public loadedInitialValue = true;
/**
* An extension of the State Class that categorizes and preserves the ordering of structured data.
@@ -72,7 +82,9 @@ export class Group<
// Add side effect to Group
// that rebuilds the Group whenever the Group value changes
- this.addSideEffect(Group.rebuildGroupSideEffectKey, () => this.rebuild());
+ this.addSideEffect(Group.rebuildGroupSideEffectKey, (state, config) => {
+ this.rebuild(config?.any?.trackedChanges || [], config);
+ });
// Initial rebuild
this.rebuild();
@@ -104,7 +116,7 @@ export class Group<
* @param itemKey - Key/Name identifier of the Item.
*/
public has(itemKey: ItemKey) {
- return this.value.findIndex((key) => key === itemKey) !== -1;
+ return this.value.indexOf(itemKey) !== -1;
}
/**
@@ -130,22 +142,46 @@ export class Group<
*/
public remove(
itemKeys: ItemKey | ItemKey[],
- config: StateIngestConfigInterface = {}
+ config: GroupRemoveConfigInterface = {}
): this {
const _itemKeys = normalizeArray(itemKeys);
const notExistingItemKeysInCollection: Array = [];
const notExistingItemKeys: Array = [];
let newGroupValue = copy(this.nextStateValue);
+ // Need to temporary update the preciseItemKeys
+ // since in the rebuild one action (trackedChanges) is performed after the other
+ // which requires a dynamic updated index
+ const updatedPreciseItemKeys = copy(this._preciseItemKeys);
+ config = defineConfig(config, {
+ softRebuild: true,
+ any: {},
+ });
+ config.any['trackedChanges'] = []; // TODO might be improved since the 'any' property is very vague
// Remove itemKeys from Group
_itemKeys.forEach((itemKey) => {
+ const exists = newGroupValue.includes(itemKey);
+
// Check if itemKey exists in Group
- if (!newGroupValue.includes(itemKey)) {
+ if (!exists) {
notExistingItemKeys.push(itemKey);
notExistingItemKeysInCollection.push(itemKey);
return;
}
+ // Track changes to soft rebuild the Group when rebuilding the Group in a side effect
+ if (config.softRebuild) {
+ const index = updatedPreciseItemKeys.findIndex((ik) => ik === itemKey);
+ if (index !== -1) {
+ updatedPreciseItemKeys.splice(index, 1);
+ config.any['trackedChanges'].push({
+ index,
+ method: TrackedChangeMethod.REMOVE,
+ key: itemKey,
+ });
+ }
+ }
+
// Check if itemKey exists in Collection
if (!this.collection().getItem(itemKey))
notExistingItemKeysInCollection.push(itemKey);
@@ -162,7 +198,7 @@ export class Group<
if (notExistingItemKeysInCollection.length >= _itemKeys.length)
config.background = true;
- this.set(newGroupValue, config);
+ this.set(newGroupValue, removeProperties(config, ['softRebuild']));
return this;
}
@@ -183,27 +219,42 @@ export class Group<
const _itemKeys = normalizeArray(itemKeys);
const notExistingItemKeysInCollection: Array = [];
const existingItemKeys: Array = [];
- let newGroupValue = copy(this.nextStateValue);
- defineConfig(config, {
+ const newGroupValue = copy(this.nextStateValue);
+ // Need to temporary update the preciseItemKeys
+ // since in the rebuild one action (trackedChanges) is performed after the other
+ // which requires a dynamic updated index
+ const updatedPreciseItemKeys = copy(this._preciseItemKeys);
+ config = defineConfig(config, {
method: 'push',
- overwrite: false,
+ softRebuild: true,
+ any: {},
});
+ config.any['trackedChanges'] = []; // TODO might be improved since the 'any' property is very vague
// Add itemKeys to Group
_itemKeys.forEach((itemKey) => {
+ const exists = newGroupValue.includes(itemKey);
+
// Check if itemKey exists in Collection
if (!this.collection().getItem(itemKey))
notExistingItemKeysInCollection.push(itemKey);
- // Remove itemKey temporary from newGroupValue
- // if it should be overwritten and already exists in the newGroupValue
- if (newGroupValue.includes(itemKey)) {
- if (config.overwrite) {
- newGroupValue = newGroupValue.filter((key) => key !== itemKey);
- } else {
- existingItemKeys.push(itemKey);
- return;
- }
+ // Handle existing Item
+ if (exists) {
+ existingItemKeys.push(itemKey);
+ return;
+ }
+
+ // Track changes to soft rebuild the Group when rebuilding the Group in a side effect
+ if (config.softRebuild) {
+ const index =
+ config.method === 'push' ? updatedPreciseItemKeys.length : 0;
+ updatedPreciseItemKeys.push(itemKey);
+ config.any['trackedChanges'].push({
+ index,
+ method: TrackedChangeMethod.ADD,
+ key: itemKey,
+ });
}
// Add new itemKey to Group
@@ -221,7 +272,10 @@ export class Group<
)
config.background = true;
- this.set(newGroupValue, removeProperties(config, ['method', 'overwrite']));
+ this.set(
+ newGroupValue,
+ removeProperties(config, ['method', 'softRebuild'])
+ );
return this;
}
@@ -339,26 +393,76 @@ export class Group<
* [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#rebuild)
*
* @internal
+ * @param trackedChanges - Changes that were tracked between two rebuilds.
* @param config - Configuration object
*/
- public rebuild(config: StateIngestConfigInterface = {}): this {
- const notFoundItemKeys: Array = []; // Item keys that couldn't be found in the Collection
- const groupItems: Array
- > = [];
-
+ public rebuild(
+ trackedChanges: TrackedChangeInterface[] = [],
+ config: GroupIngestConfigInterface = {}
+ ): this {
// Don't rebuild Group if Collection isn't correctly instantiated yet
// (because only after a successful instantiation the Collection
// contains the Items which are essential for a proper rebuild)
if (!this.collection().isInstantiated) return this;
- // Fetch Items from Collection
- this._value.forEach((itemKey) => {
- const item = this.collection().getItem(itemKey);
- if (item != null) groupItems.push(item);
- else notFoundItemKeys.push(itemKey);
- });
+ // Item keys that couldn't be found in the Collection
+ const notFoundItemKeys: Array = [];
+
+ // Soft rebuild the Collection (-> rebuild only parts of the Collection)
+ if (trackedChanges.length > 0) {
+ trackedChanges.forEach((change) => {
+ const item = this.collection().getItem(change.key);
+
+ switch (change.method) {
+ case TrackedChangeMethod.ADD:
+ // this._value.splice(change.index, 0, change.key); // Already updated in 'add' method
+ if (item != null) {
+ this._preciseItemKeys.splice(change.index, 0, change.key);
+ this.nextGroupOutput.splice(change.index, 0, copy(item._value));
+ } else {
+ notFoundItemKeys.push(change.key);
+ }
+ break;
+ case TrackedChangeMethod.UPDATE:
+ if (item != null) {
+ this.nextGroupOutput[change.index] = copy(item._value);
+ } else {
+ notFoundItemKeys.push(change.key);
+ }
+ break;
+ case TrackedChangeMethod.REMOVE:
+ // this._value.splice(change.index, 1); // Already updated in 'remove' method
+ this._preciseItemKeys.splice(change.index, 1);
+ this.nextGroupOutput.splice(change.index, 1);
+ break;
+ default:
+ break;
+ }
+ });
+ this.observers['output'].ingest(config);
+ }
+ // Hard rebuild the whole Collection
+ else {
+ const groupItemValues: Array = [];
+
+ // Reset precise itemKeys array to rebuild it from scratch
+ this._preciseItemKeys = [];
+
+ // Fetch Items from Collection
+ this._value.forEach((itemKey) => {
+ const item = this.collection().getItem(itemKey);
+ if (item != null) {
+ groupItemValues.push(item._value);
+ this._preciseItemKeys.push(itemKey);
+ } else notFoundItemKeys.push(itemKey);
+ });
+
+ // Ingest rebuilt Group output into the Runtime
+ this.observers['output'].ingestOutput(groupItemValues, config);
+ }
// Logging
- if (notFoundItemKeys.length > 0) {
+ if (notFoundItemKeys.length > 0 && this.loadedInitialValue) {
LogCodeManager.log(
'1C:02:00',
[this.collection()._key, this._key],
@@ -368,9 +472,6 @@ export class Group<
this.notFoundItemKeys = notFoundItemKeys;
- // Ingest rebuilt Group output into the Runtime
- this.observers['output'].ingestItems(groupItems, config);
-
return this;
}
}
@@ -395,11 +496,22 @@ export interface GroupAddConfigInterface extends StateIngestConfigInterface {
*/
method?: 'unshift' | 'push';
/**
- * If the to add `itemKey` already exists,
- * whether its position should be overwritten with the position of the new `itemKey`.
- * @default false
+ * Whether to soft rebuild the Group.
+ * -> only rebuild the parts of the Group that have actually changed
+ * instead of rebuilding the whole Group.
+ * @default true
+ */
+ softRebuild?: boolean;
+}
+
+export interface GroupRemoveConfigInterface extends StateIngestConfigInterface {
+ /**
+ * Whether to soft rebuild the Group.
+ * -> only rebuild the parts of the Group that have actually changed
+ * instead of rebuilding the whole Group.
+ * @default true
*/
- overwrite?: boolean;
+ softRebuild?: boolean;
}
export interface GroupConfigInterface {
@@ -425,3 +537,24 @@ export interface GroupPersistConfigInterface
*/
followCollectionPersistKeyPattern?: boolean;
}
+
+export enum TrackedChangeMethod {
+ ADD,
+ REMOVE,
+ UPDATE,
+}
+
+export interface TrackedChangeInterface {
+ /**
+ * TODO
+ */
+ method: TrackedChangeMethod;
+ /**
+ * TODO
+ */
+ key: ItemKey;
+ /**
+ * TODO
+ */
+ index: number;
+}
diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts
index 83b644fa..b386d611 100644
--- a/packages/core/src/collection/index.ts
+++ b/packages/core/src/collection/index.ts
@@ -1,1693 +1,40 @@
import {
+ Collection,
+ CollectionConfig,
+ DefaultItem,
Agile,
- Item,
- Group,
- GroupKey,
- Selector,
- SelectorKey,
- StorageKey,
- GroupConfigInterface,
- isValidObject,
- normalizeArray,
- copy,
- CollectionPersistent,
- GroupAddConfigInterface,
- ComputedTracker,
- generateId,
- SideEffectConfigInterface,
- SelectorConfigInterface,
- removeProperties,
- isFunction,
- LogCodeManager,
- PatchOptionConfigInterface,
- defineConfig,
+ shared,
} from '../internal';
-export class Collection<
- DataType extends Object = DefaultItem,
- GroupValueType = Array // To extract the Group Type Value in Integration methods like 'useAgile()'
-> {
- // Agile Instance the Collection belongs to
- public agileInstance: () => Agile;
-
- public config: CollectionConfigInterface;
- private initialConfig: CreateCollectionConfigInterface;
-
- // Key/Name identifier of the Collection
- public _key?: CollectionKey;
- // Amount of the Items stored in the Collection
- public size = 0;
- // Items stored in the Collection
- public data: { [key: string]: Item } = {};
- // Whether the Collection is persisted in an external Storage
- public isPersisted = false;
- // Manages the permanent persistent in external Storages
- public persistent: CollectionPersistent | undefined;
-
- // Registered Groups of Collection
- public groups: { [key: string]: Group } = {};
- // Registered Selectors of Collection
- public selectors: { [key: string]: Selector } = {};
-
- // Whether the Collection was instantiated correctly
- public isInstantiated = false;
-
- /**
- * A Collection manages a reactive set of Information
- * that we need to remember globally at a later point in time.
- * While providing a toolkit to use and mutate this set of Information.
- *
- * It is designed for arrays of data objects following the same pattern.
- *
- * Each of these data object must have a unique `primaryKey` to be correctly identified later.
- *
- * You can create as many global Collections as you need.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/)
- *
- * @public
- * @param agileInstance - Instance of Agile the Collection belongs to.
- * @param config - Configuration object
- */
- constructor(agileInstance: Agile, config: CollectionConfig = {}) {
- this.agileInstance = () => agileInstance;
- let _config = typeof config === 'function' ? config(this) : config;
- _config = defineConfig(_config, {
- primaryKey: 'id',
- groups: {},
- selectors: {},
- defaultGroupKey: 'default',
- });
- this._key = _config.key;
- this.config = {
- defaultGroupKey: _config.defaultGroupKey as any,
- primaryKey: _config.primaryKey as any,
- };
- this.initialConfig = _config;
-
- this.initGroups(_config.groups as any);
- this.initSelectors(_config.selectors as any);
-
- this.isInstantiated = true;
-
- // Add 'initialData' to Collection
- // (after 'isInstantiated' to add them properly to the Collection)
- if (_config.initialData) this.collect(_config.initialData);
-
- // Reselect Selector Items
- // Necessary because the selection of an Item
- // hasn't worked with a not correctly 'instantiated' Collection before
- for (const key in this.selectors) this.selectors[key].reselect();
-
- // Rebuild of Groups
- // Not necessary because if Items are added to the Collection,
- // (after 'isInstantiated = true')
- // the Groups which contain these added Items are rebuilt.
- // for (const key in this.groups) this.groups[key].rebuild();
- }
-
- /**
- * Updates the key/name identifier of the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key)
- *
- * @public
- * @param value - New key/name identifier.
- */
- public set key(value: CollectionKey | undefined) {
- this.setKey(value);
- }
-
- /**
- * Returns the key/name identifier of the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key)
- *
- * @public
- */
- public get key(): CollectionKey | undefined {
- return this._key;
- }
-
- /**
- * Updates the key/name identifier of the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#setkey)
- *
- * @public
- * @param value - New key/name identifier.
- */
- public setKey(value: CollectionKey | undefined) {
- const oldKey = this._key;
-
- // Update Collection key
- this._key = value;
-
- // Update key in Persistent (only if oldKey is equal to persistentKey
- // because otherwise the persistentKey is detached from the Collection key
- // -> not managed by Collection anymore)
- if (value != null && this.persistent?._key === oldKey)
- this.persistent?.setKey(value);
-
- return this;
- }
-
- /**
- * Creates a new Group without associating it to the Collection.
- *
- * This way of creating a Group is intended for use in the Collection configuration object,
- * where the `constructor()` takes care of the binding.
- *
- * After a successful initiation of the Collection we recommend using `createGroup()`,
- * because it automatically connects the Group to the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#group)
- *
- * @public
- * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group.
- * @param config - Configuration object
- */
- public Group(
- initialItems?: Array,
- config: GroupConfigInterface = {}
- ): Group {
- if (this.isInstantiated) {
- const key = config.key ?? generateId();
- LogCodeManager.log('1B:02:00');
- return this.createGroup(key, initialItems);
- }
-
- return new Group(this, initialItems, config);
- }
-
- /**
- * Creates a new Selector without associating it to the Collection.
- *
- * This way of creating a Selector is intended for use in the Collection configuration object,
- * where the `constructor()` takes care of the binding.
- *
- * After a successful initiation of the Collection we recommend using `createSelector()`,
- * because it automatically connects the Group to the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#selector)
- *
- * @public
- * @param initialKey - Key/Name identifier of the Item to be represented by the Selector.
- * @param config - Configuration object
- */
- public Selector(
- initialKey: ItemKey | null,
- config: SelectorConfigInterface = {}
- ): Selector {
- if (this.isInstantiated) {
- const key = config.key ?? generateId();
- LogCodeManager.log('1B:02:01');
- return this.createSelector(key, initialKey);
- }
-
- return new Selector(this, initialKey, config);
- }
-
- /**
- * Sets up the specified Groups or Group keys
- * and assigns them to the Collection if they are valid.
- *
- * It also instantiates and assigns the default Group to the Collection.
- * The default Group reflects the default pattern of the Collection.
- *
- * @internal
- * @param groups - Entire Groups or Group keys to be set up.
- */
- public initGroups(groups: { [key: string]: Group } | string[]): void {
- if (!groups) return;
- let groupsObject: { [key: string]: Group } = {};
-
- // If groups is Array of Group keys/names, create the Groups based on these keys
- if (Array.isArray(groups)) {
- groups.forEach((groupKey) => {
- groupsObject[groupKey] = new Group(this, [], {
- key: groupKey,
- });
- });
- } else groupsObject = groups;
-
- // Add default Group
- groupsObject[this.config.defaultGroupKey] = new Group(this, [], {
- key: this.config.defaultGroupKey,
- });
-
- // Assign missing key/name to Group based on the property key
- for (const key in groupsObject)
- if (groupsObject[key]._key == null) groupsObject[key].setKey(key);
-
- this.groups = groupsObject;
- }
-
- /**
- * Sets up the specified Selectors or Selector keys
- * and assigns them to the Collection if they are valid.
- *
- * @internal
- * @param selectors - Entire Selectors or Selector keys to be set up.
- */
- public initSelectors(selectors: { [key: string]: Selector } | string[]) {
- if (!selectors) return;
- let selectorsObject: { [key: string]: Selector } = {};
-
- // If selectors is Array of Selector keys/names, create the Selectors based on these keys
- if (Array.isArray(selectors)) {
- selectors.forEach((selectorKey) => {
- selectorsObject[selectorKey] = new Selector(
- this,
- selectorKey,
- {
- key: selectorKey,
- }
- );
- });
- } else selectorsObject = selectors;
-
- // Assign missing key/name to Selector based on the property key
- for (const key in selectorsObject)
- if (selectorsObject[key]._key == null) selectorsObject[key].setKey(key);
-
- this.selectors = selectorsObject;
- }
-
- /**
- * Appends new data objects following the same pattern to the end of the Collection.
- *
- * Each collected `data object` requires a unique identifier at the primaryKey property (by default 'id')
- * to be correctly identified later.
- *
- * For example, if we collect some kind of user object,
- * it must contain such unique identifier at 'id'
- * to be added to the Collection.
- * ```
- * MY_COLLECTION.collect({id: '1', name: 'jeff'}); // valid
- * MY_COLLECTION.collect({name: 'frank'}); // invalid
- * ```
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#collect)
- *
- * @public
- * @param data - Data objects or entire Items to be added.
- * @param groupKeys - Group/s to which the specified data objects or Items are to be added.
- * @param config - Configuration object
- */
- public collect(
- data: DataType | Item | Array>,
- groupKeys?: GroupKey | Array,
- config: CollectConfigInterface = {}
- ): this {
- const _data = normalizeArray>(data);
- const _groupKeys = normalizeArray(groupKeys);
- const defaultGroupKey = this.config.defaultGroupKey;
- const primaryKey = this.config.primaryKey;
- config = defineConfig(config, {
- method: 'push',
- background: false,
- patch: false,
- select: false,
- });
-
- // Add default groupKey, since all Items are added to the default Group
- if (!_groupKeys.includes(defaultGroupKey)) _groupKeys.push(defaultGroupKey);
-
- // Create not existing Groups
- _groupKeys.forEach(
- (key) => this.groups[key] == null && this.createGroup(key)
- );
-
- _data.forEach((data, index) => {
- let itemKey;
- let success = false;
-
- // Assign Data or Item to Collection
- if (data instanceof Item) {
- success = this.assignItem(data, {
- background: config.background,
- });
- itemKey = data._key;
- } else {
- success = this.assignData(data, {
- patch: config.patch,
- background: config.background,
- });
- itemKey = data[primaryKey];
- }
-
- // Add itemKey to provided Groups and create corresponding Selector
- if (success) {
- _groupKeys.forEach((groupKey) => {
- this.getGroup(groupKey)?.add(itemKey, {
- method: config.method,
- background: config.background,
- });
- });
-
- if (config.select) this.createSelector(itemKey, itemKey);
- }
-
- if (config.forEachItem) config.forEachItem(data, itemKey, success, index);
- });
-
- return this;
- }
-
- /**
- * Updates the Item `data object` with the specified `object with changes`, if the Item exists.
- * By default the `object with changes` is merged into the Item `data object` at top level.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#update)
- *
- * @public
- * @param itemKey - Key/Name identifier of the Item to be updated.
- * @param changes - Object with changes to be merged into the Item data object.
- * @param config - Configuration object
- */
- public update(
- itemKey: ItemKey,
- changes: DefaultItem | DataType,
- config: UpdateConfigInterface = {}
- ): Item | undefined {
- const item = this.getItem(itemKey, { notExisting: true });
- const primaryKey = this.config.primaryKey;
- config = defineConfig(config, {
- patch: true,
- background: false,
- });
-
- // Check if the given conditions are suitable for a update action
- if (item == null) {
- LogCodeManager.log('1B:03:00', [itemKey, this._key]);
- return undefined;
- }
- if (!isValidObject(changes)) {
- LogCodeManager.log('1B:03:01', [itemKey, this._key]);
- return undefined;
- }
-
- const oldItemKey = item._value[primaryKey];
- const newItemKey = changes[primaryKey] || oldItemKey;
-
- // Update itemKey if the new itemKey differs from the old one
- if (oldItemKey !== newItemKey)
- this.updateItemKey(oldItemKey, newItemKey, {
- background: config.background,
- });
-
- // Patch changes into Item data object
- if (config.patch) {
- // Delete primaryKey property from 'changes object' because if it has changed,
- // it is correctly updated in the above called 'updateItemKey()' method
- if (changes[primaryKey]) delete changes[primaryKey];
-
- let patchConfig: { addNewProperties?: boolean } =
- typeof config.patch === 'object' ? config.patch : {};
- patchConfig = {
- addNewProperties: true,
- ...patchConfig,
- };
-
- item.patch(changes as any, {
- background: config.background,
- addNewProperties: patchConfig.addNewProperties,
- });
- }
- // Apply changes to Item data object
- else {
- // Ensure that the current Item identifier isn't different from the 'changes object' itemKey
- if (changes[this.config.primaryKey] !== itemKey) {
- changes[this.config.primaryKey] = itemKey;
- LogCodeManager.log('1B:02:02', [], changes);
- }
-
- item.set(changes as any, {
- background: config.background,
- });
- }
-
- return item;
- }
-
- /**
- * Creates a new Group and associates it to the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createGroup)
- *
- * @public
- * @param groupKey - Unique identifier of the Group to be created.
- * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group.
- */
- public createGroup(
- groupKey: GroupKey,
- initialItems: Array = []
- ): Group {
- let group = this.getGroup(groupKey, { notExisting: true });
- if (!this.isInstantiated) LogCodeManager.log('1B:02:03');
-
- // Check if Group already exists
- if (group != null) {
- if (!group.isPlaceholder) {
- LogCodeManager.log('1B:03:02', [groupKey]);
- return group;
- }
- group.set(initialItems, { overwrite: true });
- return group;
- }
-
- // Create new Group
- group = new Group(this, initialItems, { key: groupKey });
- this.groups[groupKey] = group;
-
- return group;
- }
-
- /**
- * Returns a boolean indicating whether a Group with the specified `groupKey`
- * exists in the Collection or not.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasgroup)
- *
- * @public
- * @param groupKey - Key/Name identifier of the Group to be checked for existence.
- * @param config - Configuration object
- */
- public hasGroup(
- groupKey: GroupKey | undefined,
- config: HasConfigInterface = {}
- ): boolean {
- return !!this.getGroup(groupKey, config);
- }
-
- /**
- * Retrieves a single Group with the specified key/name identifier from the Collection.
- *
- * If the to retrieve Group doesn't exist, `undefined` is returned.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroup)
- *
- * @public
- * @param groupKey - Key/Name identifier of the Group.
- * @param config - Configuration object
- */
- public getGroup(
- groupKey: GroupKey | undefined | null,
- config: HasConfigInterface = {}
- ): Group | undefined {
- config = defineConfig(config, {
- notExisting: false,
- });
-
- // Retrieve Group
- const group = groupKey ? this.groups[groupKey] : undefined;
-
- // Check if retrieved Group exists
- if (group == null || (!config.notExisting && !group.exists))
- return undefined;
-
- ComputedTracker.tracked(group.observers['value']);
- return group;
- }
-
- /**
- * Retrieves the default Group from the Collection.
- *
- * Every Collection should have a default Group,
- * which represents the default pattern of the Collection.
- *
- * If the default Group, for what ever reason, doesn't exist, `undefined` is returned.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getdefaultgroup)
- *
- * @public
- */
- public getDefaultGroup(): Group | undefined {
- return this.getGroup(this.config.defaultGroupKey);
- }
-
- /**
- * Retrieves a single Group with the specified key/name identifier from the Collection.
- *
- * If the to retrieve Group doesn't exist, a reference Group is returned.
- * This has the advantage that Components that have the reference Group bound to themselves
- * are rerenderd when the original Group is created.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupwithreference)
- *
- * @public
- * @param groupKey - Key/Name identifier of the Group.
- */
- public getGroupWithReference(groupKey: GroupKey): Group {
- let group = this.getGroup(groupKey, { notExisting: true });
-
- // Create dummy Group to hold reference
- if (group == null) {
- group = new Group(this, [], {
- key: groupKey,
- isPlaceholder: true,
- });
- this.groups[groupKey] = group;
- }
-
- ComputedTracker.tracked(group.observers['value']);
- return group;
- }
-
- /**
- * Removes a Group with the specified key/name identifier from the Collection,
- * if it exists in the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removegroup)
- *
- * @public
- * @param groupKey - Key/Name identifier of the Group to be removed.
- */
- public removeGroup(groupKey: GroupKey): this {
- if (this.groups[groupKey] != null) delete this.groups[groupKey];
- return this;
- }
-
- /**
- * Returns the count of registered Groups in the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupcount)
- *
- * @public
- */
- public getGroupCount(): number {
- let size = 0;
- Object.keys(this.groups).map(() => size++);
- return size;
- }
-
- /**
- * Creates a new Selector and associates it to the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createSelector)
- *
- * @public
- * @param selectorKey - Unique identifier of the Selector to be created.
- * @param itemKey - Key/Name identifier of the Item to be represented by the Selector.
- */
- public createSelector(
- selectorKey: SelectorKey,
- itemKey: ItemKey | null
- ): Selector {
- let selector = this.getSelector(selectorKey, { notExisting: true });
- if (!this.isInstantiated) LogCodeManager.log('1B:02:04');
-
- // Check if Selector already exists
- if (selector != null) {
- if (!selector.isPlaceholder) {
- LogCodeManager.log('1B:03:03', [selectorKey]);
- return selector;
- }
- selector.select(itemKey, { overwrite: true });
- return selector;
- }
-
- // Create new Selector
- selector = new Selector(this, itemKey, {
- key: selectorKey,
- });
- this.selectors[selectorKey] = selector;
-
- return selector;
- }
-
- /**
- * Creates a new Selector and associates it to the Collection.
- *
- * The specified `itemKey` is used as the unique identifier key of the new Selector.
- * ```
- * MY_COLLECTION.select('1');
- * // is equivalent to
- * MY_COLLECTION.createSelector('1', '1');
- * ```
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#select)
- *
- * @public
- * @param itemKey - Key/Name identifier of the Item to be represented by the Selector
- * and used as unique identifier of the Selector.
- */
- public select(itemKey: ItemKey): Selector {
- return this.createSelector(itemKey, itemKey);
- }
-
- /**
- * Returns a boolean indicating whether a Selector with the specified `selectorKey`
- * exists in the Collection or not.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasselector)
- *
- * @public
- * @param selectorKey - Key/Name identifier of the Selector to be checked for existence.
- * @param config - Configuration object
- */
- public hasSelector(
- selectorKey: SelectorKey | undefined,
- config: HasConfigInterface = {}
- ): boolean {
- return !!this.getSelector(selectorKey, config);
- }
-
- /**
- * Retrieves a single Selector with the specified key/name identifier from the Collection.
- *
- * If the to retrieve Selector doesn't exist, `undefined` is returned.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselector)
- *
- * @public
- * @param selectorKey - Key/Name identifier of the Selector.
- * @param config - Configuration object
- */
- public getSelector(
- selectorKey: SelectorKey | undefined | null,
- config: HasConfigInterface = {}
- ): Selector | undefined {
- config = defineConfig(config, {
- notExisting: false,
- });
-
- // Get Selector
- const selector = selectorKey ? this.selectors[selectorKey] : undefined;
-
- // Check if Selector exists
- if (selector == null || (!config.notExisting && !selector.exists))
- return undefined;
-
- ComputedTracker.tracked(selector.observers['value']);
- return selector;
- }
-
- /**
- * Retrieves a single Selector with the specified key/name identifier from the Collection.
- *
- * If the to retrieve Selector doesn't exist, a reference Selector is returned.
- * This has the advantage that Components that have the reference Selector bound to themselves
- * are rerenderd when the original Selector is created.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorwithreference)
- *
- * @public
- * @param selectorKey - Key/Name identifier of the Selector.
- */
- public getSelectorWithReference(
- selectorKey: SelectorKey
- ): Selector {
- let selector = this.getSelector(selectorKey, { notExisting: true });
-
- // Create dummy Selector to hold reference
- if (selector == null) {
- selector = new Selector(this, null, {
- key: selectorKey,
- isPlaceholder: true,
- });
- this.selectors[selectorKey] = selector;
- }
-
- ComputedTracker.tracked(selector.observers['value']);
- return selector;
- }
-
- /**
- * Removes a Selector with the specified key/name identifier from the Collection,
- * if it exists in the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeselector)
- *
- * @public
- * @param selectorKey - Key/Name identifier of the Selector to be removed.
- */
- public removeSelector(selectorKey: SelectorKey): this {
- if (this.selectors[selectorKey] != null) {
- this.selectors[selectorKey].unselect();
- delete this.selectors[selectorKey];
- }
- return this;
- }
-
- /**
- * Returns the count of registered Selectors in the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorcount)
- *
- * @public
- */
- public getSelectorCount(): number {
- let size = 0;
- Object.keys(this.selectors).map(() => size++);
- return size;
- }
-
- /**
- * Returns a boolean indicating whether a Item with the specified `itemKey`
- * exists in the Collection or not.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasitem)
- *
- * @public
- * @param itemKey - Key/Name identifier of the Item.
- * @param config - Configuration object
- */
- public hasItem(
- itemKey: ItemKey | undefined,
- config: HasConfigInterface = {}
- ): boolean {
- return !!this.getItem(itemKey, config);
- }
-
- /**
- * Retrieves a single Item with the specified key/name identifier from the Collection.
- *
- * If the to retrieve Item doesn't exist, `undefined` is returned.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitem)
- *
- * @public
- * @param itemKey - Key/Name identifier of the Item.
- * @param config - Configuration object
- */
- public getItem(
- itemKey: ItemKey | undefined | null,
- config: HasConfigInterface = {}
- ): Item | undefined {
- config = defineConfig(config, {
- notExisting: false,
- });
-
- // Get Item
- const item = itemKey != null ? this.data[itemKey] : undefined;
-
- // Check if Item exists
- if (item == null || (!config.notExisting && !item.exists)) return undefined;
-
- ComputedTracker.tracked(item.observers['value']);
- return item;
- }
-
- /**
- * Retrieves a single Item with the specified key/name identifier from the Collection.
- *
- * If the to retrieve Item doesn't exist, a reference Item is returned.
- * This has the advantage that Components that have the reference Item bound to themselves
- * are rerenderd when the original Item is created.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemwithreference)
- *
- * @public
- * @param itemKey - Key/Name identifier of the Item.
- */
- public getItemWithReference(itemKey: ItemKey): Item {
- let item = this.getItem(itemKey, { notExisting: true });
-
- // Create dummy Item to hold reference
- if (item == null) item = this.createPlaceholderItem(itemKey, true);
-
- ComputedTracker.tracked(item.observers['value']);
- return item;
- }
-
- /**
- * Creates a placeholder Item
- * that can be used to hold a reference to a not existing Item.
- *
- * @internal
- * @param itemKey - Unique identifier of the to create placeholder Item.
- * @param addToCollection - Whether to add the Item to be created to the Collection.
- */
- public createPlaceholderItem(
- itemKey: ItemKey,
- addToCollection = false
- ): Item {
- // Create placeholder Item
- const item = new Item(
- this,
- {
- [this.config.primaryKey]: itemKey, // Setting primaryKey of the Item to passed itemKey
- dummy: 'item',
- } as any,
- { isPlaceholder: true }
- );
-
- // Add placeholder Item to Collection
- if (
- addToCollection &&
- !Object.prototype.hasOwnProperty.call(this.data, itemKey)
- )
- this.data[itemKey] = item;
-
- ComputedTracker.tracked(item.observers['value']);
- return item;
- }
-
- /**
- * Retrieves the value (data object) of a single Item
- * with the specified key/name identifier from the Collection.
- *
- * If the to retrieve Item containing the value doesn't exist, `undefined` is returned.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemvalue)
- *
- * @public
- * @param itemKey - Key/Name identifier of the Item.
- * @param config - Configuration object
- */
- public getItemValue(
- itemKey: ItemKey | undefined,
- config: HasConfigInterface = {}
- ): DataType | undefined {
- const item = this.getItem(itemKey, config);
- if (item == null) return undefined;
- return item.value;
- }
-
- /**
- * Retrieves all Items from the Collection.
- * ```
- * MY_COLLECTION.getAllItems();
- * // is equivalent to
- * MY_COLLECTION.getDefaultGroup().items;
- * ```
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getallitems)
- *
- * @public
- * @param config - Configuration object
- */
- public getAllItems(config: HasConfigInterface = {}): Array
- > {
- config = defineConfig(config, {
- notExisting: false,
- });
-
- const defaultGroup = this.getDefaultGroup();
- let items: Array
- > = [];
-
- // If config.notExisting transform the data object into array since it contains all Items,
- // otherwise return the default Group Items
- if (config.notExisting) {
- for (const key in this.data) items.push(this.data[key]);
- } else {
- // Why default Group Items and not all '.exists === true' Items?
- // Because the default Group keeps track of all existing Items.
- // It also does control the Collection output in binding methods like 'useAgile()'
- // and therefore should do it here too.
- items = defaultGroup?.getItems() || [];
- }
-
- return items;
- }
-
- /**
- * Retrieves the values (data objects) of all Items from the Collection.
- * ```
- * MY_COLLECTION.getAllItemValues();
- * // is equivalent to
- * MY_COLLECTION.getDefaultGroup().output;
- * ```
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getallitemvalues)
- *
- * @public
- * @param config - Configuration object
- */
- public getAllItemValues(config: HasConfigInterface = {}): Array {
- const items = this.getAllItems(config);
- return items.map((item) => item.value);
- }
-
- /**
- * Preserves the Collection `value` in the corresponding external Storage.
- *
- * The Collection key/name is used as the unique identifier for the Persistent.
- * If that is not desired or the Collection has no unique identifier,
- * please specify a separate unique identifier for the Persistent.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#persist)
- *
- * @public
- * @param config - Configuration object
- */
- public persist(config?: CollectionPersistentConfigInterface): this;
- /**
- * Preserves the Collection `value` in the corresponding external Storage.
- *
- * The specified key is used as the unique identifier for the Persistent.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#persist)
- *
- * @public
- * @param key - Key/Name identifier of Persistent.
- * @param config - Configuration object
- */
- public persist(
- key?: StorageKey,
- config?: CollectionPersistentConfigInterface
- ): this;
- public persist(
- keyOrConfig: StorageKey | CollectionPersistentConfigInterface = {},
- config: CollectionPersistentConfigInterface = {}
- ): this {
- let _config: CollectionPersistentConfigInterface;
- let key: StorageKey | undefined;
-
- if (isValidObject(keyOrConfig)) {
- _config = keyOrConfig as CollectionPersistentConfigInterface;
- key = this._key;
- } else {
- _config = config || {};
- key = keyOrConfig as StorageKey;
- }
-
- _config = defineConfig(_config, {
- loadValue: true,
- storageKeys: [],
- defaultStorageKey: null as any,
- });
-
- // Check if Collection is already persisted
- if (this.persistent != null && this.isPersisted) return this;
-
- // Create Persistent (-> persist value)
- this.persistent = new CollectionPersistent(this, {
- instantiate: _config.loadValue,
- storageKeys: _config.storageKeys,
- key: key,
- defaultStorageKey: _config.defaultStorageKey,
- });
-
- return this;
- }
-
- /**
- * Fires immediately after the persisted `value`
- * is loaded into the Collection from a corresponding external Storage.
- *
- * Registering such callback function makes only sense
- * when the Collection is [persisted](https://agile-ts.org/docs/core/collection/methods/#persist) in an external Storage.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#onload)
- *
- * @public
- * @param callback - A function to be executed after the externally persisted `value` was loaded into the Collection.
- */
- public onLoad(callback: (success: boolean) => void): this {
- if (!this.persistent) return this;
- if (!isFunction(callback)) {
- LogCodeManager.log('00:03:01', ['OnLoad Callback', 'function']);
- return this;
- }
-
- // Register specified callback
- this.persistent.onLoad = callback;
-
- // If Collection is already persisted ('isPersisted') fire specified callback immediately
- if (this.isPersisted) callback(true);
-
- return this;
- }
-
- /**
- * Removes all Items from the Collection
- * and resets all Groups and Selectors of the Collection.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#reset)
- *
- * @public
- */
- public reset(): this {
- // Reset data
- this.data = {};
- this.size = 0;
-
- // Reset Groups
- for (const key in this.groups) this.getGroup(key)?.reset();
-
- // Reset Selectors
- for (const key in this.selectors) this.getSelector(key)?.reset();
-
- return this;
- }
-
- /**
- * Puts `itemKeys/s` into Group/s.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#put)
- *
- * @public
- * @param itemKeys - `itemKey/s` to be put into the specified Group/s.
- * @param groupKeys - Key/Name Identifier/s of the Group/s the specified `itemKey/s` are to put in.
- * @param config - Configuration object
- */
- public put(
- itemKeys: ItemKey | Array,
- groupKeys: GroupKey | Array,
- config: GroupAddConfigInterface = {}
- ): this {
- const _itemKeys = normalizeArray(itemKeys);
- const _groupKeys = normalizeArray(groupKeys);
-
- // Assign itemKeys to Groups
- _groupKeys.forEach((groupKey) => {
- this.getGroup(groupKey)?.add(_itemKeys, config);
- });
-
- return this;
- }
-
- /**
- * Moves specified `itemKey/s` from one Group to another Group.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#move)
- *
- * @public
- * @param itemKeys - `itemKey/s` to be moved.
- * @param oldGroupKey - Key/Name Identifier of the Group the `itemKey/s` are moved from.
- * @param newGroupKey - Key/Name Identifier of the Group the `itemKey/s` are moved in.
- * @param config - Configuration object
- */
- public move(
- itemKeys: ItemKey | Array,
- oldGroupKey: GroupKey,
- newGroupKey: GroupKey,
- config: GroupAddConfigInterface = {}
- ): this {
- const _itemKeys = normalizeArray(itemKeys);
-
- // Remove itemKeys from old Group
- this.getGroup(oldGroupKey)?.remove(
- _itemKeys,
- removeProperties(config, ['method', 'overwrite'])
- );
-
- // Assign itemKeys to new Group
- this.getGroup(newGroupKey)?.add(_itemKeys, config);
-
- return this;
- }
-
- /**
- * Updates the key/name identifier of the Item
- * and returns a boolean indicating
- * whether the Item identifier was updated successfully.
- *
- * @internal
- * @param oldItemKey - Old key/name Item identifier.
- * @param newItemKey - New key/name Item identifier.
- * @param config - Configuration object
- */
- public updateItemKey(
- oldItemKey: ItemKey,
- newItemKey: ItemKey,
- config: UpdateItemKeyConfigInterface = {}
- ): boolean {
- const item = this.getItem(oldItemKey, { notExisting: true });
- config = defineConfig(config, {
- background: false,
- });
-
- if (item == null || oldItemKey === newItemKey) return false;
-
- // Check if Item with newItemKey already exists
- if (this.hasItem(newItemKey)) {
- LogCodeManager.log('1B:03:04', [oldItemKey, newItemKey, this._key]);
- return false;
- }
-
- // Update itemKey in data object
- delete this.data[oldItemKey];
- this.data[newItemKey] = item;
-
- // Update key/name of the Item
- item.setKey(newItemKey, {
- background: config.background,
- });
-
- // Update Persistent key of the Item if it follows the Item Storage Key pattern
- // and therefore differs from the actual Item key
- // (-> isn't automatically updated when the Item key is updated)
- if (
- item.persistent != null &&
- item.persistent._key ===
- CollectionPersistent.getItemStorageKey(oldItemKey, this._key)
- )
- item.persistent?.setKey(
- CollectionPersistent.getItemStorageKey(newItemKey, this._key)
- );
-
- // Update itemKey in Groups
- for (const groupKey in this.groups) {
- const group = this.getGroup(groupKey, { notExisting: true });
- if (group == null || !group.has(oldItemKey)) continue;
- group.replace(oldItemKey, newItemKey, { background: config.background });
- }
-
- // Update itemKey in Selectors
- for (const selectorKey in this.selectors) {
- const selector = this.getSelector(selectorKey, { notExisting: true });
- if (selector == null) continue;
-
- // Reselect Item in Selector that has selected the newItemKey.
- // Necessary because potential reference placeholder Item got overwritten
- // with the new (renamed) Item
- // -> has to find the new Item at selected itemKey
- // since the placeholder Item got overwritten
- if (selector.hasSelected(newItemKey, false)) {
- selector.reselect({
- force: true, // Because itemKeys are the same (but not the Items at this itemKey anymore)
- background: config.background,
- });
- }
-
- // Select newItemKey in Selector that has selected the oldItemKey
- if (selector.hasSelected(oldItemKey, false))
- selector.select(newItemKey, {
- background: config.background,
- });
- }
-
- return true;
- }
-
- /**
- * Returns all key/name identifiers of the Group/s containing the specified `itemKey`.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupkeysthathaveitemkey)
- *
- * @public
- * @param itemKey - `itemKey` to be contained in Group/s.
- */
- public getGroupKeysThatHaveItemKey(itemKey: ItemKey): Array {
- const groupKeys: Array = [];
- for (const groupKey in this.groups) {
- const group = this.groups[groupKey];
- if (group?.has(itemKey)) groupKeys.push(groupKey);
- }
- return groupKeys;
- }
-
- /**
- * Removes Item/s from:
- *
- * - `.everywhere()`:
- * Removes Item/s from the entire Collection and all its Groups and Selectors (i.e. from everywhere)
- * ```
- * MY_COLLECTION.remove('1').everywhere();
- * // is equivalent to
- * MY_COLLECTION.removeItems('1');
- * ```
- * - `.fromGroups()`:
- * Removes Item/s only from specified Groups.
- * ```
- * MY_COLLECTION.remove('1').fromGroups(['1', '2']);
- * // is equivalent to
- * MY_COLLECTION.removeFromGroups('1', ['1', '2']);
- * ```
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#remove)
- *
- * @public
- * @param itemKeys - Item/s with identifier/s to be removed.
- */
- public remove(
- itemKeys: ItemKey | Array
- ): {
- fromGroups: (groups: Array | ItemKey) => Collection;
- everywhere: (config?: RemoveItemsConfigInterface) => Collection;
- } {
- return {
- fromGroups: (groups: Array | ItemKey) =>
- this.removeFromGroups(itemKeys, groups),
- everywhere: (config) => this.removeItems(itemKeys, config || {}),
- };
- }
-
- /**
- * Remove Item/s from specified Group/s.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removefromgroups)
- *
- * @public
- * @param itemKeys - Key/Name Identifier/s of the Item/s to be removed from the Group/s.
- * @param groupKeys - Key/Name Identifier/s of the Group/s the Item/s are to remove from.
- */
- public removeFromGroups(
- itemKeys: ItemKey | Array,
- groupKeys: GroupKey | Array
- ): this {
- const _itemKeys = normalizeArray(itemKeys);
- const _groupKeys = normalizeArray(groupKeys);
-
- _itemKeys.forEach((itemKey) => {
- let removedFromGroupsCount = 0;
-
- // Remove itemKey from the Groups
- _groupKeys.forEach((groupKey) => {
- const group = this.getGroup(groupKey, { notExisting: true });
- if (!group?.has(itemKey)) return;
- group.remove(itemKey);
- removedFromGroupsCount++;
- });
-
- // If the Item was removed from each Group representing the Item,
- // remove it completely
- if (
- removedFromGroupsCount >=
- this.getGroupKeysThatHaveItemKey(itemKey).length
- )
- this.removeItems(itemKey);
- });
-
- return this;
- }
-
- /**
- * Removes Item/s from the entire Collection and all the Collection's Groups and Selectors.
- *
- * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeitems)
- *
- * @public
- * @param itemKeys - Key/Name identifier/s of the Item/s to be removed from the entire Collection.
- * @param config - Configuration object
- */
- public removeItems(
- itemKeys: ItemKey | Array,
- config: RemoveItemsConfigInterface = {}
- ): this {
- config = defineConfig(config, {
- notExisting: false,
- removeSelector: false,
- });
- const _itemKeys = normalizeArray(itemKeys);
-
- _itemKeys.forEach((itemKey) => {
- const item = this.getItem(itemKey, { notExisting: config.notExisting });
- if (item == null) return;
- const wasPlaceholder = item.isPlaceholder;
-
- // Remove Item from the Groups
- for (const groupKey in this.groups) {
- const group = this.getGroup(groupKey, { notExisting: true });
- if (group?.has(itemKey)) group?.remove(itemKey);
- }
-
- // Remove Item from Storage
- item.persistent?.removePersistedValue();
-
- // Remove Item from Collection
- delete this.data[itemKey];
-
- // Reselect or remove Selectors which have represented the removed Item
- for (const selectorKey in this.selectors) {
- const selector = this.getSelector(selectorKey, { notExisting: true });
- if (selector != null && selector.hasSelected(itemKey, false)) {
- if (config.removeSelector) {
- // Remove Selector
- this.removeSelector(selector._key ?? 'unknown');
- } else {
- // Reselect Item in Selector
- // in order to create a new dummyItem
- // to hold a reference to the now not existing Item
- selector.reselect({ force: true });
- }
- }
- }
-
- if (!wasPlaceholder) this.size--;
- });
-
- return this;
- }
-
- /**
- * Assigns the provided `data` object to an already existing Item
- * with specified key/name identifier found in the `data` object.
- * If the Item doesn't exist yet, a new Item with the `data` object as value
- * is created and assigned to the Collection.
- *
- * Returns a boolean indicating
- * whether the `data` object was assigned/updated successfully.
- *
- * @internal
- * @param data - Data object
- * @param config - Configuration object
- */
- public assignData(
- data: DataType,
- config: AssignDataConfigInterface = {}
- ): boolean {
- config = defineConfig(config, {
- patch: false,
- background: false,
- });
- const _data = copy(data); // Copy data object to get rid of reference
- const primaryKey = this.config.primaryKey;
-
- if (!isValidObject(_data)) {
- LogCodeManager.log('1B:03:05', [this._key]);
- return false;
- }
-
- // Check if data object contains valid itemKey,
- // otherwise add random itemKey to Item
- if (!Object.prototype.hasOwnProperty.call(_data, primaryKey)) {
- LogCodeManager.log('1B:02:05', [this._key, primaryKey]);
- _data[primaryKey] = generateId();
- }
-
- const itemKey = _data[primaryKey];
- const item = this.getItem(itemKey, { notExisting: true });
- const wasPlaceholder = item?.isPlaceholder || false;
-
- // Create new Item or update existing Item
- if (item != null) {
- if (config.patch) {
- item.patch(_data, { background: config.background });
- } else {
- item.set(_data, { background: config.background });
- }
- } else {
- this.assignItem(new Item(this, _data), {
- background: config.background,
- });
- }
-
- // Increase size of Collection if Item was previously a placeholder
- // (-> hasn't officially existed in Collection before)
- if (wasPlaceholder) this.size++;
-
- return true;
- }
-
- /**
- * Assigns the specified Item to the Collection
- * at the key/name identifier of the Item.
- *
- * And returns a boolean indicating
- * whether the Item was assigned successfully.
- *
- * @internal
- * @param item - Item to be added.
- * @param config - Configuration object
- */
- public assignItem(
- item: Item,
- config: AssignItemConfigInterface = {}
- ): boolean {
- config = defineConfig(config, {
- overwrite: false,
- background: false,
- });
- const primaryKey = this.config.primaryKey;
- let itemKey = item._value[primaryKey];
- let increaseCollectionSize = true;
-
- // Check if Item has valid itemKey,
- // otherwise add random itemKey to Item
- if (!Object.prototype.hasOwnProperty.call(item._value, primaryKey)) {
- LogCodeManager.log('1B:02:05', [this._key, primaryKey]);
- itemKey = generateId();
- item.patch(
- { [this.config.primaryKey]: itemKey },
- { background: config.background }
- );
- item._key = itemKey;
- }
-
- // Check if Item belongs to this Collection
- if (item.collection() !== this) {
- LogCodeManager.log('1B:03:06', [this._key, item.collection()._key]);
- return false;
- }
-
- // Check if Item already exists
- if (this.getItem(itemKey, { notExisting: true }) != null) {
- if (!config.overwrite) {
- this.assignData(item._value);
- return true;
- } else increaseCollectionSize = false;
- }
-
- // Assign/add Item to Collection
- this.data[itemKey] = item;
-
- // Rebuild Groups that include itemKey
- // after adding Item with itemKey to the Collection
- // (because otherwise it can't find the Item as it isn't added yet)
- this.rebuildGroupsThatIncludeItemKey(itemKey, {
- background: config.background,
- });
-
- if (increaseCollectionSize) this.size++;
-
- return true;
- }
-
- /**
- * Rebuilds all Groups that contain the specified `itemKey`.
- *
- * @internal
- * @itemKey - `itemKey` Groups must contain to be rebuilt.
- * @config - Configuration object
- */
- public rebuildGroupsThatIncludeItemKey(
- itemKey: ItemKey,
- config: RebuildGroupsThatIncludeItemKeyConfigInterface = {}
- ): void {
- config = defineConfig(config, {
- background: false,
- sideEffects: {
- enabled: true,
- exclude: [],
- },
- });
-
- // Rebuild Groups that include itemKey
- for (const groupKey in this.groups) {
- const group = this.getGroup(groupKey);
- if (group?.has(itemKey)) {
- // Not necessary because a sideEffect of ingesting the Group
- // into the runtime is to rebuilt itself
- // group.rebuild();
-
- group?.rebuild({
- background: config?.background,
- sideEffects: config?.sideEffects,
- storage: false,
- });
- }
- }
- }
-}
-
-export type DefaultItem = Record; // same as { [key: string]: any };
-export type CollectionKey = string | number;
-export type ItemKey = string | number;
-
-export interface CreateCollectionConfigInterface {
- /**
- * Initial Groups of the Collection.
- * @default []
- */
- groups?: { [key: string]: Group } | string[];
- /**
- * Initial Selectors of the Collection
- * @default []
- */
- selectors?: { [key: string]: Selector } | string[];
- /**
- * Key/Name identifier of the Collection.
- * @default undefined
- */
- key?: CollectionKey;
- /**
- * Key/Name of the property
- * which represents the unique Item identifier
- * in collected data objects.
- * @default 'id'
- */
- primaryKey?: string;
- /**
- * Key/Name identifier of the default Group that is created shortly after instantiation.
- * The default Group represents the default pattern of the Collection.
- * @default 'default'
- */
- defaultGroupKey?: GroupKey;
- /**
- * Initial data objects of the Collection.
- * @default []
- */
- initialData?: Array;
-}
-
-export type CollectionConfig =
- | CreateCollectionConfigInterface
- | ((
- collection: Collection
- ) => CreateCollectionConfigInterface);
-
-export interface CollectionConfigInterface {
- /**
- * Key/Name of the property
- * which represents the unique Item identifier
- * in collected data objects.
- * @default 'id'
- */
- primaryKey: string;
- /**
- * Key/Name identifier of the default Group that is created shortly after instantiation.
- * The default Group represents the default pattern of the Collection.
- * @default 'default'
- */
- defaultGroupKey: ItemKey;
-}
-
-export interface CollectConfigInterface
- extends AssignDataConfigInterface {
- /**
- * In which way the collected data should be added to the Collection.
- * - 'push' = at the end
- * - 'unshift' = at the beginning
- * https://www.tutorialspoint.com/what-are-the-differences-between-unshift-and-push-methods-in-javascript
- * @default 'push'
- */
- method?: 'push' | 'unshift';
- /**
- * Performs the specified action for each collected data object.
- * @default undefined
- */
- forEachItem?: (
- data: DataType | Item,
- key: ItemKey,
- success: boolean,
- index: number
- ) => void;
- /**
- * Whether to create a Selector for each collected data object.
- * @default false
- */
- select?: boolean;
-}
-
-export interface UpdateConfigInterface {
- /**
- * Whether to merge the data object with changes into the existing Item data object
- * or overwrite the existing Item data object entirely.
- * @default true
- */
- patch?: boolean | PatchOptionConfigInterface;
- /**
- * Whether to update the data object in background.
- * So that the UI isn't notified of these changes and thus doesn't rerender.
- * @default false
- */
- background?: boolean;
-}
-
-export interface UpdateItemKeyConfigInterface {
- /**
- * Whether to update the Item key/name identifier in background
- * So that the UI isn't notified of these changes and thus doesn't rerender.
- * @default false
- */
- background?: boolean;
-}
-
-export interface RebuildGroupsThatIncludeItemKeyConfigInterface {
- /**
- * Whether to rebuilt the Group in background.
- * So that the UI isn't notified of these changes and thus doesn't rerender.
- * @default false
- */
- background?: boolean;
- /**
- * Whether to execute the defined side effects.
- * @default true
- */
- sideEffects?: SideEffectConfigInterface;
-}
-
-export interface HasConfigInterface {
- /**
- * Whether Items that do not officially exist,
- * such as placeholder Items, can be found
- * @default true
- */
- notExisting?: boolean;
-}
-
-export interface CollectionPersistentConfigInterface {
- /**
- * Whether the Persistent should automatically load
- * the persisted value into the Collection after its instantiation.
- * @default true
- */
- loadValue?: boolean;
- /**
- * Key/Name identifier of Storages
- * in which the Collection value should be or is persisted.
- * @default [`defaultStorageKey`]
- */
- storageKeys?: StorageKey[];
- /**
- * Key/Name identifier of the default Storage of the specified Storage keys.
- *
- * The Collection value is loaded from the default Storage by default
- * and is only loaded from the remaining Storages (`storageKeys`)
- * if the loading from the default Storage failed.
- *
- * @default first index of the specified Storage keys or the AgileTs default Storage key
- */
- defaultStorageKey?: StorageKey;
-}
-
-export interface RemoveItemsConfigInterface {
- /**
- * Whether to remove not officially existing Items (such as placeholder Items).
- * Keep in mind that sometimes it won't remove an Item entirely
- * as another Instance (like a Selector) might need to keep reference to it.
- * https://github.com/agile-ts/agile/pull/152
- * @default false
- */
- notExisting?: boolean;
- /**
- * Whether to remove Selectors that have selected an Item to be removed.
- * @default false
- */
- removeSelector?: boolean;
-}
-
-export interface AssignDataConfigInterface {
- /**
- * When the Item identifier of the to assign data object already exists in the Collection,
- * whether to merge the newly assigned data into the existing one
- * or overwrite the existing one entirely.
- * @default true
- */
- patch?: boolean;
- /**
- * Whether to assign the data object to the Collection in background.
- * So that the UI isn't notified of these changes and thus doesn't rerender.
- * @default false
- */
- background?: boolean;
-}
-
-export interface AssignItemConfigInterface {
- /**
- * If an Item with the Item identifier already exists,
- * whether to overwrite it entirely with the new one.
- * @default false
- */
- overwrite?: boolean;
- /**
- * Whether to assign the Item to the Collection in background.
- * So that the UI isn't notified of these changes and thus doesn't rerender.
- * @default false
- */
- background?: boolean;
+export * from './collection';
+// export * from './collection.persistent';
+// export * from './group';
+// export * from './group/group.observer';
+// export * from './item';
+// export * from './selector';
+
+/**
+ * Returns a newly created Collection.
+ *
+ * A Collection manages a reactive set of Information
+ * that we need to remember globally at a later point in time.
+ * While providing a toolkit to use and mutate this set of Information.
+ *
+ * It is designed for arrays of data objects following the same pattern.
+ *
+ * Each of these data object must have a unique `primaryKey` to be correctly identified later.
+ *
+ * You can create as many global Collections as you need.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcollection)
+ *
+ * @public
+ * @param config - Configuration object
+ * @param agileInstance - Instance of Agile the Collection belongs to.
+ */
+export function createCollection(
+ config?: CollectionConfig,
+ agileInstance: Agile = shared
+): Collection {
+ return new Collection(agileInstance, config);
}
diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts
index 1be4cebf..44266aee 100644
--- a/packages/core/src/collection/item.ts
+++ b/packages/core/src/collection/item.ts
@@ -1,5 +1,5 @@
import {
- State,
+ EnhancedState,
Collection,
StateKey,
StateRuntimeJobConfigInterface,
@@ -12,7 +12,7 @@ import {
defineConfig,
} from '../internal';
-export class Item extends State<
+export class Item extends EnhancedState<
DataType
> {
// Collection the Group belongs to
@@ -166,9 +166,6 @@ export class Item extends State<
this.addSideEffect
- >(
Item.updateGroupSideEffectKey,
(instance, config) => {
- // TODO optimise this because currently the whole Group rebuilds
- // although only one Item value has changed which definitely needs no complete rebuild
- // https://github.com/agile-ts/agile/issues/113
instance.collection().rebuildGroupsThatIncludeItemKey(itemKey, config);
},
{ weight: 100 }
diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts
index 937c5458..76795f35 100644
--- a/packages/core/src/collection/selector.ts
+++ b/packages/core/src/collection/selector.ts
@@ -4,13 +4,13 @@ import {
defineConfig,
Item,
ItemKey,
- State,
+ EnhancedState,
StateRuntimeJobConfigInterface,
} from '../internal';
export class Selector<
DataType extends Object = DefaultItem
-> extends State {
+> extends EnhancedState {
// Collection the Selector belongs to
public collection: () => Collection;
diff --git a/packages/core/src/computed/computed.tracker.ts b/packages/core/src/computed/computed.tracker.ts
index e3254f90..c9c7ecc6 100644
--- a/packages/core/src/computed/computed.tracker.ts
+++ b/packages/core/src/computed/computed.tracker.ts
@@ -1,4 +1,4 @@
-import { Observer } from '../runtime/observer';
+import { Observer } from '../internal';
export class ComputedTracker {
static isTracking = false;
diff --git a/packages/core/src/computed/computed.ts b/packages/core/src/computed/computed.ts
new file mode 100644
index 00000000..416cb4db
--- /dev/null
+++ b/packages/core/src/computed/computed.ts
@@ -0,0 +1,266 @@
+import {
+ State,
+ Agile,
+ Observer,
+ StateConfigInterface,
+ ComputedTracker,
+ Collection,
+ StateIngestConfigInterface,
+ removeProperties,
+ LogCodeManager,
+ isAsyncFunction,
+ extractRelevantObservers,
+ defineConfig,
+} from '../internal';
+
+export class Computed extends State<
+ ComputedValueType
+> {
+ public config: ComputedConfigInterface;
+
+ // Function to compute the Computed Class value
+ public computeFunction: ComputeFunctionType;
+ // All dependencies the Computed Class depends on (including hardCoded and automatically detected dependencies)
+ public deps: Set = new Set();
+ // Only hardCoded dependencies the Computed Class depends on
+ public hardCodedDeps: Array = [];
+
+ // Helper property to check whether an unknown instance is a Computed,
+ // without importing the Computed itself for using 'instanceof' (Treeshaking support)
+ public isComputed = true;
+
+ /**
+ * A Computed is an extension of the State Class
+ * that computes its value based on a specified compute function.
+ *
+ * The computed value will be cached to avoid unnecessary recomputes
+ * and is only recomputed when one of its direct dependencies changes.
+ *
+ * Direct dependencies can be States and Collections.
+ * So when, for example, a dependent State value changes, the computed value is recomputed.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/computed/)
+ *
+ * @public
+ * @param agileInstance - Instance of Agile the Computed belongs to.
+ * @param computeFunction - Function to compute the computed value.
+ * @param config - Configuration object
+ */
+ constructor(
+ agileInstance: Agile,
+ computeFunction: ComputeFunctionType,
+ config: CreateComputedConfigInterface = {}
+ ) {
+ super(agileInstance, null as any, {
+ key: config.key,
+ dependents: config.dependents,
+ });
+ config = defineConfig(config, {
+ computedDeps: [],
+ autodetect: !isAsyncFunction(computeFunction),
+ });
+ this.agileInstance = () => agileInstance;
+ this.computeFunction = computeFunction;
+ this.config = {
+ autodetect: config.autodetect as any,
+ };
+
+ // Extract Observer of passed hardcoded dependency instances
+ this.hardCodedDeps = extractRelevantObservers(
+ config.computedDeps as DependableAgileInstancesType[]
+ ).filter((dep): dep is Observer => dep !== undefined);
+ this.deps = new Set(this.hardCodedDeps);
+
+ // Make this Observer depend on the specified hard coded dep Observers
+ this.deps.forEach((observer) => {
+ observer.addDependent(this.observers['value']);
+ });
+
+ // Initial recompute to assign the computed initial value to the Computed
+ // and autodetect missing dependencies
+ this.recompute({ autodetect: config.autodetect, overwrite: true });
+ }
+
+ /**
+ * Forces a recomputation of the cached value with the compute function.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#recompute)
+ *
+ * @public
+ * @param config - Configuration object
+ */
+ public recompute(config: RecomputeConfigInterface = {}): this {
+ config = defineConfig(config, {
+ autodetect: false,
+ });
+ this.compute({ autodetect: config.autodetect }).then((result) => {
+ this.observers['value'].ingestValue(
+ result,
+ removeProperties(config, ['autodetect'])
+ );
+ });
+ return this;
+ }
+
+ /**
+ * Assigns a new function to the Computed Class for computing its value.
+ *
+ * The dependencies of the new compute function are automatically detected
+ * and accordingly updated.
+ *
+ * An initial computation is performed with the new function
+ * to change the obsolete cached value.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#updatecomputefunction)
+ *
+ * @public
+ * @param computeFunction - New function to compute the value of the Computed Class.
+ * @param deps - Hard coded dependencies on which the Computed Class depends.
+ * @param config - Configuration object
+ */
+ public updateComputeFunction(
+ computeFunction: () => ComputedValueType,
+ deps: Array