├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── package.json ├── public ├── app.js ├── graphql_transport.js ├── index.html └── style.css ├── server.js └── server ├── index.js └── schema.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | dist 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kadira Inc 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A Todo App with Angular and GraphQL 2 | 3 | A simple Todo app with Angular using GraphQL as the data source. 4 | **(This app has optimistic updates support)** 5 | 6 | [![A Angular Todo App with GraphQL](https://cldup.com/Y71WEMYzG2.png)](https://graphql-angular-todo.herokuapp.com/) 7 | 8 | > This project is extended from [this](http://codepen.io/anon/pen/KdxXRP) Codepen example. Kudos to the original author. 9 | 10 | ## Setting Up 11 | 12 | * Clone this repo 13 | * Run `npm install` 14 | * Start the app with `npm run dev` 15 | 16 | ## Source Code 17 | 18 | Most of the source code well commented and used Promises in many places. 19 | 20 | Use following entry points: 21 | 22 | * Client: `public/app.js` 23 | * Server: `server/index.js` 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-graphql-todo", 3 | "version": "1.0.0", 4 | "description": "Boilerplate for ReactJS project with hot code reloading", 5 | "scripts": { 6 | "dev": "./node_modules/.bin/nodemon --watch ./server server.js", 7 | "start": "NODE_ENV=production node server.js", 8 | "lint": "eslint src" 9 | }, 10 | "devDependencies": { 11 | "nodemon": "1.x.x", 12 | "babel-eslint": "^3.1.9", 13 | "eslint-plugin-react": "^2.3.0" 14 | }, 15 | "dependencies": { 16 | "babel": "5.x.x", 17 | "babel-core": "5.x.x", 18 | "babel-loader": "^5.1.2", 19 | "babel-runtime": "5.x.x", 20 | "body-parser": "1.x.x", 21 | "express": "^4.13.3", 22 | "graphql": "0.4.x", 23 | "underscore": "1.8.x" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | // Here, we use promises to simply the logic 2 | function TodoCtrl($scope) { 3 | $scope.todos = []; 4 | var transport = new GraphQLTransport(); 5 | var todosPromise = transport.sendQuery('{items}') 6 | .then(function(res) { 7 | return res.items; 8 | }); 9 | applyTodos(todosPromise); 10 | 11 | $scope.getTotalTodos = function() { 12 | return $scope.todos.length; 13 | }; 14 | 15 | $scope.addTodo = function() { 16 | // We add the todo item right away to showcase optimistic updates 17 | var newItem = $scope.formTodoText 18 | var simulatedTodos = todosPromise 19 | .then(function(items) { 20 | return items.concat(["adding " + newItem + " ..."]); 21 | }); 22 | 23 | applyTodos(simulatedTodos); 24 | $scope.formTodoText = ''; 25 | 26 | // add the actual item 27 | transport 28 | // Invoke the GraphQL query, which invoke our `addItem` mutation 29 | .sendQuery('mutation _ {item: addItem(item: "' + newItem + '")}') 30 | .then(function(data) { 31 | return data.item; 32 | }) 33 | .then(function(item) { 34 | // if success, we replace the todosPromise by adding the newly added item 35 | todosPromise = todosPromise.then(function(items) { 36 | return items.concat([item]); 37 | }); 38 | }, function(error) { 39 | // if there is an error, we simply alert it 40 | alert('Error adding item'); 41 | }) 42 | // delay 600ms to show changes from optimistic updates 43 | .then(function() { 44 | return new Promise(function(resolve) { 45 | setTimeout(resolve, 600); 46 | }) 47 | }) 48 | .then(function() { 49 | // finally apply the actual state of the todos 50 | applyTodos(todosPromise); 51 | }) 52 | }; 53 | 54 | function applyTodos(todosPromise) { 55 | todosPromise.then(function(items) { 56 | $scope.todos = []; 57 | items.forEach(function(item) { 58 | $scope.todos.push({ 59 | text: item, 60 | done: false 61 | }); 62 | }); 63 | 64 | $scope.$apply(); 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/graphql_transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | Simple GraphQL transport which send queries to a GraphQL endpoint 3 | */ 4 | function GraphQLTransport(path) { 5 | this.path = path || "/graphql"; 6 | } 7 | 8 | GraphQLTransport.prototype.sendQuery = function (query, vars) { 9 | var self = this; 10 | vars = vars || {}; 11 | return new Promise(function(resolve, reject) { 12 | // use fetch to get the result 13 | fetch(self.path, { 14 | method: 'post', 15 | headers: { 16 | 'Accept': 'application/json', 17 | 'Content-Type': 'application/json' 18 | }, 19 | body: JSON.stringify({ 20 | query, 21 | vars 22 | }) 23 | }) 24 | // get the result as JSON 25 | .then(function(res) { 26 | return res.json(); 27 | }) 28 | // trigger result or reject 29 | .then(function(response) { 30 | if(response.errors) { 31 | return reject(response.errors); 32 | } 33 | 34 | return resolve(response.data); 35 | }) 36 | .catch(reject); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

You've got {{getTotalTodos()}} things to do

14 | 19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #13756D; 3 | font-family: Arial; 4 | } 5 | 6 | .todo-wrapper { 7 | background: #55B6AE; 8 | width: 100%; 9 | } 10 | 11 | h2 { 12 | font-size: 2em; 13 | background: #1CA89C; 14 | padding: 40px; 15 | margin: 0; 16 | color: #333; 17 | text-align: center; 18 | } 19 | .emphasis { 20 | font-size: 4em; 21 | color: #fff; 22 | } 23 | 24 | ul { 25 | padding: 0px; 26 | margin: 0px; 27 | } 28 | 29 | li { 30 | font-size: 1.3em; 31 | padding: 18px; 32 | 33 | background: #65d8cb; /* Old browsers */ 34 | background: -webkit-gradient(linear, 0 0, 0 100%, from(#65d8cb), to(#72f4e9)); 35 | background: -webkit-linear-gradient(#65d8cb 0%, #72f4e9 100%); 36 | background: -moz-linear-gradient(#65d8cb 0%, #72f4e9 100%); 37 | background: -o-linear-gradient(#65d8cb 0%, #72f4e9 100%); 38 | background: linear-gradient(#65d8cb 0%, #72f4e9 100%); 39 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#65d8cb', endColorstr='#72f4e9',GradientType=0 ); 40 | list-style-type: none; 41 | margin-left: 0px; 42 | padding-left: 20px; 43 | } 44 | 45 | li input[type="checkbox"] { 46 | width: 40px; 47 | } 48 | 49 | .done-true { 50 | text-decoration: line-through; 51 | color: #ddd; 52 | } 53 | 54 | .add-input { 55 | width: 60%; 56 | height: 20px; 57 | float: left; 58 | border: none; 59 | padding: 53px 0; 60 | font-size: 2em; 61 | text-indent: 55px; 62 | } 63 | .add-btn { 64 | width: 40%; 65 | border: none; 66 | background: #29F4E3; 67 | padding: 0; 68 | height: 100px; 69 | h2 { 70 | background: #29F4E3; 71 | padding: 0; 72 | font-size: 4em; 73 | color: #333; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Here we requiring the babel require hook. 2 | // So, all the other files require after this will be transpiled. 3 | require('babel/register')({ 4 | optional: ['runtime', 'es7.asyncFunctions'] 5 | }); 6 | require('./server/index'); 7 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './schema'; 2 | import { graphql } from 'graphql'; 3 | import bodyParser from 'body-parser'; 4 | import express from 'express'; 5 | 6 | const { 7 | PORT = "3000" 8 | } = process.env; 9 | 10 | const app = express(); 11 | app.use(express.static('public')); 12 | app.use(bodyParser.json()); 13 | app.post('/graphql', (req, res) => { 14 | const {query, vars} = req.body; 15 | graphql(Schema, query, null, vars).then(result => { 16 | res.send(result); 17 | }); 18 | }); 19 | 20 | 21 | app.listen(PORT, (err, result) => { 22 | if(err) { 23 | throw err; 24 | } 25 | console.log(`Listening at localhost:${PORT}`); 26 | }); 27 | -------------------------------------------------------------------------------- /server/schema.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLSchema, 3 | GraphQLObjectType, 4 | GraphQLString, 5 | GraphQLList, 6 | GraphQLNonNull 7 | } from 'graphql'; 8 | 9 | // In memory data store 10 | const TodoStore = [ 11 | "Learn some GraphQL", 12 | "Build a sample app" 13 | ]; 14 | 15 | // Root level queries 16 | const TodosQuery = new GraphQLObjectType({ 17 | name: "TodosQuery", 18 | fields: () => ({ 19 | items: { 20 | type: new GraphQLList(GraphQLString), 21 | description: "List of todo items", 22 | resolve() { 23 | // close and send 24 | return TodoStore.concat([]); 25 | } 26 | } 27 | }) 28 | }); 29 | 30 | // Mutations 31 | const TodosMutations = new GraphQLObjectType({ 32 | name: 'TodosMutations', 33 | fields: () => ({ 34 | addItem: { 35 | type: GraphQLString, 36 | description: "Add a new todo item", 37 | args: { 38 | item: { 39 | type: new GraphQLNonNull(GraphQLString) 40 | } 41 | }, 42 | resolve(parent, {item}) { 43 | if(TodoStore.length >= 10) { 44 | // Remove the third time by keeping the first two 45 | TodoStore.splice(2, 1); 46 | } 47 | 48 | TodoStore.push(item); 49 | return item; 50 | } 51 | } 52 | }) 53 | }); 54 | 55 | // Schema 56 | const TodosSchema = new GraphQLSchema({ 57 | name: "TodosSchema", 58 | query: TodosQuery, 59 | mutation: TodosMutations 60 | }); 61 | 62 | export default TodosSchema; --------------------------------------------------------------------------------