neo.mjs 10.0.0-alpha.5 → 10.0.0-beta.2
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/colors/view/GridContainer.mjs +1 -1
- package/apps/covid/view/AttributionComponent.mjs +1 -1
- package/apps/covid/view/HeaderContainer.mjs +6 -6
- package/apps/covid/view/MainContainerController.mjs +5 -5
- package/apps/covid/view/TableContainerController.mjs +1 -1
- package/apps/covid/view/country/Gallery.mjs +13 -13
- package/apps/covid/view/country/Helix.mjs +13 -13
- package/apps/covid/view/country/HistoricalDataTable.mjs +1 -1
- package/apps/email/view/Viewport.mjs +2 -2
- package/apps/form/view/SideNavList.mjs +1 -1
- package/apps/portal/index.html +1 -1
- package/apps/portal/resources/data/examples_devmode.json +26 -27
- package/apps/portal/resources/data/examples_dist_dev.json +26 -27
- package/apps/portal/resources/data/examples_dist_esm.json +25 -26
- package/apps/portal/resources/data/examples_dist_prod.json +26 -27
- package/apps/portal/view/HeaderToolbar.mjs +3 -3
- package/apps/portal/view/about/Container.mjs +2 -2
- package/apps/portal/view/about/MemberContainer.mjs +3 -3
- package/apps/portal/view/blog/List.mjs +7 -7
- package/apps/portal/view/examples/List.mjs +4 -4
- package/apps/portal/view/home/ContentBox.mjs +2 -2
- package/apps/portal/view/home/FeatureSection.mjs +3 -3
- package/apps/portal/view/home/FooterContainer.mjs +7 -7
- package/apps/portal/view/home/parts/AfterMath.mjs +3 -3
- package/apps/portal/view/home/parts/MainNeo.mjs +3 -3
- package/apps/portal/view/home/parts/References.mjs +6 -6
- package/apps/portal/view/learn/ContentComponent.mjs +102 -111
- package/apps/portal/view/learn/PageSectionsContainer.mjs +1 -1
- package/apps/portal/view/learn/PageSectionsList.mjs +2 -2
- package/apps/portal/view/services/Component.mjs +16 -16
- package/apps/realworld/view/FooterComponent.mjs +1 -1
- package/apps/realworld/view/HeaderComponent.mjs +8 -8
- package/apps/realworld/view/HomeComponent.mjs +6 -6
- package/apps/realworld/view/article/CommentComponent.mjs +4 -4
- package/apps/realworld/view/article/Component.mjs +14 -14
- package/apps/realworld/view/article/CreateCommentComponent.mjs +3 -3
- package/apps/realworld/view/article/CreateComponent.mjs +3 -3
- package/apps/realworld/view/article/PreviewComponent.mjs +1 -1
- package/apps/realworld/view/article/TagListComponent.mjs +2 -2
- package/apps/realworld/view/user/ProfileComponent.mjs +8 -8
- package/apps/realworld/view/user/SettingsComponent.mjs +4 -4
- package/apps/realworld/view/user/SignUpComponent.mjs +4 -4
- package/apps/realworld2/view/FooterComponent.mjs +1 -1
- package/apps/realworld2/view/HomeContainer.mjs +3 -3
- package/apps/realworld2/view/article/DetailsContainer.mjs +1 -1
- package/apps/realworld2/view/article/PreviewComponent.mjs +7 -7
- package/apps/realworld2/view/article/TagListComponent.mjs +2 -2
- package/apps/realworld2/view/user/ProfileContainer.mjs +1 -1
- package/apps/route/view/center/CardAdministration.mjs +2 -2
- package/apps/route/view/center/CardAdministrationDenied.mjs +1 -1
- package/apps/route/view/center/CardContact.mjs +2 -2
- package/apps/route/view/center/CardHome.mjs +1 -1
- package/apps/route/view/center/CardSection1.mjs +1 -1
- package/apps/route/view/center/CardSection2.mjs +1 -1
- package/apps/sharedcovid/view/AttributionComponent.mjs +1 -1
- package/apps/sharedcovid/view/HeaderContainer.mjs +6 -6
- package/apps/sharedcovid/view/MainContainerController.mjs +5 -5
- package/apps/sharedcovid/view/TableContainerController.mjs +1 -1
- package/apps/sharedcovid/view/country/Gallery.mjs +13 -13
- package/apps/sharedcovid/view/country/Helix.mjs +13 -13
- package/apps/sharedcovid/view/country/HistoricalDataTable.mjs +1 -1
- package/apps/shareddialog/childapps/shareddialog2/view/MainContainer.mjs +1 -1
- package/apps/shareddialog/view/MainContainer.mjs +1 -1
- package/buildScripts/createApp.mjs +2 -2
- package/examples/table/cellEditing/MainContainer.mjs +1 -1
- package/examples/table/container/MainContainer.mjs +3 -3
- package/examples/table/nestedRecordFields/Viewport.mjs +6 -6
- package/examples/tableFiltering/MainContainer.mjs +1 -1
- package/examples/tablePerformance/MainContainer.mjs +1 -1
- package/examples/tablePerformance/MainContainer2.mjs +1 -1
- package/examples/tablePerformance/MainContainer3.mjs +2 -2
- package/examples/tableStore/MainContainer.mjs +2 -2
- package/learn/Glossary.md +261 -0
- package/learn/UsingTheseTopics.md +2 -2
- package/learn/benefits/ConfigSystem.md +538 -28
- package/learn/benefits/Effort.md +47 -2
- package/learn/benefits/Features.md +50 -32
- package/learn/benefits/FormsEngine.md +68 -38
- package/learn/benefits/MultiWindow.md +33 -7
- package/learn/benefits/OffTheMainThread.md +2 -2
- package/learn/benefits/Quick.md +45 -12
- package/learn/benefits/RPCLayer.md +75 -0
- package/learn/benefits/Speed.md +16 -11
- package/learn/gettingstarted/ComponentModels.md +4 -4
- package/learn/gettingstarted/Config.md +6 -6
- package/learn/gettingstarted/DescribingTheUI.md +4 -4
- package/learn/gettingstarted/Events.md +6 -6
- package/learn/gettingstarted/Extending.md +4 -4
- package/learn/gettingstarted/References.md +6 -6
- package/learn/gettingstarted/Workspaces.md +6 -6
- package/learn/guides/ApplicationBootstrap.md +26 -26
- package/learn/guides/ComponentsAndContainers.md +12 -12
- package/learn/guides/ConfigSystemDeepDive.md +280 -0
- package/learn/guides/CustomComponents.md +2 -2
- package/learn/guides/DeclarativeComponentTreesVsImperativeVdom.md +17 -17
- package/learn/guides/InstanceLifecycle.md +295 -1
- package/learn/guides/MainThreadAddons.md +475 -0
- package/learn/guides/PortalApp.md +2 -2
- package/learn/guides/StateProviders.md +12 -12
- package/learn/guides/WorkingWithVDom.md +14 -14
- package/learn/guides/events/CustomEvents.md +16 -16
- package/learn/guides/events/DomEvents.md +12 -12
- package/learn/javascript/ClassFeatures.md +3 -2
- package/learn/javascript/Classes.md +8 -8
- package/learn/javascript/NewNode.md +4 -4
- package/learn/javascript/Overrides.md +8 -8
- package/learn/javascript/Super.md +10 -8
- package/learn/tree.json +52 -51
- package/learn/tutorials/Earthquakes.md +54 -57
- package/learn/tutorials/TodoList.md +4 -4
- package/package.json +2 -2
- package/resources/scss/src/apps/portal/learn/ContentComponent.scss +12 -0
- package/resources/scss/src/table/{View.scss → Body.scss} +1 -1
- package/resources/scss/src/table/plugin/CellEditing.scss +1 -1
- package/resources/scss/theme-dark/table/{View.scss → Body.scss} +1 -1
- package/resources/scss/theme-light/table/{View.scss → Body.scss} +1 -1
- package/resources/scss/theme-neo-light/Global.scss +1 -2
- package/resources/scss/theme-neo-light/table/{View.scss → Body.scss} +1 -1
- package/src/DefaultConfig.mjs +2 -2
- package/src/Main.mjs +8 -7
- package/src/Neo.mjs +16 -2
- package/src/button/Base.mjs +2 -2
- package/src/calendar/view/SettingsContainer.mjs +2 -2
- package/src/calendar/view/YearComponent.mjs +9 -9
- package/src/calendar/view/calendars/ColorsList.mjs +1 -1
- package/src/calendar/view/calendars/List.mjs +1 -1
- package/src/calendar/view/month/Component.mjs +15 -15
- package/src/calendar/view/week/Component.mjs +12 -12
- package/src/calendar/view/week/EventDragZone.mjs +4 -4
- package/src/calendar/view/week/TimeAxisComponent.mjs +3 -3
- package/src/component/Base.mjs +17 -2
- package/src/component/Carousel.mjs +2 -2
- package/src/component/Chip.mjs +3 -3
- package/src/component/Circle.mjs +2 -2
- package/src/component/DateSelector.mjs +8 -8
- package/src/component/Helix.mjs +1 -1
- package/src/component/Label.mjs +3 -18
- package/src/component/Legend.mjs +3 -3
- package/src/component/MagicMoveText.mjs +6 -14
- package/src/component/Process.mjs +3 -3
- package/src/component/Progress.mjs +1 -1
- package/src/component/StatusBadge.mjs +2 -2
- package/src/component/Timer.mjs +2 -2
- package/src/component/Toast.mjs +5 -3
- package/src/container/AccordionItem.mjs +2 -2
- package/src/container/Base.mjs +1 -1
- package/src/core/Base.mjs +77 -14
- package/src/core/Util.mjs +14 -2
- package/src/date/DayViewComponent.mjs +2 -2
- package/src/date/SelectorContainer.mjs +1 -1
- package/src/draggable/grid/header/toolbar/SortZone.mjs +21 -21
- package/src/draggable/table/header/toolbar/SortZone.mjs +1 -1
- package/src/form/field/CheckBox.mjs +4 -4
- package/src/form/field/FileUpload.mjs +25 -39
- package/src/form/field/Range.mjs +1 -1
- package/src/form/field/Text.mjs +3 -3
- package/src/form/field/TextArea.mjs +2 -3
- package/src/grid/Body.mjs +8 -5
- package/src/grid/_export.mjs +1 -1
- package/src/list/Color.mjs +2 -2
- package/src/main/DeltaUpdates.mjs +157 -98
- package/src/main/addon/AmCharts.mjs +61 -84
- package/src/main/addon/Base.mjs +161 -42
- package/src/main/addon/GoogleMaps.mjs +9 -16
- package/src/main/addon/HighlightJS.mjs +2 -13
- package/src/main/addon/IntersectionObserver.mjs +21 -21
- package/src/main/addon/MonacoEditor.mjs +32 -64
- package/src/manager/ClassHierarchy.mjs +114 -0
- package/src/menu/List.mjs +1 -1
- package/src/plugin/Popover.mjs +2 -2
- package/src/sitemap/Component.mjs +1 -1
- package/src/table/{View.mjs → Body.mjs} +25 -22
- package/src/table/Container.mjs +43 -43
- package/src/table/_export.mjs +2 -2
- package/src/table/plugin/CellEditing.mjs +19 -19
- package/src/tooltip/Base.mjs +1 -6
- package/src/tree/Accordion.mjs +3 -3
- package/src/vdom/Helper.mjs +19 -22
- package/src/worker/App.mjs +1 -2
- package/src/worker/Base.mjs +7 -5
- package/src/worker/Canvas.mjs +2 -3
- package/src/worker/Data.mjs +5 -7
- package/src/worker/Task.mjs +2 -3
- package/src/worker/VDom.mjs +3 -4
- package/src/worker/mixin/RemoteMethodAccess.mjs +5 -2
- package/learn/guides/MainThreadAddonExample.md +0 -15
- package/learn/guides/MainThreadAddonIntro.md +0 -44
@@ -0,0 +1,475 @@
|
|
1
|
+
Neo.mjs is multi-threaded. There are worker threads that handle data access, application logic, and
|
2
|
+
keeping track of DOM updates. Practically all your application logic is run in parallel in these
|
3
|
+
threads. However, anything that needs to actually reference or update the DOM (`window.document`),
|
4
|
+
or just use the `window` object, must be done in the main application thread.
|
5
|
+
|
6
|
+
That's the purpose of main thread addons. These are classes whose methods can be accessed from other
|
7
|
+
web workers, but are actually executed in the main thread.
|
8
|
+
|
9
|
+
For example, what if you needed to read the browser's URL? That information is in `window.location`.
|
10
|
+
But `window` is a main thread variable! To access that from a web-worker our code has to say "hey
|
11
|
+
main thread, please return a specified `window` property." Neo.mjs lets you do that via
|
12
|
+
`Neo.Main.getByPath()`. For example, the following statement logs the URL query string.
|
13
|
+
|
14
|
+
```javascript readonly
|
15
|
+
const search = await Neo.Main.getByPath({path: 'window.location.search'});
|
16
|
+
console.log(search); // Logs the search string
|
17
|
+
```
|
18
|
+
|
19
|
+
`Neo.Main` & `Neo.main.DomAccess` provide some basic methods for accessing the main thread, but in
|
20
|
+
case you want to use a third party library which relies on directly working with the DOM, you'd use
|
21
|
+
a main thread addon.
|
22
|
+
|
23
|
+
Google Maps is a good example of this. In Neo.mjs, most views are responsible for updating their own
|
24
|
+
vdom, but the responsibility for rendering maps and markers is handled by Google Maps itself — we
|
25
|
+
_ask_ Google Maps to do certain things via the Google Maps API. Therefore, in Neo.mjs, Google Maps
|
26
|
+
is implemented as a main thread addon which loads the libraries and exposes whatever methods we'll
|
27
|
+
need to run from the other Neo.mjs threads. In addition, in a Neo.mjs application we want to use
|
28
|
+
Google Maps like any other component, so Neo.mjs also provides a component wrapper. In summary:
|
29
|
+
- The main-thread addon contains the code run in the main thread, and exposes what methods can be
|
30
|
+
run by other web-workers (remote method access)
|
31
|
+
- The component wrapper lets you use it like any other component, internally calling the main thread
|
32
|
+
methods as needed.
|
33
|
+
|
34
|
+
## How it Works: The Round Trip of a Remote Call
|
35
|
+
|
36
|
+
When your code in the App Worker calls an addon method, a sophisticated, promise-based communication
|
37
|
+
happens automatically behind the scenes.
|
38
|
+
|
39
|
+
Let's trace the journey of a single call:
|
40
|
+
|
41
|
+
```javascript readonly
|
42
|
+
// Inside a component in the App Worker
|
43
|
+
async function getMySetting() {
|
44
|
+
let data = await Neo.main.addon.LocalStorage.readLocalStorageItem({key: 'my-setting'});
|
45
|
+
console.log(data.value);
|
46
|
+
}
|
47
|
+
```
|
48
|
+
|
49
|
+
Here's what happens when `getMySetting()` is executed:
|
50
|
+
|
51
|
+
```text
|
52
|
+
+------------------------------------------------+ +------------------------------------------------+
|
53
|
+
| App Worker | | Main Thread |
|
54
|
+
+------------------------------------------------+ +------------------------------------------------+
|
55
|
+
| | | |
|
56
|
+
| 1. Your code calls a proxy method. | | |
|
57
|
+
| e.g., `addon.readLocalStorageItem()` | | |
|
58
|
+
| | | |
|
59
|
+
| This immediately returns a `Promise`. | | |
|
60
|
+
| | | |
|
61
|
+
|------------------------------------------------| | |
|
62
|
+
| | | |
|
63
|
+
| 2. A message is sent to the Main Thread | ----> | 3. The message is received. The framework |
|
64
|
+
| containing the target & arguments. | | finds the addon instance and calls the |
|
65
|
+
| | | *real* method with the arguments. |
|
66
|
+
| | | |
|
67
|
+
|------------------------------------------------| |------------------------------------------------|
|
68
|
+
| | | |
|
69
|
+
| 5. The Promise from Step 1 is resolved with | <---- | 4. The method returns a value. The framework |
|
70
|
+
| the value from the reply message. | | packages this value in a reply message |
|
71
|
+
| | | and sends it back to the App Worker. |
|
72
|
+
| The `await` keyword gets the final value. | | |
|
73
|
+
| | | |
|
74
|
+
+------------------------------------------------+ +------------------------------------------------+
|
75
|
+
```
|
76
|
+
|
77
|
+
1. **The Call (App Worker)**: Your code calls what looks like a normal static method. However, this
|
78
|
+
`readLocalStorageItem` function is actually a "proxy" or "stub" created by the framework.
|
79
|
+
2. **The Message (App Worker -> Main Thread)**: The proxy function immediately returns a `Promise`
|
80
|
+
and sends a message to the main thread containing the addon's class name
|
81
|
+
(`Neo.main.addon.LocalStorage`), the method name (`readLocalStorageItem`), and the arguments
|
82
|
+
(`{key: 'my-setting'}`).
|
83
|
+
3. **The Execution (Main Thread)**: The main thread receives the message, finds the `LocalStorage`
|
84
|
+
addon instance, and calls the real `readLocalStorageItem` method with the provided arguments.
|
85
|
+
4. **The Return (Main Thread -> App Worker)**: The method returns the value from `localStorage`. The
|
86
|
+
main thread packages this return value into a "reply" message and sends it back to the App
|
87
|
+
Worker.
|
88
|
+
5. **Promise Resolution (App Worker)**: The App Worker receives the reply and uses it to resolve the
|
89
|
+
Promise from Step 2. The `await` is now complete, and the `data` variable receives the value.
|
90
|
+
|
91
|
+
This entire round trip is completely managed by the framework. As a developer, you only need to
|
92
|
+
`await` the result, just like any other asynchronous function.
|
93
|
+
|
94
|
+
## Anatomy of an Addon: `LocalStorage` and `Cookie` Examples
|
95
|
+
|
96
|
+
Addons are standard Neo.mjs classes that extend `Neo.main.addon.Base`. They define their public API
|
97
|
+
through the `remote` config.
|
98
|
+
|
99
|
+
### The `LocalStorage` Addon
|
100
|
+
|
101
|
+
The `LocalStorage` addon provides basic CRUD (Create, Read, Update, Delete) operations for the
|
102
|
+
browser's `window.localStorage`.
|
103
|
+
|
104
|
+
Let's look at its source code
|
105
|
+
([src/main/addon/LocalStorage.mjs](https://github.com/neomjs/neo/blob/dev/src/main/addon/LocalStorage.mjs)):
|
106
|
+
|
107
|
+
```javascript readonly
|
108
|
+
import Base from './Base.mjs';
|
109
|
+
|
110
|
+
/**
|
111
|
+
* Basic CRUD support for window.localStorage
|
112
|
+
* @class Neo.main.addon.LocalStorage
|
113
|
+
* @extends Neo.main.addon.Base
|
114
|
+
*/
|
115
|
+
class LocalStorage extends Base {
|
116
|
+
static config = {
|
117
|
+
className: 'Neo.main.addon.LocalStorage',
|
118
|
+
remote: {
|
119
|
+
app: [
|
120
|
+
'createLocalStorageItem',
|
121
|
+
'destroyLocalStorageItem',
|
122
|
+
'readLocalStorageItem',
|
123
|
+
'updateLocalStorageItem'
|
124
|
+
]
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
readLocalStorageItem(opts) {
|
129
|
+
return {
|
130
|
+
key : opts.key,
|
131
|
+
value: window.localStorage.getItem(opts.key)
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
updateLocalStorageItem(opts) {
|
136
|
+
window.localStorage.setItem(opts.key, opts.value)
|
137
|
+
}
|
138
|
+
// ... other methods
|
139
|
+
}
|
140
|
+
|
141
|
+
export default Neo.setupClass(LocalStorage);
|
142
|
+
```
|
143
|
+
|
144
|
+
### The `Cookie` Addon
|
145
|
+
|
146
|
+
The framework provides another great example of an addon for interacting with a browser API: the
|
147
|
+
`Cookie` addon. It provides methods to read and write to `document.cookie`.
|
148
|
+
|
149
|
+
Let's analyze its source code
|
150
|
+
([src/main/addon/Cookie.mjs](https://github.com/neomjs/neo/blob/dev/src/main/addon/Cookie.mjs)):
|
151
|
+
|
152
|
+
```javascript readonly
|
153
|
+
import Base from './Base.mjs';
|
154
|
+
|
155
|
+
class Cookie extends Base {
|
156
|
+
static config = {
|
157
|
+
className: 'Neo.main.addon.Cookie',
|
158
|
+
remote: {
|
159
|
+
app: [
|
160
|
+
'getCookie',
|
161
|
+
'getCookies',
|
162
|
+
'setCookie'
|
163
|
+
]
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
getCookie(name) {
|
168
|
+
let {cookie} = document
|
169
|
+
.split('; ')
|
170
|
+
.find(row => row.startsWith(name));
|
171
|
+
|
172
|
+
return cookie ? cookie.split('=')[1] : null
|
173
|
+
}
|
174
|
+
|
175
|
+
setCookie(value) {
|
176
|
+
document.cookie = value
|
177
|
+
}
|
178
|
+
// ...
|
179
|
+
}
|
180
|
+
|
181
|
+
export default Neo.setupClass(Cookie);
|
182
|
+
```
|
183
|
+
|
184
|
+
Both addons follow the same pattern:
|
185
|
+
1. They extend `Neo.main.addon.Base`.
|
186
|
+
2. They define their `remote` config to expose methods to the `app` worker.
|
187
|
+
3. Their methods directly interact with browser-specific APIs (`window.localStorage`,
|
188
|
+
`document.cookie`) that are only available on the main thread.
|
189
|
+
|
190
|
+
## Managing Addons: The Full Lifecycle
|
191
|
+
|
192
|
+
There are two primary ways to bring an addon to life within your application, each serving
|
193
|
+
different needs.
|
194
|
+
|
195
|
+
### A. Eager Loading: The Standard Approach
|
196
|
+
|
197
|
+
For addons that are essential for your application's initial operation (e.g., `LocalStorage`,
|
198
|
+
`Stylesheet`), they are typically instantiated at application startup.
|
199
|
+
|
200
|
+
1. **Configuration:** You can specify addons in your application's `neo-config.json` file.
|
201
|
+
2. **Instantiation:** `src/Main.mjs` (the main thread's entry point) is responsible for
|
202
|
+
instantiating these configured addons when the application starts. This ensures they are ready
|
203
|
+
for use as soon as possible.
|
204
|
+
|
205
|
+
### B. Lazy Loading: The Performance-Oriented Approach
|
206
|
+
|
207
|
+
For addons that are not needed immediately at startup, or for features that are only used
|
208
|
+
conditionally, you can lazy-load them on demand. This improves initial application load performance.
|
209
|
+
|
210
|
+
You can use `Neo.worker.App.getAddon()` to dynamically load and instantiate an addon:
|
211
|
+
|
212
|
+
```javascript readonly
|
213
|
+
// Example: Only load a complex charting addon when a user clicks a button
|
214
|
+
async function showChart() {
|
215
|
+
// getAddon will ensure the addon is instantiated and ready
|
216
|
+
const chartingAddon = await Neo.worker.App.getAddon('Neo.main.addon.ChartingLibrary');
|
217
|
+
chartingAddon.createChart({ /* ... config ... */ });
|
218
|
+
}
|
219
|
+
```
|
220
|
+
|
221
|
+
### C. The "Semi-Singleton" Design: Why Addons are Extensible
|
222
|
+
|
223
|
+
Addons *behave* like singletons within the main thread (meaning there's typically only one instance
|
224
|
+
of a given addon class). However, they are deliberately *not defined* with `singleton: true` in
|
225
|
+
their `static config`. This is a crucial architectural decision that enables powerful extensibility:
|
226
|
+
|
227
|
+
* **Customization:** Developers can extend a framework addon (e.g., `class MyLocalStorage extends
|
228
|
+
Neo.main.addon.LocalStorage`), override its methods, and then configure their `neo-config.json`
|
229
|
+
to load *their* custom version.
|
230
|
+
* **Flexibility:** If the base class were a true singleton, this kind of runtime extension and
|
231
|
+
override would be impossible. By making them "semi-singletons" that `Main.mjs` or
|
232
|
+
`Neo.worker.App.getAddon()` manages as single instances, the framework provides both the
|
233
|
+
convenience of a singleton and the power of class-based extension.
|
234
|
+
|
235
|
+
### Example: Customizing `LocalStorage`
|
236
|
+
|
237
|
+
Let's say you want to add a custom prefix to all keys stored in `localStorage` for your application.
|
238
|
+
You can extend the `Neo.main.addon.LocalStorage` and override its `readLocalStorageItem` and
|
239
|
+
`updateLocalStorageItem` methods.
|
240
|
+
|
241
|
+
First, create your custom addon (e.g., `workspace/src/addon/CustomLocalStorage.mjs`):
|
242
|
+
|
243
|
+
```javascript readonly
|
244
|
+
// workspace/src/addon/CustomLocalStorage.mjs
|
245
|
+
import LocalStorage from '../../../node_modules/neo.mjs/src/main/addon/LocalStorage.mjs';
|
246
|
+
|
247
|
+
class CustomLocalStorage extends LocalStorage {
|
248
|
+
static config = {
|
249
|
+
className: 'MyApp.main.addon.CustomLocalStorage',
|
250
|
+
// This is optional, Neo.Main will always convert main thread addons into singletons.
|
251
|
+
// If you want to keep your class open to further extensions, you can use the "semi-singleton" pattern too.
|
252
|
+
singleton: true,
|
253
|
+
// No need to redefine remote config, it's inherited
|
254
|
+
}
|
255
|
+
|
256
|
+
readLocalStorageItem(opts) {
|
257
|
+
opts.key = 'myApp_' + opts.key; // Add your custom prefix
|
258
|
+
return super.readLocalStorageItem(opts);
|
259
|
+
}
|
260
|
+
|
261
|
+
updateLocalStorageItem(opts) {
|
262
|
+
opts.key = 'myApp_' + opts.key; // Add your custom prefix
|
263
|
+
super.updateLocalStorageItem(opts);
|
264
|
+
}
|
265
|
+
}
|
266
|
+
|
267
|
+
export default Neo.setupClass(CustomLocalStorage);
|
268
|
+
```
|
269
|
+
|
270
|
+
Next, configure your `neo-config.json` to use your custom addon instead of the framework's default.
|
271
|
+
This is done by mapping your custom class to the framework's original class name using the `WS/` prefix.
|
272
|
+
The `WS/` prefix (which stands for "workspace") tells the framework to look for your addon within the `src/main/addon`
|
273
|
+
directory of your workspace (the output of `npx neo-app`).
|
274
|
+
|
275
|
+
[Side Note]: If you add a new addon to the framework repo, the `WS/` prefix is not needed.
|
276
|
+
|
277
|
+
```json
|
278
|
+
// neo-config.json
|
279
|
+
{
|
280
|
+
"mainThreadAddons": [
|
281
|
+
// ...
|
282
|
+
"WS/CustomLocalStorage"
|
283
|
+
]
|
284
|
+
}
|
285
|
+
```
|
286
|
+
|
287
|
+
Now, any call to `Neo.main.addon.CustomLocalStorage.readLocalStorageItem()` or `updateLocalStorageItem()`
|
288
|
+
from your app worker will actually be routed to your `CustomLocalStorage` instance on the main thread,
|
289
|
+
automatically applying your custom key prefix. This demonstrates how easily you can swap out or extend
|
290
|
+
framework-provided functionality with your own custom implementations.
|
291
|
+
|
292
|
+
## Asynchronous Initialization: `initAsync` and the `isReady` config
|
293
|
+
|
294
|
+
The multi-threaded nature of Neo.mjs introduces a subtle but important challenge: ensuring that an
|
295
|
+
addon is fully initialized and its environment is ready before it's used. This is solved by the
|
296
|
+
`initAsync` lifecycle method and the `isReady` config, managed by `Neo.core.Base`.
|
297
|
+
|
298
|
+
### The Problem: A Race Condition
|
299
|
+
|
300
|
+
Consider a scenario where a Main thread addon needs to register itself with another core framework
|
301
|
+
service (like `Neo.worker.Manager` or `Neo.manager.Instance`). These services are also instantiated
|
302
|
+
on the Main thread. If the addon's `initAsync()` (or any logic called from it) tries to interact
|
303
|
+
with such a service *during* that service's synchronous construction phase, it might try to access
|
304
|
+
properties or methods before they are fully initialized, leading to a race condition.
|
305
|
+
|
306
|
+
### The Solution: Microtasks and `isReady`
|
307
|
+
|
308
|
+
`Neo.core.Base` addresses this by scheduling the `initAsync()` method in the JavaScript microtask
|
309
|
+
queue.
|
310
|
+
|
311
|
+
1. **Synchronous Construction Completes:** The entire synchronous `construct()` method of a class
|
312
|
+
(including the worker's `construct()` that sets `Neo.currentWorker`) runs to completion first.
|
313
|
+
2. **`initAsync()` Executes:** Only *after* the current synchronous block finishes, the microtask
|
314
|
+
queue is processed, and `initAsync()` is called on all newly created instances.
|
315
|
+
3. **`isReady` Signal:** Once `initAsync()` (and any `await`ed operations within it, like
|
316
|
+
`loadFiles()`) completes, the addon's `isReady` flag is set to `true`. This is the definitive
|
317
|
+
signal that the addon is fully initialized and safe to interact with.
|
318
|
+
4. **`afterSetIsReady()`:** If needed, you can listen to changes of the `isReady` config value,
|
319
|
+
using the provided hook.
|
320
|
+
|
321
|
+
### The `cacheMethodCall()` Safety Net
|
322
|
+
|
323
|
+
The `Neo.main.addon.Base` class provides a crucial utility, `cacheMethodCall()`, for managing remote
|
324
|
+
method calls that arrive before an addon is fully `isReady`. Thanks to a generic interception
|
325
|
+
mechanism in `Neo.worker.mixin.RemoteMethodAccess`, if a remote call for a method listed in the
|
326
|
+
addon's `interceptRemotes` config arrives while `isReady` is `false`, the call is automatically
|
327
|
+
queued. Once `isReady` becomes `true`, all cached calls are processed in order.
|
328
|
+
|
329
|
+
Here's how `onInterceptRemotes()` in `Neo.main.addon.Base` handles this:
|
330
|
+
|
331
|
+
```javascript readonly
|
332
|
+
onInterceptRemotes(msg) {
|
333
|
+
return this.cacheMethodCall({fn: msg.remoteMethod, data: msg.data})
|
334
|
+
}
|
335
|
+
```
|
336
|
+
|
337
|
+
This significantly simplifies addon development by centralizing the queuing logic.
|
338
|
+
|
339
|
+
## The Component Wrapper Pattern: Putting It All Together
|
340
|
+
|
341
|
+
While you can call addon methods directly (e.g.,
|
342
|
+
`await Neo.main.addon.LocalStorage.readLocalStorageItem()`), the best practice for integrating
|
343
|
+
addons into your application's UI is to create a **component wrapper**.
|
344
|
+
|
345
|
+
A component wrapper is a standard Neo.mjs component (running in the App Worker) that encapsulates
|
346
|
+
the interaction with a main thread addon. It exposes a clean, declarative API to your application,
|
347
|
+
while internally handling the remote method calls to the addon.
|
348
|
+
|
349
|
+
The `Neo.component.wrapper.MonacoEditor` is a perfect real-world example of this pattern.
|
350
|
+
|
351
|
+
### Case Study: `Neo.component.wrapper.MonacoEditor`
|
352
|
+
|
353
|
+
The `MonacoEditor` component allows you to embed the powerful Monaco Editor (the code editor from VS
|
354
|
+
Code) into your Neo.mjs application. The Monaco Editor itself is a large, DOM-heavy library that
|
355
|
+
*must* run on the main thread.
|
356
|
+
|
357
|
+
Here's how the `MonacoEditor` component acts as a wrapper:
|
358
|
+
|
359
|
+
1. **Encapsulation:** The component's `static config` exposes properties like `value`, `language`,
|
360
|
+
`readOnly`, and `editorTheme`. These are the properties a developer interacts with, not the
|
361
|
+
low-level Monaco Editor options.
|
362
|
+
2. **Remote Method Calls:** Internally, the component's `afterSet` methods (e.g., `afterSetValue`,
|
363
|
+
`afterSetLanguage`) don't directly manipulate the editor. Instead, they make remote calls to the
|
364
|
+
`MonacoEditor` addon on the main thread:
|
365
|
+
|
366
|
+
```javascript readonly
|
367
|
+
// Inside Neo.component.wrapper.MonacoEditor.mjs
|
368
|
+
afterSetValue(value, oldValue) {
|
369
|
+
let me = this;
|
370
|
+
if (me.mounted) { // Defensive check, though addon.Base handles queuing
|
371
|
+
Neo.main.addon.MonacoEditor.setValue({
|
372
|
+
id : me.id,
|
373
|
+
value : me.stringifyValue(me.value),
|
374
|
+
windowId: me.windowId
|
375
|
+
})
|
376
|
+
}
|
377
|
+
}
|
378
|
+
```
|
379
|
+
3. **Lifecycle Management:** The wrapper component also manages the addon's lifecycle from the
|
380
|
+
worker's perspective. For example, when the component is `mounted` (meaning its DOM element is
|
381
|
+
in the document), it tells the addon to create the editor instance:
|
382
|
+
|
383
|
+
```javascript readonly
|
384
|
+
// Inside Neo.component.wrapper.MonacoEditor.mjs
|
385
|
+
afterSetMounted(value, oldValue) {
|
386
|
+
super.afterSetMounted(value, oldValue);
|
387
|
+
let me = this;
|
388
|
+
value && me.timeout(150).then(() => {
|
389
|
+
// This call will trigger the addon to create the Monaco Editor instance on the main thread
|
390
|
+
Neo.main.addon.MonacoEditor.createInstance(me.getInitialOptions()).then(() => {
|
391
|
+
me.onEditorMounted?.()
|
392
|
+
})
|
393
|
+
})
|
394
|
+
}
|
395
|
+
```
|
396
|
+
4. **Cleanup:** When the component is destroyed, it tells the addon to destroy the corresponding
|
397
|
+
editor instance on the main thread, preventing memory leaks.
|
398
|
+
|
399
|
+
This pattern ensures that your application code remains clean, declarative, and runs entirely within
|
400
|
+
the App Worker, while the complexities of main thread interaction are neatly encapsulated within the
|
401
|
+
component wrapper and its associated addon.
|
402
|
+
|
403
|
+
## Advanced: Lazy Loading External Libraries with `loadFiles()`
|
404
|
+
|
405
|
+
For addons that depend on large, external JavaScript libraries (like a charting or mapping library),
|
406
|
+
you don't want to load that library until it's actually needed. The `Neo.main.addon.Base` class
|
407
|
+
provides a powerful mechanism for this: the `async loadFiles()` method.
|
408
|
+
|
409
|
+
1. **Implement `loadFiles()`:** Place your library loading logic (e.g., dynamically injecting a
|
410
|
+
`<script>` tag) inside the `async loadFiles()` method in your addon. This method **must** return
|
411
|
+
a `Promise` that resolves when the library is fully loaded and ready.
|
412
|
+
2. **Automatic Queuing via `interceptRemotes`:** For methods listed in an addon's `interceptRemotes`
|
413
|
+
config, the framework automatically handles queuing any remote method calls that arrive before
|
414
|
+
the addon's `isReady` property is `true`.
|
415
|
+
|
416
|
+
Here's a conceptual example:
|
417
|
+
|
418
|
+
```javascript readonly
|
419
|
+
// Inside a hypothetical src/main/addon/ChartingLibrary.mjs
|
420
|
+
import Base from './Base.mjs';
|
421
|
+
|
422
|
+
class ChartingLibrary extends Base {
|
423
|
+
static config = {
|
424
|
+
className : 'Neo.main.addon.ChartingLibrary',
|
425
|
+
interceptRemotes: ['createChart'], // List methods to be automatically queued
|
426
|
+
remote : { app: ['createChart'] } // Exposes `createChart` as a remote method to the app worker
|
427
|
+
}
|
428
|
+
|
429
|
+
async loadFiles() {
|
430
|
+
// Dynamically load the external charting library script
|
431
|
+
await Neo.main.DomAccess.loadScript({
|
432
|
+
id : 'charting-lib-script',
|
433
|
+
src: 'https://example.com/charting-library.js'
|
434
|
+
});
|
435
|
+
// You might also need to wait for the library to initialize itself
|
436
|
+
// await new Promise(resolve => window.ExternalChartingLibrary.onReady(resolve));
|
437
|
+
}
|
438
|
+
|
439
|
+
createChart(opts) {
|
440
|
+
// This code will only run after the script has loaded
|
441
|
+
// and the addon is ready. The framework handles queuing automatically.
|
442
|
+
return window.ExternalChartingLibrary.create(opts.domId, opts.chartConfig);
|
443
|
+
}
|
444
|
+
}
|
445
|
+
```
|
446
|
+
|
447
|
+
When a worker calls `Neo.main.addon.ChartingLibrary.createChart()` for the first time:
|
448
|
+
1. The framework intercepts the call because `createChart` is in `interceptRemotes`.
|
449
|
+
2. If the addon is not `isReady`, the call is automatically queued.
|
450
|
+
3. `loadFiles()` is triggered (if not already running).
|
451
|
+
4. Once `loadFiles()` resolves and the addon becomes `isReady`, the queued `createChart` call is
|
452
|
+
executed.
|
453
|
+
5. The `Promise` back in the worker is resolved.
|
454
|
+
|
455
|
+
All subsequent calls will execute immediately, as the library will already be loaded. This powerful
|
456
|
+
feature ensures optimal performance by deferring the loading of heavy resources until they are
|
457
|
+
absolutely necessary.
|
458
|
+
|
459
|
+
## Conclusion: Empowering Your Application with Main Thread Addons
|
460
|
+
|
461
|
+
Main Thread Addons are a cornerstone of Neo.mjs's multi-threaded architecture, providing a robust and
|
462
|
+
elegant solution for interacting with browser-specific APIs and third-party libraries. By offloading
|
463
|
+
these tasks to the main thread while keeping your core application logic in workers, Neo.mjs ensures
|
464
|
+
unparalleled responsiveness and performance.
|
465
|
+
|
466
|
+
This guide has explored the full lifecycle of addons, from their "semi-singleton" design that promotes
|
467
|
+
extensibility, to the sophisticated `initAsync` and `isReady` mechanisms that guarantee safe,
|
468
|
+
asynchronous initialization. You've seen how the framework seamlessly handles remote method calls,
|
469
|
+
queuing them when necessary, and how the component wrapper pattern provides a clean, declarative
|
470
|
+
interface for your application.
|
471
|
+
|
472
|
+
By leveraging Main Thread Addons, you can confidently integrate any browser-dependent functionality
|
473
|
+
into your Neo.mjs application, knowing that the framework is handling the complex inter-thread
|
474
|
+
communication and lifecycle management for you. This powerful pattern is key to building
|
475
|
+
high-performance, extensible, and truly modern web applications.
|
@@ -27,9 +27,9 @@ The setting will get stored inside the LocalStorage,
|
|
27
27
|
so you will need to click the following Button a 2nd time to deactivate it again.
|
28
28
|
Or you can clear the LocalStorage manually.</br></br>
|
29
29
|
|
30
|
-
|
30
|
+
```json neo-component
|
31
31
|
{
|
32
32
|
"className": "Portal.view.learn.CubeLayoutButton",
|
33
33
|
"style" : {"margin": 0}
|
34
34
|
}
|
35
|
-
|
35
|
+
```
|
@@ -10,7 +10,7 @@ Other libraries or frameworks often call state providers "Stores".
|
|
10
10
|
|
11
11
|
## Inline State Providers
|
12
12
|
### Direct Bindings
|
13
|
-
|
13
|
+
```javascript live-preview
|
14
14
|
import Button from '../button/Base.mjs';
|
15
15
|
import Container from '../container/Base.mjs';
|
16
16
|
import Label from '../component/Label.mjs';
|
@@ -49,7 +49,7 @@ class MainView extends Container {
|
|
49
49
|
}
|
50
50
|
}
|
51
51
|
MainView = Neo.setupClass(MainView);
|
52
|
-
|
52
|
+
```
|
53
53
|
|
54
54
|
We use a Container with a stateProvider containing the data props `hello` and `world`.
|
55
55
|
Inside the Container are 2 Labels which bind their `text` config to a data prop directly.
|
@@ -58,7 +58,7 @@ We can easily bind 1:1 to specific data props using the following syntax:</br>
|
|
58
58
|
`bind: {text: data => data.hello}`
|
59
59
|
|
60
60
|
### Bindings with multiple data props
|
61
|
-
|
61
|
+
```javascript live-preview
|
62
62
|
import Button from '../button/Base.mjs';
|
63
63
|
import Container from '../container/Base.mjs';
|
64
64
|
import Label from '../component/Label.mjs';
|
@@ -104,7 +104,7 @@ class MainView extends Container {
|
|
104
104
|
}
|
105
105
|
}
|
106
106
|
MainView = Neo.setupClass(MainView);
|
107
|
-
|
107
|
+
```
|
108
108
|
|
109
109
|
We use a Container with a stateProvider containing the data props `hello` and `world`.
|
110
110
|
Inside the Container are 3 Labels which bind their `text` config to a combination of both data props.
|
@@ -128,7 +128,7 @@ data.component equals to the Button instance itself. Since the Button instance d
|
|
128
128
|
`getStateProvider()` will return the closest stateProvider inside the parent chain.
|
129
129
|
|
130
130
|
### Nested Inline State Providers
|
131
|
-
|
131
|
+
```javascript live-preview
|
132
132
|
import Button from '../button/Base.mjs';
|
133
133
|
import Container from '../container/Base.mjs';
|
134
134
|
import Label from '../component/Label.mjs';
|
@@ -182,7 +182,7 @@ class MainView extends Container {
|
|
182
182
|
}
|
183
183
|
}
|
184
184
|
MainView = Neo.setupClass(MainView);
|
185
|
-
|
185
|
+
```
|
186
186
|
|
187
187
|
The output of this demo is supposed to exactly look the same like the previous demo.
|
188
188
|
|
@@ -203,7 +203,7 @@ We can even change data props which live inside different stateProviders at once
|
|
203
203
|
Hint: Modify the example code (Button handler) to try it out right away!
|
204
204
|
|
205
205
|
### Nested Data Properties
|
206
|
-
|
206
|
+
```javascript live-preview
|
207
207
|
import Button from '../button/Base.mjs';
|
208
208
|
import Container from '../container/Base.mjs';
|
209
209
|
import Label from '../component/Label.mjs';
|
@@ -244,7 +244,7 @@ class MainView extends Container {
|
|
244
244
|
}
|
245
245
|
}
|
246
246
|
MainView = Neo.setupClass(MainView);
|
247
|
-
|
247
|
+
```
|
248
248
|
Data props inside VMs can be nested. Our stateProvider contains a `user` data prop as an object,
|
249
249
|
which contains the nested props `firstname` and `lastname`.
|
250
250
|
|
@@ -262,7 +262,7 @@ Or we can directly pass the object containing the change(s):</br>
|
|
262
262
|
Hint: This will not override left out nested data props (lastname in this case).
|
263
263
|
|
264
264
|
### Dialog connecting to a Container
|
265
|
-
|
265
|
+
```javascript live-preview
|
266
266
|
import Controller from '../controller/Component.mjs';
|
267
267
|
import Dialog from '../dialog/Base.mjs';
|
268
268
|
import Panel from '../container/Panel.mjs';
|
@@ -378,13 +378,13 @@ class MainView extends Viewport {
|
|
378
378
|
}
|
379
379
|
|
380
380
|
MainView = Neo.setupClass(MainView);
|
381
|
-
|
381
|
+
```
|
382
382
|
|
383
383
|
## Class based State Providers
|
384
384
|
When your stateProviders contain many data props or need custom logic, you can easily move them into their own classes.
|
385
385
|
|
386
386
|
### Direct Bindings
|
387
|
-
|
387
|
+
```javascript live-preview
|
388
388
|
import Button from '../button/Base.mjs';
|
389
389
|
import Container from '../container/Base.mjs';
|
390
390
|
import Label from '../component/Label.mjs';
|
@@ -436,4 +436,4 @@ class MainView extends Container {
|
|
436
436
|
}
|
437
437
|
}
|
438
438
|
MainView = Neo.setupClass(MainView);
|
439
|
-
|
439
|
+
```
|