<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Angular2 + Reactive Redux Sketch</title>
<link rel="stylesheet" href="https://cdn.rawgit.com/tastejs/todomvc-app-css/master/index.css">
<link rel="stylesheet" href="styles.css">
<!-- ES6-related imports -->
<script src="http://cdn.rawgit.com/google/traceur-compiler/90da568c7aa8e53ea362db1fc211fbb4f65b5e94/bin/traceur-runtime.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/systemjs/0.18.4/system.js"></script>
<script src="config.js"></script>
<!-- Angular2 import -->
<script src="http://code.angularjs.org/2.0.0-alpha.36/angular2.dev.js"></script>
<script>
//bootstrap the Angular2 application
System.import('app').catch(console.log.bind(console));
</script>
</head>
<body>
<todo-app>Loading...</todo-app>
</body>
</html>
import { Component, View, CORE_DIRECTIVES, ON_PUSH } from 'angular2/angular2';
import { RxPipe } from 'lib/rxPipe';
import { NgStore, NG_STORE_BINDINGS } from 'lib/ngStore'
import {TodoAppHeader} from './TodoAppHeader'
import {TodoList} from './TodoList'
import {TodoAppFooter} from './TodoAppFooter'
import Rx, {Observable} from 'rx.all'
@Component({
selector: 'todo-app',
changeDetection: ON_PUSH,
bindings: []
})
@View({
templateUrl: 'src/TodoApp.html',
directives: [CORE_DIRECTIVES, TodoAppHeader, TodoList, TodoAppFooter],
pipes: [RxPipe]
})
export class TodoApp {
constructor(public todoStore: NgStore){
this.todoFilter = this.todoStore.key('todoFilter');
this.allTodos = this.todoStore.key('todos')
this.visibleTodos =
Observable.combineLatest(this.todoFilter, this.allTodos)
.map(([todoFilter, todos]) => todos.filter(todoFilter.predicate)))
}
}
/// https://gist.github.com/jashmenn/d8f5cbf5fc20640bac30
/// <reference path="../../typings/app.d.ts" />
//
// Creates a pipe suitable for a RxJS observable:
//
// @View({
// template: '{{ someObservable | rx}}'
// pipes: [RxPipe]
// })
//
// Originally written by @gdi2290 but updated for 2.0.0.alpha-35 and use AsyncPipe
// (Soon the Angular team will be using RxJS natively and this pipe will be
// unnecessary because we'll be able to use the `async` pipe.)
//
// References:
// * rxPipeRegistry.ts https://gist.github.com/gdi2290/e9b2880a1d13057197d7 by @gdi2290
// * AsyncPipe https://github.com/angular/angular/blob/master/modules/angular2/src/pipes/async_pipe.ts
import {PipeFactory, Pipe, Injectable, bind, ChangeDetectorRef} from "angular2/angular2";
import {AsyncPipe} from "angular2/pipes";
import * as Rx from 'rx';
import {Observable} from 'rx';
function isObservable(obs) {
console.log(obs)
return obs && typeof obs.subscribe === 'function';
}
class RxStrategy {
createSubscription(async: any, updateLatestValue: any): any {
return async.subscribe((values) => {
updateLatestValue(values);
}, e => { throw e; });
}
dispose(subscription: any): void { subscription.dispose(); }
onDestroy(subscription: any): void { subscription.dispose(); }
}
var _rxStrategy = new RxStrategy();
@Pipe({name: 'rx'})
export class RxPipe extends AsyncPipe {
constructor(public _ref: ChangeDetectorRef) { super(_ref); }
supports(obs) { return isObservable(obs); }
_selectStrategy(obj: Observable<any>): any {
return _rxStrategy;
}
}
export var rxPipeInjectables: Array<any> = [
bind(RxPipe).toValue(RxPipe)
];
System.config({
defaultJSExtensions: true,
transpiler: 'typescript',
typescriptOptions: {
emitDecoratorMetadata: true
},
map: {
typescript: 'https://cdn.rawgit.com/robwormald/9883afae87bffa2f4e00/raw/8bca570a696c47a4f8dd03a52c98734676e94cea/typescript.js',
'rx.all': 'https://cdnjs.cloudflare.com/ajax/libs/rxjs/3.1.1/rx.all.js',
'redux': 'https://cdnjs.cloudflare.com/ajax/libs/redux/2.0.0/redux.js'
},
paths: {
app: 'src'
},
packages: {
//our app's source code
app: {
main: 'main.ts',
defaultExtension: 'ts',
},
lib: {
defaultExtension: 'ts'
}
}
});
export const ADD_TODO = 'ADD_TODO'
export const REMOVE_TODO = 'REMOVE_TODO'
export const UPDATE_TODO = 'UPDATE_TODO'
export const REMOVE_COMPLETED_TODOS = 'REMOVE_COMPLETED_TODOS'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const TOGGLE_ALL_TODOS = 'TOGGLE_ALL_TODOS'
export const SHOW_ALL_TODOS = 'SHOW_ALL_TODOS'
export const SHOW_COMPLETED_TODOS = 'SHOW_COMPLETED_TODOS'
export const SHOW_ACTIVE_TODOS = 'SHOW_ACTIVE_TODOS'
export const todos = (state = [], action) => {
switch(action.type){
case ADD_TODO:
let todo = Object.assign({},action.payload, {id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1})
return [...state, todo]
case REMOVE_TODO:
return state.filter(todo => todo.id !== action.payload.id)
case UPDATE_TODO:
return state.map(todo =>
todo.id === action.payload.id ?
Object.assign({}, todo, action.payload) : todo)
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.payload.id ?
Object.assign({}, todo, action.payload): todo)
case TOGGLE_ALL_TODOS:
return state.map(todo => {
return Object.assign({},todo, action.payload)
})
case REMOVE_COMPLETED_TODOS:
return state.filter(todo => !todo.completed)
default:
return state;
}
};
const showAll = (item) => true;
const showCompleted = (item) => item.completed;
const showActive = (item) => !item.completed;
export const todoFilter = (state = {predicate: showAll, key: SHOW_ALL_TODOS}, action ) => {
switch(action.type){
case SHOW_ALL_TODOS:
return Object.assign({},{predicate: showAll, key: action.type});
case SHOW_COMPLETED_TODOS:
return Object.assign({},{predicate: showCompleted, key: action.type});;
case SHOW_ACTIVE_TODOS:
return Object.assign({},{predicate: showActive, key: action.type});;
default
return state;
}
}
import Redux, {
createStore, combineReducers
}
from 'redux'
import Rx, {
Observable, Observer, Subject
}
from 'rx.all'
import {
Injectable
}
from 'angular2/di'
const INIT_STORE = 'INIT_STORE'
@Injectable()
export class NgStoreInitialState {}
@Injectable()
export class NgStoreRootReducer {}
@Injectable()
export class NgStore {
_subject: Subject<any>;
constructor(rootReducer: NgStoreRootReducer, initialState: NgStoreInitialState) {
let store = createStore(rootReducer, initialState);
const observableStore = Observable.create(changeObserver => {
let subscriber = store.subscribe(() => changeObserver.onNext(store.getState()));
return () => {
//TODO: disposal?
subscriber()
}
});
const actionDispatcher = Observer.create(store.dispatch);
this._subject = Subject.create(actionDispatcher, observableStore);
setTimeout(() => {
store.dispatch({
action: "NG_STORE_INIT"
})
})
}
createAction(type: string) {
return (payload: any) => {
console.log('dispatching', type, payload)
this.dispatch({
type, payload
})
};
}
dispatch(action) {
this._subject.onNext(action);
}
key(key) {
return this._subject.map(state => state[key]).publish().refCount();
}
}
export const NG_STORE_BINDINGS = [NgStoreInitialState, NgStoreRootReducer, NgStore];
### angular2 + redux + rxjs Todo
just a little sketch, working out how to best combine all these shiny new things
- redux provides the (immutable) store
- RxJS subject wraps the store and subscribes to it
- angular2 using ON_PUSH change detection triggered by the Rx pipe (so the store is bound to the view!)
<section class="todoapp">
<todo-app-header></todo-app-header>
<todo-list [todos]="visibleTodos"></todo-list>
<todo-app-footer></todo-app-footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="http://twitter.com/robwormald" target="_blank">@robwormald</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
import {
Component,
View
} from 'angular2/angular2'
import {
FormBuilder,
Validators,
FORM_BINDINGS,
FORM_DIRECTIVES
} from 'angular2/forms';
import {NgStore} from 'lib/ngStore'
@Component({
selector: 'todo-app-header',
bindings: [FORM_BINDINGS]
})
@View({
templateUrl: 'src/TodoAppHeader.html',
directives: [FORM_DIRECTIVES]
})
export class TodoAppHeader {
constructor(formBuilder: FormBuilder, todoStore: NgStore){
this.newTodo = formBuilder.group({
text: ["", Validators.required],
completed: false
});
this.todoStore = todoStore;
}
submit($event){
$event.preventDefault();
if(this.newTodo.valid){
this.todoStore.dispatch({
type: 'ADD_TODO',
payload: this.newTodo.value
});
this.newTodo.controls.text.updateValue("");
}
}
}
//angular imports
import {bootstrap, bind} from 'angular2/angular2'
import {combineReducers} from 'redux'
//import root component
import {TodoApp} from './TodoApp';
import {NgStoreRootReducer, NgStoreInitialState, NG_STORE_BINDINGS} from 'lib/ngStore';
import {todos, todoFilter} from './TodoStore'
bootstrap(TodoApp, [
NG_STORE_BINDINGS,
bind(NgStoreRootReducer).toValue(combineReducers({todos, todoFilter})),
bind(NgStoreInitialState).toValue({
todos: [
{
id: 1,
text: 'Learn Javascript',
completed: true
},
{
id: 2,
text: 'Forget Angular1',
completed: true
},
{
id: 3,
text: 'Use Angular2',
completed: false
}]
})
]);
import {
Component,
View,
CORE_DIRECTIVES,
ON_PUSH
} from 'angular2/angular2'
import {RxPipe} from 'lib/rxPipe'
import {NgStore} from 'lib/ngStore'
import {TodoItem} from './TodoItem'
@Component({
selector: 'todo-list',
bindings: [],
properties: ['todos'],
changeDetection: ON_PUSH
})
@View({
templateUrl: 'src/TodoList.html',
directives: [CORE_DIRECTIVES, TodoItem],
pipes: [RxPipe]
})
export class TodoList {
constructor(private todoStore: NgStore){}
toggleAll($event){
this.todoStore.dispatch({
type: 'TOGGLE_ALL_TODOS',
payload: {
completed: $event.target.checked
}
})
}
}
<section class="main">
<input class="toggle-all" type="checkbox" (change)="toggleAll($event)">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<todo-item *ng-for="#todo of todos | rx" [todo]="todo"></todo-item>
</ul>
</section>
import {
Component,
View,
CORE_DIRECTIVES,
ON_PUSH,
LifecycleEvent
} from 'angular2/angular2'
import {
FORM_DIRECTIVES,
FORM_BINDINGS,
FormBuilder,
Validators
} from 'angular2/forms'
import {RxPipe} from 'lib/rxPipe'
import {NgStore} from 'lib/ngStore'
@Component({
selector: 'todo-item',
bindings: [FORM_BINDINGS],
properties: ['todo'],
changeDetection: ON_PUSH
})
@View({
templateUrl: 'src/TodoItem.html',
directives: [CORE_DIRECTIVES],
pipes: [RxPipe]
})
export class TodoItem {
constructor(private todoStore: NgStore){
}
destroy(){
this.todoStore.dispatch({
type: 'REMOVE_TODO',
payload: this.todo
});
}
updateTodo($event,editableTodo){
$event.preventDefault()
this.todoStore.dispatch({
type: 'UPDATE_TODO',
payload: {
text: editableTodo.value,
id: this.todo.id
}
});
this.toggleEditable()
}
toggleEditable(){
this.todoStore.dispatch({
type: 'UPDATE_TODO',
payload: {id: this.todo.id, editing: !this.todo.editing}
});
}
toggleCompleted($event){
this.todoStore.dispatch({
type: 'UPDATE_TODO',
payload: {id: this.todo.id, completed: $event.target.checked}
});
}
}
<li [ng-class]="todo">
<div class="view">
<input class="toggle" type="checkbox" (change)="toggleCompleted($event)" [checked]="todo.completed">
<label (dblclick)="toggleEditable($event)">{{todo.text}}</label>
<button class="destroy" (click)="destroy()"></button>
</div>
<form (^submit)="updateTodo($event, editabletodo)">
<input #editabletodo class="edit" [value]="todo.text">
</form>
</li>
<section class="header">
<form (^submit)="submit($event)" [ng-form-model]="newTodo">
<h1>ng2dos</h1>
<input class="new-todo" placeholder="What needs to be done?" ng-control="text" autofocus>
</form>
</section>
import {
Component,
View,
CORE_DIRECTIVES,
ON_PUSH
} from 'angular2/angular2'
import {NgStore} from 'lib/ngStore'
import {RxPipe} from 'lib/rxPipe'
@Component({
selector: 'todo-app-footer',
bindings: [],
changeDetection: ON_PUSH
})
@View({
templateUrl: 'src/TodoAppFooter.html',
directives: [CORE_DIRECTIVES],
pipes: [RxPipe]
})
export class TodoAppFooter {
constructor(private todoStore: NgStore){
this.remainingTodosCount =
this.todoStore.key('todos')
.map(todos => todos.filter(todo => !todo.completed))
.map(remainingTodos => remainingTodos.length);
this.todoStore.key('todoFilter')
.map(todoFilter => todoFilter.key)
.subscribe(key => this.currentFilterKey = key);
}
showAllTodos(){
this.todoStore.dispatch({
type: 'SHOW_ALL_TODOS'
})
}
showActiveTodos(){
this.todoStore.dispatch({
type: 'SHOW_ACTIVE_TODOS'
})
}
showCompletedTodos(){
this.todoStore.dispatch({
type: 'SHOW_COMPLETED_TODOS'
})
}
clearCompletedTodos(){
this.todoStore.dispatch({
type: 'REMOVE_COMPLETED_TODOS'
})
}
}
<footer class="footer">
<span class="todo-count"><strong>{{remainingTodosCount | rx}}</strong> items left</span>
<!-- Remove this if you don't implement routing -->
<ul class="filters">
<li>
<a [ng-class]="{selected: currentFilterKey === 'SHOW_ALL_TODOS'}" href="#" (click)="showAllTodos()">All</a>
</li>
<li>
<a [ng-class]="{selected: currentFilterKey === 'SHOW_ACTIVE_TODOS'}" href="#" (click)="showActiveTodos()">Active</a>
</li>
<li>
<a [ng-class]="{selected: currentFilterKey === 'SHOW_COMPLETED_TODOS'}" href="#" (click)="showCompletedTodos()">Completed</a>
</li>
</ul>
<button class="clear-completed" (click)="clearCompletedTodos()">Clear completed</button>
</footer>