neo.mjs 4.0.50 → 4.0.53

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.
@@ -1,24 +1,42 @@
1
- import chalk from 'chalk';
2
- import { Command } from 'commander/esm.mjs';
3
- import envinfo from 'envinfo';
4
- import fs from 'fs-extra';
5
- import inquirer from 'inquirer';
6
- import os from 'os';
7
- import path from 'path';
8
-
9
- const __dirname = path.resolve(),
1
+ #!/usr/bin/env node
2
+
3
+ import chalk from 'chalk';
4
+ import { Command } from 'commander/esm.mjs';
5
+ import envinfo from 'envinfo';
6
+ import fs from 'fs-extra';
7
+ import inquirer from 'inquirer';
8
+ import os from 'os';
9
+ import path from 'path';
10
+ import {fileURLToPath} from 'url';
11
+
12
+ const
13
+ __dirname = fileURLToPath(new URL('../', import.meta.url)),
10
14
  cwd = process.cwd(),
11
15
  requireJson = path => JSON.parse(fs.readFileSync((path))),
12
16
  packageJson = requireJson(path.join(__dirname, 'package.json')),
13
17
  insideNeo = packageJson.name === 'neo.mjs',
14
18
  program = new Command(),
15
19
  programName = `${packageJson.name} create-class`,
16
- questions = [];
20
+ questions = [],
21
+ /**
22
+ * Maintain a list of dir-names recognized as source root directories.
23
+ * When not using dot notation with a class-name, the program assumes
24
+ * that we want to create the class inside the cwd. The proper namespace
25
+ * is then looked up by traversing the directory path up to the first
26
+ * folder that matches an entry in "sourceRootDirs". The owning
27
+ * folder (parent of cwd, child of sourceRootDirs[n]) will then be used as the
28
+ * namespace for this created class.
29
+ * Can be overwritten with the -s option.
30
+ * @type {string[]}
31
+ */
32
+ sourceRootDirs = ['apps'];
17
33
 
18
34
  program
19
35
  .name(programName)
20
36
  .version(packageJson.version)
21
37
  .option('-i, --info', 'print environment debug info')
38
+ .option('-d, --drop', 'drops class in the currently selected folder')
39
+ .option('-s, --source <value>', `name of the folder containing the project. Defaults to any of ${sourceRootDirs.join(',')}`)
22
40
  .option('-b, --baseClass <value>')
23
41
  .option('-c, --className <value>')
24
42
  .allowUnknownOption()
