<!DOCTYPE html>
<html>

<head>
  <title>angular2 playground</title> 
  <link rel="stylesheet" href="style.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>
  <root>
    loading...
  </root>
</body>

</html>
/* Styles go here */

.active {
  text-decoration: default;
}

.inactive {
  text-decoration: none; 
}
### Angular2 Todo List using Redux - Typescript - Beta 0

Features:
- Initial port from Redux video course by Dan Abramov. Note some parts divert from the original.
- Added Pipe to filter todos.
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: "./src",
    'redux': 'https://cdnjs.cloudflare.com/ajax/libs/redux/3.0.4/redux.js'
  },
  //packages defines our app package
  packages: {
    app: {
      main: './main.ts',
      defaultExtension: 'ts'
    }
  }
});
//main entry point
import {bootstrap} from 'angular2/platform/browser';
import {provide} from 'angular2/core';

import {App} from './app';
import {createStore} from 'redux';
import {rootReducer} from './rootReducer';
import {TodoActions} from './todoActions';

const appStore = createStore(rootReducer);

bootstrap(App, [
  provide('AppStore', { useValue: appStore }),
  TodoActions 
])
  .catch(err => console.error(err));
//our root app component
import {Component} from 'angular2/core'
import {AddTodo} from './addTodo';
import {TodoList} from './todoList';
import {Filters} from './filters';

@Component({
  selector: 'root', 
  template: 
    `<div>
      <add-todo></add-todo>
      <todo-list></todo-list>
      <filters></filters>
    </div>`,
  directives: [AddTodo, TodoList, Filters]
})
export class App { }
import {Component, Inject} from 'angular2/core';
import {TodoActions} from './todoActions';

@Component({
  selector: 'add-todo', 
  template: 
    `<div>
      <input #todo>
      <button (click)="addTodo(todo)">Add todo</button>
    </div>`
})
export class AddTodo {
  constructor(
    @Inject('AppStore') private appStore: AppStore, 
    private todoActions: TodoActions
  ){ }
  
  private addTodo(input) {
    this.appStore.dispatch(this.todoActions.addTodo(input.value));
    input.value = '';
  }
}
import {Component, Inject} from 'angular2/core';
import {Todo} from './todo';
import {VisibleTodosPipe} from './visibleTodosPipe';

@Component({
  selector: 'todo-list',
  template: `
    <ul>
      <todo 
        *ngFor="#todo of todos | visibleTodos:currentFilter"
        [completed]="todo.completed"
        [id]="todo.id"
      >{{todo.text}}</todo>
    </ul>
  `,
  directives: [Todo], 
  pipes: [VisibleTodosPipe]
})
export class TodoList implements OnDestroy {
  constructor(
    @Inject('AppStore') private appStore: AppStore
  ){
    this.unsubscribe = this.appStore.subscribe(()=> {
      let state = this.appStore.getState();
      this.currentFilter = state.currentFilter;
      this.todos = state.todos;
    });
  }
  
  private ngOnDestroy(){
    //remove listener
    this.unsubscribe();
  }
}

import {Component} from 'angular2/core'
import {FilterLink} from './filterLink';

@Component({
  selector: 'filters',
  template: 
    '<p>Show: ' +
      '<filter-link filter="SHOW_ALL">All</filter-link>, ' + 
      '<filter-link filter="SHOW_ACTIVE">Active</filter-link>, ' +
      '<filter-link filter="SHOW_COMPLETED">Completed</filter-link>' +
    '</p>',
  directives: [FilterLink] 
})
export class Filters { }
import {Component, ContentChildren, Inject, ChangeDetectionStrategy} from 'angular2/core';
import {TodoActions} from './todoActions';

@Component({
  selector: 'filter-link',
  inputs: ['filter'],
  template: 
    `<a href="#" (click)="applyFilter(filter);"
        [ngClass]="{'active': active, 'inactive': !active}">` +
      `<ng-content></ng-content>` +  
    `</a>`
})
export class FilterLink implements OnInit, OnDestroy {
  constructor(
    @Inject('AppStore') private appStore: AppStore, 
    private todoActions: TodoActions
  ){
    this.unsubscribe = this.appStore
      .subscribe(() => this.updateActive());
  }
  
