04. Компонент оценки (звёздочки)

Рефакторинг сервисов. Сервис LocalStorage

Звёзды выводятся только в списке контактов на главной «странице». Чтобы звёзды выводились в избранных и в редактировании друга, нужно копировать все методы, которые работают с ними. Но это экстенсивный путь.

Кроме того, у нас отдельно друг от друга работают с локальным хранилищем компонент редактирования и компонент звёзд. Мы избежали потенциальной ошибки перезаписи данных, которые вносит компонент FavoritesComponent данными StarsComponent. Мы это сделали за счёт добавления постфикса «-stars» к _id друга. Таким образом, если один из друзей получил оценку, то в localStorage будут две записи, одна состоит из ключа _id со значением 'true', вторая из _id+'stars' со значением, равным числу поставленных звёзд.

В работе с коммерческими проектами такой подход недопустим. Нужно, чтобы все компоненты обращались к хранилищу через общий сервис. Более того, считается хорошим тоном хранить все данные приложения в одной записи, а не в десятке-другом разных.

Значит нужен ещё сервис для работы с localStorage. Но у нас скопилось предостаточно сервисов в корне приложения. Нужно их реструктуризовать. Зачастую получается так, что в приложении много вложенных друг в друга папок и получается, что в одном компоненте приходится подключать сервис так:

// Это пример, не нужно никуда это писать или сохранять
import { FriendsService } from '../../../friends.service';

А в другом, более «глубоком», так:

// Это пример, не нужно никуда это писать или сохранять
import { FriendsService } from '../../../../friends.service';

Естественно, программист начинает теряться в количестве ../.

Реструктуризация сервисов

В ./src/app создаём папку services. Переносим в неё файлы friends.service.ts и transfer-vars.service.ts. Файлы friends.service.spec.ts и transfer-vars.service.spec.ts нужны для тестирования, нам пока далеко до этого, можете их удалить. Создаём в этой же папке (./src/app/services) файл index.ts. Пишем в нём:

export * from './friends.service';
export * from './transfer-vars.service';

Открываем на редактирование файл tsconfig.json в самом корне проекта. Сейчас он имеет такое содержиоме:

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es5",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2017",
      "dom"
    ]
  }
}

Добавим в него переменную, которая будет хранить путь до папки сервиса:

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es5",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2017",
      "dom"
    ],
	"baseUrl": "./src",
	"paths": {
		"services": ["app/services"]
	}
  }
}

Теперь в самих сервисах меняем путь до файла friend.ts:

import { Injectable } from '@angular/core';
import { Friend } from '../friend'; // вот тут добавляем точку, т.к. файл лежит выше
import { Http, Response } from '@angular/http'; 
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/share';
import { of } from 'rxjs/observable/of';

@Injectable()
export class FriendsService {

	public apiHost: string = './assets/generated.json';

	private friends: Friend[];

	private friendsObservable: Observable;

	constructor(private http: Http) {

		this.friendsObservable = this.http.get(this.apiHost)
			.map(response => response.json())
			.do(friends => {
				this.friends = friends;
			})
			.share();

	}

	getFriends():any {
		if (this.friends) {
			return of(this.friends);
		} else {
			return this.friendsObservable;
		}
	}

}

import { Injectable } from '@angular/core';
import { Friend } from '../friend';

@Injectable()
export class TransferVarsService {

	private title:string = "";

	private friends:Friend[] = [];

	public setTitle(title: string):void {
		this.title = title;
	}

	public setFriends(friends: Friend[]):void {
		this.friends = friends;
	}

}

В остальных файлах, где подключаются сервисы, меняем до них путь, используя уже переменную пути:

// Группируем импорты по типам
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';
import { FriendDetailComponent } from './friend-detail/friend-detail.component';
import { FriendsListComponent } from './friends-list/friends-list.component';
import { FavoritesComponent } from './favorites/favorites.component';
import { StarsComponent } from './stars/stars.component';

import { FriendsService } from 'services'; // и у сервисов меняем пути
import { TransferVarsService } from 'services';


@NgModule({
  declarations: [
    AppComponent,
	FriendDetailComponent,
	FriendsListComponent,
	FavoritesComponent,
	StarsComponent
  ],
  imports: [
    BrowserModule,
	HttpModule,
	FormsModule,
	AppRoutingModule
  ],
  providers: [FriendsService, TransferVarsService, { provide: 'LOCALSTORAGE', useFactory: getLocalStorage }],
  bootstrap: [AppComponent]
})
export class AppModule { }

