MP Mash: Building an Elo Rating App in Angular
Date Published

The scene in *The Social Network* where Eduardo Saverin scrawls an Elo rating algorithm on a dorm window is memorable for a reason: it makes a mathematical idea look like something one person can sit down and build. I wanted to see whether the same was true — whether the Elo calculation that governs chess and esports rankings could be wired into a working Angular application with a real database, real photos, and a real ranking table that updated as you voted.
The result is *MP Mash* — a "hot or not" game for Members of Parliament, with an Elo rating underneath. This post is what I built, why, and the specific decisions that made it work.

Scoreboard

Profiles

## Situation
I was curious about the Elo rating system. In the film, it is drawn on a window in a matter of minutes, but Elo is a real, well-defined ranking algorithm with a specific property: it is *self-correcting*. Upsets move the numbers more than expected wins, so the ranking converges toward an accurate picture as votes accumulate. That property is what makes it interesting — not the "hot or not" framing, which is incidental.
I also wanted a project that would exercise Angular 8 properly. Not a tutorial app, but something with routing, forms, tables, HTTP, and a real backend — enough moving parts that I would have to make the architectural decisions myself rather than following someone else's scaffolding.

## Task
Build an Angular 8 application that:
- Presents two randomly selected MPs side-by-side for the user to choose between
- Updates both MPs' Elo ratings based on the outcome
- Displays a sortable scoreboard of all MPs
- Shows individual MP profiles at their own routes
- Uses Angular Material for the UI shell
- Talks to a PHP/MySQL backend via HTTP
- Handles form validation and error states properly
## Action
**1. Decomposed the application by responsibility, not by convenience.**
Five components, each with a distinct job:
- `AppComponent` — the shell, with the toolbar and router outlet
- `HomeComponent` — the landing state
- `MpmashComponent` — the voting interface
- `MpmashScoreboardComponent` — the league table
- `MpProfileComponent` — the individual MP view
Each component owns one screen or one capability. Nothing is duplicated between them, and each can be understood without reading the others.
**2. Designed a hierarchical route structure that reflects the domain.**
```ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'mp/profile/:id', component: MpProfileComponent },
{ path: 'mp/mash', component: MpmashComponent },
{ path: 'mp/mash/scores', component: MpmashScoreboardComponent }
];
```
The URLs read as a hierarchy: `/mp` for Members of Parliament, `/mp/mash` for the game, `/mp/mash/scores` for the scoreboard, `/mp/profile/:id` for an individual. The route structure tells you what the app does without opening a single component.
**3. Isolated all HTTP calls in a single injectable service.**
`DataService` is the only place in the codebase that calls `HttpClient`. Five methods — `getAllMPs`, `getMP`, `getTwoRandomMPs`, `countMPs`, `updateMPRatings` — cover every backend interaction. Components never construct URLs or import the HTTP client directly.
**4. Registered the service at the application root.**
```ts
@Injectable({
providedIn: 'root'
})
```
Angular's modern DI syntax. One instance for the whole app, tree-shakable if unused, no manual provider registration. The alternative — listing it in `providers` in `AppModule` — is the older pattern and adds boilerplate for no benefit.
**5. Typed every HTTP response with an `MP` class.**
```ts
export class MP {
constructor(
id: string,
name: string,
gender: string,
rating: string,
img: string) {}
}
```
Every response the service returns is an `MP[]`. Components know exactly what fields exist. The compiler catches a typo in `mp.constit` before it becomes a runtime `undefined`.
**6. Piped every HTTP call through `map` and `catchError`.**
```ts
return this.http.get(`${this.baseUrl}/getAllMPs/`).pipe(
map((res) => {
this.mps = res['data'];
return this.mps;
}),
catchError(this.handleError)
);
```
The `map` extracts the payload from the API's envelope (`res['data']`) so callers receive the array directly. The `catchError` intercepts failures. Components subscribe and receive either data or an error — never an unhandled rejection.
**7. Centralised error handling in one method.**
```ts
private handleError(error: HttpErrorResponse) {
console.log(error);
return throwError('Error! something went wrong.');
}
```
Every HTTP call in the service routes through the same error handler. If the error strategy changes — logging, tracking, user-facing toasts — it changes in one place.
**8. Encapsulated form validation in a helper method.**
```ts
public hasError = (controlName: string, errorName: string) => {
return this.scoreboardForm.controls[controlName].hasError(errorName);
}
```
The template calls `hasError('name', 'required')`. That pattern — the form state, the control name, the error key — is expressed once in the component and reused in every error message. Without the helper, the template would repeat the same long chain at every field.
**9. Built the scoreboard form with Reactive Forms and validators.**
```ts
this.scoreboardForm = new FormGroup({
name: new FormControl('', [Validators.required, Validators.minLength(0), Validators.maxLength(60)]),
gender: new FormControl('', [Validators.required])
});
```
Declarative validation. The rules live in the component, the template binds to the form, and the browser form element is a projection of the model rather than the source of truth.
**10. Bound the submit button's disabled state to the form's validity.**
```html
<button mat-raised-button (click)="filterScoreboard()" color="primary" [disabled]="!scoreboardForm.valid">
```
The user cannot submit an invalid form because the button itself refuses to activate. The form's validity is the single source of truth for whether the action is permitted.
**11. Used `MatTableDataSource` and `MatTable` for the scoreboard.**
```ts
dataScoreboard: MatTableDataSource<dataElement>;
@ViewChild(MatTable, {static:true}) table: MatTable<any>;
```
The scoreboard is a proper Material table with defined columns — `['Image', 'Name', 'Constituency', 'Rating']` — driven by a `MatTableDataSource`. When the underlying data changes, `this.table.renderRows()` refreshes the view. No manual DOM manipulation.
**12. Made the scoreboard rows navigable.**
```html
<a [routerLink]="['/mp/profile', element.Id]">{{ element.Name }}</a>
```
Every name in the table is a link to that MP's profile page. The scoreboard is not a dead end — it's a gateway to the individual records. This is the difference between a data dump and a navigable application.
**13. Used FlexLayout for responsive behaviour without custom media queries.**
```html
<div class="container" fxLayout="row" fxLayout.xs="column" fxLayoutWrap fxLayoutGap="0.5%" fxLayoutAlign="center">
```
The voting interface is a row on desktop and a column on small screens (`fxLayout.xs="column"`). The behaviour is declared in the markup, not in a separate CSS file with a breakpoint query. FlexLayout handles the projection.
**14. Modelled the two-choice voting with a single arithmetic expression.**
```ts
voteForMP(mp_chosen: number) {
let mp_not_chosen = 1 - mp_chosen;
let winner_mp = this.twoMPsData[mp_chosen]["id"];
let loser_mp = this.twoMPsData[mp_not_chosen]["id"];
...
}
```
Given a choice of `0` or `1`, the other index is always `1 - choice`. No branch, no `if/else`, no `[0, 1].find(x => x !== choice)`. One line of arithmetic that is obviously correct on inspection.
**15. Reflected gender selection through the UI's own disabled state.**
```html
<button (click)="chooseGender(0)" [disabled]="gender_chosen==0" color="primary">Male MPs</button>
<button (click)="chooseGender(1)" [disabled]="gender_chosen==1" color="primary">Female MPs</button>
```
The currently selected gender's button is disabled. The user cannot "switch" to the option that is already active, and the visual state of the buttons communicates the current selection without needing an additional indicator.
**16. Encoded the Elo algorithm correctly — and noted where the film gets it wrong.**
The README records it explicitly:
```
Ea = 1 / (1 + 10^((Rb - Ra)/400))
Eb = 1 / (1 + 10^((Ra - Rb)/400))
```
The version drawn on the window in *The Social Network* is incorrect. Correcting it in the implementation was deliberate — the project is a chance to work with the algorithm properly, not a chance to reproduce a Hollywood approximation.
**17. Documented the ethical choice about subject matter.**
Zuckerberg's FaceMash used photographs of fellow Harvard students without consent. The README notes that for ethical reasons the project instead uses photographs of professional swimwear models — subjects who are, by profession, photographed for public display. This is the kind of decision that often goes unrecorded, and recording it makes the project's boundary conditions visible.
## Result
A working Angular 8 application with four routes, five components, a service handling every HTTP call, a Reactive Form with validators on the scoreboard, a sortable Material table, a voting interface built with FlexLayout, and a correct Elo rating algorithm on the backend. The MP profile pages link back from the scoreboard, so every entity is reachable. The gender toggle restricts the sample to male or female MPs. Every vote updates the underlying ratings and refreshes the pair.
The project is small — it does one thing and does it end to end. But it exercises the parts of Angular that matter: routing with parameters, DI with `providedIn: 'root'`, Reactive Forms, Material tables, RxJS pipelines with error handling, and a service layer that keeps HTTP concerns out of the components. The Elo algorithm on the backend was the original point, and it works: upsets move the numbers more than expected outcomes, and the ranking converges with use.
The most interesting thing in the code is not any single file — it is the fact that each piece has a defined boundary. The service knows about the API. The components know about the service. The routes know about the components. Nothing knows about anything it shouldn't. That is what makes small projects enjoyable to extend, and what makes the difference between a demo that works and a demo you can keep working on.