  private ngOnInit(){
    //set initial state
    this.updateActive();
  }
  
  private ngOnDestroy(){
    //remove change listener
    this.unsubscribe();
  }
  
  // Helper methods
  private applyFilter(filter) {
    this.appStore.dispatch(this.todoActions.setCurrentFilter(filter));
  }
  
  private updateActive(){
    this.active = (this.filter == this.appStore.getState().currentFilter);
  }
}
import {Component, ContentChildren, Inject, ChangeDetectionStrategy} from 'angular2/core';
import {TodoActions} from './todoActions';

@Component({
  selector: 'todo',
  inputs: ['completed', 'id'],
  template: `
    <li (click)="onTodoClick(id)"
      [style.textDecoration]="completed?'line-through':'none'">
      <ng-content></ng-content>
    </li> 
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class Todo { 
  constructor(
    @Inject('AppStore') private appStore: AppStore, 
    private todoActions: TodoActions
  ){ }
  
  private onTodoClick(id){
    this.appStore.dispatch(this.todoActions.toggleTodo(id));
  }
  
  private removeTodo(id){
    this.appStore.dispatch(this.todoActions.removeTodo(id));
  }
}
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
export const SET_CURRENT_FILTER = 'SET_CURRENT_FILTER';

export class TodoActions {
  constructor() {
    this.nextToDoId = 0;
  }
  
  addTodo(text){  
    return {
      type: ADD_TODO,
      id: this.nextToDoId++,
      text: text,
      completed: false
    };
  };
  
  toggleTodo(id){
    return {
      type: TOGGLE_TODO,
      id: id
    };
  };
  
  removeTodo(id){
    return {
      type: REMOVE_TODO,
      id: id
    };    
  }
  
  setCurrentFilter(filter){
    return {
      type: SET_CURRENT_FILTER,
      filter: filter
    };
  };
}
import * as TodoActions from './todoActions';

const initialState = {
  todos: [],
  currentFilter: 'SHOW_ALL'
}

export function rootReducer(state = initialState, action){
  switch (action.type) {
    case TodoActions.ADD_TODO: 
      return {
        todos: state.todos.concat({
          id: action.id,
          text: action.text,
          completed: action.completed
        }),
        currentFilter: state.currentFilter
      };
    case TodoActions.TOGGLE_TODO:
      return {
        todos: toggleTodo(state.todos, action),
        currentFilter: state.currentFilter
      };
    case TodoActions.REMOVE_TODO:
      return {
        todos: state.todos.filter(todo => todo.id != action.id),
        currentFilter: state.currentFilter
      };
    case TodoActions.SET_CURRENT_FILTER:
      return { 
        todos: state.todos.map(todo => todo),
        currentFilter: action.filter
      };
    default:
      return state;
  }
};

function toggleTodo(todos, action){
  //map returns new array
  return todos.map(todo => {
    //skip other items
    if (todo.id !== action.id)
      return todo;
    //toggle
    return {
      id: todo.id,
      text: todo.text,
      completed: !todo.completed
    };
  });
}
import {isBlank, isPresent, isArray} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {Pipe, PipeTransform} from 'angular2/core';

@Pipe({
  name: 'visibleTodos'
})
export class VisibleTodosPipe implements PipeTransform {
  transform(todos, args){ 
    if (isBlank(args) || args.length == 0) {
      throw new BaseException('VisibleTodos pipe requires one argument');
    }
    if (isPresent(todos) && !isArray(todos)){
      throw new BaseException('VisibleTodos pipe requires an Array as input');
    }
    return this.getVisibleTodos(todos, args[0]);
  }
  
  private getVisibleTodos(todos, filter){
    switch (filter) {
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed);
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed);
      case 'SHOW_ALL':
      default:
        return todos;
    }
  };
}