export function getLocalStorage() {
	return (typeof window !== "undefined") ? window.localStorage : null;
}

import { Component } from '@angular/core';
import { TransferVarsService } from 'services';


@Component({
	selector: 'app-root',
	templateUrl: './app.component.html',
	styleUrls: ['./app.component.css']
})
export class AppComponent {
	
	title:string = 'Менеджер контактов';

	constructor(private transferVarsService: TransferVarsService) {}

}

import { Component, OnInit } from '@angular/core';
import { Friend } from '../friend';
import { FriendsService } from 'services';
import { TransferVarsService } from 'services';
import { FriendsStars } from '../friends-stars';


@Component({
	selector: 'app-friends-list',
	templateUrl: './friends-list.component.html',
	styleUrls: ['./friends-list.component.css']
})
export class FriendsListComponent implements OnInit {

	title: string = 'Список друзей';

	friends: Friend[];

	stars: Array = [];

	constructor (
		private friendsService: FriendsService,
		private transferVarsService: TransferVarsService
	) {
		
	}
	
	ngOnInit() {
		this.getFriends();
		this.transferVarsService.setTitle(this.title);
	}
	
	getFriends():void {
		this.friendsService.getFriends().subscribe(result => {
			this.friends = result;
			this.friends.forEach((item) => {
				this.stars.push({id: item._id, stars: this.checkStarsInStorage(item._id)});
			});
		});
	}

	checkStarsInStorage(id: string):number {

		let stars: number = 0;
		stars = parseInt(localStorage.getItem(id + "-stars"));
		return ((stars < 6)&&(stars >= 0))? stars : 0;

	}

	getStars(id: string):number {

		return this.stars.find(friend => friend.id == id).stars;

	}

}

import { Component, Input, OnInit, Inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Friend } from '../friend';
import { FriendsService } from 'services';
import { TransferVarsService } from 'services';

export class ExtendedFriend extends Friend {
	favorite: boolean;
	constructor(fav) {
		super();
		this._id = "";
		this.index = 0;
		this.guid = "";
		this.isActive = false;
		this.balance = "";
		this.picture = "http://placehold.it/32x32";
		this.age = 0;
		this.eyeColor = "";
		this.name = "";
		this.gender = "female";
		this.company = "";
		this.email = "";
		this.phone = "";
		this.address = "";
		this.about = "";
		this.registered = "";
		this.latitude = 0;
		this.longitude = 0;
		this.tags = [];
		this.friends = [];
		this.greeting = "";
		this.favoriteFruit = "";
		this.favorite = fav;
	}
}

@Component({
	selector: 'app-friend-detail',
	templateUrl: './friend-detail.component.html',
	styleUrls: ['./friend-detail.component.css']
})
export class FriendDetailComponent implements OnInit {

	id: string;

	friends: Friend[];

	friend: ExtendedFriend;

	title: string = 'Редактирование';

	constructor(
		private route: ActivatedRoute,
		private friendsService: FriendsService,
		private transferVarsService: TransferVarsService,
		@Inject('LOCALSTORAGE') private localStorage: any
	) { }

	getFriends():void {
		this.friendsService.getFriends().subscribe(result => {
			this.friends = result;
			this.id = this.route.snapshot.paramMap.get('id');
			this.selectFriend(this.id);
			this.friend.favorite = this.checkValInStorage(this.friend._id);
		});
	}

	selectFriend(id: string):void {

		this.friend = new ExtendedFriend(false);

		let tempFriend = new Friend;

		tempFriend = this.friends.find(friend => friend._id === id);

		for (let property in tempFriend) {
			this.friend[property] = tempFriend[property];
		}

	}

	favoriteChanging():boolean {

		if ((!this.friend) || (!this.friend._id)) {
			return false;
		}

		let _id = this.friend._id;
		let fav: boolean = true;

		fav = !this.friend.favorite;

		this.locStorage(_id, fav);

	}

	locStorage(id: string, action: boolean):void {

		if (action) {
			localStorage.setItem(id, 'true');
		} else {
			localStorage.removeItem(id);
		}

	}

	checkValInStorage(id: string):boolean {

		return (localStorage.getItem(id) === "true");

	}

