Забираем данные из JSON-файла

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

В Angular часто приходится работать с файловой системой, поэтому напоминаю, если в одном из файлов указан адрес, начинающийся с ./, это означает, что файл ищется относительно текущей папки.

Поместим файл generated.json в папку ./src/assets. В данном случае у меня текущей папкой выступает директория проекта lesson01.

Компоненты и сервисы. Создание сервиса

В Angular есть понятие компонента и сервиса. Компонент — это отдельный блок на странице, выводящий какую-то визуальную информацию. Например, это список пользователей в нашем случае. Так же компонентом могут быть поисковая строка, оценка пользователя («звёздочки»), список избранных.

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

Создадим новый сервис friends.service.ts, который будет содержать в себе подключение к файлу.

ng generate service friends

Если вы обновили свой Angular-проект с версии 5 на 6, то новый сервис может оказаться сгенерированным не в папке ./src/app/, а в ./src/e2e/. В этом случае руками перенесите файл friends.service.ts в ./src/app/.

Если же всё прошло нормально, то в ./src/app/ созданы 2 файла: friends.service.spec.ts и friends.service.ts. В дальнейшем работать мы будем с friends.service.ts. Файлы, имеющие расширение .spec.ts создаются для тестирования. Мы не будем в этом курсе заниматься тестированием, поэтому можете такие файлы сразу удалять.

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

[
  {
    "_id": "58a53fbed70607f9bbc3cfb4",
    "index": 0,
    "guid": "9f4fa9a1-82fc-40ff-ad70-57992aa3f6a1",
    "isActive": false,
    "balance": "$2,542.39",
    "picture": "http://placehold.it/32x32",
    "age": 37,
    "eyeColor": "blue",
    "name": "Edith Norman",
    "gender": "female",
    "company": "MEGALL",
    "email": "edithnorman@megall.com",
    "phone": "+1 (985) 554-2597",
    "address": "652 Hubbard Street, Finderne, Northern Mariana Islands, 905",
    "about": "Adipisicing dolore magna veniam amet reprehenderit consectetur. Magna deserunt laboris amet quis excepteur deserunt exercitation fugiat. Ex qui et cupidatat qui aliqua in quis elit.\r\n",
    "registered": "2015-11-30T09:36:55 -03:00",
    "latitude": -55.336487,
    "longitude": -61.727468,
    "tags": [
      "culpa",
      "consectetur",
      "sunt",
      "ullamco",
      "in",
      "officia",
      "occaecat"
    ],
    "friends": [
      {
        "id": 0,
        "name": "Tami Fox"
      },
      {
        "id": 1,
        "name": "Denise Vang"
      },
      {
        "id": 2,
        "name": "Dalton Vincent"
      }
    ],
    "greeting": "Hello, Edith Norman! You have 10 unread messages.",
    "favoriteFruit": "banana"
  },
  {
    "_id": "58a53fbedb9b3e23e98af694",
    "index": 1,
    "guid": "1a7b9ab5-ef9d-41eb-9751-4a3f1aefa853",
    "isActive": true,
    "balance": "$1,546.20",
    "picture": "http://placehold.it/32x32",
    "age": 29,
    "eyeColor": "blue",
    "name": "Freida Hutchinson",
    "gender": "female",
    "company": "INJOY",
    "email": "freidahutchinson@injoy.com",
    "phone": "+1 (843) 536-3330",
    "address": "543 Matthews Court, Windsor, Pennsylvania, 3857",
    "about": "Do irure anim adipisicing ex. Pariatur exercitation nostrud ipsum reprehenderit laboris reprehenderit dolor quis proident reprehenderit ullamco minim quis in. Sit in et cillum exercitation magna veniam eiusmod ad amet. Commodo ut ullamco veniam aliqua. Est nulla exercitation eiusmod dolore ut non sint veniam ea ea. Eiusmod deserunt deserunt commodo id quis proident dolor et non id mollit nostrud.\r\n",
    "registered": "2014-07-17T05:35:14 -03:00",
    "latitude": 54.300674,
    "longitude": 71.873247,
    "tags": [
      "pariatur",
      "esse",
      "nostrud",
      "irure",
      "nostrud",
      "deserunt",
      "id"
    ],
    "friends": [
      {
        "id": 0,
        "name": "Ball Cain"
      },
      {
        "id": 1,
        "name": "Sherry Farmer"
      },
      {
        "id": 2,
        "name": "Laurie Mercado"
      }
    ],
    "greeting": "Hello, Freida Hutchinson! You have 7 unread messages.",
    "favoriteFruit": "strawberry"
  },
  {
    "_id": "58a53fbe07ce8f90a938de71",
    "index": 2,
    "guid": "71ffe732-d72b-4ecf-93f8-2750ea14b09a",
    "isActive": true,
    "balance": "$1,525.30",
    "picture": "http://placehold.it/32x32",
    "age": 21,
    "eyeColor": "blue",
    "name": "Rosa Richmond",
    "gender": "female",
    "company": "BUZZNESS",
    "email": "rosarichmond@buzzness.com",
    "phone": "+1 (854) 477-2191",
    "address": "871 Sumpter Street, Neahkahnie, New Hampshire, 7870",
    "about": "Aliqua enim cillum laboris Lorem mollit laboris tempor commodo consequat do. Minim exercitation ea reprehenderit eu magna. Tempor deserunt consectetur ut laboris cupidatat culpa. In veniam labore ullamco do dolore. Ex incididunt non ex fugiat quis ipsum sunt mollit ex amet proident. Incididunt ea velit et culpa eiusmod do eu sint pariatur nisi velit minim adipisicing consequat.\r\n",
    "registered": "2014-02-02T07:08:17 -03:00",
    "latitude": -71.146845,
    "longitude": 4.674137,
    "tags": [
      "elit",
      "Lorem",
      "proident",
      "sunt",
      "irure",
      "nulla",
      "laborum"
    ],
    "friends": [
      {
        "id": 0,
        "name": "Sybil Jacobson"
      },
      {
        "id": 1,
        "name": "Rich Rush"
      },
      {
        "id": 2,
        "name": "Marks Stone"
      }
    ],
    "greeting": "Hello, Rosa Richmond! You have 3 unread messages.",
    "favoriteFruit": "banana"
  }
  /* ... */
]

