UNPKG

@progress/kendo-angular-inputs

Version:

Kendo UI for Angular Inputs Package - Everything you need to build professional form functionality (Checkbox, ColorGradient, ColorPalette, ColorPicker, FlatColorPicker, FormField, MaskedTextBox, NumericTextBox, RadioButton, RangeSlider, Slider, Switch, Te

354 lines (353 loc) 14.4 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Component, ContentChild, ContentChildren, ElementRef, HostBinding, Input, isDevMode, Renderer2, QueryList } from '@angular/core'; import { NgControl, RadioControlValueAccessor } from '@angular/forms'; import { Subscription } from 'rxjs'; import { KendoInput, isDocumentAvailable } from '@progress/kendo-angular-common'; import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from '../package-metadata'; import { ErrorComponent } from './error.component'; import { HintComponent } from './hint.component'; import { FormService } from '../common/formservice.service'; import { filter } from 'rxjs/operators'; import { calculateColSpan, generateColSpanClass } from '../form/utils'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-l10n"; import * as i2 from "../common/formservice.service"; /** * Represents the Kendo UI FormField component for Angular. * Use this component to group form-bound controls (Kendo Angular components or native HTML controls). * Applies styling and behavior rules. * * @example * ```html * <kendo-formfield> * <kendo-label [for]="firstName"text="First Name"></kendo-label> * <kendo-textbox formControlName="firstName" #firstName></kendo-textbox> * <kendo-formhint>Enter your name.</kendo-formhint> * <kendo-formerror>First name is required.</kendo-formerror> * </kendo-formfield> * ``` * * @remarks * Supported children components are: {@link ErrorComponent}, {@link HintComponent}, {@link TextBoxComponent}, {@link NumericTextBoxComponent}, {@link MaskedTextBoxComponent}, {@link TextAreaComponent}, {@link DatePickerComponent}, {@link DateTimePickerComponent}, {@link DateInputComponent}, {@link OTPInputComponent}. */ export class FormFieldComponent { renderer; localizationService; hostElement; formService; hostClass = true; /** * @hidden */ direction; get errorClass() { if (!this.control) { return false; } return this.control.invalid && (this.control.touched || this.control.dirty); } get disabledClass() { if (!this.control) { return false; } // radiobutton group if (this.isRadioControl(this.control)) { return false; } return this.disabledControl() || this.disabledElement() || this.disabledKendoInput(); } set formControls(formControls) { this.validateFormControl(formControls); this.control = formControls.first; } controlElementRefs; kendoInput; errorChildren; hintChildren; /** * Specifies when to show the hint messages: * * `initial`&mdash;Shows hints when the form control is `valid` or `untouched` and `pristine`. * * `always`&mdash;Always shows hints. * * @default 'initial' */ showHints = 'initial'; /** * Specifies the layout orientation of the form field. * * @hidden * * @default 'vertical' */ orientation = 'vertical'; /** * Specifies when to show the error messages: * * `initial`&mdash;Shows errors when the form control is `invalid` and `touched` or `dirty`. * * `always`&mdash;Always shows errors. * * @default 'initial' */ showErrors = 'initial'; /** * Defines the colspan for the form field. * Can be a number or an array of responsive breakpoints. */ colSpan; /** * @hidden */ get horizontal() { return this.orientation === 'horizontal'; } /** * @hidden */ get hasHints() { return this.showHints === 'always' ? true : this.showHintsInitial(); } /** * @hidden */ get hasErrors() { return this.showErrors === 'always' ? true : this.showErrorsInitial(); } control; subscriptions = new Subscription(); rtl = false; _formWidth = null; _colSpanClass = null; _previousColSpan = null; constructor(renderer, localizationService, hostElement, formService) { this.renderer = renderer; this.localizationService = localizationService; this.hostElement = hostElement; this.formService = formService; validatePackage(packageMetadata); this.subscriptions.add(this.localizationService.changes.subscribe(({ rtl }) => { this.rtl = rtl; this.direction = this.rtl ? 'rtl' : 'ltr'; })); this.subscriptions.add(this.formService.formWidth.pipe(filter((width) => width !== null)).subscribe((width) => { this._formWidth = width; this.updateColSpanClass(); })); } ngAfterViewInit() { this.setDescription(); } ngAfterViewChecked() { this.updateDescription(); } ngOnChanges(changes) { if (changes['colSpan']) { this.updateColSpanClass(); } } ngOnDestroy() { this.subscriptions.unsubscribe(); } disabledKendoInput() { return this.kendoInput && this.kendoInput.disabled; } disabledControl() { return this.control.disabled; } disabledElement() { const elements = this.controlElementRefs.toArray(); return elements.every(e => e.nativeElement.hasAttribute('disabled')); } validateFormControl(formControls) { if (isDevMode() && formControls.length !== 1 && !this.isControlGroup(formControls)) { throw new Error('The `kendo-formfield` component should contain ' + 'only one control of type NgControl with a formControlName(https://angular.io/api/forms/FormControlName)' + 'or an ngModel(https://angular.io/api/forms/NgModel) binding.'); } } isControlGroup(formControls) { if (!formControls.length) { return false; } const name = formControls.first.name; return formControls.toArray().every(c => c.name === name && (this.isRadioControl(c))); } isRadioControl(control) { return control.valueAccessor instanceof RadioControlValueAccessor; } updateDescription() { const controls = this.findControlElements().filter(c => !!c); if (!controls) { return; } controls.forEach((control) => { if (this.errorChildren.length > 0 || this.hintChildren.length > 0) { const ariaIds = this.generateDescriptionIds(control); if (ariaIds !== '') { this.renderer.setAttribute(control, 'aria-describedby', ariaIds); } else { this.renderer.removeAttribute(control, 'aria-describedby'); } } }); } findControlElements() { if (!this.controlElementRefs) { return; } // the control is KendoInput and has focusableId - dropdowns, dateinputs, editor if (this.kendoInput && this.kendoInput.focusableId && isDocumentAvailable()) { // Editor requires special treatment when in iframe mode const isEditor = this.kendoInput.focusableId.startsWith('k-editor'); return isEditor ? [this.kendoInput.viewMountElement] : [this.hostElement.nativeElement.querySelector(`#${this.kendoInput.focusableId}`)]; } return this.controlElementRefs.map(el => el.nativeElement); } generateDescriptionIds(control) { const ids = new Set(); let errorAttribute = ''; if (control.hasAttribute('aria-describedby')) { const attributes = control.getAttribute('aria-describedby').split(' '); errorAttribute = attributes.filter(attr => attr.includes('kendo-error-'))[0]; attributes.forEach((attr) => { if (attr.includes('kendo-hint-') || attr.includes('kendo-error-')) { return; } ids.add(attr); }); } this.hintChildren.forEach((hint) => { ids.add(hint.id); }); if (this.hasErrors) { this.errorChildren.forEach((error) => { ids.add(error.id); }); } else { ids.delete(errorAttribute); } return Array.from(ids).join(' '); } showHintsInitial() { if (!this.control) { return true; } const { valid, untouched, pristine } = this.control; return valid || (untouched && pristine); } showErrorsInitial() { if (!this.control) { return false; } const { invalid, dirty, touched } = this.control; return invalid && (dirty || touched); } setDescription() { this.updateDescription(); this.subscriptions.add(this.errorChildren.changes.subscribe(() => this.updateDescription())); this.subscriptions.add(this.hintChildren.changes.subscribe(() => this.updateDescription())); } updateColSpanClass() { const hostElement = this.hostElement.nativeElement; const newColSpan = calculateColSpan(this.colSpan, this._formWidth); if (newColSpan !== this._previousColSpan) { const newClass = generateColSpanClass(newColSpan); if (this._colSpanClass) { this.renderer.removeClass(hostElement, this._colSpanClass); } if (newClass) { this.renderer.addClass(hostElement, newClass); } this._colSpanClass = newClass; this._previousColSpan = newColSpan; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FormFieldComponent, deps: [{ token: i0.Renderer2 }, { token: i1.LocalizationService }, { token: i0.ElementRef }, { token: i2.FormService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FormFieldComponent, isStandalone: true, selector: "kendo-formfield", inputs: { showHints: "showHints", orientation: "orientation", showErrors: "showErrors", colSpan: "colSpan" }, host: { properties: { "class.k-form-field": "this.hostClass", "attr.dir": "this.direction", "class.k-form-field-error": "this.errorClass", "class.k-form-field-disabled": "this.disabledClass" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.formfield' } ], queries: [{ propertyName: "kendoInput", first: true, predicate: KendoInput, descendants: true, static: true }, { propertyName: "formControls", predicate: NgControl, descendants: true }, { propertyName: "controlElementRefs", predicate: NgControl, descendants: true, read: ElementRef }, { propertyName: "errorChildren", predicate: ErrorComponent, descendants: true }, { propertyName: "hintChildren", predicate: HintComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: ` <ng-content select="label, kendo-label"></ng-content> <div class="k-form-field-wrap"> <ng-content></ng-content> @if (hasHints) { <ng-content select="kendo-formhint"></ng-content> } @if (hasErrors) { <ng-content select="kendo-formerror"></ng-content> } </div> `, isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FormFieldComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-formfield', template: ` <ng-content select="label, kendo-label"></ng-content> <div class="k-form-field-wrap"> <ng-content></ng-content> @if (hasHints) { <ng-content select="kendo-formhint"></ng-content> } @if (hasErrors) { <ng-content select="kendo-formerror"></ng-content> } </div> `, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.formfield' } ], standalone: true, imports: [] }] }], ctorParameters: () => [{ type: i0.Renderer2 }, { type: i1.LocalizationService }, { type: i0.ElementRef }, { type: i2.FormService }], propDecorators: { hostClass: [{ type: HostBinding, args: ['class.k-form-field'] }], direction: [{ type: HostBinding, args: ['attr.dir'] }], errorClass: [{ type: HostBinding, args: ['class.k-form-field-error'] }], disabledClass: [{ type: HostBinding, args: ['class.k-form-field-disabled'] }], formControls: [{ type: ContentChildren, args: [NgControl, { descendants: true }] }], controlElementRefs: [{ type: ContentChildren, args: [NgControl, { read: ElementRef, descendants: true }] }], kendoInput: [{ type: ContentChild, args: [KendoInput, { static: true }] }], errorChildren: [{ type: ContentChildren, args: [ErrorComponent, { descendants: true }] }], hintChildren: [{ type: ContentChildren, args: [HintComponent, { descendants: true }] }], showHints: [{ type: Input }], orientation: [{ type: Input }], showErrors: [{ type: Input }], colSpan: [{ type: Input }] } });