	ngOnInit() {

		this.getFriends();

		this.transferVarsService.setTitle(this.title);

	}

}

import { Component, OnInit, Inject } from '@angular/core';
import { Friend } from '../friend';
import { FriendsService } from 'services';
import { TransferVarsService } from 'services';

@Component({
  selector: 'app-favorites',
  templateUrl: './favorites.component.html',
  styleUrls: ['./favorites.component.css']
})
export class FavoritesComponent implements OnInit {

	title: string = "Избранные";

	friends: Friend[] = [];

	favoriteFriends: Friend[] = [];

	constructor(
		private friendsService: FriendsService,
		private transferVarsService: TransferVarsService,
		@Inject('LOCALSTORAGE') private localStorage: any
	) { }

	getFriends():void {
		this.friendsService.getFriends().subscribe(result => {
			this.friends = result;
			this.fillFavoriteFriends();
		});
	}

	fillFavoriteFriends():void {

		if (this.friends.length <= 0) { return; }

		let that = this;

		this.friends.forEach(function(item, i, arr) {

			if (that.checkValInStorage(item._id)) {
				that.favoriteFriends.push(item);
			}

		});

	}

	checkValInStorage(id: string):boolean {

		return (localStorage.getItem(id) === "true");

	}

	ngOnInit() {

		this.getFriends();
		
		this.transferVarsService.setTitle(this.title);

	}

}

Теперь мы можем заняться новым сервисом для работы с LocalStorage.

Сервис для работы с LocalStorage

Ниже код сервиса с пояснениями:

import { Injectable } from '@angular/core';

@Injectable()
export class LocalstorageService {
	private STORAGE_KEY: string = 'list-manager-v-01'; // Название переменной, если вдруг мы меняем формат хранения данных, то переименовываем list-manager-v-01 в list-manager-v-02 и тогда не нужно будет придумывать, как сделать совместимость старого формата с новым
	
	constructor( private defaults: any ) {
		if (!localStorage[this.STORAGE_KEY] || !JSON.parse( localStorage[this.STORAGE_KEY])) {
			this.setAll(defaults); // если такой переменной нет в localStorage клиента, то создаём, чтобы потом, когда будем забирать данные, не писать в каждом вызове проверку на undefined
		}
	}

	/**
	* В качестве key принимает _id объекта Friend, т.е.
	* строку символов.
	* 
	* Принимает value как объект, например {stars: 4},
	* после чего, делает в хранилище запись в хранилище:
	* key: {stars: 4}
	*/
	setValue(key: string, value: any) {
		let data = JSON.parse( localStorage[this.STORAGE_KEY] );
		if (data[key] === undefined) {
			data[key] = {};
		}
		data[key] = Object.assign(data[key], value); // assign принимает объекты и 'сливает' с объектом в первом параметре все объекты из последующих параметров.
		localStorage[this.STORAGE_KEY] = JSON.stringify(data); // Т.к. localStorage хранит только строку, то превращаем наш результирующий объект в строку.
	}

	setAll(value: any) { // Создаёт хранилище и, если что-то есть в параметрах, то кладёт это в свежесозданное хранилище.
		localStorage[this.STORAGE_KEY] = JSON.stringify(value);
	}

	getValue(key: string) { // Возвращает определённый параметр из хранилища по его id. Обратите внимание, ищет только на первом уровне. По вложенным объектам и свойствам искать будем сами. Undefined обрабатываем тоже сами.
		let data = JSON.parse( localStorage[this.STORAGE_KEY] );
		return data[key];
	}

	getAll() { // Возвращает всё содержиоме. Это для сложных случаев.
		return JSON.parse( localStorage[this.STORAGE_KEY] );
	}
}

Регистрируем новый сервис в ./src/app/services/index.ts:

export * from './friends.service';
export * from './transfer-vars.service';
export * from './localstorage.service';

Вносим изменения в корневой модуль (./src/app/app.module.ts):

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';
import { FriendDetailComponent } from './friend-detail/friend-detail.component';
import { FriendsListComponent } from './friends-list/friends-list.component';
import { FavoritesComponent } from './favorites/favorites.component';
import { StarsComponent } from './stars/stars.component';

import { FriendsService } from 'services';
import { TransferVarsService } from 'services';
import { LocalstorageService } from 'services';