Определим тип каждой записи в объекте друга:

[
  {
    "_id": "58a53fbed70607f9bbc3cfb4",				/* тип string */
    "index": 0,							/* тип number */
    "guid": "9f4fa9a1-82fc-40ff-ad70-57992aa3f6a1",		/* тип string */
    "isActive": false,						/* тип boolean */
    "balance": "$2,542.39",					/* тип string */
    "picture": "http://placehold.it/32x32",			/* тип string */
    "age": 37,							/* тип number */
    "eyeColor": "blue",						/* тип string */
    "name": "Edith Norman",					/* тип string */
    "gender": "female",						/* тип string */
    "company": "MEGALL",					/* тип string */
    "email": "edithnorman@megall.com",				/* тип string */
    "phone": "+1 (985) 554-2597",				/* тип string */
    "address": "652 Hubbard Street, Finderne, Northern Mariana Islands, 905",
    "about": "Adipisicing dolore magna veniam amet reprehenderit consectetur. Magna deserunt laboris amet quis excepteur deserunt exercitation fugiat. Ex qui et cupidatat qui aliqua in quis elit.\r\n",	/* тип string */
    "registered": "2015-11-30T09:36:55 -03:00",		/* тип Date, но нам это поле будет проще определить как string */
    "latitude": -55.336487,					/* тип number */
    "longitude": -61.727468,					/* тип number */
    "tags": [							/* тип Array<string> или string[] */
      "culpa",
      "consectetur",
      "sunt",
      "ullamco",
      "in",
      "officia",
      "occaecat"
    ],
    "friends": [ /* Далее определим тип FriendOfFrind, в принципе, можно бы было описать как any */
      {
        "id": 0,
        "name": "Tami Fox"
      },
      {
        "id": 1,
        "name": "Denise Vang"
      },
      {
        "id": 2,
        "name": "Dalton Vincent"
      }
    ],
    "greeting": "Hello, Edith Norman! You have 10 unread messages.",	/* тип string */
    "favoriteFruit": "banana"						/* тип string */
  }
  /* ... */
]

