neo.mjs 6.10.11 → 6.10.13
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/apps/ServiceWorker.mjs +2 -2
- package/apps/portal/view/learn/LivePreview.mjs +1 -0
- package/buildScripts/createAppMinimal.mjs +0 -30
- package/examples/ServiceWorker.mjs +2 -2
- package/examples/toolbar/paging/view/MainContainer.mjs +6 -1
- package/package.json +5 -6
- package/resources/data/deck/learnneo/p/2023-10-14T19-25-08-153Z.md +0 -1
- package/resources/data/deck/learnneo/p/ComponentModels.md +26 -14
- package/resources/data/deck/learnneo/p/Earthquakes.md +1226 -31
- package/resources/data/deck/learnneo/p/Events.md +11 -0
- package/resources/data/deck/learnneo/p/Extending.md +1 -0
- package/resources/data/deck/learnneo/p/GuideEvents.md +153 -0
- package/resources/data/deck/learnneo/t.json +3 -1
- package/resources/scss/src/apps/portal/learn/ContentView.scss +0 -1
- package/resources/scss/src/list/Base.scss +8 -0
- package/src/DefaultConfig.mjs +2 -2
- package/src/form/field/Date.mjs +22 -1
- package/src/main/DomUtils.mjs +1 -1
- package/src/main/addon/Navigator.mjs +6 -2
- package/src/tooltip/Base.mjs +6 -2
- package/src/util/String.mjs +5 -4
@@ -14,13 +14,38 @@ You'll do this in a series of labs:
|
|
14
14
|
1. Listen to events
|
15
15
|
1. Make the app multi-window
|
16
16
|
|
17
|
-
##
|
17
|
+
## Goals
|
18
18
|
|
19
|
-
|
20
|
-
tutorial, and others, We'll have plenty of time to get into the code, and we'll do
|
21
|
-
most things multiple times.
|
19
|
+
What's the goal of this lengthy topic?
|
22
20
|
|
23
|
-
|
21
|
+
- To give you hands-on coding a simple app
|
22
|
+
- To introduce fundamental Neo concepts
|
23
|
+
- To do some coding without emphasizing syntax details
|
24
|
+
|
25
|
+
Most of these labs are copy-and-paste because we're focusing on _what_ it's doing on rather than _how_.
|
26
|
+
|
27
|
+
As we progress through the training we'll spend more and more time on syntax and how, and you'll
|
28
|
+
become more and more proficient at writing your own code.
|
29
|
+
|
30
|
+
## Key concepts
|
31
|
+
|
32
|
+
A few key concepts we'll be discussing:
|
33
|
+
|
34
|
+
- Creating a starter app
|
35
|
+
- Configuring components
|
36
|
+
- Debugging
|
37
|
+
- Class-based coding
|
38
|
+
- View models
|
39
|
+
- Events
|
40
|
+
- Controllers
|
41
|
+
|
42
|
+
## Advice
|
43
|
+
|
44
|
+
A word of advice: Keep a high-level perspective, especially early on. We'll have plenty of time to get
|
45
|
+
into the code, and we'll do most things multiple times. In other words, focus on what you're accomplishing,
|
46
|
+
and don't worry about syntax details.
|
47
|
+
|
48
|
+
## Lab. Generate a workspace
|
24
49
|
|
25
50
|
In this lab, you'll generate a Neo.mjs workspace and run the starter app.
|
26
51
|
|
@@ -123,8 +148,7 @@ In this lab you'll create a starter app and add a single component.
|
|
123
148
|
<summary>Use the command-line to create a starter app</summary>
|
124
149
|
|
125
150
|
Use a terminal window to navigate to the workspace and run the following script. Use "Earthquakes"
|
126
|
-
as the app name, and
|
127
|
-
keys and the space bar to toggle add-on options). Use defaults for everything else.
|
151
|
+
as the app name, and defaults for everything else.
|
128
152
|
|
129
153
|
npm run generate-app-minimal
|
130
154
|
|
@@ -136,45 +160,194 @@ After the script runs yous should see these files in the `app/earthquakes` direc
|
|
136
160
|
- `neo-config.json`
|
137
161
|
|
138
162
|
If you look in `neo-config.json` you should see this content. Note the `mainThreadAddons` block
|
139
|
-
— it
|
163
|
+
— it reflects the add-ons you chose when you followed the instructions in the script.
|
140
164
|
<pre>
|
141
165
|
{
|
142
|
-
"appPath": "../../apps/
|
166
|
+
"appPath": "../../apps/earthquakes/app.mjs",
|
143
167
|
"basePath": "../../",
|
144
168
|
"environment": "development",
|
145
169
|
"mainPath": "../node_modules/neo.mjs/src/Main.mjs",
|
146
|
-
"
|
147
|
-
|
148
|
-
"
|
149
|
-
]
|
150
|
-
|
151
|
-
</pre>
|
170
|
+
"mainThreadAddons": [
|
171
|
+
"DragDrop",
|
172
|
+
"Stylesheet"
|
173
|
+
],
|
174
|
+
"workerBasePath": "../../node_modules/neo.mjs/src/worker/"
|
175
|
+
}</pre>
|
176
|
+
|
177
|
+
You're free to edit `neo-config.json` if you were to change your mind later about the theme or need for other add-ons.
|
178
|
+
|
179
|
+
If you refresh browser at <a href="http://localhost:8080/apps/" target="apps">http://localhost:8080/apps/</a>
|
180
|
+
you'll see the new _earthquakes_ app listed, and if you run it you'll see... nothing! That's because the
|
181
|
+
minimal starter app is the shell of an application without any view content. We'll add a little content
|
182
|
+
later in the lab.
|
183
|
+
|
184
|
+
<img style="width:80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/EmptyEarthquakes.png"></img>
|
152
185
|
|
153
|
-
When the script finishes you should see
|
154
186
|
|
155
|
-
You'll be propted for a workspace name, starter app name, etc — accept the default for everything.
|
156
|
-
As the command finishes it starts a server and opens a browser window.
|
157
187
|
</details>
|
158
188
|
|
159
189
|
<details>
|
160
190
|
<summary>Look at the main view source</summary>
|
161
191
|
|
192
|
+
Use a code editor and look at `workspace/apps/earthquakes/src/view/MainView.mjs`. You'll see the
|
193
|
+
following class definition:
|
194
|
+
|
195
|
+
<pre data-javascript>
|
196
|
+
|
197
|
+
import Base from '../../../node_modules/neo.mjs/src/container/Base.mjs';
|
198
|
+
import Controller from './MainViewController.mjs';
|
199
|
+
import ViewModel from './MainViewModel.mjs';
|
200
|
+
|
201
|
+
class MainView extends Base {
|
202
|
+
static config = {
|
203
|
+
className: 'Earthquakes.view.MainView',
|
204
|
+
ntype: 'earthquakes-main',
|
205
|
+
|
206
|
+
controller: {module: Controller},
|
207
|
+
model: {module: ViewModel},
|
208
|
+
|
209
|
+
layout: {ntype: 'fit'},
|
210
|
+
items: [],
|
211
|
+
}
|
212
|
+
}
|
213
|
+
|
214
|
+
Neo.applyClassConfig(MainView);
|
215
|
+
|
216
|
+
export default MainView;
|
217
|
+
</pre>
|
218
|
+
|
219
|
+
As you can see, `MainView extends Base`, and `Base` is a _container_ (`Neo.container.Base`).
|
220
|
+
A container is a component — it holds other components, specified in `items:[]`. There
|
221
|
+
are no items in the starter app. The `layout` config specifies how the items are arranged.
|
222
|
+
|
162
223
|
</details>
|
163
224
|
|
164
225
|
<details>
|
165
226
|
<summary>Add a component</summary>
|
166
227
|
|
228
|
+
Let's add a button. To do that, add an import for the button base class, then configure it
|
229
|
+
in the container's `items:[]`. If you were to read the API docs for buttons, you'd see
|
230
|
+
that buttons have various configs, such as `text`, which is the button text, `iconCls`, which
|
231
|
+
is typically a FontAwesome CSS class used to show an icon, and `handler`, which specified
|
232
|
+
which method to run when the button is clicked. We'll use `text`.
|
233
|
+
|
234
|
+
<pre data-javascript>
|
235
|
+
|
236
|
+
import Base from '../../../node_modules/neo.mjs/src/container/Base.mjs';
|
237
|
+
import Button from '../../../node_modules/neo.mjs/src/button/Base.mjs';
|
238
|
+
import Controller from './MainViewController.mjs';
|
239
|
+
import ViewModel from './MainViewModel.mjs';
|
240
|
+
|
241
|
+
class MainView extends Base {
|
242
|
+
static config = {
|
243
|
+
className: 'Earthquakes.view.MainView',
|
244
|
+
ntype: 'earthquakes-main',
|
245
|
+
|
246
|
+
controller: {module: Controller},
|
247
|
+
model: {module: ViewModel},
|
248
|
+
|
249
|
+
layout: {ntype: 'fit'},
|
250
|
+
items: [{
|
251
|
+
module: Button,
|
252
|
+
text: 'Button!'
|
253
|
+
}],
|
254
|
+
}
|
255
|
+
}
|
256
|
+
|
257
|
+
Neo.applyClassConfig(MainView);
|
258
|
+
|
259
|
+
export default MainView;
|
260
|
+
</pre>
|
261
|
+
|
262
|
+
|
263
|
+
When you run the app you'll see the single button.
|
264
|
+
|
265
|
+
<img src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/EarthquakesSingleFitButton.png" style="width:80%"/>
|
266
|
+
|
267
|
+
The button takes up the full width. Buttons look different depending on the theme. We're using
|
268
|
+
the _neo-theme-neo-light_ theme, which controls button height. Otherwise, child items a using the _fit_ layout
|
269
|
+
take up the full window.
|
167
270
|
</details>
|
168
271
|
|
169
272
|
|
170
273
|
<details>
|
171
|
-
<summary>
|
274
|
+
<summary>Modify the layout</summary>
|
275
|
+
The `layout` configures how child items are visually arranged. First, note that the config
|
276
|
+
specifies `ntype`. We used `module` for the button config. An `ntype` is a class alias — if a class
|
277
|
+
has already been imported, you can use the `ntype` rather than importing it again and using the `module`
|
278
|
+
config. We haven't imported any layouts, but it turns out that `Neo.container.Base` _does_ import all the
|
279
|
+
layout types, which means we're always free to use `ntype` with layouts. You're free to specify an `ntype`
|
280
|
+
for the classes you define.
|
281
|
+
|
282
|
+
Let's change the layout to arrange items vertically, with items aligned horizontally at the start.
|
283
|
+
|
284
|
+
<pre data-javascript>
|
285
|
+
layout: {
|
286
|
+
ntype: 'vbox',
|
287
|
+
align: 'start'
|
288
|
+
}
|
289
|
+
</pre>
|
290
|
+
|
291
|
+
<img src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/EarthquakesSingleVoxStartButton.png" style="width:80%"/>
|
172
292
|
|
173
293
|
</details>
|
174
294
|
|
175
295
|
<!-- /lab -->
|
176
296
|
|
177
|
-
##
|
297
|
+
## Debugging
|
298
|
+
|
299
|
+
At startup a Neo.mjs application launches three Web Workers:
|
300
|
+
|
301
|
+
- neomjs-app-worker
|
302
|
+
- neomjs-data-worker
|
303
|
+
- neomjs-vdom-worker
|
304
|
+
|
305
|
+
As a developer, your code is run in _neomjs-app-worker_. When you're debugging,
|
306
|
+
choose that worker in the DevTools JavaScript context dropdown.
|
307
|
+
|
308
|
+
<img width="80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/DevToolsJavaScriptContext.png">
|
309
|
+
|
310
|
+
A basic debugging (and coding!) task is getting a referernce to a component.
|
311
|
+
You can then see and update the component's state and run its methods.
|
312
|
+
|
313
|
+
There are a few ways to get a component reference.
|
314
|
+
- `Neo.manager.Component.items` <tt>// Returns [Component]</tt>
|
315
|
+
- `Neo.find({property:'value'})` <tt>// Returns [{}] of instances<t/t>
|
316
|
+
- `Neo.findFirst({property:'value'})` <tt>// Returns first instance</tt>
|
317
|
+
- Doing a Shift-Ctrl-right-click on a component
|
318
|
+
|
319
|
+
Keep in mind that `Neo.manager.Component.items`, `Neo.find()` and `Neo.findFirst()`
|
320
|
+
are debugging aids _only_, and _should never be used in app logic_.
|
321
|
+
|
322
|
+
Why? There's nothing stopping you from using then, and they would work fine,
|
323
|
+
but those methods completely break encapsulation and scoping principles! Their
|
324
|
+
use would make an application brittle and hard to maintain.
|
325
|
+
|
326
|
+
Once we have a reference in the debugger console you can inspect and update its
|
327
|
+
properties, or run its methods. For example, if we have devtools open in the
|
328
|
+
`earthquakes` app, then run `Neo.findFirst({ntype:'button'})` from the _neomjs-app-worker_
|
329
|
+
context, we can inspect the button.
|
330
|
+
|
331
|
+
<img width="80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/EarthquakesFindFirstButton.png">
|
332
|
+
|
333
|
+
Once we find the component, we can expand it and scroll down until we see the grayed-out properties —
|
334
|
+
those are setter/getter properties.
|
335
|
+
|
336
|
+
We can choose whatever property we're interested in, and click on the ellipses. That runs the getter, and if
|
337
|
+
we change the value we'll be running the setter. An obvious button property to change is `text`.
|
338
|
+
Editing that value is immediately reflected in the view.
|
339
|
+
|
340
|
+
<img width="80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/EarthquakesFindFirstButtonChangeText.png">
|
341
|
+
|
342
|
+
Or, we can change the property directly via `Neo.findFirst({ntype:'button'}).text = "Hi there!`.
|
343
|
+
|
344
|
+
There's an even more convenient way to get a component reference: Doing a Shift-Ctrl-right-click on a component
|
345
|
+
will show the container hierarchy for the selected component.
|
346
|
+
|
347
|
+
<img width="80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/EarthquakesDemoShiftCtrl.png">
|
348
|
+
|
349
|
+
At this point the app is so simple there's not much to see, but in a more complex app you can see the hierarchy
|
350
|
+
and inspect or update component.
|
178
351
|
|
179
352
|
##Lab. Debugging
|
180
353
|
|
@@ -184,31 +357,1053 @@ and runing methods.
|
|
184
357
|
<!-- lab -->
|
185
358
|
|
186
359
|
<details>
|
187
|
-
<summary>
|
360
|
+
<summary>Use `Neo.manager.Component.items`</summary>
|
361
|
+
|
362
|
+
While running the _earthquakes_ app, open Chrome devtools, choose the _neomjs-app-worker_ JavaScript
|
363
|
+
context, and run this statement:
|
364
|
+
|
365
|
+
Neo.manager.Component.items
|
366
|
+
|
367
|
+
The `items` property is an array of all created components. The array may have a lot of entries, depending on
|
368
|
+
the complexity of an app and how much you've done while running it. But it's an easy way to explore what's
|
369
|
+
been created.
|
188
370
|
|
189
|
-
The workspace contains a local copy of the API docs, an `apps` directory (where your apps are found),
|
190
|
-
and some other directories.
|
191
371
|
</details>
|
192
372
|
|
193
373
|
<details>
|
194
|
-
<summary>
|
195
|
-
From the root of the `workspace` start the server via `npm run server-start`. That starts a server
|
196
|
-
at port 8080 and opens a new browser window.
|
374
|
+
<summary>Store as global variable</summary>
|
197
375
|
|
198
|
-
|
376
|
+
Any time you have a object reference in the console — even if it's nested within an array or object —
|
377
|
+
you can right click on the object and choose _Store as global_ variable. Chrome will create a variable named
|
378
|
+
`temp1`, `temp2`, etc., that references the object. That can make it easier to inspect the object and run its methods..
|
379
|
+
|
380
|
+
|
381
|
+
<img src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/StoreAsGlobal.png" style="width:80%"/>
|
199
382
|
|
200
383
|
</details>
|
201
384
|
|
202
385
|
|
203
386
|
<details>
|
204
|
-
<summary>
|
387
|
+
<summary>Use `Neo.find()` and `Neo.findFirst()`</summary>
|
205
388
|
|
206
|
-
|
207
|
-
|
389
|
+
If you know what you're looking for, and don't want to bother inspecting everything in `Neo.manager.Component.items`,
|
390
|
+
you can use `Neo.find()`, passing an object used to match against what you're searching for.
|
208
391
|
|
209
|
-
|
392
|
+
`Neo.find()` returns an array of matching instances, and `Neo.findFirst()` returns the first matching item.
|
393
|
+
Often you know there's only a single instance, so in practice `Neo.findFirst()` is more commonly used.
|
394
|
+
|
395
|
+
You could find the button via Neo.find({ntype:'button'}) or Neo.find({text:'Button!'} (assuming you haven't changed
|
396
|
+
the button's text.) You can even match a property you give the button. For example, if you configured it with a made-up
|
397
|
+
property `foo:true`, you could find it via `Neo.findFirst({foo:true})`. The point is you can search for any properties
|
398
|
+
you'd like.
|
399
|
+
|
400
|
+
Try this out.
|
401
|
+
|
402
|
+
`Neo.findFirst({text:'Button!'}).text = 'Foo!'
|
403
|
+
|
404
|
+
You should see the button's text change immediately.
|
405
|
+
|
406
|
+
</details>
|
407
|
+
|
408
|
+
|
409
|
+
<details>
|
410
|
+
<summary>Use `Shift-Ctrl-right-click`</summary>
|
411
|
+
|
412
|
+
With your cursor over the button, press _Shift-Ctrl-right-click_. The console will log the button, its parent `MainView`
|
413
|
+
and the subsequent parent `Viewport. The button reference shows up as `Base` because the button class name is `Neo.button.Base`.
|
414
|
+
|
415
|
+
<img width="80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/EarthquakesDemoShiftCtrl.png">
|
416
|
+
|
417
|
+
Note that _Shift-Ctrl-right-click_ is only available during development — it isn't available in a build.
|
418
|
+
|
419
|
+
</details>
|
420
|
+
|
421
|
+
|
422
|
+
<details>
|
423
|
+
<summary>Add a method</summary>
|
424
|
+
|
425
|
+
As we mentioned, when debugging, if you a have a reference you can access or update its properties, or run
|
426
|
+
its methods. Let's try that out by adding a method.
|
427
|
+
|
428
|
+
Edit `apps/earthquakes/view/MainView.mjs` and add a method.
|
429
|
+
|
430
|
+
<pre data-javascript>
|
431
|
+
|
432
|
+
import Base from '../../../node_modules/neo.mjs/src/container/Base.mjs';
|
433
|
+
import Button from '../../../node_modules/neo.mjs/src/button/Base.mjs';
|
434
|
+
import Controller from './MainViewController.mjs';
|
435
|
+
import ViewModel from './MainViewModel.mjs';
|
436
|
+
|
437
|
+
class MainView extends Base {
|
438
|
+
static config = {
|
439
|
+
className: 'Earthquakes.view.MainView',
|
440
|
+
ntype: 'earthquakes-main',
|
441
|
+
|
442
|
+
controller: {module: Controller},
|
443
|
+
model: {module: ViewModel},
|
444
|
+
|
445
|
+
layout: {
|
446
|
+
ntype: 'vbox',
|
447
|
+
align: 'start'
|
448
|
+
},
|
449
|
+
items: [{
|
450
|
+
module: Button,
|
451
|
+
foo: true,
|
452
|
+
text: 'Button!'
|
453
|
+
}],
|
454
|
+
}
|
455
|
+
doFoo (){
|
456
|
+
console.log('foo!');
|
457
|
+
}
|
458
|
+
}
|
459
|
+
|
460
|
+
Neo.applyClassConfig(MainView);
|
461
|
+
|
462
|
+
export default MainView;
|
463
|
+
</pre>
|
464
|
+
|
465
|
+
Save your changes.
|
466
|
+
|
467
|
+
As you can see, the code defined an instance method `doFoo()` that simply logs a message. We'll run the method via debugging techniques in the next step.
|
468
|
+
|
469
|
+
</details>
|
470
|
+
|
471
|
+
<details>
|
472
|
+
<summary>Use `Neo.component.Manager.items` to run the method</summary>
|
473
|
+
|
474
|
+
On the console run `Neo.component.Manager.items`. Expand the array and right-click on the entry for `MainView` and
|
475
|
+
choose `Store object as global variable`. Then type `temp1.doFoo()` — you should see "foo!" being logged.
|
476
|
+
|
477
|
+
Remember that you _must_ run console statement in the _neomjs-app-worker_ context, and every time your choose
|
478
|
+
`Store object as global variable` it'll increment the name of the temp variable: `temp1`, `temp2`, etc.
|
479
|
+
</details>
|
480
|
+
|
481
|
+
<details>
|
482
|
+
<summary>Use _Shift-Ctrl-right-click_ to run the method</summary>
|
483
|
+
|
484
|
+
Now try the _Shift-Ctrl-right-click_ technique.
|
485
|
+
|
486
|
+
With your cursor over the button, do a _Shift-Ctrl-right-click_ — you'll see the component hierarchy logged.
|
487
|
+
As you did in the previous step, right-click on the entry for `MainView` and choose `Store object as global variable`.
|
488
|
+
Then run `doFoo()` using that variable.
|
489
|
+
</details>
|
490
|
+
|
491
|
+
<!-- /lab -->
|
492
|
+
|
493
|
+
At this point we have a application with minimal content. You also know how to do some debugging. Let's do something more interesting.
|
494
|
+
|
495
|
+
##Lab. Fetch earthquakes data and show it in a table
|
496
|
+
|
497
|
+
<!-- lab -->
|
498
|
+
|
499
|
+
<details>
|
500
|
+
<summary>Add a table</summary>
|
501
|
+
|
502
|
+
Replace the button with a table by replacing `MainView.mjs` with the following content.
|
503
|
+
|
504
|
+
<pre data-javascript>
|
505
|
+
|
506
|
+
import Base from '../../../node_modules/neo.mjs/src/container/Base.mjs';
|
507
|
+
import Table from '../../../node_modules/neo.mjs/src/table/Container.mjs';
|
508
|
+
import Store from '../../../node_modules/neo.mjs/src/data/Store.mjs';
|
509
|
+
import Controller from './MainViewController.mjs';
|
510
|
+
import ViewModel from './MainViewModel.mjs';
|
511
|
+
|
512
|
+
class MainView extends Base {
|
513
|
+
static config = {
|
514
|
+
className: 'Earthquakes.view.MainView',
|
515
|
+
ntype: 'earthquakes-main',
|
516
|
+
|
517
|
+
controller: {module: Controller},
|
518
|
+
model: {module: ViewModel},
|
519
|
+
|
520
|
+
layout: {ntype: 'vbox', align: 'stretch'},
|
521
|
+
items: [{
|
522
|
+
module: Table,
|
523
|
+
store: {
|
524
|
+
module: Store,
|
525
|
+
model: {
|
526
|
+
fields: [{
|
527
|
+
name: "humanReadableLocation",
|
528
|
+
}, {
|
529
|
+
name: "size",
|
530
|
+
}, {
|
531
|
+
name: "timestamp",
|
532
|
+
type: "Date",
|
533
|
+
}],
|
534
|
+
},
|
535
|
+
url: "https://apis.is/earthquake/is",
|
536
|
+
responseRoot: "results",
|
537
|
+
autoLoad: true,
|
538
|
+
},
|
539
|
+
style: {width: '100%'},
|
540
|
+
columns: [{
|
541
|
+
dataField: "timestamp",
|
542
|
+
text: "Date",
|
543
|
+
renderer: (data) => data.value.toLocaleDateString(undefined, {weekday: "long", year: "numeric", month: "long", day: "numeric"}),
|
544
|
+
}, {
|
545
|
+
dataField: "humanReadableLocation",
|
546
|
+
text: "Location",
|
547
|
+
}, {
|
548
|
+
dataField: "size",
|
549
|
+
text: "Magnitude",
|
550
|
+
align: "right",
|
551
|
+
renderer: (data) => data.value.toLocaleString(),
|
552
|
+
}],
|
553
|
+
}],
|
554
|
+
}
|
555
|
+
}
|
556
|
+
|
557
|
+
Neo.applyClassConfig(MainView);
|
558
|
+
|
559
|
+
export default MainView;
|
560
|
+
</pre>
|
561
|
+
|
562
|
+
Save and refresh.
|
563
|
+
|
564
|
+
<img width="80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/EarthquakesTable.png">
|
565
|
+
|
566
|
+
|
567
|
+
</details>
|
568
|
+
|
569
|
+
|
570
|
+
<!-- /lab -->
|
571
|
+
|
572
|
+
##Key Features
|
573
|
+
|
574
|
+
The code accomplishes a lot.
|
575
|
+
|
576
|
+
As we discussed before, the app is
|
577
|
+
- Class-based
|
578
|
+
- Declarative
|
579
|
+
|
580
|
+
The app logic
|
581
|
+
- Calls a web service
|
582
|
+
- Populates a store
|
583
|
+
- Shows store data in a table
|
584
|
+
|
585
|
+
Let's review the code and see what it's doing.
|
586
|
+
|
587
|
+
### The store
|
588
|
+
|
589
|
+
A store is a collection of records. A record is described in the `model` and the model's `fields`.
|
590
|
+
Here's the config for the store.
|
591
|
+
|
592
|
+
<pre data-javascript>
|
593
|
+
{
|
594
|
+
module: Store,
|
595
|
+
model: {
|
596
|
+
fields: [{
|
597
|
+
name: "humanReadableLocation",
|
598
|
+
}, {
|
599
|
+
name: "size",
|
600
|
+
}, {
|
601
|
+
name: "timestamp",
|
602
|
+
type: "Date",
|
603
|
+
}],
|
604
|
+
},
|
605
|
+
url: "https://apis.is/earthquake/is",
|
606
|
+
responseRoot: "results",
|
607
|
+
autoLoad: true,
|
608
|
+
}
|
609
|
+
</pre>
|
610
|
+
|
611
|
+
The feed looks like this. (For simplicity, some values are omitted.)
|
612
|
+
<pre data-javascript>
|
613
|
+
{
|
614
|
+
"results": [{
|
615
|
+
"timestamp": "2017-10-13T12:07:24.000Z",
|
616
|
+
"latitude": 63.976,
|
617
|
+
"longitude": -21.949,
|
618
|
+
"size": 0.6,
|
619
|
+
"humanReadableLocation": "6,1 km SV af Helgafelli"
|
620
|
+
}, {
|
621
|
+
"timestamp": "2017-10-13T09:50:50.000Z",
|
622
|
+
"latitude": 65.124,
|
623
|
+
"longitude": -16.288,
|
624
|
+
"size": 0.9,
|
625
|
+
"humanReadableLocation": "6,1 km NA af Her\u00F0ubrei\u00F0art\u00F6glum"
|
626
|
+
},
|
627
|
+
...]
|
628
|
+
</pre>
|
629
|
+
|
630
|
+
The store defines a `type` for the date field. There are a few pre-defined field types that convert
|
631
|
+
the value from the feed into what's stored in the store's record. The store specifies the URL for the
|
632
|
+
data feed, and the store uses `responseRoot` to specify the value in the feed that holds the array
|
633
|
+
of items.
|
634
|
+
|
635
|
+
###The Table
|
636
|
+
|
637
|
+
Tables have two key configs: `store` and `columns`. Here's the columns config:
|
638
|
+
|
639
|
+
<pre data-javascript>
|
640
|
+
columns: [{
|
641
|
+
dataField: "timestamp",
|
642
|
+
text: "Date",
|
643
|
+
renderer: (data) => data.value.toLocaleDateString(undefined, {weekday: "long", year: "numeric", month: "long", day: "numeric"}),
|
644
|
+
}, {
|
645
|
+
dataField: "humanReadableLocation",
|
646
|
+
text: "Location",
|
647
|
+
}, {
|
648
|
+
dataField: "size",
|
649
|
+
text: "Magnitude",
|
650
|
+
align: "right",
|
651
|
+
renderer: (data) => data.value.toLocaleString(),
|
652
|
+
}]
|
653
|
+
</pre>
|
654
|
+
|
655
|
+
By default, a column just runs `toString()` on the record property specified in the column's `dataField`.
|
656
|
+
You can also provide a `renderer`, which is a function you provide to format the value any way you'd like.
|
657
|
+
In the code above it's using standard JavaScript methods to format the data and magnitude.
|
658
|
+
|
659
|
+
## Definining Views as Reusable Components
|
660
|
+
|
661
|
+
The way we've coded the app, the grid is _not_ reusable. In other words, if we needed two identical grids we'd
|
662
|
+
have to copy-and-paste the same config block.
|
663
|
+
|
664
|
+
You can reuse any class config block by creating a new class that extends the component's class. In other words,
|
665
|
+
if you want to reuse a table, you create a new class that extends `Neo.container.Table` and uses the same config.
|
666
|
+
|
667
|
+
Besides reuse, other good reasons to simplify and modularize your code is to make your views more descriptive and
|
668
|
+
abstract, and it allows those classes to be tested in isolation.
|
669
|
+
|
670
|
+
## Lab. Refactor the Table Into its Own Class
|
671
|
+
|
672
|
+
<!-- lab -->
|
673
|
+
|
674
|
+
<details>
|
675
|
+
<summary>Copy the table into its own class</summary>
|
676
|
+
|
677
|
+
Create a new file named `apps/earthquakes/view/earthquakes/Table.mjs` with this content.
|
678
|
+
|
679
|
+
<pre data-javascript>
|
680
|
+
import Base from '../../../../node_modules/neo.mjs/src/table/Container.mjs';
|
681
|
+
|
682
|
+
class Table extends Base {
|
683
|
+
static config = {
|
684
|
+
className: 'Earthquakes.view.earthquakes.Table',
|
685
|
+
ntype: 'earthquakes-table',
|
686
|
+
layout: {ntype: 'vbox', align: 'stretch'},
|
687
|
+
style: {width: '100%'},
|
688
|
+
columns: [{
|
689
|
+
dataField: "timestamp",
|
690
|
+
text: "Date",
|
691
|
+
renderer: (data) => data.value.toLocaleDateString(undefined, {weekday: "long", year: "numeric", month: "long", day: "numeric"}),
|
692
|
+
}, {
|
693
|
+
dataField: "humanReadableLocation",
|
694
|
+
text: "Location",
|
695
|
+
}, {
|
696
|
+
dataField: "size",
|
697
|
+
text: "Magnitude",
|
698
|
+
align: "right",
|
699
|
+
renderer: (data) => data.value.toLocaleString(),
|
700
|
+
}],
|
701
|
+
}
|
702
|
+
}
|
703
|
+
|
704
|
+
Neo.applyClassConfig(Table);
|
705
|
+
|
706
|
+
export default Table;
|
707
|
+
</pre>
|
708
|
+
|
709
|
+
</details>
|
710
|
+
|
711
|
+
<details>
|
712
|
+
<summary>Review the code</summary>
|
713
|
+
|
714
|
+
- The class extends `Neo.table.Container`
|
715
|
+
- It has an `ntype`, which we can use when creating an instance, or when debugging
|
716
|
+
- Each column has `text` and `dataField` configs, and some have renderers
|
717
|
+
|
718
|
+
</details>
|
719
|
+
|
720
|
+
<details>
|
721
|
+
<summary>Use the new component</summary>
|
722
|
+
|
723
|
+
Edit `apps/earthquakes/view/MainView` and make these changes.
|
724
|
+
|
725
|
+
- Add `import EarthquakesTable from './earthquakes/Table.mjs';`
|
726
|
+
- Replace the `module: Table` with `module: EarthquakesTable`
|
727
|
+
- Remove the `columns:[]` config
|
728
|
+
- Leave the `store` config alone
|
729
|
+
|
730
|
+
Save and refresh the browser, and your app should run as before.
|
731
|
+
|
732
|
+
You can confim that the new class _is being loaded_ by using DevTools to try to open `earthquakes/Table` — if it
|
733
|
+
was imported, it'll be listed.
|
734
|
+
|
735
|
+
You can confirm that an instance _was created_ by using the DevTools console and searching for it via
|
736
|
+
|
737
|
+
Neo.first('earthquakes-table')
|
738
|
+
|
739
|
+
</details>
|
740
|
+
|
741
|
+
<details>
|
742
|
+
<summary>Here's the code</summary>
|
743
|
+
|
744
|
+
<pre data-javascript>
|
745
|
+
import Base from '../../../node_modules/neo.mjs/src/container/Base.mjs';
|
746
|
+
import EarthquakesTable from './earthquakes/Table.mjs';
|
747
|
+
import Store from '../../../node_modules/neo.mjs/src/data/Store.mjs';
|
748
|
+
import Controller from './MainViewController.mjs';
|
749
|
+
import ViewModel from './MainViewModel.mjs';
|
750
|
+
|
751
|
+
class MainView extends Base {
|
752
|
+
static config = {
|
753
|
+
className: 'Earthquakes.view.MainView',
|
754
|
+
ntype: 'earthquakes-main',
|
755
|
+
|
756
|
+
controller: {module: Controller},
|
757
|
+
model: {module: ViewModel},
|
758
|
+
|
759
|
+
layout: {ntype: 'vbox', align: 'stretch'},
|
760
|
+
items: [{
|
761
|
+
module: EarthquakesTable,
|
762
|
+
store: {
|
763
|
+
module: Store,
|
764
|
+
model: {
|
765
|
+
fields: [{
|
766
|
+
name: "humanReadableLocation",
|
767
|
+
}, {
|
768
|
+
name: "size",
|
769
|
+
}, {
|
770
|
+
name: "timestamp",
|
771
|
+
type: "Date",
|
772
|
+
}],
|
773
|
+
},
|
774
|
+
url: "https://apis.is/earthquake/is",
|
775
|
+
responseRoot: "results",
|
776
|
+
autoLoad: true,
|
777
|
+
},
|
778
|
+
style: {width: '100%'},
|
779
|
+
}],
|
780
|
+
}
|
781
|
+
}
|
782
|
+
|
783
|
+
Neo.applyClassConfig(MainView);
|
784
|
+
|
785
|
+
export default MainView;
|
786
|
+
</pre>
|
787
|
+
|
788
|
+
</details>
|
789
|
+
|
790
|
+
<details>
|
791
|
+
<summary>Why are some things in `MainView` and not in `Table`?</summary>
|
792
|
+
|
793
|
+
When we refactored the table into its own class we didn't move all the configs. Both
|
794
|
+
the width styling and `store` were left in `MainView`. Why?
|
795
|
+
|
796
|
+
It's a matter of re-use and what you need in a given situation. By leaving the width specification
|
797
|
+
outside the table class we're to specify a different value in all the places we're using the table.
|
798
|
+
|
799
|
+
Similarly, if the store were in the table class, it would be using that specific store and
|
800
|
+
each instance of the table would have its own instance of the store. If we want multiple
|
801
|
+
instance of the table with each using a different store — or if
|
802
|
+
we wanted to share the store with other components — then it makes sense for the
|
803
|
+
store to be outside the table class.
|
804
|
+
|
805
|
+
</details>
|
806
|
+
|
807
|
+
<details>
|
808
|
+
<summary>Make a second instance of the table</summary>
|
809
|
+
|
810
|
+
To further illustrate that the table is reusable, let's create a second instance.
|
811
|
+
|
812
|
+
Simply copy-and-paste the value in the `MainView` `items` with an identical second item.
|
813
|
+
|
814
|
+
Save and refresh and you should see two tables.
|
815
|
+
|
816
|
+
<img style="width:80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/EarthquakesTwoTables.png"></img>
|
817
|
+
|
818
|
+
</details>
|
210
819
|
|
820
|
+
<!-- /lab -->
|
821
|
+
|
822
|
+
## Shared Bindable Data
|
823
|
+
|
824
|
+
The _earthquakes_ app has a problem: even though the table is nicely reusable, we duplicated the config for the store,
|
825
|
+
and we can't share it. If you were to look at network traffic you'd see that we're also fetching the data
|
826
|
+
twice.
|
827
|
+
|
828
|
+
If we simply wanted to re-use the store's description we could refactor the store config into a new
|
829
|
+
store class, just like we did for the table. But in _earthquakes_ we want to share the store _instance_.
|
830
|
+
|
831
|
+
Neo has a feature that allows shared, bindable, data. A `Neo.model.Component` instance holds properties that
|
832
|
+
can be values like strings, numbers, or even references, like component or store references. `Neo.model.Component`
|
833
|
+
is commonly called a _view model_ or _component model_.<small><sup>*</sup></small>
|
834
|
+
|
835
|
+
The `create-app-minimal` script includes a view model and view controller config. The view model
|
836
|
+
will hold the store.
|
837
|
+
|
838
|
+
<br>
|
839
|
+
<br>
|
840
|
+
<br>
|
841
|
+
<small>* There's a longer write-up on view controllers in the "Getting Started" section.</small>
|
842
|
+
|
843
|
+
|
844
|
+
|
845
|
+
## Lab. Use a View Model
|
846
|
+
|
847
|
+
<!-- lab -->
|
848
|
+
|
849
|
+
<details>
|
850
|
+
<summary>Look at network traffic</summary>
|
851
|
+
|
852
|
+
Before making any changes, open devtools in the Network tab and refresh _earthquakes_. You'll see two
|
853
|
+
calls to the web service.
|
854
|
+
|
855
|
+
<img style="width:80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/EarthquakesTwoTablesTwoCalls.png"></img>
|
211
856
|
|
212
857
|
</details>
|
213
858
|
|
859
|
+
<details>
|
860
|
+
<summary>Copy the store config to the view model</summary>
|
861
|
+
|
862
|
+
View models have two key configs: `data` and `stores`.
|
863
|
+
|
864
|
+
- `data` holds name/value pairs where the value can be a simple value, or object references
|
865
|
+
- `stores` holds configs of stores
|
866
|
+
|
867
|
+
Add a `stores` property to the view model config that holds a copy of the store.
|
868
|
+
|
869
|
+
<pre data-javascript>
|
870
|
+
|
871
|
+
import Base from '../../../node_modules/neo.mjs/src/container/Base.mjs';
|
872
|
+
import EarthquakesTable from './earthquakes/Table.mjs';
|
873
|
+
import Store from '../../../node_modules/neo.mjs/src/data/Store.mjs';
|
874
|
+
import Controller from './MainViewController.mjs';
|
875
|
+
import ViewModel from './MainViewModel.mjs';
|
876
|
+
|
877
|
+
class MainView extends Base {
|
878
|
+
static config = {
|
879
|
+
className: 'Earthquakes.view.MainView',
|
880
|
+
ntype: 'earthquakes-main',
|
881
|
+
|
882
|
+
controller: {module: Controller},
|
883
|
+
model: {
|
884
|
+
module: ViewModel,
|
885
|
+
stores: {
|
886
|
+
earthquakes: {
|
887
|
+
module: Store,
|
888
|
+
model: {
|
889
|
+
fields: [{
|
890
|
+
name: "humanReadableLocation",
|
891
|
+
}, {
|
892
|
+
name: "size",
|
893
|
+
}, {
|
894
|
+
name: "timestamp",
|
895
|
+
type: "Date",
|
896
|
+
}],
|
897
|
+
},
|
898
|
+
url: "https://apis.is/earthquake/is",
|
899
|
+
responseRoot: "results",
|
900
|
+
autoLoad: true,
|
901
|
+
},
|
902
|
+
}
|
903
|
+
},
|
904
|
+
|
905
|
+
layout: {
|
906
|
+
ntype: 'vbox', align: 'stretch'
|
907
|
+
},
|
908
|
+
items: [{
|
909
|
+
module: EarthquakesTable,
|
910
|
+
store: {
|
911
|
+
module: Store,
|
912
|
+
model: {
|
913
|
+
fields: [{
|
914
|
+
name: "humanReadableLocation",
|
915
|
+
}, {
|
916
|
+
name: "size",
|
917
|
+
}, {
|
918
|
+
name: "timestamp",
|
919
|
+
type: "Date",
|
920
|
+
}],
|
921
|
+
},
|
922
|
+
url: "https://apis.is/earthquake/is",
|
923
|
+
responseRoot: "results",
|
924
|
+
autoLoad: true,
|
925
|
+
},
|
926
|
+
style: {width: '100%'},
|
927
|
+
},{
|
928
|
+
module: EarthquakesTable,
|
929
|
+
store: {
|
930
|
+
module: Store,
|
931
|
+
model: {
|
932
|
+
fields: [{
|
933
|
+
name: "humanReadableLocation",
|
934
|
+
}, {
|
935
|
+
name: "size",
|
936
|
+
}, {
|
937
|
+
name: "timestamp",
|
938
|
+
type: "Date",
|
939
|
+
}],
|
940
|
+
},
|
941
|
+
url: "https://apis.is/earthquake/is",
|
942
|
+
responseRoot: "results",
|
943
|
+
autoLoad: true,
|
944
|
+
},
|
945
|
+
style: {width: '100%'},
|
946
|
+
}],
|
947
|
+
}
|
948
|
+
}
|
949
|
+
|
950
|
+
Neo.applyClassConfig(MainView);
|
951
|
+
|
952
|
+
export default MainView;
|
953
|
+
|
954
|
+
</pre>
|
955
|
+
|
956
|
+
In the `stores` config we named the store _earthquakes_. We could have named it anything, like _foo_
|
957
|
+
or _myStore_. We're calling it _earthquakes_ simply because that seems like a good descriptive name
|
958
|
+
of the data the store holds.
|
959
|
+
|
960
|
+
At this point we have _three_ identical store configs! Save and refresh, and look at network traffic — you
|
961
|
+
should see three calls.
|
962
|
+
|
963
|
+
Having an instance in the view model means we can share it. It can be shared anywhere in the containment
|
964
|
+
hierarchy. The app doesn't have much of a hierarchy: it's just the main view and two child components (the two
|
965
|
+
tables). But now that the store is in the parent's view model we can share it.
|
966
|
+
|
967
|
+
</details>
|
968
|
+
|
969
|
+
<details>
|
970
|
+
<summary>Use the shared store</summary>
|
971
|
+
|
972
|
+
The way to bind an instance to a view model property is with the `bind` config. For example
|
973
|
+
|
974
|
+
bind: {
|
975
|
+
store: 'stores.earthquakes'
|
976
|
+
}
|
977
|
+
|
978
|
+
binds a `store` property to a store called `foo`. The code is saying _in the future, when the value
|
979
|
+
of "stores.earthquakes" changes, assign it to this object's "store" property_. In this case, `stores.earthquakes`
|
980
|
+
starts out undefined, then at runtime within a few milliseconds as the view model is processed, the configured
|
981
|
+
store is created and a reference is assigned to `stores.earthquakes`. That wakes the binding up, and the
|
982
|
+
value is assigned to the table's `store` property.
|
983
|
+
|
984
|
+
Replace each table's `store` config with the binding.
|
985
|
+
|
986
|
+
<pre data-javascript>
|
987
|
+
|
988
|
+
import Base from '../../../node_modules/neo.mjs/src/container/Base.mjs';
|
989
|
+
import EarthquakesTable from './earthquakes/Table.mjs';
|
990
|
+
import Store from '../../../node_modules/neo.mjs/src/data/Store.mjs';
|
991
|
+
import Controller from './MainViewController.mjs';
|
992
|
+
import ViewModel from './MainViewModel.mjs';
|
993
|
+
|
994
|
+
class MainView extends Base {
|
995
|
+
static config = {
|
996
|
+
className: 'Earthquakes.view.MainView',
|
997
|
+
ntype: 'earthquakes-main',
|
998
|
+
controller: {module: Controller},
|
999
|
+
model: {
|
1000
|
+
module: ViewModel,
|
1001
|
+
stores: {
|
1002
|
+
earthquakes: {
|
1003
|
+
module: Store,
|
1004
|
+
model: {
|
1005
|
+
fields: [{
|
1006
|
+
name: "humanReadableLocation",
|
1007
|
+
}, {
|
1008
|
+
name: "size",
|
1009
|
+
}, {
|
1010
|
+
name: "timestamp",
|
1011
|
+
type: "Date",
|
1012
|
+
}],
|
1013
|
+
},
|
1014
|
+
url: "https://apis.is/earthquake/is",
|
1015
|
+
responseRoot: "results",
|
1016
|
+
autoLoad: true,
|
1017
|
+
},
|
1018
|
+
}
|
1019
|
+
},
|
1020
|
+
|
1021
|
+
layout: { ntype: 'vbox', align: 'stretch' },
|
1022
|
+
items: [{
|
1023
|
+
module: EarthquakesTable,
|
1024
|
+
bind: {
|
1025
|
+
store: 'stores.earthquakes'
|
1026
|
+
},
|
1027
|
+
style: {width: '100%'},
|
1028
|
+
},{
|
1029
|
+
module: EarthquakesTable,
|
1030
|
+
bind: {
|
1031
|
+
store: 'stores.earthquakes'
|
1032
|
+
},
|
1033
|
+
style: {width: '100%'},
|
1034
|
+
}],
|
1035
|
+
}
|
1036
|
+
}
|
1037
|
+
|
1038
|
+
Neo.applyClassConfig(MainView);
|
1039
|
+
|
1040
|
+
export default MainView;
|
1041
|
+
</pre>
|
1042
|
+
|
1043
|
+
Save, refresh, and look at network traffic: you'll see a _single_ call to the web service.
|
1044
|
+
|
1045
|
+
<img style="width:80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/EarthquakesTwoTablesOneCall.png"></img>
|
1046
|
+
|
1047
|
+
You can further prove we're using a shared instance by running these statements in the console.
|
1048
|
+
|
1049
|
+
<pre data-javascript>
|
1050
|
+
a = Neo.findFirst({ntype:'earthquakes-main'}).model.stores.earthquakes;
|
1051
|
+
b = Neo.find({ntype:'earthquakes-table'})[0].store;
|
1052
|
+
c = Neo.find({ntype:'earthquakes-table'})[1].store;
|
1053
|
+
|
1054
|
+
(a === b) && (a === c) && (b === c) // true
|
1055
|
+
</pre>
|
1056
|
+
|
1057
|
+
</details>
|
1058
|
+
|
1059
|
+
<details>
|
1060
|
+
<summary>Use the view model class</summary>
|
1061
|
+
|
1062
|
+
We configured the view model in-line, in the `model` config at the top of `MainView`. But the starter app
|
1063
|
+
has a `MainViewModel` class. In theory, if you have a trivial view model you could configure it in-line. But
|
1064
|
+
in general you want to keep that code separate by coding it in a separate class. This is what we did for the
|
1065
|
+
table config — we started by coding it in-line in the main view, then we refactored it into its own
|
1066
|
+
class. The result was a simpler and more abstract main view. We want to do the same for the view model.
|
1067
|
+
|
1068
|
+
Since the starter app already provides `MainViewModel`, all we need to do is copy the `stores` property.
|
1069
|
+
|
1070
|
+
Here's the resulting code you should place into `MainViewModel.mjs`.
|
1071
|
+
|
1072
|
+
<pre data-javascript>
|
1073
|
+
|
1074
|
+
import Base from '../../../node_modules/neo.mjs/src/model/Component.mjs';
|
1075
|
+
import Store from '../../../node_modules/neo.mjs/src/data/Store.mjs';
|
1076
|
+
|
1077
|
+
class MainViewModel extends Base {
|
1078
|
+
static config = {
|
1079
|
+
className: 'Earthquakes.view.MainViewModel',
|
1080
|
+
|
1081
|
+
data: {},
|
1082
|
+
stores: {
|
1083
|
+
earthquakes: {
|
1084
|
+
module: Store,
|
1085
|
+
model: {
|
1086
|
+
fields: [{
|
1087
|
+
name: "humanReadableLocation",
|
1088
|
+
}, {
|
1089
|
+
name: "size",
|
1090
|
+
}, {
|
1091
|
+
name: "timestamp",
|
1092
|
+
type: "Date",
|
1093
|
+
}],
|
1094
|
+
},
|
1095
|
+
url: "https://apis.is/earthquake/is",
|
1096
|
+
responseRoot: "results",
|
1097
|
+
autoLoad: true,
|
1098
|
+
},
|
1099
|
+
}
|
1100
|
+
}
|
1101
|
+
}
|
1102
|
+
|
1103
|
+
Neo.applyClassConfig(MainViewModel);
|
1104
|
+
|
1105
|
+
export default MainViewModel;
|
1106
|
+
</pre>
|
1107
|
+
|
1108
|
+
And you need to remove the `stores` config from the main view as follows.
|
1109
|
+
|
1110
|
+
<pre data-javascript>
|
1111
|
+
|
1112
|
+
import Base from '../../../node_modules/neo.mjs/src/container/Base.mjs';
|
1113
|
+
import EarthquakesTable from './earthquakes/Table.mjs';
|
1114
|
+
import Controller from './MainViewController.mjs';
|
1115
|
+
import ViewModel from './MainViewModel.mjs';
|
1116
|
+
|
1117
|
+
class MainView extends Base {
|
1118
|
+
static config = {
|
1119
|
+
className: 'Earthquakes.view.MainView',
|
1120
|
+
ntype: 'earthquakes-main',
|
1121
|
+
controller: {module: Controller},
|
1122
|
+
model: {
|
1123
|
+
module: ViewModel
|
1124
|
+
},
|
1125
|
+
|
1126
|
+
layout: { ntype: 'vbox', align: 'stretch' },
|
1127
|
+
items: [{
|
1128
|
+
module: EarthquakesTable,
|
1129
|
+
bind: {
|
1130
|
+
store: 'stores.earthquakes'
|
1131
|
+
},
|
1132
|
+
style: {width: '100%'},
|
1133
|
+
},{
|
1134
|
+
module: EarthquakesTable,
|
1135
|
+
bind: {
|
1136
|
+
store: 'stores.earthquakes'
|
1137
|
+
},
|
1138
|
+
style: {width: '100%'},
|
1139
|
+
}],
|
1140
|
+
}
|
1141
|
+
}
|
1142
|
+
|
1143
|
+
Neo.applyClassConfig(MainView);
|
1144
|
+
|
1145
|
+
export default MainView;
|
1146
|
+
</pre>
|
1147
|
+
|
1148
|
+
The refactorings to have separate table and view model classes means the code is more modular, more reusable,
|
1149
|
+
and each class is simpler than using complex source files that try to configure every detail.
|
1150
|
+
|
1151
|
+
</details>
|
1152
|
+
|
1153
|
+
<!-- /lab -->
|
1154
|
+
|
1155
|
+
### Google Maps Add-on
|
1156
|
+
|
1157
|
+
Neo.mjs has a Google Map component. This component is a little different than a button or table,
|
1158
|
+
becauase it's implemented as a _main thread add-on_.
|
1159
|
+
|
1160
|
+
When you use Google Maps you use the Google Map API to ask it to draw the map and markers.
|
1161
|
+
In a normal app, Google Maps &mdahs; and everything else — runs in the main browser thread.
|
1162
|
+
But as you know, Neo.mjs logic runs in _neomjs-app-worker_. That means Neo.mjs has to pass instructions
|
1163
|
+
run in _neomjs-app-worker_ to the main thread.
|
1164
|
+
|
1165
|
+
To handle this situation, Neo.mjs has the concept of a main-thread add-on, which is a class that exposes main
|
1166
|
+
thread methods to the _neomjs-app-worker_ thread. In addition, if the library you're using has a UI, it's common
|
1167
|
+
to also provide a wrapper class so it can be used like any other component within Neo.mjs. That's how Google
|
1168
|
+
Maps is implemented: there's a main-thread add-on and a corresponding Neo.mjs component. The add-on is
|
1169
|
+
specified in _neo-config.json_, and the component is imported and used like any other component.
|
1170
|
+
|
1171
|
+
Ultimately, normal components are responsible for specifying how
|
1172
|
+
they're rendered (which is usually handled by Neo.mjs).
|
1173
|
+
|
1174
|
+
How do you specify which main-thread add-ons you want? If you recall the script you used to create the starter
|
1175
|
+
app, it has a step that asks what add-ons you want. That results in populating the `mainThreadAddons` property
|
1176
|
+
in `neo-config.json`. We didn't choose Google Maps when we ran the script, but we need it. That means we
|
1177
|
+
need to edit `neo-config.json` and add it. Google Maps also requires an API key, which is also configured in
|
1178
|
+
`neo-config.json`.
|
1179
|
+
|
1180
|
+
The Google Maps component has a few key configs:
|
1181
|
+
|
1182
|
+
- `center:{lat, lng}`
|
1183
|
+
- `zoom`
|
1184
|
+
- `markerStore`
|
1185
|
+
|
1186
|
+
Marker store records are required to have these properties:
|
1187
|
+
|
1188
|
+
- `position` — the location of the marker, of the form `{lat, lng}`
|
1189
|
+
- `title` — a description of the marker
|
1190
|
+
|
1191
|
+
## Lab. Use the Google Maps Main-thread Add-on
|
1192
|
+
|
1193
|
+
<!-- lab -->
|
1194
|
+
|
1195
|
+
<details>
|
1196
|
+
<summary>Specficy the main-thread add-on</summary>
|
1197
|
+
|
1198
|
+
Edit `apps/earthquakes/neo-config.json` and add entries for the Google Maps add-on and the map key.
|
1199
|
+
|
1200
|
+
<pre data-javascript>
|
1201
|
+
{
|
1202
|
+
"appPath": "../../apps/earthquakes/app.mjs",
|
1203
|
+
"basePath": "../../",
|
1204
|
+
"environment": "development",
|
1205
|
+
"mainPath": "../node_modules/neo.mjs/src/Main.mjs",
|
1206
|
+
"mainThreadAddons": [
|
1207
|
+
"DragDrop",
|
1208
|
+
"WS/GoogleMaps",
|
1209
|
+
"Stylesheet"
|
1210
|
+
],
|
1211
|
+
"googleMapsApiKey": "AIzaSyD4Y2xvl9mGT8HiVvQiZluT5gah3OIveCE",
|
1212
|
+
"themes" : ["neo-theme-neo-light"],
|
1213
|
+
"workerBasePath": "../../node_modules/neo.mjs/src/worker/"
|
1214
|
+
}
|
1215
|
+
</pre>
|
1216
|
+
|
1217
|
+
Save and refresh, and you'll see a console log emanating from the plugin.
|
1218
|
+
|
1219
|
+
<img style="width:80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/GoogleMapsLoaded.png"></img>
|
1220
|
+
|
1221
|
+
</details>
|
1222
|
+
|
1223
|
+
<details>
|
1224
|
+
<summary>Add the required fields to the records</summary>
|
1225
|
+
|
1226
|
+
The Google Maps component has a `markerStore` property, which is a reference to a store whose records have
|
1227
|
+
the properties `title` and `location`, where `location` is of the form `{lat: 0, lng: 0}`. The `fields:[]`
|
1228
|
+
lets us implement those via two properties:
|
1229
|
+
|
1230
|
+
- `mapping` — the path to a feed property holding the value
|
1231
|
+
- `calculate` — a function that returns a value
|
1232
|
+
|
1233
|
+
Edit `apps/earthquakes/view/MainViewModel.mjs` and modify `fields` as follows.
|
1234
|
+
|
1235
|
+
<pre data-javascript>
|
1236
|
+
fields: [{
|
1237
|
+
name: "humanReadableLocation",
|
1238
|
+
}, {
|
1239
|
+
name: "size",
|
1240
|
+
}, {
|
1241
|
+
name: "timestamp",
|
1242
|
+
type: "Date",
|
1243
|
+
}, {
|
1244
|
+
name: 'title',
|
1245
|
+
mapping: "humanReadableLocation"
|
1246
|
+
}, {
|
1247
|
+
name: "position",
|
1248
|
+
calculate: (data, field, item)=>({lat: item.latitude, lng: item.longitude})
|
1249
|
+
}],
|
1250
|
+
</pre>
|
1251
|
+
|
1252
|
+
As you can see, _title_ is mapped to the existing feed value _humanReadableLocation_, and _position_ is
|
1253
|
+
calculated by returning an object with _lat_ and _lng_ set to the corresponding values from the feed.
|
1254
|
+
|
1255
|
+
Save and refresh _earthquakes_. You can use the debugger to inspect the store via _Shift-Ctrl-right-click_ and
|
1256
|
+
putting the main view into a global variable. Then run
|
1257
|
+
|
1258
|
+
temp1.getModel().stores.earthquakes.items
|
1259
|
+
|
1260
|
+
Look at one of the items and you should see that _title_ and _location_ are in each record.
|
1261
|
+
|
1262
|
+
<img style="width:80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/StoreHasTitleAndLocation.png"></img>
|
1263
|
+
|
1264
|
+
</details>
|
1265
|
+
|
1266
|
+
<details>
|
1267
|
+
<summary>Use the Google Map Component</summary>
|
1268
|
+
|
1269
|
+
We're going to replace the top table with a Google Map. To do that we need to import the Google Maps component
|
1270
|
+
and show it implace of the top table. The map should be centered on Iceland. To wit
|
1271
|
+
|
1272
|
+
<pre>
|
1273
|
+
{
|
1274
|
+
module: GoogleMapsComponent,
|
1275
|
+
flex: 1,
|
1276
|
+
center: {
|
1277
|
+
lat: 64.8014187,
|
1278
|
+
lng: -18.3096357
|
1279
|
+
},
|
1280
|
+
zoom: 6,
|
1281
|
+
}
|
1282
|
+
</pre>
|
1283
|
+
|
1284
|
+
If we replace the top table with the map, `view/MainView.mjs` ends up with this content.
|
1285
|
+
|
1286
|
+
<pre data-javascript>
|
1287
|
+
|
1288
|
+
import Base from '../../../node_modules/neo.mjs/src/container/Base.mjs';
|
1289
|
+
import EarthquakesTable from './earthquakes/Table.mjs';
|
1290
|
+
import Controller from './MainViewController.mjs';
|
1291
|
+
import ViewModel from './MainViewModel.mjs';
|
1292
|
+
import GoogleMapsComponent from '../../../src/component/wrapper/GoogleMaps.mjs';
|
1293
|
+
|
1294
|
+
class MainView extends Base {
|
1295
|
+
static config = {
|
1296
|
+
className: 'Earthquakes.view.MainView',
|
1297
|
+
ntype: 'earthquakes-main',
|
1298
|
+
controller: {module: Controller},
|
1299
|
+
model: {
|
1300
|
+
module: ViewModel
|
1301
|
+
},
|
1302
|
+
|
1303
|
+
layout: { ntype: 'vbox', align: 'stretch' },
|
1304
|
+
items: [{
|
1305
|
+
module: GoogleMapsComponent,
|
1306
|
+
flex: 1,
|
1307
|
+
center: {
|
1308
|
+
lat: 64.8014187,
|
1309
|
+
lng: -18.3096357
|
1310
|
+
},
|
1311
|
+
zoom: 6,
|
1312
|
+
},{
|
1313
|
+
module: EarthquakesTable,
|
1314
|
+
bind: {
|
1315
|
+
store: 'stores.earthquakes'
|
1316
|
+
},
|
1317
|
+
style: {width: '100%'},
|
1318
|
+
wrapperStyle: {
|
1319
|
+
height: 'auto' // Because neo-table-wrapper sets height:'100%', which it probably shouldn't
|
1320
|
+
}
|
1321
|
+
}],
|
1322
|
+
}
|
1323
|
+
}
|
1324
|
+
|
1325
|
+
Neo.applyClassConfig(MainView);
|
1326
|
+
|
1327
|
+
export default MainView;
|
1328
|
+
|
1329
|
+
</pre>
|
1330
|
+
|
1331
|
+
<img style="width:80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/CenteredMap.png"></img>
|
1332
|
+
|
1333
|
+
|
1334
|
+
</details>
|
1335
|
+
|
1336
|
+
<details>
|
1337
|
+
<summary>Show the markers</summary>
|
1338
|
+
|
1339
|
+
The markers are shown by setting up the marker store, which is a regular store whose records must contain
|
1340
|
+
_location_ and _title_. We assign the store using a `bind`, just like we did with the tables.
|
1341
|
+
|
1342
|
+
Add this config to the map.
|
1343
|
+
|
1344
|
+
<pre data-javascript>
|
1345
|
+
bind: {
|
1346
|
+
markerStore: 'stores.earthquakes'
|
1347
|
+
},
|
1348
|
+
</pre>
|
1349
|
+
<img style="width:80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/InitialMapWithMarkers.png"></img>
|
1350
|
+
|
1351
|
+
</details>
|
1352
|
+
|
1353
|
+
<!-- /lab -->
|
1354
|
+
|
1355
|
+
## Events
|
1356
|
+
|
1357
|
+
Neo.mjs has an `Neo.core.Observable` class that handles configuring listeners and associated event handler functions.
|
1358
|
+
All components are observable, and some non-visual classes, like stores, are also observable.
|
1359
|
+
|
1360
|
+
Listeners are set up either declaratively, via the `listeners:{}` config, or procedurally,
|
1361
|
+
via the `component.on()` method.
|
1362
|
+
|
1363
|
+
## Lab. Events
|
1364
|
+
|
1365
|
+
<!-- lab -->
|
1366
|
+
|
1367
|
+
In this lab you'll set up an event handler for the table and map.
|
1368
|
+
|
1369
|
+
<details>
|
1370
|
+
<summary>Add a listener to the table</summary>
|
1371
|
+
|
1372
|
+
Tables fire a select event, passing an object that contains a reference to the corresponding row.
|
1373
|
+
|
1374
|
+
Add this table config:
|
1375
|
+
|
1376
|
+
listeners: {
|
1377
|
+
select: (data) => console.log(data.record)
|
1378
|
+
}
|
1379
|
+
|
1380
|
+
Save and refresh, then click on a table row. If you look at the debugger console you'll see the record being logged.
|
1381
|
+
|
1382
|
+
Just for fun, expand the logged value and look for the size property. If you recall, that's a value from the feed, and one of the things we configured in the store's fields:[].
|
1383
|
+
|
1384
|
+
In the console, click on the ellipses by size and enter a new value, like 2.5. (Don't enter a larger value, or you may destroy that part of Iceland.)
|
1385
|
+
|
1386
|
+
<img style="width:80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/LogTableClick.png"></img>
|
1387
|
+
|
1388
|
+
After changing the value you should immediately see it reflected in the table row.
|
1389
|
+
|
1390
|
+
</details>
|
1391
|
+
|
1392
|
+
<details>
|
1393
|
+
<summary>Add a listener to a map event
|
1394
|
+
</summary>
|
1395
|
+
|
1396
|
+
Now add a `markerClick` listener to the Google Map.
|
1397
|
+
|
1398
|
+
listeners: {
|
1399
|
+
markerClick: data => console.log(data.data.record)
|
1400
|
+
},
|
1401
|
+
|
1402
|
+
Save, refresh, and confirm that you see the value logged when you click on a map marker.
|
1403
|
+
|
1404
|
+
</details>
|
1405
|
+
|
1406
|
+
|
1407
|
+
|
1408
|
+
|
214
1409
|
<!-- /lab -->
|