neo.mjs 6.38.0 → 6.40.0

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.
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='6.38.0'
23
+ * @member {String} version='6.40.0'
24
24
  */
25
- version: '6.38.0'
25
+ version: '6.40.0'
26
26
  }
27
27
 
28
28
  /**
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='6.38.0'
23
+ * @member {String} version='6.40.0'
24
24
  */
25
- version: '6.38.0'
25
+ version: '6.40.0'
26
26
  }
27
27
 
28
28
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "6.38.0",
3
+ "version": "6.40.0",
4
4
  "description": "The webworkers driven UI framework",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,265 @@
1
+ ## Forms include a State-Provider
2
+
3
+ You do not need to define a state tree on your own.
4
+ It if sufficient to just use namespaces inside the `name` attribute of each field.
5
+
6
+ <pre data-neo>
7
+ import Button from '../button/Base.mjs';
8
+ import FormContainer from '../form/Container.mjs';
9
+ import TextField from '../form/field/Text.mjs';
10
+
11
+ class MainView extends FormContainer {
12
+ static config = {
13
+ className: 'Example.view.MainView',
14
+ layout : {ntype:'vbox', align:'start'},
15
+
16
+ items: [{
17
+ module : TextField,
18
+ labelText: 'Firstname',
19
+ name : 'user.firstname',
20
+ value : 'John'
21
+ }, {
22
+ module : TextField,
23
+ labelText: 'Lastname',
24
+ name : 'user.lastname',
25
+ value : 'Doe'
26
+ }, {
27
+ module : Button,
28
+ handler: 'up.getFormValues',
29
+ style : {marginTop: '1em'},
30
+ text : 'Get Form Values'
31
+ }]
32
+ }
33
+
34
+ async getFormValues(data) {
35
+ const formValues = await this.getValues();
36
+ // Logs {user: {firstname: 'John', lastname: 'Doe'}}
37
+ Neo.Main.log({value: formValues})
38
+ }
39
+ }
40
+ Neo.setupClass(MainView);
41
+ </pre>
42
+
43
+ ## Forms can get validated without being mounted
44
+
45
+ Neo.mjs Forms live as a pure abstraction layer inside JavaScript.
46
+ The following example is similar to the first one, but this time the form instance will not get mounted.
47
+ Getting the field values still works like before.
48
+
49
+ Use case: In case you have a form split into multiple pages and only one of them is mounted to keep
50
+ the DOM minimal, you can still get all field values.
51
+
52
+ <pre data-neo>
53
+ import Button from '../button/Base.mjs';
54
+ import Container from '../container/Base.mjs';
55
+ import FormContainer from '../form/Container.mjs';
56
+ import TextField from '../form/field/Text.mjs';
57
+
58
+ const myForm = Neo.create({
59
+ module: FormContainer,
60
+ items : [{
61
+ module : TextField,
62
+ labelText: 'Firstname',
63
+ name : 'user.firstname',
64
+ value : 'John'
65
+ }, {
66
+ module : TextField,
67
+ labelText: 'Lastname',
68
+ name : 'user.lastname',
69
+ value : 'Doe'
70
+ }]
71
+ });
72
+
73
+ class MainView extends Container {
74
+ static config = {
75
+ className: 'Example.view.MainView',
76
+ layout : {ntype:'vbox', align:'start'},
77
+
78
+ items: [{
79
+ module : Button,
80
+ handler: 'up.getFormValues',
81
+ text : 'Get Form Values'
82
+ }]
83
+ }
84
+
85
+ async getFormValues(data) {
86
+ const formValues = await myForm.getValues();
87
+ // Logs {user: {firstname: 'John', lastname: 'Doe'}}
88
+ Neo.Main.log({value: formValues})
89
+ }
90
+ }
91
+ Neo.setupClass(MainView);
92
+ </pre>
93
+
94
+ ## Nested Forms
95
+
96
+ Inside the DOM, it is impossible to nest form tags.
97
+ Neo forms can get easily mapped to other nodes:</br>
98
+ `{module: FormContainer, tag: 'div'}`
99
+
100
+ This allows us to nest forms and validate or retrieve form values of each nested form or
101
+ for the top-level form (including all nested items) as we see fit.
102
+
103
+ Inside the example preview, clear the user lastname via hitting the x-button.
104
+ Afterwards, click on the 3 buttons at the bottom and inspect the output inside the main window console carefully.
105
+
106
+ The main form will log:
107
+ <pre data-javascript>
108
+ {
109
+ account: 'My Account',
110
+ product: {brand: 'Tesla', name: 'Car'},
111
+ user : {firstname: 'John', lastname: null}
112
+ }
113
+ 'isValid: false'
114
+ </pre>
115
+
116
+ The user form will log:
117
+ <pre data-javascript>
118
+ {user: {firstname: 'John', lastname: null}}
119
+ 'isValid: false'
120
+ </pre>
121
+
122
+ The product form will log:
123
+ <pre data-javascript>
124
+ {product: {brand: 'Tesla', name: 'Car'}}
125
+ 'isValid: true'
126
+ </pre>
127
+
128
+ <pre data-neo>
129
+ import Button from '../button/Base.mjs';
130
+ import Container from '../container/Base.mjs';
131
+ import FormContainer from '../form/Container.mjs';
132
+ import TabContainer from '../tab/Container.mjs';
133
+ import TextField from '../form/field/Text.mjs';
134
+
135
+ class MainView extends FormContainer {
136
+ static config = {
137
+ className: 'Example.view.MainView',
138
+ layout : {ntype:'vbox', align:'stretch'},
139
+
140
+ items: [{
141
+ module : TextField,
142
+ flex : 'none',
143
+ labelPosition: 'inline',
144
+ labelText : 'Account',
145
+ name : 'account',
146
+ value : 'My Account'
147
+ }, {
148
+ module: TabContainer,
149
+ items : [{
150
+ module : FormContainer,
151
+ itemDefaults : {module: TextField, labelPosition: 'inline'},
152
+ layout : {ntype:'vbox', align:'start'},
153
+ reference : 'user-form',
154
+ tabButtonConfig: {text: 'User'},
155
+ tag : 'div',
156
+
157
+ items: [{
158
+ labelText: 'Firstname',
159
+ name : 'user.firstname',
160
+ value : 'John'
161
+ }, {
162
+ labelText: 'Lastname',
163
+ name : 'user.lastname',
164
+ required : true,
165
+ value : 'Doe'
166
+ }]
167
+ }, {
168
+ module : FormContainer,
169
+ itemDefaults : {module: TextField, labelPosition: 'inline'},
170
+ layout : {ntype:'vbox', align:'start'},
171
+ reference : 'product-form',
172
+ tabButtonConfig: {text: 'Product'},
173
+ tag : 'div',
174
+
175
+ items: [{
176
+ labelText: 'Name',
177
+ name : 'product.name',
178
+ value : 'Car'
179
+ }, {
180
+ labelText: 'Brand',
181
+ name : 'product.brand',
182
+ required : true,
183
+ value : 'Tesla'
184
+ }]
185
+ }]
186
+ }, {
187
+ module : Container,
188
+ flex : 'none',
189
+ itemDefaults: {module: Button},
190
+ layout : {ntype: 'hbox'},
191
+
192
+ items : [{
193
+ handler: 'up.getMainFormValues',
194
+ text : 'Get Main Values'
195
+ }, {
196
+ handler: 'up.getUserFormValues',
197
+ text : 'Get User Values'
198
+ }, {
199
+ handler: 'up.getProductFormValues',
200
+ text : 'Get Product Values'
201
+ }]
202
+ }]
203
+ }
204
+
205
+ async getMainFormValues(data) {
206
+ const formValues = await this.getValues();
207
+ Neo.Main.log({value: formValues});
208
+
209
+ const isValid = await this.validate();
210
+ Neo.Main.log({value: `isValid: ${isValid}`});
211
+ }
212
+
213
+ async getProductFormValues(data) {
214
+ const
215
+ form = this.getReference('product-form'),
216
+ formValues = await form.getValues();
217
+
218
+ Neo.Main.log({value: formValues});
219
+
220
+ const isValid = await form.validate();
221
+ Neo.Main.log({value: `isValid: ${isValid}`});
222
+ }
223
+
224
+ async getUserFormValues(data) {
225
+ const
226
+ form = this.getReference('user-form'),
227
+ formValues = await form.getValues();
228
+
229
+ Neo.Main.log({value: formValues});
230
+
231
+ const isValid = await form.validate();
232
+ Neo.Main.log({value: `isValid: ${isValid}`});
233
+ }
234
+ }
235
+ Neo.setupClass(MainView);
236
+ </pre>
237
+
238
+ Bonus: Inspect the DOM Inside the `TabContainer`.
239
+ You will notice that only the active Tab is mounted inside the DOM.
240
+
241
+ 1. We can still get field values of unmounted forms
242
+ 2. We can still validate unmounted forms
243
+
244
+ ## Nested lazy-loaded Forms
245
+
246
+ If you look close into the `Button` handlers of the last example:
247
+ `getValues()` and `validate()` are both async.
248
+
249
+ The reason for this is that `form.getFields()` itself is async as well:
250
+ It will lazy-load (but not necessarily mount) missing fields when needed.
251
+
252
+ The lazy-loading use case is not easy to display inside the `LivePreview`,
253
+ since it does rely on defining child modules inside their own class files
254
+ and dynamically importing them.
255
+
256
+ In a nutshell:
257
+ <pre data-javascript>
258
+ {
259
+ module: TabContainer,
260
+ items : [
261
+ {module: () => import('./MyChildForm1')},
262
+ {module: () => import('./MyChildForm2')}
263
+ ]
264
+ }
265
+ </pre>
@@ -7,6 +7,7 @@
7
7
  {"name": "Multi-Window Applications", "parentId": "WhyNeo", "isLeaf": true, "id": "WhyNeo-Multi-Window"},
