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.
@@ -140,3 +140,14 @@ class MainView extends Base {
140
140
  }
141
141
  Neo.applyClassConfig(MainView);
142
142
  </pre>
143
+
144
+ How are events set up? We don't really care, but in case you're curious: Neo.mjs has a `Neo.core.Observable` class
145
+ that can be mixed into any class. It maintains a `listeners` object map that's a key-value pair, where
146
+ the key is the event name, and the value is an array of function references. The first time a listener is
147
+ added an entry is added to the map using the event name as the key, and the event handler added as the first
148
+ item in the associated array. If another listener is added for the same event, a second item is added to the
149
+ array. If a new event is added, a new entry is added. Etc. When the event is fired, Neo.mjs looks up the map
150
+ entry for the event name, then runs each function in the array, passing whatever data is specified in the
151
+ call to `fire()`.
152
+
153
+ <img style="width:80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/gettingStarted/events/ObservableInMemory.png"></img>
@@ -1,3 +1,4 @@
1
+
1
2
  In theory, a Neo.mjs app could be defined in a single `.mjs` source file. But that would be very hard to
2
3
  maintain, and any reusable configs would have to be duplicated. Instead, each of your views and reusable
3
4
  widgets will be defined as its own class. The result is simpler views which are inherently reusable and easier
