03. Локальное хранилище. Компонент «Избранные»

Стилизация чекбокса. Основа работы с localStorage

Состояние папки проекта на данный момент:

Задача, которую предстоит решить в этой главе: требуется создать функционал работы с локальным хранилищем и с помощью него реализовать возможность отмечать друга в качестве избранного. Помимо этого сделать вывод избранных друзей на отдельной странице.

На этой странице мы сделаем кастомный чекбокс и рассмотрим азы работы с localStorage.

Кастомизация чекбокса

Добавим в компоненте FriendDetailComponent в конец файла ./src/app/friend-detail/friend-detail.component.css стили:

.checkbox_fav {
  position:relative;
  width:80px;
  height:36px;
  text-align:center;
}
.checkbox_fav-right {
  margin:0 0 0 auto;
  position:relative;
  left:28px;
}
.checkbox_fav input[type=checkbox] {
  position:absolute;
  left:-9999px;
}
.checkbox_fav input[type=checkbox] + label {
  opacity:.2;
  transition: opacity, color 300ms linear;
}
.checkbox_fav input[type=checkbox]:checked + label {
  color:#4ce4e4;
  opacity:1;
}

Теперь в шаблоне реализуем чекбокс. В зависимости от пола друга, текст рядом с чекбоксом будет гендерозависимым: Избранный или Избранная. ./src/app/friend-detail/friend-detail.component.html:

<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" [(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>

У метки (элемента label) нет состояния :checked, но спасает то, что она стоит сразу после чекбокса, а значит селектором нам послужит .checkbox_fav input[type=checkbox]:checked + label. « + » — означает следующий элемент этого же уровня. Таким образом, чекбокс мы выносим за видимые пределы, не забывая отсечь его с помощью свойства overflow: hidden у одного из элементов-родителей. А стилизуем мы на самом деле метку, элемент label.