@NgModule({
	declarations: [
		AppComponent,
		FriendDetailComponent,
		FriendsListComponent,
		FavoritesComponent,
		StarsComponent
	],
	imports: [
		BrowserModule,
		HttpClientModule,
		FormsModule,
		AppRoutingModule
	],
	providers: [
		FriendsService,
		TransferVarsService,
		{provide: LocalstorageService, useFactory: getLocalstorage }
	],
	bootstrap: [AppComponent]
})
export class AppModule { }

export function getLocalstorage() {
	return new LocalstorageService( { // Возвращает новый объект типа LocalstorageService, тот самый, который берём из ./src/app/services/localstorage.servise.ts
		// здесь можно задать какие-то начальные данные в формате key: value
	} );
}

Изменения в подключении localStorage приводят к необходимости в изменении всех компонентов и сервисов, где мы использовали локальное хранилище.

Помимо подключения нового сервиса, в ./src/app/friends-list/friends-list.component.ts нужно будет переписать метод checkStarsInStorage():

Старый метод: checkStarsInStorage()

Новый метод: checkStarsInStorage()

				checkStarsInStorage(id: string):number {

		let stars: number = 0;
		stars = parseInt(localStorage.getItem(id + "-stars"));
		return ((stars < 6) && (stars >= 0))? stars : 0;

	}
			
			
				checkStarsInStorage(id: string):number {

		let stars: number = 0;
		let obj = this.localstorageService.getValue(id);
		if (obj === undefined) return 0;

		stars = (obj.stars !== undefined) ? parseInt(obj.stars) : 0;

		return ((stars < 6) && (stars >= 0))? stars : 0;

	}
			
			

Полный код всех изменений:

import { Component, OnInit } from '@angular/core';
import { Friend } from '../friend';
import { FriendsService } from 'services';
import { TransferVarsService } from 'services';
import { LocalstorageService } from 'services';
import { FriendsStars } from '../friends-stars';


@Component({
	selector: 'app-friends-list',
	templateUrl: './friends-list.component.html',
	styleUrls: ['./friends-list.component.css']
})
export class FriendsListComponent implements OnInit {

	title: string = 'Список друзей';

	friends: Friend[];

	stars: Array = [];

	constructor (
		private friendsService: FriendsService,
		private transferVarsService: TransferVarsService,
		private localstorageService: LocalstorageService
	) {
		
	}
	
	ngOnInit() {
		this.getFriends();
		this.transferVarsService.setTitle(this.title);
	}
	
	getFriends():void {
		this.friendsService.getFriends().subscribe(result => {
			this.friends = result;
			this.friends.forEach((item) => {
				this.stars.push({id: item._id, stars: this.checkStarsInStorage(item._id)});
			});
		});
	}

	checkStarsInStorage(id: string):number {

		let stars: number = 0;
		let obj = this.localstorageService.getValue(id);
		if (obj === undefined) return 0;

		stars = (obj.stars !== undefined) ? parseInt(obj.stars) : 0;

		return ((stars < 6)&&(stars >= 0))? stars : 0;

	}

	getStars(id: string):number {

		return this.stars.find(friend => friend.id == id).stars;

	}

}

Изменения произошли даже в сервисе ./src/app/services/friends.service.ts. Мы в ./src/app/app.module.ts изменили подключение Http:

import { HttpModule } from '@angular/http';

Теперь у нас так:

import { HttpClientModule } from '@angular/common/http';

Практическая разница в том, что первый даёт данные как есть, а второй преобразовывает в JSON. Что нам весьма удобно, т.к. хранилище содержит строку, а нам нужны структурированные данные. В частности в ./src/app/services/friends.service.ts произошли такие изменения:

Старый конструктор:

Новый конструктор:

				constructor(private http: Http) {

		this.friendsObservable = this.http.get(this.apiHost)
			.map(response => response.json())
			.do(friends => {
				this.friends = friends;
			})
			.share();

	}
			
			
				constructor(private http: HttpClient) {

		this.friendsObservable = this.http.get(this.apiHost)
			.map(response => response)
			.do(friends => {
				this.friends = friends;
			})
			.share();

	}
			
			

Весь код этого сервиса:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; 
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/share';
import { of } from 'rxjs/observable/of';

@Injectable()
export class FriendsService {

	public apiHost: string = './assets/generated.json';