Обратите внимание, что для друзей друга мы создали отдельный тип FriendOfFrind. Определение Object[] не удовлетворило бы транспайлер, и при попытках присвоения он выдавал бы ошибку, что в типе Object не существует свойств id и name. Если у вас нет времени задавать структуру второстепенным данным, то в таких случаях допустимо определять как any.

На основе анализа объекта друга определим тип Friend. Для этого создадим файл friend.ts в папке ./src/app.

export class Friend {
  _id: string;
  index: number;
  guid: string;
  isActive: boolean;
  balance: string;
  picture: string;
  age: number;
  eyeColor: string;
  name: string;
  gender: string;
  company: string;
  email: string;
  phone: string;
  address: string;
  about: string;
  registered: string;
  latitude: number;
  longitude: number;
  tags: string[];
  friends: FriendOfFrind[];
  greeting: string;
  favoriteFruit: string;
}

export class FriendOfFrind {
  id: number;
  name: string;
}


Теперь глобально для проекта подключим Http модуль. Для этого в ./src/app/app.module.ts добавим следующие строки:

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

Затем в декораторе @NgModule в зоне imports пропишем HttpClientModule:

  imports: [
    BrowserModule,
    HttpClientModule
  ],

После всех манипуляций файл ./src/app/app.module.ts выглядит так:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

import { FriendsService } from './friends.service';

import { AppComponent } from './app.component';


@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Работа с асинхронным кодом в Angular 2/6. Observable

Теперь нужно прописать в созданном нами сервисе всё волшебство, которое позволит вызывать из компонента методы сервиса, импортирующие содержимое файла. Когда мы в сервисе подключаем файл с данными, то используем объект Observable из библиотеки RxJS. Дело в том, что файл с данными, как правило, зачастую не хранится на сервере, а генерируется посредством запроса к БД или запроса к стороннему серверу/приложению. T.е. его может и не быть в запрашиваемый момент. Но он может появиться через десяток миллисекунд после запроса. Но будет уже поздно. В этом нам и поможет Observable. Он предоставляет ожидаемые данные.

В файле ./app/src/friends.service.ts подключаем Observable. Для этого добавляем подключение библиотеки. Приведу сразу весь код с методами, которые делают «магию».

import { Injectable } from '@angular/core';
import { Friend } from './friend';
import { HttpClient } from '@angular/common/http'; // Чтобы забрать файл с сервера.
import { Observable } from 'rxjs'; // Чтобы сервис умел работать с ожидаемыми данными.
import { map } from 'rxjs/operators'; // Чтобы распарсить, при необходимости, полученные данные.

@Injectable({
  providedIn: 'root'
})
export class FriendsService {

  public apiHost = './assets/generated.json'; // Хорошим тоном считается определять пути в переменных. Если путь изменится, не надо потом будет заменять его по всему файлу.

  private friends: Observable<Friend[]>;

  constructor(private http: HttpClient) {
    this.friends = this.http.get(this.apiHost) // метод get автоматически распарсит данные в JSON-формат, в Angular 5 это было не так.
      .pipe(map((friends: Friend[]) => friends)); // Это нужно, чтобы мы получили данные в формате Observable<Friend[]>. Без этой строчки .get вернёт нам Observable<Object>. Страшного ничего не случится, но в компонентах переменную, принимающуюя эти данные теперь можно определить как Observable<Friend[]>. Помимо улучшения читабельности кода, мы избегаем ошибок при присвоениях, в которых транспайлер будет сообщать, что, например, свойство _id не принадлежит типу Object.
  }

  getFriends(): Observable<Friend[]> {
    return this.friends;
  }

}

Теперь из компонента надо вызвать метод сервиса getFriends.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { Friend } from './friend';
import { FriendsService } from './friends.service';


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

  friends: Friend[]; // из методов компонента мы будем обращаться к этой переменной как this.friends
  selectedFriend: Friend;
  subscription: Subscription;

  constructor (private friendsService: FriendsService) { // теперь из любого метода мы можем обратиться к сервису как this.friendsService

  }

   // Этот метод обращается к методу getFriends сервиса FriendsService и передаёт полученные данные в переменную this.friends.
  getFriends(): void {
  
   // Подписываемся на получение ожидаемых данных
    this.subscription = this.friendsService.getFriends()
      .subscribe(result => {
        this.friends = result;
        console.log(this.friends); // (23) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
      });
    console.log(this.friends); // undefined, т.к. файл не был предоставлен сиюсекундно
  }

  // Cохраняем в отдельную переменную друга.
  // В шаблоне эту функцию присвоим обработчику (click).
  // Приложение будет знать, по кому кликнул пользователь. Это понадобится дальше.
  selectFriend(friend: Friend): void { 
    this.selectedFriend = friend;
  }

  ngOnInit() {
    this.getFriends();
  }

  // Уничтожаем подписку при удаление компонента (если таковое в будущем состоится).
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

