neo.mjs 4.0.51 → 4.0.54

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,6 +67,28 @@ 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',
@@ -71,6 +111,7 @@ if (programOpts.info) {
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
116
  classFolder, file, folderDelta, index, ns, root, rootLowerCase, viewFile;
76
117
 
@@ -78,36 +119,97 @@ if (programOpts.info) {
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}));
201
+ fs.writeFileSync(path.join(classFolder, file + '.mjs'), createContent({baseClass, className, file, folderDelta, ns, root}));
96
202
 
97
- if (baseClass === 'controller.Component') {
98
- index = file.indexOf('Controller');
203
+ if (baseClass === 'controller.Component') {
204
+ index = file.indexOf('Controller');
99
205
 
100
- if (index > 0) {
101
- viewFile = path.join(classFolder, file.substr(0, index) + '.mjs');
206
+ if (index > 0) {
207
+ viewFile = path.join(classFolder, file.substr(0, index) + '.mjs');
102
208
 
103
- if (fs.existsSync(viewFile)) {
104
- adjustView({file, viewFile});
105
- }
209
+ if (fs.existsSync(viewFile)) {
210
+ adjustView({file, viewFile});
106
211
  }
107
212
  }
108
- } else {
109
- console.log('\nNon existing neo app name:', chalk.red(root));
110
- process.exit(1);
111
213
  }
112
214
  }
113
215
 
@@ -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,
@@ -129,11 +151,13 @@ class MainContainer extends ConfigurationViewport {
129
151
 
130
152
  createExampleComponent() {
131
153
  return Neo.create(TextField, {
132
- clearable : true,
133
- labelText : 'Label',
134
- labelWidth: 70,
135
- value : 'Hello World',
136
- width : 200
154
+ clearable : true,
155
+ labelPosition: 'inline',
156
+ labelText : 'Label',
157
+ labelWidth : 70,
158
+ minLength : 3,
159
+ value : 'Hello World',
160
+ width : 200
137
161
  });
138
162
  }
139
163
  }
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "4.0.51",
3
+ "version": "4.0.54",
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,31 @@
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:not(.neo-disabled) {
16
+ .neo-input-wrapper {
17
+ border-color: v(textfield-border-color-invalid);
18
+ }
19
+
20
+ .neo-label-wrapper {
21
+ .neo-center-border, .neo-left-border, .neo-right-border {
22
+ border-bottom-color: v(textfield-border-color-invalid);
23
+ }
24
+
25
+ .neo-left-border, .neo-right-border {
26
+ border-top-color: v(textfield-border-color-invalid);
27
+ }
28
+
29
+ .neo-left-border {
30
+ border-left-color: v(textfield-border-color-invalid);
31
+ }
32
+
33
+ .neo-right-border {
34
+ border-right-color: v(textfield-border-color-invalid);
35
+ }
12
36
  }
13
37
  }
14
38
 
@@ -36,7 +60,7 @@
36
60
  }
37
61
 
38
62
  .neo-center-border {
39
- border-top-color: transparent;
63
+ border-top-color: transparent !important;
40
64
  }
41
65
 
42
66
  .neo-left-border {
@@ -70,7 +94,7 @@
70
94
  }
71
95
 
72
96
  .neo-input-wrapper {
73
- border-color: transparent;
97
+ border-color: transparent !important;
74
98
  }
75
99
 
76
100
  .neo-label-wrapper {
@@ -156,10 +180,6 @@
156
180
  margin : 0; // important for Safari => #1125
157
181
  min-height : 25px;
158
182
  width : 30px;
159
-
160
- &:invalid {
161
- border: 1px solid brown;
162
- }
163
183
  }
164
184
  }
165
185
 
@@ -179,14 +199,18 @@
179
199
  outline : none;
180
200
  }
181
201
 
182
- &:invalid {
183
- border-color: brown;
184
- }
185
-
186
202
  &::-webkit-input-placeholder {
187
203
  color : v(textfield-input-placeholder-color) !important;
188
204
  opacity: v(textfield-input-placeholder-opacity) !important;
189
205
  }
