Skip to content

Commit

Permalink
[#11878] Upgrade instructor request form UI (#12929)
Browse files Browse the repository at this point in the history
* Add confirmation prompt

* Remove old form iframe

* Improve declaration view spacing

* Edit page heading phrasing for clarity

* Create request form

* Add validation messages

* Fix form validation

* Set up form submission confirmation

* Create submission acknowledgement view

* Fix URL checking regex

* Fix initial state

* Display placeholder when optional field is empty

* Fix code style

* Edit comment for clarity

* Fix institution and country combination

Co-authored-by: Jay Aljelo Ting <65202977+jayasting98@users.noreply.github.com>

* Fix naming

* Remove hard line break

* Add explanatory comment for regex

* Remove newline

* Add newlines at end of file

* Clear styles file

* Re-add styles file

* Include test

* Add test cases for requestSubmissionEvent

* Improve test case readability

* Edit test case name for clarity

* Add snapshot tests

* Revert "Add snapshot tests"

This reverts commit ec7395d.

* Fix lint errors

* Rename methods to be clearer

* Disable submit button when not ready to submit

---------

Co-authored-by: Jay Aljelo Ting <65202977+jayasting98@users.noreply.github.com>
  • Loading branch information
xenosf and jayasting98 committed Mar 27, 2024
1 parent cb29108 commit f7eaa61
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type InstructorRequestFormModel = {
name: string,
institution: string,
country: string,
email: string,
homePage: string,
comments: string,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<p aria-hidden="true">
<!-- aria-hidden as screen readers use inputs' required attribute instead (and cannot see the asterisks) -->
Questions marked with an asterisk <span class="red-font">*</span> are required.
</p>
<form (ngSubmit)="onSubmit()" [formGroup]="arf">
<div class="form-group {{checkIsFieldRequired(name) ? 'required' : ''}}">
<label for="name" id="name-label" class="qn">
Full Name
</label>
<p class="help-block">
This is the name that will be shown to your students. You may include salutation (Dr. Prof. etc.)
</p>
<input class="form-control {{getFieldValidationClasses(name)}}" type="text" id="name" autocomplete="name"
[formControl]="name" [required]="checkIsFieldRequired(name)" [attr.aria-invalid]="checkIsFieldInvalid(name)">
<div *ngIf="checkIsFieldInvalid(name)" role="alert" aria-describedby="name-label" tabindex="0" class="invalid-feedback">
Please enter your name.
</div>
</div>
<br>
<div class="form-group {{checkIsFieldRequired(institution) ? 'required' : ''}}">
<label for="institution" id="institution-label" class="qn">
University/school/institution
</label>
<p class="help-block">
Please give full name of the university/institution.
</p>
<input class="form-control {{getFieldValidationClasses(institution)}}" type="text" id="institution"
autocomplete="organization" [formControl]="institution" [required]="checkIsFieldRequired(institution)"
[attr.aria-invalid]="checkIsFieldInvalid(institution)">
<div *ngIf="checkIsFieldInvalid(institution)" role="alert" aria-describedby="institution-label" tabindex="0"
class="invalid-feedback">
Please enter your institution.
</div>
</div>
<br>
<div class="form-group {{checkIsFieldRequired(country) ? 'required' : ''}}">
<label for="country" id="country-label" class="qn">
Country
</label>
<p class="help-block">
Which country is your university/institution based in?
</p>
<input class="form-control {{getFieldValidationClasses(country)}}" type="text" id="country"
autocomplete="country-name" [formControl]="country" [required]="checkIsFieldRequired(country)"
[attr.aria-invalid]="checkIsFieldInvalid(country)">
<div *ngIf="checkIsFieldInvalid(country)" role="alert" aria-describedby="country-label" tabindex="0"
class="invalid-feedback">
Please enter your institution's country.
</div>
</div>
<br>
<div class="form-group {{checkIsFieldRequired(email) ? 'required' : ''}}">
<label for="email" id="email-label" class="qn">
Official email address
</label>
<p class="help-block">
Please use the email address <b>given to you by your school/university</b>
(not your personal Gmail/Hotmail address).
Note that this email address will be visible to the students you enroll in TEAMMATES.
</p>
<input class="form-control {{getFieldValidationClasses(email)}}" type="email" id="email" autocomplete="email"
[formControl]="email" [required]="checkIsFieldRequired(email)" [attr.aria-invalid]="checkIsFieldInvalid(email)">
<div *ngIf="checkIsFieldInvalid(email)" role="alert" aria-describedby="email-label" tabindex="0"
class="invalid-feedback">
Please enter a valid email address.
</div>
</div>
<br>
<div class="form-group {{checkIsFieldRequired(homePage) ? 'required' : ''}}">
<label for="homePage" id="homePage-label" class="qn">
URL of your home page (if any)
</label>
<input class="form-control {{getFieldValidationClasses(homePage)}}" type="url" id="homePage" autocomplete="url"
[formControl]="homePage" [required]="checkIsFieldRequired(homePage)" [attr.aria-invalid]="checkIsFieldInvalid(homePage)">
<div *ngIf="checkIsFieldInvalid(homePage)" role="alert" aria-describedby="homePage-label" tabindex="0"
class="invalid-feedback">
Please enter a valid URL.
</div>
</div>
<br>
<div class="form-group {{checkIsFieldRequired(comments) ? 'required' : ''}}">
<label for="comments" id="comments-label" class="qn">
Any other comments/queries
</label>
<textarea class="form-control {{getFieldValidationClasses(comments)}}" [formControl]="comments"
[attr.aria-invalid]="checkIsFieldInvalid(comments)"></textarea>
</div>
<br>
<button type="submit" class="btn btn-primary" id="submit-button" [disabled]="!checkCanSubmit()">
Submit
</button>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
label.qn {
font-weight: bold;
font-size: 1rem;
margin-bottom: 0.3rem;
}

.form-group {
margin-bottom: 0.5rem;
}

.form-group.required > label::after {
content:"*";
color: red;
}

.help-block {
margin-bottom: 0.8rem;
}

.red-font {
color: red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { first } from 'rxjs';
import { InstructorRequestFormModel } from './instructor-request-form-model';
import { InstructorRequestFormComponent } from './instructor-request-form.component';

describe('InstructorRequestFormComponent', () => {
let component: InstructorRequestFormComponent;
let fixture: ComponentFixture<InstructorRequestFormComponent>;
const typicalModel: InstructorRequestFormModel = {
name: 'John Doe',
institution: 'Example Institution',
country: 'Example Country',
email: 'jd@example.edu',
homePage: 'xyz.example.edu/john',
comments: '',
};

/**
* Fills in form fields with the given data.
*
* @param data Data to fill form with.
*/
function fillFormWith(data: InstructorRequestFormModel): void {
component.name.setValue(data.name);
component.institution.setValue(data.institution);
component.country.setValue(data.country);
component.email.setValue(data.email);
component.homePage.setValue(data.homePage);
component.comments.setValue(data.comments);
}

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [InstructorRequestFormComponent],
imports: [ReactiveFormsModule],
});
fixture = TestBed.createComponent(InstructorRequestFormComponent);
component = fixture.componentInstance;

fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should emit requestSubmissionEvent once when submit button is clicked', () => {
jest.spyOn(component.requestSubmissionEvent, 'emit');

fillFormWith(typicalModel);
const submitButton = fixture.debugElement.query(By.css('#submit-button'));
submitButton.nativeElement.click();

expect(component.requestSubmissionEvent.emit).toHaveBeenCalledTimes(1);
});

it('should emit requestSubmissionEvent with the correct data when form is submitted', () => {
// Listen for emitted value
let actualModel: InstructorRequestFormModel | null = null;
component.requestSubmissionEvent.pipe(first())
.subscribe((data: InstructorRequestFormModel) => { actualModel = data; });

fillFormWith(typicalModel);
component.onSubmit();

expect(actualModel).toBeTruthy();
expect(actualModel!.name).toBe(typicalModel.name);
expect(actualModel!.institution).toBe(typicalModel.institution);
expect(actualModel!.country).toBe(typicalModel.country);
expect(actualModel!.email).toBe(typicalModel.email);
expect(actualModel!.homePage).toBe(typicalModel.homePage);
expect(actualModel!.comments).toBe(typicalModel.comments);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { InstructorRequestFormModel } from './instructor-request-form-model';

// Use regex to validate URL field as Angular does not have a built-in URL validator
// eslint-disable-next-line
const URL_REGEX = /(https?:\/\/)?(www\.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)|(https?:\/\/)?(www\.)?(?!ww)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/;

@Component({
selector: 'tm-instructor-request-form',
templateUrl: './instructor-request-form.component.html',
styleUrls: ['./instructor-request-form.component.scss'],
})
export class InstructorRequestFormComponent {

arf = new FormGroup({
name: new FormControl('', [Validators.required]),
institution: new FormControl('', [Validators.required]),
country: new FormControl('', [Validators.required]),
email: new FormControl('', [Validators.required, Validators.email]),
homePage: new FormControl('', [Validators.pattern(URL_REGEX)]),
comments: new FormControl(''),
}, { updateOn: 'submit' });

// Create members for easier access of arf controls
name = this.arf.controls.name;
institution = this.arf.controls.institution;
country = this.arf.controls.country;
email = this.arf.controls.email;
homePage = this.arf.controls.homePage;
comments = this.arf.controls.comments;

hasSubmitAttempt = false;

@Output() requestSubmissionEvent = new EventEmitter<InstructorRequestFormModel>();

checkIsFieldRequired(field: FormControl): boolean {
return field.hasValidator(Validators.required);
}

checkIsFieldInvalid(field: FormControl): boolean {
return field.invalid;
}

checkCanSubmit(): boolean {
return true; // TODO: API integration
}

getFieldValidationClasses(field: FormControl): string {
let str = '';
if (this.hasSubmitAttempt) {
if (field.invalid) {
str = 'is-invalid';
} else if (field.value !== '') {
str = 'is-valid';
}
}
return str;
}

onSubmit(): void {
this.hasSubmitAttempt = true;

if (this.arf.invalid) {
// Do not submit form
return;
}

const name = this.name.value!.trim();
const email = this.email.value!.trim();
const country = this.country.value!.trim();
const institution = this.institution.value!.trim();
const combinedInstitution = `${institution}, ${country}`;
const homePage = this.homePage.value!;
const comments = this.comments.value!.trim();

const submittedData = {
name,
email,
institution: combinedInstitution,
homePage,
comments,
};
// TODO: connect to API
// eslint-disable-next-line
submittedData; // PLACEHOLDER

// Pass form input to parent to display confirmation
this.requestSubmissionEvent.emit({
name,
institution,
country,
email,
homePage,
comments,
});
}
}
74 changes: 63 additions & 11 deletions src/web/app/pages-static/request-page/request-page.component.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,66 @@
<h1 class="color-orange">
Request for an Account
Request for an Instructor Account
</h1>
<div *ngIf="accountRequestFormUrl">
<p>
Cannot see the request form below? <a [href]="accountRequestFormUrl" target="_blank" rel="noopener noreferrer">Click here.</a>
</p>
<iframe [src]="accountRequestFormUrl" width="760px" height="880px" frameborder="0" marginheight="0" marginwidth="0">
Loading...
</iframe>
</div>
<div *ngIf="!accountRequestFormUrl">
The URL for the account request form is not set.
<div class="col-xs-12 col-md-10 col-lg-8 col-xl-7 col-xxl-6">
<div *ngIf="!submittedFormData">
<p>
Request for an instructor account using this form if you are an instructor and want to use TEAMMATES to manage peer evaluations and/or other feedback paths of your students.
</p>
<hr>
<div *ngIf="!isDeclarationDone">
<p>
Note: <b>Students should not use this form to request for TEAMMATES accounts</b>, as students do not need accounts to use TEAMMATES. Instead, TEAMMATES will email students (who have been added to TEAMMATES by a course instructor) an access link when there is a TEAMMATES session available for them to access.
</p>
<a type="button" class="btn btn-secondary" tmRouterLink="/web/front/home">Back to home page</a>
<button type="button" class="btn btn-primary ms-3" (click)="onDeclarationButtonClicked()">I am an instructor</button>
</div>
<div *ngIf="isDeclarationDone">
<tm-instructor-request-form *ngIf="!submittedFormData" (requestSubmissionEvent)="onRequestSubmitted($event)"></tm-instructor-request-form>
</div>
<hr>
</div>
<div *ngIf="submittedFormData">
<p>
Your request has been submitted successfully:
</p>
<table class="table table-bordered my-3">
<tbody>
<tr>
<th scope="row" class="col-3">Full Name</th>
<td>{{submittedFormData.name}}</td>
</tr>
<tr>
<th scope="row">Institution</th>
<td>{{submittedFormData.institution}}</td>
</tr>
<tr>
<th scope="row">Country</th>
<td>{{submittedFormData.country}}</td>
</tr>
<tr>
<th scope="row">Email</th>
<td>{{submittedFormData.email}}</td>
</tr>
<tr>
<th scope="row">Home Page URL</th>
<td>
{{submittedFormData.homePage}}
<span class="empty-field-placeholder" *ngIf="!submittedFormData.homePage"></span>
</td>
</tr>
<tr>
<th scope="row">Comments</th>
<td>
{{submittedFormData.comments}}
<span class="empty-field-placeholder" *ngIf="!submittedFormData.comments"></span>
</td>
</tr>
</tbody>
</table>
<p>
We have sent an acknowledgement email to your email address <b>{{submittedFormData.email}}</b>.
Please check your email inbox or spam folder.
If you do not receive the acknowledgement email within 1 hour, please <a tmRouterLink="/web/front/contact">contact</a> us.
</p>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.empty-field-placeholder::after {
content: "(empty)";
opacity: 0.5;
font-style: italic;
}
Loading

0 comments on commit f7eaa61

Please sign in to comment.