8
8
  {"name": "Quick Application Development", "parentId": "WhyNeo", "isLeaf": true, "id": "WhyNeo-Quick"},
9
9
  {"name": "Complexity and Effort", "parentId": "WhyNeo", "isLeaf": true, "id": "WhyNeo-Effort"},
10
+ {"name": "Forms Engine", "parentId": "WhyNeo", "isLeaf": true, "id": "benefits.FormsEngine"},
10
11
  {"name": "Features and Benefits Summary", "parentId": "WhyNeo", "isLeaf": true, "id": "WhyNeo-Features"},
11
12
  {"name": "Getting Started", "parentId": null, "isLeaf": false, "id": "GettingStarted", "collapsed": true},
12
13
  {"name": "Setup", "parentId": "GettingStarted", "isLeaf": true, "id": "Setup"},
@@ -95,6 +95,10 @@
95
95
  pointer-events: none;
96
96
  text-transform: var(--button-text-transform);
97
97
  z-index : 2;
98
+
99
+ @media (max-width: 600px) {
100
+ font-size: 13px;
101
+ }
98
102
  }
99
103
 
100
104
  &:active {
@@ -260,12 +260,12 @@ const DefaultConfig = {
260
260
  useVdomWorker: true,
261
261
  /**
262
262
  * buildScripts/injectPackageVersion.mjs will update this value
263
- * @default '6.38.0'
263
+ * @default '6.40.0'
264
264
  * @memberOf! module:Neo
265
265
  * @name config.version
266
266
  * @type String
267
267
  */
268
- version: '6.38.0'
268
+ version: '6.40.0'
269
269
  };
270
270
 
271
271
  Object.assign(DefaultConfig, {
@@ -23,6 +23,11 @@ class HighlightJS extends Base {
23
23
  * @protected
24
24
  */
25
25
  highlightJsLineNumbersPath: Neo.config.basePath + 'node_modules/highlightjs-line-numbers.js/dist/highlightjs-line-numbers.min.js',
26
+ /**
27
+ * @member {Boolean} libraryLoaded_=true
28
+ * @protected
29
+ */
30
+ libraryLoaded_: false,
26
31
  /**
27
32
  * Remote method access for other workers
28
33
  * @member {Object} remote
@@ -46,6 +51,43 @@ class HighlightJS extends Base {
46
51
  themePath: './resources/highlightjs-custom-github-theme.css'
47
52
  }
48
53
 
54
+ /**
55
+ * @member {Object[]} cache=[]
56
+ * @protected
57
+ */
58
+ cache = []
59
+
60
+ /**
61
+ * Triggered after the libraryLoaded config got changed
62
+ * @param {Boolean} value
63
+ * @param {Boolean} oldValue
64
+ * @protected
65
+ */
66
+ afterSetLibraryLoaded(value, oldValue) {
67
+ if (value) {
68
+ let me = this,
69
+ returnValue;
70
+
71
+ me.cache.forEach(item => {
72
+ returnValue = me[item.fn](item.data);
73
+ item.resolve(returnValue)
74
+ });
75
+
76
+ me.cache = []
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Internally caches call when the hljs namespace does not exist yet
82
+ * @param item
83
+ * @returns {Promise<unknown>}
84
+ */
85
+ cacheMethodCall(item) {
86
+ return new Promise((resolve, reject) => {
87
+ this.cache.push({...item, resolve})
88
+ })
89
+ }
90
+
49
91
  /**
50
92
  * See: https://highlightjs.readthedocs.io/en/latest/api.html#highlightauto
51
93
  * @param {Object} data
@@ -53,10 +95,10 @@ class HighlightJS extends Base {
53
95
  * @returns {Object} of the form {language, relevance, value, secondBest}
54
96
  */
55
97
  highlightAuto(data) {
56
- if (hljs) {
98
+ if (window.hljs) {
57
99
  return hljs.highlightAuto(data.html)
58
100
  } else {
59
- console.error('highlight.js is not included inside the main thread.')
101
+ return this.cacheMethodCall({fn: 'highlightAuto', data})
60
102
  }
61
103
  }
62
104
 
@@ -77,6 +119,8 @@ class HighlightJS extends Base {
77
119
 
78
120
  Neo.main.addon.Stylesheet.createStyleSheet(null, 'hljs-theme', me.themePath);
79
121
 
122
+ this.libraryLoaded = true;
123
+
80
124
  return true
81
125
  }
82
126
 
@@ -118,13 +162,15 @@ class HighlightJS extends Base {
118
162
  * @param {String} data.vnodeId
119
163
  */
120
164
  syntaxHighlight(data) {
121
- if (hljs) {
165
+ if (window.hljs) {
122
166
  let node = document.getElementById(data.vnodeId);
123
167
 
124
- hljs.highlightBlock(node);
125
- hljs.lineNumbersBlock(node)
168
+ if (node) {
169
+ hljs.highlightBlock(node);
170
+ hljs.lineNumbersBlock(node)
171
+ }
126
172
  } else {
127
- console.error('highlight.js is not included inside the main thread.')
173
+ return this.cacheMethodCall({fn: 'syntaxHighlight', data})
128
174
  }
129
175
  }
130
176
 
@@ -132,11 +178,11 @@ class HighlightJS extends Base {
132
178
  * @param {Object} data
133
179
  */
134
180
  syntaxHighlightInit(data) {
135
- if (hljs) {
181
+ if (window.hljs) {
136
182
  let blocks = document.querySelectorAll('pre code:not(.hljs)');
137
183
  Array.prototype.forEach.call(blocks, hljs.highlightBlock)
138
184
  } else {
139
- console.error('highlight.js is not included inside the main thread.')
185
+ return this.cacheMethodCall({fn: 'syntaxHighlightInit', data})
140
186
  }
141
187
  }
142
188
 
@@ -147,6 +193,10 @@ class HighlightJS extends Base {
147
193
  * @param {Number} data.removeLine
148
194
  */
149
195
  syntaxHighlightLine(data) {
196
+ if (!window.hljs) {
197
+ return this.cacheMethodCall({fn: 'syntaxHighlightLine', data})
198
+ }
199
+
150
200
  let parentEl = document.getElementById(data.vnodeId),
151
201
  cls = 'neo-highlighted-line',
152
202
  el;