Обратите внимание на появление в консоли данных из вызовов console.log:

undefined
core.js:3687 Angular is running in the development mode. Call enableProdMode() to enable the production mode.
app.component.ts:23 (23) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]

console.log вызываемый в методе subscribe выводится последним, т.к. файл не был получен мгновенно. То, что в subscribe выполняется после основного потока кода. Если в дальнейшем надо надо будет обратиться к переменной this.friends, то мы должны держать в уме тот факт, что данные в неё поставляются асинхронно. На практике это означает, что мы прописываем обращение к ней после строки this.friends = result; внутри subscribe метода getFriends (там, где сейчас console.log).

Работа с CSS в Angular 2/6

Теперь нам нужно вывести полученные данные не в консоли, а наглядно, на странице. Для этого надо провести общую подготовку проекта.

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

./src/app/app.component.css — это стиль компонента, то, что будет здесь прописано, пройдёт подготовку и будет доступно только для компонента app. Даже если в этом компоненте будут компоненты-потомки, то на них не распространится действие этих стилей. Чтобы это работало именно так, Angular прописывает в стили собственные аттрибуты. Затем эти атрибуты тегов используются и в HTML и в CSS коде.

./src/style.css — а вот тут прописываются глобальные стили. В глобальном файле я хочу указать базовые настройки, а именно, чтобы <body> не имел отступов, а все элементы во всех компонентах использовали модель расчёта ширины border-box.

@import url('https://fonts.googleapis.com/css?family=Roboto:400,400i,700,700i&subset=cyrillic');
* {
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
}

html, body {
  margin:0;
  padding:0;
  min-width: 320px;
  height: 100%;
}