В логику компонента пока добавим лишь переменную isFavorite:

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

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

  id: string;
  isFavorite = false;
  friends: Friend[];
  friend: Friend;
  title = 'Редактирование';
  subscription: Subscription;

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

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

  selectFriend(id: string): void {
    this.friend = this.friends.find(friend => friend._id === id);
  }

  ngOnInit() {
    this.getFriends();
    this.transferVarsService.setTitle(this.title);
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

В промежуточном билде мы видим, что визуально чекбокс гораздо лучше того, что идёт по умолчанию.

Основы работы с localStorage

В localStorage мы можем хранить данные только в виде текста. Это немного затрудняет работу, но мы это быстро обойдём.

Отслеживать состояние хранилища приложения можно в отладчике браузера: F12 → Application → в левом меню LocalStorage. Это поможет нам контролировать, какие переменные хранятся в данный момент на на нашем домене. Адреса, типа http://localhost:4200 тоже воспринимаются как разные домены.

Для двух разных номеров портов будут созданы два непересекающихся хранилища. А вот если вы по очереди запустите на одном порте локалхоста несколько приложений и каждое сохранит свои переменные, то каждое последующее приложение на этом порте будет видеть переменные предыдущих приложений.

Локальное хранилище доступно в каждом участке приложения из глобальной переменной localStorage. Дополнительно ничего можно не подключать. Чтобы сохранить переменную в хранилище, нужно вызвать метод setItem. Например, сохраним в переменную "9f4fa9a1-82fc-40ff-ad70-57992aa3f6a1" значение "true". Это пример:

localStorage.setItem('9f4fa9a1-82fc-40ff-ad70-57992aa3f6a1', 'true');

Чтобы считать эту переменную, нужно выполнить такой код:

localStorage.getItem('9f4fa9a1-82fc-40ff-ad70-57992aa3f6a1');

Как видите, довольно просто. В 5-м Angular было допустимо сохранять и считывать JSON-данные так:

let data = JSON.parse( localStorage['9f4fa9a1-82fc-40ff-ad70-57992aa3f6a1'] ); // считывание с сохранением в переменной.
localStorage['9f4fa9a1-82fc-40ff-ad70-57992aa3f6a1'] = JSON.stringify({favorite: true}); // Запись в локальное хранилище.

В Angular 6 это считается ошибкой, причём транспайлер даже не показывает строчку, где она произошла, поэтому заранее предостерегаю от такого использования хранилища.

У новичков, как это было и у меня, возникает искушение реализовать сохранение так: для каждого из контактов завести переменную в локальном хранилище и туда сохранять true или false. Однако, практика работы в больших проектах показала порочность такого подхода.

Напрмер, нам нужно будет потом реализовать компонент оценки друзей и логично будет для каждого друга хранить уже не true или false, а кусок такого кода:

{
  favorite: true,
  stars: 4
}

Поэтому в боевых проектах стараются укладывать все данные приложения в одну строку, в названии переменной указывается версия структуры хранимых данных, например APPNAME_v1. Версионность переменных позволяет не заморачиваться над поддержкой старых форматов, мы в одном месте в приложении меняем название на APPNAME_v2 и не нужно писать код, который будет различать, что же сохранено, "true" или "{favorite: true,stars: 4}".

Сервис для сохранения в localStorage мы сделаем на следующей странице.

Результирующие листинги изменённых файлов
.friend-card {
  margin:2px;
  position:relative;
  overflow:hidden;
  padding:0 0 12px 0;
}
.friend-card:before {
  display:block;
  content:"";
  position:absolute;
  top:0;
  right:0;
  bottom:0;
  left:0;
  background:#fff;
  opacity:.4;
  -webkit-box-shadow: 0 3px 15px 0 rgba(0, 0, 0, 0.75);
  -moz-box-shadow:0 3px 15px 0 rgba(0, 0, 0, 0.75);
  box-shadow: 0 3px 15px 0 rgba(0, 0, 0, 0.75);
  z-index:1;
}
.friend-card__row {
  position:relative;
  width:100%;
  display:flex;
  justify-content:space-between;
  z-index:2;
}
.friend-card__cell {
  padding:4px;
  line-height:36px;
}
.friend-card__cell h1,
.friend-card__cell .h1,
.friend-card__cell h2,
.friend-card__cell .h2,
.friend-card__cell h3,
.friend-card__cell .h3 {
  margin:6px auto;
}
.friend-card__cell_ta-right {
  text-align:right;
}
.friend-card__cell_ta-center {
  text-align:center;
}
.friend-card__cell_ta-center h1,
.friend-card__cell_ta-center .h1,
.friend-card__cell_ta-center h2,
.friend-card__cell_ta-center .h2,
.friend-card__cell_ta-center h3,
.friend-card__cell_ta-center .h3 {
  text-align:center;
}
.friend-card__cell_double {
  width:100%;
}
.friend-card__cell_width-40 {
  width:40%;
}
.friend-card__cell_width-60 {
  width:60%;
}
.friend-card__danger {
  background:#d10c31;
}
.checkbox_fav {
  position:relative;
  width:80px;
  height:36px;
  text-align:center;
}
.checkbox_fav-right {
  margin:0 0 0 auto;
  position:relative;
  left:28px;
}
.checkbox_fav input[type=checkbox] {
  position:absolute;
  left:-9999px;
}
.checkbox_fav input[type=checkbox] + label {
  opacity:.2;
  transition: opacity, color 300ms linear;
}
.checkbox_fav input[type=checkbox]:checked + label {
  color:#4ce4e4;
  opacity:1;
}

<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" [(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>

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

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

  id: string;
  isFavorite = false;
  friends: Friend[];
  friend: Friend;
  title = 'Редактирование';
  subscription: Subscription;

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

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

  selectFriend(id: string): void {
    this.friend = this.friends.find(friend => friend._id === id);
  }

  ngOnInit() {
    this.getFriends();
    this.transferVarsService.setTitle(this.title);
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}


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