├── src
├── assets
│ ├── .gitkeep
│ └── css
│ │ ├── global.css
│ │ ├── todomvc-common.css
│ │ └── todomvc-app.css
├── app
│ ├── app.component.css
│ ├── index.ts
│ ├── todo.ts
│ ├── todo.spec.ts
│ ├── app.module.ts
│ ├── app.component.ts
│ ├── app.component.html
│ ├── app.component.spec.ts
│ ├── todo-data.service.ts
│ └── todo-data.service.spec.ts
├── favicon.ico
├── styles.css
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── typings.d.ts
├── main.ts
├── tsconfig.json
├── index.html
├── polyfills.ts
└── test.ts
├── e2e
├── app.po.ts
├── app.e2e-spec.ts
└── tsconfig.json
├── .editorconfig
├── .gitignore
├── protractor.conf.js
├── karma.conf.js
├── angular-cli.json
├── package.json
├── README.md
└── tslint.json
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/app.component.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/index.ts:
--------------------------------------------------------------------------------
1 | export * from './app.component';
2 | export * from './app.module';
3 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sitepoint-editors/todo-app/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/src/assets/css/global.css:
--------------------------------------------------------------------------------
1 | .app-root-loader{
2 | text-align: center;
3 | padding: 30px;
4 | }
5 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | // Typings reference file, you can add your own global typings here
2 | // https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html
3 |
--------------------------------------------------------------------------------
/src/app/todo.ts:
--------------------------------------------------------------------------------
1 | export class Todo {
2 | id: number;
3 | title: string = '';
4 | complete: boolean = false;
5 |
6 | constructor(values: Object = {}) {
7 | Object.assign(this, values);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, element, by } from 'protractor';
2 |
3 | export class TodoAppPage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('app-root h1')).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { TodoAppPage } from './app.po';
2 |
3 | describe('todo-app App', function() {
4 | let page: TodoAppPage;
5 |
6 | beforeEach(() => {
7 | page = new TodoAppPage();
8 | });
9 |
10 | it('should display message saying app works', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('app works!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import './polyfills.ts';
2 |
3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
4 | import { enableProdMode } from '@angular/core';
5 | import { environment } from './environments/environment';
6 | import { AppModule } from './app/';
7 |
8 | if (environment.production) {
9 | enableProdMode();
10 | }
11 |
12 | platformBrowserDynamic().bootstrapModule(AppModule);
13 |
--------------------------------------------------------------------------------
/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "declaration": false,
5 | "emitDecoratorMetadata": true,
6 | "experimentalDecorators": true,
7 | "module": "commonjs",
8 | "moduleResolution": "node",
9 | "outDir": "../dist/out-tsc-e2e",
10 | "sourceMap": true,
11 | "target": "es5",
12 | "typeRoots": [
13 | "../node_modules/@types"
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false
8 | };
9 |
--------------------------------------------------------------------------------
/src/app/todo.spec.ts:
--------------------------------------------------------------------------------
1 | import {Todo} from './todo';
2 |
3 | describe('Todo', () => {
4 | it('should create an instance', () => {
5 | expect(new Todo()).toBeTruthy();
6 | });
7 |
8 | it('should accept values in the constructor', () => {
9 | let todo = new Todo({
10 | title: 'hello',
11 | complete: true
12 | });
13 | expect(todo.title).toEqual('hello');
14 | expect(todo.complete).toEqual(true);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "",
4 | "declaration": false,
5 | "emitDecoratorMetadata": true,
6 | "experimentalDecorators": true,
7 | "lib": ["es6", "dom"],
8 | "mapRoot": "./",
9 | "module": "es6",
10 | "moduleResolution": "node",
11 | "outDir": "../dist/out-tsc",
12 | "sourceMap": true,
13 | "target": "es5",
14 | "typeRoots": [
15 | "../node_modules/@types"
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | TodoApp
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Loading...
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 | import { FormsModule } from '@angular/forms';
4 | import { HttpModule } from '@angular/http';
5 |
6 | import { AppComponent } from './app.component';
7 |
8 | @NgModule({
9 | declarations: [
10 | AppComponent
11 | ],
12 | imports: [
13 | BrowserModule,
14 | FormsModule,
15 | HttpModule
16 | ],
17 | providers: [],
18 | bootstrap: [AppComponent]
19 | })
20 | export class AppModule { }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 |
7 | # dependencies
8 | /node_modules
9 | /bower_components
10 |
11 | # IDEs and editors
12 | /.idea
13 | /.vscode
14 | .project
15 | .classpath
16 | .c9/
17 | *.launch
18 | .settings/
19 |
20 | # misc
21 | /.sass-cache
22 | /connect.lock
23 | /coverage/*
24 | /libpeerconnection.log
25 | npm-debug.log
26 | testem.log
27 | /typings
28 |
29 | # e2e
30 | /e2e/*.js
31 | /e2e/*.map
32 |
33 | #System Files
34 | .DS_Store
35 | Thumbs.db
36 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | // This file includes polyfills needed by Angular 2 and is loaded before
2 | // the app. You can add your own extra polyfills to this file.
3 | import 'core-js/es6/symbol';
4 | import 'core-js/es6/object';
5 | import 'core-js/es6/function';
6 | import 'core-js/es6/parse-int';
7 | import 'core-js/es6/parse-float';
8 | import 'core-js/es6/number';
9 | import 'core-js/es6/math';
10 | import 'core-js/es6/string';
11 | import 'core-js/es6/date';
12 | import 'core-js/es6/array';
13 | import 'core-js/es6/regexp';
14 | import 'core-js/es6/map';
15 | import 'core-js/es6/set';
16 | import 'core-js/es6/reflect';
17 |
18 | import 'core-js/es7/reflect';
19 | import 'zone.js/dist/zone';
20 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {Todo} from './todo';
3 | import {TodoDataService} from './todo-data.service';
4 |
5 | @Component({
6 | selector: 'app-root',
7 | templateUrl: './app.component.html',
8 | styleUrls: ['./app.component.css'],
9 | providers: [TodoDataService]
10 | })
11 | export class AppComponent {
12 |
13 | newTodo: Todo = new Todo();
14 |
15 | constructor(private todoDataService: TodoDataService) {
16 | }
17 |
18 | addTodo() {
19 | this.todoDataService.addTodo(this.newTodo);
20 | this.newTodo = new Todo();
21 | }
22 |
23 | toggleTodoComplete(todo) {
24 | this.todoDataService.toggleTodoComplete(todo);
25 | }
26 |
27 | removeTodo(todo) {
28 | this.todoDataService.deleteTodoById(todo.id);
29 | }
30 |
31 | get todos() {
32 | return this.todoDataService.getAllTodos();
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/docs/referenceConf.js
3 |
4 | /*global jasmine */
5 | var SpecReporter = require('jasmine-spec-reporter');
6 |
7 | exports.config = {
8 | allScriptsTimeout: 11000,
9 | specs: [
10 | './e2e/**/*.e2e-spec.ts'
11 | ],
12 | capabilities: {
13 | 'browserName': 'chrome'
14 | },
15 | directConnect: true,
16 | baseUrl: 'http://localhost:4200/',
17 | framework: 'jasmine',
18 | jasmineNodeOpts: {
19 | showColors: true,
20 | defaultTimeoutInterval: 30000,
21 | print: function() {}
22 | },
23 | useAllAngular2AppRoots: true,
24 | beforeLaunch: function() {
25 | require('ts-node').register({
26 | project: 'e2e'
27 | });
28 | },
29 | onPrepare: function() {
30 | jasmine.getEnv().addReporter(new SpecReporter());
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | import './polyfills.ts';
2 |
3 | import 'zone.js/dist/long-stack-trace-zone';
4 | import 'zone.js/dist/proxy.js';
5 | import 'zone.js/dist/sync-test';
6 | import 'zone.js/dist/jasmine-patch';
7 | import 'zone.js/dist/async-test';
8 | import 'zone.js/dist/fake-async-test';
9 | import { getTestBed } from '@angular/core/testing';
10 | import {
11 | BrowserDynamicTestingModule,
12 | platformBrowserDynamicTesting
13 | } from '@angular/platform-browser-dynamic/testing';
14 |
15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
16 | declare var __karma__: any;
17 | declare var require: any;
18 |
19 | // Prevent Karma from running prematurely.
20 | __karma__.loaded = function () {};
21 |
22 | // First, initialize the Angular testing environment.
23 | getTestBed().initTestEnvironment(
24 | BrowserDynamicTestingModule,
25 | platformBrowserDynamicTesting()
26 | );
27 | // Then we find all the tests.
28 | let context = require.context('./', true, /\.spec\.ts/);
29 | // And load the modules.
30 | context.keys().map(context);
31 | // Finally, start Karma to run the tests.
32 | __karma__.start();
33 |
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async } from '@angular/core/testing';
4 | import { AppComponent } from './app.component';
5 | import { FormsModule } from '@angular/forms';
6 | import { Todo } from './todo';
7 |
8 | describe('AppComponent', () => {
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | imports: [
12 | FormsModule
13 | ],
14 | declarations: [
15 | AppComponent
16 | ],
17 | });
18 | });
19 |
20 | it('should create the app', async(() => {
21 | let fixture = TestBed.createComponent(AppComponent);
22 | let app = fixture.debugElement.componentInstance;
23 | expect(app).toBeTruthy();
24 | }));
25 |
26 | it(`should have a newTodo todo`, async(() => {
27 | let fixture = TestBed.createComponent(AppComponent);
28 | let app = fixture.debugElement.componentInstance;
29 | expect(app.newTodo instanceof Todo).toBeTruthy();
30 | }));
31 |
32 | it('should display "Todos" in h1 tag', async(() => {
33 | let fixture = TestBed.createComponent(AppComponent);
34 | fixture.detectChanges();
35 | let compiled = fixture.debugElement.nativeElement;
36 | expect(compiled.querySelector('h1').textContent).toContain('Todos');
37 | }));
38 | });
39 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/0.13/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', 'angular-cli'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-remap-istanbul'),
12 | require('angular-cli/plugins/karma')
13 | ],
14 | files: [
15 | { pattern: './src/test.ts', watched: false }
16 | ],
17 | preprocessors: {
18 | './src/test.ts': ['angular-cli']
19 | },
20 | mime: {
21 | 'text/x-typescript': ['ts','tsx']
22 | },
23 | remapIstanbulReporter: {
24 | reports: {
25 | html: 'coverage',
26 | lcovonly: './coverage/coverage.lcov'
27 | }
28 | },
29 | angularCli: {
30 | config: './angular-cli.json',
31 | environment: 'dev'
32 | },
33 | reporters: config.angularCli && config.angularCli.codeCoverage
34 | ? ['progress', 'karma-remap-istanbul']
35 | : ['progress'],
36 | port: 9876,
37 | colors: true,
38 | logLevel: config.LOG_INFO,
39 | autoWatch: true,
40 | browsers: ['Chrome'],
41 | singleRun: false
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "project": {
3 | "version": "1.0.0-beta.21",
4 | "name": "todo-app"
5 | },
6 | "apps": [
7 | {
8 | "root": "src",
9 | "outDir": "dist",
10 | "assets": [
11 | "assets",
12 | "favicon.ico"
13 | ],
14 | "index": "index.html",
15 | "main": "main.ts",
16 | "test": "test.ts",
17 | "tsconfig": "tsconfig.json",
18 | "prefix": "app",
19 | "mobile": false,
20 | "styles": [
21 | "styles.css"
22 | ],
23 | "scripts": [],
24 | "environments": {
25 | "source": "environments/environment.ts",
26 | "dev": "environments/environment.ts",
27 | "prod": "environments/environment.prod.ts"
28 | }
29 | }
30 | ],
31 | "addons": [],
32 | "packages": [],
33 | "e2e": {
34 | "protractor": {
35 | "config": "./protractor.conf.js"
36 | }
37 | },
38 | "test": {
39 | "karma": {
40 | "config": "./karma.conf.js"
41 | }
42 | },
43 | "defaults": {
44 | "styleExt": "css",
45 | "prefixInterfaces": false,
46 | "inline": {
47 | "style": false,
48 | "template": false
49 | },
50 | "spec": {
51 | "class": false,
52 | "component": true,
53 | "directive": true,
54 | "module": false,
55 | "pipe": true,
56 | "service": true
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todo-app",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "angular-cli": {},
6 | "scripts": {
7 | "start": "ng serve",
8 | "lint": "tslint \"src/**/*.ts\"",
9 | "test": "ng test",
10 | "pree2e": "webdriver-manager update",
11 | "e2e": "protractor"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/common": "2.2.1",
16 | "@angular/compiler": "2.2.1",
17 | "@angular/core": "2.2.1",
18 | "@angular/forms": "2.2.1",
19 | "@angular/http": "2.2.1",
20 | "@angular/platform-browser": "2.2.1",
21 | "@angular/platform-browser-dynamic": "2.2.1",
22 | "@angular/router": "3.2.1",
23 | "core-js": "^2.4.1",
24 | "rxjs": "5.0.0-beta.12",
25 | "ts-helpers": "^1.1.1",
26 | "zone.js": "^0.6.23"
27 | },
28 | "devDependencies": {
29 | "@angular/compiler-cli": "2.2.1",
30 | "@types/jasmine": "2.5.38",
31 | "@types/node": "^6.0.42",
32 | "angular-cli": "1.0.0-beta.21",
33 | "codelyzer": "~1.0.0-beta.3",
34 | "jasmine-core": "2.5.2",
35 | "jasmine-spec-reporter": "2.5.0",
36 | "karma": "1.2.0",
37 | "karma-chrome-launcher": "^2.0.0",
38 | "karma-cli": "^1.0.1",
39 | "karma-jasmine": "^1.0.2",
40 | "karma-remap-istanbul": "^0.2.1",
41 | "protractor": "4.0.9",
42 | "todomvc-app-css": "^2.0.6",
43 | "todomvc-common": "^1.0.3",
44 | "ts-node": "1.2.1",
45 | "tslint": "3.13.0",
46 | "typescript": "~2.0.3",
47 | "webdriver-manager": "10.2.5"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/app/todo-data.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Todo} from './todo';
3 |
4 | @Injectable()
5 | export class TodoDataService {
6 |
7 | // Placeholder for last id so we can simulate
8 | // automatic incrementing of id's
9 | lastId: number = 0;
10 |
11 | // Placeholder for todo's
12 | todos: Todo[] = [];
13 |
14 | constructor() {
15 | }
16 |
17 | // Simulate POST /todos
18 | addTodo(todo: Todo): TodoDataService {
19 | if (!todo.id) {
20 | todo.id = ++this.lastId;
21 | }
22 | this.todos.push(todo);
23 | return this;
24 | }
25 |
26 | // Simulate DELETE /todos/:id
27 | deleteTodoById(id: number): TodoDataService {
28 | this.todos = this.todos
29 | .filter(todo => todo.id !== id);
30 | return this;
31 | }
32 |
33 | // Simulate PUT /todos/:id
34 | updateTodoById(id: number, values: Object = {}): Todo {
35 | let todo = this.getTodoById(id);
36 | if (!todo) {
37 | return null;
38 | }
39 | Object.assign(todo, values);
40 | return todo;
41 | }
42 |
43 | // Simulate GET /todos
44 | getAllTodos(): Todo[] {
45 | return this.todos;
46 | }
47 |
48 | // Simulate GET /todos/:id
49 | getTodoById(id: number): Todo {
50 | return this.todos
51 | .filter(todo => todo.id === id)
52 | .pop();
53 | }
54 |
55 | // Toggle todo complete
56 | toggleTodoComplete(todo: Todo) {
57 | let updatedTodo = this.updateTodoById(todo.id, {
58 | complete: !todo.complete
59 | });
60 | return updatedTodo;
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/assets/css/todomvc-common.css:
--------------------------------------------------------------------------------
1 | hr {
2 | margin: 20px 0;
3 | border: 0;
4 | border-top: 1px dashed #c5c5c5;
5 | border-bottom: 1px dashed #f7f7f7;
6 | }
7 |
8 | .learn a {
9 | font-weight: normal;
10 | text-decoration: none;
11 | color: #b83f45;
12 | }
13 |
14 | .learn a:hover {
15 | text-decoration: underline;
16 | color: #787e7e;
17 | }
18 |
19 | .learn h3,
20 | .learn h4,
21 | .learn h5 {
22 | margin: 10px 0;
23 | font-weight: 500;
24 | line-height: 1.2;
25 | color: #000;
26 | }
27 |
28 | .learn h3 {
29 | font-size: 24px;
30 | }
31 |
32 | .learn h4 {
33 | font-size: 18px;
34 | }
35 |
36 | .learn h5 {
37 | margin-bottom: 0;
38 | font-size: 14px;
39 | }
40 |
41 | .learn ul {
42 | padding: 0;
43 | margin: 0 0 30px 25px;
44 | }
45 |
46 | .learn li {
47 | line-height: 20px;
48 | }
49 |
50 | .learn p {
51 | font-size: 15px;
52 | font-weight: 300;
53 | line-height: 1.3;
54 | margin-top: 0;
55 | margin-bottom: 0;
56 | }
57 |
58 | #issue-count {
59 | display: none;
60 | }
61 |
62 | .quote {
63 | border: none;
64 | margin: 20px 0 60px 0;
65 | }
66 |
67 | .quote p {
68 | font-style: italic;
69 | }
70 |
71 | .quote p:before {
72 | content: '“';
73 | font-size: 50px;
74 | opacity: .15;
75 | position: absolute;
76 | top: -20px;
77 | left: 3px;
78 | }
79 |
80 | .quote p:after {
81 | content: '”';
82 | font-size: 50px;
83 | opacity: .15;
84 | position: absolute;
85 | bottom: -42px;
86 | right: 3px;
87 | }
88 |
89 | .quote footer {
90 | position: absolute;
91 | bottom: -40px;
92 | right: 0;
93 | }
94 |
95 | .quote footer img {
96 | border-radius: 3px;
97 | }
98 |
99 | .quote footer a {
100 | margin-left: 5px;
101 | vertical-align: middle;
102 | }
103 |
104 | .speech-bubble {
105 | position: relative;
106 | padding: 10px;
107 | background: rgba(0, 0, 0, .04);
108 | border-radius: 5px;
109 | }
110 |
111 | .speech-bubble:after {
112 | content: '';
113 | position: absolute;
114 | top: 100%;
115 | right: 30px;
116 | border: 13px solid transparent;
117 | border-top-color: rgba(0, 0, 0, .04);
118 | }
119 |
120 | .learn-bar > .learn {
121 | position: absolute;
122 | width: 272px;
123 | top: 8px;
124 | left: -300px;
125 | padding: 10px;
126 | border-radius: 5px;
127 | background-color: rgba(255, 255, 255, .6);
128 | transition-property: left;
129 | transition-duration: 500ms;
130 | }
131 |
132 | @media (min-width: 899px) {
133 | .learn-bar {
134 | width: auto;
135 | padding-left: 300px;
136 | }
137 |
138 | .learn-bar > .learn {
139 | left: 8px;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TodoApp
2 |
3 | This repository contains the source code for the [SitePoint](https://www.sitepoint.com) article ["Angular 2 Tutorial: Create a CRUD App with Angular CLI"](https://www.sitepoint.com/angular-2-tutorial) by [Todd Motto](https://twitter.com/toddmotto) and [Jurgen Van de Moere](https://twitter.com/jvandemo).
4 |
5 | 
6 |
7 | Here is a [live demo](https://sitepoint-editors.github.io/todo-app/) of the application.
8 |
9 | ## Development server
10 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
11 |
12 | ## Code scaffolding
13 |
14 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class`.
15 |
16 | ## Build
17 |
18 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
19 |
20 | ## Running unit tests
21 |
22 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
23 |
24 | ## Running end-to-end tests
25 |
26 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
27 | Before running the tests make sure you are serving the app via `ng serve`.
28 |
29 | ## Deploying to Github Pages
30 |
31 | Run `ng github-pages:deploy` to deploy to Github Pages.
32 |
33 | ## Further help
34 |
35 | To get more help on the `angular-cli` use `ng --help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
36 |
37 | ## License
38 |
39 | The MIT License (MIT)
40 |
41 | Copyright (c) 2016 SitePoint
42 |
43 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
44 |
45 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
46 |
47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
48 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "class-name": true,
7 | "comment-format": [
8 | true,
9 | "check-space"
10 | ],
11 | "curly": true,
12 | "eofline": true,
13 | "forin": true,
14 | "indent": [
15 | true,
16 | "spaces"
17 | ],
18 | "label-position": true,
19 | "label-undefined": true,
20 | "max-line-length": [
21 | true,
22 | 140
23 | ],
24 | "member-access": false,
25 | "member-ordering": [
26 | true,
27 | "static-before-instance",
28 | "variables-before-functions"
29 | ],
30 | "no-arg": true,
31 | "no-bitwise": true,
32 | "no-console": [
33 | true,
34 | "debug",
35 | "info",
36 | "time",
37 | "timeEnd",
38 | "trace"
39 | ],
40 | "no-construct": true,
41 | "no-debugger": true,
42 | "no-duplicate-key": true,
43 | "no-duplicate-variable": true,
44 | "no-empty": false,
45 | "no-eval": true,
46 | "no-inferrable-types": true,
47 | "no-shadowed-variable": true,
48 | "no-string-literal": false,
49 | "no-switch-case-fall-through": true,
50 | "no-trailing-whitespace": true,
51 | "no-unused-expression": true,
52 | "no-unused-variable": true,
53 | "no-unreachable": true,
54 | "no-use-before-declare": true,
55 | "no-var-keyword": true,
56 | "object-literal-sort-keys": false,
57 | "one-line": [
58 | true,
59 | "check-open-brace",
60 | "check-catch",
61 | "check-else",
62 | "check-whitespace"
63 | ],
64 | "quotemark": [
65 | true,
66 | "single"
67 | ],
68 | "radix": true,
69 | "semicolon": [
70 | "always"
71 | ],
72 | "triple-equals": [
73 | true,
74 | "allow-null-check"
75 | ],
76 | "typedef-whitespace": [
77 | true,
78 | {
79 | "call-signature": "nospace",
80 | "index-signature": "nospace",
81 | "parameter": "nospace",
82 | "property-declaration": "nospace",
83 | "variable-declaration": "nospace"
84 | }
85 | ],
86 | "variable-name": false,
87 | "whitespace": [
88 | true,
89 | "check-branch",
90 | "check-decl",
91 | "check-operator",
92 | "check-separator",
93 | "check-type"
94 | ],
95 |
96 | "directive-selector-prefix": [true, "app"],
97 | "component-selector-prefix": [true, "app"],
98 | "directive-selector-name": [true, "camelCase"],
99 | "component-selector-name": [true, "kebab-case"],
100 | "directive-selector-type": [true, "attribute"],
101 | "component-selector-type": [true, "element"],
102 | "use-input-property-decorator": true,
103 | "use-output-property-decorator": true,
104 | "use-host-property-decorator": true,
105 | "no-input-rename": true,
106 | "no-output-rename": true,
107 | "use-life-cycle-interface": true,
108 | "use-pipe-transform-interface": true,
109 | "component-class-suffix": true,
110 | "directive-class-suffix": true,
111 | "templates-use-public": true,
112 | "invoke-injectable": true
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/app/todo-data.service.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import {TestBed, async, inject} from '@angular/core/testing';
4 | import {Todo} from './todo';
5 | import {TodoDataService} from './todo-data.service';
6 |
7 | describe('TodoDataService', () => {
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({
10 | providers: [TodoDataService]
11 | });
12 | });
13 |
14 | it('should ...', inject([TodoDataService], (service: TodoDataService) => {
15 | expect(service).toBeTruthy();
16 | }));
17 |
18 | describe('#getAllTodos()', () => {
19 |
20 | it('should return an empty array by default', inject([TodoDataService], (service: TodoDataService) => {
21 | expect(service.getAllTodos()).toEqual([]);
22 | }));
23 |
24 | it('should return all todos', inject([TodoDataService], (service: TodoDataService) => {
25 | let todo1 = new Todo({title: 'Hello 1', complete: false});
26 | let todo2 = new Todo({title: 'Hello 2', complete: true});
27 | service.addTodo(todo1);
28 | service.addTodo(todo2);
29 | expect(service.getAllTodos()).toEqual([todo1, todo2]);
30 | }));
31 |
32 | });
33 |
34 | describe('#save(todo)', () => {
35 |
36 | it('should automatically assign an incrementing id', inject([TodoDataService], (service: TodoDataService) => {
37 | let todo1 = new Todo({title: 'Hello 1', complete: false});
38 | let todo2 = new Todo({title: 'Hello 2', complete: true});
39 | service.addTodo(todo1);
40 | service.addTodo(todo2);
41 | expect(service.getTodoById(1)).toEqual(todo1);
42 | expect(service.getTodoById(2)).toEqual(todo2);
43 | }));
44 |
45 | });
46 |
47 | describe('#deleteTodoById(id)', () => {
48 |
49 | it('should remove todo with the corresponding id', inject([TodoDataService], (service: TodoDataService) => {
50 | let todo1 = new Todo({title: 'Hello 1', complete: false});
51 | let todo2 = new Todo({title: 'Hello 2', complete: true});
52 | service.addTodo(todo1);
53 | service.addTodo(todo2);
54 | expect(service.getAllTodos()).toEqual([todo1, todo2]);
55 | service.deleteTodoById(1);
56 | expect(service.getAllTodos()).toEqual([todo2]);
57 | service.deleteTodoById(2);
58 | expect(service.getAllTodos()).toEqual([]);
59 | }));
60 |
61 | it('should not removing anything if todo with corresponding id is not found', inject([TodoDataService], (service: TodoDataService) => {
62 | let todo1 = new Todo({title: 'Hello 1', complete: false});
63 | let todo2 = new Todo({title: 'Hello 2', complete: true});
64 | service.addTodo(todo1);
65 | service.addTodo(todo2);
66 | expect(service.getAllTodos()).toEqual([todo1, todo2]);
67 | service.deleteTodoById(3);
68 | expect(service.getAllTodos()).toEqual([todo1, todo2]);
69 | }));
70 |
71 | });
72 |
73 | describe('#updateTodoById(id, values)', () => {
74 |
75 | it('should return todo with the corresponding id and updated data', inject([TodoDataService], (service: TodoDataService) => {
76 | let todo = new Todo({title: 'Hello 1', complete: false});
77 | service.addTodo(todo);
78 | let updatedTodo = service.updateTodoById(1, {
79 | title: 'new title'
80 | });
81 | expect(updatedTodo.title).toEqual('new title');
82 | }));
83 |
84 | it('should return null if todo is not found', inject([TodoDataService], (service: TodoDataService) => {
85 | let todo = new Todo({title: 'Hello 1', complete: false});
86 | service.addTodo(todo);
87 | let updatedTodo = service.updateTodoById(2, {
88 | title: 'new title'
89 | });
90 | expect(updatedTodo).toEqual(null);
91 | }));
92 |
93 | });
94 |
95 | describe('#toggleTodoComplete(todo)', () => {
96 |
97 | it('should return the updated todo with inverse complete status', inject([TodoDataService], (service: TodoDataService) => {
98 | let todo = new Todo({title: 'Hello 1', complete: false});
99 | service.addTodo(todo);
100 | let updatedTodo = service.toggleTodoComplete(todo);
101 | expect(updatedTodo.complete).toEqual(true);
102 | service.toggleTodoComplete(todo);
103 | expect(updatedTodo.complete).toEqual(false);
104 | }));
105 |
106 | });
107 |
108 | });
109 |
--------------------------------------------------------------------------------
/src/assets/css/todomvc-app.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | font-weight: inherit;
16 | color: inherit;
17 | -webkit-appearance: none;
18 | appearance: none;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | }
22 |
23 | body {
24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
25 | line-height: 1.4em;
26 | background: #f5f5f5;
27 | color: #4d4d4d;
28 | min-width: 230px;
29 | max-width: 550px;
30 | margin: 0 auto;
31 | -webkit-font-smoothing: antialiased;
32 | -moz-osx-font-smoothing: grayscale;
33 | font-weight: 300;
34 | }
35 |
36 | :focus {
37 | outline: 0;
38 | }
39 |
40 | .hidden {
41 | display: none;
42 | }
43 |
44 | .todoapp {
45 | background: #fff;
46 | margin: 130px 0 40px 0;
47 | position: relative;
48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
49 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
50 | }
51 |
52 | .todoapp input::-webkit-input-placeholder {
53 | font-style: italic;
54 | font-weight: 300;
55 | color: #e6e6e6;
56 | }
57 |
58 | .todoapp input::-moz-placeholder {
59 | font-style: italic;
60 | font-weight: 300;
61 | color: #e6e6e6;
62 | }
63 |
64 | .todoapp input::input-placeholder {
65 | font-style: italic;
66 | font-weight: 300;
67 | color: #e6e6e6;
68 | }
69 |
70 | .todoapp h1 {
71 | position: absolute;
72 | top: -155px;
73 | width: 100%;
74 | font-size: 100px;
75 | font-weight: 100;
76 | text-align: center;
77 | color: rgba(175, 47, 47, 0.15);
78 | -webkit-text-rendering: optimizeLegibility;
79 | -moz-text-rendering: optimizeLegibility;
80 | text-rendering: optimizeLegibility;
81 | }
82 |
83 | .new-todo,
84 | .edit {
85 | position: relative;
86 | margin: 0;
87 | width: 100%;
88 | font-size: 24px;
89 | font-family: inherit;
90 | font-weight: inherit;
91 | line-height: 1.4em;
92 | border: 0;
93 | color: inherit;
94 | padding: 6px;
95 | border: 1px solid #999;
96 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
97 | box-sizing: border-box;
98 | -webkit-font-smoothing: antialiased;
99 | -moz-osx-font-smoothing: grayscale;
100 | }
101 |
102 | .new-todo {
103 | padding: 16px 16px 16px 60px;
104 | border: none;
105 | background: rgba(0, 0, 0, 0.003);
106 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
107 | }
108 |
109 | .main {
110 | position: relative;
111 | z-index: 2;
112 | border-top: 1px solid #e6e6e6;
113 | }
114 |
115 | label[for='toggle-all'] {
116 | display: none;
117 | }
118 |
119 | .toggle-all {
120 | position: absolute;
121 | top: -55px;
122 | left: -12px;
123 | width: 60px;
124 | height: 34px;
125 | text-align: center;
126 | border: none; /* Mobile Safari */
127 | }
128 |
129 | .toggle-all:before {
130 | content: '❯';
131 | font-size: 22px;
132 | color: #e6e6e6;
133 | padding: 10px 27px 10px 27px;
134 | }
135 |
136 | .toggle-all:checked:before {
137 | color: #737373;
138 | }
139 |
140 | .todo-list {
141 | margin: 0;
142 | padding: 0;
143 | list-style: none;
144 | }
145 |
146 | .todo-list li {
147 | position: relative;
148 | font-size: 24px;
149 | border-bottom: 1px solid #ededed;
150 | }
151 |
152 | .todo-list li:last-child {
153 | border-bottom: none;
154 | }
155 |
156 | .todo-list li.editing {
157 | border-bottom: none;
158 | padding: 0;
159 | }
160 |
161 | .todo-list li.editing .edit {
162 | display: block;
163 | width: 506px;
164 | padding: 12px 16px;
165 | margin: 0 0 0 43px;
166 | }
167 |
168 | .todo-list li.editing .view {
169 | display: none;
170 | }
171 |
172 | .todo-list li .toggle {
173 | text-align: center;
174 | width: 40px;
175 | /* auto, since non-WebKit browsers doesn't support input styling */
176 | height: auto;
177 | position: absolute;
178 | top: 0;
179 | bottom: 0;
180 | margin: auto 0;
181 | border: none; /* Mobile Safari */
182 | -webkit-appearance: none;
183 | appearance: none;
184 | }
185 |
186 | .todo-list li .toggle:after {
187 | content: url('data:image/svg+xml;utf8,');
188 | }
189 |
190 | .todo-list li .toggle:checked:after {
191 | content: url('data:image/svg+xml;utf8,');
192 | }
193 |
194 | .todo-list li label {
195 | word-break: break-all;
196 | padding: 15px 60px 15px 15px;
197 | margin-left: 45px;
198 | display: block;
199 | line-height: 1.2;
200 | transition: color 0.4s;
201 | }
202 |
203 | .todo-list li.completed label {
204 | color: #d9d9d9;
205 | text-decoration: line-through;
206 | }
207 |
208 | .todo-list li .destroy {
209 | display: none;
210 | position: absolute;
211 | top: 0;
212 | right: 10px;
213 | bottom: 0;
214 | width: 40px;
215 | height: 40px;
216 | margin: auto 0;
217 | font-size: 30px;
218 | color: #cc9a9a;
219 | margin-bottom: 11px;
220 | transition: color 0.2s ease-out;
221 | }
222 |
223 | .todo-list li .destroy:hover {
224 | color: #af5b5e;
225 | }
226 |
227 | .todo-list li .destroy:after {
228 | content: '×';
229 | }
230 |
231 | .todo-list li:hover .destroy {
232 | display: block;
233 | }
234 |
235 | .todo-list li .edit {
236 | display: none;
237 | }
238 |
239 | .todo-list li.editing:last-child {
240 | margin-bottom: -1px;
241 | }
242 |
243 | .footer {
244 | color: #777;
245 | padding: 10px 15px;
246 | height: 20px;
247 | text-align: center;
248 | border-top: 1px solid #e6e6e6;
249 | }
250 |
251 | .footer:before {
252 | content: '';
253 | position: absolute;
254 | right: 0;
255 | bottom: 0;
256 | left: 0;
257 | height: 50px;
258 | overflow: hidden;
259 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
260 | 0 8px 0 -3px #f6f6f6,
261 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
262 | 0 16px 0 -6px #f6f6f6,
263 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
264 | }
265 |
266 | .todo-count {
267 | float: left;
268 | text-align: left;
269 | }
270 |
271 | .todo-count strong {
272 | font-weight: 300;
273 | }
274 |
275 | .filters {
276 | margin: 0;
277 | padding: 0;
278 | list-style: none;
279 | position: absolute;
280 | right: 0;
281 | left: 0;
282 | }
283 |
284 | .filters li {
285 | display: inline;
286 | }
287 |
288 | .filters li a {
289 | color: inherit;
290 | margin: 3px;
291 | padding: 3px 7px;
292 | text-decoration: none;
293 | border: 1px solid transparent;
294 | border-radius: 3px;
295 | }
296 |
297 | .filters li a:hover {
298 | border-color: rgba(175, 47, 47, 0.1);
299 | }
300 |
301 | .filters li a.selected {
302 | border-color: rgba(175, 47, 47, 0.2);
303 | }
304 |
305 | .clear-completed,
306 | html .clear-completed:active {
307 | float: right;
308 | position: relative;
309 | line-height: 20px;
310 | text-decoration: none;
311 | cursor: pointer;
312 | }
313 |
314 | .clear-completed:hover {
315 | text-decoration: underline;
316 | }
317 |
318 | .info {
319 | margin: 65px auto 0;
320 | color: #bfbfbf;
321 | font-size: 10px;
322 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
323 | text-align: center;
324 | }
325 |
326 | .info p {
327 | line-height: 1;
328 | }
329 |
330 | .info a {
331 | color: inherit;
332 | text-decoration: none;
333 | font-weight: 400;
334 | }
335 |
336 | .info a:hover {
337 | text-decoration: underline;
338 | }
339 |
340 | /*
341 | Hack to remove background from Mobile Safari.
342 | Can't use it globally since it destroys checkboxes in Firefox
343 | */
344 | @media screen and (-webkit-min-device-pixel-ratio:0) {
345 | .toggle-all,
346 | .todo-list li .toggle {
347 | background: none;
348 | }
349 |
350 | .todo-list li .toggle {
351 | height: 40px;
352 | }
353 |
354 | .toggle-all {
355 | -webkit-transform: rotate(90deg);
356 | transform: rotate(90deg);
357 | -webkit-appearance: none;
358 | appearance: none;
359 | }
360 | }
361 |
362 | @media (max-width: 430px) {
363 | .footer {
364 | height: 50px;
365 | }
366 |
367 | .filters {
368 | bottom: 10px;
369 | }
370 | }
371 |
--------------------------------------------------------------------------------