UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

980 lines 136 kB
import { __decorate, __metadata } from "tslib"; import { ChangeDetectorRef, ContentChild, Directive, EventEmitter, HostBinding, Injector, Input, Output, TemplateRef } from '@angular/core'; import { NgControl } from '@angular/forms'; import { Yield } from '@microsoft/windows-admin-center-sdk/core/base/decorators/yield.decorator'; import { Logging } from '@microsoft/windows-admin-center-sdk/core/diagnostics/logging'; import { Dom } from '@microsoft/windows-admin-center-sdk/core/dom/dom'; import { fromEvent, merge, of } from 'rxjs'; import { BaseComponent } from '../../common/base.component'; import { DisabledChangedEvent } from '../../common/events/disabledChanged.event'; import { SME_LAYOUT_PROVIDER } from '../../common/layout'; import { ValidationAlertSeverity } from '../validation-alert/validation-alert'; import { FormFieldAccessorDirective } from './form-field-accessor.directive'; import { SME_FORM_FIELD_AGGREGATOR_PROVIDER } from './form-field-aggregator'; import { FormFieldAsyncValidatorDirective } from './form-field-async-validator.directive'; import { FormFieldOrientation } from './form-field-orientation'; import { FormFieldValidatorDirective } from './form-field-validator.directive'; import * as i0 from "@angular/core"; /** * Base component class for form fields * A circular reference would be created by injecting both ngModel * and providing @see ControlValueAccessor and @see Validator implementations on the same component. * Therefore, we have broken the FormField component into 3 parts. * - The @see FormFieldValidatorDirective handles the implementation of @see Validator and @see NG_VALIDATORS * - The @see FormFieldAccessorDirective handles the implementation of @see ControlValueAccessor and @see NG_VALUE_ACCESSOR * - This @see FormFieldComponent is our glue that pulls it all together with labels and alerts and injects the NgControl instance safely. * * In order for this to work, all three components must use the same base selector @see FormFieldComponent.selector. * This is enforced in the constructor of this component, * However the directives are done by convention and will just crash if the injector cannot find them. * */ /** * * @smeDoc {@label Form fields @id sme-form-fields-component} * * @overview * @file {@filepath ./examples/overview.md} * * @example {@label Search with emphasized search button @id search-emphasis} * @file {@filename component.ts @filepath ./examples/search-emphasize.component.ts} * @file {@filename component.html @filepath ./examples/search-emphasize.component.html} * * @example {@label Search with hidden label @id search-hidden} * @file {@filename component.ts @filepath ./examples/search-hidden-label.component.ts} * @file {@filename component.html @filepath ./examples/search-hidden-label.component.html} * * @example {@label Search with required field @id search-required} * @file {@filename component.ts @filepath ./examples/search-required.component.ts} * @file {@filename component.html @filepath ./examples/search-required.component.html} * * @example {@label Calendar @id calendar} * @file {@filename component.html @filepath ./examples/calendar.component.html} * * @example {@label Clock @id clock} * @file {@filename component.ts @filepath ./examples/clock.component.ts} * @file {@filename component.html @filepath ./examples/clock.component.html} * * @example {@label Datetime @id datetime} * @file {@filename component.ts @filepath ./examples/datetime.component.ts} * @file {@filename component.html @filepath ./examples/datetime.component.html} * * @example {@label Validation error @id validation-error} * @file {@filename component.ts @filepath ./examples/validation-error.component.ts} * @file {@filename component.html @filepath ./examples/validation-error.component.html} * * @example {@label Validation pending @id validation-pending} * @file {@filename component.ts @filepath ./examples/validation-pending.component.ts} * @file {@filename component.html @filepath ./examples/validation-pending.component.html} * * @example {@label Validation with custom markdown @id validation-markdown} * @file {@filename component.ts @filepath ./examples/validation-markdown.component.ts} * @file {@filename component.ts @filepath ./examples/validation-markdown.component.html} * * @example {@label Basic combobox @id combobox} * @file {@filename component.ts @filepath ./examples/combobox.component.ts} * @file {@filename component.html @filepath ./examples/combobox.component.html} * * @example {@label Combobox with search @id combobox-search} * @file {@filename component.ts @filepath ./examples/combobox-search.component.ts} * @file {@filename component.html @filepath ./examples/combobox-search.component.html} * * @example {@label Multiple select comboxbox @id combobox-multi} * @file {@filename component.ts @filepath ./examples/combobox-multi.component.ts} * @file {@filename component.html @filepath ./examples/combobox-multi.component.html} * * @example {@label Multiple select combobox with minwidth @id combobox-multi-minwidth} * @file {@filename component.ts @filepath ./examples/combobox-multi-minwidth.component.ts} * @file {@filename component.html @filepath ./examples/combobox-multi-minwidth.component.html} * * @example {@label Multiple select combobox with custom tooltip @id custom-tooltip} * @file {@filename component.ts @filepath ./examples/custom-tooltip.component.ts} * @file {@filename component.html @filepath ./examples/custom-tooltip.component.html} */ // eslint-disable-next-line @angular-eslint/directive-class-suffix export class FormFieldComponent extends BaseComponent { /** * Constructs a new instance of @see FormFieldComponent * @param injector the angular injection service for the base classes @SmeInjectableBase Annotation. */ constructor(injector) { super(injector); /** * Indicates if to maintain the space left after hidding header */ this.hideHeaderSpace = true; /** * It determines whether to render the label in vertical middle */ this.verticalMiddleLabel = false; this.errorAlertBorder = false; this.ngModel = injector.get(NgControl, null, { optional: true }); this.accessor = injector.get(FormFieldAccessorDirective); this.validator = injector.get(FormFieldValidatorDirective); this.asyncValidator = injector.get(FormFieldAsyncValidatorDirective, null, { optional: true }); this.aggregator = injector.get(SME_FORM_FIELD_AGGREGATOR_PROVIDER, null, { optional: true }); this.layout = injector.get(SME_LAYOUT_PROVIDER, null, { optional: true, skipSelf: true }); this.changeDetectorRef = injector.get(ChangeDetectorRef); this.required = false; this.placeholder = ''; this.autofocus = null; this.initialValue = null; this.valueInitialized = false; this.immediateValidation = this.defaultImmediateValidation; this.ancestorDisabled = false; this.expectDisabledByAncestor = false; this.expectDisabledByInput = false; this.change = new EventEmitter(); this.calculatedOrientation = FormFieldOrientation.Horizontal; this.previouslyCheckedLayoutWidth = this.layoutWidth; // Runtime Linting is not necessarily useful for framework controls such as // form fields since they are designed with accessibility in mind. this.setAttribute('sme-lint-disable', ''); // validate selector to be in the 'sme-form-field[type="<type>"]' format if (this.hostElement.nativeElement.tagName.toLowerCase() !== FormFieldComponent.selector) { Logging.logError('FormField.constructor', `Components derived from ${FormFieldComponent.selector} must use a base selector of ${FormFieldComponent.selector}[type="..."]`); } } /** * The description of this field; A description for the user of the meaning and purpose of this field */ get description() { return this.internalDescription; } set description(value) { this.internalDescription = value; this.updateDetails(); } /** * The instructions of this control; A built in description for the user of how this control works. * This should be provided by the control and applies to all instances of the control, but can be overridden as an attribute. */ get instructions() { return this.internalInstructions; } set instructions(value) { this.internalInstructions = value; this.updateDetails(); } /** * Indicates that this form control is truly disabled * This will be true if the accessor (model), input, or ancestor is disabled * Its value is set only on DoCheck. */ get disabled() { // return true if we are disabled, our model is disabled or an ancestor is disabled return this.internalDisabled; } /** * It indicates whether the form field is pending validation */ get pending() { return this.ngModel && this.ngModel.pending; } get dirty() { return this.ngModel && this.ngModel.dirty; } get touched() { return this.ngModel && this.ngModel.touched; } get loadingOrDisabled() { return (this.disabled || this.loading) ? true : null; } /** * The form field's UTA id getter. * The ID is based on the form field name that's set on the ngModel. */ get dataUtaId() { // First, get the name from the attribute set in the HTML const formFieldNameAttribute = this.getAttribute('formcontrolname') || this.getAttribute('name'); // Then, if no HTML attribute is set, try to get the name from the ngModel const formFieldName = formFieldNameAttribute || this.ngModel && this.ngModel.name && this.ngModel.name.toString(); // If none of these are set, then look for the utaId input const formFieldId = formFieldName || this.utaId; // Finally, only set the data-uta-id attribute if neither name nor formcontrolname are set on the HTML already return MsftSme.isNullOrUndefined(formFieldNameAttribute) ? formFieldId : undefined; } /** * Placeholder for the combined disabled state */ get internalDisabled() { return this.ancestorDisabled || this.disabledInput || this.disabledByModel; } get fieldName() { const strings = MsftSme.self().Resources.strings.MsftSmeShell.Angular.Common.Form.Validation; return this.label || ((this.ngModel && this.ngModel.name) ? this.ngModel.name.toString() : strings.UnknownFieldName); } /** * The value of this field. * safe wrapper around this.accessor.value */ get value() { return this.accessor.getValue(); } set value(value) { this.accessor.writeValue(value, true); } get showInfoBubble() { return (!!this.resolvedTitle || !!this.resolvedTooltipTemplate); } get resolvedTooltipTemplate() { return this.tooltipTemplate; } get resolvedTitle() { return this.tooltip || this.details; } /** * Indicates the default value for immediateValidation. * This is meant to be overridden by derived classes */ get defaultImmediateValidation() { return false; } /** * Indicates if the form field should layout horizontally */ get isHorizontalLayout() { const orientation = FormFieldOrientation.fromBindableType(this.orientation) || FormFieldOrientation.fromBindableType(this.calculatedOrientation); return orientation === FormFieldOrientation.Horizontal; } /** * Indicates if the form field should layout vertically */ get isVerticalLayout() { const orientation = FormFieldOrientation.fromBindableType(this.orientation) || FormFieldOrientation.fromBindableType(this.calculatedOrientation); return orientation === FormFieldOrientation.Vertical; } /** * Getter for the available width for subfields to consume */ get subFieldLayoutWidth() { const element = this.hostElement.nativeElement; return element.clientWidth; } /** * Gets the width to base layout decisions on */ get layoutWidth() { const element = this.hostElement.nativeElement; if (!element) { return 0; } return element.offsetParent ? element.offsetParent.clientWidth : element.clientWidth; } /** * Implementation of angular OnInit interface */ ngOnInit() { super.ngOnInit(); if (!this.type) { throw new Error(`The "type" attribute is required on all ${FormFieldComponent.selector} components`); } if (!this.ngModel) { // eslint-disable-next-line max-len throw new Error(`${FormFieldComponent.selector} components require the use of an NgControl directive such as [ngModel], [formControlName], or [formControl]`); } if (this.formFieldContainer) { this.formFieldContainer.checkValidation = () => { this.ngModel.control.markAsDirty(); this.ngModel.control.updateValueAndValidity(); return this.ngModel.valid; }; this.formFieldContainer.immediateValidation = () => { if (this.immediateValidation) { this.ngModel.control.markAsDirty(); this.ngModel.control.updateValueAndValidity(); this.ngModel.control.markAsPristine(); } return this.ngModel.valid; }; this.formFieldContainer.isDirty = () => { return this.ngModel.dirty; }; } // hook up layout directive to get layout width if (this.layout) { this.subscriptions.push(this.layout.layoutChanged.subscribe(() => { this.yieldOnLayoutChanged(); })); } // subscribe to our validators customValidate event. this.subscriptions.push(this.validator.customValidate.subscribe( // Merge our alerts with the alerts object given in the event args (args) => { MsftSme.deepAssign(args.alerts, this.validate(args.formControl)); })); // subscribe to our validators alert event. this.subscriptions.push(this.validator.alert.subscribe((alerts) => { this.onAlert(alerts); })); if (this.asyncValidator && !this.asyncValidator.customAsyncValidateDisabled) { // subscribe to our asyncValidators customAsyncValidate event. this.subscriptions.push(this.asyncValidator.customAsyncValidate.subscribe( // Merge our alerts with the alerts object given in the event args (args) => { args.alerts.push(this.asyncValidate(args.formControl)); })); // subscribe to our asyncValidators asyncAlert event. this.subscriptions.push(this.asyncValidator.asyncAlert.subscribe((alerts) => { this.onAsyncAlert(alerts); })); } // listen to disabled state changes from our model this.subscriptions.push(this.accessor.disabledChange.subscribe((disabled => this.ngModelDisabledChanged(disabled)))); // subscribe to ngModel status changes. this.subscriptions.push(this.ngModel.statusChanges.subscribe(status => this.onStatusChanged(status))); this.subscriptions.push(this.ngModel.valueChanges.subscribe((value) => this.onValueChanged(value))); // if we have a parent aggregator, then register to it if (this.aggregator) { this.aggregator.addFormField(this); } // our model may have started in the disabled state, so initialize our tracking property to the current accessor state. this.disabledByModel = this.accessor.disabled; // ensure our model starts in a pristine state this.ngModel.control.markAsPristine(); // a bit hacky, but this works to trigger validation immediately when the form loads. // this method does not work properly with editable grids using formFieldContainer, use explicit immediateValidation call above. if (!this.formFieldContainer && this.immediateValidation) { this.value = this.value; this.ngModel.control.markAsPristine(); } } /** * Implementation of angular DoCheck interface */ ngDoCheck() { super.ngDoCheck(); // Check to see if we need to recalculate our layout const layoutWidth = this.layoutWidth; if (layoutWidth !== this.previouslyCheckedLayoutWidth) { this.onLayoutChanged(); } this.previouslyCheckedLayoutWidth = layoutWidth; } /** * synchronizes the expected disabled state with the actual state of the model */ synchronizeDisabledState() { const disable = this.internalDisabled; if (disable === this.accessor.disabled) { return; } if (disable) { if (this.ancestorDisabled) { this.expectDisabledByAncestor = true; } if (this.disabledInput) { this.expectDisabledByInput = true; } this.ngModel.control.disable(); } else if (!this.disabledByModel) { this.expectDisabledByAncestor = false; this.expectDisabledByInput = false; this.ngModel.control.enable(); } } ngOnChanges(changes) { super.ngOnChanges(changes); if (changes.loading) { this.updateAlerts(); } if (changes.disabledInput) { setTimeout(() => this.synchronizeDisabledState()); } } ngAfterViewInit() { super.ngAfterViewInit(); // initialize fieldset ancestors. Our ancestry should never change, so we can save perf by collecting this now. const fieldsetAncestors = Dom.getAllAncestors(this.hostElement.nativeElement, (element) => element.tagName.toUpperCase() === 'FIELDSET'); // listen to the custom disabledChanged event on our ancestor fieldsets const disabledChangedObservables = fieldsetAncestors.map(f => fromEvent(f, DisabledChangedEvent.typeName)); this.subscriptions.push(merge(...disabledChangedObservables).subscribe(() => { this.ancestorDisabled = fieldsetAncestors.some(f => f.disabled); this.synchronizeDisabledState(); })); // initialize our ancestral disabled state this.ancestorDisabled = fieldsetAncestors.some(f => f.disabled); this.synchronizeDisabledState(); } /** * Implementation of angular OnDestroy interface * derived classes are always expected to call super.ngOnDestroy() when overriding */ ngOnDestroy() { super.ngOnDestroy(); // if we have a parent aggregator, then register to it if (this.aggregator) { this.aggregator.removeFormField(this); } } /** * On layout changed event handler, occurs every time the layout has been changed. */ onLayoutChanged() { const layoutWidth = this.layoutWidth; // 600px is breakpoint for form field vertical/horizontal layout const orientation = layoutWidth >= 600 ? FormFieldOrientation.Horizontal : FormFieldOrientation.Vertical; if (this.calculatedOrientation !== orientation) { this.calculatedOrientation = orientation; this.changeDetectorRef.detectChanges(); this.yieldOnOrientationChanged(); } } /** * Occurs every time the orientation has been changed. */ onOrientationChanged() { // To be implemented optionally by derived components. } /** * Occurs every time the layout has been changed. */ yieldOnLayoutChanged() { if (!this.ngIsDestroyed) { this.onLayoutChanged(); } } /** * Occurs every time the layout has been changed. */ yieldOnOrientationChanged() { if (!this.ngIsDestroyed) { this.onLayoutChanged(); } } /** * Resets this field to its initial value */ reset() { this.ngModel.reset(); } /** * Clears the value of this field base on the initial values type. */ clear() { this.value = this.getClearValue(); } /** * Copies the fields value to the clipboard. */ copyToClipboard() { MsftSme.copyToClipboard(this.value); } /** * Check whether the header should be displayed. */ shouldDisplayHeader() { return !this.hideHeader && (!!(this.label || this.details) || this.required); } /** * Gets a value indicating if this form field is valid */ isValid() { return this.ngModel.valid; } /** * Gets a value indicating if the form field is dirty. */ isDirty() { return this.ngModel.dirty; } /** * Marks the form field as pristine. */ markAsPristine() { this.ngModel.control.markAsPristine(); } /** * Marks the form field as dirty. */ markAsDirty() { this.ngModel.control.markAsDirty(); } /** * Applies the focus to the current element */ focus() { const next = Dom.getNextFocusableElement(this.hostElement.nativeElement); if (next) { next.focus(); } } /** * Occurs every time the control is disabled or enabled. * @param disabled The new disabled state of the control */ ngModelDisabledChanged(disabled) { if (disabled) { const expectDisabled = this.expectDisabledByAncestor || this.expectDisabledByInput; if (this.expectDisabledByAncestor) { this.expectDisabledByAncestor = false; } if (this.expectDisabledByInput) { this.expectDisabledByInput = false; } if (!expectDisabled) { this.disabledByModel = true; this.synchronizeDisabledState(); } } else { this.disabledByModel = false; this.synchronizeDisabledState(); } } /** * Determines the value to use when clearing the field based on the initial value type * Derived fields may override this to change the clear value behavior */ getClearValue() { if (MsftSme.isNullOrUndefined(this.initialValue)) { return null; } if (Array.isArray(this.initialValue)) { return []; } switch (typeof this.initialValue) { case 'string': { return ''; } case 'boolean': { return false; } default: { return this.initialValue; } } } /** * Performs validation that is internal to this control * @param c The form control attached to this instance */ validate(c) { // To be implemented optionally by derived components. return {}; } /** * Performs asynchronous validation that is internal to this control * @param c The form control attached to this instance */ asyncValidate(c) { // To be implemented optionally by derived components. return of({}); } /** * Occurs any time value changed. */ updateDetails() { const details = []; if (!MsftSme.isNullOrWhiteSpace(this.description)) { details.push(this.description); } if (!MsftSme.isNullOrWhiteSpace(this.instructions)) { details.push(this.instructions); } // separate details and instructions with double line breaks. this.details = details.length > 0 ? details.join('\n\n') : null; } /** * Gets the initial host classes to be applied to this element * When called in the @see BaseComponent super class initialization, These classes will be automatically assigned to the host element. */ getInitialHostClasses() { return super.getInitialHostClasses().concat([ 'sme-form-field' ]); } /** * Occurs every time the value of the control changes, in the UI or programmatically. * @param value the value of the form control */ onValueChanged(value) { if (!this.valueInitialized) { this.initialValue = value; this.valueInitialized = true; } else { if (this.formFieldContainer) { this.formFieldContainer.reportChange(); } if (this.change.observers.length > 0) { if (this.ngModel['update'].observers.length > 0) { setTimeout(() => this.change.emit()); } else { this.value = value; this.change.emit(); } } } } /** * Occurs every time the validation status of the control has been re-calculated. * @param status the status object of the form control */ onStatusChanged(status) { this.updateAlerts(); if (this.aggregator) { this.aggregator.updateStatus(); } } /** * Occurs every time there are alert changes from the validator directive * @param alerts the alerts from the validator directive */ onAlert(alerts) { this.nonErrorAlerts = this.removeErrorAlerts(alerts); this.updateAlerts(); } /** * Occurs every time there are alert changes from the validator directive * @param alerts the alerts from the validator directive */ onAsyncAlert(alerts) { this.nonErrorAsyncAlerts = this.removeErrorAlerts(alerts); this.updateAlerts(); } /** * Removes the error alerts from a ValidationAlert Objects * errors will be surfaced in the this.ngModel.control.errors instead * @param alerts the alerts to filter */ removeErrorAlerts(alerts) { const nonErrorAlerts = {}; Object.keys(alerts).forEach(key => { const alert = alerts[key]; if (alert && alert.valid) { nonErrorAlerts[key] = alert; } }); return nonErrorAlerts; } /** * Updates the form field border to reflect error alerts */ updateBorderState() { if (this.alert && this.alert.severity === ValidationAlertSeverity.Error && (this.alert.valid || this.alert.showWhenPristine || this.ngModel.dirty)) { this.errorAlertBorder = true; } else { this.errorAlertBorder = false; } } /** * Updates the active alerts of this control */ updateAlerts() { const strings = MsftSme.self().Resources.strings; this.alert = null; // When in a loading state, show that alert only. if (this.loading) { this.alert = { pending: true, message: this.loadingMessage || strings.MsftSmeShell.Angular.Common.Form.Loading.message }; return; } // When in a pending state, show that alert only. if (this.pending) { this.alert = { pending: true, message: this.pendingMessage || strings.MsftSmeShell.Angular.Common.Form.Validation.Pending.message }; return; } // gather our alerts. const alertsArray = []; this.addVisibleAlerts(alertsArray, this.nonErrorAlerts); this.addVisibleAlerts(alertsArray, this.nonErrorAsyncAlerts); this.addVisibleAlerts(alertsArray, this.ngModel.control.errors); // clear active alert if there are no alerts if (alertsArray.length === 0) { if (this.formFieldContainer) { this.formFieldContainer.updateAlert(null); } this.updateBorderState(); return; } const formFieldErrorNames = { required: 'required', invalidDisplayValue: 'invalidDisplayValue' }; // Pre sort for invalid display errors to come before required errors, they have the same severity alertsArray.sort((a, b) => { if (a.name === formFieldErrorNames.required && b.name === formFieldErrorNames.invalidDisplayValue) { return 1; } if (a.name === formFieldErrorNames.invalidDisplayValue && b.name === formFieldErrorNames.required) { return -1; } return 0; }); const sortedAlerts = alertsArray .map(e => this.getAlert(e.name, e.value)) .sort((a, b) => { // first sort on valid property if (a.valid !== b.valid) { return a.valid ? 1 : -1; } // then sort on severity if (a.severity !== b.severity) { return a.severity < b.severity ? 1 : -1; } // otherwise they are considered equal for sorting return 0; }); sortedAlerts.some(alert => { // Show alert in these scenarios: // - If the form field is required and it is not the initial load of the form // - If a custom alert marks showWhenPrisitine as true // - If the alert is marked as valid // - Any other alerts after the form is marked as dirty if (alert.severity !== ValidationAlertSeverity.Error || alert.valid || alert.showWhenPristine || this.ngModel.dirty) { this.alert = alert; if (this.formFieldContainer) { this.formFieldContainer.updateAlert(alert); } return true; } return false; }); this.updateBorderState(); } addVisibleAlerts(target, alerts) { if (!MsftSme.isNullOrUndefined(alerts)) { MsftSme.forEachKey(alerts, (k, v) => { if (!v.hidden) { target.push({ name: k, value: v }); } }); } } /** * gets an alert object from a key and validation result */ getAlert(key, alert) { // if the alert is false, null, or valid with no message, then its not really an alert that needs to be shown and should be ignored if (alert === false || MsftSme.isNullOrUndefined(alert) || (alert.valid === true) && MsftSme.isNullOrWhiteSpace(alert.message)) { return null; } let processedAlert; // if the alert is a string, show it as an error if (typeof alert === 'string') { processedAlert = { valid: false, severity: ValidationAlertSeverity.Error, message: alert }; } else { processedAlert = this.getKnownNgAlert(key, alert); if (!processedAlert) { // if alert is a simple true, we dont really know how to deal with it, so just create an empty validation object processedAlert = alert === true ? {} : { ...alert }; // if valid is not set, then set it explicitly to false if (MsftSme.isNullOrUndefined(processedAlert.valid)) { processedAlert.valid = false; } // if there is no message, create a default one if (MsftSme.isNullOrWhiteSpace(processedAlert.message)) { processedAlert.message = `Validation failed: ${key}`; } // if severity is not set correctly and the field is not valid, then set it to error if (!processedAlert.valid && MsftSme.isNullOrUndefined(ValidationAlertSeverity[processedAlert.severity])) { processedAlert.severity = ValidationAlertSeverity.Error; } } } return processedAlert; } /** * Gets an alert object from known angular alert keys */ getKnownNgAlert(key, alert) { const vStrings = MsftSme.self().Resources.strings.MsftSmeShell.Angular.Common.Form.Validation; let message; // first check for known angular 'built-in' validations switch (key) { case 'min': { message = vStrings.Min.format.format(this.fieldName, alert.min); break; } case 'max': { message = vStrings.Max.format.format(this.fieldName, alert.max); break; } case 'required': { message = vStrings.Required.format.format(this.fieldName); break; } case 'email': { message = vStrings.Email.format.format(this.fieldName); break; } case 'minlength': { message = vStrings.MinLength.format.format(this.fieldName, alert.requiredLength); break; } case 'maxlength': { message = vStrings.MaxLength.format.format(this.fieldName, alert.requiredLength); break; } case 'pattern': { message = vStrings.Pattern.format.format(this.fieldName, alert.requiredPattern); break; } default: { break; } } if (MsftSme.isNullOrWhiteSpace(message)) { return null; } let valid = false; let severity = ValidationAlertSeverity.Error; if (!MsftSme.isNullOrWhiteSpace(alert)) { // merge alert properties if they exist valid = !!alert.valid; severity = MsftSme.isNullOrUndefined(alert.severity) ? severity : alert.severity; message = MsftSme.isNullOrUndefined(alert.message) ? message : alert.message; } return { valid: valid, severity: severity, message: message }; } /** * In some cases, we need a combined version of the label and description so that the screen read can read it appropriately. * Common cases for this are when using role="group" or role="radioGroup" because they does not support aria-describedby * we achieve the same effect by splitting the description and and label with a '.' (period)" * TODO: verify how this localizes or find a better way. */ getMergedDescriptionLabel() { if (MsftSme.isNullOrWhiteSpace(this.description)) { return this.label || ''; } if (MsftSme.isNullOrWhiteSpace(this.label)) { return this.description || ''; } return `${this.label}. ${this.description}`; } } /** * The selector that all form fields must use in order to work properly with * @see FormFieldAccessorDirective and @see FormFieldValidatorDirective */ FormFieldComponent.selector = 'sme-form-field'; /** @nocollapse */ FormFieldComponent.ɵfac = function FormFieldComponent_Factory(t) { return new (t || FormFieldComponent)(i0.ɵɵdirectiveInject(i0.Injector)); }; /** @nocollapse */ FormFieldComponent.ɵdir = /** @pureOrBreakMyCode */ i0.ɵɵdefineDirective({ type: FormFieldComponent, contentQueries: function FormFieldComponent_ContentQueries(rf, ctx, dirIndex) { if (rf & 1) { i0.ɵɵcontentQuery(dirIndex, TemplateRef, 5); } if (rf & 2) { let _t; i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.subFormTemplate = _t.first); } }, hostVars: 15, hostBindings: function FormFieldComponent_HostBindings(rf, ctx) { if (rf & 2) { i0.ɵɵattribute("disabled", ctx.loadingOrDisabled)("aria-disabled", ctx.loadingOrDisabled)("data-uta-id", ctx.dataUtaId); i0.ɵɵclassProp("sme-form-field-auto-height", ctx.compact)("sme-form-field-minimal", ctx.minimal)("sme-disabled", ctx.loadingOrDisabled)("sme-form-field-error-alert-border", ctx.errorAlertBorder)("sme-form-field-orientation-horizontal", ctx.isHorizontalLayout)("sme-form-field-orientation-vertical", ctx.isVerticalLayout); } }, inputs: { type: "type", hideHeader: "hideHeader", hideHeaderSpace: "hideHeaderSpace", required: "required", autofocus: "autofocus", placeholder: "placeholder", label: "label", verticalMiddleLabel: "verticalMiddleLabel", description: "description", compact: "compact", minimal: "minimal", readonly: "readonly", instructions: "instructions", tooltipTemplate: "tooltipTemplate", tooltip: "tooltip", tooltipContext: "tooltipContext", disabledInput: ["disabled", "disabledInput"], loading: "loading", loadingMessage: "loadingMessage", pendingMessage: "pendingMessage", utaId: "utaId", immediateValidation: "immediateValidation", orientation: "orientation", formFieldContainer: "formFieldContainer" }, outputs: { change: "change" }, features: [i0.ɵɵInheritDefinitionFeature, i0.ɵɵNgOnChangesFeature] }); __decorate([ Yield(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], FormFieldComponent.prototype, "yieldOnLayoutChanged", null); __decorate([ Yield(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], FormFieldComponent.prototype, "yieldOnOrientationChanged", null); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FormFieldComponent, [{ type: Directive }], function () { return [{ type: i0.Injector }]; }, { type: [{ type: Input }], hideHeader: [{ type: Input }], hideHeaderSpace: [{ type: Input }], required: [{ type: Input }], autofocus: [{ type: Input }], placeholder: [{ type: Input }], label: [{ type: Input }], verticalMiddleLabel: [{ type: Input }], subFormTemplate: [{ type: ContentChild, args: [TemplateRef] }], description: [{ type: Input }], compact: [{ type: Input }, { type: HostBinding, args: ['class.sme-form-field-auto-height'] }], minimal: [{ type: Input }, { type: HostBinding, args: ['class.sme-form-field-minimal'] }], readonly: [{ type: Input }], instructions: [{ type: Input }], tooltipTemplate: [{ type: Input }], tooltip: [{ type: Input }], tooltipContext: [{ type: Input }], loadingOrDisabled: [{ type: HostBinding, args: ['class.sme-disabled'] }, { type: HostBinding, args: ['attr.disabled'] }, { type: HostBinding, args: ['attr.aria-disabled'] }], disabledInput: [{ type: Input, args: ['disabled'] }], loading: [{ type: Input }], loadingMessage: [{ type: Input }], pendingMessage: [{ type: Input }], utaId: [{ type: Input }], dataUtaId: [{ type: HostBinding, args: ['attr.data-uta-id'] }], immediateValidation: [{ type: Input }], change: [{ type: Output }], errorAlertBorder: [{ type: HostBinding, args: ['class.sme-form-field-error-alert-border'] }], isHorizontalLayout: [{ type: HostBinding, args: ['class.sme-form-field-orientation-horizontal'] }], isVerticalLayout: [{ type: HostBinding, args: ['class.sme-form-field-orientation-vertical'] }], orientation: [{ type: Input }], formFieldContainer: [{ type: Input }], yieldOnLayoutChanged: [], yieldOnOrientationChanged: [] }); })(); /** * Internal base component for SME for fields. It simply removes the need to supply the string type parameter to FormFieldComponent * This class is exported from this file, but not meant to be exported from index.ts bundles. */ // eslint-disable-next-line @angular-eslint/directive-class-suffix export class SmeInternalFormFieldComponent extends FormFieldComponent { } /** @nocollapse */ SmeInternalFormFieldComponent.ɵfac = /** @pureOrBreakMyCode */ function () { let ɵSmeInternalFormFieldComponent_BaseFactory; return function SmeInternalFormFieldComponent_Factory(t) { return (ɵSmeInternalFormFieldComponent_BaseFactory || (ɵSmeInternalFormFieldComponent_BaseFactory = i0.ɵɵgetInheritedFactory(SmeInternalFormFieldComponent)))(t || SmeInternalFormFieldComponent); }; }(); /** @nocollapse */ SmeInternalFormFieldComponent.ɵdir = /** @pureOrBreakMyCode */ i0.ɵɵdefineDirective({ type: SmeInternalFormFieldComponent, features: [i0.ɵɵInheritDefinitionFeature] }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(SmeInternalFormFieldComponent, [{ type: Directive }], null, null); })(); //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9ybS1maWVsZC5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9hbmd1bGFyL3NyYy9jb250cm9scy9mb3JtL2Zvcm0tZmllbGQvZm9ybS1maWVsZC5jb21wb25lbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU8sRUFDWSxpQkFBaUIsRUFBRSxZQUFZLEVBQUUsU0FBUyxFQUN6RCxZQUFZLEVBQUUsV0FBVyxFQUFVLFFBQVEsRUFBRSxLQUFLLEVBQTBDLE1BQU0sRUFBaUIsV0FBVyxFQUNqSSxNQUFNLGVBQWUsQ0FBQztBQUN2QixPQUFPLEVBQWUsU0FBUyxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDeEQsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLDBFQUEwRSxDQUFDO0FBQ2pHLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSw4REFBOEQsQ0FBQztBQUN2RixPQUFPLEVBQUUsR0FBRyxFQUFFLE1BQU0sa0RBQWtELENBQUM7QUFDdkUsT0FBTyxFQUFFLFNBQVMsRUFBRSxLQUFLLEVBQWMsRUFBRSxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBRXhELE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUM1RCxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSwyQ0FBMkMsQ0FBQztBQUNqRixPQUFPLEVBQVUsbUJBQW1CLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUNsRSxPQUFPLEVBQXFDLHVCQUF1QixFQUFvQixNQUFNLHNDQUFzQyxDQUFDO0FBQ3BJLE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQzdFLE9BQU8sRUFBOEMsa0NBQWtDLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUN6SCxPQUFPLEVBQWlDLGdDQUFnQyxFQUFFLE1BQU0sd0NBQXdDLENBQUM7QUFFekgsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sMEJB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