<!DOCTYPE html>
<html>
<head>
<title>Angular 2 and Redux Tutorial: Contact List</title>
<link rel="stylesheet" href="main.css">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous">
<link href='https://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
<script src="https://code.angularjs.org/2.0.0-beta.15/angular2-polyfills.js"></script>
<script src="https://code.angularjs.org/tools/system.js"></script>
<script src="https://code.angularjs.org/tools/typescript.js"></script>
<script src="config.js"></script>
<script src="https://code.angularjs.org/2.0.0-beta.15/Rx.js"></script>
<script src="https://code.angularjs.org/2.0.0-beta.15/angular2.js"></script>
<script src="https://code.angularjs.org/2.0.0-beta.15/http.min.js"></script>
<script>
System.import('app')
.catch(console.error.bind(console));
</script>
</head>
<body>
<contact-list>Loading...</contact-list>
</body>
</html>
### Angular 2, Immutable.js and Redux Tutorial: Contact List
- A simple contact list application built with Angular 2, Immutable.js and Redux.
System.config({
//use typescript for compilation
transpiler: 'typescript',
//typescript compiler options
typescriptOptions: {
emitDecoratorMetadata: true
},
//map tells the System loader where to look for things
map: {
app: "./app",
'redux': 'https://cdnjs.cloudflare.com/ajax/libs/redux/3.5.2/redux.js',
'immutable': 'https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js'
},
//packages defines our app package
packages: {
app: {
main: './main.ts',
defaultExtension: 'ts'
}
}
});
html, body {
font-family: "Open Sans", sans-serif;
margin: 0;
padding: 0;
font-size: 1em;
background: #F4F3F0;
}
import { bootstrap } from 'angular2/platform/browser';
import { ContactList } from './contact-list';
import { ContactStore } from './contact-store';
bootstrap(ContactList, [ContactStore]);
import { Component, Input, ChangeDetectionStrategy} from 'angular2/core';
import { ContactStore, Contact as ContactModel} from './contact-store';
import { removeContact, starContact } from './actions';
@Component({
selector: 'contact',
templateUrl: 'app/contact.html',
styleUrls: ['app/contact.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export default class Contact {
@Input()
contact: ContactModel;
constructor(private store: ContactStore) { }
removeContact(contact) {
this.store.dispatch(removeContact(contact.id));
}
starContact(contact) {
this.store.dispatch(starContact(contact.id));
}
}
import Immutable = require('immutable');
import { createStore } from 'redux';
import { IContactAction } from './actions';
import { reducer } from './reducer';
export class Contact {
id: number;
name: String;
star: boolean;
}
export class ContactStore {
store = createStore(reducer, Immutable.List<Contact>());
get contacts(): Immutable.List<Contact> {
return this.store.getState();
}
dispatch(action: IContactAction) {
this.store.dispatch(action);
}
}
<div class="contact-container">
<i class="fa fa-user fa-2x contact-icon"></i>
<div class="contact-info">
<h3 class="heading--name">{{ contact.name }}</h3>
<div class="contact-item"><i class="fa fa-phone"></i> 647-XXX-XXXX</div>
</div>
<button class="contact-action" (click)="starContact(contact)">
<i class="fa fa-2x" [class.fa-star]="contact.star" [class.fa-star-o]="!contact.star"></i>
</button>
<button class="contact-action" (click)="removeContact(contact)">
<i class="fa fa-trash fa-2x"></i>
</button>
</div>
.contact-action {
color: #3F51B5;
margin-right: 6px;
background-color: #fff;
border-radius: 5px;
font-size: 0.8em;
}
.contact-action:hover {
cursor: pointer;
margin-top: 2px;
}
.contact-action:focus {
outline: none;
}
.contact-list {
margin: 0;
padding: 0;
}
.contact-list li {
display: flex;
align-items: center;
color: #919191;
padding: 10px;
min-height: 60px;
}
.contact-icon {
border-radius: 50%;
float: left;
margin-left: 10px;
width: 10%;
margin-right: 10px;
}
.heading--name {
color: #444;
padding: 0;
margin-top: 0;
margin-bottom: 0;
}
.contact-info {
width: 80%;
}
.contact-item {
display: inline-block;
font-size: 0.7em;
padding-top: 5px;
}
.contact-item .fa {
font-size: 1.3em;
margin-right: 2px;
margin-left: 2px;
}
.contact-container {
display: flex;
align-items: center;
color: #919191;
padding: 10px;
min-height: 60px;
}
import { Component } from 'angular2/core';
import { ContactStore } from './contact-store';
import Contact from './contact';
import { addContact } from './actions';
@Component({
selector: 'contact-list',
templateUrl: 'app/contact-list.html',
styleUrls: ['app/contact-list.css'],
directives: [Contact]
})
export class ContactList {
contactID: number;
constructor(private store: ContactStore) {
this.contactID = 0;
}
addContact(contact) {
this.store.dispatch(addContact(contact, this.contactID++));
}
}
<div id="container">
<header>
<div class="user">
<img src="https://hdjirdeh.github.io/public/me.jpg">
</div>
<div class="heading-user">
<strong>Houssein Djirdeh</strong>
<br/>
<small>@hdjirdeh</small>
</div>
</header>
<input #newContact class="add-contact" placeholder="Add Contact"
(keyup.enter)="addContact(newContact.value); newContact.value='' ">
<ul class="contact-list">
<li *ngFor="#contact of store.contacts">
<contact [contact]="contact"></contact>
</li>
</ul>
</div>
header {
background-color: #3F51B5;
color: #fff;
padding: 20px;
box-shadow: 0 2px 2px -2px #444;
}
#container {
width: 420px;
display: block;
position: relative;
margin: 200px auto;
background: #fff;
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2);
overflow: hidden;
border-radius: 10px;
}
.user {
display: inline-block;
float: left;
margin-right: 15px;
width: 10%;
}
.user img {
width: 40px;
border-radius: 50%;
border: #fff 2px solid;
}
.add-contact {
border: 1px solid #f1f1f1;
height: 55px;
padding: 5px;
padding-left: 25px;
display: block;
width: 100%;
font-size: 1em;
color: #444;
}
.add-contact:focus {
outline: none;
}
.contact-list {
margin: 0;
padding: 0;
}
.contact-list li {
display: block;
}
import { Contact as ContactModel} from './contact-store';
export interface IContactAction {
type: string;
id: number;
name?: string;
}
export function addContact(name: string, id: number): IContactAction {
return {
type: 'ADD',
id,
name
};
}
export function removeContact(id: number): IContactAction {
return {
type: 'REMOVE',
id
};
}
export function starContact(id: number): IContactAction {
return {
type: 'STAR',
id
};
}
import Immutable = require('immutable');
import { IContactAction } from './actions';
import { Contact as ContactModel} from './contact-store';
export function reducer(state: Immutable.List<ContactModel> = Immutable.List<ContactModel>(), action: IContactAction) {
switch (action.type) {
case 'ADD':
return state.push({
id: action.id,
name: action.name,
star: false
});
case 'REMOVE':
return state.delete(findIndexById());
case 'STAR':
return (<any>state).update(findIndexById(), (contact) => {
return {
id: contact.id,
name: contact.name,
star: !contact.star
};
});
default:
return state;
}
function findIndexById() {
return state.findIndex((contact) => contact.id === action.id);
}
}