├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── app.js ├── controllers │ └── main.js └── models │ └── Todo.js ├── views ├── index.jade └── templates │ └── TodoComponent.jade ├── webpack.config.babel.js └── webpack ├── index.js └── js ├── TodoComponent.js ├── TodoService.js └── boot.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | lib/ 4 | public/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Nathan Leung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular 2 Todo App 2 | 3 | ![Angular 2 Logo](https://i.imgur.com/8t4Ck5I.png) 4 | 5 | A basic todo app written using using [Angular 2](https://angular.io/) and the rest of the MEAN stack. The full tutorial can be found on [nathanhleung.com](https://www.nathanhleung.com/article/2015/12/26/getting-started-with-the-mean-stack/). 6 | 7 | Demo here: https://angular2-todo-app.herokuapp.com/ 8 | 9 | To run, `git clone` and then run `npm run webpack && npm start`. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todoapp", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "engines": { 7 | "node": "4.2.1" 8 | }, 9 | "scripts": { 10 | "babel": "babel src --out-dir lib", 11 | "babel:w": "babel -w src --out-dir lib", 12 | "webpack": "webpack", 13 | "webpack:w": "webpack --watch", 14 | "postinstall": "npm run babel && npm run webpack", 15 | "start": "node lib/app" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "angular2": "2.0.0-beta.15", 21 | "babel-cli": "6.7.7", 22 | "babel-core": "6.7.7", 23 | "babel-loader": "6.2.4", 24 | "babel-preset-es2015": "6.6.0", 25 | "body-parser": "1.15.0", 26 | "bootstrap": "3.3.6", 27 | "css-loader": "0.23.1", 28 | "es6-promise": "3.1.2", 29 | "es6-shim": "0.35.0", 30 | "express": "4.13.4", 31 | "file-loader": "0.8.5", 32 | "jade": "1.11.0", 33 | "method-override": "2.3.5", 34 | "mongoose": "4.4.13", 35 | "morgan": "1.7.0", 36 | "reflect-metadata": "0.1.2", 37 | "rxjs": "5.0.0-beta.2", 38 | "script-loader": "0.6.1", 39 | "style-loader": "0.13.0", 40 | "systemjs": "0.19.26", 41 | "webpack": "1.13.0", 42 | "zone.js": "0.6.10" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import dependencies 3 | */ 4 | import express from 'express'; 5 | import logger from 'morgan'; // Logs each server request to the console 6 | import bodyParser from 'body-parser'; // Takes information from POST requests and puts it into an object 7 | import methodOverride from 'method-override'; // Allows for PUT and DELETE methods to be used in browsers where they are not supported 8 | import mongoose from 'mongoose'; // Wrapper for interacting with MongoDB 9 | import path from 'path'; // File path utilities to make sure we're using the right type of slash (/ vs \) 10 | import http from 'http'; // To make the periodic DB clean requests 11 | 12 | /** 13 | * Import controllers 14 | */ 15 | import mainController from './controllers/main'; 16 | 17 | /** 18 | * Configure database 19 | */ 20 | mongoose.connect(process.env.MONGO_URL || 'mongodb://localhost:27017/todoDB'); // Connects to your MongoDB. Make sure mongod is running! 21 | mongoose.connection.on('error', function() { 22 | console.log('MongoDB Connection Error. Please make sure that MongoDB is running.'); 23 | process.exit(1); 24 | }); 25 | 26 | /** 27 | * Configure app 28 | */ 29 | let app = express(); // Creates an Express app 30 | app.set('port', process.env.PORT || 3000); // Set port to 3000 or the provided PORT variable 31 | app.set('views', path.join(__dirname, '..', 'views')); // Set our views directory to be `/views` (in the app root, which is one level above) 32 | app.set('view engine', 'jade'); // Set our view engine to be Jade (so when we render these views, they are compiled with the Jade compiler) 33 | app.use(express.static(path.join(__dirname, '..', 'public'))); // Set the static files directory - /public will be / on the frontend 34 | app.use(logger('dev')); // Log requests to the console 35 | app.use(bodyParser.json()); // Parse JSON data and put it into an object which we can access 36 | app.use(methodOverride()); // Allow PUT/DELETE 37 | 38 | /** 39 | * Configure routes 40 | */ 41 | app.get('/', mainController.getIndex); 42 | app.get('/templates/:template', mainController.getTemplate); 43 | app.get('/todos', mainController.getAllTodos); 44 | app.post('/todos', mainController.postNewTodo); 45 | app.delete('/todos', mainController.deleteAllTodos); 46 | app.delete('/todos/:id', mainController.deleteTodo); 47 | 48 | /** 49 | * Periodically clean database (every ten minutes, for Heroku demo) 50 | */ 51 | setInterval(() => { 52 | let req = http.request({ 53 | hostname: "localhost", 54 | port: app.get('port'), 55 | path: '/todos', 56 | method: 'DELETE' 57 | }, (res) => { 58 | res.setEncoding('utf8'); 59 | res.on('data', function (chunk) { 60 | console.log('Response body: ' + chunk); 61 | }); 62 | res.on('end', function() { 63 | console.log('Response end.') 64 | }); 65 | }); 66 | req.on('error', function(e) { 67 | console.log('Error with request: ' + e.message); 68 | }); 69 | req.end(); 70 | }, 1000 * 60 * 10); 71 | 72 | /** 73 | * Start app 74 | */ 75 | app.listen(app.get('port'), function() { 76 | console.log(`App listening on port ${app.get('port')}!`); 77 | }); 78 | -------------------------------------------------------------------------------- /src/controllers/main.js: -------------------------------------------------------------------------------- 1 | import Todo from '../models/Todo'; // Import the Todo model so we can query the DB 2 | 3 | let mainController = { 4 | getIndex: (req, res) => { 5 | res.render('index'); // Compiles the file named "index" in the views directory (`/views`) using the view engine (Jade). 6 | }, 7 | getTemplate: (req, res) => { 8 | res.render('templates/' + req.params.template); 9 | }, 10 | // This gets all Todos in the collection and sends it back in JSON format 11 | getAllTodos: (req, res) => { 12 | Todo.find({}, (err, todos) => { 13 | if (err) { 14 | // Send the error to the client if there is one 15 | return res.send(err); 16 | } 17 | // Send todos in JSON format 18 | res.json(todos); 19 | }); 20 | }, 21 | postNewTodo: (req, res) => { 22 | // This creates a new todo using POSTed data (in req.body) 23 | Todo.create({ 24 | text: req.body.text, 25 | done: false 26 | }, (err, todo) => { 27 | if (err) { 28 | return res.send(err); 29 | } 30 | Todo.find({}, (err, todos) => { 31 | if (err) { 32 | return res.send(err); 33 | } 34 | // Send list of all todos after new one has been created and saved 35 | res.json(todos); 36 | }); 37 | }); 38 | }, 39 | deleteTodo: (req, res) => { 40 | Todo.remove({ 41 | _id: req.params.id 42 | }, (err, todo) => { 43 | if (err) { 44 | return res.send(err); 45 | } 46 | Todo.find({}, (err, todos) => { 47 | if (err) { 48 | return res.send(err); 49 | } 50 | res.json(todos); 51 | }); 52 | }); 53 | }, 54 | deleteAllTodos: (req, res) => { 55 | Todo.remove({}, (err, todo) => { 56 | if (err) { 57 | return res.send(err); 58 | } 59 | Todo.find({}, (err, todos) => { 60 | if (err) { 61 | return res.send(err); 62 | } 63 | res.json(todos); 64 | }); 65 | }); 66 | } 67 | } 68 | 69 | export default mainController; 70 | -------------------------------------------------------------------------------- /src/models/Todo.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | // Create a schema for the Todo object 3 | let todoSchema = new mongoose.Schema({ 4 | text: String 5 | }); 6 | // Expose the model so that it can be imported and used in the controller 7 | export default mongoose.model('Todo', todoSchema); 8 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Angular 2/MEAN Stack Todo App 5 | meta(name='description' content='A small todo app written using the Angular 2 beta and the rest of the MEAN stack') 6 | meta(charset='utf-8') 7 | meta(name='viewport' content='width=device-width,initial-scale=1.0') 8 | link(rel='icon' href='https://i.imgur.com/IXtkiAk.png') 9 | style. 10 | /* Initial CSS to prevent FOUC (Flash of Unstyled Content) */ 11 | body { 12 | font-family: Arial; 13 | } 14 | .loading { 15 | padding-top: 15px; 16 | font-size: 36px; 17 | text-align: center; 18 | } 19 | body 20 | todo-app 21 | .loading(style='margin-top: 30px;') Loading... 22 | .container.text-center(style='margin-bottom: 30px;') 23 | p 24 | a(href='https://www.nathanhleung.com/article/2015/12/26/getting-started-with-the-mean-stack/') Tutorial 25 | |  -  26 | a(href='https://github.com/nathanhleung/angular2-todo-app') Source 27 | script(src="/bundle.js") 28 | -------------------------------------------------------------------------------- /views/templates/TodoComponent.jade: -------------------------------------------------------------------------------- 1 | .container 2 | .jumbotron.text-center 3 | h1 Todos  4 | span.label.label-info {{todos.length}} 5 | .row 6 | .col-sm-6.col-sm-offset-3 7 | table.table.table-bordered 8 | thead 9 | tr 10 | th.text-center Todos 11 | tbody 12 | tr(*ngFor="var todo of todos") 13 | td 14 | input(type='checkbox' on-click='deleteTodo(todo._id)' bind-checked="false") 15 | |    16 | | {{todo.text}} 17 | .row 18 | .col-sm-6.col-sm-offset-3 19 | form(on-ngSubmit='createTodo()') 20 | .form-group 21 | input.form-control.input-lg.text-center(type='text' placeholder='What to do next?' bindon-ngModel='todoData.text' required) 22 | .form-group.text-center 23 | br 24 | button.btn.btn-success.btn-lg(type='submit') Do Something 25 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | let config = { 4 | entry: path.join(__dirname, 'webpack', 'index.js'), 5 | output: { 6 | path: path.join(__dirname, 'public'), 7 | filename: 'bundle.js' 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.js$/, 13 | loaders: ['babel-loader'] 14 | }, 15 | { 16 | test: /\.css$/, 17 | loaders: ['style', 'css'] 18 | }, 19 | { 20 | test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, 21 | loaders: ['file-loader'] 22 | } 23 | ] 24 | } 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /webpack/index.js: -------------------------------------------------------------------------------- 1 | require('bootstrap/dist/css/bootstrap.min.css') 2 | 3 | // Import modules without parsing with script-loader, 4 | // !! to override all loaders 5 | require('!!script!angular2/bundles/angular2-polyfills.min.js'); 6 | require('!!script!rxjs/bundles/Rx.umd.min.js'); 7 | require('!!script!angular2/bundles/angular2-all.umd.min.js'); 8 | 9 | // Import boot, resolve imports/requires, and pass through Babel 10 | require('./js/boot'); 11 | -------------------------------------------------------------------------------- /webpack/js/TodoComponent.js: -------------------------------------------------------------------------------- 1 | import {Component, View} from 'angular2/core'; 2 | import {HTTP_PROVIDERS} from 'angular2/http'; // We're using http in our TodoService 3 | import {TodoService} from './TodoService' 4 | 5 | class TodoComponent { 6 | constructor(todoService) { 7 | // So it isn't undefined 8 | this.todos = []; 9 | this.todoData = { 10 | text: '' 11 | }; 12 | this.todoService = todoService; 13 | this.todoService.getAllTodos() 14 | .subscribe((res) => { 15 | this.todos = res; 16 | }); 17 | } 18 | createTodo() { 19 | this.todoService.postNewTodo(this.todoData) 20 | .subscribe((res) => { 21 | this.todos = res; 22 | this.todoData.text = ''; 23 | }); 24 | } 25 | deleteTodo(id) { 26 | this.todoService.deleteTodo(id) 27 | .subscribe((res) => { 28 | this.todos = res; 29 | }) 30 | } 31 | }; 32 | 33 | TodoComponent.annotations = [ 34 | new Component({ 35 | selector: 'todo-app', 36 | providers: [TodoService, HTTP_PROVIDERS], 37 | templateUrl: 'templates/TodoComponent' 38 | }), 39 | ]; 40 | 41 | TodoComponent.parameters = [[TodoService]]; 42 | 43 | export {TodoComponent}; 44 | -------------------------------------------------------------------------------- /webpack/js/TodoService.js: -------------------------------------------------------------------------------- 1 | import {Inject} from 'angular2/core'; 2 | import {Http, Headers} from 'angular2/http'; 3 | import 'rxjs/add/operator/map' 4 | 5 | class TodoService { 6 | constructor(http) { 7 | this.http = http; 8 | } 9 | getAllTodos() { 10 | return this.http.get('/todos') 11 | .map((res) => { 12 | return JSON.parse(res._body); 13 | }); 14 | } 15 | postNewTodo(data) { 16 | let headers = new Headers(); 17 | headers.append('Content-Type', 'application/json'); 18 | return this.http.post('/todos', JSON.stringify(data), { 19 | headers: headers 20 | }).map((res) => { 21 | return JSON.parse(res._body); 22 | }); 23 | } 24 | deleteTodo(id) { 25 | return this.http.delete('/todos/' + id) 26 | .map((res) => { 27 | return JSON.parse(res._body); 28 | }); 29 | } 30 | } 31 | 32 | TodoService.parameters = [new Inject(Http)]; 33 | 34 | export {TodoService} 35 | -------------------------------------------------------------------------------- /webpack/js/boot.js: -------------------------------------------------------------------------------- 1 | import {bootstrap} from 'angular2/platform/browser'; 2 | import {TodoComponent} from './TodoComponent'; 3 | 4 | let boot = document.addEventListener('DOMContentLoaded', () => { 5 | bootstrap(TodoComponent); 6 | }); 7 | 8 | // Expose boot so it can be required by webpack. 9 | module.exports = boot; 10 | --------------------------------------------------------------------------------