diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index ba0242fc..a46c1a84 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -31,4 +31,4 @@ jobs:
title: Next Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
\ No newline at end of file
+ NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
diff --git a/examples/react/develop/functional-component-ts/package.json b/examples/react/develop/functional-component-ts/package.json
index be742bb8..08b5324c 100644
--- a/examples/react/develop/functional-component-ts/package.json
+++ b/examples/react/develop/functional-component-ts/package.json
@@ -30,7 +30,7 @@
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
- "install:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/api @agile-ts/multieditor @agile-ts/event & yarn install"
+ "install:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/api @agile-ts/multieditor @agile-ts/event @agile-ts/proxytree & yarn install"
},
"eslintConfig": {
"extends": "react-app"
diff --git a/examples/react/develop/functional-component-ts/src/App.tsx b/examples/react/develop/functional-component-ts/src/App.tsx
index ad7478e5..0574cf89 100644
--- a/examples/react/develop/functional-component-ts/src/App.tsx
+++ b/examples/react/develop/functional-component-ts/src/App.tsx
@@ -4,6 +4,7 @@ import { useAgile, useWatcher, useProxy } from '@agile-ts/react';
import { useEvent } from '@agile-ts/event';
import {
COUNTUP,
+ externalCreatedItem,
MY_COLLECTION,
MY_COMPUTED,
MY_EVENT,
@@ -12,7 +13,7 @@ import {
MY_STATE_3,
STATE_OBJECT,
} from './core';
-import { generateId, globalBind } from '@agile-ts/core';
+import { generateId, globalBind, Item } from '@agile-ts/core';
let rerenderCount = 0;
let rerenderCountInCountupView = 0;
@@ -42,11 +43,10 @@ const App = (props: any) => {
]);
const [myGroup] = useAgile([MY_COLLECTION.getGroupWithReference('myGroup')]);
- const [stateObject, item2, collection2] = useProxy([
- STATE_OBJECT,
- MY_COLLECTION.getItem('id2'),
- MY_COLLECTION,
- ]);
+ const [stateObject, item2, collection2] = useProxy(
+ [STATE_OBJECT, MY_COLLECTION.getItem('id2'), MY_COLLECTION],
+ { key: 'useProxy' }
+ );
console.log('Item1: ', item2?.name);
console.log('Collection: ', collection2.slice(0, 2));
@@ -142,6 +142,12 @@ const App = (props: any) => {
}>
Collect
+
diff --git a/examples/react/develop/functional-component-ts/src/core/index.ts b/examples/react/develop/functional-component-ts/src/core/index.ts
index c55426d9..cf448f46 100644
--- a/examples/react/develop/functional-component-ts/src/core/index.ts
+++ b/examples/react/develop/functional-component-ts/src/core/index.ts
@@ -1,4 +1,4 @@
-import { Agile, clone, Collection, Logger } from '@agile-ts/core';
+import { Agile, clone, Item, Logger } from '@agile-ts/core';
import Event from '@agile-ts/event';
export const myStorage: any = {};
@@ -86,7 +86,7 @@ export const MY_COLLECTION = App.createCollection(
).persist();
MY_COLLECTION.collect({ key: 'id1', name: 'test' });
MY_COLLECTION.collect({ key: 'id2', name: 'test2' }, 'myGroup');
-MY_COLLECTION.update('id1', { id: 'id1Updated', name: 'testUpdated' });
+MY_COLLECTION.update('id1', { key: 'id1Updated', name: 'testUpdated' });
MY_COLLECTION.getGroup('myGroup')?.persist({
followCollectionPersistKeyPattern: true,
});
@@ -94,6 +94,11 @@ MY_COLLECTION.onLoad(() => {
console.log('On Load MY_COLLECTION');
});
+export const externalCreatedItem = new Item(MY_COLLECTION, {
+ key: 'id10',
+ name: 'test',
+}).persist({ followCollectionPersistKeyPattern: true });
+
console.log('Initial: myCollection ', clone(MY_COLLECTION));
export const MY_EVENT = new Event<{ name: string }>(App, {
diff --git a/examples/react/develop/functional-component-ts/yarn.lock b/examples/react/develop/functional-component-ts/yarn.lock
index 54955758..21b151e8 100644
--- a/examples/react/develop/functional-component-ts/yarn.lock
+++ b/examples/react/develop/functional-component-ts/yarn.lock
@@ -3,46 +3,46 @@
"@agile-ts/api@file:.yalc/@agile-ts/api":
- version "0.0.16"
+ version "0.0.18"
dependencies:
- "@agile-ts/utils" "^0.0.2"
+ "@agile-ts/utils" "^0.0.4"
"@agile-ts/core@file:.yalc/@agile-ts/core":
- version "0.0.15"
+ version "0.0.17"
dependencies:
- "@agile-ts/logger" "^0.0.2"
- "@agile-ts/utils" "^0.0.2"
+ "@agile-ts/logger" "^0.0.4"
+ "@agile-ts/utils" "^0.0.4"
"@agile-ts/event@file:.yalc/@agile-ts/event":
- version "0.0.5"
+ version "0.0.7"
-"@agile-ts/logger@^0.0.2":
- version "0.0.2"
- resolved "https://registry.yarnpkg.com/@agile-ts/logger/-/logger-0.0.2.tgz#80a726531dd63ca7d1c9a123383e57b5501efbb0"
- integrity sha512-rJJ5pqXtOriYxjuZPhHs2J9N1FnIaAZqItCw0MXW9/5od/uhJ28aiG7w9RUBZts9SjDcICYEfjFMcTJ/kYJsMg==
+"@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==
dependencies:
- "@agile-ts/utils" "^0.0.2"
+ "@agile-ts/utils" "^0.0.4"
"@agile-ts/multieditor@file:.yalc/@agile-ts/multieditor":
- version "0.0.15"
+ version "0.0.17"
-"@agile-ts/proxytree@^0.0.2":
- version "0.0.2"
- resolved "https://registry.yarnpkg.com/@agile-ts/proxytree/-/proxytree-0.0.2.tgz#516ed19ee8d58aeecb291788a1e47be3dc23df8c"
- integrity sha512-PbSiChF0GcUoWnrbnHauzBxZ5r/+4pZSZWpYjkBcIFa48DgTtFzg5DfQzsW3Rc1Y0QSFGYqcZOvCK1xAjLIQ2g==
+"@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/proxytree@file:.yalc/@agile-ts/proxytree":
- version "0.0.1"
+ version "0.0.3"
"@agile-ts/react@file:.yalc/@agile-ts/react":
- version "0.0.16"
+ version "0.0.18"
dependencies:
- "@agile-ts/proxytree" "^0.0.2"
+ "@agile-ts/proxytree" "^0.0.3"
-"@agile-ts/utils@^0.0.2":
- version "0.0.2"
- resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.2.tgz#5f03761ace569b6c9ddd28c22f7b0fbec8b006b1"
- integrity sha512-LqgQyMdK+zDuTCmOX6FOxTH4JNXhEvGFqIyNqRDoP99BK6MHGrK+n7nOW+1b4x6ZCYe0+VmwtG5CeOPOm3Siow==
+"@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==
"@babel/code-frame@7.8.3":
version "7.8.3"
diff --git a/examples/vue/develop/my-project/src/core.js b/examples/vue/develop/my-project/src/core.js
index fe4efb2f..2e3f7902 100644
--- a/examples/vue/develop/my-project/src/core.js
+++ b/examples/vue/develop/my-project/src/core.js
@@ -1,4 +1,4 @@
-import { Agile, Logger } from '@agile-ts/core';
+import { Agile, Logger, globalBind } from '@agile-ts/core';
import vueIntegration from '@agile-ts/vue';
// Create Agile Instance
@@ -7,9 +7,18 @@ export const App = new Agile({
}).integrate(vueIntegration);
// Create State
-export const MY_STATE = App.createState('Hello World');
+export const MY_STATE = App.createState('World', {
+ key: 'my-state',
+}).computeValue((v) => {
+ return `Hello ${v}`;
+});
// Create Collection
export const TODOS = App.createCollection({
initialData: [{ id: 1, name: 'Clean Bathroom' }],
+ selectors: [1],
}).persist('todos');
+
+// TODOS.collect({ id: 2, name: 'jeff' });
+
+globalBind('__core__', { App, MY_STATE, TODOS });
diff --git a/examples/vue/develop/my-project/yarn.lock b/examples/vue/develop/my-project/yarn.lock
index 9bd6b58a..04c336c0 100644
--- a/examples/vue/develop/my-project/yarn.lock
+++ b/examples/vue/develop/my-project/yarn.lock
@@ -3,25 +3,25 @@
"@agile-ts/core@file:.yalc/@agile-ts/core":
- version "0.0.16"
+ version "0.0.17"
dependencies:
- "@agile-ts/logger" "^0.0.3"
- "@agile-ts/utils" "^0.0.3"
+ "@agile-ts/logger" "^0.0.4"
+ "@agile-ts/utils" "^0.0.4"
-"@agile-ts/logger@^0.0.3":
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/@agile-ts/logger/-/logger-0.0.3.tgz#21f460bab99b5a1f50fbe6be95e1e9ed471ef456"
- integrity sha512-8yejNCB7LXJ334smxovGaBWoqyXIUTHHO0/l2jPJt7WiMag0337KWbo1jyx6D8IkDioI9lunsN2U4CIBsRRhYA==
+"@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==
dependencies:
- "@agile-ts/utils" "^0.0.3"
+ "@agile-ts/utils" "^0.0.4"
-"@agile-ts/utils@^0.0.3":
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.3.tgz#f0e99d9ed9b21744f31effd99f7f7f32d26e3aec"
- integrity sha512-h/gbPRRnFYxpIH4D0F/+6gVcZoZ2YPreT+cl8TCysjkjR6XnZ4YgC7patHIopX7ZvR97IMiu+BtpmS1UDbOftg==
+"@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/vue@file:.yalc/@agile-ts/vue":
- version "0.0.4"
+ version "0.0.5"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13":
version "7.12.13"
diff --git a/package.json b/package.json
index 5ace251e..472b9722 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
},
"devDependencies": {
"@changesets/cli": "^2.12.0",
+ "@size-limit/file": "^4.12.0",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.7",
"@typescript-eslint/eslint-plugin": "^4.12.0",
@@ -62,6 +63,7 @@
"lerna-changelog": "^1.0.1",
"nodemon": "^2.0.6",
"prettier": "2.1.2",
+ "size-limit": "^4.12.0",
"ts-jest": "^26.4.4",
"ts-node": "^8.10.2",
"tsc-watch": "^4.1.0",
diff --git a/packages/core/.size-limit.js b/packages/core/.size-limit.js
new file mode 100644
index 00000000..1b8247f6
--- /dev/null
+++ b/packages/core/.size-limit.js
@@ -0,0 +1,6 @@
+module.exports = [
+ {
+ path: 'dist/*',
+ limit: '35 kB',
+ },
+];
diff --git a/packages/core/package.json b/packages/core/package.json
index 334f739d..c1b896fb 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -36,7 +36,8 @@
"preview": "npm pack",
"test": "jest",
"test:coverage": "jest --coverage",
- "lint": "eslint src/**/*"
+ "lint": "eslint src/**/*",
+ "size": "yarn run build && size-limit"
},
"devDependencies": {
"@agile-ts/logger": "file:../logger",
diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts
index f4c765f2..d47775ae 100644
--- a/packages/core/src/agile.ts
+++ b/packages/core/src/agile.ts
@@ -8,7 +8,6 @@ import {
DefaultItem,
Computed,
Integrations,
- Observer,
SubController,
globalBind,
Storages,
@@ -19,35 +18,62 @@ import {
CreateLoggerConfigInterface,
StateConfigInterface,
flatMerge,
- Group,
LogCodeManager,
+ ComputedConfigInterface,
+ SubscribableAgileInstancesType,
} from './internal';
export class Agile {
public config: AgileConfigInterface;
- public runtime: Runtime; // Handles assigning Values to Agile Instances
- public subController: SubController; // Handles subscriptions to Components
- public storages: Storages; // Handles permanent saving
+ // Queues and executes incoming Observer-based Jobs
+ 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
- public integrations: Integrations; // Integrated frameworks
- static initialIntegrations: Integration[] = []; // External added initial Integrations
+ // Integrations (UI-Frameworks) that are integrated into AgileTs
+ public integrations: Integrations;
+ // External added Integrations that are to integrate into AgileTs when it is instantiated
+ static initialIntegrations: Integration[] = [];
- // Static Logger with default config -> will be overwritten by config of last created Agile Instance
+ // Static AgileTs Logger with the default config
+ // (-> is overwritten by the last created Agile Instance)
static logger = new Logger({
prefix: 'Agile',
active: true,
level: Logger.level.WARN,
});
- // Key used to bind AgileTs globally
+ // Identifier used to bind an Agile Instance globally
static globalKey = '__agile__';
/**
+ * The Agile Class is the main Instance of AgileTs
+ * and should be unique to your application.
+ *
+ * Simply put, the Agile Instance is the brain of AgileTs
+ * and manages all [Agile Sub Instance](https://agile-ts.org/docs/introduction/#agile-sub-instance)
+ * such as States.
+ *
+ * It should be noted that it doesn't store the States;
+ * It only manages them. Each State has an Instance of the Agile Class,
+ * for example, to ingest its changes into the Runtime.
+ * In summary, the main tasks of the Agile Class are to:
+ * - queue [Agile Sub Instance](https://agile-ts.org/docs/introduction/#agile-sub-instance)
+ * 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.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/agile-instance/)
+ *
* @public
- * Agile - Global state and logic framework for reactive Typescript & Javascript applications
- * @param config - Config
+ * @param config - Configuration object
*/
constructor(config: CreateAgileConfigInterface = {}) {
config = defineConfig(config, {
@@ -73,39 +99,51 @@ export class Agile {
localStorage: config.localStorage,
});
- // Assign customized config to Logger
+ // Assign customized Logger config to the static Logger
Agile.logger = new Logger(config.logConfig);
- // Logging
LogCodeManager.log('10:00:00', [], this, Agile.logger);
- // Create global instance of Agile
- // Why? getAgileInstance() returns the global AgileInstance if it couldn't find the Agile Instance in the passed Instance
- if (config.bindGlobal) {
+ // Create a global instance of the Agile Instance.
+ // Why? 'getAgileInstance()' returns the global Agile Instance
+ // if it couldn't find any Agile Instance in the specified Instance.
+ if (config.bindGlobal)
if (!globalBind(Agile.globalKey, this)) LogCodeManager.log('10:02:00');
- }
}
- //=========================================================================================================
- // Storage
- //=========================================================================================================
/**
+ * 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
- * Storage - Handy Interface for storing Items permanently
- * @param config - Config
+ * @param config - Configuration object
*/
public createStorage(config: CreateStorageConfigInterface): Storage {
return new Storage(config);
}
- //=========================================================================================================
- // State
- //=========================================================================================================
/**
+ * 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
- * State - Class that holds one Value and causes rerender on subscribed Components
- * @param initialValue - Initial Value of the State
- * @param config - Config
+ * @param initialValue - Initial value of the State.
+ * @param config - Configuration object
*/
public createState(
initialValue: ValueType,
@@ -114,13 +152,23 @@ export class Agile {
return new State(this, initialValue, config);
}
- //=========================================================================================================
- // Collection
- //=========================================================================================================
/**
+ * 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
- * Collection - Class that holds a List of Objects with key and causes rerender on subscribed Components
- * @param config - Config
+ * @param config - Configuration object
*/
public createCollection(
config?: CollectionConfig
@@ -128,77 +176,98 @@ export class Agile {
return new Collection(this, config);
}
- //=========================================================================================================
- // 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#createstate)
+ *
* @public
- * Computed - Function that recomputes its value if a dependency changes
- * @param computeFunction - Function for computing value
- * @param config - Config
- * @param deps - Hard coded dependencies of Computed Function
+ * @param computeFunction - Function to compute the computed value.
+ * @param config - Configuration object
*/
public createComputed(
computeFunction: () => ComputedValueType,
- config?: StateConfigInterface,
- deps?: Array
+ config?: ComputedConfigInterface
): 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
- * Computed - Function that recomputes its value if a dependency changes
- * @param computeFunction - Function for computing value
- * @param deps - Hard coded dependencies of Computed Function
+ * @param computeFunction - Function to compute the computed value.
+ * @param deps - Hard-coded dependencies on which the Computed Class should depend.
*/
public createComputed(
computeFunction: () => ComputedValueType,
- deps?: Array
+ deps?: Array
): Computed;
public createComputed(
computeFunction: () => ComputedValueType,
- configOrDeps?: StateConfigInterface | Array,
- deps?: Array
+ configOrDeps?:
+ | ComputedConfigInterface
+ | Array
): Computed {
- let _deps: Array;
- let _config: StateConfigInterface;
+ let _config: ComputedConfigInterface = {};
if (Array.isArray(configOrDeps)) {
- _deps = configOrDeps;
- _config = {};
+ _config = flatMerge(_config, {
+ computedDeps: configOrDeps,
+ });
} else {
- _config = configOrDeps || {};
- _deps = deps || [];
+ if (configOrDeps) _config = configOrDeps;
}
- return new Computed(
- this,
- computeFunction,
- flatMerge(_config, {
- computedDeps: _deps,
- })
- );
+ return new Computed(this, computeFunction, _config);
}
- //=========================================================================================================
- // Integrate
- //=========================================================================================================
/**
+ * Registers the specified Integration with AgileTs.
+ *
+ * After a successful registration,
+ * [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) such as States
+ * can be bound to the Integration's UI-Components for reactivity.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#integrate)
+ *
* @public
- * Integrates framework into Agile
- * @param integration - Integration that gets registered/integrated
+ * @param integration - Integration to be integrated/registered.
*/
public integrate(integration: Integration) {
this.integrations.integrate(integration);
return this;
}
- //=========================================================================================================
- // Register Storage
- //=========================================================================================================
/**
+ * 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
- * Registers new Storage as Agile Storage
- * @param storage - new Storage
- * @param config - Config
+ * @param storage - Storage to be registered.
+ * @param config - Configuration object
*/
public registerStorage(
storage: Storage,
@@ -208,45 +277,67 @@ export class Agile {
return this;
}
- //=========================================================================================================
- // Has Integration
- //=========================================================================================================
/**
+ * Returns a boolean indicating whether any Integration
+ * has been registered with AgileTs or not.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#hasintegration)
+ *
* @public
- * Checks if Agile has any registered Integration
*/
public hasIntegration(): boolean {
return this.integrations.hasIntegration();
}
- //=========================================================================================================
- // Has Storage
- //=========================================================================================================
/**
+ * 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
- * Checks if Agile has any registered Storage
*/
public hasStorage(): boolean {
return this.storages.hasStorage();
}
}
-/**
- * @param logJobs - Allow Agile Logs
- * @param waitForMount - If Agile should wait until the component mounts
- * @param storageConfig - To configure Agile Storage
- * @param bindGlobal - Binds Agile Instance Global
- */
export interface CreateAgileConfigInterface {
+ /**
+ * Configures the logging behaviour of AgileTs.
+ * @default {
+ prefix: 'Agile',
+ active: true,
+ level: Logger.level.WARN,
+ canUseCustomStyles: true,
+ allowedTags: ['runtime', 'storage', 'subscription', 'multieditor'],
+ }
+ */
logConfig?: CreateLoggerConfigInterface;
+ /**
+ * Whether the Subscription Container shouldn't be ready
+ * until the UI-Component it represents has been mounted.
+ * @default true
+ */
waitForMount?: boolean;
+ /**
+ * Whether the Local Storage should be registered as a Agile Storage by default.
+ * @default true
+ */
localStorage?: boolean;
+ /**
+ * Whether the Agile Instance should be globally bound (globalThis)
+ * and thus be globally available.
+ * @default false
+ */
bindGlobal?: boolean;
}
-/**
- * @param waitForMount - If Agile should wait until the component mounts
- */
export interface AgileConfigInterface {
+ /**
+ * Whether the Subscription Container shouldn't be ready
+ * until the UI-Component it represents has been mounted.
+ * @default true
+ */
waitForMount: boolean;
}
diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts
index 04bbe301..f7425ff1 100644
--- a/packages/core/src/collection/collection.persistent.ts
+++ b/packages/core/src/collection/collection.persistent.ts
@@ -1,5 +1,4 @@
import {
- Agile,
Collection,
CollectionKey,
CreatePersistentConfigInterface,
@@ -17,6 +16,7 @@ import {
export class CollectionPersistent<
DataType extends Object = DefaultItem
> extends Persistent {
+ // Collection the Persistent belongs to
public collection: () => Collection;
static defaultGroupSideEffectKey = 'rebuildGroupStorageValue';
@@ -24,10 +24,11 @@ export class CollectionPersistent<
static storageGroupKeyPattern = '_${collectionKey}_group_${groupKey}';
/**
+ * Internal Class for managing the permanent persistence of a Collection.
+ *
* @internal
- * Collection Persist Manager - Handles permanent storing of Collection Value
- * @param collection - Collection that gets stored
- * @param config - Config
+ * @param collection - Collection to be persisted.
+ * @param config - Configuration object
*/
constructor(
collection: Collection,
@@ -48,47 +49,16 @@ export class CollectionPersistent<
defaultStorageKey: config.defaultStorageKey,
});
- // Load/Store persisted Value/s for the first Time
+ // Load/Store persisted value/s for the first time
if (this.ready && config.instantiate) this.initialLoading();
}
- //=========================================================================================================
- // Set Key
- //=========================================================================================================
/**
+ * Loads the persisted value into the Collection
+ * or persists the Collection value in the corresponding Storage.
+ * This behaviour depends on whether the Collection has been persisted before.
+ *
* @internal
- * Updates Key/Name of Persistent
- * @param value - New Key/Name of Persistent
- */
- public async setKey(value?: StorageKey): Promise {
- const oldKey = this._key;
- const wasReady = this.ready;
-
- // Assign Key
- if (value === this._key) return;
- this._key = value || Persistent.placeHolderKey;
-
- const isValid = this.validatePersistent();
-
- // Try to Initial Load Value if persistent wasn't ready
- if (!wasReady) {
- if (isValid) await this.initialLoading();
- return;
- }
-
- // Remove value at old Key
- await this.removePersistedValue(oldKey);
-
- // Assign Value to new Key
- if (isValid) await this.persistValue(value);
- }
-
- //=========================================================================================================
- // Initial Loading
- //=========================================================================================================
- /**
- * @internal
- * Loads/Saves Storage Value for the first Time
*/
public async initialLoading() {
super.initialLoading().then(() => {
@@ -96,195 +66,267 @@ export class CollectionPersistent<
});
}
- //=========================================================================================================
- // Load Persisted Value
- //=========================================================================================================
/**
+ * Loads Collection Instances (like Items or Groups) from the corresponding Storage
+ * and sets up side effects that dynamically update
+ * the Storage value when the Collection (Instances) changes.
+ *
* @internal
- * Loads Collection from Storage
- * @param storageItemKey - Prefix Key of Persisted Instances (default PersistentKey)
- * @return Success?
+ * @param storageItemKey - Prefix Storage key of the to load Collection Instances.
+ * | default = Persistent.key |
+ * @return Whether the loading of the persisted Collection Instances and setting up of the corresponding side effects was successful.
*/
public async loadPersistedValue(
storageItemKey?: PersistentKey
): Promise {
if (!this.ready) return false;
- const _storageItemKey = storageItemKey || this._key;
+ const _storageItemKey = storageItemKey ?? this._key;
- // Check if Collection is Persisted
+ // Check if Collection is already persisted
+ // (indicated by the persistence of 'true' at '_storageItemKey')
const isPersisted = await this.agileInstance().storages.get(
_storageItemKey,
this.config.defaultStorageKey as any
);
+
+ // Return 'false' if Collection isn't persisted yet
if (!isPersisted) return false;
- // Loads Values into Collection
+ // Helper function to load persisted values into the Collection
const loadValuesIntoCollection = async () => {
- const defaultGroup = this.collection().getGroup(
- this.collection().config.defaultGroupKey
+ const defaultGroup = this.collection().getDefaultGroup();
+ if (defaultGroup == null) return false;
+ const defaultGroupStorageKey = CollectionPersistent.getGroupStorageKey(
+ defaultGroup._key,
+ _storageItemKey
);
- if (!defaultGroup) return false;
- // Persist Default Group and load its Value manually to be 100% sure it got loaded
- defaultGroup.persist({
+ // Persist default Group and load its value manually to be 100% sure
+ // that it was loaded completely
+ defaultGroup.persist(defaultGroupStorageKey, {
loadValue: false,
- followCollectionPersistKeyPattern: true,
+ defaultStorageKey: this.config.defaultStorageKey || undefined,
+ storageKeys: this.storageKeys,
+ followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above
});
- if (defaultGroup.persistent?.ready) {
- await defaultGroup.persistent?.initialLoading();
- defaultGroup.isPersisted = true;
- }
-
- // Load Items into Collection
+ 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);
const itemStorageKey = CollectionPersistent.getItemStorageKey(
itemKey,
_storageItemKey
);
- // Get Storage Value
- const storageValue = await this.agileInstance().storages.get(
- itemStorageKey,
- this.config.defaultStorageKey as any
- );
- if (!storageValue) continue;
-
- // Collect found Storage Value
- this.collection().collect(storageValue);
+ // Persist and therefore load already present Item
+ if (item != null) {
+ item.persist(itemStorageKey, {
+ defaultStorageKey: this.config.defaultStorageKey || undefined,
+ storageKeys: this.storageKeys,
+ followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above
+ });
+ }
+ // Persist and therefore load not present Item
+ else {
+ // Create temporary placeholder Item in which the Item value will be loaded
+ const dummyItem = this.collection().createPlaceholderItem(itemKey);
+
+ // Persist dummy Item and load its value manually to be 100% sure
+ // that it was loaded completely and exists at all
+ dummyItem?.persist(itemStorageKey, {
+ loadValue: false,
+ defaultStorageKey: this.config.defaultStorageKey || undefined,
+ storageKeys: this.storageKeys,
+ followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above
+ });
+ if (dummyItem?.persistent?.ready) {
+ const loadedPersistedValueIntoItem = await dummyItem.persistent.loadPersistedValue(
+ itemStorageKey
+ ); // TODO FIRST GROUP REBUILD (by assigning loaded value to Item)
+
+ // If successfully loaded Item value, assign Item to Collection
+ if (loadedPersistedValueIntoItem)
+ this.collection().assignItem(dummyItem); // TODO SECOND GROUP REBUILD (by calling rebuildGroupThatInclude() in the assignItem() method)
+ }
+ }
}
+
return true;
};
const success = await loadValuesIntoCollection();
- // Persist Collection, so that the Storage Value updates dynamically if the Collection updates
- if (success) await this.persistValue(_storageItemKey);
+ // Setup side effects to keep the Storage value in sync
+ // with the Collection (Instances) value
+ if (success) this.setupSideEffects(_storageItemKey);
return success;
}
- //=========================================================================================================
- // Persist Value
- //=========================================================================================================
/**
+ * Persists Collection Instances (like Items or Groups) in the corresponding Storage
+ * and sets up side effects that dynamically update
+ * the Storage value when the Collection (Instances) changes.
+ *
* @internal
- * Sets everything up so that the Collection gets saved in the Storage
- * @param storageItemKey - Prefix Key of Persisted Instances (default PersistentKey)
- * @return Success?
+ * @param storageItemKey - Prefix Storage key of the to persist Collection Instances.
+ * | default = Persistent.key |
+ * @return Whether the persisting of the Collection Instances and the setting up of the corresponding side effects was successful.
*/
public async persistValue(storageItemKey?: PersistentKey): Promise {
if (!this.ready) return false;
- const _storageItemKey = storageItemKey || this._key;
- const defaultGroup = this.collection().getGroup(
- this.collection().config.defaultGroupKey
+ const _storageItemKey = storageItemKey ?? this._key;
+ const defaultGroup = this.collection().getDefaultGroup();
+ if (defaultGroup == null) return false;
+ const defaultGroupStorageKey = CollectionPersistent.getGroupStorageKey(
+ defaultGroup._key,
+ _storageItemKey
);
- if (!defaultGroup) return false;
- // Set Collection to Persisted (in Storage)
+ // Set flag in Storage to indicate that the Collection is persisted
this.agileInstance().storages.set(_storageItemKey, true, this.storageKeys);
// Persist default Group
- if (!defaultGroup.isPersisted)
- defaultGroup.persist({ followCollectionPersistKeyPattern: true });
-
- // Add sideEffect to default Group which adds and removes Items from the Storage depending on the Group Value
- defaultGroup.addSideEffect(
- CollectionPersistent.defaultGroupSideEffectKey,
- () => this.rebuildStorageSideEffect(defaultGroup, _storageItemKey),
- { weight: 0 }
- );
+ defaultGroup.persist(defaultGroupStorageKey, {
+ defaultStorageKey: this.config.defaultStorageKey || undefined,
+ storageKeys: this.storageKeys,
+ followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above
+ });
- // Persist Collection Items
+ // Persist Items found in the default Group's value
for (const itemKey of defaultGroup._value) {
const item = this.collection().getItem(itemKey);
const itemStorageKey = CollectionPersistent.getItemStorageKey(
itemKey,
_storageItemKey
);
- item?.persist(itemStorageKey);
+ item?.persist(itemStorageKey, {
+ defaultStorageKey: this.config.defaultStorageKey || undefined,
+ storageKeys: this.storageKeys,
+ followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above
+ });
}
+ // Setup side effects to keep the Storage value in sync
+ // with the Collection (Instances) value
+ this.setupSideEffects(_storageItemKey);
+
this.isPersisted = true;
return true;
}
- //=========================================================================================================
- // Remove Persisted Value
- //=========================================================================================================
/**
+ * Sets up side effects to keep the Storage value in sync
+ * with the Collection (Instances) value.
+ *
+ * @internal
+ * @param storageItemKey - Prefix Storage key of the to remove Collection Instances.
+ * | default = Persistent.key |
+ */
+ public setupSideEffects(storageItemKey?: PersistentKey): void {
+ const _storageItemKey = storageItemKey ?? this._key;
+ const defaultGroup = this.collection().getDefaultGroup();
+ if (defaultGroup == null) return;
+
+ // Add side effect to the default Group
+ // that adds and removes Items from the Storage based on the Group value
+ defaultGroup.addSideEffect(
+ CollectionPersistent.defaultGroupSideEffectKey,
+ (instance) => this.rebuildStorageSideEffect(instance, _storageItemKey),
+ { weight: 0 }
+ );
+ }
+
+ /**
+ * Removes the Collection from the corresponding Storage.
+ * -> Collection is no longer persisted
+ *
* @internal
- * Removes Collection from the Storage
- * @param storageItemKey - Prefix Key of Persisted Instances (default PersistentKey)
- * @return Success?
+ * @param storageItemKey - Prefix Storage key of the persisted Collection Instances.
+ * | default = Persistent.key |
+ * @return Whether the removal of the Collection Instances was successful.
*/
public async removePersistedValue(
storageItemKey?: PersistentKey
): Promise {
if (!this.ready) return false;
- const _storageItemKey = storageItemKey || this._key;
- const defaultGroup = this.collection().getGroup(
- this.collection().config.defaultGroupKey
- );
+ const _storageItemKey = storageItemKey ?? this._key;
+ const defaultGroup = this.collection().getDefaultGroup();
if (!defaultGroup) return false;
+ const defaultGroupStorageKey = CollectionPersistent.getGroupStorageKey(
+ defaultGroup._key,
+ _storageItemKey
+ );
- // Set Collection to not Persisted
+ // Remove Collection is persisted indicator flag from Storage
this.agileInstance().storages.remove(_storageItemKey, this.storageKeys);
- // Remove default Group from Storage
- defaultGroup.persistent?.removePersistedValue();
-
- // Remove Rebuild Storage sideEffect from default Group
+ // Remove default Group from the Storage
+ defaultGroup.persistent?.removePersistedValue(defaultGroupStorageKey);
defaultGroup.removeSideEffect(
CollectionPersistent.defaultGroupSideEffectKey
);
- // Remove Collection Items from Storage
+ // Remove Items found in the default Group's value from the Storage
for (const itemKey of defaultGroup._value) {
const item = this.collection().getItem(itemKey);
- item?.persistent?.removePersistedValue();
+ const itemStorageKey = CollectionPersistent.getItemStorageKey(
+ itemKey,
+ _storageItemKey
+ );
+ item?.persistent?.removePersistedValue(itemStorageKey);
}
this.isPersisted = false;
return true;
}
- //=========================================================================================================
- // Format Key
- //=========================================================================================================
/**
+ * Formats the specified key so that it can be used as a valid Storage key
+ * and returns the formatted variant of it.
+ *
+ * If no formatable key (`undefined`/`null`) was provided,
+ * an attempt is made to use the Collection identifier key as Storage key.
+ *
* @internal
- * Formats Storage Key
- * @param key - Key that gets formatted
+ * @param key - Storage key to be formatted.
*/
- public formatKey(key?: StorageKey): StorageKey | undefined {
- const collection = this.collection();
-
- // Get key from Collection
- if (key == null && collection._key) return collection._key;
-
+ public formatKey(key: StorageKey | undefined | null): StorageKey | undefined {
+ if (key == null && this.collection()._key) return this.collection()._key;
if (key == null) return;
-
- // Set Storage Key to Collection Key if Collection has no key
- if (collection._key == null) collection._key = key;
-
+ if (this.collection()._key == null) this.collection()._key = key;
return key;
}
- //=========================================================================================================
- // Rebuild Storage SideEffect
- //=========================================================================================================
/**
+ * Adds and removes Items from the Storage based on the Group value.
+ *
* @internal
- * Rebuilds Storage depending on Group
- * @param group - Group
- * @param key - Prefix Key of Persisted Instances (default PersistentKey)
+ * @param group - Group whose Items are to be dynamically added or removed from the Storage.
+ * @param storageItemKey - Prefix Storage key of the persisted Collection Instances.
+ * | default = Persistent.key |
*/
- public rebuildStorageSideEffect(group: Group, key?: PersistentKey) {
+ public rebuildStorageSideEffect(
+ group: Group,
+ storageItemKey?: PersistentKey
+ ) {
const collection = group.collection();
- const _key = key || collection.persistent?._key;
+ const _storageItemKey = storageItemKey || collection.persistent?._key;
- // Return if only a ItemKey got updated
+ // Return if no Item got added or removed
+ // because then the changed Item performs the Storage update itself
if (group.previousStateValue.length === group._value.length) return;
+ // Extract Item keys that got added or removed from the Group
const addedKeys = group._value.filter(
(key) => !group.previousStateValue.includes(key)
);
@@ -292,36 +334,43 @@ export class CollectionPersistent<
(key) => !group._value.includes(key)
);
- // Persist Added Keys
+ // Persist newly added Items
addedKeys.forEach((itemKey) => {
const item = collection.getItem(itemKey);
- const _itemKey = CollectionPersistent.getItemStorageKey(itemKey, _key);
- if (!item) return;
- if (!item.isPersisted) item.persist(_itemKey);
- else item.persistent?.persistValue(_itemKey);
+ const itemStorageKey = CollectionPersistent.getItemStorageKey(
+ itemKey,
+ _storageItemKey
+ );
+ if (item != null && !item.isPersisted)
+ item.persist(itemStorageKey, {
+ defaultStorageKey: this.config.defaultStorageKey || undefined,
+ storageKeys: this.storageKeys,
+ followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above
+ });
});
- // Unpersist removed Keys
+ // Remove removed Items from the Storage
removedKeys.forEach((itemKey) => {
const item = collection.getItem(itemKey);
- const _itemKey = CollectionPersistent.getItemStorageKey(itemKey, _key);
- if (!item) return;
- if (item.isPersisted) item.persistent?.removePersistedValue(_itemKey);
+ const itemStorageKey = CollectionPersistent.getItemStorageKey(
+ itemKey,
+ _storageItemKey
+ );
+ if (item != null && item.isPersisted)
+ item.persistent?.removePersistedValue(itemStorageKey);
});
}
- //=========================================================================================================
- // Get Item Storage Key
- //=========================================================================================================
/**
+ * Builds valid Item Storage key based on the 'Collection Item Persist Pattern'.
+ *
* @internal
- * Build Item StorageKey with Collection Persist Pattern
- * @param itemKey - Key of Item
- * @param collectionKey - Key of Collection
+ * @param itemKey - Key identifier of Item
+ * @param collectionKey - Key identifier of Collection
*/
public static getItemStorageKey(
- itemKey?: ItemKey,
- collectionKey?: CollectionKey
+ itemKey: ItemKey | undefined | null,
+ collectionKey: CollectionKey | undefined | null
): string {
if (itemKey == null || collectionKey == null)
LogCodeManager.log('1A:02:00');
@@ -332,24 +381,21 @@ export class CollectionPersistent<
.replace('${itemKey}', itemKey.toString());
}
- //=========================================================================================================
- // Get Group Storage Key
- //=========================================================================================================
/**
+ * Builds valid Item Storage key based on the 'Collection Group Persist Pattern'.
+ *
* @internal
- * Build Group StorageKey with Collection Persist Pattern
- * @param groupKey - Key of Group
- * @param collectionKey - Key of Collection
+ * @param groupKey - Key identifier of Group
+ * @param collectionKey - Key identifier of Collection
*/
public static getGroupStorageKey(
- groupKey?: GroupKey,
- collectionKey?: CollectionKey
+ groupKey: GroupKey | undefined | null,
+ collectionKey: CollectionKey | undefined | null
): string {
if (groupKey == null || collectionKey == null)
LogCodeManager.log('1A:02:01');
if (groupKey == null) groupKey = 'unknown';
if (collectionKey == null) collectionKey = 'unknown';
-
return this.storageGroupKeyPattern
.replace('${collectionKey}', collectionKey.toString())
.replace('${groupKey}', groupKey.toString());
diff --git a/packages/core/src/collection/group.ts b/packages/core/src/collection/group.ts
index 98fa4ccc..52dd9b6e 100644
--- a/packages/core/src/collection/group.ts
+++ b/packages/core/src/collection/group.ts
@@ -12,7 +12,6 @@ import {
isValidObject,
PersistentKey,
ComputedTracker,
- StateRuntimeJobConfigInterface,
StateIngestConfigInterface,
removeProperties,
LogCodeManager,
@@ -21,19 +20,32 @@ import {
export class Group extends State<
Array
> {
+ // Collection the Group belongs to
+ collection: () => Collection;
+
static rebuildGroupSideEffectKey = 'rebuildGroup';
- collection: () => Collection; // Collection the Group belongs to
- _output: Array = []; // Output of Group
- _items: Array<() => Item> = []; // Items of Group
- notFoundItemKeys: Array = []; // Contains all keys of Group that can't be found in Collection
+ // Item values represented by the Group
+ _output: Array = [];
+ // Items represented by the Group
+ _items: Array<() => Item> = [];
+
+ // Keeps track of all Item identifiers for Items that couldn't be found in the Collection
+ notFoundItemKeys: Array = [];
/**
+ * An extension of the State Class that categorizes and preserves the ordering of structured data.
+ * It allows us to cluster together data from a Collection as an array of Item keys.
+ *
+ * Note that a Group doesn't store the actual Items. It only keeps track of the Item keys
+ * and retrieves the fitting Items when needed.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/group/)
+ *
* @public
- * Group - Holds Items of Collection
- * @param collection - Collection to that the Group belongs
- * @param initialItems - Initial Key of Items in this Group
- * @param config - Config
+ * @param collection - Collection to which the Group belongs.
+ * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group.
+ * @param config - Configuration object
*/
constructor(
collection: Collection,
@@ -43,78 +55,79 @@ export class Group extends State<
super(collection.agileInstance(), initialItems || [], config);
this.collection = () => collection;
- // Add rebuild to sideEffects to rebuild Group on Value Change
+ // Add side effect to Group
+ // that rebuilds the Group whenever the Group value changes
this.addSideEffect(Group.rebuildGroupSideEffectKey, () => this.rebuild());
- // Initial Rebuild
+ // Initial rebuild
this.rebuild();
}
/**
+ * Returns the values of the Items clustered by the Group.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/group/properties#output)
+ *
* @public
- * Get Item Values of Group
*/
public get output(): Array {
ComputedTracker.tracked(this.observer);
- return this._output;
+ return copy(this._output);
}
- /**
- * @public
- * Set Item Values of Group
- */
public set output(value: DataType[]) {
- this._output = value;
+ LogCodeManager.log('1C:03:00', [this._key]);
}
/**
+ * Returns the Items clustered by the Group.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/group/properties#items)
+ *
* @public
- * Get Items of Group
*/
public get items(): Array> {
ComputedTracker.tracked(this.observer);
return this._items.map((item) => item());
}
- /**
- * @public
- * Set Items of Group
- */
public set items(value: Array>) {
- this._items = value.map((item) => () => item);
+ LogCodeManager.log('1C:03:01', [this._key]);
}
- //=========================================================================================================
- // Has
- //=========================================================================================================
/**
+ * Returns a boolean indicating whether an Item with the specified `itemKey`
+ * is clustered in the Group or not.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods/#has)
+ *
* @public
- * Checks if Group contains ItemKey
- * @param itemKey - ItemKey that gets checked
+ * @param itemKey - Key/Name identifier of the Item.
*/
public has(itemKey: ItemKey) {
return this.value.findIndex((key) => key === itemKey) !== -1;
}
- //=========================================================================================================
- // Size
- //=========================================================================================================
/**
+ * Returns the count of Items clustered by the Group.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/group/properties#size)
+ *
* @public
- * Get size of Group (-> How many Items it contains)
*/
public get size(): number {
return this.value.length;
}
- //=========================================================================================================
- // Remove
- //=========================================================================================================
/**
+ * Removes an Item with the specified key/name identifier from the Group,
+ * if it exists in the Group.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#remove)
+ *
* @public
- * Removes ItemKey/s from Group
- * @param itemKeys - ItemKey/s that get removed from Group
- * @param config - Config
+ * @param itemKeys - Key/Name identifier/s of the Item/s to be removed.
+ * @param config - Configuration object
*/
public remove(
itemKeys: ItemKey | ItemKey[],
@@ -125,7 +138,7 @@ export class Group extends State<
const notExistingItemKeys: Array = [];
let newGroupValue = copy(this.nextStateValue);
- // Remove ItemKeys from Group
+ // Remove itemKeys from Group
_itemKeys.forEach((itemKey) => {
// Check if itemKey exists in Group
if (!newGroupValue.includes(itemKey)) {
@@ -134,18 +147,19 @@ export class Group extends State<
return;
}
- // Check if ItemKey exists in Collection
+ // Check if itemKey exists in Collection
if (!this.collection().getItem(itemKey))
notExistingItemKeysInCollection.push(itemKey);
- // Remove ItemKey from Group
+ // Remove itemKey from Group
newGroupValue = newGroupValue.filter((key) => key !== itemKey);
});
- // Return if passed ItemKeys doesn't exist
+ // Return if none of the specified itemKeys exists
if (notExistingItemKeys.length >= _itemKeys.length) return this;
- // If all removed ItemKeys doesn't exist in Collection -> no rerender necessary since output doesn't change
+ // If all removed itemKeys don't exist in the Collection
+ // -> no rerender necessary since the output won't change
if (notExistingItemKeysInCollection.length >= _itemKeys.length)
config.background = true;
@@ -154,14 +168,14 @@ export class Group extends State<
return this;
}
- //=========================================================================================================
- // Add
- //=========================================================================================================
/**
+ * Appends new Item/s to the end of the Group.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#add)
+ *
* @public
- * Adds ItemKey/s to Group
- * @param itemKeys - ItemKey/s that get added to the Group
- * @param config - Config
+ * @param itemKeys - Key/Name identifier/s of Item/s to be added.
+ * @param config - Configuration object
*/
public add(
itemKeys: ItemKey | ItemKey[],
@@ -176,16 +190,15 @@ export class Group extends State<
overwrite: false,
});
- // Add ItemKeys to Group
+ // Add itemKeys to Group
_itemKeys.forEach((itemKey) => {
- const existsInGroup = newGroupValue.includes(itemKey);
-
- // Check if ItemKey exists in Collection
+ // Check if itemKey exists in Collection
if (!this.collection().getItem(itemKey))
notExistingItemKeysInCollection.push(itemKey);
- // Remove ItemKey from Group if it should get overwritten and already exists
- if (existsInGroup) {
+ // 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 {
@@ -194,14 +207,15 @@ export class Group extends State<
}
}
- // Add new ItemKey to Group
+ // Add new itemKey to Group
newGroupValue[config.method || 'push'](itemKey);
});
- // Return if passed ItemKeys already exist
+ // Return if all specified itemKeys already exist
if (existingItemKeys.length >= _itemKeys.length) return this;
- // If all added ItemKeys doesn't exist in Collection or already exist -> no rerender necessary since output doesn't change
+ // If all added itemKeys don't exist in the Collection
+ // -> no rerender necessary since the output won't change
if (
notExistingItemKeysInCollection.concat(existingItemKeys).length >=
_itemKeys.length
@@ -213,20 +227,20 @@ export class Group extends State<
return this;
}
- //=========================================================================================================
- // Replace
- //=========================================================================================================
/**
+ * Replaces the old `itemKey` with a new specified `itemKey`.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#replace)
+ *
* @public
- * Replaces oldItemKey with newItemKey
- * @param oldItemKey - Old ItemKey
- * @param newItemKey - New ItemKey
- * @param config - Config
+ * @param oldItemKey - Old `itemKey` to be replaced.
+ * @param newItemKey - New `itemKey` to replace the before specified old `itemKey`.
+ * @param config - Configuration object
*/
public replace(
oldItemKey: ItemKey,
newItemKey: ItemKey,
- config: StateRuntimeJobConfigInterface = {}
+ config: StateIngestConfigInterface = {}
): this {
const newGroupValue = copy(this._value);
newGroupValue.splice(newGroupValue.indexOf(oldItemKey), 1, newItemKey);
@@ -234,20 +248,29 @@ export class Group extends State<
return this;
}
- //=========================================================================================================
- // Persist
- //=========================================================================================================
/**
+ * Preserves the Group `value` in the corresponding external Storage.
+ *
+ * The Group key/name is used as the unique identifier for the Persistent.
+ * If that is not desired or the Group has no unique identifier,
+ * please specify a separate unique identifier for the Persistent.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist)
+ *
* @public
- * Stores Group Value into Agile Storage permanently
- * @param config - Config
+ * @param config - Configuration object
*/
public persist(config?: GroupPersistConfigInterface): this;
/**
+ * Preserves the Group `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/state/methods/#persist)
+ *
* @public
- * Stores Group Value into Agile Storage permanently
- * @param key - Key/Name of created Persistent (Note: Key required if Group has no set Key!)
- * @param config - Config
+ * @param key - Key/Name identifier of Persistent.
+ * @param config - Configuration object
*/
public persist(
key?: PersistentKey,
@@ -270,12 +293,12 @@ export class Group extends State<
_config = defineConfig(_config, {
loadValue: true,
- followCollectionPattern: false,
+ followCollectionPersistKeyPattern: true,
storageKeys: [],
defaultStorageKey: null,
});
- // Create storageItemKey based on Collection Name
+ // Create storageItemKey based on Collection key/name identifier
if (_config.followCollectionPersistKeyPattern) {
key = CollectionPersistent.getGroupStorageKey(
key || this._key,
@@ -283,6 +306,7 @@ export class Group extends State<
);
}
+ // Persist Group
super.persist(key, {
loadValue: _config.loadValue,
storageKeys: _config.storageKeys,
@@ -292,30 +316,33 @@ export class Group extends State<
return this;
}
- //=========================================================================================================
- // Rebuild
- //=========================================================================================================
/**
+ * Rebuilds the entire `output` and `items` property of the Group.
+ *
+ * In doing so, it traverses the Group `value` (Item identifiers)
+ * and fetches the fitting Items accordingly.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#rebuild)
+ *
* @internal
- * Rebuilds Output and Items of Group
*/
public rebuild(): this {
- const notFoundItemKeys: Array = []; // Item Keys that couldn't be found in Collection
+ const notFoundItemKeys: Array = []; // Item keys that couldn't be found in the Collection
const groupItems: Array> = [];
- // Don't rebuild Group if Collection is not properly instantiated
+ // Don't rebuild Group if Collection isn't correctly instantiated yet
// (because only after a successful instantiation the Collection
- // contains Items which are essential for a proper rebuild)
+ // contains the Items which are essential for a proper rebuild)
if (!this.collection().isInstantiated) return this;
- // Create groupItems by finding Item at ItemKey in Collection
+ // Fetch Items from Collection
this._value.forEach((itemKey) => {
const item = this.collection().getItem(itemKey);
if (item != null) groupItems.push(item);
else notFoundItemKeys.push(itemKey);
});
- // Create groupOutput out of groupItems
+ // Extract Item values from the retrieved Items
const groupOutput = groupItems.map((item) => {
return item.getPublicValue();
});
@@ -329,7 +356,7 @@ export class Group extends State<
);
}
- this.items = groupItems;
+ this._items = groupItems.map((item) => () => item);
this._output = groupOutput;
this.notFoundItemKeys = notFoundItemKeys;
@@ -339,36 +366,43 @@ export class Group extends State<
export type GroupKey = string | number;
-/**
- * @param method - Way of adding ItemKey to Group (push, unshift)
- * @param overwrite - If adding ItemKey overwrites old ItemKey (-> otherwise it gets added to the end of the Group)
- * @param background - If adding ItemKey happens in the background (-> not causing any rerender)
- */
export interface GroupAddConfigInterface extends StateIngestConfigInterface {
+ /**
+ * In which way the `itemKey` should be added to the Group.
+ * - '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?: 'unshift' | 'push';
+ /**
+ * If the to add `itemKey` already exists,
+ * whether its position should be overwritten with the position of the new `itemKey`.
+ * @default false
+ */
overwrite?: boolean;
}
-/**
- * @param background - If removing ItemKey happens in the background (-> not causing any rerender)
- */
-export interface GroupRemoveConfigInterface {
- background?: boolean;
-}
-
-/**
- * @param key - Key/Name of Group
- * @param isPlaceholder - If Group is initially a Placeholder
- */
export interface GroupConfigInterface {
+ /**
+ * Key/Name identifier of the Group.
+ * @default undefined
+ */
key?: GroupKey;
+ /**
+ * Whether the Group should be a placeholder
+ * and therefore should only exist in the background.
+ * @default false
+ */
isPlaceholder?: boolean;
}
-/**
- * @param useCollectionPattern - If Group storageKey follows the Collection Group StorageKey Pattern
- */
export interface GroupPersistConfigInterface
extends StatePersistentConfigInterface {
+ /**
+ * Whether to format the specified Storage key following the Collection Group Storage key pattern.
+ * `_${collectionKey}_group_${groupKey}`
+ * @default true
+ */
followCollectionPersistKeyPattern?: boolean;
}
diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts
index d88e53db..edf12d81 100644
--- a/packages/core/src/collection/index.ts
+++ b/packages/core/src/collection/index.ts
@@ -20,30 +20,51 @@ import {
removeProperties,
isFunction,
LogCodeManager,
+ PatchOptionConfigInterface,
} from '../internal';
export class Collection {
+ // Agile Instance the Collection belongs to
public agileInstance: () => Agile;
public config: CollectionConfigInterface;
private initialConfig: CreateCollectionConfigInterface;
- public size = 0; // Amount of Items stored in Collection
- public data: { [key: string]: Item } = {}; // Collection Data
+ // Key/Name identifier of the Collection
public _key?: CollectionKey;
- public isPersisted = false; // If Collection can be stored in Agile Storage (-> successfully integrated persistent)
- public persistent: CollectionPersistent | undefined; // Manages storing Collection Value into Storage
-
+ // 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
- * Collection - Class that holds a List of Objects with key and causes rerender on subscribed Components
- * @param agileInstance - An instance of Agile
- * @param config - Config
+ * @param agileInstance - Instance of Agile the Collection belongs to.
+ * @param config - Configuration object
*/
constructor(agileInstance: Agile, config: CollectionConfig = {}) {
this.agileInstance = () => agileInstance;
@@ -64,66 +85,84 @@ export class Collection {
this.initGroups(_config.groups as any);
this.initSelectors(_config.selectors as any);
- if (_config.initialData) this.collect(_config.initialData);
-
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 'instantiated' Collection
+ // 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,
- // the Groups which contain these added Items get rebuilt.
+ // (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
- * Set Key/Name of Collection
+ * @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
- * Get Key/Name of Collection
*/
public get key(): CollectionKey | undefined {
return this._key;
}
- //=========================================================================================================
- // Set Key
- //=========================================================================================================
/**
+ * Updates the key/name identifier of the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#setkey)
+ *
* @public
- * Set Key/Name of Collection
- * @param value - New Key/Name of Collection
+ * @param value - New key/name identifier.
*/
public setKey(value: CollectionKey | undefined) {
const oldKey = this._key;
- // Update State Key
+ // Update Collection key
this._key = value;
- // Update Key in Persistent (only if oldKey equal to persistentKey -> otherwise the PersistentKey got formatted and will be set where other)
+ // 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 && this.persistent?._key === oldKey)
this.persistent?.setKey(value);
return this;
}
- //=========================================================================================================
- // Group
- //=========================================================================================================
/**
+ * 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
- * Group - Holds Items of this Collection
- * @param initialItems - Initial ItemKeys of Group
- * @param config - Config
+ * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group.
+ * @param config - Configuration object
*/
public Group(
initialItems?: Array,
@@ -138,14 +177,20 @@ export class Collection {
return new Group(this, initialItems, config);
}
- //=========================================================================================================
- // Selector
- //=========================================================================================================
/**
+ * 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
- * Selector - Represents an Item of this Collection
- * @param initialKey - Key of Item that the Selector represents
- * @param config - Config
+ * @param initialKey - Key/Name identifier of the Item to be represented by the Selector.
+ * @param config - Configuration object
*/
public Selector(
initialKey: ItemKey,
@@ -160,18 +205,21 @@ export class Collection {
return new Selector(this, initialKey, config);
}
- //=========================================================================================================
- // Init Groups
- //=========================================================================================================
/**
+ * 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
- * Instantiates Groups
+ * @param groups - Entire Groups or Group keys to be set up.
*/
- public initGroups(groups: { [key: string]: Group } | string[]) {
+ public initGroups(groups: { [key: string]: Group } | string[]): void {
if (!groups) return;
let groupsObject: { [key: string]: Group } = {};
- // If groups is Array of GroupNames transform it to Group Object
+ // 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, [], {
@@ -185,25 +233,25 @@ export class Collection {
key: this.config.defaultGroupKey,
});
- // Set Key/Name of Group to property Name
+ // 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;
}
- //=========================================================================================================
- // Init Selectors
- //=========================================================================================================
/**
+ * Sets up the specified Selectors or Selector keys
+ * and assigns them to the Collection if they are valid.
+ *
* @internal
- * Instantiates Selectors
+ * @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 SelectorNames transform it to Selector Object
+ // 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(
@@ -216,29 +264,40 @@ export class Collection {
});
} else selectorsObject = selectors;
- // Set Key/Name of Selector to property Name
+ // 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;
}
- //=========================================================================================================
- // Collect
- //=========================================================================================================
/**
+ * 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
- * Collect Item/s
- * @param data - Data that gets added to Collection
- * @param groupKeys - Add collected Item/s to certain Groups
- * @param config - Config
+ * @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 | Array,
+ data: DataType | Item | Array>,
groupKeys?: GroupKey | Array,
config: CollectConfigInterface = {}
): this {
- const _data = normalizeArray(data);
+ const _data = normalizeArray>(data);
const _groupKeys = normalizeArray(groupKeys);
const defaultGroupKey = this.config.defaultGroupKey;
const primaryKey = this.config.primaryKey;
@@ -249,7 +308,7 @@ export class Collection {
select: false,
});
- // Add default GroupKey, because Items get always added to default Group
+ // Add default groupKey, since all Items are added to the default Group
if (!_groupKeys.includes(defaultGroupKey)) _groupKeys.push(defaultGroupKey);
// Create not existing Groups
@@ -258,39 +317,51 @@ export class Collection {
);
_data.forEach((data, index) => {
- const itemKey = data[primaryKey];
-
- // Add Item to Collection
- const success = this.setData(data, {
- patch: config.patch,
- background: config.background,
- });
- if (!success) return this;
+ let itemKey;
+ let success = false;
- // Add ItemKey to provided Groups
- _groupKeys.forEach((groupKey) => {
- this.getGroup(groupKey)?.add(itemKey, {
- method: config.method,
+ // 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, index);
+ if (config.select) this.createSelector(itemKey, itemKey);
+ }
+
+ if (config.forEachItem) config.forEachItem(data, itemKey, success, index);
});
return this;
}
- //=========================================================================================================
- // Update
- //=========================================================================================================
/**
+ * 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
- * Updates Item at provided Key
- * @param itemKey - ItemKey of Item that gets updated
- * @param changes - Changes that will be merged into the Item (flatMerge)
- * @param config - Config
+ * @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,
@@ -304,6 +375,7 @@ export class Collection {
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;
@@ -315,17 +387,17 @@ export class Collection {
const oldItemKey = item._value[primaryKey];
const newItemKey = changes[primaryKey] || oldItemKey;
- const updateItemKey = oldItemKey !== newItemKey;
- // Update ItemKey
- if (updateItemKey)
+ // 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
+ // Patch changes into Item data object
if (config.patch) {
- // Delete primaryKey from 'changes' because if it has changed, it gets properly updated in 'updateItemKey' (see above)
+ // 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 } =
@@ -334,22 +406,19 @@ export class Collection {
addNewProperties: true,
});
- // Apply changes to Item
item.patch(changes as any, {
background: config.background,
addNewProperties: patchConfig.addNewProperties,
});
}
-
- // Set changes into Item
- if (!config.patch) {
- // To make sure that the primaryKey doesn't differ from the changes object primaryKey
+ // 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);
}
- // Apply changes to Item
item.set(changes as any, {
background: config.background,
});
@@ -358,21 +427,20 @@ export class Collection {
return item;
}
- //=========================================================================================================
- // Create Group
- //=========================================================================================================
/**
+ * Creates a new Group and associates it to the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createGroup)
+ *
* @public
- * Creates new Group that can hold Items of Collection
- * @param groupKey - Name/Key of Group
- * @param initialItems - Initial ItemKeys of Group
+ * @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
@@ -385,21 +453,22 @@ export class Collection {
return group;
}
- // Create Group
+ // Create new Group
group = new Group(this, initialItems, { key: groupKey });
this.groups[groupKey] = group;
return group;
}
- //=========================================================================================================
- // Has 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
- * Check if Group exists in Collection
- * @param groupKey - Key/Name of Group
- * @param config - Config
+ * @param groupKey - Key/Name identifier of the Group to be checked for existence.
+ * @param config - Configuration object
*/
public hasGroup(
groupKey: GroupKey | undefined,
@@ -408,14 +477,16 @@ export class Collection {
return !!this.getGroup(groupKey, config);
}
- //=========================================================================================================
- // Get Group
- //=========================================================================================================
/**
+ * 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
- * Get Group by Key/Name
- * @param groupKey - Key/Name of Group
- * @param config - Config
+ * @param groupKey - Key/Name identifier of the Group.
+ * @param config - Configuration object
*/
public getGroup(
groupKey: GroupKey | undefined,
@@ -425,35 +496,44 @@ export class Collection {
notExisting: false,
});
- // Get Group
+ // Retrieve Group
const group = groupKey ? this.groups[groupKey] : undefined;
- // Check if Group exists
- if (group == null || (!config.notExisting && group.isPlaceholder))
+ // Check if retrieved Group exists
+ if (group == null || (!config.notExisting && !group.exists))
return undefined;
ComputedTracker.tracked(group.observer);
return group;
}
- //=========================================================================================================
- // Get Default 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
- * Get default Group of Collection
*/
public getDefaultGroup(): Group | undefined {
return this.getGroup(this.config.defaultGroupKey);
}
- //=========================================================================================================
- // Get Group With Reference
- //=========================================================================================================
/**
+ * 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
- * Get Group by Key/Name or a Reference to it if it doesn't exist yet
- * @param groupKey - Name/Key of Group
+ * @param groupKey - Key/Name identifier of the Group.
*/
public getGroupWithReference(groupKey: GroupKey): Group {
let group = this.getGroup(groupKey, { notExisting: true });
@@ -471,35 +551,47 @@ export class Collection {
return group;
}
- //=========================================================================================================
- // Remove 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
- * Removes Group by Key/Name
- * @param groupKey - Name/Key of Group
+ * @param groupKey - Key/Name identifier of the Group to be removed.
*/
public removeGroup(groupKey: GroupKey): this {
- if (this.groups[groupKey] == null) return this;
- delete this.groups[groupKey];
+ if (this.groups[groupKey] != null) delete this.groups[groupKey];
return this;
}
- //=========================================================================================================
- // Create Selector
- //=========================================================================================================
/**
+ * Returns the count of registered Groups in the Collection.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupcount)
+ *
* @public
- * Creates new Selector that represents an Item of the Collection
- * @param selectorKey - Name/Key of Selector
- * @param itemKey - Key of Item which the Selector represents
+ */
+ 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
): Selector {
let selector = this.getSelector(selectorKey, { notExisting: true });
-
if (!this.isInstantiated) LogCodeManager.log('1B:02:04');
// Check if Selector already exists
@@ -512,7 +604,7 @@ export class Collection {
return selector;
}
- // Create Selector
+ // Create new Selector
selector = new Selector(this, itemKey, {
key: selectorKey,
});
@@ -521,26 +613,35 @@ export class Collection {
return selector;
}
- //=========================================================================================================
- // Select
- //=========================================================================================================
/**
+ * 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
- * Creates new Selector that represents an Item of the Collection
- * @param itemKey - Key of Item which the Selector represents
+ * @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);
}
- //=========================================================================================================
- // Has Selector
- //=========================================================================================================
/**
+ * 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
- * Check if Selector exists in Collection
- * @param selectorKey - Key/Name of Selector
- * @param config - Config
+ * @param selectorKey - Key/Name identifier of the Selector to be checked for existence.
+ * @param config - Configuration object
*/
public hasSelector(
selectorKey: SelectorKey | undefined,
@@ -549,14 +650,16 @@ export class Collection {
return !!this.getSelector(selectorKey, config);
}
- //=========================================================================================================
- // Get Selector
- //=========================================================================================================
/**
+ * 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
- * Get Selector by Key/Name
- * @param selectorKey - Key/Name of Selector
- * @param config - Config
+ * @param selectorKey - Key/Name identifier of the Selector.
+ * @param config - Configuration object
*/
public getSelector(
selectorKey: SelectorKey | undefined,
@@ -570,20 +673,24 @@ export class Collection {
const selector = selectorKey ? this.selectors[selectorKey] : undefined;
// Check if Selector exists
- if (selector == null || (!config.notExisting && selector.isPlaceholder))
+ if (selector == null || (!config.notExisting && !selector.exists))
return undefined;
ComputedTracker.tracked(selector.observer);
return selector;
}
- //=========================================================================================================
- // Get Selector With Reference
- //=========================================================================================================
/**
+ * 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
- * Get Selector by Key/Name or a Reference to it if it doesn't exist yet
- * @param selectorKey - Name/Key of Selector
+ * @param selectorKey - Key/Name identifier of the Selector.
*/
public getSelectorWithReference(
selectorKey: SelectorKey
@@ -607,29 +714,45 @@ export class Collection {
return selector;
}
- //=========================================================================================================
- // Remove 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
- * Removes Selector by Key/Name
- * @param selectorKey - Name/Key of Selector
+ * @param selectorKey - Key/Name identifier of the Selector to be removed.
*/
public removeSelector(selectorKey: SelectorKey): this {
- if (this.selectors[selectorKey] == null) return this;
- this.selectors[selectorKey].unselect(); // Unselects current selected Item
- delete this.selectors[selectorKey];
+ if (this.selectors[selectorKey] != null) {
+ this.selectors[selectorKey].unselect();
+ delete this.selectors[selectorKey];
+ }
return this;
}
- //=========================================================================================================
- // Has Item
- //=========================================================================================================
/**
+ * 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
- * Check if Item exists in Collection
- * @param itemKey - Key/Name of Item
- * @param config - Config
+ * @param itemKey - Key/Name identifier of the Item.
+ * @param config - Configuration object
*/
public hasItem(
itemKey: ItemKey | undefined,
@@ -638,14 +761,16 @@ export class Collection {
return !!this.getItem(itemKey, config);
}
- //=========================================================================================================
- // Get Item by Id
- //=========================================================================================================
/**
+ * 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
- * Get Item by Key/Name
- * @param itemKey - ItemKey of Item
- * @param config - Config
+ * @param itemKey - Key/Name identifier of the Item.
+ * @param config - Configuration object
*/
public getItem(
itemKey: ItemKey | undefined,
@@ -666,40 +791,71 @@ export class Collection {
}
/**
+ * 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
- * Get Item by Key/Name or a Reference to it if it doesn't exist yet
- * @param itemKey - Key/Name of Item
+ * @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 = new Item(
- this,
- {
- [this.config.primaryKey]: itemKey, // Setting PrimaryKey of Item to passed itemKey
- dummy: 'item',
- } as any,
- {
- isPlaceholder: true,
- }
- );
+ if (item == null) item = this.createPlaceholderItem(itemKey, true);
+
+ ComputedTracker.tracked(item.observer);
+ 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.observer);
return item;
}
- //=========================================================================================================
- // Get Value by Id
- //=========================================================================================================
/**
+ * 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
- * Get Value of Item by Key/Name
- * @param itemKey - ItemKey of Item that holds the Value
- * @param config - Config
+ * @param itemKey - Key/Name identifier of the Item.
+ * @param config - Configuration object
*/
public getItemValue(
itemKey: ItemKey | undefined,
@@ -710,13 +866,18 @@ export class Collection {
return item.value;
}
- //=========================================================================================================
- // Get All Items
- //=========================================================================================================
/**
+ * 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
- * Get all Items of Collection
- * @param config - Config
+ * @param config - Configuration object
*/
public getAllItems(config: HasConfigInterface = {}): Array> {
config = defineConfig(config, {
@@ -726,46 +887,62 @@ export class Collection {
const defaultGroup = this.getDefaultGroup();
let items: Array> = [];
- // If config.notExisting transform this.data into array, otherwise return the default Group items
+ // 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 defaultGroup 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 useAgile() and should do it here too
+ // 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?.items || [];
}
return items;
}
- //=========================================================================================================
- // Get All Item Values
- //=========================================================================================================
/**
+ * 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
- * Get all Values of Items in a Collection
- * @param config - Config
+ * @param config - Configuration object
*/
public getAllItemValues(config: HasConfigInterface = {}): Array {
const items = this.getAllItems(config);
return items.map((item) => item.value);
}
- //=========================================================================================================
- // Persist
- //=========================================================================================================
/**
+ * 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
- * Stores Collection Value into Agile Storage permanently
- * @param config - Config
+ * @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
- * Stores Collection Value into Agile Storage permanently
- * @param key - Key/Name of created Persistent (Note: Key required if Collection has no set Key!)
- * @param config - Config
+ * @param key - Key/Name identifier of Persistent.
+ * @param config - Configuration object
*/
public persist(
key?: StorageKey,
@@ -792,7 +969,10 @@ export class Collection {
defaultStorageKey: null,
});
- // Create persistent -> Persist Value
+ // 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,
@@ -803,67 +983,44 @@ export class Collection {
return this;
}
- //=========================================================================================================
- // On Load
- //=========================================================================================================
/**
+ * 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
- * Callback Function that gets called if the persisted Value gets loaded into the Collection for the first Time
- * Note: Only useful for persisted Collections!
- * @param callback - Callback Function
+ * @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;
-
- // Check if Callback is valid Function
if (!isFunction(callback)) {
LogCodeManager.log('00:03:01', ['OnLoad Callback', 'function']);
return this;
}
+ // Register specified callback
this.persistent.onLoad = callback;
- // If Collection is already 'isPersisted' the loading was successful -> callback can be called
+ // If Collection is already persisted ('isPersisted') fire specified callback immediately
if (this.isPersisted) callback(true);
return this;
}
- //=========================================================================================================
- // Get Group Count
- //=========================================================================================================
- /**
- * @public
- * Get count of registered Groups in Collection
- */
- public getGroupCount(): number {
- let size = 0;
- for (const group in this.groups) size++;
- return size;
- }
-
- //=========================================================================================================
- // Get Selector Count
- //=========================================================================================================
- /**
- * @public
- * Get count of registered Selectors in Collection
- */
- public getSelectorCount(): number {
- let size = 0;
- for (const selector in this.selectors) size++;
- return size;
- }
-
- //=========================================================================================================
- // Reset
- //=========================================================================================================
/**
+ * 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
- * Resets this Collection
*/
public reset(): this {
- // Reset Data
+ // Reset data
this.data = {};
this.size = 0;
@@ -876,15 +1033,15 @@ export class Collection {
return this;
}
- //=========================================================================================================
- // Put
- //=========================================================================================================
/**
+ * Puts `itemKeys/s` into Group/s.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#put)
+ *
* @public
- * Puts ItemKey/s into Group/s (GroupKey/s)
- * @param itemKeys - ItemKey/s that get added to provided Group/s
- * @param groupKeys - Group/s to which the ItemKey/s get added
- * @param config - Config
+ * @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,
@@ -894,7 +1051,7 @@ export class Collection {
const _itemKeys = normalizeArray(itemKeys);
const _groupKeys = normalizeArray(groupKeys);
- // Add ItemKeys to Groups
+ // Assign itemKeys to Groups
_groupKeys.forEach((groupKey) => {
this.getGroup(groupKey)?.add(_itemKeys, config);
});
@@ -902,16 +1059,16 @@ export class Collection {
return this;
}
- //=========================================================================================================
- // Move
- //=========================================================================================================
/**
+ * Moves specified `itemKey/s` from one Group to another Group.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#move)
+ *
* @public
- * Move ItemKey/s from one Group to another
- * @param itemKeys - ItemKey/s that are moved
- * @param oldGroupKey - GroupKey of the Group that currently keeps the Items at itemKey/s
- * @param newGroupKey - GroupKey of the Group into which the Items at itemKey/s are moved
- * @param config - Config
+ * @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,
@@ -927,21 +1084,21 @@ export class Collection {
removeProperties(config, ['method', 'overwrite'])
);
- // Add itemKeys to new Group
+ // Assign itemKeys to new Group
this.getGroup(newGroupKey)?.add(_itemKeys, config);
return this;
}
- //=========================================================================================================
- // Update Item Key
- //=========================================================================================================
/**
+ * Updates the key/name identifier of the Item
+ * and returns a boolean indicating
+ * whether the Item identifier was updated successfully.
+ *
* @internal
- * Updates Key/Name of Item in all Instances (Group, Selector, ..)
- * @param oldItemKey - Old ItemKey
- * @param newItemKey - New ItemKey
- * @param config - Config
+ * @param oldItemKey - Old key/name Item identifier.
+ * @param newItemKey - New key/name Item identifier.
+ * @param config - Configuration object
*/
public updateItemKey(
oldItemKey: ItemKey,
@@ -961,39 +1118,47 @@ export class Collection {
return false;
}
- // Remove Item from old ItemKey and add Item to new ItemKey
+ // Update itemKey in data object
delete this.data[oldItemKey];
this.data[newItemKey] = item;
- // Update Key/Name of Item
+ // Update key/name of the Item
item.setKey(newItemKey, {
background: config.background,
});
- // Update persist Key of Item (Doesn't get updated by updating key of Item because PersistKey is special formatted)
- item.persistent?.setKey(
- CollectionPersistent.getItemStorageKey(newItemKey, this._key)
- );
+ // 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
+ // Update itemKey in Groups
for (const groupKey in this.groups) {
const group = this.getGroup(groupKey, { notExisting: true });
- if (!group?.has(oldItemKey)) continue;
- group?.replace(oldItemKey, newItemKey, { background: config.background });
+ if (group == null || !group.has(oldItemKey)) continue;
+ group.replace(oldItemKey, newItemKey, { background: config.background });
}
- // Update ItemKey in Selectors
+ // 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 the reference placeholder Item got removed
- // and replaced with the new Item (Item of which the primaryKey was renamed)
- // -> needs to find new Item with the same itemKey
+ // 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
+ force: true, // Because itemKeys are the same (but not the Items at this itemKey anymore)
background: config.background,
});
}
@@ -1008,30 +1173,45 @@ export class Collection {
return true;
}
- //=========================================================================================================
- // Get GroupKeys That Have ItemKey
- //=========================================================================================================
/**
+ * 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
- * Gets GroupKeys that contain the passed ItemKey
- * @param itemKey - ItemKey
+ * @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.getGroup(groupKey, { notExisting: true });
+ const group = this.groups[groupKey];
if (group?.has(itemKey)) groupKeys.push(groupKey);
}
return groupKeys;
}
- //=========================================================================================================
- // Remove
- //=========================================================================================================
/**
+ * 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
- * Remove Items from Collection
- * @param itemKeys - ItemKey/s that get removed
+ * @param itemKeys - Item/s with identifier/s to be removed.
*/
public remove(
itemKeys: ItemKey | Array
@@ -1046,14 +1226,14 @@ export class Collection {
};
}
- //=========================================================================================================
- // Remove From Groups
- //=========================================================================================================
/**
+ * Remove Item/s from specified Group/s.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removefromgroups)
+ *
* @public
- * Removes Item/s from Group/s
- * @param itemKeys - ItemKey/s that get removed from Group/s
- * @param groupKeys - GroupKey/s of Group/s form which the ItemKey/s will be removed
+ * @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,
@@ -1065,7 +1245,7 @@ export class Collection {
_itemKeys.forEach((itemKey) => {
let removedFromGroupsCount = 0;
- // Remove ItemKey from Groups
+ // Remove itemKey from the Groups
_groupKeys.forEach((groupKey) => {
const group = this.getGroup(groupKey, { notExisting: true });
if (!group?.has(itemKey)) return;
@@ -1073,7 +1253,8 @@ export class Collection {
removedFromGroupsCount++;
});
- // If Item got removed from every Groups the Item was in, remove it completely
+ // If the Item was removed from each Group representing the Item,
+ // remove it completely
if (
removedFromGroupsCount >=
this.getGroupKeysThatHaveItemKey(itemKey).length
@@ -1084,14 +1265,14 @@ export class Collection {
return this;
}
- //=========================================================================================================
- // Remove Items
- //=========================================================================================================
/**
+ * 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
- * Removes Item completely from Collection
- * @param itemKeys - ItemKey/s of Item/s
- * @param config - Config
+ * @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,
@@ -1108,7 +1289,7 @@ export class Collection {
if (item == null) return;
const wasPlaceholder = item.isPlaceholder;
- // Remove Item from Groups
+ // 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);
@@ -1120,16 +1301,18 @@ export class Collection {
// Remove Item from Collection
delete this.data[itemKey];
- // Reselect or remove Selectors representing the removed Item
+ // 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?.hasSelected(itemKey, false)) {
+ if (selector != null && selector.hasSelected(itemKey, false)) {
if (config.removeSelector) {
// Remove Selector
- this.removeSelector(selector?._key ?? 'unknown');
+ this.removeSelector(selector._key ?? 'unknown');
} else {
- // Reselect Item in Selector (to create new dummyItem to hold a reference to this removed Item)
- selector?.reselect({ force: true });
+ // 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 });
}
}
}
@@ -1140,68 +1323,134 @@ export class Collection {
return this;
}
- //=========================================================================================================
- // Set Data
- //=========================================================================================================
/**
+ * 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
- * Updates existing or creates Item from provided Data
- * @param data - Data
- * @param config - Config
+ * @param data - Data object
+ * @param config - Configuration object
*/
- public setData(data: DataType, config: SetDataConfigInterface = {}): boolean {
- const _data = copy(data as any); // Transformed Data to any because of unknown Object (DataType)
- const primaryKey = this.config.primaryKey;
+ 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, this.config.primaryKey]);
- _data[this.config.primaryKey] = generateId();
+ LogCodeManager.log('1B:02:05', [this._key, primaryKey]);
+ _data[primaryKey] = generateId();
}
const itemKey = _data[primaryKey];
- let item = this.getItem(itemKey, { notExisting: true });
+ const item = this.getItem(itemKey, { notExisting: true });
const wasPlaceholder = item?.isPlaceholder || false;
- const createItem = item == null;
-
- // Create or update Item
- if (!createItem && config.patch)
- item?.patch(_data, { background: config.background });
- if (!createItem && !config.patch)
- item?.set(_data, { background: config.background });
- if (createItem) {
- // Create and assign Item to Collection
- item = new Item(this, _data);
- this.data[itemKey] = item;
- // Rebuild Groups That include ItemKey after assigning Item to Collection (otherwise it can't find Item)
- this.rebuildGroupsThatIncludeItemKey(itemKey, {
+ // 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 (createItem || wasPlaceholder) this.size++;
+ // Increase size of Collection if Item was previously a placeholder
+ // (-> hasn't officially existed in Collection before)
+ if (wasPlaceholder) this.size++;
return true;
}
- //=========================================================================================================
- // Rebuild Groups That Includes Item Key
- //=========================================================================================================
/**
+ * 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
- * Rebuilds Groups that include the provided ItemKey
- * @itemKey - Item Key
- * @config - Config
+ * @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) != null) {
+ if (!config.overwrite) 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,
@@ -1215,16 +1464,19 @@ export class Collection {
},
});
- // Rebuild Groups that include ItemKey
+ // Rebuild Groups that include itemKey
for (const groupKey in this.groups) {
const group = this.getGroup(groupKey);
if (group?.has(itemKey)) {
- // group.rebuild(); Not necessary because a sideEffect of the Group is to rebuild it self
+ // Not necessary because a sideEffect of ingesting the Group
+ // into the runtime is to rebuilt itself
+ // group.rebuild();
+
group?.ingest({
background: config?.background,
- force: true, // because Group value doesn't change only the output changes
+ force: true, // because Group value didn't change, only the output might change
sideEffects: config?.sideEffects,
- storage: false, // because Group only rebuilds and doesn't change its value
+ storage: false, // because Group only rebuilds (-> actual persisted value hasn't changed)
});
}
}
@@ -1235,115 +1487,206 @@ export type DefaultItem = Record; // same as { [key: string]: any }
export type CollectionKey = string | number;
export type ItemKey = string | number;
-/**
- * @param key - Key/Name of Collection
- * @param groups - Groups of Collection
- * @param selectors - Selectors of Collection
- * @param primaryKey - Name of Property that holds the PrimaryKey (default = id)
- * @param defaultGroupKey - Key/Name of Default Group that holds all collected Items
- * @param initialData - Initial Data of Collection
- */
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;
}
-/**
- * @param primaryKey - Name of Property that holds the PrimaryKey (default = id)
- * @param defaultGroupKey - Key/Name of Default Group that holds all collected Items
- */
+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;
}
-/**
- * @param patch - If Item gets patched into existing Item with the same Id
- * @param method - Way of adding Item to Collection (push, unshift)
- * @param forEachItem - Gets called for each Item that got collected
- * @param background - If collecting an Item happens in the background (-> not causing any rerender)
- * @param select - If collected Items get selected with a Selector
- */
-export interface CollectConfigInterface {
- patch?: boolean;
+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';
- forEachItem?: (data: DataType, key: ItemKey, index: number) => void;
- background?: boolean;
+ /**
+ * 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;
}
-/**
- * @param patch - If Data gets merged into the current Data
- * @param background - If updating an Item happens in the background (-> not causing any rerender)
- */
export interface UpdateConfigInterface {
- patch?: boolean | { addNewProperties?: boolean };
+ /**
+ * 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;
}
-/**
- * @param background - If updating the primaryKey of an Item happens in the background (-> not causing any rerender)
- */
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;
}
-/**
- * @param background - If assigning a new value happens in the background (-> not causing any rerender)
- * @param force - Force creating and performing Job
- * @param sideEffects - If Side Effects of Group gets executed
- */
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;
- force?: boolean;
+ /**
+ * Whether to execute the defined side effects.
+ * @default true
+ */
sideEffects?: SideEffectConfigInterface;
}
-/**
- * @param notExisting - If placeholder can be found
- */
export interface HasConfigInterface {
+ /**
+ * Whether Items that do not officially exist,
+ * such as placeholder Items, can be found
+ * @default true
+ */
notExisting?: boolean;
}
-/**
- * @param loadValue - If Persistent loads the persisted value into the Collection
- * @param storageKeys - Key/Name of Storages which gets used to persist the Collection Value (NOTE: If not passed the default Storage will be used)
- * @param defaultStorageKey - Default Storage Key (if not provided it takes the first index of storageKeys or the AgileTs default Storage)
- */
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;
}
-/*
- * @param notExisting - If not existing Items like placeholder Items can be removed.
- * Keep in mind that sometimes it won't remove the Item entirely
- * because another Instance (like a Selector) needs to keep reference to it.
- * https://github.com/agile-ts/agile/pull/152
- * @param - If Selectors that have selected an Item to be removed, should be removed too
- */
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;
}
-/**
- * @param patch - If Data gets patched into existing Item
- * @param background - If assigning Data happens in background
- */
-export interface SetDataConfigInterface {
+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 type CollectionConfig =
- | CreateCollectionConfigInterface
- | ((
- collection: Collection
- ) => CreateCollectionConfigInterface);
+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;
+}
diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts
index 35843c30..5375b143 100644
--- a/packages/core/src/collection/item.ts
+++ b/packages/core/src/collection/item.ts
@@ -1,26 +1,37 @@
import {
State,
Collection,
- DefaultItem,
StateKey,
StateRuntimeJobConfigInterface,
defineConfig,
SelectorKey,
+ PersistentKey,
+ isValidObject,
+ CollectionPersistent,
+ StatePersistentConfigInterface,
+ DefaultItem,
} from '../internal';
export class Item extends State<
DataType
> {
- static updateGroupSideEffectKey = 'rebuildGroup';
- public selectedBy: Set = new Set(); // Keys of Selectors that have selected this Item
+ // Collection the Group belongs to
public collection: () => Collection;
+ static updateGroupSideEffectKey = 'rebuildGroup';
+
+ // Key/Name identifiers of Selectors which have selected the Item
+ public selectedBy: Set = new Set();
+
/**
+ * An extension of the State Class that represents a single data object of a Collection.
+ *
+ * It can be used independently, but is always synchronized with the Collection.
+ *
* @public
- * Item of Collection
- * @param collection - Collection to which the Item belongs
- * @param data - Data that the Item holds
- * @param config - Config
+ * @param collection - Collection to which the Item belongs.
+ * @param data - Data object to be represented by the Item.
+ * @param config - Configuration object
*/
constructor(
collection: Collection,
@@ -29,24 +40,23 @@ export class Item extends State<
) {
super(collection.agileInstance(), data, {
isPlaceholder: config.isPlaceholder,
- key: data[collection.config.primaryKey], // Set Key/Name of Item to primaryKey of Data
+ key: data[collection.config.primaryKey], // Set key/name of Item to identifier at primaryKey property
});
this.collection = () => collection;
- // Add rebuildGroupsThatIncludeItemKey to sideEffects to rebuild Groups that include this Item if it mutates
- this.addRebuildGroupThatIncludeItemKeySideEffect(
- this._key != null ? this._key : 'unknown'
- );
+ // Add side effect to Item
+ // that rebuilds all Groups containing the Item whenever it changes
+ if (this._key != null) {
+ this.addRebuildGroupThatIncludeItemKeySideEffect(this._key);
+ }
}
- //=========================================================================================================
- // Set Key
- //=========================================================================================================
/**
+ * Updates the key/name identifier of Item.
+ *
* @internal
- * Updates Key/Name of State
- * @param value - New Key/Name of State
- * @param config - Config
+ * @param value - New key/name identifier.
+ * @param config - Configuration object
*/
public setKey(
value: StateKey | undefined,
@@ -65,47 +75,122 @@ export class Item extends State<
});
if (value == null) return this;
- // Remove old rebuildGroupsThatIncludeItemKey sideEffect
+ // Update 'rebuildGroupsThatIncludeItemKey' side effect to the new itemKey
this.removeSideEffect(Item.updateGroupSideEffectKey);
-
- // Add rebuildGroupsThatIncludeItemKey to sideEffects to rebuild Groups that include this Item if it mutates
this.addRebuildGroupThatIncludeItemKeySideEffect(value);
- // Update ItemKey in ItemValue (After updating the sideEffect because otherwise it calls the old sideEffect)
- this.patch(
- { [this.collection().config.primaryKey]: value },
- {
- sideEffects: config.sideEffects,
- background: config.background,
- force: config.force,
- storage: config.storage,
- overwrite: config.overwrite,
- }
- );
+ // Update itemKey in Item value
+ // (After updating the side effect, because otherwise it would call the old side effect)
+ this.patch({ [this.collection().config.primaryKey]: value }, config);
+
+ return this;
+ }
+
+ /**
+ * Preserves the Item `value` in the corresponding external Storage.
+ *
+ * The Item key/name is used as the unique identifier for the Persistent.
+ * If that is not desired or the Item has no unique identifier,
+ * please specify a separate unique identifier for the Persistent.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist)
+ *
+ * @public
+ * @param config - Configuration object
+ */
+ public persist(config?: ItemPersistConfigInterface): this;
+ /**
+ * Preserves the Item `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/state/methods/#persist)
+ *
+ * @public
+ * @param key - Key/Name identifier of Persistent.
+ * @param config - Configuration object
+ */
+ public persist(
+ key?: PersistentKey,
+ config?: ItemPersistConfigInterface
+ ): this;
+ public persist(
+ keyOrConfig: PersistentKey | ItemPersistConfigInterface = {},
+ config: ItemPersistConfigInterface = {}
+ ): this {
+ let _config: ItemPersistConfigInterface;
+ let key: PersistentKey | undefined;
+
+ if (isValidObject(keyOrConfig)) {
+ _config = keyOrConfig as ItemPersistConfigInterface;
+ key = this._key;
+ } else {
+ _config = config || {};
+ key = keyOrConfig as PersistentKey;
+ }
+
+ _config = defineConfig(_config, {
+ loadValue: true,
+ followCollectionPersistKeyPattern: true,
+ storageKeys: [],
+ defaultStorageKey: null,
+ });
+
+ // Create storageItemKey based on Collection key/name identifier
+ if (_config.followCollectionPersistKeyPattern) {
+ key = CollectionPersistent.getItemStorageKey(
+ key || this._key,
+ this.collection()._key
+ );
+ }
+
+ // Persist Item
+ super.persist(key, {
+ loadValue: _config.loadValue,
+ storageKeys: _config.storageKeys,
+ defaultStorageKey: _config.defaultStorageKey,
+ });
+
return this;
}
- //=========================================================================================================
- // Add Rebuild Group That Include ItemKey SideEffect
- //=========================================================================================================
/**
+ * Adds side effect to Item
+ * that rebuilds all Groups containing the specified Item identifier
+ * whenever the Item changes.
+ *
* @internal
- * Adds rebuildGroupThatIncludeItemKey to the Item sideEffects
- * @param itemKey - ItemKey at which the groups has to rebuild
+ * @param itemKey - Item identifier that has to be contained in Groups.
*/
public addRebuildGroupThatIncludeItemKeySideEffect(itemKey: StateKey) {
this.addSideEffect>(
Item.updateGroupSideEffectKey,
- (instance, config) =>
- instance.collection().rebuildGroupsThatIncludeItemKey(itemKey, config),
+ (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 }
);
}
}
-/**
- * @param isPlaceholder - If Item is initially a Placeholder
- */
export interface ItemConfigInterface {
+ /**
+ * Whether the Item should be a placeholder
+ * and therefore should only exist in the background.
+ * @default false
+ */
isPlaceholder?: boolean;
}
+
+export interface ItemPersistConfigInterface
+ extends StatePersistentConfigInterface {
+ /**
+ * Whether to format the specified Storage key following the Collection Item Storage key pattern.
+ * `_${collectionKey}_item_${itemKey}`
+ * @default true
+ */
+ followCollectionPersistKeyPattern?: boolean;
+}
diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts
index deaf17b7..1366f24d 100644
--- a/packages/core/src/collection/selector.ts
+++ b/packages/core/src/collection/selector.ts
@@ -11,19 +11,31 @@ import {
export class Selector extends State<
DataType | undefined
> {
+ // Collection the Selector belongs to
+ public collection: () => Collection;
+
static unknownItemPlaceholderKey = '__UNKNOWN__ITEM__KEY__';
static rebuildSelectorSideEffectKey = 'rebuildSelector';
static rebuildItemSideEffectKey = 'rebuildItem';
- public collection: () => Collection;
- public item: Item | undefined;
- public _itemKey: ItemKey; // Key of Item the Selector represents
+
+ // Item the Selector represents
+ public _item: Item | undefined;
+ // Key/Name identifier of the Item the Selector represents
+ public _itemKey: ItemKey;
/**
+ * A Selector represents an Item from a Collection in the long term.
+ * It can be mutated dynamically and remains in sync with the Collection.
+ *
+ * Components that need one piece of data from a Collection such as the "current user"
+ * would benefit from using Selectors.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/selector)
+ *
* @public
- * Represents Item of Collection
- * @param collection - Collection that contains the Item
- * @param itemKey - ItemKey of Item that the Selector represents
- * @param config - Config
+ * @param collection - Collection to which the Selector belongs.
+ * @param itemKey - Key/Name identifier of the Item to be represented by the Selector.
+ * @param config - Configuration object
*/
constructor(
collection: Collection,
@@ -35,41 +47,73 @@ export class Selector extends State<
});
super(collection.agileInstance(), undefined, config);
this.collection = () => collection;
- this.item = undefined;
+ this._item = undefined;
this._itemKey = !config.isPlaceholder
? itemKey
: Selector.unknownItemPlaceholderKey;
this._key = config?.key;
this.isPlaceholder = true; // Because hasn't selected any Item yet
- // Initial Select
+ // Initial select of the Item
if (!config.isPlaceholder) this.select(itemKey, { overwrite: true });
}
/**
+ * Returns the `itemKey` currently selected by the Selector.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/selector/properties#itemkey)
+ *
* @public
- * Set ItemKey that the Selector represents
+ */
+ public get itemKey(): ItemKey {
+ return this._itemKey;
+ }
+
+ /**
+ * Updates the currently selected Item of the Selector
+ * based on the specified `itemKey`.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/selector/properties#itemkey)
+ *
+ * @public
+ * @param value - New key/name identifier of the Item to be represented by the Selector.
*/
public set itemKey(value: ItemKey) {
this.select(value);
}
/**
+ * Retrieves the Item currently selected by the Selector.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/selector/properties#item)
+ *
* @public
- * Get ItemKey that the Selector represents
*/
- public get itemKey() {
- return this._itemKey;
+ public get item(): Item | undefined {
+ return this._item;
}
- //=========================================================================================================
- // Select
- //=========================================================================================================
/**
+ * Updates the currently selected Item of the Selector
+ * based on the specified Item.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/selector/properties#item)
+ *
* @public
- * Select new ItemKey
- * @param itemKey - New ItemKey
- * @param config - Config
+ * @param value - New Item to be represented by the Selector.
+ */
+ public set item(value: Item | undefined) {
+ if (value?._key) this.select(value._key);
+ }
+
+ /**
+ * Updates the currently selected Item of the Selector.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#select)
+ *
+ * @public
+ * @param itemKey - New key/name identifier of the Item to be represented by the Selector.
+ * @param config - Configuration object
*/
public select(
itemKey: ItemKey,
@@ -82,11 +126,11 @@ export class Selector extends State<
exclude: [],
},
force: false,
- overwrite: this.item?.isPlaceholder ?? false,
+ overwrite: this._item?.isPlaceholder ?? false,
storage: true,
});
- // Don't select Item if Collection is not properly instantiated
+ // Don't select Item if Collection is not correctly instantiated yet
// (because only after a successful instantiation the Collection
// contains the Items which are essential for a proper selection)
if (
@@ -98,27 +142,29 @@ export class Selector extends State<
// Unselect old Item
this.unselect({ background: true });
- // Get new Item
+ // Retrieve new Item from Collection
const newItem = this.collection().getItemWithReference(itemKey);
// Select new Item
this._itemKey = itemKey;
- this.item = newItem;
+ this._item = newItem;
newItem.selectedBy.add(this._key as any);
- // Add SideEffect to newItem, that rebuild this Selector depending on the current Item Value
+ // Add side effect to the newly selected Item
+ // that rebuilds the Selector value depending on the current Item value
newItem.addSideEffect(
Selector.rebuildSelectorSideEffectKey,
(instance, config) => this.rebuildSelector(config),
{ weight: 100 }
);
- // Add sideEffect to Selector, that updates the Item Value if this Value got updated
+ // Add side effect to Selector
+ // that updates the Item value depending on the current Selector value
this.addSideEffect>(
Selector.rebuildItemSideEffectKey,
(instance, config) => {
- if (!instance.item?.isPlaceholder)
- instance.item?.set(instance._value as any, {
+ if (!instance._item?.isPlaceholder)
+ instance._item?.set(instance._value as any, {
...config,
...{
sideEffects: {
@@ -131,21 +177,25 @@ export class Selector extends State<
{ weight: 90 }
);
- // Rebuild Selector for instantiating new 'selected' ItemKey properly
+ // Rebuild the Selector to properly 'instantiate' the newly selected Item
this.rebuildSelector(config);
return this;
}
- //=========================================================================================================
- // Reselect
- //=========================================================================================================
/**
+ * Reselects the currently selected Item.
+ *
+ * This might be helpful if the Selector failed to select the Item correctly before
+ * and therefore should try to select it again.
+ *
+ * You can use the 'hasSelected()' method to check
+ * whether the 'selected' Item is selected correctly.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#reselect)
+ *
* @public
- * Reselects current Item
- * Might help if the Selector failed to select an Item correctly.
- * You can check with 'hasSelected()' if an Item got correctly selected.
- * @param config - Config
+ * @param config - Configuration object
*/
public reselect(config: StateRuntimeJobConfigInterface = {}): this {
if (
@@ -156,18 +206,19 @@ export class Selector extends State<
return this;
}
- //=========================================================================================================
- // Unselect
- //=========================================================================================================
/**
+ * Unselects the currently selected Item.
+ *
+ * Therefore, it sets the `itemKey` and `item` property to `undefined`,
+ * since the Selector no longer represents any Item.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#unselect)
+ *
* @public
- * Unselects current selected Item.
- * Often not necessary because by selecting a new Item,
- * the old Item is automatically unselected.
- * @param config - Config
+ * @param config - Configuration object
*/
public unselect(config: StateRuntimeJobConfigInterface = {}): this {
- // Because this.item might be outdated
+ // Retrieve Item from the Collection because 'this._item' might be outdated
const item = this.collection().getItem(this._itemKey, {
notExisting: true,
});
@@ -180,53 +231,54 @@ export class Selector extends State<
if (item.isPlaceholder) delete this.collection().data[this._itemKey];
}
- // Reset and rebuild Selector
- this.item = undefined;
+ // Reset Selector
+ this._item = undefined;
this._itemKey = Selector.unknownItemPlaceholderKey;
this.rebuildSelector(config);
-
this.isPlaceholder = true;
return this;
}
- //=========================================================================================================
- // Has Selected
- //=========================================================================================================
/**
- * Checks if Selector has correctly selected the Item at the passed itemKey
- * @param itemKey - ItemKey
- * @param correctlySelected - If it should consider only correctly selected Items
+ * Returns a boolean indicating whether an Item with the specified `itemKey`
+ * is selected by the Selector or not.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#hasselected)
+ *
+ * @public
+ * @param itemKey - Key/Name identifier of the Item.
+ * @param correctlySelected - Whether the Item has to be selected correctly.
*/
public hasSelected(itemKey: ItemKey, correctlySelected = true): boolean {
if (correctlySelected) {
return (
this._itemKey === itemKey &&
- this.item != null &&
- this.item.selectedBy.has(this._key as any)
+ this._item != null &&
+ this._item.selectedBy.has(this._key as any)
);
}
return this._itemKey === itemKey;
}
- //=========================================================================================================
- // Rebuild Selector
- //=========================================================================================================
/**
+ * Rebuilds the Selector.
+ * During this process, it updates the Selector `value` based on the Item `value`.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/collection/selector/methods#rebuild)
+ *
* @public
- * Rebuilds Selector,
- * which updates the Selector value based on the Item value
- * @param config - Config
+ * @param config - Configuration object
*/
public rebuildSelector(config: StateRuntimeJobConfigInterface = {}): this {
- // Set Selector Value to undefined if Item doesn't exist
- if (this.item == null || this.item.isPlaceholder) {
+ // Assign 'undefined' to the Selector value if no Item is set
+ if (this._item == null || this._item.isPlaceholder) {
this.set(undefined, config);
return this;
}
- // Set Selector Value to updated Item Value
- this.set(this.item._value, config);
+ // Assign the current Item value to the Selector value
+ this.set(this._item._value, config);
return this;
}
@@ -234,11 +286,16 @@ export class Selector extends State<
export type SelectorKey = string | number;
-/**
- * @param key - Key/Name of Selector
- * @param isPlaceholder - If Selector is initially a Placeholder
- */
export interface SelectorConfigInterface {
+ /**
+ * Key/Name identifier of the Selector.
+ * @default undefined
+ */
key?: SelectorKey;
+ /**
+ * Whether the Selector should be a placeholder
+ * and therefore should only exist in the background.
+ * @default false
+ */
isPlaceholder?: boolean;
}
diff --git a/packages/core/src/computed/computed.tracker.ts b/packages/core/src/computed/computed.tracker.ts
index 48d8b87e..e3254f90 100644
--- a/packages/core/src/computed/computed.tracker.ts
+++ b/packages/core/src/computed/computed.tracker.ts
@@ -4,40 +4,45 @@ export class ComputedTracker {
static isTracking = false;
static trackedObservers: Set = new Set();
- //=========================================================================================================
- // Track
- //=========================================================================================================
/**
+ * Helper Class for automatic tracking used Observers (dependencies) in a compute function.
+ *
+ * @internal
+ */
+ constructor() {
+ // empty
+ }
+
+ /**
+ * Activates Computed Tracker to globally track used Observers.
+ *
* @internal
- * Starts tracking Observers
*/
static track(): void {
this.isTracking = true;
}
- //=========================================================================================================
- // Tracked
- //=========================================================================================================
/**
+ * Tracks the passed Observer and caches it
+ * when the Computed Tracker is actively tracking.
+ *
* @internal
- * Adds passed Observer to tracked Observers, if ComputedTracker is currently tracking
* @param observer - Observer
*/
static tracked(observer: Observer) {
if (this.isTracking) this.trackedObservers.add(observer);
}
- //=========================================================================================================
- // Get Tracked Observers
- //=========================================================================================================
/**
+ * Returns the latest tracked Observers
+ * and stops the Computed Tracker from tracking any more Observers.
+ *
* @internal
- * Returns tracked Observers and stops tracking anymore Observers
*/
static getTrackedObservers(): Array {
const trackedObservers = Array.from(this.trackedObservers);
- // Reset tracking
+ // Reset Computed Tracker
this.isTracking = false;
this.trackedObservers = new Set();
diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts
index e4470c22..f0d95f6b 100644
--- a/packages/core/src/computed/index.ts
+++ b/packages/core/src/computed/index.ts
@@ -15,18 +15,32 @@ import {
export class Computed extends State<
ComputedValueType
> {
+ // Agile Instance the Computed belongs to
public agileInstance: () => Agile;
+ // Function to compute the Computed Class value
public computeFunction: () => ComputedValueType;
- public deps: Array = []; // All Dependencies of Computed (hardCoded and autoDetected)
- public hardCodedDeps: Array = []; // HardCoded Dependencies of Computed
+ // All dependencies the Computed Class depends on (including hardCoded and automatically detected dependencies)
+ public deps: Array = [];
+ // Only hardCoded dependencies the Computed Class depends on
+ public hardCodedDeps: Array = [];
/**
+ * 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
- * Computed - Function that recomputes its value if a dependency changes
- * @param agileInstance - An instance of Agile
- * @param computeFunction - Function for computing value
- * @param config - Config
+ * @param agileInstance - Instance of Agile the Computed belongs to.
+ * @param computeFunction - Function to compute the computed value.
+ * @param config - Configuration object
*/
constructor(
agileInstance: Agile,
@@ -43,23 +57,23 @@ export class Computed extends State<
this.agileInstance = () => agileInstance;
this.computeFunction = computeFunction;
- // Format hardCodedDeps
+ // Extract Observer of passed hardcoded dependency instances
this.hardCodedDeps = extractObservers(config.computedDeps).filter(
(dep): dep is Observer => dep !== undefined
);
this.deps = this.hardCodedDeps;
- // Recompute for setting initial value and adding missing dependencies
+ // Initial recompute to assign initial value and autodetect missing dependencies
this.recompute({ autodetect: true });
}
- //=========================================================================================================
- // Recompute
- //=========================================================================================================
/**
+ * Forces a recomputation of the cached value with the compute function.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#recompute)
+ *
* @public
- * Recomputes Value of Computed
- * @param config - Config
+ * @param config - Configuration object
*/
public recompute(config: RecomputeConfigInterface = {}): this {
config = defineConfig(config, {
@@ -72,15 +86,21 @@ export class Computed extends State<
return this;
}
- //=========================================================================================================
- // Updates Compute Function
- //=========================================================================================================
/**
+ * 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
- * Applies new compute Function to Computed
- * @param computeFunction - New Function for computing value
- * @param deps - Hard coded dependencies of Computed Function
- * @param config - Config
+ * @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,
@@ -92,7 +112,7 @@ export class Computed extends State<
autodetect: true,
});
- // Update deps
+ // Update dependencies of Computed
const newDeps = extractObservers(deps).filter(
(dep): dep is Observer => dep !== undefined
);
@@ -103,25 +123,25 @@ export class Computed extends State<
// Update computeFunction
this.computeFunction = computeFunction;
- // Recompute for setting initial Computed Function Value and adding missing Dependencies
+ // Recompute to assign new computed value and autodetect missing dependencies
this.recompute(removeProperties(config, ['overwriteDeps']));
return this;
}
- //=========================================================================================================
- // Compute
- //=========================================================================================================
/**
+ * Computes the new value of the Computed Class
+ * and autodetects used dependencies in the compute function.
+ *
* @internal
- * Recomputes value and adds missing dependencies to Computed
+ * @param config - Configuration object
*/
public compute(config: ComputeConfigInterface = {}): ComputedValueType {
config = defineConfig(config, {
autodetect: true,
});
- // Start auto tracking Observers the computeFunction might depend on
+ // Start auto tracking of Observers on which the computeFunction might depend
if (config.autodetect) ComputedTracker.track();
const computedValue = this.computeFunction();
@@ -129,14 +149,12 @@ export class Computed extends State<
// Handle auto tracked Observers
if (config.autodetect) {
const foundDeps = ComputedTracker.getTrackedObservers();
-
- // Handle foundDeps and hardCodedDeps
const newDeps: Array = [];
this.hardCodedDeps.concat(foundDeps).forEach((observer) => {
newDeps.push(observer);
- // Make this Observer depend on foundDep Observer
- observer.depend(this.observer);
+ // Make this Observer depend on the found dep Observers
+ observer.addDependent(this.observer);
});
this.deps = newDeps;
@@ -145,45 +163,54 @@ export class Computed extends State<
return computedValue;
}
- //=========================================================================================================
- // Overwriting some functions which aren't allowed to use in Computed
- //=========================================================================================================
-
+ /**
+ * Not usable in Computed Class.
+ */
public patch() {
LogCodeManager.log('19:03:00');
return this;
}
+ /**
+ * Not usable in Computed Class.
+ */
public persist(): this {
LogCodeManager.log('19:03:01');
return this;
}
+ /**
+ * Not usable in Computed Class.
+ */
public invert(): this {
LogCodeManager.log('19:03:02');
return this;
}
}
-/**
- * @param computedDeps - Hard coded dependencies of Computed Function
- */
export interface ComputedConfigInterface extends StateConfigInterface {
+ /**
+ * Hard-coded dependencies on which the Computed Class should depend.
+ * @default []
+ */
computedDeps?: Array;
}
-/**
- * @param autodetect - If dependencies get autodetected
- */
export interface ComputeConfigInterface {
+ /**
+ * Whether to automatically detect used dependencies in the compute method.
+ * @default true
+ */
autodetect?: boolean;
}
-/**
- * @param overwriteDeps - If old hardCoded deps get overwritten
- */
export interface UpdateComputeFunctionConfigInterface
extends RecomputeConfigInterface {
+ /**
+ * Whether to overwrite the old hard-coded dependencies with the new ones
+ * or merge them into the new ones.
+ * @default false
+ */
overwriteDeps?: boolean;
}
@@ -191,4 +218,4 @@ export interface RecomputeConfigInterface
extends StateIngestConfigInterface,
ComputeConfigInterface {}
-type SubscribableAgileInstancesType = State | Collection | Observer;
+export type SubscribableAgileInstancesType = State | Collection | Observer;
diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts
index 09cfa12f..038166db 100644
--- a/packages/core/src/integrations/index.ts
+++ b/packages/core/src/integrations/index.ts
@@ -1,31 +1,35 @@
import { Agile, Integration, LogCodeManager } from '../internal';
export class Integrations {
+ // Agile Instance the Integrations belongs to
public agileInstance: () => Agile;
- public integrations: Set = new Set(); // All registered Integrations
+ // Registered Integrations
+ public integrations: Set = new Set();
/**
+ * The Integrations Class manages all Integrations for an Agile Instance
+ * and provides an interface to easily update
+ * and invoke functions in all registered Integrations.
+ *
* @internal
- * Integrations - Manages Integrations of Agile
- * @param agileInstance - An Instance of Agile
+ * @param agileInstance - Instance of Agile the Integrations belongs to.
*/
constructor(agileInstance: Agile) {
this.agileInstance = () => agileInstance;
- // Integrate initial Integrations which are static and got set external
+ // Integrate initial Integrations which were statically set externally
Agile.initialIntegrations.forEach((integration) =>
this.integrate(integration)
);
}
- //=========================================================================================================
- // Integrate
- //=========================================================================================================
/**
- * @internal
- * Integrates Framework(Integration) into Agile
- * @param integration - Integration/Framework that gets integrated
+ * Integrates the specified Integration into AgileTs
+ * and sets it to ready when the binding was successful.
+ *
+ * @public
+ * @param integration - Integration to be integrated into AgileTs.
*/
public async integrate(integration: Integration): Promise {
// Check if Integration is valid
@@ -34,12 +38,12 @@ export class Integrations {
return false;
}
- // Bind Framework to Agile
+ // Bind to integrate Integration to AgileTs
if (integration.methods.bind)
integration.ready = await integration.methods.bind(this.agileInstance());
else integration.ready = true;
- // Integrate Framework
+ // Integrate Integration
this.integrations.add(integration);
integration.integrated = true;
@@ -48,15 +52,16 @@ export class Integrations {
return true;
}
- //=========================================================================================================
- // Update
- //=========================================================================================================
/**
- * @internal
- * Updates registered and ready Integrations
- * -> calls 'updateMethod' in all registered and ready Integrations
- * @param componentInstance - Component that gets updated
- * @param updatedData - Properties that differ from the last Value
+ * Updates the specified UI-Component Instance
+ * with the updated data object in all registered Integrations that are ready.
+ *
+ * In doing so, it calls the `updateMethod()` method
+ * in all registered Integrations with the specified parameters.
+ *
+ * @public
+ * @param componentInstance - Component Instance to be updated.
+ * @param updatedData - Data object with updated data.
*/
public update(componentInstance: any, updatedData: Object): void {
this.integrations.forEach((integration) => {
@@ -69,12 +74,11 @@ export class Integrations {
});
}
- //=========================================================================================================
- // Has Integration
- //=========================================================================================================
/**
- * @internal
- * Check if at least one Integration got registered
+ * Returns a boolean indicating whether any Integration
+ * has been registered with the Agile Instance or not.
+ *
+ * @public
*/
public hasIntegration(): boolean {
return this.integrations.size > 0;
diff --git a/packages/core/src/integrations/integration.ts b/packages/core/src/integrations/integration.ts
index 94c011db..f2c409ac 100644
--- a/packages/core/src/integrations/integration.ts
+++ b/packages/core/src/integrations/integration.ts
@@ -1,16 +1,26 @@
import { Agile } from '../internal';
export class Integration {
+ // Key/Name identifier of the Integration
public _key: IntegrationKey;
+ // Instance of the Framework the Integration represents
public frameworkInstance?: F;
+ // Whether the Integration is ready and the binding to AgileTs was successful
public ready = false;
+ // Whether the Integration was integrated into AgileTs
public integrated = false;
+ // Methods to interact with the Framework represented by the Integration
public methods: IntegrationMethods;
/**
+ * An Integration is an interface to a UI-Framework,
+ * and allows the easy interaction with that Framework.
+ *
+ * Due to the Integration, AgileTs can be integrated into almost any UI-Framework
+ * without a huge overhead.
+ *
* @public
- * Integration - Represents a Framework/Integration of Agile
- * @param config - Config
+ * @param config - Configuration object
*/
constructor(config: CreateIntegrationConfig) {
this._key = config.key;
@@ -22,38 +32,63 @@ export class Integration {
}
/**
+ * Updates the key/name identifier of the Integration.
+ *
* @public
- * Set Value of Integration
+ * @param value - New key/name identifier.
*/
- public set key(key: IntegrationKey) {
- this._key = key;
+ public set key(value: IntegrationKey) {
+ this._key = value;
}
/**
+ * Returns the key/name identifier of the Integration.
+ *
* @public
- * Get Value of Integration
*/
public get key(): IntegrationKey {
return this._key;
}
}
-/**
- * @param key - Key/Name of Integration
- * @param frameworkInstance - An Instance of the Framework that this Integration represents (for instance React)
- */
export interface CreateIntegrationConfig
extends IntegrationMethods {
+ /**
+ * Key/Name identifier of the Integration.
+ * @default undefined
+ */
key: string;
+ /**
+ * An Instance of the UI-Framework to be represented by the Integration.
+ * For example, in the case of React, the React Instance.
+ * @default undefined
+ */
frameworkInstance?: F;
}
-/**
- * @param bind - Binds the Framework/Integration to Agile | Will be called after a successful integration
- * @param updateMethod - Will be called if a Observer updates his subs (Only in Component based Subscriptions!)
- */
export interface IntegrationMethods {
+ /**
+ * Binds the Integration to an Agile Instance.
+ *
+ * This method is called shortly after the Integration was registered with an Agile Instance.
+ * It is intended to set up things that are important
+ * for an seamless integration into AgileTs on the UI-Framework side.
+ *
+ * @param agileInstance - Agile Instance into which the Integration is to be integrated.
+ * @return Indicating whether the to integrate Integration is ready on the Framework side.
+ */
bind?: (agileInstance: Agile) => Promise;
+ /**
+ * Method to apply the updated data to the provided UI-Component
+ * in order to trigger a re-render on it.
+ *
+ * This method is called when the value of an [Agile Sub Instance](https://agile-ts.org/docs/introduction/#agile-sub-instance)
+ * bound to the specified UI-Component changes ([Component based Subscription](https://agile-ts.org/docs/core/integration/#component-based)).
+ * The updated Agile Sub Instance values were mapped in the provided `updatedData` object.
+ *
+ * @param componentInstance - Component Instance of the to update UI-Component.
+ * @param updatedData - Data object containing the updated data.
+ */
updateMethod?: (componentInstance: C, updatedData: Object) => void;
}
diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts
index 1f098fbd..9df75ad7 100644
--- a/packages/core/src/logCodeManager.ts
+++ b/packages/core/src/logCodeManager.ts
@@ -1,11 +1,19 @@
import { Agile } from './agile';
+// The Log Code Manager keeps track
+// and manages all important Logs of AgileTs.
+//
+// How does the identification of Log Messages work?
+// Let's take a look at this example:
// 00:00:00
+//
// |00|:00:00 first digits are based on the Agile Class
// 00 = General
// 10 = Agile
// 11 = Storage
// ..
+//
+// ---
// 00:|00|:00 second digits are based on the Log Type
const logCodeTypes = {
'00': 'success',
@@ -13,6 +21,8 @@ const logCodeTypes = {
'02': 'warn',
'03': 'error',
};
+//
+// ---
// 00:00:|00| third digits are based on the Log Message (ascending counted)
const logCodeMessages = {
@@ -21,7 +31,7 @@ const logCodeMessages = {
'10:02:00':
'Be careful when binding multiple Agile Instances globally in one application!',
- // Storage
+ // Storages
'11:02:00':
"The 'Local Storage' is not available in your current environment." +
"To use the '.persist()' functionality, please provide a custom Storage!",
@@ -49,6 +59,9 @@ const logCodeMessages = {
"The Storage with the key/name '${1}' doesn't exists!`",
// Storage
+ '13:01:00': "GET value at key '${1}' from Storage '${0}'.",
+ '13:01:01': "SET value at key '${1}' in Storage '${0}'.",
+ '13:01:02': "REMOVE value at key '${1}' from Storage '${0}'.",
'13:02:00':
'Using normalGet() in a async-based Storage might result in an unexpected return value. ' +
'Instead of a resolved value a Promise is returned!',
@@ -60,9 +73,8 @@ const logCodeMessages = {
'14:03:01':
"'${1}' is a not supported type! Supported types are: String, Boolean, Array, Object, Number.",
'14:03:02': "The 'patch()' method works only in object based States!",
- '14:03:03': "Watcher Callback with the key/name '${0}' already exists!",
- '14:03:04': 'Only one Interval can be active at once!',
- '14:03:05': "The 'invert()' method works only in boolean based States!",
+ '14:03:03': 'Only one Interval can be active at once!',
+ '14:03:04': "Failed to invert value of the type '${0}'!",
// SubController
'15:01:00': "Unregistered 'Callback' based Subscription.",
@@ -134,11 +146,19 @@ const logCodeMessages = {
"Couldn't update ItemKey from '${0}' to '${1}' " +
"because an Item with the key/name '${1}' already exists in the Collection '${2}'!",
'1B:03:05': "Item Data of Collection '${0}' has to be a valid object!",
+ '1B:03:06':
+ "Item tried to add to the Collection '${0}' belongs to another Collection '${1}'!",
// Group
'1C:02:00':
"Couldn't find some Items in the Collection '${0}' " +
"during the rebuild of the Group '${1}' output.",
+ '1C:03:00':
+ "The 'output' property of the Group '${0}' is a automatically generated readonly property " +
+ 'that can only be mutated by the Group itself!',
+ '1C:03:01':
+ "The 'item' property of the Group '${0}' is a automatically generated readonly property " +
+ 'that can only be mutated by the Group itself!',
// Utils
'20:03:00': 'Failed to get Agile Instance from',
@@ -151,15 +171,13 @@ const logCodeMessages = {
'00:03:01': "'${0}' has to be of the type ${1}!",
};
-//=========================================================================================================
-// Get Log
-//=========================================================================================================
/**
+ * Returns the log message according to the specified log code.
+ *
* @internal
- * Returns the log message according to the passed logCode
- * @param logCode - Log Code of Message
+ * @param logCode - Log code of the message to be returned.
* @param replacers - Instances that replace these '${x}' placeholders based on the index
- * For example: replacers[0] replaces '${0}', replacers[1] replaces '${1}', ...
+ * For example: 'replacers[0]' replaces '${0}', 'replacers[1]' replaces '${1}', ..
*/
function getLog>(
logCode: T,
@@ -175,16 +193,15 @@ function getLog>(
return result;
}
-//=========================================================================================================
-// Log
-//=========================================================================================================
/**
+ * Logs the log message according to the specified log code
+ * with the Agile Logger.
+ *
* @internal
- * Logs message at the provided logCode with the Agile.logger
- * @param logCode - Log Code of Message
+ * @param logCode - Log code of the message to be returned.
* @param replacers - Instances that replace these '${x}' placeholders based on the index
- * For example: replacers[0] replaces '${0}', replacers[1] replaces '${1}', ..
- * @param data - Data attached to the end of the log message
+ * For example: 'replacers[0]' replaces '${0}', 'replacers[1]' replaces '${1}', ..
+ * @param data - Data to be attached to the end of the log message.
*/
function log>(
logCode: T,
@@ -197,8 +214,10 @@ function log>(
}
/**
+ * The Log Code Manager keeps track
+ * and manages all important Logs of AgileTs.
+ *
* @internal
- * Manages logCode based logging of AgileTs
*/
export const LogCodeManager = {
getLog,
diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts
index e3b1c409..9a93ce62 100644
--- a/packages/core/src/runtime/index.ts
+++ b/packages/core/src/runtime/index.ts
@@ -6,75 +6,109 @@ import {
ComponentSubscriptionContainer,
defineConfig,
notEqual,
- isValidObject,
LogCodeManager,
} from '../internal';
export class Runtime {
+ // Agile Instance the Runtime belongs to
public agileInstance: () => Agile;
- // Queue system
+ // Job that is currently being performed
public currentJob: RuntimeJob | null = null;
+ // Jobs to be performed
public jobQueue: Array = [];
- public notReadyJobsToRerender: Set = new Set(); // Jobs that got performed but aren't ready to get rerendered (wait for mount)
- public jobsToRerender: Array = []; // Jobs that are performed and will be rendered
+
+ // Jobs that were performed and are ready to re-render
+ public jobsToRerender: Array = [];
+ // Jobs that were performed and couldn't be re-rendered yet.
+ // That is the case when at least one Subscription Container (UI-Component) in the Job
+ // wasn't ready to update (re-render).
+ public notReadyJobsToRerender: Set = new Set();
+
+ // Whether the `jobQueue` is currently being actively processed
+ public isPerformingJobs = false;
/**
+ * The Runtime queues and executes incoming Observer-based Jobs
+ * to prevent [race conditions](https://en.wikipedia.org/wiki/Race_condition#:~:text=A%20race%20condition%20or%20race,the%20possible%20behaviors%20is%20undesirable.)
+ * and optimized the re-rendering of the Observer's subscribed UI-Components.
+ *
+ * Each queued Job is executed when it is its turn
+ * by calling the Job Observer's `perform()` method.
+ *
+ * After successful execution, the Job is added to a re-render queue,
+ * which is first put into the browser's 'Bucket' and started to work off
+ * when resources are left.
+ *
+ * The re-render queue is designed for optimizing the render count
+ * by batching multiple re-render Jobs of the same UI-Component
+ * and ignoring re-render requests for unmounted UI-Components.
+ *
* @internal
- * Runtime - Performs ingested Observers
- * @param agileInstance - An instance of Agile
+ * @param agileInstance - Instance of Agile the Runtime belongs to.
*/
constructor(agileInstance: Agile) {
this.agileInstance = () => agileInstance;
}
- //=========================================================================================================
- // Ingest
- //=========================================================================================================
/**
- * @internal
- * Ingests Job into Runtime that gets performed
- * @param job - Job
- * @param config - Config
+ * Adds the specified Observer-based Job to the internal Job queue,
+ * where it is executed when it is its turn.
+ *
+ * After successful execution, the Job is assigned to the re-render queue,
+ * where all the Observer's subscribed Subscription Containers (UI-Components)
+ * are updated (re-rendered).
+ *
+ * @public
+ * @param job - Job to be added to the Job queue.
+ * @param config - Configuration object
*/
public ingest(job: RuntimeJob, config: IngestConfigInterface = {}): void {
config = defineConfig(config, {
- perform: true,
+ perform: !this.isPerformingJobs,
});
+ // Add specified Job to the queue
this.jobQueue.push(job);
Agile.logger.if
.tag(['runtime'])
.info(LogCodeManager.getLog('16:01:00', [job._key]), job);
- // Perform Job
+ // Run first Job from the queue
if (config.perform) {
const performJob = this.jobQueue.shift();
if (performJob) this.perform(performJob);
}
}
- //=========================================================================================================
- // Perform
- //=========================================================================================================
/**
+ * Performs the specified Job
+ * and assigns it to the re-render queue if necessary.
+ *
+ * After the execution of the provided Job, it is checked whether
+ * there are still Jobs left in the Job queue.
+ * - If so, the next Job in the `jobQueue` is performed.
+ * - If not, the `jobsToRerender` queue is started to work off.
+ *
* @internal
- * Performs Job and adds it to the rerender queue if necessary
- * @param job - Job that gets performed
+ * @param job - Job to be performed.
*/
public perform(job: RuntimeJob): void {
+ this.isPerformingJobs = true;
this.currentJob = job;
// Perform Job
job.observer.perform(job);
job.performed = true;
- // Ingest Dependents of Observer into Runtime
+ // Ingest dependents of the Observer into runtime,
+ // since they depend on the Observer and therefore have properly changed too
job.observer.dependents.forEach((observer) =>
observer.ingest({ perform: false })
);
+ // Add Job to rerender queue and reset current Job property
if (job.rerender) this.jobsToRerender.push(job);
this.currentJob = null;
@@ -82,11 +116,14 @@ export class Runtime {
.tag(['runtime'])
.info(LogCodeManager.getLog('16:01:01', [job._key]), job);
- // Perform Jobs as long as Jobs are left in queue, if no job left update/rerender Subscribers of jobsToRerender
+ // Perform Jobs as long as Jobs are left in the queue.
+ // If no Job is left start updating (re-rendering) Subscription Container (UI-Components)
+ // of the Job based on the 'jobsToRerender' queue.
if (this.jobQueue.length > 0) {
const performJob = this.jobQueue.shift();
if (performJob) this.perform(performJob);
} else {
+ this.isPerformingJobs = false;
if (this.jobsToRerender.length > 0) {
// https://stackoverflow.com/questions/9083594/call-settimeout-without-delay
setTimeout(() => {
@@ -96,50 +133,63 @@ export class Runtime {
}
}
- //=========================================================================================================
- // Update Subscribers
- //=========================================================================================================
/**
+ * Processes the `jobsToRerender` queue by updating (causing a re-render on)
+ * the subscribed Subscription Containers (UI-Components) of each Job Observer.
+ *
+ * It returns a boolean indicating whether
+ * any Subscription Container (UI-Component) was updated (re-rendered) or not.
+ *
* @internal
- * Updates/Rerenders all Subscribed Components (SubscriptionContainer) of the Job (Observer)
- * @return If any subscriptionContainer got updated (-> triggered a rerender on the Component it represents)
*/
public updateSubscribers(): boolean {
- if (!this.agileInstance().hasIntegration()) {
- this.jobsToRerender = [];
- this.notReadyJobsToRerender = new Set();
- return false;
- }
- if (
- this.jobsToRerender.length <= 0 &&
- this.notReadyJobsToRerender.size <= 0
- )
- return false;
-
- // Subscriptions that has to be updated/rerendered
- // A Set() to combine several equal SubscriptionContainers into one (optimizes rerender)
- // (Even better would be to combine SubscriptionContainer based on the Component,
- // since a Component can have multiple SubscriptionContainers)
- const subscriptionsToUpdate = new Set();
-
- // Build final jobsToRerender array based on new jobsToRerender and not ready jobsToRerender
+ // Build final 'jobsToRerender' array
+ // based on the new 'jobsToRerender' array and the 'notReadyJobsToRerender' array
const jobsToRerender = this.jobsToRerender.concat(
Array.from(this.notReadyJobsToRerender)
);
this.notReadyJobsToRerender = new Set();
this.jobsToRerender = [];
- // Check if Job SubscriptionContainers should be updated and if so add them to the subscriptionsToUpdate array
- jobsToRerender.forEach((job) => {
+ if (!this.agileInstance().hasIntegration() || jobsToRerender.length <= 0)
+ return false;
+
+ // Extract the Subscription Container to be re-rendered from the Jobs
+ const subscriptionContainerToUpdate = this.extractToUpdateSubscriptionContainer(
+ jobsToRerender
+ );
+ if (subscriptionContainerToUpdate.length <= 0) return false;
+
+ // Update Subscription Container (trigger re-render on the UI-Component they represent)
+ this.updateSubscriptionContainer(subscriptionContainerToUpdate);
+
+ return true;
+ }
+
+ /**
+ * Extracts the Subscription Containers (UI-Components)
+ * to be updated (re-rendered) from the specified Runtime Jobs.
+ *
+ * @internal
+ * @param jobs - Jobs from which to extract the Subscription Containers to be updated.
+ */
+ public extractToUpdateSubscriptionContainer(
+ jobs: Array
+ ): Array {
+ const subscriptionsToUpdate = new Set();
+
+ jobs.forEach((job) => {
job.subscriptionContainersToUpdate.forEach((subscriptionContainer) => {
+ let updateSubscriptionContainer = true;
+
+ // Handle not ready Subscription Container
if (!subscriptionContainer.ready) {
if (
- !job.config.numberOfTriesToUpdate ||
- job.triesToUpdate < job.config.numberOfTriesToUpdate
+ !job.config.maxTriesToUpdate ||
+ job.timesTriedToUpdateCount < job.config.maxTriesToUpdate
) {
- job.triesToUpdate++;
+ job.timesTriedToUpdateCount++;
this.notReadyJobsToRerender.add(job);
-
LogCodeManager.log(
'16:02:00',
[subscriptionContainer.key],
@@ -148,165 +198,145 @@ export class Runtime {
} else {
LogCodeManager.log(
'16:02:01',
- [job.config.numberOfTriesToUpdate],
+ [job.config.maxTriesToUpdate],
subscriptionContainer
);
}
return;
}
- // Handle Object based Subscription
- if (subscriptionContainer.isObjectBased)
- this.handleObjectBasedSubscription(subscriptionContainer, job);
-
- // Check if subscriptionContainer should be updated
- const updateSubscriptionContainer = subscriptionContainer.proxyBased
- ? this.handleProxyBasedSubscription(subscriptionContainer, job)
- : true;
-
- if (updateSubscriptionContainer)
+ // TODO has to be overthought because when it is a Component based Subscription
+ // the rerender is triggered via merging the changed properties into the Component.
+ // Although the 'componentId' might be equal, it doesn't mean
+ // that the changed properties are equal! (-> changed properties might get missing)
+ // Check if Subscription Container with same 'componentId'
+ // is already in the 'subscriptionToUpdate' queue (rerender optimisation)
+ // updateSubscriptionContainer =
+ // updateSubscriptionContainer &&
+ // Array.from(subscriptionsToUpdate).findIndex(
+ // (sc) => sc.componentId === subscriptionContainer.componentId
+ // ) === -1;
+
+ // Check whether a selected part of the Observer value has changed
+ updateSubscriptionContainer =
+ updateSubscriptionContainer &&
+ this.handleSelectors(subscriptionContainer, job);
+
+ // Add Subscription Container to the 'subscriptionsToUpdate' queue
+ if (updateSubscriptionContainer) {
+ subscriptionContainer.updatedSubscribers.add(job.observer);
subscriptionsToUpdate.add(subscriptionContainer);
+ }
job.subscriptionContainersToUpdate.delete(subscriptionContainer);
});
});
- if (subscriptionsToUpdate.size <= 0) return false;
+ return Array.from(subscriptionsToUpdate);
+ }
- // Update Subscription Containers (trigger rerender on subscribed Component)
+ /**
+ * Updates the specified Subscription Containers.
+ *
+ * Updating a Subscription Container triggers a re-render
+ * on the Component it represents, based on the type of the Subscription Containers.
+ *
+ * @internal
+ * @param subscriptionsToUpdate - Subscription Containers to be updated.
+ */
+ public updateSubscriptionContainer(
+ subscriptionsToUpdate: Array
+ ): void {
subscriptionsToUpdate.forEach((subscriptionContainer) => {
// Call 'callback function' if Callback based Subscription
if (subscriptionContainer instanceof CallbackSubscriptionContainer)
subscriptionContainer.callback();
- // Call 'update method' if Component based Subscription
+ // Call 'update method' in Integrations if Component based Subscription
if (subscriptionContainer instanceof ComponentSubscriptionContainer)
this.agileInstance().integrations.update(
subscriptionContainer.component,
- this.getObjectBasedProps(subscriptionContainer)
+ this.getUpdatedObserverValues(subscriptionContainer)
);
+
+ subscriptionContainer.updatedSubscribers.clear();
});
Agile.logger.if
.tag(['runtime'])
.info(LogCodeManager.getLog('16:01:02'), subscriptionsToUpdate);
-
- return true;
}
- //=========================================================================================================
- // Handle Object Based Subscription
- //=========================================================================================================
/**
+ * Maps the values of the updated Observers (`updatedSubscribers`)
+ * of the specified Subscription Container into a key map object.
+ *
+ * The key containing the Observer value is extracted from the Observer itself
+ * or from the Subscription Container's `subscriberKeysWeakMap`.
+ *
* @internal
- * Finds key of Observer (Job) in subsObject and adds it to 'changedObjectKeys'
- * @param subscriptionContainer - Object based SubscriptionContainer
- * @param job - Job that holds the searched Observer
+ * @param subscriptionContainer - Subscription Container from which the `updatedSubscribers` are to be mapped into a key map.
*/
- public handleObjectBasedSubscription(
- subscriptionContainer: SubscriptionContainer,
- job: RuntimeJob
- ): void {
- let foundKey: string | null = null;
-
- // Check if SubscriptionContainer is Object based
- if (!subscriptionContainer.isObjectBased) return;
-
- // Find Key of Job Observer in SubscriptionContainer
- for (const key in subscriptionContainer.subsObject)
- if (subscriptionContainer.subsObject[key] === job.observer)
- foundKey = key;
-
- if (foundKey) subscriptionContainer.observerKeysToUpdate.push(foundKey);
- }
-
- //=========================================================================================================
- // Get Object Based Props
- //=========================================================================================================
- /**
- * @internal
- * Builds Object out of changedObjectKeys with Observer Value
- * @param subscriptionContainer - Object based SubscriptionContainer
- */
- public getObjectBasedProps(
+ public getUpdatedObserverValues(
subscriptionContainer: SubscriptionContainer
): { [key: string]: any } {
const props: { [key: string]: any } = {};
-
- // Map trough observerKeysToUpdate and build object out of Observer value
- if (subscriptionContainer.subsObject)
- for (const updatedKey of subscriptionContainer.observerKeysToUpdate)
- props[updatedKey] = subscriptionContainer.subsObject[updatedKey]?.value;
-
- subscriptionContainer.observerKeysToUpdate = [];
+ for (const observer of subscriptionContainer.updatedSubscribers) {
+ const key =
+ subscriptionContainer.subscriberKeysWeakMap.get(observer) ??
+ observer.key;
+ if (key != null) props[key] = observer.value;
+ }
return props;
}
- //=========================================================================================================
- // Handle Proxy Based Subscription
- //=========================================================================================================
/**
+ * Returns a boolean indicating whether the specified Subscription Container can be updated or not,
+ * based on its selector functions (`selectorsWeakMap`).
+ *
+ * This is done by checking the '.value' and the '.previousValue' property of the Observer represented by the Job.
+ * If a selected property differs, the Subscription Container (UI-Component) is allowed to update (re-render)
+ * and `true` is returned.
+ *
+ * If the Subscription Container has no selector function at all, `true` is returned.
+ *
* @internal
- * Checks if the subscriptionContainer should be updated.
- * Therefore it reviews the '.value' and the '.previousValue' property of the Observer the Job represents.
- * If a property at the proxy detected path differs, the subscriptionContainer is allowed to update.
- * @param subscriptionContainer - SubscriptionContainer
- * @param job - Job
- * @return {boolean} If the subscriptionContainer should be updated
- * -> If a from the Proxy Tree detected property differs from the same property in the previous value
- * or the passed subscriptionContainer isn't properly proxy based
+ * @param subscriptionContainer - Subscription Container to be checked if it can be updated.
+ * @param job - Job containing the Observer that is subscribed to the Subscription Container.
*/
- public handleProxyBasedSubscription(
+ public handleSelectors(
subscriptionContainer: SubscriptionContainer,
job: RuntimeJob
): boolean {
- // Return true because in this cases the subscriptionContainer isn't properly proxyBased
- if (
- !subscriptionContainer.proxyBased ||
- !job.observer._key ||
- !subscriptionContainer.proxyKeyMap[job.observer._key]
- )
- return true;
-
- const paths = subscriptionContainer.proxyKeyMap[job.observer._key].paths;
-
- if (paths) {
- for (const path of paths) {
- // Get property in new Value located at path
- let newValue = job.observer.value;
- let newValueDeepness = 0;
- for (const branch of path) {
- if (!isValidObject(newValue, true)) break;
- newValue = newValue[branch];
- newValueDeepness++;
- }
-
- // Get property in previous Value located at path
- let previousValue = job.observer.previousValue;
- let previousValueDeepness = 0;
- for (const branch of path) {
- if (!isValidObject(previousValue, true)) break;
- previousValue = previousValue[branch];
- previousValueDeepness++;
- }
-
- // Check if found values differ
- if (
- notEqual(newValue, previousValue) ||
- newValueDeepness !== previousValueDeepness
- ) {
- return true;
- }
- }
+ const selectorMethods = subscriptionContainer.selectorsWeakMap.get(
+ job.observer
+ )?.methods;
+
+ // If no selector functions found, return true.
+ // Because no specific part of the Observer was selected.
+ // -> The Subscription Container should be updated
+ // no matter what has updated in the Observer.
+ if (selectorMethods == null) return true;
+
+ // Check if a selected part of the Observer value has changed
+ const previousValue = job.observer.previousValue;
+ const newValue = job.observer.value;
+ for (const selectorMethod of selectorMethods) {
+ if (
+ notEqual(selectorMethod(newValue), selectorMethod(previousValue))
+ // || newValueDeepness !== previousValueDeepness // Not possible to check the object deepness
+ )
+ return true;
}
return false;
}
}
-/**
- * @param perform - If Job gets performed immediately
- */
export interface IngestConfigInterface {
+ /**
+ * Whether the ingested Job should be performed immediately
+ * or added to the queue first and then executed when it is his turn.
+ */
perform?: boolean;
}
diff --git a/packages/core/src/runtime/observer.ts b/packages/core/src/runtime/observer.ts
index 5710bd18..ebffc0fb 100644
--- a/packages/core/src/runtime/observer.ts
+++ b/packages/core/src/runtime/observer.ts
@@ -7,25 +7,54 @@ import {
IngestConfigInterface,
CreateRuntimeJobConfigInterface,
LogCodeManager,
+ generateId,
} from '../internal';
export type ObserverKey = string | number;
export class Observer {
+ // Agile Instance the Observer belongs to
public agileInstance: () => Agile;
+ // Key/Name identifier of the Observer
public _key?: ObserverKey;
- public dependents: Set = new Set(); // Observers that depend on this Observer
- public subscribedTo: Set = new Set(); // SubscriptionContainers (Components) that this Observer is subscribed to
- public value?: ValueType; // Value of Observer
- public previousValue?: ValueType; // Previous Value of Observer
+ // Observers that depend on this Observer
+ public dependents: Set = new Set();
+ // Subscription Containers (UI-Components) the Observer is subscribed to
+ public subscribedTo: Set = new Set();
+
+ // Current value of the Observer
+ public value?: ValueType;
+ // Previous value of the Observer
+ public previousValue?: ValueType;
/**
+ * An Observer manages the subscriptions to Subscription Containers (UI-Components)
+ * and dependencies to other Observers (Agile Classes)
+ * for an Agile Class such as the `State Class`.
+ *
+ * Agile Classes often use an Observer as an interface to the Runtime.
+ * In doing so, they ingest their own Observer into the Runtime
+ * when the Agile Class has changed in such a way
+ * that these changes need to be applied to UI-Components
+ * or dependent other Observers.
+ *
+ * After the Observer has been ingested into the Runtime
+ * wrapped into a Runtime-Job, it is first added to the Jobs queue
+ * to prevent race conditions.
+ * When it is executed, the Observer's `perform()` method is called,
+ * where the accordingly changes are applied to the Agile Class.
+ *
+ * Now that the Job was performed, it is added to the rerender queue,
+ * where the subscribed Subscription Container (UI-Components)
+ * of the Observer are updated (re-rendered).
+ *
+ * Note that the Observer itself is no standalone class
+ * and should be adapted to the Agile Class needs it belongs to.
+ *
* @internal
- * Observer - Handles subscriptions and dependencies of an Agile Class and is like an instance to the Runtime
- * Note: No stand alone class!!
- * @param agileInstance - An instance of Agile
- * @param config - Config
+ * @param agileInstance - Instance of Agile the Observer belongs to.
+ * @param config - Configuration object
*/
constructor(
agileInstance: Agile,
@@ -39,35 +68,40 @@ export class Observer {
this._key = config.key;
this.value = config.value;
this.previousValue = config.value;
- config.dependents?.forEach((observer) => this.depend(observer));
+ config.dependents?.forEach((observer) => this.addDependent(observer));
config.subs?.forEach((subscriptionContainer) =>
- this.subscribe(subscriptionContainer)
+ subscriptionContainer.addSubscription(this)
);
}
/**
- * @internal
- * Set Key/Name of Observer
+ * Updates the key/name identifier of the Observer.
+ *
+ * @public
+ * @param value - New key/name identifier.
*/
public set key(value: StateKey | undefined) {
this._key = value;
}
/**
- * @internal
- * Get Key/Name of Observer
+ * Returns the key/name identifier of the Observer.
+ *
+ * @public
*/
public get key(): StateKey | undefined {
return this._key;
}
- //=========================================================================================================
- // Ingest
- //=========================================================================================================
/**
- * @internal
- * Ingests Observer into Runtime
- * @param config - Configuration
+ * Passes the Observer into the runtime wrapped into a Runtime-Job
+ * where it is executed accordingly.
+ *
+ * During the execution the runtime performs the Observer's `perform()` method,
+ * updates its dependents and re-renders the UI-Components it is subscribed to.
+ *
+ * @public
+ * @param config - Configuration object
*/
public ingest(config: ObserverIngestConfigInterface = {}): void {
config = defineConfig(config, {
@@ -80,86 +114,80 @@ export class Observer {
force: false,
});
- // Create Job
+ // Create Runtime-Job
const job = new RuntimeJob(this, {
force: config.force,
sideEffects: config.sideEffects,
background: config.background,
- key: config.key || this._key,
+ key:
+ config.key ??
+ `${this._key != null ? this._key + '_' : ''}${generateId()}`,
});
+ // Pass created Job into the Runtime
this.agileInstance().runtime.ingest(job, {
perform: config.perform,
});
}
- //=========================================================================================================
- // Perform
- //=========================================================================================================
/**
- * @internal
- * Performs Job of Runtime
- * @param job - Job that gets performed
+ * Method executed by the Runtime to perform the Runtime-Job,
+ * previously ingested via the `ingest()` method.
+ *
+ * Note that this method should be overwritten
+ * to correctly apply the changes to the Agile Class
+ * the Observer belongs to.
+ *
+ * @public
+ * @param job - Runtime-Job to be performed.
*/
public perform(job: RuntimeJob): void {
LogCodeManager.log('17:03:00');
}
- //=========================================================================================================
- // Depend
- //=========================================================================================================
/**
- * @internal
- * Adds Dependent to Observer which gets ingested into the Runtime whenever this Observer mutates
- * @param observer - Observer that will depend on this Observer
+ * Makes the specified Observer depend on the Observer.
+ *
+ * A dependent Observer is always ingested into the Runtime,
+ * when the Observer it depends on has also been ingested.
+ *
+ * @public
+ * @param observer - Observer to depend on the Observer.
*/
- public depend(observer: Observer): void {
+ public addDependent(observer: Observer): void {
if (!this.dependents.has(observer)) this.dependents.add(observer);
}
+}
- //=========================================================================================================
- // Subscribe
- //=========================================================================================================
+export interface CreateObserverConfigInterface {
/**
- * @internal
- * Adds Subscription to Observer
- * @param subscriptionContainer - SubscriptionContainer(Component) that gets subscribed by this Observer
+ * Initial Observers to depend on the Observer.
+ * @default []
*/
- public subscribe(subscriptionContainer: SubscriptionContainer): void {
- if (!this.subscribedTo.has(subscriptionContainer)) {
- this.subscribedTo.add(subscriptionContainer);
-
- // Add this to subscriptionContainer to keep track of the Observers the subscriptionContainer hold
- subscriptionContainer.subscribers.add(this);
- }
- }
-
- //=========================================================================================================
- // Unsubscribe
- //=========================================================================================================
+ dependents?: Array;
/**
- * @internal
- * Removes Subscription from Observer
- * @param subscriptionContainer - SubscriptionContainer(Component) that gets unsubscribed by this Observer
+ * Initial Subscription Containers the Observer is subscribed to.
+ * @default []
*/
- public unsubscribe(subscriptionContainer: SubscriptionContainer): void {
- if (this.subscribedTo.has(subscriptionContainer)) {
- this.subscribedTo.delete(subscriptionContainer);
- subscriptionContainer.subscribers.delete(this);
- }
- }
-}
-
-/**
- * @param deps - Initial Dependents of Observer
- * @param subs - Initial Subscriptions of Observer
- * @param key - Key/Name of Observer
- * @param value - Initial Value of Observer
- */
-export interface CreateObserverConfigInterface {
- dependents?: Array;
subs?: Array;
+ /**
+ * Key/Name identifier of the Observer.
+ * @default undefined
+ */
key?: ObserverKey;
+ /**
+ * Initial value of the Observer.
+ *
+ * The value of an Observer is given to the Integration's `updateMethod()` method
+ * (Component Subscription Container) where it can be,
+ * for example, merged in a local State Management property of the UI-Component
+ * it is subscribed to.
+ *
+ * Also the selection of specific properties of an Agile Class value
+ * is based on the Observer `value` and `previousValue`.
+ *
+ * @default undefined
+ */
value?: ValueType;
}
diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts
index a3e559ea..d3eea3f2 100644
--- a/packages/core/src/runtime/runtime.job.ts
+++ b/packages/core/src/runtime/runtime.job.ts
@@ -1,19 +1,32 @@
import { Observer, defineConfig, SubscriptionContainer } from '../internal';
export class RuntimeJob {
- public _key?: RuntimeJobKey;
public config: RuntimeJobConfigInterface;
- public observer: ObserverType; // Observer the Job represents
- public rerender: boolean; // If Job will cause rerender on subscriptionContainer in Observer
- public performed = false; // If Job has been performed by Runtime
- public subscriptionContainersToUpdate = new Set(); // SubscriptionContainer (from Observer) that have to be updated/rerendered
- public triesToUpdate = 0; // How often not ready subscriptionContainers of this Job have been tried to update
+
+ // Key/Name identifier of the Runtime Job
+ public _key?: RuntimeJobKey;
+ // Observer the Job represents
+ public observer: ObserverType;
+ // Whether the Subscription Containers (UI-Components) of the Observer should be updated (re-rendered)
+ public rerender: boolean;
+ // Subscription Containers (UI-Components) of the Observer that have to be updated (re-rendered)
+ public subscriptionContainersToUpdate = new Set();
+ // How often not ready Subscription Containers of the Observer have been tried to update
+ public timesTriedToUpdateCount = 0;
+
+ // Whether the Job has been performed by the runtime
+ public performed = false;
/**
+ * A Runtime Job is sent to the Runtime on behalf of the Observer it represents.
+ *
+ * In the Runtime, the Observer is performed via its `perform()` method
+ * and the Subscription Containers (UI-Components)
+ * to which it is subscribed are updated (re-rendered) accordingly.
+ *
* @internal
- * Job - Represents Observer that gets performed by the Runtime
- * @param observer - Observer
- * @param config - Config
+ * @param observer - Observer to be represented by the Runtime Job.
+ * @param config - Configuration object
*/
constructor(
observer: ObserverType,
@@ -26,13 +39,13 @@ export class RuntimeJob {
exclude: [],
},
force: false,
- numberOfTriesToUpdate: 3,
+ maxTriesToUpdate: 3,
});
this.config = {
background: config.background,
force: config.force,
sideEffects: config.sideEffects,
- numberOfTriesToUpdate: config.numberOfTriesToUpdate,
+ maxTriesToUpdate: config.maxTriesToUpdate,
};
this.observer = observer;
this.rerender =
@@ -42,45 +55,76 @@ export class RuntimeJob {
this.subscriptionContainersToUpdate = new Set(observer.subscribedTo);
}
- public get key(): RuntimeJobKey | undefined {
- return this._key;
- }
-
+ /**
+ * Updates the key/name identifier of the Runtime Job.
+ *
+ * @public
+ * @param value - New key/name identifier.
+ */
public set key(value: RuntimeJobKey | undefined) {
this._key = value;
}
+
+ /**
+ * Returns the key/name identifier of the Runtime Job.
+ *
+ * @public
+ */
+ public get key(): RuntimeJobKey | undefined {
+ return this._key;
+ }
}
export type RuntimeJobKey = string | number;
-/**
- * @param key - Key/Name of RuntimeJob
- */
export interface CreateRuntimeJobConfigInterface
extends RuntimeJobConfigInterface {
+ /**
+ * Key/Name identifier of the Runtime Job.
+ * @default undefined
+ */
key?: RuntimeJobKey;
}
-/**
- * @param background - If Job gets executed in the background -> not causing any rerender
- * @param sideEffects - If SideEffects get executed
- * @param force - Force performing Job
- * @param numberOfTriesToUpdate - How often the runtime should try to update not ready SubscriptionContainers of this Job
- * If 'null' the runtime tries to update the not ready SubscriptionContainer until they are ready (infinite).
- * But be aware that this can lead to an overflow of 'old' Jobs after some time. (affects performance)
- */
export interface RuntimeJobConfigInterface {
+ /**
+ * Whether to perform the Runtime Job in background.
+ * So that the Subscription Containers (UI-Components) aren't notified
+ * of these changes and thus doesn't update (re-render).
+ * @default false
+ */
background?: boolean;
+ /**
+ * Configuration of the execution of defined side effects.
+ * @default {enabled: true, exclude: []}
+ */
sideEffects?: SideEffectConfigInterface;
+ /**
+ * Whether the Runtime Job should be forced through the runtime
+ * although it might be useless from the current viewpoint of the runtime.
+ * @default false
+ */
force?: boolean;
- numberOfTriesToUpdate?: number | null;
+ /**
+ * How often the Runtime should try to update not ready Subscription Containers
+ * subscribed by the Observer which the Job represents.
+ *
+ * When `null` the Runtime tries to update the not ready Subscription Containers
+ * until they are ready (infinite).
+ * @default 3
+ */
+ maxTriesToUpdate?: number | null;
}
-/**
- * @param enabled - If SideEffects get executed
- * @param exclude - SideEffect at Keys that doesn't get executed
- */
export interface SideEffectConfigInterface {
+ /**
+ * Whether to execute the defined side effects.
+ * @default true
+ */
enabled?: boolean;
+ /**
+ * Side effect key identifier that won't be executed.
+ * @default []
+ */
exclude?: string[];
}
diff --git a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts
index e9771a0b..bf974bea 100644
--- a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts
+++ b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts
@@ -5,18 +5,30 @@ import {
} from '../../../internal';
export class CallbackSubscriptionContainer extends SubscriptionContainer {
+ /**
+ * Callback function to trigger a re-render
+ * on the UI-Component which is represented by the Subscription Container.
+ */
public callback: Function;
/**
+ * A Callback Subscription Container represents a UI-Component in AgileTs
+ * and triggers re-renders on the UI-Component via the specified callback function.
+ *
+ * The Callback Subscription Container doesn't keep track of the Component itself.
+ * It only knows how to trigger re-renders on it by calling the callback function.
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/integration#callback-based)
+ *
* @internal
- * CallbackSubscriptionContainer - Subscription Container for Callback based Subscriptions
- * @param callback - Callback Function that causes rerender on Component that is subscribed by Agile
- * @param subs - Initial Subscriptions
- * @param config - Config
+ * @param callback - Callback function to cause a rerender on the Component
+ * to be represented by the Subscription Container.
+ * @param subs - Observers to be initial subscribed to the Subscription Container.
+ * @param config - Configuration object
*/
constructor(
callback: Function,
- subs: Array = [],
+ subs: Array | { [key: string]: Observer },
config: SubscriptionContainerConfigInterface = {}
) {
super(subs, config);
diff --git a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts
index d71c3e2f..ff80eded 100644
--- a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts
+++ b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts
@@ -4,19 +4,63 @@ import {
SubscriptionContainerConfigInterface,
} from '../../../internal';
-export class ComponentSubscriptionContainer extends SubscriptionContainer {
- public component: any;
+export class ComponentSubscriptionContainer<
+ C = any
+> extends SubscriptionContainer {
+ /**
+ * UI-Component which is represented by the Subscription Container
+ * and mutated via the Integration's `updateMethod()` method
+ * to cause re-renders on it.
+ */
+ public component: C;
/**
+ * A Component Subscription Container represents a UI-Component in AgileTs
+ * and triggers re-renders on the UI-Component by muting the specified Component Instance
+ * via the Integration's `updateMethod()` method.
+ * For example by updating a local State Management property of the Component
+ * (like in React Class Components the `this.state` property).
+ *
+ * The Component Subscription Container keeps track of the Component itself,
+ * to mutate it appropriately so that re-renders can be triggered on it.
+ *
+ * For this to work well, a Component Subscription Container is often object based.
+ * Meaning that each Observer was provided in an object keymap
+ * with a unique key identifier.
+ * ```
+ * // Object based (guaranteed unique key identifier)
+ * {
+ * state1: Observer,
+ * state2: Observer
+ * }
+ *
+ * // Array based (no guaranteed unique key identifier)
+ * [Observer, Observer]
+ * ```
+ * Thus the Integration's 'updateMethod()' method can be called
+ * with a complete object of updated Observer values.
+ * ```
+ * updateMethod: (componentInstance, updatedData) => {
+ * console.log(componentInstance); // Returns 'this.component'
+ * console.log(updatedData); // Returns updated Observer values keymap (see below)
+ * // {
+ * // state1: Observer.value,
+ * // state2: Observer.value,
+ * // }
+ * }
+ * ```
+ *
+ * [Learn more..](https://agile-ts.org/docs/core/integration#component-based)
+ *
* @internal
- * ComponentSubscriptionContainer - SubscriptionContainer for Component based Subscription
- * @param component - Component that is subscribed by Agile
- * @param subs - Initial Subscriptions
- * @param config - Config
+ * @param component - UI-Component to be represented by the Subscription Container
+ * and mutated via the Integration's 'updateMethod()' method to trigger re-renders on it.
+ * @param subs - Observers to be initial subscribed to the Subscription Container.
+ * @param config - Configuration object
*/
constructor(
- component: any,
- subs: Array