@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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`—Shows hints when the form control is `valid` or `untouched` and `pristine`.
* * `always`—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`—Shows errors when the form control is `invalid` and `touched` or `dirty`.
* * `always`—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>
(hasHints) {
<ng-content select="kendo-formhint"></ng-content>
}
(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>
(hasHints) {
<ng-content select="kendo-formhint"></ng-content>
}
(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
}] } });