206
+
207
+ &.neo-invalid {
208
+ border-color: v(textfield-border-color-invalid);
209
+
210
+ &.neo-disabled {
211
+ border-color: inherit;
212
+ }
213
+ }
190
214
  }
191
215
 
192
216
  .neo-textfield-label {
@@ -1,6 +1,7 @@
1
1
  $neoMap: map-merge($neoMap, (
2
2
  'textfield-border-color' : #424242,
3
3
  'textfield-border-color-active' : #5d83a7,
4
+ 'textfield-border-color-invalid' : brown,
4
5
  'textfield-border-radius' : 0,
5
6
  'textfield-input-background-color' : #2b2b2b,
6
7
  'textfield-input-color' : #ccc,
@@ -13,6 +14,7 @@ $neoMap: map-merge($neoMap, (
13
14
  :root .neo-theme-dark { // .neo-textfield
14
15
  --textfield-border-color : #{neo(textfield-border-color)};
15
16
  --textfield-border-color-active : #{neo(textfield-border-color-active)};
17
+ --textfield-border-color-invalid : #{neo(textfield-border-color-invalid)};
16
18
  --textfield-border-radius : #{neo(textfield-border-radius)};
17
19
  --textfield-input-background-color : #{neo(textfield-input-background-color)};
18
20
  --textfield-input-color : #{neo(textfield-input-color)};
@@ -20,4 +22,4 @@ $neoMap: map-merge($neoMap, (
20
22
  --textfield-input-placeholder-opacity: #{neo(textfield-input-placeholder-opacity)};
21
23
  --textfield-label-color : #{neo(textfield-label-color)};
22
24
  }
23
- }
25
+ }
@@ -1,6 +1,7 @@
1
1
  $neoMap: map-merge($neoMap, (
2
2
  'textfield-border-color' : #ddd,
3
3
  'textfield-border-color-active' : #1c60a0,
4
+ 'textfield-border-color-invalid' : brown,
4
5
  'textfield-border-radius' : 3px,
5
6
  'textfield-input-background-color' : #fff,
6
7
  'textfield-input-color' : #000,
@@ -13,6 +14,7 @@ $neoMap: map-merge($neoMap, (
13
14
  :root .neo-theme-light { // .neo-textfield
14
15
  --textfield-border-color : #{neo(textfield-border-color)};
15
16
  --textfield-border-color-active : #{neo(textfield-border-color-active)};
17
+ --textfield-border-color-invalid : #{neo(textfield-border-color-invalid)};
16
18
  --textfield-border-radius : #{neo(textfield-border-radius)};
17
19
  --textfield-input-background-color : #{neo(textfield-input-background-color)};
18
20
  --textfield-input-color : #{neo(textfield-input-color)};
@@ -20,4 +22,4 @@ $neoMap: map-merge($neoMap, (
20
22
  --textfield-input-placeholder-opacity: #{neo(textfield-input-placeholder-opacity)};
21
23
  --textfield-label-color : #{neo(textfield-label-color)};
22
24
  }
23
- }
25
+ }
@@ -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,19 @@ 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
+ nullable = field.nullable,
235
+ oldValue = recordConfig?.[field.name] || record[field.name],
236
+ type = field.type?.toLowerCase();
232
237
 
233
238
  // only trigger mappings for initial values
234
239
  // dynamic changes of a field will not pass the recordConfig
@@ -240,7 +245,28 @@ class RecordFactory extends Base {
240
245
  value = ns[key];
241
246
  }
242
247
 
243
- if (type === 'date') {
248
+ if (Object.hasOwn(field, 'maxLength')) {
249
+ if (value?.toString() > maxLength) {
250
+ console.warn(`Setting record field: ${field} value: ${value} conflicts with maxLength: ${maxLength}`);
251
+ return oldValue;
252
+ }
253
+ }
254
+
255
+ if (Object.hasOwn(field, 'minLength')) {
256
+ if (value?.toString() < minLength) {
257
+ console.warn(`Setting record field: ${field} value: ${value} conflicts with minLength: ${minLength}`);
258
+ return oldValue;
259
+ }
260
+ }
261
+
262
+ if (Object.hasOwn(field, 'nullable')) {
263
+ if (nullable === false && value === null) {
264
+ console.warn(`Setting record field: ${field} value: ${value} conflicts with nullable: ${nullable}`);
265
+ return oldValue;
266
+ }
267
+ }
268
+
269
+ if (type === 'date' && Neo.typeOf(value) !== 'Date') {
244
270
  return new Date(value);
245
271
  }
246
272
 
@@ -259,6 +285,7 @@ class RecordFactory extends Base {
259
285
 
260
286
  Object.entries(fields).forEach(([key, value]) => {
261
287
  oldValue = record[key];
288
+ value = instance.parseRecordValue(record, model.getField(key), value);
262
289
 
263
290
  if (!Neo.isEqual(oldValue, value)) {
264
291
  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
 
@@ -178,14 +180,17 @@ class Number extends Text {
178
180
  * @returns {Boolean}
179
181
  */
180
182
  isValid() {
181
- let me = this,
182
- value = me.value;
183
+ let me = this,
184
+ maxValue = me.maxValue,
185
+ minValue = me.minValue,
186
+ value = me.value,
187
+ isNumber = Neo.isNumber(value);
183
188
 
184
- if (Neo.isNumber(me.maxValue) && value > me.maxValue) {
189
+ if (Neo.isNumber(maxValue) && isNumber && value > maxValue) {
185
190
  return false;
186
191
  }
187
192
 
188
- if (Neo.isNumber(me.minValue) && value < me.minValue) {
193
+ if (Neo.isNumber(minValue) && isNumber && value < minValue) {
189
194
  return false;
190
195
  }
191
196
 
@@ -237,12 +242,13 @@ class Number extends Text {
237
242
  */
238
243
  onSpinButtonDownClick() {
239
244
  let me = this,
240
- oldValue = me.value || (me.maxValue + me.stepSize),
241
- 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);
242
248
 
243
249
  if (me.excludedValues) {
244
250
  while(me.excludedValues.includes(value)) {
245
- value = Math.max(me.minValue, value - me.stepSize);
251
+ value = Math.max(me.minValue, value - stepSize);
246
252
  }
247
253
  }
248
254
 
@@ -256,12 +262,13 @@ class Number extends Text {
256
262
  */
257
263
  onSpinButtonUpClick() {
258
264
  let me = this,
259
- oldValue = me.value || (me.minValue - me.stepSize),
260
- 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);
261
268
 
262
269
  if (me.excludedValues) {
263
270
  while(me.excludedValues.includes(value)) {
264
- value = Math.min(me.maxValue, value + me.stepSize);
271
+ value = Math.min(me.maxValue, value + stepSize);
265
272
  }
266
273
  }
267
274
 
@@ -375,6 +375,7 @@ class Text extends Base {
375
375
  * @protected
376
376
  */
377
377
  afterSetMaxLength(value, oldValue) {
378
+ this.updateValidationIndicators();
378
379
  this.changeInputElKey('maxlength', value);
379
380
  }
380
381
 
@@ -385,6 +386,7 @@ class Text extends Base {
385
386
  * @protected
386
387
  */
387
388
  afterSetMinLength(value, oldValue) {
389
+ this.updateValidationIndicators();
388
390
  this.changeInputElKey('minlength', value);
389
391
  }
390
392
 
@@ -437,6 +439,7 @@ class Text extends Base {
437
439
  * @protected
438
440
  */
439
441
  afterSetRequired(value, oldValue) {
442
+ this.updateValidationIndicators();
440
443
  this.changeInputElKey('required', value ? value : null);
441
444
  }
442
445
 
@@ -532,6 +535,7 @@ class Text extends Base {
532
535
  }
533
536
 
534
537
  NeoArray[me.originalConfig.value !== value ? 'add' : 'remove'](me._cls, 'neo-is-dirty');
538
+ me.updateValidationIndicators();
535
539
 
536
540
  me.vdom = vdom;
537
541
 
@@ -1011,6 +1015,20 @@ class Text extends Base {
1011
1015
  });
1012
1016
  });
1013
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
+ }
1014
1032
  }
1015
1033
 
1016
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