Skip to content

Commit

Permalink
feat(app): an implementation of TodoMVC using ng-forward
Browse files Browse the repository at this point in the history
  • Loading branch information
petebacondarwin committed Oct 14, 2015
1 parent 13137fe commit 931b534
Show file tree
Hide file tree
Showing 18 changed files with 819 additions and 177 deletions.
11 changes: 0 additions & 11 deletions app/Nested/Nested.js

This file was deleted.

18 changes: 18 additions & 0 deletions app/components/Filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Component, View} from 'ng-forward';

@Component({
selector: 'filters',
properties: ['status']
})
@View({
template:
`
<ul class="filters">
<li><a ng-class="{selected: filters.status == ''}" href="#/">All</a></li>
<li><a ng-class="{selected: filters.status == 'active'}" href="#/active">Active</a></li>
<li><a ng-class="{selected: filters.status == 'completed'}" href="#/completed">Completed</a></li>
</ul>
`
})
export default class Filters {
}
24 changes: 24 additions & 0 deletions app/components/Footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {Component, View, EventEmitter} from 'ng-forward';
import Filters from './Filters';

@Component({
selector: 'footer',
properties: ['status', 'remainingCount', 'completedCount'],
events: ['clearCompleted']
})
@View({
directives: [Filters],
template:
`
<span class="todo-count"><strong>{{footer.remainingCount}}</strong>
<ng-pluralize count="footer.remainingCount" when="{ one: 'item left', other: 'items left' }"></ng-pluralize>
</span>
<filters [status]="footer.status"></filters>
<button class="clear-completed" ng-click="footer.clearCompleted.next()" ng-show="footer.completedCount">Clear completed</button>
`
})
export default class Footer {
constructor() {
this.clearCompleted = new EventEmitter();
}
}
40 changes: 40 additions & 0 deletions app/components/TextEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {Component, View, EventEmitter, Inject} from 'ng-forward';
import FocusOn from '../directives/FocusOn';

const ESC_KEY = 27;
const ENTER_KEY = 13;

@Component({
selector: 'text-editor',
properties: ['value', 'placeholder', 'inputClasses', 'focusOn'],
events: ['start', 'enter', 'end', 'abort']
})
@View({
directives: [FocusOn],
template:
`<input class="{{textEditor.inputClasses}}" placeholder="{{textEditor.placeholder}}"
ng-model="textEditor.value"
(keyup)="textEditor.keyup($event.keyCode)"
(focus)="textEditor.start.next()"
(blur)="textEditor.end.next()"
autofocus focus-on="textEditor.focusOn">`
})
@Inject('$element')
export default class TextEditor {
constructor($element) {
this.$input = $element.find('input')[0];
this.start = new EventEmitter();
this.enter = new EventEmitter();
this.end = new EventEmitter();
this.abort = new EventEmitter();
}

keyup(keyCode) {
if(keyCode === ENTER_KEY) {
this.enter.next();
}
if(keyCode === ESC_KEY) {
this.abort.next();
}
}
}
41 changes: 41 additions & 0 deletions app/components/TodoApp.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<section class="todoapp">

<header class="header">
<h1>todos</h1>
<text-editor
(enter)="todoApp.addTodo()"
(abort)="todoApp.newTodoTitle = ''()"
[(value)]="todoApp.newTodoTitle"
placeholder="What needs to be done?"
input-classes="new-todo"></text-editor>
</header>

<section class="main" ng-show="todoApp.todoStore.todos.length" ng-cloak>

<input class="toggle-all"
type="checkbox"
ng-model="todoApp.allChecked"
ng-change="todoApp.todoStore.setAllTo(todoApp.allChecked)">
<label for="toggle-all">Mark all as complete</label>

<ul class="todo-list">
<li ng-repeat="todo in todoApp.getFilteredTodos() track by $index"
ng-class="{completed: todo.completed, editing: todo.editing}">

<todo-view
[todo]="todo"
(title-change)="todoApp.todoStore.save()"
(completed-change)="todoApp.todoStore.save()"
(remove)="todoApp.todoStore.remove(todo)"></todo-view>
</li>
</ul>

</section>

<footer class="footer"
[remaining-count]="todoApp.todoStore.countRemaining()"
[completed-count]="todoApp.todoStore.countCompleted()"
(clear-completed)="todoApp.todoStore.removeCompleted()"
ng-show="todoApp.todoStore.todos.length"
ng-cloak></footer>
</section>
46 changes: 46 additions & 0 deletions app/components/TodoApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {Inject, Component, View} from 'ng-forward';
import {TodoStore} from '../services/TodoStore';
import Footer from './Footer';
import TextEditor from './TextEditor';
import TodoView from './TodoView';


