neo.mjs 10.0.0-beta.2 → 10.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ServiceWorker.mjs +2 -2
- package/apps/form/view/FormPageContainer.mjs +2 -3
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/portal/view/learn/ContentComponent.mjs +18 -11
- package/learn/README.md +9 -14
- package/learn/guides/Collections.md +436 -0
- package/learn/guides/CustomComponents.md +256 -14
- package/learn/guides/ExtendingNeoClasses.md +331 -0
- package/learn/guides/Forms.md +449 -1
- package/learn/guides/Layouts.md +246 -1
- package/learn/guides/Records.md +286 -0
- package/learn/guides/form_fields/ComboBox.md +241 -0
- package/learn/tree.json +8 -3
- package/package.json +1 -1
- package/resources/scss/src/apps/portal/learn/ContentComponent.scss +9 -0
- package/src/DefaultConfig.mjs +2 -2
- package/src/form/field/ComboBox.mjs +6 -1
- package/src/vdom/Helper.mjs +7 -5
package/ServiceWorker.mjs
CHANGED
@@ -26,10 +26,9 @@ class FormPageContainer extends FormContainer {
|
|
26
26
|
*/
|
27
27
|
style: {overflow: 'auto'},
|
28
28
|
/**
|
29
|
-
* @member {
|
29
|
+
* @member {String} tag='div'
|
30
30
|
*/
|
31
|
-
|
32
|
-
{cn: []} // using a div instead of a form tag
|
31
|
+
tag: 'div' // using a div instead of a form tag
|
33
32
|
}
|
34
33
|
}
|
35
34
|
|
package/apps/portal/index.html
CHANGED
@@ -3,11 +3,12 @@ import LivePreview from '../../../../src/code/LivePreview.mjs';
|
|
3
3
|
import {marked} from '../../../../node_modules/marked/lib/marked.esm.js';
|
4
4
|
|
5
5
|
const
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
regexInlineCode = /`([^`]+)`/g,
|
7
|
+
regexLabClose = /<!--\s*\/lab\s*-->/g,
|
8
|
+
regexLabOpen = /<!--\s*lab\s*-->/g,
|
9
|
+
regexLivePreview = /```(javascript|html|css|json)\s+live-preview\s*\n([\s\S]*?)\n\s*```/g,
|
10
|
+
regexNeoComponent = /```json\s+neo-component\s*\n([\s\S]*?)\n\s*```/g,
|
11
|
+
regexReadonly = /```(javascript|html|css|json)\s+readonly\s*\n([\s\S]*?)\n\s*```/g;
|
11
12
|
|
12
13
|
/**
|
13
14
|
* @class Portal.view.learn.ContentComponent
|
@@ -233,10 +234,10 @@ class ContentComponent extends Component {
|
|
233
234
|
*/
|
234
235
|
insertLabDivs(inputString) {
|
235
236
|
// Replace <!-- lab --> with <div class="lab">
|
236
|
-
inputString = inputString.replace(
|
237
|
+
inputString = inputString.replace(regexLabOpen, '<div class="lab">');
|
237
238
|
|
238
239
|
// Replace <!-- /lab --> with </div>
|
239
|
-
inputString = inputString.replace(
|
240
|
+
inputString = inputString.replace(regexLabClose, '</div>');
|
240
241
|
|
241
242
|
return inputString
|
242
243
|
}
|
@@ -307,7 +308,7 @@ class ContentComponent extends Component {
|
|
307
308
|
replacementPromises.push(
|
308
309
|
Neo.main.addon.HighlightJS.highlightAuto({html: code, windowId})
|
309
310
|
.then(highlightedHtml => ({
|
310
|
-
after: `<pre data-javascript id="pre-readonly-${Neo.core.IdGenerator.getId()}">${highlightedHtml}</pre>`,
|
311
|
+
after: `<pre data-javascript id="pre-readonly-${Neo.core.IdGenerator.getId()}">${highlightedHtml.trim()}</pre>`,
|
311
312
|
token: token
|
312
313
|
}))
|
313
314
|
);
|
@@ -336,7 +337,7 @@ class ContentComponent extends Component {
|
|
336
337
|
contentArray = content.split('\n'),
|
337
338
|
i = 1,
|
338
339
|
storeData = [],
|
339
|
-
tag;
|
340
|
+
headline, sideNavTitle, tag;
|
340
341
|
|
341
342
|
contentArray.forEach((line, index) => {
|
342
343
|
tag = null;
|
@@ -352,9 +353,15 @@ class ContentComponent extends Component {
|
|
352
353
|
}
|
353
354
|
|
354
355
|
if (tag) {
|
355
|
-
|
356
|
+
// Convert backticks to <code> tags for the article headline tags
|
357
|
+
headline = line.replace(regexInlineCode, '<code>$1</code>');
|
356
358
|
|
357
|
-
|
359
|
+
// Markdown titles can contain inline code, which we don't want to display inside PageSectionsList.
|
360
|
+
sideNavTitle = line.replaceAll('`', '');
|
361
|
+
|
362
|
+
storeData.push({id: i, name: sideNavTitle, sourceId: me.id, tag});
|
363
|
+
|
364
|
+
contentArray[index] = `<${tag} class="neo-${tag}" data-record-id="${i}">${headline}</${tag}>`;
|
358
365
|
|
359
366
|
i++
|
360
367
|
}
|
package/learn/README.md
CHANGED
@@ -16,7 +16,7 @@ Neo.mjs is the **first and only JavaScript framework** that achieves:
|
|
16
16
|
## 🌐 Best Learning Experience
|
17
17
|
|
18
18
|
**For the optimal learning experience, visit:**
|
19
|
-
**https://neomjs.com/dist/production/apps/portal/index.html#/learn**
|
19
|
+
**[neomjs.com#/learn](https://neomjs.com/dist/production/apps/portal/index.html#/learn)**
|
20
20
|
|
21
21
|
The web portal provides:
|
22
22
|
- ✨ **Enhanced Markdown rendering** with syntax highlighting
|
@@ -25,25 +25,20 @@ The web portal provides:
|
|
25
25
|
- 📱 **Responsive layout** optimized for learning
|
26
26
|
- 🔗 **Interactive navigation** between topics
|
27
27
|
|
28
|
-
## 📚 What's Inside
|
29
|
-
|
30
|
-
- **[Benefits](./benefits/)** - Why choose Neo.mjs and its revolutionary OMT advantages
|
31
|
-
- **[Getting Started](./gettingstarted/)** - Step-by-step introduction to Off-Main-Thread development
|
32
|
-
- **[Guides](./guides/)** - In-depth technical documentation on advanced concepts
|
33
|
-
- **[Tutorials](./tutorials/)** - Hands-on projects and practical examples
|
34
|
-
- **[JavaScript Classes](./javascript/)** - JavaScript fundamentals for Neo.mjs development
|
35
|
-
|
36
28
|
## 🎯 Learning Path
|
37
29
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
30
|
+
For a structured and effective learning experience, we recommend the following progression:
|
31
|
+
|
32
|
+
1. Start with [Benefits](./benefits/) to understand Neo.mjs's revolutionary OMT approach
|
33
|
+
2. Follow [Getting Started](./gettingstarted/) for Off-Main-Thread development basics
|
34
|
+
3. Explore [Guides](./guides/) for comprehensive technical knowledge
|
35
|
+
4. Practice with [Tutorials](./tutorials/) for hands-on experience
|
36
|
+
5. Review [JavaScript Classes](./javascript/) for JavaScript fundamentals relevant to Neo.mjs development
|
42
37
|
|
43
38
|
## 📖 Reading Options
|
44
39
|
|
45
40
|
- **🌐 Web Portal** (Recommended): Enhanced experience with live previews
|
46
|
-
→ [neomjs.com
|
41
|
+
→ [neomjs.com#/learn](https://neomjs.com/dist/production/apps/portal/index.html#/learn)
|
47
42
|
- **📁 GitHub**: Raw markdown files for quick reference or offline reading
|
48
43
|
- **💻 Local**: Clone the repo and browse files directly
|
49
44
|
|
@@ -0,0 +1,436 @@
|
|
1
|
+
|
2
|
+
Neo.mjs provides a powerful and flexible `Neo.collection.Base` class for managing data. This guide will explore its
|
3
|
+
features, including:
|
4
|
+
|
5
|
+
- **Core Concepts**: Understanding how collections store and manage data.
|
6
|
+
- **Adding and Removing Items**: Methods like `add`, `remove`, `insert`, `pop`, `push`, `shift`, and `unshift`.
|
7
|
+
- **Filtering Data**: Using `Neo.collection.Filter` to filter collection items.
|
8
|
+
- **Sorting Data**: Using `Neo.collection.Sorter` to sort collection items.
|
9
|
+
- **Event Handling**: How collections emit events on data mutations.
|
10
|
+
- **Performance Considerations**: Tips for optimizing collection usage.
|
11
|
+
|
12
|
+
## Core Concepts
|
13
|
+
|
14
|
+
The `Neo.collection.Base` class is the foundation for all collections in Neo.mjs. While a standard JavaScript array can
|
15
|
+
store data, it lacks the advanced features required for complex application development, such as automatic sorting,
|
16
|
+
filtering, efficient key-based lookups, and robust eventing. `Neo.collection.Base` addresses these needs by combining
|
17
|
+
an array with a Map, offering a powerful and performant data management solution.
|
18
|
+
|
19
|
+
Key characteristics include:
|
20
|
+
|
21
|
+
- **`items_` (Array)**: This private array holds the actual data items within the collection. While these can be any
|
22
|
+
JavaScript object, in many real-world Neo.mjs applications, especially when working with `Neo.data.Store`, these items
|
23
|
+
are `Record` instances (super lightweight object extensions). It provides ordered access to items, which is essential for
|
24
|
+
operations like iteration and maintaining insertion order (unless sorting is applied).
|
25
|
+
- **`map_` (Map)**: A `Map` object that stores a key-value pair for each collection item. The key is derived from the
|
26
|
+
`keyProperty` of each item, and the value is a reference to the item itself. This `Map` is crucial for enabling extremely
|
27
|
+
fast lookups.
|
28
|
+
|
29
|
+
### Why the Array + Map Combination?
|
30
|
+
|
31
|
+
This dual-structure approach provides the best of both worlds:
|
32
|
+
|
33
|
+
- **Ordered Access & Iteration (Array)**: The internal `items_` array allows for straightforward iteration over the
|
34
|
+
collection in a defined order (either insertion order or sorted order). Accessing an item by its index is an O(1)
|
35
|
+
(constant time) operation.
|
36
|
+
- **Efficient Key-Based Lookups (Map)**: The `map_` enables direct, constant-time (O(1)) retrieval of items by their
|
37
|
+
unique `keyProperty`. This is a significant performance advantage over searching a plain array, which would require
|
38
|
+
iterating through items until a match is found, resulting in an an O(n) (linear time) operation in the worst case.
|
39
|
+
|
40
|
+
**Performance Impact (Big O Notation):**
|
41
|
+
|
42
|
+
Consider a collection with 100,000 items:
|
43
|
+
|
44
|
+
- **Getting an item by ID (`collection.get(id)`)**: Thanks to the `map_`, this operation takes approximately the same
|
45
|
+
amount of time regardless of whether the collection has 10 items or 100,000 items. This is an O(1) operation.
|
46
|
+
- **Searching a plain array for an item by ID**: In the worst case (item is at the end or not present), you would have
|
47
|
+
to check every single item. For 100,000 items, this could mean 100,000 comparisons. This is an O(n) operation.
|
48
|
+
|
49
|
+
This fundamental difference in lookup efficiency is a primary reason for the `Neo.collection.Base` design, ensuring high
|
50
|
+
performance even with very large datasets.
|
51
|
+
|
52
|
+
- **`keyProperty`**: By default, this is set to `'id'`, meaning each item in the collection should have a unique `id`
|
53
|
+
property. This is crucial for the `map_` to function correctly.
|
54
|
+
- **`autoSort`**: If set to `true`, the collection will automatically sort its items when new ones are added or
|
55
|
+
inserted, based on the configured sorters.
|
56
|
+
- **`sourceId_`**: Collections can be linked to a source collection. This is a powerful feature where a collection
|
57
|
+
automatically mirrors the data mutations (additions, removals, reordering) of another collection. This is particularly
|
58
|
+
useful for creating filtered or sorted views of a larger dataset without duplicating the data.
|
59
|
+
|
60
|
+
### The `sourceId` Concept and Real-World Use Cases
|
61
|
+
|
62
|
+
The `sourceId` configuration allows you to create "derived" or "linked" collections that automatically stay in sync with
|
63
|
+
a "source" collection. This is achieved by the dependent collection listening to the `mutate` event of its source. Any
|
64
|
+
changes to the source collection's items are automatically propagated to the dependent collection.
|
65
|
+
|
66
|
+
**Real-World Use Cases:**
|
67
|
+
|
68
|
+
1. **Source-Detail Views:** Display a source list (source collection) and a filtered or sorted subset of that data in
|
69
|
+
a detail view (dependent collection). Changes in the source list automatically update the detail view.
|
70
|
+
2. **Multiple Synchronized Components:** On a dashboard, multiple widgets might display different views (filtered,
|
71
|
+
sorted, or transformed) of the same underlying dataset. A central source collection ensures all widgets remain consistent.
|
72
|
+
3. **Data Transformation Pipelines:** Chain collections together, where the output of one collection (as a source)
|
73
|
+
becomes the input for the next, allowing for complex data processing flows.
|
74
|
+
|
75
|
+
This mechanism significantly reduces boilerplate code for data synchronization and ensures data consistency across your
|
76
|
+
application.
|
77
|
+
|
78
|
+
**Advanced Use Case: Grid and ComboBox with Shared Data**
|
79
|
+
|
80
|
+
Consider a scenario where you have a large dataset (e.g., a list of products) that needs to be displayed in a grid, and
|
81
|
+
a subset of that data needs to be available in a combobox picker list.
|
82
|
+
|
83
|
+
If you were to use a single collection for both, typing into the combobox's input field to filter its options would
|
84
|
+
inadvertently filter the data displayed in your grid, which is typically not the desired behavior.
|
85
|
+
|
86
|
+
The `sourceId` concept provides an elegant solution:
|
87
|
+
|
88
|
+
1. **Primary Store (Collection)**: Create a primary `Neo.collection.Base` instance (acting as your data store) that
|
89
|
+
fetches the complete product list from a backend. This collection is not directly bound to any UI component.
|
90
|
+
2. **Grid Store (Child Collection)**: Create a second `Neo.collection.Base` instance for your grid. Set its `sourceId`
|
91
|
+
to the ID of your primary store. This grid store will automatically receive all data and mutations from the primary.
|
92
|
+
It can then apply its own sorting or filtering (e.g., to display only "in-stock" items) without affecting the primary
|
93
|
+
or other child collections.
|
94
|
+
3. **ComboBox Store (Child Collection)**: Create a third `Neo.collection.Base` instance for your combobox's picker list.
|
95
|
+
Set its `sourceId` to the ID of your primary store. This combobox store can then apply its own filters (e.g., based on
|
96
|
+
user input in the combobox field) and sorters, completely independently of the grid store or the primary store.
|
97
|
+
|
98
|
+
```javascript
|
99
|
+
import Collection from '../../src/collection/Base.mjs';
|
100
|
+
import Filter from '../../src/collection/Filter.mjs';
|
101
|
+
|
102
|
+
// 1. Primary Store: Fetches data from backend (simulated)
|
103
|
+
const primaryProductsStore = Neo.create(Collection, {
|
104
|
+
id: 'primaryProductsStore',
|
105
|
+
items: [
|
106
|
+
{id: 1, name: 'Laptop', category: 'Electronics', price: 1200, inStock: true},
|
107
|
+
{id: 2, name: 'Mouse', category: 'Electronics', price: 25, inStock: true},
|
108
|
+
{id: 3, name: 'Keyboard', category: 'Electronics', price: 75, inStock: false},
|
109
|
+
{id: 4, name: 'Monitor', category: 'Electronics', price: 300, inStock: true},
|
110
|
+
{id: 5, name: 'Desk Chair', category: 'Furniture', price: 150, inStock: true},
|
111
|
+
{id: 6, name: 'Webcam', category: 'Electronics', price: 50, inStock: false}
|
112
|
+
]
|
113
|
+
});
|
114
|
+
|
115
|
+
// 2. Grid Store: Displays all in-stock electronics, sorted by price
|
116
|
+
const gridStore = Neo.create(Collection, {
|
117
|
+
id : 'gridStore',
|
118
|
+
sourceId: 'primaryProductsStore', // Linked to primary
|
119
|
+
filters: [
|
120
|
+
{property: 'inStock', value: true},
|
121
|
+
{property: 'category', value: 'Electronics'}
|
122
|
+
],
|
123
|
+
sorters: [
|
124
|
+
{property: 'price', direction: 'ASC'}
|
125
|
+
]
|
126
|
+
});
|
127
|
+
|
128
|
+
// 3. ComboBox Store: Filters based on user input (e.g., 'web')
|
129
|
+
const comboBoxStore = Neo.create(Collection, {
|
130
|
+
id : 'comboBoxStore',
|
131
|
+
sourceId: 'primaryProductsStore', // Linked to primary
|
132
|
+
filters: [
|
133
|
+
// This filter would be dynamically updated by the combobox input
|
134
|
+
{property: 'name', operator: 'like', value: 'web'}
|
135
|
+
]
|
136
|
+
});
|
137
|
+
|
138
|
+
console.log('Primary Store Count:', primaryProductsStore.getCount()); // Output: 6
|
139
|
+
console.log('Grid Store Count (in-stock electronics):', gridStore.getCount()); // Output: 3 (Laptop, Mouse, Monitor)
|
140
|
+
console.log('ComboBox Store Count (name like "web"):', comboBoxStore.getCount()); // Output: 1 (Webcam)
|
141
|
+
|
142
|
+
// Simulate adding a new product to the primary store
|
143
|
+
primaryProductsStore.add({id: 7, name: 'Headphones', category: 'Electronics', price: 100, inStock: true});
|
144
|
+
|
145
|
+
console.log('Primary Store Count after add:', primaryProductsStore.getCount()); // Output: 7
|
146
|
+
console.log('Grid Store Count after add (Headphones match filters):', gridStore.getCount()); // Output: 4
|
147
|
+
console.log('ComboBox Store Count after add (Headphones do not match "web")):', comboBoxStore.getCount()); // Output: 1
|
148
|
+
|
149
|
+
// Simulate changing the combobox filter
|
150
|
+
comboBoxStore.filters[0].value = 'key';
|
151
|
+
console.log('ComboBox Store Count (name like "key"):', comboBoxStore.getCount()); // Output: 1 (Keyboard)
|
152
|
+
console.log('Grid Store Count (still unaffected):', gridStore.getCount()); // Output: 4
|
153
|
+
```
|
154
|
+
|
155
|
+
This example demonstrates how `sourceId` enables powerful data management patterns, allowing different parts of your
|
156
|
+
application to work with synchronized data while maintaining their own independent views.
|
157
|
+
|
158
|
+
|
159
|
+
- **`observable`**: Collections are observable, meaning they can emit events when their data changes (e.g., `mutate`,
|
160
|
+
- `filter`, `sort`).
|
161
|
+
|
162
|
+
### Example: Basic Collection Usage
|
163
|
+
|
164
|
+
```javascript
|
165
|
+
import Collection from '../../src/collection/Base.mjs';
|
166
|
+
|
167
|
+
const myCollection = Neo.create(Collection, {
|
168
|
+
items: [
|
169
|
+
{id: 1, name: 'Alice', age: 30},
|
170
|
+
{id: 2, name: 'Bob', age: 24},
|
171
|
+
{id: 3, name: 'Charlie', age: 35}
|
172
|
+
]
|
173
|
+
});
|
174
|
+
|
175
|
+
console.log(myCollection.getCount()); // Output: 3
|
176
|
+
console.log(myCollection.get(2)); // Output: {id: 2, name: 'Bob', age: 24}
|
177
|
+
```
|
178
|
+
|
179
|
+
## Adding and Removing Items
|
180
|
+
|
181
|
+
Collections provide several methods for manipulating their contents. When adding raw data (plain JavaScript objects) to
|
182
|
+
a `Neo.data.Store` (which extends `Neo.collection.Base`), these objects are automatically converted into Record
|
183
|
+
instances based on the store's model definition.
|
184
|
+
|
185
|
+
- **`add(item)`**: Adds one or more items (or Records) to the end of the collection.
|
186
|
+
- **`insert(index, item)`**: Inserts one or more items at a specific index.
|
187
|
+
- **`remove(key)`**: Removes an item by its key or the item itself.
|
188
|
+
- **`removeAt(index)`**: Removes an item at a specific index.
|
189
|
+
- **`pop()`**: Removes and returns the last item from the collection.
|
190
|
+
- **`push(item)`**: Adds one or more items to the end of the collection (alias for `add`).
|
191
|
+
- **`shift()`**: Removes and returns the first item from the collection.
|
192
|
+
- **`unshift(item)`**: Adds one or more items to the beginning of the collection.
|
193
|
+
- **`splice(index, removeCountOrToRemoveArray, toAddArray)`**: This is the **central and most powerful method** for
|
194
|
+
modifying a collection. It can simultaneously remove and add items at a specified index. Many other collection methods,
|
195
|
+
such as `add()`, `remove()`, `insert()`, `pop()`, `push()`, `shift()`, and `unshift()`, internally use `splice()` to
|
196
|
+
perform their operations, making it the core mechanism for all data mutations within a collection.
|
197
|
+
- **`clear()`**: Removes all items from the collection.
|
198
|
+
|
199
|
+
### Example: Adding and Removing
|
200
|
+
|
201
|
+
```javascript
|
202
|
+
import Collection from '../../src/collection/Base.mjs';
|
203
|
+
|
204
|
+
const users = Neo.create(Collection, {
|
205
|
+
items: [
|
206
|
+
{id: 1, name: 'Alice'},
|
207
|
+
{id: 2, name: 'Bob'}
|
208
|
+
]
|
209
|
+
});
|
210
|
+
|
211
|
+
users.add({id: 3, name: 'Charlie'});
|
212
|
+
console.log(users.getCount()); // Output: 3
|
213
|
+
|
214
|
+
users.insert(1, {id: 4, name: 'David'});
|
215
|
+
console.log(users.getAt(1)); // Output: {id: 4, name: 'David'}
|
216
|
+
|
217
|
+
users.remove(2); // Removes Bob
|
218
|
+
console.log(users.findFirst('name', 'Bob')); // Output: null
|
219
|
+
|
220
|
+
users.pop(); // Removes Charlie
|
221
|
+
console.log(users.getCount()); // Output: 2
|
222
|
+
|
223
|
+
users.clear();
|
224
|
+
console.log(users.getCount()); // Output: 0
|
225
|
+
```
|
226
|
+
|
227
|
+
## Filtering Data
|
228
|
+
|
229
|
+
`Neo.collection.Filter` instances are used to filter the items within a collection.
|
230
|
+
|
231
|
+
- **`property`**: The property of the item to filter by.
|
232
|
+
- **`operator`**: The comparison operator (e.g., `'===', '>', 'like', 'startsWith'`).
|
233
|
+
- **`value`**: The value to compare against.
|
234
|
+
- **`filterBy`**: A custom function for more complex filtering logic.
|
235
|
+
- **`disabled`**: Temporarily disable a filter.
|
236
|
+
|
237
|
+
The `Neo.collection.Base` class has a `filters_` config which accepts an array of `Filter` instances or configurations.
|
238
|
+
|
239
|
+
### The `allItems` Property
|
240
|
+
|
241
|
+
When a collection is filtered, the original, unfiltered set of items is preserved in the `allItems` property. This is a
|
242
|
+
significant advantage over simply filtering a JavaScript array, as it allows you to easily revert to the full dataset or
|
243
|
+
perform operations on it without losing the original data.
|
244
|
+
|
245
|
+
### Reactivity of Filters
|
246
|
+
|
247
|
+
Each `Neo.collection.Filter` is a Neo instance, and its configuration properties are reactive. This means that changes
|
248
|
+
to the `filters` array or to properties of individual `Filter` instances within the array will automatically trigger a
|
249
|
+
re-filter of the collection. **It's important to note that simply mutating the `filters` array (e.g., using `push()`,
|
250
|
+
`pop()`, `splice()`) will not trigger reactivity. For changes to the array's contents to take effect, you must reassign
|
251
|
+
the `filters` property (e.g., `collection.filters = [...collection.filters];`) or modify properties of existing filter
|
252
|
+
instances.**
|
253
|
+
|
254
|
+
### Example: Filtering a Collection
|
255
|
+
|
256
|
+
```javascript
|
257
|
+
import Collection from '../../src/collection/Base.mjs';
|
258
|
+
import Filter from '../../src/collection/Filter.mjs';
|
259
|
+
|
260
|
+
const products = Neo.create(Collection, {
|
261
|
+
items: [
|
262
|
+
{id: 1, name: 'Laptop', price: 1200},
|
263
|
+
{id: 2, name: 'Mouse', price: 25},
|
264
|
+
{id: 3, name: 'Keyboard', price: 75},
|
265
|
+
{id: 4, name: 'Monitor', price: 300}
|
266
|
+
]
|
267
|
+
});
|
268
|
+
|
269
|
+
// Filter products with price > 100
|
270
|
+
products.filters = [{
|
271
|
+
property: 'price',
|
272
|
+
operator: '>',
|
273
|
+
value : 100
|
274
|
+
}];
|
275
|
+
|
276
|
+
console.log(products.getCount()); // Output: 2 (Laptop, Monitor)
|
277
|
+
console.log(products.allItems.getCount()); // Output: 4 (all original items)
|
278
|
+
|
279
|
+
// Add another filter: name contains 'o'
|
280
|
+
products.filters.push({
|
281
|
+
property: 'name',
|
282
|
+
operator: 'like',
|
283
|
+
value : 'o'
|
284
|
+
});
|
285
|
+
// Reassign the filters array to trigger reactivity
|
286
|
+
products.filters = [...products.filters];
|
287
|
+
|
288
|
+
console.log(products.getCount()); // Output: 1 (Monitor)
|
289
|
+
|
290
|
+
// Dynamically disable the first filter
|
291
|
+
products.filters[0].disabled = true;
|
292
|
+
console.log(products.getCount()); // Output: 1 (Monitor - only the 'name like o' filter is active)
|
293
|
+
|
294
|
+
// Clear filters
|
295
|
+
products.clearFilters();
|
296
|
+
console.log(products.getCount()); // Output: 4
|
297
|
+
|
298
|
+
// Adding an item to a filtered collection
|
299
|
+
products.filters = [{ property: 'price', operator: '>', value: 500 }];
|
300
|
+
products.add({ id: 5, name: 'Webcam', price: 50 }); // Does not match filter
|
301
|
+
console.log(products.getCount()); // Output: 1 (Laptop)
|
302
|
+
console.log(products.allItems.getCount()); // Output: 5 (Webcam is in allItems)
|
303
|
+
```
|
304
|
+
|
305
|
+
## Sorting Data
|
306
|
+
|
307
|
+
`Neo.collection.Sorter` instances are used to sort the items within a collection.
|
308
|
+
|
309
|
+
- **`property`**: The property of the item to sort by.
|
310
|
+
- **`direction`**: The sort direction (`'ASC'` or `'DESC'`).
|
311
|
+
- **`sortBy`**: A custom function for more complex sorting logic.
|
312
|
+
|
313
|
+
The `Neo.collection.Base` class has a `sorters_` config which accepts an array of `Sorter` instances or configurations.
|
314
|
+
|
315
|
+
### Reactivity of Sorters
|
316
|
+
|
317
|
+
Each `Neo.collection.Sorter` is a Neo instance, and its configuration properties are reactive. This means that changes
|
318
|
+
to the `sorters` array or to properties of individual `Sorter` instances within the array will automatically trigger a
|
319
|
+
re-sort of the collection. **It's important to note that simply mutating the `sorters` array (e.g., using `push()`,
|
320
|
+
`pop()`, `splice()`) will not trigger reactivity. For changes to the array's contents to take effect, you must reassign
|
321
|
+
the `sorters` property (e.g., `collection.sorters = [...collection.sorters];`) or modify properties of existing sorter
|
322
|
+
instances.**
|
323
|
+
|
324
|
+
### Example: Sorting a Collection
|
325
|
+
|
326
|
+
```javascript
|
327
|
+
import Collection from '../../src/collection/Base.mjs';
|
328
|
+
import Sorter from '../../src/collection/Sorter.mjs';
|
329
|
+
|
330
|
+
const employees = Neo.create(Collection, {
|
331
|
+
items: [
|
332
|
+
{id: 1, name: 'Alice', salary: 50000},
|
333
|
+
{id: 2, name: 'Bob', salary: 75000},
|
334
|
+
{id: 3, name: 'Charlie', salary: 60000}
|
335
|
+
]
|
336
|
+
});
|
337
|
+
|
338
|
+
// Sort by salary descending
|
339
|
+
employees.sorters = [{
|
340
|
+
property : 'salary',
|
341
|
+
direction: 'DESC'
|
342
|
+
}];
|
343
|
+
|
344
|
+
console.log(employees.first().name); // Output: Bob
|
345
|
+
|
346
|
+
// Dynamically change the sort property
|
347
|
+
employees.sorters[0].property = 'name';
|
348
|
+
employees.sorters[0].direction = 'ASC';
|
349
|
+
console.log(employees.first().name); // Output: Alice
|
350
|
+
|
351
|
+
// Clear sorters and restore original order (if applicable)
|
352
|
+
employees.clearSorters(true);
|
353
|
+
console.log(employees.first().name); // Output: Alice (assuming original order was by id)
|
354
|
+
```
|
355
|
+
|
356
|
+
## Event Handling
|
357
|
+
|
358
|
+
Collections are observable and emit events when their state changes. The primary event is `mutate`, which fires after
|
359
|
+
any operation that changes the collection's items.
|
360
|
+
|
361
|
+
- **`mutate`**: Fired after items are added, removed, or reordered. Provides `addedItems` and `removedItems` arrays.
|
362
|
+
- **`filter`**: Fired after the collection is filtered.
|
363
|
+
- **`sort`**: Fired after the collection is sorted.
|
364
|
+
|
365
|
+
### Example: Listening to Collection Events
|
366
|
+
|
367
|
+
```javascript
|
368
|
+
import Collection from '../../src/collection/Base.mjs';
|
369
|
+
|
370
|
+
const tasks = Neo.create(Collection, {
|
371
|
+
items: [
|
372
|
+
{id: 1, description: 'Buy groceries'},
|
373
|
+
{id: 2, description: 'Walk the dog'}
|
374
|
+
]
|
375
|
+
});
|
376
|
+
|
377
|
+
tasks.on('mutate', ({addedItems, removedItems}) => {
|
378
|
+
if (addedItems.length > 0) {
|
379
|
+
console.log('Added:', addedItems.map(item => item.description));
|
380
|
+
}
|
381
|
+
if (removedItems.length > 0) {
|
382
|
+
console.log('Removed:', removedItems.map(item => item.description));
|
383
|
+
}
|
384
|
+
});
|
385
|
+
|
386
|
+
tasks.add({id: 3, description: 'Clean the house'});
|
387
|
+
// Output: Added: ["Clean the house"]
|
388
|
+
|
389
|
+
tasks.remove(1);
|
390
|
+
// Output: Removed: ["Buy groceries"]
|
391
|
+
```
|
392
|
+
|
393
|
+
## Performance Considerations
|
394
|
+
|
395
|
+
- **`startUpdate()` and `endUpdate()`**: For bulk operations (multiple additions, removals, or modifications), wrap
|
396
|
+
your changes within `startUpdate()` and `endUpdate()`. This prevents multiple `mutate` events from firing, improving performance.
|
397
|
+
- **`silentUpdateMode`**: When using `startUpdate(true)`, the `mutate` event will not fire at all when `endUpdate(true)`
|
398
|
+
is called. This is useful when you want to perform internal updates without notifying listeners.
|
399
|
+
- **`filterMode`**: The `filterMode` config can be set to `'primitive'` or `'advanced'`. `'primitive'` is generally
|
400
|
+
faster for simple filters, while `'advanced'` is needed for filters that depend on other collection items.
|
401
|
+
- **`keyProperty`**: Ensure your `keyProperty` is truly unique for each item to leverage the `map_` for efficient lookups.
|
402
|
+
|
403
|
+
## Cloning Collections
|
404
|
+
|
405
|
+
When you clone a collection using `collection.clone()`, a new collection instance is created with a shallow copy of the
|
406
|
+
*current items*. However, the filters and sorters of the cloned collection are based on the *original configuration* of
|
407
|
+
the source collection, not its currently active filters or sorters. This means if the original collection had filters or
|
408
|
+
sorters applied after its initial creation, the cloned collection will not inherit those dynamic changes unless explicitly
|
409
|
+
reapplied.
|
410
|
+
|
411
|
+
## Collections and Records: A Powerful Combination
|
412
|
+
|
413
|
+
While `Neo.collection.Base` is designed to manage any type of JavaScript object, its full potential within the Neo.mjs
|
414
|
+
framework is often realized when combined with `Neo.data.Model` instances (Records) through `Neo.data.Store`.
|
415
|
+
`Neo.data.Store` extends `Neo.collection.Base`, specializing it for the management of structured, reactive data.
|
416
|
+
|
417
|
+
When a `Neo.data.Store` holds `Records`:
|
418
|
+
|
419
|
+
- **Automatic Record Conversion**: Raw data (plain JavaScript objects) added to a `Store` are automatically converted
|
420
|
+
into reactive `Record` instances based on the `Store`'s associated `Neo.data.Model`.
|
421
|
+
- **Dual-Layered Reactivity**: You gain both collection-level reactivity (for structural changes like adding/removing
|
422
|
+
records) and record-level reactivity (for changes to individual record fields). This enables highly optimized UI
|
423
|
+
updates, as components can react precisely to the specific data changes that affect them.
|
424
|
+
- **Enhanced Data Management**: Records bring features like type conversion, validation, and dirty tracking to the
|
425
|
+
items within the collection, providing a more robust data management solution.
|
426
|
+
|
427
|
+
This powerful combination forms the backbone of data-driven components like Grids, ComboBoxes, and other data-bound UI
|
428
|
+
elements in Neo.mjs.
|
429
|
+
|
430
|
+
## Conclusion
|
431
|
+
|
432
|
+
Neo.mjs Collections provide a robust, high-performance, and flexible solution for managing data within your applications.
|
433
|
+
By combining the ordered access of arrays with the efficient key-based lookups of Maps, and offering powerful features
|
434
|
+
like reactive filtering, sorting, and `sourceId` linking, they significantly simplify complex data management tasks.
|
435
|
+
Understanding these core concepts and leveraging the provided methods will enable you to build highly responsive and
|
436
|
+
data-driven Neo.mjs applications with ease.
|