@@ -0,0 +1,153 @@
1
+ All components fire events. For example, form fields fire a `change` event, various
2
+ focus events, and others. Some other types fire events too, such as `Neo.data.Store`,
3
+ which fires a `load` event after the store is loaded with data.
4
+
5
+ Some terminology related to events is that events are _fired_, and as a result, some
6
+ event _handler_ &mdash; or _listener_ &mdash; is run.
7
+
8
+ To specify an event handler, use `listeners: {}`, specifying in as many event/handler
9
+ pairs as you need.
10
+
11
+ The code below shows two text fields, with `listeners` for `change` and `focusEnter`.
12
+ (The events for any component are documened in the API docs.)
13
+
14
+ <pre data-neo>
15
+ import Base from '../../../../src/container/Base.mjs';
16
+ import TextField from '../../../../src/form/field/Text.mjs';
17
+ class MainView extends Base {
18
+ static config = {
19
+ className : 'Example.view.MainView',
20
+ layout: {ntype:'vbox', align:'start'},
21
+ items : [{
22
+ module: TextField,
23
+ labelText : 'First name',
24
+ listeners: {
25
+ change: data=>console.log(data.value), // There are other properties, like oldValue
26
+ focusEnter: data=>console.log(`Entering ${data.component.labelText}`)
27
+ }
28
+ },
29
+ {
30
+ module: TextField,
31
+ labelText : 'Last name',
32
+ listeners: {
33
+ change: data=>console.log(data.value), // There are other properties, like oldValue
34
+ focusEnter: data=>console.log(`Entering ${data.component.labelText}`)
35
+ }
36
+ }]
37
+ }
38
+ }
39
+ Neo.applyClassConfig(MainView);
40
+ </pre>
41
+
42
+ If you run the example, and open the browser's debugger, you'll see the console being logged as you type or give
43
+ focus to either field.
44
+
45
+ Note that the handlers specify an in-line function. For trivial cases, that might be ok. But normally
46
+ you'd want better separation of concerns by placing those event handlers in a separate class. Neo.mjs provides
47
+ that with a _component controller_.
48
+
49
+ A `Neo.controller.Component` is a simple class associated with a component class. As a view is created, an
50
+ instance of its associated contoller is automatically created.
51
+
52
+ <pre data-neo>
53
+ import Base from '../../../../src/container/Base.mjs';
54
+ import Controller from '../../../../src/controller/Component.mjs';
55
+ import TextField from '../../../../src/form/field/Text.mjs';
56
+
57
+ class MainViewController extends Controller {
58
+ static config = {
59
+ className: 'Example.view.MainViewController'
60
+ }
61
+ onChange(data){
62
+ console.log(data.value);
63
+ }
64
+ }
65
+ Neo.applyClassConfig(MainViewController);
66
+
67
+
68
+ class MainView extends Base {
69
+ static config = {
70
+ className : 'Example.view.MainView',
71
+ controller: MainViewController,
72
+ layout: {ntype:'vbox', align:'start'},
73
+ items : [{
74
+ module: TextField,
75
+ labelText : 'Name',
76
+ listeners: {
77
+ change: 'onChange'
78
+ }
79
+ }]
80
+ }
81
+ }
82
+ Neo.applyClassConfig(MainView);
83
+ </pre>
84
+
85
+ (It's important to keep in mind that in Neo.mjs, all class definitions are coded in their own
86
+ source file: one class per file. In the examples we're putting all the relevant classes together
87
+ to make it easier to see the source code for every class being used. But in an
88
+ actual applications the controller class would be coded in its own source file &mdash; named something
89
+ like `MainViewController.mjs` &mdash; and that would be imported into the view.)
90
+
91
+ The ability to fire events and add listeners is provided by `Neo.core.Observable`, which is mixed into
92
+ classes that need that ability. All components are observable, `Neo.data.Store` is observable, and some
93
+ others. `Neo.core.Observable` introduces a few methods and properties, such as `listeners`, which
94
+ is used in the examples above, `on()` for procedurally adding an event listener, and `fire()`, which is
95
+ how you fire events in the custom classes you create.
96
+
97
+ Here's example illustrating how `fire()` is used. The code defines a `ToggleButton`
98
+ class, which is just a button with a `checked` property: the button shows a checked or unchecked
99
+ checkbox depending on the value of `checked`.
100
+
101
+ The code uses a special Neo.mjs feature you haven't seen yet &mdash; the use of an underscore property.
102
+ We'll discuss that at length later, but in a nutshell, config properties ending in an underscore
103
+ automatically get lifecycle methods run before the value is assigned, after the value is assigned, and
104
+ before the value is accessed. We're using the _after_ method to fire a `change` event.
105
+
106
+ <pre data-neo>
107
+ import Base from '../../../../src/container/Base.mjs';
108
+ import Button from '../../../../src/button/Base.mjs';
109
+ import TextField from '../../../../src/form/field/Text.mjs';
110
+
111
+ class ToggleButton extends Button {
112
+ static config = {
113
+ className: 'Example.view.ToggleButton',
114
+ checked_: false
115
+ }
116
+ afterSetChecked(checked){
117
+ this.iconCls = checked?'fa fa-square-check':'fa fa-square';
118
+ this.fire('change', {component: this, checked}); // This is where our custom event is being fired
119
+ }
120
+ onClick(data){
121
+ super.onClick(data);
122
+ this.checked = !this.checked;
123
+ }
124
+ }
125
+ Neo.applyClassConfig(ToggleButton);
126
+
127
+
128
+ class MainView extends Base {
129
+ static config = {
130
+ className : 'Example.view.MainView',
131
+ layout: {ntype:'vbox', align:'start'},
132
+ items : [{
133
+ module: ToggleButton,
134
+ text: 'Toggle',
135
+ listeners: {
136
+ change: data => console.log(data.checked) // Here, we're listening to the custom event
137
+ }
138
+ }]
139
+ }
140
+ }
141
+ Neo.applyClassConfig(MainView);
142
+ </pre>
143
+
144
+ How are events set up? We don't really care, but in case you're curious: Neo.mjs has a `Neo.core.Observable` class
145
+ that can be mixed into any class. It maintains a `listeners` object map that's a key-value pair, where
146
+ the key is the event name, and the value is an array of function references. The first time a listener is
147
+ added an entry is added to the map using the event name as the key, and the event handler added as the first
148
+ item in the associated array. If another listener is added for the same event, a second item is added to the
149
+ array. If a new event is added, a new entry is added. Etc. When the event is fired, Neo.mjs looks up the map
150
+ entry for the event name, then runs each function in the array, passing whatever data is specified in the
151
+ call to `fire()`.
152
+
153
+ <img style="width:80%" src="https://s3.amazonaws.com/mjs.neo.learning.images/gettingStarted/events/ObservableInMemory.png"></img>
@@ -12,11 +12,13 @@
12
12
  {"name": "Tutorials", "parentId": null, "isLeaf": false, "expanded": false, "id": "Tutorials"},
13
13
  {"name": "Rock Scissors Paper", "parentId": "Tutorials", "isLeaf": true, "expanded": false, "id": "RSP"},
14
14
  {"name": "Earthquakes", "parentId": "Tutorials", "isLeaf": true, "expanded": false, "id": "Earthquakes"},
