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

feat: update forms doc #1694

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions apps/showcase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@o3r/configuration": "workspace:^",
"@o3r/core": "workspace:^",
"@o3r/dynamic-content": "workspace:^",
"@o3r/forms": "workspace:^",
"@o3r/localization": "workspace:^",
"@o3r/logger": "workspace:^",
"@o3r/routing": "workspace:^",
Expand Down
1 change: 1 addition & 0 deletions apps/showcase/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const appRoutes: Routes = [
{path: 'run-app-locally', loadComponent: () => import('./run-app-locally/index').then((m) => m.RunAppLocallyComponent), title: 'Otter Showcase - Run App Locally'},
{path: 'sdk', loadComponent: () => import('./sdk/index').then((m) => m.SdkComponent), title: 'Otter Showcase - SDK'},
{path: 'placeholder', loadComponent: () => import('./placeholder/index').then((m) => m.PlaceholderComponent), title: 'Otter Showcase - Placeholder'},
{path: 'forms', loadComponent: () => import('./forms/index').then((m) => m.FormsComponent), title: 'Otter Showcase - Forms'},
{path: '**', redirectTo: '/home', pathMatch: 'full'}
];

Expand Down
3 changes: 2 additions & 1 deletion apps/showcase/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export class AppComponent implements OnDestroy {
{ url: '/dynamic-content', label: 'Dynamic content' },
{ url: '/component-replacement', label: 'Component replacement' },
{ url: '/rules-engine', label: 'Rules engine' },
{ url: '/placeholder', label: 'Placeholder' }
{ url: '/placeholder', label: 'Placeholder' },
{ url: '/forms', label: 'Forms' }
]
},
{
Expand Down
5 changes: 5 additions & 0 deletions apps/showcase/src/app/forms/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Forms

A showcase page that demonstrates how to use the otter forms feature inside an application.

The page contains both a step by step explanation to guide the users as well as a sample component that can be used as a reference and that illustrates the capabilities of the feature.
34 changes: 34 additions & 0 deletions apps/showcase/src/app/forms/forms.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { AsyncPipe } from '@angular/common';
import { AfterViewInit, ChangeDetectionStrategy, Component, inject, QueryList, ViewChildren, ViewEncapsulation } from '@angular/core';
import { RouterModule } from '@angular/router';
import { O3rComponent } from '@o3r/core';
import { CopyTextPresComponent, FormsParentComponent, IN_PAGE_NAV_PRES_DIRECTIVES, InPageNavLink, InPageNavLinkDirective, InPageNavPresService } from '../../components/index';

@O3rComponent({ componentType: 'Page' })
@Component({
selector: 'o3r-forms',
standalone: true,
imports: [
RouterModule,
FormsParentComponent,
CopyTextPresComponent,
IN_PAGE_NAV_PRES_DIRECTIVES,
AsyncPipe
],
templateUrl: './forms.template.html',
styleUrl: './forms.style.scss',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FormsComponent implements AfterViewInit {
@ViewChildren(InPageNavLinkDirective)
private readonly inPageNavLinkDirectives!: QueryList<InPageNavLink>;

private readonly inPageNavPresService = inject(InPageNavPresService);

public links$ = this.inPageNavPresService.links$;

public ngAfterViewInit() {
this.inPageNavPresService.initialize(this.inPageNavLinkDirectives);
}
}
23 changes: 23 additions & 0 deletions apps/showcase/src/app/forms/forms.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterModule } from '@angular/router';
import { mockTranslationModules } from '@o3r/testing/localization';
import { FormsComponent } from './forms.component';

describe('FormsComponent', () => {
let component: FormsComponent;
let fixture: ComponentFixture<FormsComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterModule.forRoot([]), FormsComponent,...mockTranslationModules()]
}).compileComponents();

fixture = TestBed.createComponent(FormsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
3 changes: 3 additions & 0 deletions apps/showcase/src/app/forms/forms.style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
o3r-forms {

}
51 changes: 51 additions & 0 deletions apps/showcase/src/app/forms/forms.template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<h1>Forms</h1>
<div class="row">
<div class="right-nav order-1 order-lg-2 col-12 col-lg-2 sticky-lg-top pt-5 pt-lg-0">
<o3r-in-page-nav-pres
id="forms-nav"
[links]="links$ | async"
>
</o3r-in-page-nav-pres>
</div>
<div class="order-2 order-lg-1 col-12 col-lg-10">
<h2 id="forms-description">Description</h2>
<div>
<p>This module provides utilities to enhance the build of Angular reactive forms for specific use cases, including:</p>
<ul>
<li>A container/presenter structure for components</li>
<li>Handling form submission at page (or parent component) level</li>
<li>Displaying the error message outside the form</li>
</ul>
</div>

<h2 id="forms-example">Example</h2>
<div>
<p>
In the following example, we have a parent component with two subcomponents, each containing a form.
</p>
<p>
The first form requires the user to define their personal information (name and date of birth).
The second form requires the definition of the user's emergency contact information (name, phone number, and email address).
Both forms contain validators, such as certain fields being required or specific values having to follow a certain pattern.
</p>
<p>
The submit of both forms is triggered at parent component level.
</p>
<o3r-forms-parent></o3r-forms-parent>
<p>
Do not hesitate to run the application locally, if not installed yet, follow the <a routerLink="/run-app-locally">instructions</a>.
</p>
<a href="https://github.com/AmadeusITGroup/otter/blob/main/apps/showcase/src/components/showcase/localization" target="_blank" rel="noopener">Source code</a>
</div>
<h2 id="forms-install">How to install</h2>
<o3r-copy-text-pres [wrap]="true" language="bash" text="ng add @o3r/forms"></o3r-copy-text-pres>
<h2 id="forms-references">References</h2>
<div>
<ul>
<li>
<a href="https://github.com/AmadeusITGroup/otter/blob/main/packages/%40o3r/forms/README.md" target="_blank" rel="noopener">Documentation</a>
</li>
</ul>
</div>
</div>
</div>
2 changes: 2 additions & 0 deletions apps/showcase/src/app/forms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './forms.component';

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# FormsParent

Showcase of an Otter component with forms
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** Model used to create Personal Info form */
export interface PersonalInfo {
/** Name */
name: string;
/** Date of birth */
dateOfBirth: string;
}

/** Model used to create Emergency Contact form */
export interface EmergencyContact {
/** Emergency contact name */
name: string;
/** Emergency contact phone number */
phone: string;
/** Emergency contact email address */
email: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './form-models';
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { AsyncPipe, CommonModule, formatDate } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { O3rComponent } from '@o3r/core';
import { Localization } from '@o3r/localization';
import { CustomFormValidation } from '@o3r/forms';
import { CopyTextPresComponent, FormsEmergencyContactPresComponent, FormsPersonalInfoPresComponent } from '../../utilities';
import { EmergencyContact, PersonalInfo } from './contracts';
import { FormsParentTranslation, translations } from '../forms-parent/forms-parent.translation';
import { dateCustomValidator, formsParentValidatorGlobal } from '../forms-parent/forms-parent.validators';

@O3rComponent({ componentType: 'Component' })
@Component({
selector: 'o3r-forms-parent',
standalone: true,
imports: [
AsyncPipe,
CommonModule,
CopyTextPresComponent,
FormsEmergencyContactPresComponent,
FormsPersonalInfoPresComponent,
ReactiveFormsModule
],
templateUrl: '../forms-parent/forms-parent.template.html',
styleUrl: './forms-parent.style.scss',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FormsParentComponent {

/** Localization of the component */
@Input()
@Localization('./forms-parent.localization.json')
public translations: FormsParentTranslation = translations;

/** The personal info form object model */
public personalInfo: PersonalInfo = { name: '', dateOfBirth: this.formatDate(Date.now()) };
/** The emergency contact form object model */
public emergencyContact: EmergencyContact = { name: '', phone: '', email: '' };

/** The form control object bind to the personal info component */
public personalInfoFormControl: UntypedFormControl = new UntypedFormControl(this.personalInfo);
/** The form control object bind to the emergency contact component */
public emergencyContactFormControl: UntypedFormControl = new UntypedFormControl(this.emergencyContact);

public submittedFormValue = '';

public firstSubmit = true;
public firstEmergencyContactFormSubmit = true;
public firstPersonalInfoFormSubmit = true;

private readonly forbiddenName = 'Test';

/** Form validators for personal info */
public personalInfoValidators: CustomFormValidation<PersonalInfo> = {
global: formsParentValidatorGlobal(this.forbiddenName, translations.globalForbiddenName, translations.globalForbiddenNameLong, { name: this.forbiddenName }),
fields: {
dateOfBirth: dateCustomValidator(translations.dateInThePast)
}
};
/** Form validators for emergency contact */
public emergencyContactValidators: CustomFormValidation<EmergencyContact> = {
global: formsParentValidatorGlobal(this.forbiddenName, translations.globalForbiddenName, translations.globalForbiddenNameLong, { name: this.forbiddenName })
};

private formatDate(dateTime: number) {
return formatDate(dateTime, 'yyyy-MM-dd', 'en-GB');
}

/** This will store the function to make the personal info form as dirty and touched */
public _markPersonalInfoInteraction: () => void = () => {};
/** This will store the function to make the emergency contact form as dirty and touched */
public _markEmergencyContactInteraction: () => void = () => {};

/**
* Register the function to be called to mark the personal info form as touched and dirty
*
* @param fn
*/
public registerPersonalInfoInteraction(fn: () => void) {
this._markPersonalInfoInteraction = fn;
}

/**
* Register the function to be called to mark the personal emergency contact form as touched and dirty
*
* @param fn
*/
public registerEmergencyContactInteraction(fn: () => void) {
this._markEmergencyContactInteraction = fn;
}

/** submit function */
public submitAction() {
if (this.firstSubmit) {
this._markPersonalInfoInteraction();
this._markEmergencyContactInteraction();
this.firstSubmit = false;
this.firstPersonalInfoFormSubmit = false;
this.firstEmergencyContactFormSubmit = false;
}
this.submitPersonalInfoForm();
this.submitEmergencyContactForm();
this.submittedFormValue = JSON.stringify(this.personalInfoFormControl.value) + '\n' + JSON.stringify(this.emergencyContactFormControl.value);
}

/** Submit emergency contact form */
public submitPersonalInfoForm() {
if (this.firstPersonalInfoFormSubmit) {
this._markPersonalInfoInteraction();
this.firstPersonalInfoFormSubmit = false;
}
const isValid = !this.personalInfoFormControl.errors;
if (isValid) {
this.submittedFormValue = JSON.stringify(this.personalInfoFormControl.value);
// eslint-disable-next-line no-console
console.log('FORMS PARENT COMPONENT: personal info form status', this.personalInfoFormControl.status);
// eslint-disable-next-line no-console
console.log('FORMS PARENT COMPONENT: personal info form value', this.personalInfoFormControl.value);
}
// eslint-disable-next-line no-console
console.log('FORMS PARENT COMPONENT: personal info form is valid:', isValid);
}

/** Submit emergency contact form */
public submitEmergencyContactForm() {
if (this.firstEmergencyContactFormSubmit) {
this._markEmergencyContactInteraction();
this.firstEmergencyContactFormSubmit = false;
}
const isValid = !this.emergencyContactFormControl.errors;
if (isValid) {
this.submittedFormValue = JSON.stringify(this.emergencyContactFormControl.value);
// eslint-disable-next-line no-console
console.log('FORMS PARENT COMPONENT: emergency contact form status', this.emergencyContactFormControl.status);
// eslint-disable-next-line no-console
console.log('FORMS PARENT COMPONENT: emergency contact form value', this.emergencyContactFormControl.value);
}
// eslint-disable-next-line no-console
console.log('FORMS PARENT COMPONENT: emergency contact form is valid:', isValid);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"o3r-forms-parent.dateOfBirth.dateInThePast": {
"description": "Validator for date of birth",
"defaultValue": "Date of birth should be in the past"
},
"o3r-forms-parent.globalForbiddenName": {
"description": "This validator will check if the name will be the given config",
"defaultValue": "Name cannot be { name }"
},
"o3r-forms-parent.globalForbiddenName.long": {
"description": "This validator will check if the name will be the given config",
"defaultValue": "The value introduced for the name cannot be { name }"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { mockTranslationModules } from '@o3r/testing/localization';
import { FormsParentComponent } from './forms-parent.component';

describe('FormsParentComponent', () => {
let component: FormsParentComponent;
let fixture: ComponentFixture<FormsParentComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormsParentComponent, ...mockTranslationModules(), ReactiveFormsModule]
}).compileComponents();

fixture = TestBed.createComponent(FormsParentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
o3r-forms-parent {
// Your component custom SCSS
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<div class="card my-3">
<div class="row g-0">
<div class="col-6 align-items-center justify-center">
<o3r-forms-personal-info-pres
[id]="'personal-info'"
[config]="{nameMaxLength: 5}"
[customValidators]="personalInfoValidators"
(registerInteraction)="registerPersonalInfoInteraction($event)"
(submitPersonalInfoForm)="submitPersonalInfoForm()"
[formControl]="personalInfoFormControl">
</o3r-forms-personal-info-pres>
</div>
<div class="col-6 align-items-center justify-center">
<o3r-forms-emergency-contact-pres
[id]="'emergency-contact'"
[customValidators]="emergencyContactValidators"
(registerInteraction)="registerEmergencyContactInteraction($event)"
(submitEmergencyContactForm)="submitEmergencyContactForm()"
[formControl]="emergencyContactFormControl">
</o3r-forms-emergency-contact-pres>
</div>
</div>
<div class="row m-auto pb-3">
<button type="button" class="btn btn-primary" id="btn-submit" (click)="submitAction()">Submit All</button>
</div>
<div class="row">
<o3r-copy-text-pres language="html" [text]="'Submitted Form Value:\n' + submittedFormValue"></o3r-copy-text-pres>
</div>
</div>
Loading
Loading