Skip to content

Commit

Permalink
feat: update forms doc
Browse files Browse the repository at this point in the history
  • Loading branch information
sdo-1A committed May 16, 2024
1 parent bbae039 commit ddba39a
Show file tree
Hide file tree
Showing 44 changed files with 1,667 additions and 1,343 deletions.
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.
33 changes: 33 additions & 0 deletions apps/showcase/src/app/forms/forms.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { AsyncPipe } from '@angular/common';
import { AfterViewInit, ChangeDetectionStrategy, Component, QueryList, ViewChildren, ViewEncapsulation } from '@angular/core';
import { RouterModule } from '@angular/router';
import { O3rComponent } from '@o3r/core';
import { CopyTextPresComponent, FormsPresComponent, IN_PAGE_NAV_PRES_DIRECTIVES, InPageNavLink, InPageNavLinkDirective, InPageNavPresService } from '../../components/index';

@O3rComponent({ componentType: 'Page' })
@Component({
selector: 'o3r-forms',
standalone: true,
imports: [
RouterModule,
FormsPresComponent,
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>;
public links$ = this.inPageNavPresService.links$;

constructor(private readonly inPageNavPresService: InPageNavPresService) {}

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-pres></o3r-forms-pres>
<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';

3 changes: 3 additions & 0 deletions apps/showcase/src/components/showcase/forms/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# FormsPres

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';
158 changes: 158 additions & 0 deletions apps/showcase/src/components/showcase/forms/forms-pres.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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 { FormsPresTranslation, translations } from './forms-pres.translation';
import { dateCustomValidator, formsPresValidatorGlobal } from './forms-pres.validators';

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

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

/** The personal info form object model */
public personalInfo: PersonalInfo;
/** The emergency contact form object model */
public emergencyContact: EmergencyContact;

/** Form validators for personal info */
public personalInfoValidators: CustomFormValidation<PersonalInfo>;
/** Form validators for emergency contact */
public emergencyContactValidators: CustomFormValidation<EmergencyContact>;

/** The form control object bind to the personal info component */
public personalInfoFormControl: UntypedFormControl;
/** The form control object bind to the emergency contact component */
public emergencyContactFormControl: UntypedFormControl;

public submittedFormValue = '';

public firstSubmit = true;
public firstEmergencyContactFormSubmit = true;
public firstPersonalInfoFormSubmit = true;
private readonly forbiddenName = 'Test';

constructor() {
this.translations = translations;
this.personalInfo = { name: '', dateOfBirth: this.formatDate(Date.now()) };
this.personalInfoFormControl = new UntypedFormControl(this.personalInfo);
this.emergencyContact = { name: '', phone: '', email: '' };
this.emergencyContactFormControl = new UntypedFormControl(this.emergencyContact);
this.personalInfoValidators = {
global: formsPresValidatorGlobal(this.forbiddenName, translations.globalForbiddenName, translations.globalForbiddenNameLong, { name: this.forbiddenName }),
fields: {
dateOfBirth: dateCustomValidator(translations.dateInThePast)
}
};
this.emergencyContactValidators = {
global: formsPresValidatorGlobal(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;
}
const isValid = !this.personalInfoFormControl.errors && !this.emergencyContactFormControl.errors;
if (isValid) {
this.submittedFormValue = JSON.stringify(this.personalInfoFormControl.value) + '\n' + JSON.stringify(this.emergencyContactFormControl.value);
// eslint-disable-next-line no-console
console.log('FORMS COMPONENT: personal info form status', this.personalInfoFormControl.status);
// eslint-disable-next-line no-console
console.log('FORMS COMPONENT: emergency contact form status', this.emergencyContactFormControl.status);
// eslint-disable-next-line no-console
console.log('FORMS COMPONENT: value of forms', this.personalInfoFormControl.value, this.emergencyContactFormControl.value);
}
// eslint-disable-next-line no-console
console.log('FORMS COMPONENT: forms are valid:', isValid);
}

/** 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 COMPONENT: personal info form status', this.personalInfoFormControl.status);
// eslint-disable-next-line no-console
console.log('FORMS COMPONENT: personal info form value', this.personalInfoFormControl.value);
}
// eslint-disable-next-line no-console
console.log('FORMS 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 COMPONENT: emergency contact form status', this.emergencyContactFormControl.status);
// eslint-disable-next-line no-console
console.log('FORMS COMPONENT: emergency contact form value', this.emergencyContactFormControl.value);
}
// eslint-disable-next-line no-console
console.log('FORMS COMPONENT: emergency contact form is valid:', isValid);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"form.dateOfBirth.dateInThePast": {
"description": "Validator for date of birth",
"defaultValue": "Date of birth should be in the past"
},
"form.globalForbiddenName": {
"description": "This validator will check if the name will be the given config",
"defaultValue": "Name cannot be { name }"
},
"form.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 }"
}
}
23 changes: 23 additions & 0 deletions apps/showcase/src/components/showcase/forms/forms-pres.spec.ts
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 { FormsPresComponent } from './forms-pres.component';

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

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

fixture = TestBed.createComponent(FormsPresComponent);
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-pres {
// Your component custom SCSS
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="card my-3">
<div class="row g-0">
<div class="col-6 align-items-center justify-center">
<o3r-forms-personal-info-pres
[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
[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

0 comments on commit ddba39a

Please sign in to comment.