15
- {"name": "Cookbook", "parentId": null, "isLeaf": false, "expanded": false, "id": "InDepth"},
15
+ {"name": "Guides", "parentId": null, "isLeaf": false, "expanded": false, "id": "InDepth"},
16
16
  {"name": "Config", "parentId": "InDepth", "isLeaf": false, "id": "Config"},
17
17
  {"name": "Instance Lifecycle", "parentId": "InDepth", "isLeaf": false, "id": "InstanceLifecycle"},
18
18
  {"name": "User Input (Forms)", "parentId": "InDepth", "isLeaf": false, "id": "Forms"},
19
+ {"name": "Layouts", "parentId": "InDepth", "isLeaf": false, "id": "Layouts"},
19
20
  {"name": "Custom Components", "parentId": "InDepth", "isLeaf": false, "id": "CustomComponents"},
21
+ {"name": "Events", "parentId": "InDepth", "isLeaf": true, "expanded": false, "id": "GuideEvents"},
20
22
  {"name": "Tables (Stores)", "parentId": "InDepth", "isLeaf": false, "id": "Tables"},
21
23
  {"name": "Shared Bindable Data (Component Models)", "parentId": "InDepth", "isLeaf": false, "id": "InDepthComponentModels"},
22
24
  {"name": "Multi-Window Applications", "parentId": "InDepth", "isLeaf": false, "id": "MultiWindow"},
@@ -76,5 +76,4 @@
76
76
  .lab:hover {
77
77
  box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
78
78
  }
79
-
80
79
  }
@@ -101,6 +101,14 @@
101
101
  background-color: var(--list-item-background-color-active);
102
102
  color : var(--list-container-list-color);
103
103
  }
104
+ &:active {
105
+ background-color: var(--list-item-background-color-active);
106
+ color : var(--list-container-list-color);
107
+ }
108
+ &:active {
109
+ background-color: var(--list-item-background-color-active);
110
+ color : var(--list-container-list-color);
111
+ }
104
112
  }
105
113
 
