I recently took a class on Angular 2+ at Code School.
I then rebuilt my homepage using Angular 4.
I would greatly appreciate if any developer experienced with Angular 2+ can check out my code on git and offer constructive feedback.
This site features loading content from JSON services into dynamic templates, unit tests, and advanced CSS.
Full code here: https://github.com/garyv/garyv
Projects page here: http://garyvonschilling.com/work
<!-- projects.component.html -->
<div class='big-margin-bottom'>
<h3>Technology used:</h3>
<ul class='tags row'>
<li *ngFor="let tag of projectTags.tags"
class="col tag" [class.active]="projectTags.isActive(tag)"
(click)="toggleTag(tag)">
{{tag}}
<i [class.fa]="true"
[class.fa-check]="projectTags.isActive(tag)"
[class.fa-minus-circle]="!projectTags.isActive(tag)"></i>
</li>
</ul>
</div>
<div class='row' [class.fade-in]="!skipFade">
<div *ngIf="!projectTags.activeProjects.length">
<h4>
Click a tag above to see projects
<i class='fa fa-level-up'></i>
</h4>
</div>
<div *ngFor="let project of projectTags.activeProjects"
class='col project grid-4 half big-margin-bottom medium-padding'
[class.active]="project.active" [class.hidden]="!project.active">
<h4>
<a [routerLink]="[project.friendlyId]">{{project.title}}</a>
</h4>
<a *ngIf="project.image?.src" [routerLink]="[project.friendlyId]">
<img alt=''
[src]="project.image.src"
[srcset]="project.image.srcset"
sizes="(min-width: 37.5em) 30vw, 48vw" />
</a>
<div class='tags'>
Tags:
<span [innerHTML]="projectTags.activeProjectTags(project)"></span>
</div>
</div>
</div>
// projects.component.ts
import { Component, Input, OnInit } from '@angular/core';
import { Project } from './project/project.model';
import { ProjectTags } from './project-tags.model';
import { ProjectsService } from './projects.service';
import { StateService } from '../state/state.service';
@Component({
selector: 'app-projects',
templateUrl: './projects.component.html',
styleUrls: ['./projects.component.css']
})
export class ProjectsComponent implements OnInit {
projects: Project[];
projectTags: ProjectTags;
@Input() skipFade: boolean;
constructor(private projectsService: ProjectsService) { }
ngOnInit() {
this.projectTags = new ProjectTags();
this.projectsService.getProjects()
.subscribe( (projects) => {
this.projects = projects;
this.projectTags.populateTags(projects);
});
}
toggleTag(tag) {
this.projectTags.toggleTag(tag, this.projects);
}
}
// projects.component.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { HttpModule } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { ProjectsComponent } from './projects.component';
import { Project } from './project/project.model';
import { ProjectComponent } from './project/project.component';
import { ProjectTags } from './project-tags.model';
import { ProjectsService } from './projects.service';
import { StateService } from '../state/state.service';
import { Router, RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
describe('ProjectsComponent', () => {
let component: ProjectsComponent;
let fixture: ComponentFixture<ProjectsComponent>;
let debugElement: DebugElement;
let projectsService: ProjectsService;
let mockProjects: Project[] = [
{
friendlyId: 'example-title',
image: {
src: 'https://www.fillmurray.com/400/300/',
},
link: {
address: '//example.com',
text: 'example text'
},
text: '<p>This is an example project.</p>',
title: 'Example Title',
tags: ['Example Tag', 'Same']
},
{
friendlyId: 'lorem-ipsum-title',
image: {
src: 'https://www.fillmurray.com/400/300/g',
},
link: {
address: '//lorempixel.com',
text: 'lorem ipsum'
},
text: '<p>Lorem ipsum dolor sit amet.</p>',
title: 'Lorem Ipsum Title',
tags: ['Lorem', 'Same']
}
];
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterModule,
HttpModule,
RouterTestingModule.withRoutes(
[{path: 'work/:friendly-id', component: ProjectComponent}]
)
],
declarations: [
ProjectsComponent,
ProjectComponent
],
providers: [
ProjectsService,
StateService
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectsComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
projectsService = fixture.debugElement.injector.get(ProjectsService);
spyOn(projectsService, 'getProjects')
.and.returnValue(Observable.of(mockProjects));
StateService.set('tags', '["Same"]');
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
it('should list tags', () => {
let tagsElement = debugElement.query(By.css('.tags li'));
expect(tagsElement.nativeElement.textContent).toContain('Example Tag');
});
it('should list projects', () => {
let projectElements = debugElement.query(By.css('.project'));
expect(projectElements.nativeElement.textContent).toContain('Example Title');
});
it('should link to individual project page', () => {
let projectLink = debugElement.query(By.css('a[href$=example-title]'));
expect(projectLink).toBeTruthy();
});
it('should hide inactive projects', () => {
component.projectTags.activeProjects.splice(0, 1);
fixture.detectChanges();
let projectElements = debugElement.query(By.css('.project'));
expect(projectElements.nativeElement.textContent).toContain('Lorem Ipsum');
});
});
// projects.service.ts
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import { Project } from './project/project.model';
@Injectable()
export class ProjectsService {
constructor(private http:Http) {}
getProjects() {
return this.http.get('app/projects/projects.json')
.map( (response) => {
let projects = <Project[]>response.json().projects;
for (let project of projects) {
if (project.title && !project.friendlyId) {
project.friendlyId = ProjectsService.getFriendlyId(project.title);
}
}
return projects;
});
}
static getFriendlyId(title: string): string {
return title.toLowerCase()
.replace(/\W+/g, '-')
.replace(/^-|-$/g, '');
}
}
// projects.service.spec.ts
import { TestBed, async, inject } from '@angular/core/testing';
import {
HttpModule,
Http,
Response,
ResponseOptions,
XHRBackend
} from '@angular/http';
import { MockBackend } from '@angular/http/testing';
import { ProjectsService } from './projects.service';
import { Project } from './project/project.model';
describe('ProjectsService', () => {
let projectsService: ProjectsService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpModule],
providers: [
ProjectsService,
{ provide: XHRBackend, useClass: MockBackend }
]
});
});
describe('getProjects()', () => {
it('should return an Observable<Project[]>',
inject([ProjectsService, XHRBackend], (projectsService, mockBackend) => {
const mockResponse = {
projects: [
{
//friendlyId: 'example-title',
image: {
src: 'https://www.fillmurray.com/400/300/',
},
link: {
address: '//example.com',
text: 'example text'
},
text: '<p>This is an example project.</p>',
title: 'Example Title',
tags: ['Example Tag', 'Same']
},
{
//friendlyId: 'lorem-ipsum',
image: {
src: 'http://lorempixel.com/400/300/',
},
link: {
address: '//lipsum.com',
text: 'lorem ipsum'
},
text: '<p>Lorem ipsum dolor sit amet.</p>',
title: 'Lorem Ipsum',
tags: ['Lorem', 'Same']
}
]
};
mockBackend.connections.subscribe( (connection) => {
let response = new ResponseOptions({body: JSON.stringify(mockResponse)});
connection.mockRespond(new Response(response));
});
projectsService.getProjects().subscribe( (projects) => {
expect(projects.length).toEqual(2);
expect(projects[0].title).toEqual('Example Title');
expect(projects[1].title).toEqual('Lorem Ipsum');
expect(projects[1].friendlyId).toEqual('lorem-ipsum');
expect(projects[1].image).toEqual({src: 'http://lorempixel.com/400/300/'});
expect(projects[1].link).toEqual({address: '//lipsum.com', text: 'lorem ipsum'});
expect(projects[1].text).toEqual('<p>Lorem ipsum dolor sit amet.</p>');
expect(projects[1].tags).toEqual(['Lorem', 'Same']);
});
})
);
describe('getFriendlyId()', () => {
it('should make titles url friendly', () => {
let friendlyId = ProjectsService.getFriendlyId(" ¿ HellO World #👋🌎 !! ");
expect(friendlyId).toEqual('hello-world');
});
});
});
});
// projects-tags.model.ts
import { Input, OnInit } from '@angular/core';
import { Project } from './project/project.model';
import { StateService } from '../state/state.service';
const defaultTags:string[] = ['JavaScript', 'Ruby on Rails'];
const storageKey:string = 'tags';
export class ProjectTags {
tags: string[] = [];
activeTags: string[] = [];
activeProjects: Project[] = [];
@Input() highlightTag: string;
constructor() {
this.activeTags = JSON.parse(StateService.get(storageKey));
if (!this.activeTags || !this.activeTags.length) {
this.activeTags = defaultTags;
}
}
isActive(tag:string):boolean {
return this.activeTags.indexOf(tag) != -1;
}
populateTags(projects:Project[]):void {
this.activeProjects = [];
for (let project of projects) {
project.active = false;
for (let tag of project.tags) {
if (this.tags.indexOf(tag) == -1) {
this.tags.push(tag);
}
if (!project.active && this.activeTags.indexOf(tag) != -1) {
project.active = true;
}
}
if (project.active) {
this.activeProjects.push(project);
}
}
this.tags = this.tags.sort();
StateService.set(storageKey, JSON.stringify(this.activeTags));
}
toggleTag(tag:string, projects:Project[]):void {
let index = this.activeTags.indexOf(tag);
if (index == -1) {
this.highlightTag = tag;
this.activeTags.push(tag);
} else {
this.highlightTag = null;
this.activeTags.splice(index, 1);
}
this.populateTags(projects);
}
activeProjectTags(project:Project):string {
let tags:string[] = [];
for (let tag of project.tags) {
if (this.isActive(tag)) {
if (tag == this.highlightTag) {
tags.push(`<span class='highlight'>${tag}</span>`);
} else {
tags.push(`<span>${tag}</span>`);
}
}
}
return tags.join(', ');
}
}