Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/form field #78

Merged
merged 26 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c58091a
feat(form-field): component + prefix/suffix/label directives
Aug 5, 2024
4f29252
feat(form-field): variant (all), style (in progress)
Aug 5, 2024
0df4b2e
Merge branch 'main' into feature/form-field
Aug 12, 2024
d727a10
feat(form-field): updated token package
Aug 12, 2024
d9983f9
feat(form-field): prefix, suffix, leadingIcon, trailingIcon
Aug 13, 2024
8b444d2
feat(form-field): style for infix (states missing)
Aug 13, 2024
f9f17e7
feat(form-field): demo
Aug 14, 2024
1a1e2ad
feat(form-field): input directive (wip)
Aug 14, 2024
e1250b8
Merge branch 'main' into feature/form-field
Aug 15, 2024
63dbd99
feat(form-field): demo title
Aug 15, 2024
e8862dd
feat(form-field): IDS_FORM_ELEMENT check in ngAfterContentInit
Aug 15, 2024
2441c3a
feat(form-field): Disabled state handling, FormFieldVariant
Aug 15, 2024
e4d26d3
feat(form-field): style for disabled, error (focus, hover, active) - …
Aug 15, 2024
4da0472
feat(form-field): enhanced demo page (looped variant)
Aug 15, 2024
fd27f44
feat(form-field): pnpm lock (token 0.0.17)
Aug 15, 2024
d007840
feat(form-field): tokens package update 0.0.18
Aug 15, 2024
8e0c563
feat(form-field): demo textarea placeholder fixes
Aug 15, 2024
4c75087
feat(form-field): padding fix for inputs, textarea resizable only ver…
Aug 15, 2024
ea4060c
feat(form-field): ErrorStateMatcher
Aug 16, 2024
24befe4
feat(form-field): SuccessStateMatcher
Aug 16, 2024
73cdffa
feat(form-field): infix token name fixes
Aug 26, 2024
41cba44
feat(form-field): style form field action token fixes
Aug 26, 2024
5d972c9
feat(form-field): hover only for field-wrapper
Aug 26, 2024
41f0b3b
feat(form-field): Styles come from styles repo
Aug 26, 2024
eb123a3
feat(form-field): demo page input rows fix
Sep 3, 2024
0d30f1d
feat(form-field): import fixes, public api alphabetic ordering
Sep 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"@angular/platform-browser": "^17.3.1",
"@angular/platform-browser-dynamic": "^17.3.1",
"@angular/router": "^17.3.1",
"@i-cell/ids-styles": "0.0.10",
"@i-cell/ids-tokens": "0.0.16",
"@i-cell/ids-styles": "0.0.12",
"@i-cell/ids-tokens": "0.0.22",
"@mdi/js": "^7.4.47",
"@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "^8.0.0",
Expand Down
9,459 changes: 4,084 additions & 5,375 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions projects/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class AppComponent implements OnInit {
{ name: 'COMPONENTS.CHECKBOX', path: '/components/checkbox' },
{ name: 'COMPONENTS.DIALOG', path: '/components/dialog' },
{ name: 'COMPONENTS.DIVIDER', path: '/components/divider' },
{ name: 'COMPONENTS.FORM_FIELD', path: '/components/form-field' },
{ name: 'COMPONENTS.ICON_BUTTON', path: '/components/icon-button' },
{ name: 'COMPONENTS.PAGINATOR', path: '/components/paginator' },
{ name: 'COMPONENTS.RADIO', path: '/components/radio' },
Expand Down
4 changes: 4 additions & 0 deletions projects/demo/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export const routes: Routes = [
path: 'components/divider',
loadComponent: () => import('./pages/divider/divider-demo.component').then((module) => module.DividerDemoComponent),
},
{
path: 'components/form-field',
loadComponent: () => import('./pages/form-field/form-field-demo.component').then((module) => module.FormFieldDemoComponent),
},
{
path: 'components/icon-button',
loadComponent: () => import('./pages/icon-button/icon-button-demo.component').then((module) => module.IconButtonDemoComponent),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<h1>{{ "COMPONENTS.FORM_FIELD" | translate }}</h1>
@for (variant of variants; track $index) {
<section [class]="variant">
<h2>{{ variant | uppercase }}</h2>
<div class="form-field-column">
<h3>Default</h3>
<h4>Input</h4>
@for (size of sizes; track $index) {
<h5>{{ size }}</h5>
<div class="form-field-item">
<ids-form-field [size]="size" [variant]="variant">
<ids-label>Your name</ids-label>
<ids-icon idsLeadingIcon [icon]="iconSun" />
<span idsPrefix>Prefix</span>
<input idsInput ngModel="John" placeholder="Type your name" required />
<span idsSuffix>Suffix</span>
<ids-icon idsTrailingIcon [icon]="iconMoon" />
<div idsAction>
<button type="button" idsIconButton variant="surface">
<ids-icon aria-hidden="true" alt="" [icon]="searchIcon" />
</button>
</div>
<ids-hint-message>Type your name</ids-hint-message>
<ids-error-message>Name is mandatory</ids-error-message>
</ids-form-field>
</div>
}
<h4>Textarea</h4>
@for (size of sizes; track $index) {
<h5>{{ size }}</h5>
<div class="form-field-item">
<ids-form-field [size]="size" [variant]="variant">
<ids-label>Story about you</ids-label>
<textarea idsInput ngModel="John" placeholder="Type your story" rows="3" required></textarea>
<ids-hint-message>Type your story</ids-hint-message>
<ids-error-message>Story is mandatory</ids-error-message>
</ids-form-field>
</div>
}
</div>

<div class="form-field-column">
<h3>Disabled</h3>
<h4>Input</h4>
@for (size of sizes; track $index) {
<h5>{{ size }}</h5>
<div class="form-field-item">
<ids-form-field [size]="size" [variant]="variant">
<ids-label>Your name</ids-label>
<ids-icon idsLeadingIcon [icon]="iconSun" />
<span idsPrefix>Prefix</span>
<input idsInput ngModel="John" placeholder="Type your name" disabled />
<span idsSuffix>Suffix</span>
<ids-icon idsTrailingIcon [icon]="iconMoon" />
<div idsAction>
<button type="button" idsIconButton variant="surface">
<ids-icon aria-hidden="true" alt="" [icon]="searchIcon" />
</button>
</div>
<ids-hint-message>Type your name</ids-hint-message>
<ids-error-message>Name is mandatory</ids-error-message>
</ids-form-field>
</div>
}
<h4>Textarea</h4>
@for (size of sizes; track $index) {
<h5>{{ size }}</h5>
<div class="form-field-item">
<ids-form-field [size]="size" [variant]="variant">
<ids-label>Story about you</ids-label>
<textarea idsInput ngModel="John" placeholder="Type your story" rows="3" disabled></textarea>
<ids-hint-message>Type your story</ids-hint-message>
<ids-error-message>Story is mandatory</ids-error-message>
</ids-form-field>
</div>
}
</div>

<div class="form-field-column">
<h3>Error</h3>
<h4>Input</h4>
@for (size of sizes; track $index) {
<h5>{{ size }}</h5>
<div class="form-field-item">
<ids-form-field [size]="size" [variant]="variant">
<ids-label>Your name</ids-label>
<ids-icon idsLeadingIcon [icon]="iconSun" />
<span idsPrefix>Prefix</span>
<input idsInput ngModel="" placeholder="Type your name" required />
<span idsSuffix>Suffix</span>
<ids-icon idsTrailingIcon [icon]="iconMoon" />
<div idsAction>
<button type="button" idsIconButton variant="surface">
<ids-icon aria-hidden="true" alt="" [icon]="searchIcon" />
</button>
</div>
<ids-hint-message>Type your name</ids-hint-message>
<ids-error-message>Name is mandatory</ids-error-message>
</ids-form-field>
</div>
}
<h4>Textarea</h4>
@for (size of sizes; track $index) {
<h5>{{ size }}</h5>
<div class="form-field-item">
<ids-form-field [size]="size" [variant]="variant">
<ids-label>Story about you</ids-label>
<textarea idsInput ngModel="" placeholder="Type your story" rows="3" required></textarea>
<ids-hint-message>Type your story</ids-hint-message>
<ids-error-message>Story is mandatory</ids-error-message>
</ids-form-field>
</div>
}
</div>

<div class="form-field-column">
<h3>Success</h3>
<h4>Input</h4>
@for (size of sizes; track $index) {
<h5>{{ size }}</h5>
<div class="form-field-item">
<ids-form-field [size]="size" [variant]="variant">
<ids-label>Your name</ids-label>
<ids-icon idsLeadingIcon [icon]="iconSun" />
<span idsPrefix>Prefix</span>
<input idsInput ngModel="John" placeholder="Type your name" required [canHandleSuccessState]="true" />
<span idsSuffix>Suffix</span>
<ids-icon idsTrailingIcon [icon]="iconMoon" />
<div idsAction>
<button type="button" idsIconButton variant="surface">
<ids-icon aria-hidden="true" alt="" [icon]="searchIcon" />
</button>
</div>
<ids-hint-message>Type your name</ids-hint-message>
<ids-success-message>Name is correctly filled</ids-success-message>
<ids-error-message>Name is mandatory</ids-error-message>
</ids-form-field>
</div>
}
<h4>Textarea</h4>
@for (size of sizes; track $index) {
<h5>{{ size }}</h5>
<div class="form-field-item">
<ids-form-field [size]="size" [variant]="variant">
<ids-label>Story about you</ids-label>
<textarea idsInput ngModel="John" placeholder="Type your story" rows="3" required [canHandleSuccessState]="true"></textarea>
<ids-hint-message>Type your story</ids-hint-message>
<ids-success-message>Story is correctly filled</ids-success-message>
<ids-error-message>Story is mandatory</ids-error-message>
</ids-form-field>
</div>
}
</div>
</section>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
:host {
padding: 2rem;
}

h1 {
margin-top: 0;
}

section {
display: grid;
grid-template-columns: repeat(4, 1fr);
column-gap: 2rem;
padding: 2rem;
width: fit-content;

h2 {
grid-area: 1 / 1 / 1 / -1;
}

&.dark,
&.surface {
background-color: transparent;
}
&.light {
background-color: #252526;
h1,
h2,
h3,
h4,
h5,
h6 {
color: white;
}
}

.form-field-item {
display: flex;
flex-direction: column;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { UpperCasePipe } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Size, SizeType } from '@i-cell/ids-angular/core';
import { FormFieldVariant, FormFieldVariantType, IdsActionDirective, IdsErrorMessageComponent, IdsFormFieldComponent, IdsHintMessageComponent, IdsInputDirective, IdsLabelDirective, IdsPrefixDirective, IdsSuccessMessageComponent, IdsSuffixDirective } from '@i-cell/ids-angular/forms';
import { IdsIconComponent } from '@i-cell/ids-angular/icon';
import { IdsIconButtonComponent } from '@i-cell/ids-angular/icon-button';
import { mdiMagnify, mdiMoonWaningCrescent, mdiWhiteBalanceSunny } from '@mdi/js';
import { TranslateModule } from '@ngx-translate/core';

@Component({
selector: 'app-form-field-demo',
standalone: true,
imports: [
IdsFormFieldComponent,
IdsLabelDirective,
IdsInputDirective,
IdsPrefixDirective,
IdsSuffixDirective,
IdsActionDirective,
IdsIconButtonComponent,
IdsIconComponent,
IdsHintMessageComponent,
IdsSuccessMessageComponent,
IdsErrorMessageComponent,
FormsModule,
ReactiveFormsModule,
UpperCasePipe,
TranslateModule,
],
templateUrl: './form-field-demo.component.html',
styleUrl: './form-field-demo.component.scss',
})
export class FormFieldDemoComponent {
public iconSun = mdiWhiteBalanceSunny;
public iconMoon = mdiMoonWaningCrescent;
public searchIcon = mdiMagnify;

public sizes: SizeType[] = [
Size.DENSE,
Size.COMPACT,
Size.COMFORTABLE,
Size.SPACIOUS,
];

public variants: FormFieldVariantType[] = [
FormFieldVariant.SURFACE,
FormFieldVariant.LIGHT,
];
}
1 change: 1 addition & 0 deletions projects/demo/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"CHECKBOX": "Checkbox",
"DIALOG": "Dialog",
"DIVIDER": "Divider",
"FORM_FIELD": "Form field",
"ICON_BUTTON": "Icon button",
"SEGMENTED_CONTROL": "Segmented control",
"SEGMENTED_CONTROL_TOGGLE": "Segmented control toggle",
Expand Down
41 changes: 41 additions & 0 deletions projects/widgets/forms/common/error/error-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { AbstractControl, FormGroupDirective, NgControl, NgForm } from '@angular/forms';
import { Subject } from 'rxjs';

export abstract class AbstractErrorStateMatcher {
public abstract isErrorState(control: AbstractControl | null, form: FormGroupDirective | NgForm | null): boolean;
}

@Injectable({ providedIn: 'root' })
export class ErrorStateMatcher extends AbstractErrorStateMatcher {
public isErrorState(control: AbstractControl | null, form: FormGroupDirective | NgForm | null): boolean {
return !!(control && control.invalid && (control.touched || (form && form.submitted)));
}
}

export class ErrorStateTracker {
/** Whether the tracker is currently in an error state. */
public hasErrorState = false;

constructor(
private _matcher: ErrorStateMatcher | null,
private _ngControl: NgControl | null,
private _parentFormGroup: FormGroupDirective | null,
private _parentForm: NgForm | null,
private _stateChanges?: Subject<void>,
) {}

/** Updates the error state based on the provided error state matcher. */
public updateErrorState(): void {
const oldState = this.hasErrorState;
const parent = this._parentFormGroup || this._parentForm;
const matcher = this._matcher;
const control = this._ngControl ? (this._ngControl.control as AbstractControl) : null;
const newState = matcher?.isErrorState(control, parent) ?? false;

if (newState !== oldState) {
this.hasErrorState = newState;
this._stateChanges?.next();
}
}
}
41 changes: 41 additions & 0 deletions projects/widgets/forms/common/success/success-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { AbstractControl, FormGroupDirective, NgControl, NgForm } from '@angular/forms';
import { Subject } from 'rxjs';

export abstract class AbstractSuccessStateMatcher {
public abstract isSuccessState(control: AbstractControl | null, form: FormGroupDirective | NgForm | null): boolean;
}

@Injectable({ providedIn: 'root' })
export class SuccessStateMatcher extends AbstractSuccessStateMatcher {
public isSuccessState(control: AbstractControl | null, form: FormGroupDirective | NgForm | null): boolean {
return !!(control && control.valid && (control.touched || (form && form.submitted)));
}
}

export class SuccessStateTracker {
/** Whether the tracker is currently in an success state. */
public hasSuccessState = false;

constructor(
private _matcher: SuccessStateMatcher | null,
private _ngControl: NgControl | null,
private _parentFormGroup: FormGroupDirective | null,
private _parentForm: NgForm | null,
private _stateChanges?: Subject<void>,
) {}

/** Updates the success state based on the provided success state matcher. */
public updateSuccessState(): void {
const oldState = this.hasSuccessState;
const parent = this._parentFormGroup || this._parentForm;
const matcher = this._matcher;
const control = this._ngControl ? (this._ngControl.control as AbstractControl) : null;
const newState = matcher?.isSuccessState(control, parent) ?? false;

if (newState !== oldState) {
this.hasSuccessState = newState;
this._stateChanges?.next();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* eslint-disable no-magic-numbers */

import { FormFieldVariant, FormFieldVariantType } from './types/ids-form-field-variant.type';

import { InjectionToken } from '@angular/core';
import { Size, SizeType } from '@i-cell/ids-angular/core';

export interface IdsFormFieldDefaultOptions {
size?: SizeType
variant?: FormFieldVariantType
}

export const IDS_FORM_FIELD_DEFAULT_OPTIONS = new InjectionToken<IdsFormFieldDefaultOptions>(
'IDS_FORM_FIELD_DEFAULT_OPTIONS',
{
providedIn: 'root',
factory: IDS_FORM_FIELD_DEFAULT_OPTIONS_FACTORY,
},
);

export function IDS_FORM_FIELD_DEFAULT_OPTIONS_FACTORY(): Required<IdsFormFieldDefaultOptions> {
return {
size: Size.COMPACT,
variant: FormFieldVariant.SURFACE,
};
}
Loading