	private friends: any; // тип Friend меняем на any, иначе при присвоении будет ошибка, т.к. из файла придут данные типа Object. Соответственно, импорт класса Friend выше мы убрали

	private friendsObservable: Observable;

	constructor(private http: HttpClient) {

		this.friendsObservable = this.http.get(this.apiHost)
			.map(response => response)
			.do(friends => {
				this.friends = friends;
			})
			.share();

	}

	getFriends():any {
		if (this.friends) {
			return of(this.friends);
		} else {
			return this.friendsObservable;
		}
	}

}

В выводе списка избранных (компонент ./src/app/favorites/favorites.component.ts) нам больше не нужен отдельный метод для получения информации из локального хранилищаcheckValInStorage(). Мы его убираем и изменяем fillFavoriteFriends():

Было:

Стало:

			fillFavoriteFriends():void {

		if (this.friends.length <= 0) { return; }

		let that = this;

		this.friends.forEach(function(item, i, arr) {

			if (that.checkValInStorage(item._id)) {
				that.favoriteFriends.push(item);
			}

		});

	}

	checkValInStorage(id: string):boolean {

		return (localStorage.getItem(id) === "true");

	}
			
			
				fillFavoriteFriends():void {

		if (this.friends.length <= 0) { return; }

		this.friends.forEach((item, i, arr) => {

			if (this.localstorageService.getValue(item._id) === undefined) return;
			if (this.localstorageService.getValue(item._id).favorite) {
				this.favoriteFriends.push(item);
			};

		});

	}
			
			

Полный код произошедших в ./src/app/favorites/favorites.component.ts изменений:

import { Component, OnInit } from '@angular/core';
import { Friend } from '../friend';
import { FriendsService } from 'services';
import { TransferVarsService } from 'services';
import { LocalstorageService } from 'services';

@Component({
  selector: 'app-favorites',
  templateUrl: './favorites.component.html',
  styleUrls: ['./favorites.component.css']
})
export class FavoritesComponent implements OnInit {

	title: string = "Избранные";

	friends: Friend[] = [];

	favoriteFriends: Friend[] = [];

	constructor(
		private friendsService: FriendsService,
		private transferVarsService: TransferVarsService,
		private localstorageService: LocalstorageService
	) { }

	getFriends():void {
		this.friendsService.getFriends().subscribe(result => {
			this.friends = result;
			this.fillFavoriteFriends();
		});
	}

	fillFavoriteFriends():void {

		if (this.friends.length <= 0) { return; }

		this.friends.forEach((item, i, arr) => {

			if (this.localstorageService.getValue(item._id) === undefined) return;
			if (this.localstorageService.getValue(item._id).favorite) {
				this.favoriteFriends.push(item);
			};

		});

	}

	ngOnInit() {

		this.getFriends();
		
		this.transferVarsService.setTitle(this.title);

	}

}

В компоненте ./src/app/stars/stars.component.ts произошли наименее интересные изменения, его листинг можно посмотреть в результирующих листингах изменённых файлов внизу страницы.

Следующим у нас идёт компонент редактирования ./src/app/friend-detail/friend-detail.component.ts. В нём мы потренировались в создании объекта на основе экземпляра другого класса, избежав ошибки несовместимости типов (type mismatch). При работе в TypeScript это довольно важное умение. По сути отметку можно и нужно делать гораздо проще, мой пример переприсвоения объекта был высосан из пальца. И раз уж мы рефакторим код, заодно изменим возможность отмечать избранным по образу и подобию того, как мы только что получали массив избранных, т.е. без нагромождения дополнительных типов, которые наследуют другой тип, без поштучного копирования свойств объекта из одного в другой и т.п.

Методы locStorage(), checkValInStorage() и selectFriend() вместе с классом EntendedFriend покидают нас. Пофайловое сравнение не привожу, ибо будет очень много кода. Скопируйте предыдущую версию кода из листинга выше и текущую версию к себе в текстовый редактор или IDE и там сравните их:

import { Component, Input, OnInit, Inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Friend } from '../friend';
import { FriendsService } from 'services';
import { TransferVarsService } from 'services';
import { LocalstorageService } from 'services';

@Component({
	selector: 'app-friend-detail',
	templateUrl: './friend-detail.component.html',
	styleUrls: ['./friend-detail.component.css']
})
export class FriendDetailComponent implements OnInit {

	id: string;
	