body {
  font-size: 18px;
  font-family: Roboto, Calibri, sans-serif;
  background: #182340;
  background: -moz-linear-gradient(45deg, #182340 0%, #3c9ffc 100%);
  background: -webkit-linear-gradient(45deg, #182340 0%,#3c9ffc 100%);
  background: linear-gradient(45deg, #182340 0%,#3c9ffc 100%);
  background-attachment:fixed;
  color:#fff;
}

a:link {color:#fff;}
a:visited {color:#fff;}
a:hover {color:#fff;}
a:active {color:#fff;}

/* Список друзей */
ul.friends-list {
  margin:0 0 12px 0;
  padding:0 2px 2px 2px;
  list-style-type:none;
}
ul.friends-list>li {
  overflow:hidden;
  display:flex;
  position:relative;
  justify-content:space-between;
  padding:12px 20px;
  line-height:24px;
  margin:0 0 2px 0;
}
ul.friends-list>li:last-child {
  margin-bottom:0;
}
ul.friends-list>li.selected {
  color:#24bbbb;
}
ul.friends-list>li.selected a {
  color:#24bbbb;
}
ul.friends-list li:before {
  display:block;
  content:"";
  position:absolute;
  top:0;
  right:0;
  bottom:0;
  left:0;
  background:#fff;
  opacity:.3;
  z-index:1;
}
ul.friends-list>li.selected:before {
  opacity:.7;
}
ul.friends-list>li span {
  position:relative;
  z-index:2;
}
.friend-name {
  width:100%;
  margin-right:20px;
}

@media (max-width: 560px) {
  h1 {
    font-size:28px;
  }
}

Каких-то особых стилей именно для компонента app у нас не будет, поэтому ./src/app/app.component.css оставим пустым.

Шаблон компонента

Теперь пишем собственно HTML, который обернёт наши данные. HTML компонента app хранится в файле ./src/app/app.component.html.

<div class="outer">
  <div class="toppanel">

  </div>
  <div class="header">
    <div class="header__logo"><img class="logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg=="></div>
    <div class="header__header"><h1>Список контактов</h1></div>
  </div>
  <ul class="friends-list">
    <li *ngFor="let friend of friends">
      <span (click)="selectFriend(friend)" class="friend-name">{{friend.name}}</span>
    </li>
  </ul>
</div>

Как видите, код в html-файлах обрабатывается препроцессором.

*ngForструктурная или конструктивная директива. Все структурные директивы начинаются со звёздочки-астерикса «*». *ngFor сообщает препроцессору, что мы хотим взять переменную friends компонента и вывести её содержимое. На каждой итерации вывода отдельного элемента массива friends, к элементу возможно обращение через переменную {{ friend }}, а к параметру, например, _id, мы сможем обратиться как {{ friend._id }}.

Результирующие листинги изменённых файлов
export class Friend {
  _id: string;
  index: number;
  guid: string;
  isActive: boolean;
  balance: string;
  picture: string;
  age: number;
  eyeColor: string;
  name: string;
  gender: string;
  company: string;
  email: string;
  phone: string;
  address: string;
  about: string;
  registered: string;
  latitude: number;
  longitude: number;
  tags: string[];
  friends: FriendOfFrind[];
  greeting: string;
  favoriteFruit: string;
}

export class FriendOfFrind {
  id: number;
  name: string;
}

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

import { Injectable } from '@angular/core';
import { Friend } from './friend';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class FriendsService {

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

  private friends: Observable<Friend[]>;

  constructor(private http: HttpClient) {
    this.friends = this.http.get(this.apiHost)
      .pipe(map((friends: Friend[]) => friends));
  }

  getFriends(): Observable<Friend[]> {
    return this.friends;
  }

}

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { Friend } from './friend';
import { FriendsService } from './friends.service';

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

  friends: Friend[];
  selectedFriend: Friend;
  subscription: Subscription;

  constructor (private friendsService: FriendsService) {}

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

  selectFriend(friend: Friend): void {
    this.selectedFriend = friend;
  }

  ngOnInit() {
    this.getFriends();
  }

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

}

<div class="outer">
  <div class="toppanel">

  </div>
  <div class="header">
    <div class="header__logo"><img class="logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg=="></div>
    <div class="header__header"><h1>Список контактов</h1></div>
  </div>
  <ul class="friends-list">
    <li *ngFor="let friend of friends">
      <span (click)="selectFriend(friend)" class="friend-name">{{friend.name}}</span>
    </li>
  </ul>
</div>

@import url('https://fonts.googleapis.com/css?family=Roboto:400,400i,700,700i&subset=cyrillic');
* {
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
}

html, body {
  margin:0;
  padding:0;
  min-width: 320px;
  height: 100%;
}

body {
  font-size: 18px;
  font-family: Roboto, Calibri, sans-serif;
  background: #182340;
  background: -moz-linear-gradient(45deg, #182340 0%, #3c9ffc 100%);
  background: -webkit-linear-gradient(45deg, #182340 0%,#3c9ffc 100%);
  background: linear-gradient(45deg, #182340 0%,#3c9ffc 100%);
  background-attachment:fixed;
  color:#fff;
}

a:link {color:#fff;}
a:visited {color:#fff;}
a:hover {color:#fff;}
a:active {color:#fff;}

/* Список друзей */
ul.friends-list {
  margin:0 0 12px 0;
  padding:0 2px 2px 2px;
  list-style-type:none;
}
ul.friends-list>li {
  overflow:hidden;
  display:flex;
  position:relative;
  justify-content:space-between;
  padding:12px 20px;
  line-height:24px;
  margin:0 0 2px 0;
}
ul.friends-list>li:last-child {
  margin-bottom:0;
}
ul.friends-list>li.selected {
  color:#24bbbb;
}
ul.friends-list>li.selected a {
  color:#24bbbb;
}
ul.friends-list li:before {
  display:block;
  content:"";
  position:absolute;
  top:0;
  right:0;
  bottom:0;
  left:0;
  background:#fff;
  opacity:.3;
  z-index:1;
}
ul.friends-list>li.selected:before {
  opacity:.7;
}
ul.friends-list>li span {
  position:relative;
  z-index:2;
}
.friend-name {
  width:100%;
  margin-right:20px;
}

@media (max-width: 560px) {
  h1 {
    font-size:28px;
  }
}