-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
44 changed files
with
1,667 additions
and
1,343 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
o3r-forms { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './forms.component'; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# FormsPres | ||
|
||
Showcase of an Otter component with forms |
17 changes: 17 additions & 0 deletions
17
apps/showcase/src/components/showcase/forms/contracts/form-models.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './form-models'; |
158 changes: 158 additions & 0 deletions
158
apps/showcase/src/components/showcase/forms/forms-pres.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
apps/showcase/src/components/showcase/forms/forms-pres.localization.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
23
apps/showcase/src/components/showcase/forms/forms-pres.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
3 changes: 3 additions & 0 deletions
3
apps/showcase/src/components/showcase/forms/forms-pres.style.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
o3r-forms-pres { | ||
// Your component custom SCSS | ||
} |
27 changes: 27 additions & 0 deletions
27
apps/showcase/src/components/showcase/forms/forms-pres.template.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.