	isFavorite = false;

	friends: Friend[];

	friend: Friend;

	title: string = 'Редактирование';

	constructor(
		private route: ActivatedRoute,
		private friendsService: FriendsService,
		private transferVarsService: TransferVarsService,
		private localstorageService: LocalstorageService
	) { }

	getFriends():void {
		this.friendsService.getFriends().subscribe(result => {
			this.friends = result;
			this.id = this.route.snapshot.paramMap.get('id');
			this.friend = this.friends.find(friend => friend._id === this.id);
			let friendFromLocalStorage = this.localstorageService.getValue(this.id);
			if (friendFromLocalStorage !== undefined) {
				if (friendFromLocalStorage.favorite !== undefined) {
					this.isFavorite = friendFromLocalStorage.favorite;
				}
			}
		});
	}

	favoriteChanging():boolean {

		if ((!this.friend) || (!this.friend._id)) {
			return false;
		}

		let fav: boolean = !this.isFavorite;

		this.localstorageService.setValue(this.friend._id, {favorite: fav});

	}

	ngOnInit() {

		this.getFriends();

		this.transferVarsService.setTitle(this.title);

	}

}

В шаблоне изменим переменную, которая отвечает за статус избранный/не избранный.

<div *ngIf="friend" class="friend-card">

	<div class="friend-card__row">
		<div class="friend-card__cell friend-card__cell_double friend-card__cell_ta-center">
			<h2>{{ friend.name }}</h2>
		</div>
	</div>
	<div class="friend-card__row">
		<div class="friend-card__cell friend-card__cell_width-40 friend-card__cell_ta-right">
			<span>ID:</span>
		</div>
		<div class="friend-card__cell friend-card__cell_width-60">
			<span>{{ friend._id }}</span>
		</div>
	</div>
	<div class="friend-card__row">
		<div class="friend-card__cell friend-card__cell_width-40 friend-card__cell_ta-right">
			<label for="friend-card__name">Имя:</label>
		</div>
		<div class="friend-card__cell friend-card__cell_width-60">
			<input id="friend-card__name" type="text" [(ngModel)]="friend.name" placeholder="Имя друга"/>
		</div>
	</div>
	<div class="friend-card__row">
		<div class="friend-card__cell friend-card__cell_width-40 friend-card__cell_ta-right">
			<div class="checkbox_fav checkbox_fav-right">
				<input id="friend-card__favorite" (ngModelChange)="favoriteChanging()" [(ngModel)]="isFavorite" type="checkbox"/> <!-- Вот тут меняем friend.favorite на isFavorite -->
				<label for="friend-card__favorite"><i class="fa fa-heart"></i></label>
			</div>
		</div>
		<div class="friend-card__cell friend-card__cell_width-60">
			<label *ngIf="friend.gender == 'female'" for="friend-card__favorite">избранная</label>
			<label *ngIf="friend.gender == 'male'" for="friend-card__favorite">избранный</label>
		</div>
	</div>

</div>

Результат:

Результирующие листинги изменённых файлов

Конфигурационные файлы и модули

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';
import { FriendDetailComponent } from './friend-detail/friend-detail.component';
import { FriendsListComponent } from './friends-list/friends-list.component';
import { FavoritesComponent } from './favorites/favorites.component';
import { StarsComponent } from './stars/stars.component';

import { FriendsService } from 'services';
import { TransferVarsService } from 'services';
import { LocalstorageService } from 'services';


@NgModule({
	declarations: [
		AppComponent,
		FriendDetailComponent,
		FriendsListComponent,
		FavoritesComponent,
		StarsComponent
	],
	imports: [
		BrowserModule,
		HttpClientModule,
		FormsModule,
		AppRoutingModule
	],
	providers: [
		FriendsService,
		TransferVarsService,
		{provide: LocalstorageService, useFactory: getLocalstorage }
	],
	bootstrap: [AppComponent]
})
export class AppModule { }

export function getLocalstorage() {
	return new LocalstorageService( {
		
	} );
}

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es5",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2017",
      "dom"
    ],
	"baseUrl": "./src",
	"paths": {
		"services": ["app/services"]
	}
  }
}

Сервисы

export * from './friends.service';
export * from './transfer-vars.service';
export * from './localstorage.service';

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; 
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/share';
import { of } from 'rxjs/observable/of';

@Injectable()
export class FriendsService {