106
114
  &.neo-navigator-active-item {
@@ -236,12 +236,12 @@ const DefaultConfig = {
236
236
  useVdomWorker: true,
237
237
  /**
238
238
  * buildScripts/injectPackageVersion.mjs will update this value
239
- * @default '6.10.11'
239
+ * @default '6.10.13'
240
240
  * @memberOf! module:Neo
241
241
  * @name config.version
242
242
  * @type String
243
243
  */
244
- version: '6.10.11'
244
+ version: '6.10.13'
245
245
  };
246
246
 
247
247
  Object.assign(DefaultConfig, {
@@ -37,6 +37,10 @@ class DateField extends Picker {
37
37
  * @member {String} errorTextInvalidDate='Not a valid date'
38
38
  */
39
39
  errorTextInvalidDate: 'Not a valid date',
40
+ /**
41
+ * @member {Boolean} isoDate=false
42
+ */
43
+ isoDate: false,
40
44
  /**
41
45
  * True to hide the DatePicker when selecting a day
42
46
  * @member {Boolean} hidePickerOnSelect=false
@@ -158,6 +162,17 @@ class DateField extends Picker {
158
162
  }
159
163
  }
160
164
 
165
+ /**
166
+ * Triggered before the value config got changed
167
+ * @param {String} value
168
+ * @param {String} oldValue
169
+ * @protected
170
+ */
171
+ beforeSetValue(value, oldValue) {
172
+ const val = super.beforeSetValue(value, oldValue);
173
+ return (this.isoDate && val) ? val.substring(0, 10) : val;
174
+ }
175
+
161
176
  /**
162
177
  * @returns {Neo.component.DateSelector}
163
178
  */
@@ -171,7 +186,13 @@ class DateField extends Picker {
171
186
  getValue() {
172
187
  let value = this.value;
173
188
 
174
- return this.submitDateObject && value ? new Date(`${value}T00:00:00.000Z`) : value
189
+ if(this.submitDateObject && value) {
190
+ return new Date(`${value}T00:00:00.000Z`);
191
+ } else if(this.isoDate && value) {
192
+ return new Date(value).toISOString();
193
+ }
194
+
195
+ return value;
175
196
  }
176
197
 
177
198
  /**
@@ -31,7 +31,7 @@ export default class DomUtils extends Base {
31
31
  * @param {HTMLElement} el The element to start from.
32
32
  * @param {Function} filterFn A function which returns `true` when the desired element is reached.
33
33
  * @param {HTMLElement} [limit] The element to stop at. This is *not* considered for matching.
34
- * @returns
34
+ * @returns {Boolean}
35
35
  */
36
36
  static closest(el, filterFn, limit = document.body) {
37
37
  while (el?.nodeType === Node.ELEMENT_NODE && el !== limit) {
@@ -220,7 +220,6 @@ class Navigator extends Base {
220
220
  * @param {String|Number} newActiveElement The id of the new active element in the subject
221
221
  * element, or the index of the item.
222
222
  * @param {Object} data The data block as passed to {@link #subscribe}
223
- * @returns
224
223
  */
225
224
  navigateTo(newActiveElement, data) {
226
225
  if (!data.subject) {
@@ -233,12 +232,17 @@ class Navigator extends Base {
233
232
  // Can navigate by index. This is useful if the active item is deleted.
234
233
  // We can navigate to the same index and preserve UI stability.
235
234
  if (typeof newActiveElement === 'number') {
236
- newActiveElement = data.subject.querySelectorAll(data.selector)[newActiveElement];
235
+ newActiveElement = data.subject.querySelectorAll(data.selector)?.[newActiveElement];
237
236
  }
238
237
  else if (typeof newActiveElement === 'string') {
239
238
  newActiveElement = DomAccess.getElement(newActiveElement);
240
239
  }
241
240
 
241
+ // Could not do what was asked because we could not find the requested item
242
+ if (!newActiveElement) {
243
+ return;
244
+ }
245
+
242
246
  // Find a focusable element which may be the item, or inside the item to draw focus to.
243
247
  // For example a Chip list in which .neo-list-items contain focusable Chips.
244
248
  const focusTarget = [newActiveElement, ...newActiveElement.querySelectorAll('*')].find(DomUtils.isFocusable);
@@ -261,14 +261,18 @@ class Base extends Container {
261
261
  // If it's an internal move within the delegate, do nothing
262
262
  if (currentTarget !== me.activeTarget?.id) {
263
263
  me.activeTarget = Neo.get(currentTarget);
264
- me.align.target = currentTarget;
265
- me.align.targetMargin = 10;
266
264
 
265
+ // Allow listeners (eg the Tooltip singleton) which is shared between all Components
266
+ // listens for this in order to reconfigure itself from the activeTarget.
267
+ // So this event must be fired before the alignment is set up.
267
268
  me.fire('targetOver', {
268
269
  target : me.activeTarget,
269
270
  data
270
271
  });
271
272
 
273
+ me.align.target = currentTarget;
274
+ me.align.targetMargin = 10;
275
+
272
276
  // Still visible, just realign
273
277
  if (me.mounted) {
274
278
  me.show();
@@ -16,7 +16,8 @@ class StringUtil extends Base {
16
16
  '"' : '&quot;',
17
17
  '\'': '&apos;',
18
18
  '$' : '&dollar;',
19
- '\\': '&bsol;'
19
+ '\\': '&bsol;',
20
+ '/' : '&sol;'
20
21
  }
21
22
  /**
22
23
  * @member {RegExp} charPattern
@@ -27,7 +28,7 @@ class StringUtil extends Base {
27
28
  * @member {RegExp} entityPattern
28
29
  * @static
29
30
  */
30
- static entityPattern = /(&amp;)|(&lt;)|(&gt;)|(&quot;)|(&apos;)|(&dollar;)|(&bsol;)/g
31
+ static entityPattern = /(&amp;)|(&lt;)|(&gt;)|(&quot;)|(&apos;)|(&dollar;)|(&bsol;)|(&sol;)/g
31
32
 
32
33
  static config = {
33
34
  /**
@@ -84,8 +85,8 @@ class StringUtil extends Base {
84
85
 
85
86
  /**
86
87
  * Returns the passed string with the first letter uncapitalized.
87
- * @param {Strinhg} value
88
- * @returns
88
+ * @param {String} value
89
+ * @returns {String}
89
90
  */
90
91
  static uncapitalize(value) {
91
92
  return value && value[0].toLowerCase() + value.substring(1)