@@ -33,7 +51,7 @@ const programOpts = program.opts();
33
51
  if (programOpts.info) {
34
52
  console.log(chalk.bold('\nEnvironment Info:'));
35
53
  console.log(`\n current version of ${packageJson.name}: ${packageJson.version}`);
36
- console.log(` running from ${__dirname}`);
54
+ console.log(` running from ${cwd}`);
37
55
 
38
56
  envinfo
39
57
  .run({
@@ -49,12 +67,34 @@ if (programOpts.info) {
49
67
  } else {
50
68
  console.log(chalk.green(programName));
51
69
 
70
+ if (programOpts.drop) {
71
+ // change source folder if the user wants to
72
+ if (programOpts.source) {
73
+ while (sourceRootDirs.length) {
74
+ sourceRootDirs.pop();
75
+ }
76
+ sourceRootDirs.push(programOpts.source);
77
+ }
78
+
79
+ if (!programOpts.className || !programOpts.baseClass) {
80
+ console.error(chalk.red('-d is non interactive. Please provide name base class, and optionally the source parent for the class to create'));
81
+ console.info(chalk.bgCyan('Usage: createClass -d -c <className> -b <baseClass> [-s sourceParent]'));
82
+ process.exit(1);
83
+ }
84
+
85
+ if (programOpts.className.indexOf('.') !== -1) {
86
+ console.error(chalk.red('No .dot-notation available when -d option is selected.'));
87
+ console.info(chalk.bgCyan('Usage: createClass -d -c <className> -b <baseClass> [-s sourceParent]'));
88
+ process.exit(1);
89
+ }
90
+ }
91
+
52
92
  if (!programOpts.className) {
53
93
  questions.push({
54
94
  type : 'input',
55
95
  name : 'className',
56
96
  message: 'Please choose the namespace for your class:',
57
- default: 'Covid.view.FooContainer'
97
+ default: 'Covid.view.HeaderContainerController'
58
98
  });
59
99
  }
60
100
 
@@ -63,39 +103,113 @@ if (programOpts.info) {
63
103
  type : 'list',
64
104
  name : 'baseClass',
65
105
  message: 'Please pick the base class, which you want to extend:',
66
- choices: ['component.Base', 'container.Base'],
67
- default: 'component.Base'
106
+ choices: ['component.Base', 'container.Base', 'controller.Component', 'core.Base'],
107
+ default: 'container.Base'
68
108
  });
69
109
  }
70
110
 
71
111
  inquirer.prompt(questions).then(answers => {
72
112
  let baseClass = programOpts.baseClass || answers.baseClass,
73
113
  className = programOpts.className || answers.className,
114
+ isDrop = programOpts.drop,
74
115
  startDate = new Date(),
75
- classFolder, file, folderDelta, ns, root, rootLowerCase;
116
+ classFolder, file, folderDelta, index, ns, root, rootLowerCase, viewFile;
76
117
 
77
118
  if (className.endsWith('.mjs')) {
78
119
  className = className.slice(0, -4);
79
120
  }
80
121
 
81
- ns = className.split('.');
82
- file = ns.pop();
83
- root = ns.shift();
84
- rootLowerCase = root.toLowerCase();
122
+ if (!isDrop) {
123
+ ns = className.split('.');
124
+ file = ns.pop();
125
+ root = ns.shift();
126
+ rootLowerCase = root.toLowerCase();
127
+ }
85
128
 
86
129
  if (root === 'Neo') {
87
130
  console.log('todo: create the file inside the src folder');
88
131
  } else {
89
- if (fs.existsSync(path.resolve(cwd, 'apps', rootLowerCase))) {
90
- classFolder = path.resolve(cwd, 'apps', rootLowerCase, ns.join('/'));
132
+ if (isDrop === true) {
133
+ ns = [];
134
+
135
+ let pathInfo = path.parse(cwd),
136
+ sep = path.sep,
137
+ baseName, loc = baseName = '',
138
+ tmpNs;
139
+
140
+ sourceRootDirs.some(dir => {
141
+ loc = cwd;
142
+ tmpNs = [];
143
+
144
+ while (pathInfo.root !== loc) {
145
+ baseName = path.resolve(loc, './').split(sep).pop();
146
+
147
+ if (baseName === dir) {
148
+ ns = tmpNs.reverse();
149
+ classFolder = path.resolve(loc, ns.join(sep));
150
+ file = className;
151
+ className = ns.concat(className).join('.');
152
+ loc = path.resolve(loc, ns.join(sep));
153
+ return true;
154
+ }
155
+
156
+ tmpNs.push(baseName);
157
+ loc = path.resolve(loc, '../');
158
+ }
159
+ });
160
+
161
+ if (!ns.length) {
162
+ console.error(chalk.red(
163
+ 'Could not determine namespace for application. Did you provide the ' +
164
+ `correct source parent with -s? (was: ${sourceRootDirs.join(',')}`));
165
+ process.exit(1);
166
+ }
167
+
168
+ console.info(
169
+ chalk.yellow(`Creating ${chalk.bgGreen(className)} extending ${chalk.bgGreen(baseClass)} in ${loc}${sep}${file}.mjs`)
170
+ );
171
+
172
+ let delta_l = path.normalize(__dirname),
173
+ delta_r = path.normalize(loc);
174
+
175
+ if (delta_r.indexOf(delta_l) !== 0) {
176
+ console.error(chalk.red(`Could not determine ${loc} being a child of ${__dirname}`));
177
+ process.exit(1);
178
+ }
179
+
180
+ let delta = delta_r.replace(delta_l, ''),
181
+ parts = delta.split(sep);
182
+
183
+ folderDelta = parts.length;
184
+ }
185
+
186
+ if (isDrop !== true) {
187
+ if (fs.existsSync(path.resolve(__dirname, 'apps', rootLowerCase))) {
188
+ classFolder = path.resolve(__dirname, 'apps', rootLowerCase, ns.join('/'));
189
+ } else {
190
+ console.log('\nNon existing neo app name:', chalk.red(root));
191
+ process.exit(1);
192
+ }
193
+ }
194
+
195
+ if (folderDelta === undefined) {
91
196
  folderDelta = ns.length + 2;
197
+ }
92
198
 
93
- fs.mkdirpSync(classFolder);
199
+ fs.mkdirpSync(classFolder);
94
200
 
95
- fs.writeFileSync(path.join(classFolder, file + '.mjs'), createContent({baseClass, className, file, folderDelta, ns, root}));
96
- } else {
97
- console.log('\nNon existing neo app name:', chalk.red(root));
98
- process.exit(1);
201
+ fs.writeFileSync(path.join(classFolder, file + '.mjs'), createContent({baseClass, className, file, folderDelta, ns, root}));
202
+
203
+ if (baseClass === 'controller.Component') {
204
+ index = file.indexOf('Controller');
205
+
206
+ if (index > 0) {
207
+ viewFile = path.join(classFolder, file.substr(0, index) + '.mjs');
208
+
209
+ if (fs.existsSync(viewFile)) {
210
+ adjustView({file, viewFile});
211
+ }
212
+ }
99
213
  }
100
214
  }
101
215
 
@@ -108,9 +222,84 @@ if (programOpts.info) {
108
222
  /**
109
223
  * Adds a comma to the last element of the contentArray
110
224
  * @param {String[]} contentArray
225
+ * @returns {String[]}
111
226
  */
112
227
  function addComma(contentArray) {
113
228
  contentArray[contentArray.length - 1] += ',';
229
+ return contentArray;
230
+ }
231
+
232
+ /**
233
+ * Adjusts the views related to controller.Component or model.Component
234
+ * @param {Object} opts
235
+ * @param {String} opts.file
236
+ * @param {String} opts.viewFile
237
+ */
238
+ function adjustView(opts) {
239
+ let file = opts.file,
240
+ viewFile = opts.viewFile,
241
+ content = fs.readFileSync(viewFile).toString().split(os.EOL),
242
+ fromMaxPosition = 0,
243
+ i = 0,
244
+ len = content.length,
245
+ adjustSpaces, codeLine, fromPosition, importLength, importName, j, spaces;
246
+
247
+ // find the index where we want to insert our import statement
248
+ for (; i < len; i++) {
249
+ codeLine = content[i];
250
+
251
+ if (codeLine === '') {
252
+ break;
253
+ }
254
+
255
+ importName = codeLine.substr(7);
256
+ importName = importName.substr(0, importName.indexOf(' '));
257
+ importLength = importName.length;
258
+
259
+ if (importName > file) {
260
+ break;
261
+ }
262
+ }
263
+
264
+ content.splice(i, 0, `import ${file} from './${file}.mjs';`);
265
+
266
+ // find the longest import module name
267
+ for (i=0; i < len; i++) {
268
+ codeLine = content[i];
269
+
270
+ if (codeLine === '') {
271
+ break;
272
+ }
273
+
274
+ fromMaxPosition = Math.max(fromMaxPosition, codeLine.indexOf('from'));
275
+ }
276
+
277
+ // adjust the block-formatting for imports
278
+ for (i=0; i < len; i++) {
279
+ codeLine = content[i];
280
+
281
+ if (codeLine === '') {
282
+ break;
283
+ }
284
+
285
+ fromPosition = codeLine.indexOf('from');
286
+ adjustSpaces = fromMaxPosition - fromPosition;
287
+
288
+ if (adjustSpaces > 0) {
289
+ spaces = '';
290
+
291
+ for (j=0; j < adjustSpaces; j++) {
292
+ spaces += ' ';
293
+ }
294
+
295
+ content[i] = codeLine.substr(0, fromPosition) + spaces + codeLine.substr(fromPosition);
296
+ }
297
+ }
298
+
299
+ fs.writeFileSync(viewFile, content.join(os.EOL));
300
+
301
+ console.log(i, opts.file);
302
+ console.log(content);
114
303
  }
115
304
 
116
305
  /**
@@ -125,13 +314,13 @@ if (programOpts.info) {
125
314
  * @returns {String}
126
315
  */
127
316
  function createContent(opts) {
128
- let baseClass = opts.baseClass,
129
- baseClassNs = baseClass.split('.'),
317
+ let baseClass = opts.baseClass,
318
+ baseClassNs = baseClass.split('.'),
130
319
  baseFileName = baseClassNs.pop(),
131
- className = opts.className,
132
- file = opts.file,
133
- i = 0,
134
- importDelta = '';
320
+ className = opts.className,
321
+ file = opts.file,
322
+ i = 0,
323
+ importDelta = '';
135
324
 
136
325
  for (; i < opts.folderDelta; i++) {
137
326
  importDelta += '../';
@@ -141,8 +330,8 @@ if (programOpts.info) {
141
330
  `import ${baseFileName} from '${importDelta}${(insideNeo ? '' : 'node_modules/neo.mjs/')}src/${baseClassNs.join('/')}/${baseFileName}.mjs';`,
142
331
  "",
143
332
  "/**",
144
- " * @class " + className,
145
- " * @extends Neo." + baseClass,
333
+ ` * @class ${className}`,
334
+ ` * @extends Neo.${baseClass}`,
146
335
  " */",
147
336
  `class ${file} extends ${baseFileName} {`,
148
337
  " static getConfig() {return {",
@@ -153,28 +342,20 @@ if (programOpts.info) {
153
342
  ` className: '${className}'`
154
343
  ];
155
344
 
156
- if (baseClass === 'component.Base') {
157
- addComma(classContent);
158
-
159
- classContent.push(
345
+ baseClass === 'container.Base' && addComma(classContent).push(
160
346
  " /*",
161
- " * @member {Object} _vdom",
347
+ " * @member {Object[]} items",
162
348
  " */",
163
- " _vdom:",
164
- " {}",
165
- );
166
- }
167
-
168
- if (baseClass === 'container.Base') {
169
- addComma(classContent);
349
+ " items: []"
350
+ );
170
351
 
171
- classContent.push(
352
+ baseClass === 'component.Base' && addComma(classContent).push(
172
353
  " /*",
173
- " * @member {Object[]} items",
354
+ " * @member {Object} _vdom",
174
355
  " */",
175
- " items: []",
176
- );
177
- }
356
+ " _vdom:",
357
+ " {}"
358
+ );
178
359
 
179
360
  classContent.push(
180
361
  " }}",
@@ -36,6 +36,12 @@ class MainContainer extends ConfigurationViewport {
36
36
  labelText: 'clearToOriginalValue',
37
37
  listeners: {change: me.onConfigChange.bind(me, 'clearToOriginalValue')},
38
38
  style : {marginTop: '10px'}
39
+ }, {
40
+ module : CheckBox,
41
+ checked : me.exampleComponent.disabled,
42
+ labelText: 'disabled',
43
+ listeners: {change: me.onConfigChange.bind(me, 'disabled')},
44
+ style : {marginTop: '10px'}
39
45
  }, {
40
46
  module : CheckBox,
41
47
  checked : me.exampleComponent.hideLabel,
@@ -97,6 +103,22 @@ class MainContainer extends ConfigurationViewport {
97
103
  minValue : 50,
98
104
  stepSize : 5,
99
105
  value : me.exampleComponent.labelWidth
106
+ }, {
107
+ module : NumberField,
108
+ labelText: 'maxLength',
109
+ listeners: {change: me.onConfigChange.bind(me, 'maxLength')},
110
+ maxValue : 50,
111
+ minValue : 1,
112
+ stepSize : 1,
113
+ value : me.exampleComponent.maxLength
114
+ }, {
115
+ module : NumberField,
116
+ labelText: 'minLength',
117
+ listeners: {change: me.onConfigChange.bind(me, 'minLength')},
118
+ maxValue : 50,
119
+ minValue : 1,
120
+ stepSize : 1,
121
+ value : me.exampleComponent.minLength
100
122
  }, {
101
123
  module : TextField,
102
124
  clearable: true,
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "4.0.50",
3
+ "version": "4.0.53",
4
4
  "description": "The webworkers driven UI framework",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/neomjs/neo.git"
9
9
  },
10
+ "bin": {
11
+ "neo-cc": "./buildScripts/createClass.mjs"
12
+ },
10
13
  "scripts": {
11
14
  "build-all": "node ./buildScripts/buildAll.mjs -f -n",
12
15
  "build-all-questions": "node ./buildScripts/buildAll.mjs -f",
@@ -51,7 +54,7 @@
51
54
  "neo-jsdoc": "^1.0.1",
52
55
  "neo-jsdoc-x": "^1.0.4",
53
56
  "postcss": "^8.4.14",
54
- "sass": "^1.52.3",
57
+ "sass": "^1.53.0",
55
58
  "webpack": "^5.73.0",
56
59
  "webpack-cli": "^4.10.0",
57
60
  "webpack-dev-server": "4.9.2",
@@ -59,7 +62,8 @@
59
62
  "webpack-node-externals": "^3.0.0"
60
63
  },
61
64
  "devDependencies": {
62
- "siesta-lite": "^5.5.2"
65
+ "siesta-lite": "^5.5.2",
66
+ "url": "^0.11.0"
63
67
  },
64
68
  "funding": {
65
69
  "type": "GitHub Sponsors",
@@ -8,7 +8,19 @@
8
8
 
9
9
  &.neo-focus {
10
10
  .neo-input-wrapper {
11
- border-color: v(textfield-border-color-active);
11
+ border-color: v(textfield-border-color-active) !important;
12
+ }
13
+ }
14
+
15
+ &.neo-invalid {
16
+ .neo-input-wrapper {
17
+ border-color: brown;
18
+ }
19
+
20
+ &.neo-disabled {
21
+ .neo-input-wrapper {
22
+ border-color: inherit;
23
+ }
12
24
  }
13
25
  }
14
26
 
@@ -156,10 +168,6 @@
156
168
  margin : 0; // important for Safari => #1125
157
169
  min-height : 25px;
158
170
  width : 30px;
159
-
160
- &:invalid {
161
- border: 1px solid brown;
162
- }
163
171
  }
164
172
  }
165
173
 
@@ -179,14 +187,18 @@
179
187
  outline : none;
180
188
  }
181
189
 
182
- &:invalid {
183
- border-color: brown;
184
- }
185
-
186
190
  &::-webkit-input-placeholder {
187
191
  color : v(textfield-input-placeholder-color) !important;
188
192
  opacity: v(textfield-input-placeholder-opacity) !important;
189
193
  }
194
+
195
+ &.neo-invalid {
196
+ border-color: brown;
197
+
198
+ &.neo-disabled {
199
+ border-color: inherit;
200
+ }
201
+ }
190
202
  }
191
203
 
192
204
  .neo-textfield-label {
@@ -28,9 +28,9 @@ class RecordFactory extends Base {
28
28
  */
29
29
  ovPrefix: 'ov_',
30
30
  /**
31
- * @member {String} recordNamespace='Neo.data.record.'
31
+ * @member {String} recordNamespace='Neo.data.record'
32
32
  */
33
- recordNamespace: 'Neo.data.record.'
33
+ recordNamespace: 'Neo.data.record'
34
34
  }}
35
35
 
36
36
  /**
@@ -39,7 +39,7 @@ class RecordFactory extends Base {
39
39
  * @returns {Object}
40
40
  */
41
41
  createRecord(model, config) {
42
- let recordClass = Neo.ns(this.recordNamespace + model.className);
42
+ let recordClass = Neo.ns(`${this.recordNamespace}.${model.className}.${model.id}`);
43
43
 
44
44
  if (!recordClass) {
45
45
  recordClass = this.createRecordClass(model);
@@ -54,7 +54,7 @@ class RecordFactory extends Base {
54
54
  */
55
55
  createRecordClass(model) {
56
56
  if (model instanceof Model) {
57
- let className = this.recordNamespace + model.className,
57
+ let className = `${this.recordNamespace}.${model.className}.${model.id}`,
58
58
  ns = Neo.ns(className),
59
59
  key, nsArray;
60
60
 
@@ -76,7 +76,7 @@ class RecordFactory extends Base {
76
76
 
77
77
  if (Array.isArray(model.fields)) {
78
78
  model.fields.forEach(field => {
79
- let parsedValue = instance.parseRecordValue(field, config[field.name], config),
79
+ let parsedValue = instance.parseRecordValue(me, field, config[field.name], config),
80
80
  symbol = Symbol.for(field.name);
81
81
 
82
82
  properties = {
@@ -97,9 +97,9 @@ class RecordFactory extends Base {
97
97
  let me = this,
98
98
  oldValue = me[symbol];
99
99
 
100
- if (!Neo.isEqual(value, oldValue)) {
101
- value = instance.parseRecordValue(field, value, null);
100
+ value = instance.parseRecordValue(me, field, value);
102
101
 
102
+ if (!Neo.isEqual(value, oldValue)) {
103
103
  me[symbol] = value;
104
104
 
105
105
  me._isModified = true;
@@ -221,14 +221,18 @@ class RecordFactory extends Base {
221
221
 
222
222
  /**
223
223
  * todo: parse value for more field types
224
+ * @param {Object} record
224
225
  * @param {Object} field
225
226
  * @param {*} value
226
- * @param {Object} recordConfig
227
+ * @param {Object} recordConfig=null
227
228
  * @returns {*}
228
229
  */
229
- parseRecordValue(field, value, recordConfig) {
230
- let mapping = field.mapping,
231
- type = field.type?.toLowerCase();
230
+ parseRecordValue(record, field, value, recordConfig=null) {
231
+ let mapping = field.mapping,
232
+ maxLength = field.maxLength,
233
+ minLength = field.minLength,
234
+ oldValue = recordConfig?.[field.name] || record[field.name],
235
+ type = field.type?.toLowerCase();
232
236
 
233
237
  // only trigger mappings for initial values
234
238
  // dynamic changes of a field will not pass the recordConfig
@@ -240,7 +244,21 @@ class RecordFactory extends Base {
240
244
  value = ns[key];
241
245
  }
242
246
 
243
- if (type === 'date') {
247
+ if (Object.hasOwn(field, maxLength)) {
248
+ if (value?.toString() > maxLength) {
249
+ console.warn(`Setting record field: ${field} value: ${value} conflicts with the maxLength: ${maxLength}`);
250
+ return oldValue;
251
+ }
252
+ }
253
+
254
+ if (Object.hasOwn(field, minLength)) {
255
+ if (value?.toString() < minLength) {
256
+ console.warn(`Setting record field: ${field} value: ${value} conflicts with the minLength: ${minLength}`);
257
+ return oldValue;
258
+ }
259
+ }
260
+
261
+ if (type === 'date' && Neo.typeOf(value) !== 'Date') {
244
262
  return new Date(value);
245
263
  }
246
264
 
@@ -259,6 +277,7 @@ class RecordFactory extends Base {
259
277
 
260
278
  Object.entries(fields).forEach(([key, value]) => {
261
279
  oldValue = record[key];
280
+ value = instance.parseRecordValue(record, model.getField(key), value);
262
281
 
263
282
  if (!Neo.isEqual(oldValue, value)) {
264
283
  record[Symbol.for(key)] = value; // silent update
@@ -99,6 +99,7 @@ class Number extends Text {
99
99
  * @protected
100
100
  */
101
101
  afterSetMaxValue(value, oldValue) {
102
+ this.updateValidationIndicators();
102
103
  this.changeInputElKey('max', value);
103
104
  }
104
105
 
@@ -109,6 +110,7 @@ class Number extends Text {
109
110
  * @protected
110
111
  */
111
112
  afterSetMinValue(value, oldValue) {
113
+ this.updateValidationIndicators();
112
114
  this.changeInputElKey('min', value);
113
115
  }
114
116
 
@@ -174,6 +176,31 @@ class Number extends Text {
174
176
  return this.beforeSetEnumValue(value, oldValue, 'triggerPosition');
175
177
  }
176
178
 
179
+ /**
180
+ * @returns {Boolean}
181
+ */
182
+ isValid() {
183
+ let me = this,
184
+ maxValue = me.maxValue,
185
+ minValue = me.minValue,
186
+ value = me.value,
187
+ isNumber = Neo.isNumber(value);
188
+
189
+ if (Neo.isNumber(maxValue) && isNumber && value > maxValue) {
190
+ return false;
191
+ }
192
+
193
+ if (Neo.isNumber(minValue) && isNumber && value < minValue) {
194
+ return false;
195
+ }
196
+
197
+ if (value % me.stepSize !== 0) {
198
+ return false;
199
+ }
200
+
201
+ return super.isValid();
202
+ }
203
+
177
204
  /**
178
205
  *
179
206
  */
@@ -215,12 +242,13 @@ class Number extends Text {
215
242
  */
216
243
  onSpinButtonDownClick() {
217
244
  let me = this,
218
- oldValue = me.value || (me.maxValue + me.stepSize),
219
- value = Math.max(me.minValue, oldValue - me.stepSize);
245
+ stepSize = me.stepSize,
246
+ oldValue = Neo.isNumber(me.value) ? me.value : me.minValue,
247
+ value = (oldValue - stepSize) < me.minValue ? me.maxValue : (oldValue - stepSize);
220
248
 
221
249
  if (me.excludedValues) {
222
250
  while(me.excludedValues.includes(value)) {
223
- value = Math.max(me.minValue, value - me.stepSize);
251
+ value = Math.max(me.minValue, value - stepSize);
224
252
  }
225
253
  }
226
254
 
@@ -234,12 +262,13 @@ class Number extends Text {
234
262
  */
235
263
  onSpinButtonUpClick() {
236
264
  let me = this,
237
- oldValue = me.value || (me.minValue - me.stepSize),
238
- value = Math.min(me.maxValue, oldValue + me.stepSize);
265
+ stepSize = me.stepSize,
266
+ oldValue = Neo.isNumber(me.value) ? me.value : me.maxValue,
267
+ value = (oldValue + stepSize) > me.maxValue ? me.minValue : (oldValue + stepSize);
239
268
 
240
269
  if (me.excludedValues) {
241
270
  while(me.excludedValues.includes(value)) {
242
- value = Math.min(me.maxValue, value + me.stepSize);
271
+ value = Math.min(me.maxValue, value + stepSize);
243
272
  }
244
273
  }
245
274
 
@@ -93,6 +93,16 @@ class Text extends Base {
93
93
  * @member {Number|String} labelWidth_=150
94
94
  */
95
95
  labelWidth_: 150,
96
+ /**
97
+ * The maximum amount of chars which you can enter into this field
98
+ * @member {Number|null} maxLength_=null
99
+ */
100
+ maxLength_: null,
101
+ /**
102
+ * The minimum amount of chars which you can enter into this field
103
+ * @member {Number|null} minLength_=null
104
+ */
105
+ minLength_: null,
96
106
  /**
97
107
  * @member {String|null} placeholderText_=null
98
108
  */
@@ -358,6 +368,28 @@ class Text extends Base {
358
368
  }
359
369
  }
360
370
 
371
+ /**
372
+ * Triggered after the maxLength config got changed
373
+ * @param {Number|null} value
374
+ * @param {Number|null} oldValue
375
+ * @protected
376
+ */
377
+ afterSetMaxLength(value, oldValue) {
378
+ this.updateValidationIndicators();
379
+ this.changeInputElKey('maxlength', value);
380
+ }
381
+
382
+ /**
383
+ * Triggered after the minLength config got changed
384
+ * @param {Number|null} value
385
+ * @param {Number|null} oldValue
386
+ * @protected
387
+ */
388
+ afterSetMinLength(value, oldValue) {
389
+ this.updateValidationIndicators();
390
+ this.changeInputElKey('minlength', value);
391
+ }
392
+
361
393
  /**
362
394
  * Triggered after the mounted config got changed
363
395
  * @param {Boolean} value
@@ -407,6 +439,7 @@ class Text extends Base {
407
439
  * @protected
408
440
  */
409
441
  afterSetRequired(value, oldValue) {
442
+ this.updateValidationIndicators();
410
443
  this.changeInputElKey('required', value ? value : null);
411
444
  }
412
445
 
@@ -502,6 +535,7 @@ class Text extends Base {
502
535
  }
503
536
 
504
537
  NeoArray[me.originalConfig.value !== value ? 'add' : 'remove'](me._cls, 'neo-is-dirty');
538
+ me.updateValidationIndicators();
505
539
 
506
540
  me.vdom = vdom;
507
541
 
@@ -776,9 +810,19 @@ class Text extends Base {
776
810
  * @returns {Boolean}
777
811
  */
778
812
  isValid() {
779
- let me = this;
813
+ let me = this,
814
+ value = me.value,
815
+ valueLength = value?.toString().length;
780
816
 
781
- if (me.required && (!me.value || me.value?.length < 1)) {
817
+ if (me.required && (!value || valueLength < 1)) {
818
+ return false;
819
+ }
820
+
821
+ if (Neo.isNumber(me.maxLength) && valueLength > me.maxLength) {
822
+ return false;
823
+ }
824
+
825
+ if (Neo.isNumber(me.minLength) && valueLength < me.minLength) {
782
826
  return false;
783
827
  }
784
828
 
@@ -971,6 +1015,20 @@ class Text extends Base {
971
1015
  });
972
1016
  });
973
1017
  }
1018
+
1019
+ /**
1020
+ * @param {Boolean} silent=true
1021
+ */
1022
+ updateValidationIndicators(silent=true) {
1023
+ let me = this,
1024
+ vdom = me.vdom;
1025
+
1026
+ NeoArray[!me.isValid() ? 'add' : 'remove'](me._cls, 'neo-invalid');
1027
+
1028
+ if (!silent) {
1029
+ me.vdom = vdom;
1030
+ }
1031
+ }
974
1032
  }
975
1033
 
976
1034
  Neo.applyClassConfig(Text);
@@ -186,11 +186,15 @@ class DeltaUpdates extends Base {
186
186
  }
187
187
  } else if (key === 'id') {
188
188
  node[Neo.config.useDomIds ? 'id' : 'data-neo-id'] = val;
189
- }else if (key === 'spellcheck' && val === 'false') {
189
+ } else if (key === 'spellcheck' && val === 'false') {
190
190
  // see https://github.com/neomjs/neo/issues/1922
191
191
  node[key] = false;
192
192
  } else {
193
- node[key] = val;
193
+ if (key === 'value') {
194
+ node[key] = val;
195
+ } else {
196
+ node.setAttribute(key, val);
197
+ }
194
198
  }
195
199
  });
196
200
  break;
@@ -247,9 +247,9 @@ class Container extends BaseContainer {
247
247
  }
248
248
 
249
249
  if (value) {
250
- let me = this;
250
+ let me = this,
251
251
 
252
- const listeners = {
252
+ listeners = {
253
253
  filter : me.onStoreFilter,
254
254
  load : me.onStoreLoad,
255
255
  recordChange: me.onStoreRecordChange,
@@ -258,13 +258,10 @@ class Container extends BaseContainer {
258
258
 
259
259
  if (value instanceof Store) {
260
260
  value.on(listeners);
261
-
262
- if (value.getCount() > 0) {
263
- me.onStoreLoad(value.items);
264
- }
261
+ value.getCount() > 0 && me.onStoreLoad(value.items);
265
262
  } else {
266
263
  value = ClassSystemUtil.beforeSetInstance(value, Store, {
267
- listeners: listeners
264
+ listeners
268
265
  });
269
266
  }
270
267