	public apiHost: string = './assets/generated.json';

	private friends: any;

	private friendsObservable: Observable;

	constructor(private http: HttpClient) {

		this.friendsObservable = this.http.get(this.apiHost)
			.map(response => response)
			.do(friends => {
				this.friends = friends;
			})
			.share();

	}

	getFriends():any {
		if (this.friends) {
			return of(this.friends);
		} else {
			return this.friendsObservable;
		}
	}

}

import { Injectable } from '@angular/core';
import { Friend } from '../friend';

@Injectable()
export class TransferVarsService {

	private title:string = "";

	private friends:Friend[] = [];

	public setTitle(title: string):void {
		this.title = title;
	}

	public setFriends(friends: Friend[]):void {
		this.friends = friends;
	}

}

import { Injectable } from '@angular/core';

@Injectable()
export class LocalstorageService {
	private STORAGE_KEY: string = 'list-manager-v-01';
	
	constructor( private defaults: any ) {
		if( !localStorage[this.STORAGE_KEY] || !JSON.parse( localStorage[this.STORAGE_KEY] )){
			this.setAll(defaults);
		}
	}

	setValue(key: string, value: any) {
		let data = JSON.parse( localStorage[this.STORAGE_KEY] );
		if (data[key] === undefined) {
			data[key] = {};
		}
		data[key] = Object.assign(data[key], value);
		localStorage[this.STORAGE_KEY] = JSON.stringify(data);
	}

	setAll(value: any) {
		localStorage[this.STORAGE_KEY] = JSON.stringify(value);
	}

	getValue(key: string) {
		let data = JSON.parse( localStorage[this.STORAGE_KEY] );
		return data[key];
	}

	getAll() {
		return JSON.parse( localStorage[this.STORAGE_KEY] );
	}
}

Компоненты

import { Component } from '@angular/core';
import { TransferVarsService } from 'services';


@Component({
	selector: 'app-root',
	templateUrl: './app.component.html',
	styleUrls: ['./app.component.css']
})
export class AppComponent {
	
	title:string = 'Менеджер контактов';

	constructor(private transferVarsService: TransferVarsService) {}

}

import { Component, OnInit, Input } from '@angular/core';
import { LocalstorageService } from 'services';

@Component({
	selector: 'app-stars',
	templateUrl: './stars.component.html',
	styleUrls: ['./stars.component.css']
})
export class StarsComponent implements OnInit {

	@Input() stars: number;

	@Input() id: string;

	setStars(index: number):void {

		this.stars = index;
		this.localstorageService.setValue(this.id, {stars: index});

	}

	constructor(
		private localstorageService: LocalstorageService
	) { }

	ngOnInit() {
	}

}

import { Component, OnInit } from '@angular/core';
import { Friend } from '../friend';
import { FriendsService } from 'services';
import { TransferVarsService } from 'services';
import { LocalstorageService } from 'services';
import { FriendsStars } from '../friends-stars';


@Component({
	selector: 'app-friends-list',
	templateUrl: './friends-list.component.html',
	styleUrls: ['./friends-list.component.css']
})
export class FriendsListComponent implements OnInit {

	title: string = 'Список друзей';

	friends: Friend[];

	stars: Array = [];

	constructor (
		private friendsService: FriendsService,
		private transferVarsService: TransferVarsService,
		private localstorageService: LocalstorageService
	) {
		
	}
	
	ngOnInit() {
		this.getFriends();
		this.transferVarsService.setTitle(this.title);
	}
	
	getFriends():void {
		this.friendsService.getFriends().subscribe(result => {
			this.friends = result;
			this.friends.forEach((item) => {
				this.stars.push({id: item._id, stars: this.checkStarsInStorage(item._id)});
			});
		});
	}

	checkStarsInStorage(id: string):number {

		let stars: number = 0;
		let obj = this.localstorageService.getValue(id);
		if (obj === undefined) return 0;

		stars = (obj.stars !== undefined) ? parseInt(obj.stars) : 0;

		return ((stars < 6)&&(stars >= 0))? stars : 0;

	}

	getStars(id: string):number {

		return this.stars.find(friend => friend.id == id).stars;

	}

}

import { Component, Input, OnInit, Inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Friend } from '../friend';
import { FriendsService } from 'services';
import { TransferVarsService } from 'services';
import { LocalstorageService } from 'services';

