-
- Name is required.
-
-
- Name can not have more than 40 characters.
-
-
- Name can contain lower case letters (a-z), numbers and dashes (not at the end).
-
-
-
+
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
index b358dcbb3..7d691444b 100644
--- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
+++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
@@ -24,7 +24,8 @@ import {
SchemaDetailsDto,
SchemasService,
UpdateFieldDto,
- UsersProviderService
+ UsersProviderService,
+ ValidatorsEx
} from 'shared';
import { SchemaPropertiesDto } from './schema-properties';
@@ -63,7 +64,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
[
Validators.required,
Validators.maxLength(40),
- Validators.pattern('[a-z0-9]+(\-[a-z0-9]+)*')
+ ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'Name can contain lower case letters (a-z), numbers and dashes (not at the end).')
]]
});
@@ -203,6 +204,10 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
}
}
+ public resetFieldForm() {
+ this.addFieldForm.reset();
+ }
+
public onSchemaSaved(properties: SchemaPropertiesDto) {
this.updateProperties(properties);
diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html
index 497dc8037..68bb2a0f0 100644
--- a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html
+++ b/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html
@@ -3,13 +3,7 @@
-
-
-
- Placeholder can not have more than 100 characters.
-
-
-
- Name is required.
-
-
- Name can not have more than 40 characters.
-
-
- Name can contain lower case letters (a-z), numbers and dashes only (not at the end).
-
-
-
+
-
- The schema name becomes part of the api url, e.g https://{{appName}}.squidex.io/{{schemaName | async}}/.
-
-
- It must contain lower case letters (a-z), numbers and dashes only, and cannot be longer than 40 characters. The name cannot be changed later.
-
+ The schema name becomes part of the api url, e.g https://{{appName}}.squidex.io/{{schemaName | async}}/.
+
+
+ It must contain lower case letters (a-z), numbers and dashes only, and cannot be longer than 40 characters. The name cannot be changed later.
diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
index 8765e1c20..b37411fe4 100644
--- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
+++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
@@ -15,7 +15,8 @@ import {
DateTime,
fadeAnimation,
SchemaDto,
- SchemasService
+ SchemasService,
+ ValidatorsEx
} from 'shared';
const FALLBACK_NAME = 'my-schema';
@@ -45,7 +46,7 @@ export class SchemaFormComponent {
[
Validators.required,
Validators.maxLength(40),
- Validators.pattern('[a-z0-9]+(\-[a-z0-9]+)*')
+ ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'Name can contain lower case letters (a-z), numbers and dashes only (not at the end).')
]]
});
diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.html b/src/Squidex/app/features/settings/pages/clients/client.component.html
index d7f39d1b3..e364e32d2 100644
--- a/src/Squidex/app/features/settings/pages/clients/client.component.html
+++ b/src/Squidex/app/features/settings/pages/clients/client.component.html
@@ -15,13 +15,7 @@
diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts b/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts
index d05620c56..3f4430260 100644
--- a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts
+++ b/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts
@@ -19,7 +19,8 @@ import {
MessageBus,
NotificationService,
UpdateAppClientDto,
- UsersProviderService
+ UsersProviderService,
+ ValidatorsEx
} from 'shared';
function rename(client: AppClientDto, name: string): AppClientDto {
@@ -40,7 +41,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
[
Validators.required,
Validators.maxLength(40),
- Validators.pattern('[a-z0-9]+(\-[a-z0-9]+)*')
+ ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'Name can contain lower case letters (a-z), numbers and dashes (not at the end).')
]]
});
@@ -88,6 +89,10 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
});
}
+ public resetClientForm() {
+ this.addClientForm.reset();
+ }
+
public attachClient() {
this.addClientForm.markAsDirty();
diff --git a/src/Squidex/app/framework/angular/control-errors.component.html b/src/Squidex/app/framework/angular/control-errors.component.html
new file mode 100644
index 000000000..351a7e126
--- /dev/null
+++ b/src/Squidex/app/framework/angular/control-errors.component.html
@@ -0,0 +1,7 @@
+
+
+
+ {{message}}
+
+
+
\ No newline at end of file
diff --git a/src/Squidex/app/framework/angular/control-errors.component.scss b/src/Squidex/app/framework/angular/control-errors.component.scss
new file mode 100644
index 000000000..d1951ab29
--- /dev/null
+++ b/src/Squidex/app/framework/angular/control-errors.component.scss
@@ -0,0 +1,2 @@
+@import '_mixins';
+@import '_vars';
\ No newline at end of file
diff --git a/src/Squidex/app/framework/angular/control-errors.component.ts b/src/Squidex/app/framework/angular/control-errors.component.ts
new file mode 100644
index 000000000..d2c7a9d68
--- /dev/null
+++ b/src/Squidex/app/framework/angular/control-errors.component.ts
@@ -0,0 +1,118 @@
+/*
+ * Squidex Headless CMS
+ *
+ * @license
+ * Copyright (c) Sebastian Stehle. All rights reserved
+ */
+
+import { ChangeDetectorRef, ChangeDetectionStrategy, Component, Host, Input, OnChanges, OnInit, OnDestroy, Optional } from '@angular/core';
+import { AbstractControl, FormGroupDirective } from '@angular/forms';
+import { Subscription } from 'rxjs';
+
+import { fadeAnimation } from './animations';
+
+const DEFAULT_ERRORS: { [key: string]: string } = {
+ required: '{field} is required.',
+ pattern: '{field} does not follow the pattern.',
+ patternMessage: '{message}',
+ minValue: '{field} must be larget than {minValue}.',
+ maxValue: '{field} must be larget than {maxValue}.',
+ minLength: '{field} must have more than {minLength} characters.',
+ maxLength: '{field} cannot have more than {maxLength} characters.',
+ validNumber: '{field} is not a valid number.',
+ validValues: '{field} is not a valid value.'
+};
+
+@Component({
+ selector: 'sqx-control-errors',
+ styleUrls: ['./control-errors.component.scss'],
+ templateUrl: './control-errors.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ animations: [
+ fadeAnimation
+ ]
+})
+export class ControlErrorsComponent implements OnChanges, OnInit, OnDestroy {
+ private formSubscription: Subscription;
+ private displayFieldName: string;
+ private control: AbstractControl;
+
+ @Input()
+ public for: string;
+
+ @Input()
+ public fieldName: string;
+
+ @Input()
+ public errors: string;
+
+ @Input()
+ public submitted: boolean;
+
+ public errorMessages: string[];
+
+ constructor(@Optional() @Host() private readonly form: FormGroupDirective,
+ private readonly changeDetector: ChangeDetectorRef
+ ) {
+ if (!this.form) {
+ throw new Error('control-errors must be used with a parent formGroup directive');
+ }
+ }
+
+ public ngOnChanges() {
+ if (this.fieldName) {
+ this.displayFieldName = this.fieldName;
+ } else if (this.for) {
+ this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1);
+ }
+
+ this.update();
+ }
+
+ public ngOnDestroy() {
+ this.formSubscription.unsubscribe();
+ }
+
+ public ngOnInit() {
+ this.control = this.form.form.controls[this.for];
+
+ this.formSubscription =
+ this.form.form.statusChanges.merge(this.control.statusChanges)
+ .subscribe(_ => {
+ this.update();
+ });
+ }
+
+ private update() {
+ if (!this.control) {
+ return;
+ }
+
+ if (this.control.invalid && (this.control.touched || this.form.form.touched)) {
+ const errors: string[] = [];
+
+ for (let key in this.control.errors) {
+ if (this.control.errors.hasOwnProperty(key)) {
+ let message: string = (this.errors ? this.errors[key] : null) || DEFAULT_ERRORS[key];
+ let properties = this.control.errors[key];
+
+ for (let property in properties) {
+ if (properties.hasOwnProperty(property)) {
+ message = message.replace('{' + property + '}', properties[property]);
+ }
+ }
+
+ message = message.replace('{field}', this.displayFieldName);
+
+ errors.push(message);
+ }
+ }
+
+ this.errorMessages = errors.length > 0 ? errors : null;
+ } else {
+ this.errorMessages = null;
+ }
+
+ this.changeDetector.markForCheck();
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex/app/framework/angular/validators.spec.ts b/src/Squidex/app/framework/angular/validators.spec.ts
index 7a0bced37..342710ac1 100644
--- a/src/Squidex/app/framework/angular/validators.spec.ts
+++ b/src/Squidex/app/framework/angular/validators.spec.ts
@@ -7,13 +7,13 @@
import { FormControl } from '@angular/forms';
-import { Validators } from './../';
+import { ValidatorsEx } from './../';
describe('Validators', () => {
let validateBetween: any;
beforeEach(() => {
- validateBetween = Validators.between(10, 200);
+ validateBetween = ValidatorsEx.between(10, 200);
});
it('should return error when not a number', () => {
diff --git a/src/Squidex/app/framework/angular/validators.ts b/src/Squidex/app/framework/angular/validators.ts
index c4d8d6a4f..fc0fa095f 100644
--- a/src/Squidex/app/framework/angular/validators.ts
+++ b/src/Squidex/app/framework/angular/validators.ts
@@ -5,19 +5,66 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
-import { AbstractControl } from '@angular/forms';
+import { AbstractControl, ValidatorFn, Validators } from '@angular/forms';
-export class Validators {
- public static between(minValue: number, maxValue: number) {
+export class ValidatorsEx {
+ public static pattern(pattern: string | RegExp, message: string | undefined = undefined): ValidatorFn {
+ if (!pattern) {
+ return Validators.nullValidator;
+ }
+
+ let regex: RegExp;
+ let regexStr: string;
+
+ if (typeof pattern === 'string') {
+ regexStr = `^${pattern}$`;
+ regex = new RegExp(regexStr);
+ } else {
+ regexStr = pattern.toString();
+ regex = pattern;
+ }
+
+ return (control: AbstractControl): { [key: string]: any } => {
+ const n: string = control.value;
+
+ if (n == null || n.length === 0) {
+ return null;
+ }
+
+ if (!regex.test(n)) {
+ if (message) {
+ return { patternMessage: { requiredPattern: regexStr, actualValue: n, message } };
+ } else {
+ return { pattern: { requiredPattern: regexStr, actualValue: n } };
+ }
+ }
+
+ return null;
+ };
+ }
+
+ public static between(minValue: number | undefined, maxValue: number | undefined) {
return (control: AbstractControl): { [key: string]: any } => {
const n: number = control.value;
if (typeof n !== 'number') {
- return { 'validNumber': false };
- } else if (n < minValue) {
- return { 'minValue': { 'requiredValue': minValue, 'actualValue': n } };
- } else if (n > maxValue) {
- return { 'maxValue': { 'requiredValue': maxValue, 'actualValue': n } };
+ return { validNumber: false };
+ } else if (minValue && n < minValue) {
+ return { minValue: { minValue, actualValue: n } };
+ } else if (maxValue && n > maxValue) {
+ return { maxValue: { maxValue, actualValue: n } };
+ }
+
+ return {};
+ };
+ }
+
+ public static validValues(values: T[]) {
+ return (control: AbstractControl): { [key: string]: any } => {
+ const n: T = control.value;
+
+ if (values.indexOf(n) < 0) {
+ return { validValues: false };
}
return {};
diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts
index 8168d1601..0a443319d 100644
--- a/src/Squidex/app/framework/declarations.ts
+++ b/src/Squidex/app/framework/declarations.ts
@@ -9,6 +9,7 @@ export * from './angular/animations';
export * from './angular/autocomplete.component';
export * from './angular/validators';
export * from './angular/cloak.directive';
+export * from './angular/control-errors.component';
export * from './angular/copy.directive';
export * from './angular/date-time.pipes';
export * from './angular/focus-on-change.directive';
diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts
index 277aa31e3..fa8a5a97f 100644
--- a/src/Squidex/app/framework/module.ts
+++ b/src/Squidex/app/framework/module.ts
@@ -15,6 +15,7 @@ import {
AutocompleteComponent,
ClipboardService,
CloakDirective,
+ ControlErrorsComponent,
CopyDirective,
DayOfWeekPipe,
DayPipe,
@@ -55,6 +56,7 @@ import {
declarations: [
AutocompleteComponent,
CloakDirective,
+ ControlErrorsComponent,
CopyDirective,
DayOfWeekPipe,
DayPipe,
@@ -80,6 +82,7 @@ import {
exports: [
AutocompleteComponent,
CloakDirective,
+ ControlErrorsComponent,
CopyDirective,
DayOfWeekPipe,
DayPipe,
diff --git a/src/Squidex/app/shared/components/app-form.component.html b/src/Squidex/app/shared/components/app-form.component.html
index 4484d2088..9cd1a7a5f 100644
--- a/src/Squidex/app/shared/components/app-form.component.html
+++ b/src/Squidex/app/shared/components/app-form.component.html
@@ -8,29 +8,16 @@
-
-
-
- Name is required.
-
-
- Name can not have more than 40 characters.
-
-
- Name can contain lower case letters (a-z), numbers and dashes only (not at the end).
-
-
-
+
-
- The app name becomes part of the api url, e.g https://{{appName | async}}.squidex.io/.
-
-
- It must contain lower case letters (a-z), numbers and dashes only, and cannot be longer than 40 characters. The name cannot be changed later.
-
+ The app name becomes part of the api url, e.g https://{{appName | async}}.squidex.io/.
+
+
+
+ It must contain lower case letters (a-z), numbers and dashes only, and cannot be longer than 40 characters. The name cannot be changed later.
diff --git a/src/Squidex/app/shared/components/app-form.component.ts b/src/Squidex/app/shared/components/app-form.component.ts
index 7f5b2aec3..b1157483b 100644
--- a/src/Squidex/app/shared/components/app-form.component.ts
+++ b/src/Squidex/app/shared/components/app-form.component.ts
@@ -9,7 +9,7 @@ import { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
-import { fadeAnimation } from 'framework';
+import { ValidatorsEx } from 'framework';
import { AppsStoreService } from './../services/apps-store.service';
import { AppDto, CreateAppDto } from './../services/apps.service';
@@ -19,10 +19,7 @@ const FALLBACK_NAME = 'my-app';
@Component({
selector: 'sqx-app-form',
styleUrls: ['./app-form.component.scss'],
- templateUrl: './app-form.component.html',
- animations: [
- fadeAnimation
- ]
+ templateUrl: './app-form.component.html'
})
export class AppFormComponent {
@Output()
@@ -38,7 +35,7 @@ export class AppFormComponent {
[
Validators.required,
Validators.maxLength(40),
- Validators.pattern('[a-z0-9]+(\-[a-z0-9]+)*')
+ ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'Name can contain lower case letters (a-z), numbers and dashes (not at the end).')
]]
});