Architecture of Web User Interfaces
SWEN-261
Introduction to Software Engineering
Department of Software Engineering
Rochester Institute of Technology
Chapters of this Lecture
2
1) View Basics: essential aspects of build UI components in Angular
3
What is Angular?
4
A View component has structure, style and behavior
Reference: Angular Templates
5
The MyPage View component gets rendered in a web browser.
The MyPage View component is composed of three elements: HTML, CSS and TypeScript.
You can use a browser’s Dev Tools to inspect the DOM structure of your View components.
To use Dev Tools: Right-click to pop-up context menu, then select Inspect item.
6
Use mouse to hover over DOM elements in the Elements tab tree display to find where that element is rendered.
Use the box-pointer icon and then select an element in the rendered view to “open up” the DOM element in the tree display,
Views can display more than simple strings and numbers; they can display complex objects (Entities) like this.
export class App implements OnInit {
// VM data for basic view binding
readonly swen261 = new Course({
code: 'SWEN-261',
title: 'Introduction to Software Engineering',
});�}
<h2>Display of Complex Objects</h2>
<div class="courseCard" [class]="swen261.curriculum">
<h3>{{swen261.code}}: </h3>
<p><em>{{swen261.title}}</em></p>
</div>
export class Course {
readonly code: string;
readonly title: string;
constructor(
json: CourseJSON,
) {
this.code = json.code;
this.title = json.title;
}
// extract the curriculum code from the course code
get curriculum(): string {
return this.code.substring(0, 4);
}
}
7
Data to populate views can be static, but also dynamic using the Observer pattern via Angular Signals.
8
<p>
Using an asynchronous signal: <br>
{{ message() }}
</p>
export class App implements OnInit {
readonly message = signal('Initializing...');
ngOnInit() {
setTimeout(() => this.message.set(
'Delayed message. Was it worth the wait?'
),
2000 // after two seconds
);
}
}
Source: Angular Signals (Medium article)
after a two second delay
HTML provides hundreds of content elements.
References:
9
CSS provides very flexible rules to style the view structure.
References:
10
Let’s look at an example of CSS styling for the LMS system.
div.courseCard {
border: 2px solid rgb(0 0 0);
border-radius: 7px;
background-color: rgba(0 0 0 / 0.25);
padding: 5px;
margin-top: 5px;
width: fit-content;
}
/* other courseCard styles not shown */
div.courseCard.SWEN {
border-color: rgb(var(--swenColor));
background-color: rgba(var(--swenColor) / 0.25);
}
div.courseCard.CSCI {
border-color: rgb(var(--csciColor));
background-color: rgba(var(--csciColor) / 0.25);
}
<div class="courseCard" [class]="swen261.curriculum">
<h4>{{swen261.code}}: </h4>
<p><em>{{swen261.title}}</em></p>
</div>
<div class="courseCard" [class]="csci141.curriculum">
<h4>{{csci141.code}}: </h4>
<p><em>{{csci141.title}}</em></p>
</div>
h1, h2, h3, h4, h5, h6 {
font-family: 'Montserrat', sans-serif;
color: var(--primaryColor);
}
/*
* CSS variables/constants
*/
:root {
--redColor: 255 0 0;
--blackColor: 0 0 0;
--primaryColor: rgb(255, 165, 0);
/* Curriculum colors */
--swenColor: 255 165 0; /* orange */
--csciColor: 200 200 255; /* light blue */
--physColor: 90 90 255; /* purple */
}
11
Global styles defined in src/styles.css
Component-specific styles.
You can influence DOM elements via attribute binding.
12
You can also perform dynamic operations when the user interacts with a DOM element.
<h3>Dynamic Element Attributes and Event Handlers</h3>
<button (click)="toggleAction($event)"
[innerText]="buttonText()"
[disabled]="buttonState()"
></button>
13
User clicks on the button…
DOM Attributes and Interaction (Part 2)
Controller code:
// VM data for element attributes and interactions
buttonText = signal('Click me!');
buttonState = signal<Nullable<string>>(null);
// VM event handlers for element interaction
toggleAction($event: Event) {
console.log($event);
this.buttonText.set('Click on me again, I dare you!');
this.buttonState.set('disabled');
}
14
CSS Style code:
button:not(:disabled) {
cursor: pointer;
border-color: rgb(var(--blueColor));
background-color: unset;
color: rgb(var(--blueColor));
}
button:disabled {
cursor: not-allowed;
border-color: rgb(var(--redColor));
color: unset;
}
You would think the DOM disabled attribute would hold a boolean value; true for disabled or false for enabled. NOPE!
A null value means “I’m enabled” and the value disabled is means “I’m disabled.”
These are truly ugly styles. Please don’t replicate these in your project. 😉
Let’s create a simple Angular widget.
Case Study: Create a simple, numeric slider widget.
15
A widget is a View component. View components can come in many “sizes”.
A parent View communicates with a child View using input signals.
The child uses output signals to communicate with the parent.
Let’s design the slider Angular widget.
16
And now the HTML template code used in App and for the Slider.
App component template code:
<h3>Simple Angular Widget</h3>
<app-slider-widget [initialValue]="5"
(valueChanged)="handleSlider($event)"
></app-slider-widget>
<p>Value: <span [innerText]="currentSliderValue()"></span></p>
Slider template code:
<input type="range"
class="slider"
[min]="minValue()"
[max]="maxValue()"
[value]="initialValue()"
(change)="updateValue($event)"
>
17
You can also use control flows in Angular Views.
18
Conditional Logic
Iteration Logic
<h2>Conditional Logic: Launch Countdown</h2>
@if (countDown() > 5) {
<p>Count down: {{countDown()}}</p>
} @else if (countDown() > 0) {
<h4 class="final countDown">{{countDown()}}</h4>
} @else {
<h4 class="final">Blast off!</h4>
}
<h2>Iteration Logic: Course List</h2>
@for (course of courseList(); track course.code) {
<div class="courseCard" [class]="course.curriculum">
<h4>{{course.code}}: </h4>
<p><em>{{course.title}}</em></p>
</div>
} @empty {
<p>Wait for it...</p>
}
1) View Basics: essential aspects of build UI components in Angular
STOP HERE
Take a break before proceeding to the next chapter.�Walk around for 5 minutes and get glass of water.
DEMO
Let’s take this moment to look at the working increment we built in this chapter.
19
2) Routing: apps contain multiple pages and how to navigate between them.
20
An application is composed of pages.
21
Let’s consider a relatively simple GUI for a Learning Management System
22
Use Cases
Page State Machine
There is only one App component. Pages are displayed within it. Routing is the process of switching between pages.
23
export const routes: Routes = [
{path: 'courses/view-all', component: ViewAllCourses}
];
<main>
<router-outlet/>
</main>
app.html
app.routes.ts
The Router Config file maps URL patterns to the Page components.
24
export const routes: Routes = [
{path: 'courses/view-all', component: ViewAllCourses},
{path: 'courses/view-detail/:courseId', component: ViewCourseDetail},
{path: 'courses/create-course', component: EditCourse},
{path: 'courses/update-course/:courseId', component: EditCourse},
];
Each path in the state machine has a corresponding URL in the router configuration.
The route path is then mapped to a specific View component to render.
Some routes can share the same View component, like Create and Update routes with EditCourse.
The Analysis model of the View all Courses user story.
25
The Design model of the View all Courses user story.
26
2) Routing: apps contain multiple pages and how to navigate between them.
STOP HERE
Take a break before proceeding to the next chapter.�Walk around for 5 minutes and get glass of water.
DEMO
Let’s take this moment to look at the working increment we built in this chapter.
27
3) Services: introduction to simple, data providers
28
What is a Service (in the frontend)?
A Service is any OO object that can be injected into other components, such as View components, to perform logic that is View-independent.
29
Angular has a built-in dependency injection mechanism.
30
@Injectable({
providedIn: 'root'
})
export class CourseService {
findAllByCurriculum(curriculum: Curriculum):
Observable<ReadonlyArray<CourseSummary>> {
// TODO logic to retrieve a collection of Course summaries
}
}
import {inject /* and others */} from '@angular/core';
import {CourseService} from '../../../services/course-service';
export class ViewAllCourses {
private readonly courseService = inject(CourseService);
readonly courseList: WritableSignal<ReadonlyArray<CourseSummary>> = signal([]);
selectCurriculum(curriculum: Curriculum) {
this.courseService.findAllByCurriculum(curriculum)
.subscribe({
next: (courses: ReadonlyArray<CourseSummary>) => {
// display the Course list in the UI
this.courseList.set(courses);
},
});
}
view-all-courses.ts
course-service.ts
The @Injectable annotation tells Angular that this class can be injected into other components. This is what the inject function does in the ViewAllCourses class (below).
Let’s take another look at the Observer pattern in this design.
31
Converting from RxJS Observable to Angular Signals.
32
import {buildSignal} from '../../utils/signal-utils';
// other code not shown�readonly courseList!: Signal<ReadonlyArray<CourseSummary>>;
ngOnInit() {
// use a Service for the list creation
this.courseList = buildSignal(this.courseService.findAllByCurriculum(Curriculum.SWEN), [], this.injector);
}
The Analysis model of the View a Course Details user story.
33
The Design model of the View a Course Details user story.
34
The Angular Router can pass data to the View.
{path: 'courses/view-detail/:courseId', component: ViewCourseDetail},
export class ViewCourseDetail implements OnInit {
private readonly activatedRoute = inject(ActivatedRoute);
private readonly courseService = inject(CourseService);
readonly course: WritableSignal<Nullable<Course>> = signal(null);
ngOnInit() {
this.activatedRoute.params.subscribe(params => {
const id: UUID = params['courseId'];
this.courseService.findById(id).subscribe(course => {
this.course.set(course);
});
});
}
}
35
Use a Data Resolver component in your routes.
{path: 'courses/view-detail/:courseId', component: ViewCourseDetail,
resolve: {course: CourseResolver},
},
@Injectable({providedIn: 'root'})
export class CourseResolver implements Resolve<Nullable<Course>> {
private readonly courseService = inject(CourseService);
private readonly router = inject(Router);
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): MaybeAsync<Nullable<Course> | RedirectCommand> {
const courseId = route.params['courseId'] as UUID;
if (isUUID(courseId)) {
// return the Observable returned by the 'find' command
return this.courseService.findById(courseId);
} else {
// return a redirect command that issues a 404 page
return new RedirectCommand(this.router.parseUrl('/page-not-found'))
}
}
}
36
Update the View component to use this resolver:
export class ViewCourseDetail implements OnInit {
readonly course: WritableSignal<Nullable<Course>> = signal(null);
// more code not shown
ngOnInit() {
this.activatedRoute.data
.pipe(
// extract the 'course' data object from the resolver
map((data:Data) => data['course'] as Course),
)
.subscribe({
next: (c: Course) => {
this.course.set(c);
}
});
}
}
37
Let’s look at a visual model of the Router operation.
38
3) Services: introduction to simple, data providers
STOP HERE
Take a break before proceeding to the next chapter.�Walk around for 5 minutes and get glass of water.
DEMO
Let’s take this moment to look at the working increment we built in this chapter.
39
4) Forms: build data-entry View components
40
Template-driven forms is good for simple UI interactions.
Curriculum selection in View All Courses page.
41
Use ngModel and ngModelChange to select a Curriculum to display.
Use <select> HTML control in the View template:
<label for="curriculum">Change curricula: </label>
<select id="curriculum" [(ngModel)]="selectedCurriculum" (ngModelChange)="selectCurriculum($event)">
@for (curr of curricula; track curr) {
<option [ngValue]="curr">{{curr}}</option>
}
</select>
View Controller uses ngModelChanges to update the list:
selectCurriculum(curriculum: Curriculum) {
this.courseService.findAllByCurriculum(curriculum).subscribe({
next: (summaries) => this.courseList.set(summaries),
});
}
42
Reactive Forms is good for more complex data-entry views.
43
The Analysis model and UX design of the Create a Course user story.
44
This is only half of the Create a Course data-entry View. The whole thing wouldn’t fit.
The Design model for the Create a Course user story.
45
Let’s see how this maps to the UX design.
46
HTML provides a fairly rich set of basic form control widgets.
47
Reactive Forms handle automated field validation and error messages.
48
The Submit button is disabled as long as any form field is invalid. Again Angular forms API provides the necessary hooks to make this easy.
4) Forms: build data-entry View components
STOP HERE
Take a break before proceeding to the next chapter.�Walk around for 5 minutes and get glass of water.
DEMO
Let’s take this moment to look at the working increment we built in this chapter.
49
5: Services: that provide CRUD operations
In the previous chapter we focused on the View-side of building data-entry pages; let’s take a brief pause to discuss the Service-side.
50
The Analysis model of the Update a Course user story.
51
The Design model of the Update a Course user story.
52
How to pre-populate the EditCourse view with an existing Course.
ngOnInit(): void {
this.activatedRoute.data
.pipe(
map((data:Data) => data['course'] ?? null),
)
.subscribe({
next: (course: Nullable<Course>) => {
this.form = this.makeForm(course);
}
});
}
private makeForm(course: Nullable<Course>):� FormGroup<EditCourseForm> {
// extract initial data
const initialData : EditCourseData = {
id: course?.id ?? null,
code: course?.code ?? '',
title: course?.title ?? '',
units: course?.units ?? 3,
// some fields not shown
}
// create form configuration (partial)
return this.fb.group({
id: new FormControl<Nullable<UUID>>(initialData.id, {...})
code: new FormControl<CourseCode>(initialData.code, {...})
title: new FormControl<string>(initialData.title, {...})
units: new FormControl<number>(initialData.units, {...})
53
The View is initialized with the ActivatedRoute and pulls the Course from the data Observable.
The Course object is passed to the method that builds the form. It extracts the Course data into an initialData object literal. If there is no Course (we are creating not updating), then we provide default values.
That is then used to populate the initial values of each form control for the form group.
The Analysis model of the Delete a Course user story.
54
Basically, the same Analysis model for being able to delete Courses from the View All Courses page.
The Design model of the Delete a Course user story.
55
5: Services: that provide CRUD operations
In the previous chapter we focused on the View-side of building data-entry pages; let’s take a brief pause to discuss the Service-side.
STOP HERE
Take a break before proceeding to the next chapter.�Walk around for 5 minutes and get glass of water.
DEMO
Let’s take this moment to look at the working increment we built in this chapter.
56
6) View Composition: decompose complex views into smaller, easier to maintain subviews.
57
Pages, and Views in general, are a natural hierarchy of subviews.
<body>� <h2>View All Courses</h2>
<p><a routerLink="/courses/create-course">Create a Course</a></p>� </body>
58
Let’s consider the View All Courses page and propose new subviews.
59
The page is roughly composed of three elements: a title (<h2>), a curriculum filter (<select>) and a table of Courses.
Let’s propose two new subview components: a CourseCard and a fancy IconButton.
Create subview components when you see duplicated view code.
60
<div class="courseCard"
[class]="course.curriculum"
(click)="viewCourse(course)"
>
<h4>{{ course.code }}: </h4>
<p><em>{{ course.title }}</em></p>
</div>
<app-course-card [course]="course"
(courseClicked)="viewCourse($event)"
width="100%"
></app-course-card>
build a subview component
Creating widgets allows for greater consistency and ease of development.
Our current HTML buttons are okay:
We can make them a little more fancy by adding a icon:
We’ll use the FontAwesome collection of icons.
61
The template of the IconButton is simple:
<button type="{{type()}}"
class="icon-button"
(click)="activateButton($event)"
[disabled]="disabled()"
>
<i class="fa {{iconName()}}"></i> {{ title() }}
</button>
62
The name of the FontAwesome icon, such as fa-pencil (used for Edit) or fa-ban (used for Delete).
The text title of the button, such as “Edit” or “Delete”.
The disabled button flag is controlled by an input Signal so it is dynamically controlled by the parent View.
Sometimes we need the Parent view to inject HTML content into the body of a Child view.
<app-course-detail-section title="Code">
<p class="projected-content">
{{ courseCode() }}
</p>
</app-course-detail-section>
<section>
<h3> {{ title() }} </h3>
<ng-content></ng-content>
</section>
63
Paragraph is injected into the section below the heading.
ViewCourseDetails
CourseDetailSection
6) View Composition: decompose complex views into smaller, easier to maintain subviews.
STOP HERE
Take a break before proceeding to the next chapter.�Walk around for 5 minutes and get glass of water.
DEMO
Let’s take this moment to look at the working increment we built in this chapter.
64
7) Global Layout: add UI elements that are common across all pages.
65
Most web applications use a global header and footer that is shared across all pages. Use the App view for that.
66
Header has application title and nav and user controls.
Footer usually has Copyright info and year.
The Header and Footer is part of the App component.
<header>
<h1>{{appTitle}}</h1>
<nav>
<a>Global nav (TBD)</a>
</nav>
<div class="user-controls">
<a>User controls (TBD)</a>
</div>
</header>
<main>
<router-outlet/>
</main>
<footer>
<p>SWEN-261 © {{year}}</p>
</footer>
67
Global navigation bar is usually either at the top or on the left side of the app.
68
Nav links that are shared across all pages.
Global navigation bar is usually a list of links.
<header>
<h1>{{appTitle}}</h1>
<nav>
<ul>
<li><a routerLink="/courses/view-all">View all courses</a></li>
<li><a routerLink="/courses/create-course">Create a course</a></li>
</ul>
</nav>
<div class="user-controls">
<a>User controls (TBD)</a>
</div>
</header>
69
Fixed position for Header and Footer
70
The Header is fixed at the top of the page.
The Footer is fixed at the bottom of the page.
The Page content is fixed in the center and is scrollable.
7) Global Layout: add UI elements that are common across all pages.
STOP HERE
Take a break before proceeding to the next chapter.�Walk around for 5 minutes and get glass of water.
DEMO
Let’s take this moment to look at the working increment we built in this chapter.
71
8: Security: users, authentication and authorization
72
What is security?
73
Security code can be tricky. We will provide you with the essentials that you can tweak as needed.
74
Create a Route guard to protect secure pages.
75
Security information to be used to conditionalize aspects of the UI.
76
Log in link is only shown when no one is logged in.
The Delete button is disabled when no one is logged in.
If an Admin logs in, then the Delete button is enabled.
If an Student logs in, then the Delete button is hidden.
The UserWidget in the global layout provide a pop-up dialog.
77
Security is a complex topic, we are just scratching the surface…
78
8: Security: users, authentication and authorization
STOP HERE
Take a break before proceeding to the next chapter.�Walk around for 5 minutes and get glass of water.
DEMO
Let’s take this moment to look at the working increment we built in this chapter.
79
What’s coming up…
But first… do the Activity: Setup Your Dev Environment before the next class.
80
STOP HERE
The rest of this lecture is beyond the scope of the UI Spike activity.
You may read the following content to learn more about about topics that I think will be useful to you. Come back to these pages as reference throughout the semester as a reference guide.
81
9: Dev Tools: debug your View and Service components on the browser
82
10. Asynchronicity: the main types of Observer pattern tools
83