Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<abp-card>
<abp-card-header>
<h3>Abp Form Field Demo</h3>
</abp-card-header>
<abp-card-body>
<form [formGroup]="form" (ngSubmit)="submit()">
<abp-form-field>
<abp-form-field-label>Name</abp-form-field-label>
<abp-input formControlName="name" />
</abp-form-field>

<abp-form-field>
<abp-form-field-label>Email</abp-form-field-label>
<abp-input formControlName="email" />
</abp-form-field>

<abp-form-field>
<abp-form-field-label>Password</abp-form-field-label>
<abp-input formControlName="password" />
</abp-form-field>

<abp-form-field>
<abp-form-field-label>Age</abp-form-field-label>
<abp-input formControlName="age" />
</abp-form-field>

<button type="submit" class="btn btn-primary" [disabled]="form.invalid">Submit</button>
</form>
</abp-card-body>
</abp-card>
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { AbpFormFieldComponent, AbpFormFieldLabelComponent } from '@abp/ng.components/abp-form-field';
import { CardComponent, CardBodyComponent, CardHeaderComponent } from '@abp/ng.theme.shared';
import { CommonModule } from '@angular/common';
import { AbpInputComponent } from '@abp/ng.components/abp-input';

@Component({
selector: 'app-abp-form-field-demo',
templateUrl: './abp-form-field-demo.component.html',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
AbpFormFieldComponent,
CardComponent,
CardBodyComponent,
CardHeaderComponent,
AbpInputComponent,
AbpFormFieldLabelComponent,
],
})
export class AbpFormFieldDemoComponent {
private fb = inject(FormBuilder);

form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]],
age: [null, [Validators.required, Validators.min(18)]],
description: ['', [Validators.maxLength(200)]],
agree: [false, [Validators.requiredTrue]],
});