@Component({
	selector: 'app-friend-detail',
	templateUrl: './friend-detail.component.html',
	styleUrls: ['./friend-detail.component.css']
})
export class FriendDetailComponent implements OnInit {

	id: string;
	
	isFavorite = false;

	friends: Friend[];

	friend: Friend;

	title: string = 'Редактирование';

	constructor(
		private route: ActivatedRoute,
		private friendsService: FriendsService,
		private transferVarsService: TransferVarsService,
		private localstorageService: LocalstorageService
	) { }

	getFriends():void {
		this.friendsService.getFriends().subscribe(result => {
			this.friends = result;
			this.id = this.route.snapshot.paramMap.get('id');
			this.friend = this.friends.find(friend => friend._id === this.id);
			let friendFromLocalStorage = this.localstorageService.getValue(this.id);
			if (friendFromLocalStorage !== undefined) {
				if (friendFromLocalStorage.favorite !== undefined) {
					this.isFavorite = friendFromLocalStorage.favorite;
				}
			}
		});
	}

	favoriteChanging():boolean {

		if ((!this.friend) || (!this.friend._id)) {
			return false;
		}

		let fav: boolean = !this.isFavorite;

		this.localstorageService.setValue(this.friend._id, {favorite: fav});

	}

	ngOnInit() {

		this.getFriends();

		this.transferVarsService.setTitle(this.title);

	}

}

import { Component, OnInit } from '@angular/core';
import { Friend } from '../friend';
import { FriendsService } from 'services';
import { TransferVarsService } from 'services';
import { LocalstorageService } from 'services';

@Component({
  selector: 'app-favorites',
  templateUrl: './favorites.component.html',
  styleUrls: ['./favorites.component.css']
})
export class FavoritesComponent implements OnInit {

	title: string = "Избранные";

	friends: Friend[] = [];

	favoriteFriends: Friend[] = [];

	constructor(
		private friendsService: FriendsService,
		private transferVarsService: TransferVarsService,
		private localstorageService: LocalstorageService
	) { }

	getFriends():void {
		this.friendsService.getFriends().subscribe(result => {
			this.friends = result;
			this.fillFavoriteFriends();
		});
	}

	fillFavoriteFriends():void {

		if (this.friends.length <= 0) { return; }

		this.friends.forEach((item, i, arr) => {

			if (this.localstorageService.getValue(item._id) === undefined) return;
			if (this.localstorageService.getValue(item._id).favorite) {
				this.favoriteFriends.push(item);
			};

		});

	}

	ngOnInit() {

		this.getFriends();
		
		this.transferVarsService.setTitle(this.title);

	}

}

Шаблоны

<div *ngIf="friend" class="friend-card">

	<div class="friend-card__row">
		<div class="friend-card__cell friend-card__cell_double friend-card__cell_ta-center">
			<h2>{{ friend.name }}</h2>
		</div>
	</div>
	<div class="friend-card__row">
		<div class="friend-card__cell friend-card__cell_width-40 friend-card__cell_ta-right">
			<span>ID:</span>
		</div>
		<div class="friend-card__cell friend-card__cell_width-60">
			<span>{{ friend._id }}</span>
		</div>
	</div>
	<div class="friend-card__row">
		<div class="friend-card__cell friend-card__cell_width-40 friend-card__cell_ta-right">
			<label for="friend-card__name">Имя:</label>
		</div>
		<div class="friend-card__cell friend-card__cell_width-60">
			<input id="friend-card__name" type="text" [(ngModel)]="friend.name" placeholder="Имя друга"/>
		</div>
	</div>
	<div class="friend-card__row">
		<div class="friend-card__cell friend-card__cell_width-40 friend-card__cell_ta-right">
			<div class="checkbox_fav checkbox_fav-right">
				<input id="friend-card__favorite" (ngModelChange)="favoriteChanging()" [(ngModel)]="isFavorite" type="checkbox"/>
				<label for="friend-card__favorite"><i class="fa fa-heart"></i></label>
			</div>
		</div>
		<div class="friend-card__cell friend-card__cell_width-60">
			<label *ngIf="friend.gender == 'female'" for="friend-card__favorite">избранная</label>
			<label *ngIf="friend.gender == 'male'" for="friend-card__favorite">избранный</label>
		</div>
	</div>

</div>


Дополнительные ссылки