01. Инициализация. Создание сервиса. CSS. Шаблон

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

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

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

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

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

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

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

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

ng generate service friends

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

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

В декораторе @NgModule в разделе providers добавляем код:

providers: [FriendsService],

Результирующий листинг файла ./src/app/app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FriendsService } from './friends.service';


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


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

Мы подготовили сервис для работы с ним, но чтобы забрать 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": [						/* тип Array<Object> или Object[] */
      {
        "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 */
  }
  /* ... */
]

На основе анализа объекта друга определим тип 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[]; /* Обратите внимание, TypeScript не удовлетворится описанием Array<Object>, поэтому определяем новый тип */
	greeting:	string;
	favoriteFruit:	string;
}

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

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

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

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

  imports: [
    BrowserModule,
	HttpModule
  ],

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

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FriendsService } from './friends.service';
import { HttpModule } from '@angular/http';


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


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

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

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

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

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<any>;

	constructor(private http: Http) { // делаем Http доступным в методах сервиса. Теперь к нему можно обращаться через this.http
	// Напоминаю, конструктор вызывается при создании объекта сервиса (в данном случае FriendsService)
		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;
		}
	}

}


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

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


@Component({
	selector: 'app-root',
	templateUrl: './app.component.html',
	styleUrls: ['./app.component.css']
})
export class AppComponent {
	title = 'app';
	friends: Friend[]; // из методов компонента мы будем обращаться к этой переменной как this.friends
	constructor (private friendsService: FriendsService) { // теперь из любого метода мы можем обратиться к сервису как this.friendsService
		
	}
	ngOnInit() { // этот код выполнится один раз при визуальной инициализации компонента
		this.getFriends();
	}
	getFriends():void { // этот метод обращается к методу getFriends сервиса FriendsService и пробрасывает полученные данные в переменную this.friends
		this.friendsService.getFriends().subscribe(result => { // подписываемся на получение ожидаемых данных
			this.friends = result;
			console.log(this.friends); // (23) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
		});
		console.log(this.friends); // undefined, т.к. файл не был предоставлен сиюсекундно
	}
}



Обратите внимание на появление в консоли данных из вызовов 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). Либо из любого другого метода, НО мы должны внутри setInterval проверять переменную на undefined (этот код сейчас не нужно добавлять в компонент, это пример не очень хорошей практики работы с переменной, получающей данные асинхронно):

checkReady: boolean = false;

someMethod():void {
	this.checkReady = setInterval(() => {
		if (this.friends != undefined) {
			clearInterval(this.checkReady); // если данные получены, то очищаем счётчик, чтобы не загружать процессор попусту
		}
		/* Вот тут this.friends уже содержит в себе полученные данные */
	}, 500);
}

Почему пример выше не очень хорош, хотя и работает? Потому что, когда другой программист будет разбираться в коде, он будет ожидать получение асинхронных данных внутри subscribe. Мы так и сделали и в нашем случае данные передаются в строке this.friends = result; в компоненте ./src/app/app.component.ts. Кроме того, мы плодим сущности и создаём лишний setInterval.

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

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

В 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;
		}

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

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

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

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <img width="150" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<ul class="friends-list">
	<li *ngFor="let friend of friends">
		<span>{{friend.name}}</span>
	</li>
</ul>

Как видите, код в 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 { FriendsService } from './friends.service';
import { HttpModule } from '@angular/http';


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


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

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<any>;

	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 { Component, OnInit } from '@angular/core';
import { Friend } from './friend';
import { FriendsService } from './friends.service';


@Component({
	selector: 'app-root',
	templateUrl: './app.component.html',
	styleUrls: ['./app.component.css']
})
export class AppComponent {
	title = 'app';
	friends: Friend[];
	constructor (private friendsService: FriendsService) {
		
	}
	ngOnInit() {
		this.getFriends();
	}
	getFriends():void {
		this.friendsService.getFriends().subscribe(result => {
			this.friends = result;
		});
	}
}


<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <img width="150" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>
<ul class="friends-list">
	<li *ngFor="let friend of friends">
		<span>{{friend.name}}</span>
	</li>
</ul>

@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;
		}


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