diff --git a/src/app/public/modules/checkbox/checkbox.component.ts b/src/app/public/modules/checkbox/checkbox.component.ts
index 45351ae2..02f6b97c 100644
--- a/src/app/public/modules/checkbox/checkbox.component.ts
+++ b/src/app/public/modules/checkbox/checkbox.component.ts
@@ -10,11 +10,14 @@ import {
} from '@angular/core';
import {
- AbstractControl,
ControlValueAccessor,
NgControl
} from '@angular/forms';
+import {
+ SkyFormsUtility
+} from '../shared/forms-utility';
+
/**
* Monotonically increasing integer used to auto-generate unique ids for checkbox components.
*/
@@ -101,7 +104,7 @@ export class SkyCheckboxComponent implements ControlValueAccessor, OnInit {
*/
@Input()
set required(value: boolean) {
- this._required = this.coerceBooleanProperty(value);
+ this._required = SkyFormsUtility.coerceBooleanProperty(value);
}
get required(): boolean {
@@ -127,7 +130,7 @@ export class SkyCheckboxComponent implements ControlValueAccessor, OnInit {
public ngOnInit(): void {
if (this.ngControl) {
// Backwards compatibility support for anyone still using Validators.Required.
- this.required = this.required || this.hasRequiredValidation(this.ngControl);
+ this.required = this.required || SkyFormsUtility.hasRequiredValidation(this.ngControl);
}
}
@@ -201,26 +204,4 @@ export class SkyCheckboxComponent implements ControlValueAccessor, OnInit {
this.checked = !this.checked;
}
- /**
- * Gets the required state of the checkbox.
- * Currently, Angular doesn't offer a way to get this easily, so we have to create an empty
- * control using the current validator to see if it throws a `required` validation error.
- * https://github.com/angular/angular/issues/13461#issuecomment-340368046
- */
- private hasRequiredValidation(ngControl: NgControl): boolean {
- const abstractControl = ngControl.control as AbstractControl;
- if (abstractControl && abstractControl.validator) {
- const validator = abstractControl.validator({} as AbstractControl);
- if (validator && validator.required) {
- return true;
- }
- }
- return false;
- }
-
- /** Coerces a data-bound value (typically a string) to a boolean. */
- private coerceBooleanProperty(value: any): boolean {
- return value !== undefined && `${value}` !== 'false';
- }
-
}
diff --git a/src/app/public/modules/file-attachment/file-attachment.component.html b/src/app/public/modules/file-attachment/file-attachment.component.html
index d1b56c75..f49cabf3 100644
--- a/src/app/public/modules/file-attachment/file-attachment.component.html
+++ b/src/app/public/modules/file-attachment/file-attachment.component.html
@@ -4,6 +4,7 @@
diff --git a/src/app/public/modules/file-attachment/file-attachment.component.spec.ts b/src/app/public/modules/file-attachment/file-attachment.component.spec.ts
index 77adc9b4..99f9eb1e 100644
--- a/src/app/public/modules/file-attachment/file-attachment.component.spec.ts
+++ b/src/app/public/modules/file-attachment/file-attachment.component.spec.ts
@@ -59,6 +59,7 @@ describe('File attachment', () => {
fileAttachmentInstance = fixture.componentInstance.fileAttachmentComponent;
});
+ //#region helpers
function getInputDebugEl(): DebugElement {
return fixture.debugElement.query(By.css('input'));
}
@@ -304,18 +305,55 @@ describe('File attachment', () => {
}
//#endregion
- it('should allow the user to specify if the file is required', fakeAsync(() => {
+ it('should not have required class and aria-reqiured attribute when not required', fakeAsync(() => {
fileAttachmentInstance.ngAfterViewInit();
tick();
fixture.detectChanges();
+ const labelWrapper = getLabelWrapper();
+ const input = getInputDebugEl();
+
+ expect(input.nativeElement.getAttribute('required')).toBeNull();
+ expect(labelWrapper.classList.contains('sky-control-label-required')).toBe(false);
+ expect(labelWrapper.getAttribute('aria-required')).toBeNull();
+ }));
- let labelWrapper = getLabelWrapper();
+ it('should have appropriate classes when file is required', fakeAsync(() => {
+ fixture.componentInstance.required = true;
+ fileAttachmentInstance.ngAfterViewInit();
+ tick();
+ fixture.detectChanges();
+ const labelWrapper = getLabelWrapper();
+ const input = getInputDebugEl();
+
+ expect(input.nativeElement.getAttribute('required')).not.toBeNull();
+ expect(labelWrapper.classList.contains('sky-control-label-required')).toBe(true);
+ expect(labelWrapper.getAttribute('aria-required')).toBe('true');
+ }));
+
+ it('should have appropriate classes when file is required and initialized with file', fakeAsync(() => {
+ fixture.componentInstance.required = true;
+ const testFile = {
+ file: {
+ name: 'myFile',
+ type: '',
+ size: 1
+ },
+ url: 'myFile'
+ };
+ fileAttachmentInstance.value = testFile;
+ fileAttachmentInstance.ngAfterViewInit();
+ tick();
+ fixture.detectChanges();
+ const labelWrapper = getLabelWrapper();
+ const input = getInputDebugEl();
- expect(fileAttachmentInstance.required).toBe(true);
+ expect(input.nativeElement.getAttribute('required')).not.toBeNull();
expect(labelWrapper.classList.contains('sky-control-label-required')).toBe(true);
+ expect(labelWrapper.getAttribute('aria-required')).toBe('true');
}));
it('should handle removing the label', fakeAsync(() => {
+ fixture.componentInstance.required = true;
fileAttachmentInstance.ngAfterViewInit();
fileAttachmentInstance.ngAfterContentInit();
tick();
diff --git a/src/app/public/modules/file-attachment/file-attachment.component.ts b/src/app/public/modules/file-attachment/file-attachment.component.ts
index e0de6e66..0b650b86 100644
--- a/src/app/public/modules/file-attachment/file-attachment.component.ts
+++ b/src/app/public/modules/file-attachment/file-attachment.component.ts
@@ -7,25 +7,27 @@ import {
ContentChildren,
ElementRef,
EventEmitter,
- forwardRef,
Input,
OnDestroy,
+ Optional,
Output,
- ViewChild,
- QueryList
+ QueryList,
+ Self,
+ ViewChild
} from '@angular/core';
import {
- ControlValueAccessor,
- AbstractControl,
- NG_VALUE_ACCESSOR,
- NG_VALIDATORS
+ NgControl
} from '@angular/forms';
import {
Subject
} from 'rxjs';
+import {
+ SkyFormsUtility
+} from '../shared/forms-utility';
+
import {
SkyFileAttachmentChange
} from './types/file-attachment-change';
@@ -50,31 +52,16 @@ import {
SkyFileItemService
} from './file-item.service';
-// tslint:disable:no-forward-ref no-use-before-declare
-const SKY_FILE_ATTACHMENT_VALUE_ACCESSOR = {
- provide: NG_VALUE_ACCESSOR,
- useExisting: forwardRef(() => SkyFileAttachmentComponent),
- multi: true
-};
-
-const SKY_FILE_ATTACHMENT_VALIDATOR = {
- provide: NG_VALIDATORS,
- useExisting: forwardRef(() => SkyFileAttachmentComponent),
- multi: true
-};
-
let uniqueId = 0;
+
@Component({
selector: 'sky-file-attachment',
templateUrl: './file-attachment.component.html',
styleUrls: ['./file-attachment.component.scss'],
- providers: [
- SKY_FILE_ATTACHMENT_VALUE_ACCESSOR,
- SKY_FILE_ATTACHMENT_VALIDATOR
- ],
changeDetection: ChangeDetectionStrategy.OnPush
})
-export class SkyFileAttachmentComponent implements ControlValueAccessor, AfterViewInit, AfterContentInit, OnDestroy {
+export class SkyFileAttachmentComponent implements AfterViewInit, AfterContentInit, OnDestroy {
+
@Input()
public acceptedTypes: string;
@@ -105,6 +92,13 @@ export class SkyFileAttachmentComponent implements ControlValueAccessor, AfterVi
public rejectedOver: boolean = false;
+ /**
+ * Indicates whether the input is required for form validation.
+ * When you set this property to `true`, the component adds `aria-required` and `required`
+ * attributes to the input element so that forms display an invalid state until the input element
+ * is complete. This property accepts a `boolean` value.
+ */
+ @Input()
public required: boolean = false;
public set value(value: SkyFileItem) {
@@ -130,17 +124,24 @@ export class SkyFileAttachmentComponent implements ControlValueAccessor, AfterVi
@ContentChildren(SkyFileAttachmentLabelComponent)
private labelComponents: QueryList;
- private control: AbstractControl;
private enterEventTarget: any;
+
private fileAttachmentId = uniqueId++;
+
private ngUnsubscribe = new Subject();
+
private _value: any;
constructor(
private changeDetector: ChangeDetectorRef,
private fileAttachmentService: SkyFileAttachmentService,
- private fileItemService: SkyFileItemService
- ) { }
+ private fileItemService: SkyFileItemService,
+ @Self() @Optional() private ngControl: NgControl
+ ) {
+ if (this.ngControl) {
+ this.ngControl.valueAccessor = this;
+ }
+ }
public ngAfterViewInit(): void {
// This is needed to address a bug in Angular 7.
@@ -149,19 +150,16 @@ export class SkyFileAttachmentComponent implements ControlValueAccessor, AfterVi
// Of note is the parent check which allows us to determine if the form is reactive.
// Without this check there is a changed before checked error
/* istanbul ignore else */
- if (this.control) {
+ if (this.ngControl) {
setTimeout(() => {
- this.control.setValue(this.value, {
+ this.ngControl.control.setValue(this.value, {
emitEvent: false
});
-
- // Set required to apply required state to label
- if (this.control.errors && this.control.errors.required) {
- this.required = true;
- }
-
this.changeDetector.markForCheck();
});
+
+ // Backwards compatibility support for anyone still using Validators.Required.
+ this.required = this.required || SkyFormsUtility.hasRequiredValidation(this.ngControl);
}
}
@@ -286,14 +284,6 @@ export class SkyFileAttachmentComponent implements ControlValueAccessor, AfterVi
this.changeDetector.markForCheck();
}
- public validate(control: AbstractControl): { [key: string]: any } {
- if (!this.control) {
- this.control = control;
- }
-
- return undefined;
- }
-
public emitClick(): void {
this.fileClick.emit({
file: this.value
diff --git a/src/app/public/modules/file-attachment/fixtures/file-attachment.component.fixture.html b/src/app/public/modules/file-attachment/fixtures/file-attachment.component.fixture.html
index d6b12c10..69a78cb7 100644
--- a/src/app/public/modules/file-attachment/fixtures/file-attachment.component.fixture.html
+++ b/src/app/public/modules/file-attachment/fixtures/file-attachment.component.fixture.html
@@ -4,6 +4,7 @@
>
Toggle showLabel
+
@@ -23,6 +30,7 @@
>
;
+ public required: boolean = true;
+
public showLabel: boolean = true;
constructor(
@@ -44,7 +45,7 @@ export class SingleFileAttachmentVisualComponent implements OnInit {
}
public ngOnInit(): void {
- this.attachment = new FormControl(undefined, Validators.required);
+ this.attachment = new FormControl(undefined);
this.fileForm = this.formBuilder.group({
attachment: this.attachment
});