@Component({
selector: 'todo-app',
bindings: [TodoStore]
})
@View({
directives: [TextEditor, TodoView, Footer],
template: require('./TodoApp.html')
})
@Inject(TodoStore, '$location')
export default class TodoApp {

todoStore: TodoStore;
newTodoTitle: string;

constructor(todoStore, $location) {
this.$location = $location;
this.todoStore = todoStore;
this.newTodoTitle = '';
}

addTodo() {
let newTodoTitle = this.newTodoTitle && this.newTodoTitle.trim();
if (newTodoTitle) {
this.todoStore.add(newTodoTitle);
this.newTodoTitle = '';
}
}

getFilteredTodos() {
switch(this.$location.path()) {
case '/completed':
return this.todoStore.todos.filter(todo => todo.completed);
case '/active':
return this.todoStore.todos.filter(todo => !todo.completed);
default:
return this.todoStore.todos;
}
}
}
47 changes: 47 additions & 0 deletions app/components/TodoView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {Component, View, EventEmitter} from 'ng-forward';

@Component({
selector: 'todo-view',
properties: ['todo'],
events: ['titleChange', 'completedChange', 'remove']
})
@View({
template:
`
<div class="view">
<input class="toggle" type="checkbox" ng-model="todoView.todo.completed" ng-change="todoView.completedChange.next()">
<label ng-dblclick="todoView.todo.editing = true" ng-hide="todoView.todo.editing">{{todoView.todo.title}}</label>
<button class="destroy" ng-click="todoView.remove.next()"></button>
</div>
<text-editor
ng-show="todoView.todo.editing"
[focus-on]="todoView.todo.editing"
(start)="todoView.saveOriginal()"
(end)="todoView.updateTitle()"
(enter)="todoView.updateTitle()"
(abort)="todoView.resetTodo()"
[(value)]="todoView.todo.title"
input-classes="edit"></text-editor>
`
})
export default class TodoView {
constructor() {
this.titleChange = new EventEmitter();
this.completedChange = new EventEmitter();
this.remove = new EventEmitter();
}

updateTitle() {
this.titleChange.next();
this.todo.editing = false;
}

saveOriginal() {
this.originalTitle = this.todo.title;
}

resetTodo() {
this.todo.title = this.originalTitle;
this.todo.editing = false;
}
}
17 changes: 17 additions & 0 deletions app/directives/FocusOn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Directive, Inject} from 'ng-forward';

/**
* Directive that places focus on the element it is applied to when the
* expression it binds to evaluates to true
*/
@Directive({ selector: '[focus-on]' })
@Inject('$timeout', '$scope', '$attrs', '$element')
export default class FocusOn {
constructor($timeout, $scope, $attrs, $element) {
$scope.$watch($attrs.focusOn, newVal => {
if (newVal) {
$timeout(() => $element[0].focus(), 0, false);
}
});
}
}
66 changes: 3 additions & 63 deletions app/index.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,7 @@
import 'babel/polyfill';
import 'angular';
import 'zone.js';
import {Component, View, bootstrap} from 'ng-forward';
import uiRouter from 'angular-ui-router';
import InnerApp from './innerApp/innerApp';
import Test from './services/Test';
import Nested from './Nested/Nested';
import {bootstrap} from 'ng-forward';
import TodoApp from './components/TodoApp';

// Our root component which we will bootstrap below. Again no module management
// needed. Here we specify the non-directive injectables we want to provide for
// injection in the 'bindings' array in @Component. Notice we are passing in
// "ui.router" as a string; ng-forward recognizes this as a module and bundles
// it with this component. All of ui-router's injectables are now available to
// inject into controllers or use in your templates.
@Component({
selector: 'app',
bindings: [Test, uiRouter]
})
@View({
// Again specifying directives to use. We really wanted to support specifying
// dependencies as Objects and not strings wherever possible. So we just pass
// in the InnerApp and Nested class references.
directives: [InnerApp, Nested],
template: `
<h1>App</h1>
<nested></nested>
<p>Trigger count: {{ app.triggers }}</p>
<!-- You still have to use non-event ng1 directives, such as ng-model used here. -->
<h4>One Way Binding to Child:</h4>
<input ng-model="app.message1"/>
<h4>Two Way Binding to/from Child:</h4>
<input ng-model="app.message2"/>
<hr/>
<!-- Here we see various bindings and events in use. We are listening for
the event1 and event2 events on inner-app. You have to prepend with
'()' for events. With message 1, 2 and 3, we show the three ways you
can bind to component properties: prop (with no prefix) will pass in
a simple string, [prop] will one-way bind to an expression, and
[(prop)] will two way bind to an expression. -->
<inner-app (event1)="app.onIncrement()" (event2)="app.onIncrement()"
[message1]="app.message1" [(message2)]="app.message2" message3="Hey, inner app... nothin'">
</inner-app>
`
})
class App{
constructor(){
this.triggers = 0;
this.message1 = 'Hey, inner app, you can not change this';
this.message2 = 'Hey, inner app, change me';
}

onIncrement(){
this.triggers++;
}
}

// Finally go ahead and bootstrap a component. It will look for the selector
// in your html and call ng1's bootstrap method on it. What's cool is if you
// include zone.js in your app, we'll automatically bootstrap your app within
// the context of a zone so you don't need to call $scope.$apply (Mike is
// this really true??).
bootstrap(App);
bootstrap(TodoApp);
Empty file removed app/index.scss
Empty file.
30 changes: 0 additions & 30 deletions app/innerApp/innerApp.html

This file was deleted.

Loading

0 comments on commit 931b534

Please sign in to comment.