neo.mjs 6.10.12 → 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.
@@ -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
- ##Advice
17
+ ## Goals
18
18
 
19
- A word of advice: Keep a high-level perspective, especially early on. Throughout this
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
- ##Lab. Generate a workspace
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 <b>when prompted for "Main thread add-ons" choose GoogleMaps</b> (using arrow
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
- &mdash; it specifies the default add-ons, as well as the GoogleMaps add-on you specified.
163
+ &mdash; it reflects the add-ons you chose when you followed the instructions in the script.
140
164
  <pre>
141
165
  {
142
- "appPath": "../../apps/myapp/app.mjs",
166
+ "appPath": "../../apps/earthquakes/app.mjs",
143
167
  "basePath": "../../",
144
168
  "environment": "development",
145
169
  "mainPath": "../node_modules/neo.mjs/src/Main.mjs",
146
- "workerBasePath": "../../node_modules/neo.mjs/src/worker/",
147
- "themes": [
148
- "neo-theme-neo-light"
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 &mdash; 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 &mdash; 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>Start the server</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 &mdash; 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
- ##Introduction to Debugging
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 &mdash;
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>Inspect the workspace</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>Start the server</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
- <img src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/StartServer.png" style="width:80%"/>
376
+ Any time you have a object reference in the console &mdash; even if it's nested within an array or object &mdash;
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>Run the starter app</summary>
387
+ <summary>Use `Neo.find()` and `Neo.findFirst()`</summary>
205
388
 
206
- By default, an app named `myapp` was created. You can run it by entering the `apps` directory and
207
- clicking `myapp`. It's a folder containing an `index.html` along with the source code for the app.
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
- <img src="https://s3.amazonaws.com/mjs.neo.learning.images/earthquakes/RunTheStarterApp.png" style="width:80%"/>
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 &mdash; 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()` &mdash; 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_ &mdash; 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` &mdash; 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 &mdash; or if
802
+ we wanted to share the store with other components &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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` &mdash; the location of the marker, of the form `{lat, lng}`
1189
+ - `title` &mdash; 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` &mdash; the path to a feed property holding the value
1231
+ - `calculate` &mdash; 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 -->