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(', '); } }