submit() {
if (this.form.valid) {
console.log(this.form.value);
} else {
console.log('Form is invalid');
}
}
}
4 changes: 4 additions & 0 deletions npm/ng-packs/apps/dev-app/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const appRoutes: Routes = [
pathMatch: 'full',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent),
},
{
path: 'form-field-demo',
loadComponent: () => import('./abp-form-field-demo/abp-form-field-demo.component').then(m => m.AbpFormFieldDemoComponent),
},
{
path: 'account',
loadChildren: () => import('@abp/ng.account').then(m => m.createRoutes()),
Expand Down
7 changes: 7 additions & 0 deletions npm/ng-packs/apps/dev-app/src/app/route.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ function configureRoutes() {
iconClass: 'fas fa-home',
order: 1,
layout: eLayoutType.application,
},
{
path: '/form-field-demo',
name: 'Form Field Demo',
iconClass: 'fas fa-file-alt',
order: 2,
layout: eLayoutType.application,
}
]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
selector: 'abp-form-field-error',
template: `<div class="invalid-feedback d-block">
<ng-content></ng-content>
</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})

export class AbpFormFieldErrorComponent {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
selector: 'abp-form-field-hint',
template: `<small class="form-text text-muted">
<ng-content></ng-content>
</small>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AbpFormFieldHintComponent {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';

@Component({
selector: 'abp-form-field-label',
template: `<ng-content></ng-content>`,
changeDetection: ChangeDetectionStrategy.OnPush
})

export class AbpFormFieldLabelComponent {
for= input<string>('');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="form-group">
<label class="form-label" [for]="labelComponent()?.for()">
<ng-content select="abp-form-field-label" />
</label>
<ng-content select="abp-input" />
<ng-content select="abp-form-field-hint" />
<ng-content select="abp-validation-error" />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Component,
ChangeDetectionStrategy,
input,
HostBinding,
InjectionToken,
QueryList,
ContentChild,
contentChild,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { AbpFormFieldLabelComponent } from './abp-form-field-label.component';

export const ABP_FORM_FIELD = new InjectionToken<AbpFormFieldComponent>('AbpFormFieldComponent');

@Component({
selector: 'abp-form-field',
templateUrl: './abp-form-field.component.html',
imports: [CommonModule],
exportAs: 'abpFormField',
providers: [{ provide: ABP_FORM_FIELD, useExisting: AbpFormFieldComponent }],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AbpFormFieldComponent {

containerClass = input<string>('mb-3');
labelComponent = contentChild(AbpFormFieldLabelComponent);

@HostBinding('class')
get hostClasses(): string {
return `d-block mb-3 ${this.containerClass()}`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './abp-form-field.component';
export * from './abp-form-field-hint.component';
export * from './abp-form-field-label.component';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './abp-form-field';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib';
6 changes: 6 additions & 0 deletions npm/ng-packs/packages/components/abp-input/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@if(abpFormField) {
<div [formGroup]="abpInputFormGroup">
<input
[type]="type()"
[id]="id()"
class="form-control"
[placeholder]="placeholder()"
formControlName="value"
/>
</div>
} @else {
<div class="form-group" [formGroup]="abpInputFormGroup">
@if (label()) {
<label [for]="id()" class="form-label">{{ label() | abpLocalization }}</label>
}
<input
[type]="type()"
[id]="id()"
class="form-control"
[placeholder]="placeholder()"
formControlName="value"
/>
@if (hint()) {
<small class="form-text text-muted">{{ hint() | abpLocalization }}</small>
}
@if (errors.length > 0) {
<div class="invalid-feedback d-block">
@for (error of errors; track error) {
<div>{{ error }}</div>
}
</div>
}
<div></div>
</div>
}
109 changes: 109 additions & 0 deletions npm/ng-packs/packages/components/abp-input/src/abp-input.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
forwardRef,
inject,
OnInit,
input,
Injector
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
FormControl,
FormControlName,
FormGroup,
FormGroupDirective,
NG_VALUE_ACCESSOR,
NgControl,
ReactiveFormsModule,
} from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { LocalizationPipe } from '@abp/ng.core';
import { ABP_FORM_FIELD } from '@abp/ng.components/abp-form-field';

const ABP_INPUT_CONTROL_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AbpInputComponent),
multi: true,
};

@Component({
selector: 'abp-input',
templateUrl: './abp-input.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ReactiveFormsModule, LocalizationPipe],
exportAs: 'abpInput',
host: {
class: 'abp-input',
},
providers: [ABP_INPUT_CONTROL_VALUE_ACCESSOR],
})
export class AbpInputComponent implements OnInit, ControlValueAccessor {
label = input<string>();
type = input<'text' | 'number' | 'password'>('text');
id = input<string>('');
placeholder = input<string>('');
hint = input<string>('');
control: FormControl;
readonly formBuilder = inject(FormBuilder);
readonly changeDetectorRef = inject(ChangeDetectorRef);
readonly destroyRef = inject(DestroyRef);
readonly injector = inject(Injector);
readonly abpFormField = inject(ABP_FORM_FIELD, { optional: true });
abpInputFormGroup: FormGroup;

ngOnInit() {

Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Extra blank line after method declaration should be removed for consistency.

Suggested change

Copilot uses AI. Check for mistakes.
const ngControl = this.injector.get(NgControl, null);
if (ngControl) {
this.control = this.injector.get(FormGroupDirective).getControl(ngControl as FormControlName);
}

this.abpInputFormGroup = this.formBuilder.group({
value: [''],
});

this.value.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
this.onChange(val);
});
}

writeValue(value: any): void {
this.value.setValue(value);
this.changeDetectorRef.markForCheck();
}

registerOnChange(fn: any): void {
this.onChange = fn;
}

registerOnTouched(fn: any): void {
this.onTouched = fn;
}

setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.value.disable();
} else {
this.value.enable();
}
}

get errors(): string[] {
if (this.control && this.control.errors) {
return []
}
return []
Comment on lines +98 to +100
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The errors getter always returns an empty array regardless of whether there are actual errors. It should return the actual error messages when this.control.errors exists.

Suggested change
return []
}
return []
return Object.keys(this.control.errors);
}
return [];

Copilot uses AI. Check for mistakes.
}

get value(): AbstractControl<any> {
return this.abpInputFormGroup.get('value');
}

private onChange: (value: any) => void = () => {};
private onTouched: () => void = () => {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './abp-input.component';
2 changes: 2 additions & 0 deletions npm/ng-packs/tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"@abp/ng.account.core/proxy": ["packages/account-core/proxy/src/public-api.ts"],
"@abp/ng.account/config": ["packages/account/config/src/public-api.ts"],
"@abp/ng.components": ["packages/components/src/public-api.ts"],
"@abp/ng.components/abp-form-field": ["packages/components/abp-form-field/src/public-api.ts"],
"@abp/ng.components/abp-input": ["packages/components/abp-input/src/public-api.ts"],
"@abp/ng.components/chart.js": ["packages/components/chart.js/src/public-api.ts"],
"@abp/ng.components/extensible": ["packages/components/extensible/src/public-api.ts"],
"@abp/ng.components/lookup": ["packages/components/lookup/src/public-api.ts"],
Expand Down