├── .gitattributes ├── client ├── ngapp │ ├── .buildignore │ ├── robots.txt │ ├── views │ │ ├── user.html │ │ ├── login.html │ │ ├── register.html │ │ ├── welcome.html │ │ ├── changes.html │ │ └── todos.html │ ├── favicon.ico │ ├── scripts │ │ ├── controllers │ │ │ ├── home.js │ │ │ ├── login.js │ │ │ ├── user.js │ │ │ ├── register.js │ │ │ ├── change.js │ │ │ └── todo.js │ │ ├── services │ │ │ └── lbclient.js │ │ └── app.js │ ├── config │ │ ├── routes.json │ │ └── bundle.js │ ├── test │ │ ├── .jshintrc │ │ ├── spec │ │ │ ├── controllers │ │ │ │ ├── home.js │ │ │ │ ├── user.js │ │ │ │ ├── login.js │ │ │ │ ├── todo.js │ │ │ │ ├── register.js │ │ │ │ └── change.js │ │ │ └── services │ │ │ │ └── lbclient.js │ │ └── karma.conf.js │ ├── index.html │ └── styles │ │ └── main.css └── lbclient │ ├── .gitignore │ ├── package.json │ ├── models │ ├── local-todo.json │ └── remote-todo.json │ ├── datasources.json │ ├── model-config.json │ ├── lbclient.js │ ├── datasources.local.js │ ├── build.js │ └── boot │ └── replication.js ├── .bowerrc ├── server ├── package.json ├── datasources.json ├── component-config.json ├── middleware.development.json ├── model-config.json ├── test │ ├── user.test.js │ ├── .jshintrc │ ├── todo.test.js │ └── helpers │ │ └── loopback-testing-helpers.js ├── boot │ ├── authentication.js │ ├── change-tracking.js │ ├── livereload.js │ ├── angular-routes.js │ └── dev-assets.js ├── datasources.production.js ├── config.json ├── datasources.staging.js ├── middleware.json ├── config.local.js └── server.js ├── .jshintignore ├── .npmignore ├── common └── models │ ├── test │ ├── todo.test.js │ └── user.test.js │ ├── user.json │ ├── todo.json │ └── todo.js ├── CODEOWNERS ├── .gitignore ├── .jshintrc ├── .editorconfig ├── bower.json ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── stale.yml ├── global-config.js ├── LICENSE ├── package.json ├── README.md ├── CONTRIBUTING.md └── Gruntfile.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /client/ngapp/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /client/lbclient/.gitignore: -------------------------------------------------------------------------------- 1 | browser.bundle.js 2 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /client/ngapp/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /client/ngapp/views/user.html: -------------------------------------------------------------------------------- 1 |

User

2 | 3 |

Under construction.

4 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "main": "server.js" 4 | } 5 | -------------------------------------------------------------------------------- /client/ngapp/views/login.html: -------------------------------------------------------------------------------- 1 |

Login

2 | 3 |

Under construction.

4 | -------------------------------------------------------------------------------- /client/lbclient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "main": "lbclient.js" 4 | } 5 | -------------------------------------------------------------------------------- /client/ngapp/views/register.html: -------------------------------------------------------------------------------- 1 |

Register

2 | 3 |

Under construction.

4 | -------------------------------------------------------------------------------- /client/lbclient/models/local-todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LocalTodo", 3 | "base": "Todo" 4 | } 5 | -------------------------------------------------------------------------------- /server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /server/component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopback-component-explorer": { 3 | "mountPath": "/explorer" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/ngapp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-offline-sync/HEAD/client/ngapp/favicon.ico -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /bower_components/ 3 | /client/ngapp/config/bundle.js 4 | /client/lbclient/browser.bundle.js 5 | -------------------------------------------------------------------------------- /client/ngapp/views/welcome.html: -------------------------------------------------------------------------------- 1 |

Welcome

2 | 3 | This is the welcome view. 4 | 5 | View your todo list. 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.dat 3 | *.gz 4 | *.log 5 | *.out 6 | *.pid 7 | *.seed 8 | .DS_Store 9 | lib-cov 10 | logs 11 | npm-debug.log 12 | pids 13 | results 14 | -------------------------------------------------------------------------------- /client/lbclient/models/remote-todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RemoteTodo", 3 | "base": "Todo", 4 | "plural": "Todos", 5 | "trackChanges": false, 6 | "enableRemoteReplication": true 7 | } 8 | -------------------------------------------------------------------------------- /client/lbclient/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "remote": { 3 | "connector": "remote" 4 | }, 5 | "local": { 6 | "connector": "memory", 7 | "localStorage": "todo-db" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/middleware.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "final:after": { 3 | "strong-error-handler": { 4 | "params": { 5 | "debug": true, 6 | "log": true 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": ["../common/models"] 4 | }, 5 | "Todo": { 6 | "dataSource": "db" 7 | }, 8 | "user": { 9 | "dataSource": "db" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/test/user.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | -------------------------------------------------------------------------------- /common/models/test/todo.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | -------------------------------------------------------------------------------- /common/models/test/user.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners, 3 | # the last matching pattern has the most precedence. 4 | 5 | # Core team members from IBM 6 | * @kjdelisle @jannyHou @loay @b-admike @ssh24 @virkt25 @dhmlau 7 | -------------------------------------------------------------------------------- /client/lbclient/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": ["../../common/models", "./models"] 4 | }, 5 | "RemoteTodo": { 6 | "dataSource": "remote" 7 | }, 8 | "LocalTodo": { 9 | "dataSource": "local" 10 | }, 11 | "user": { 12 | "dataSource": "remote" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/boot/authentication.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | module.exports = function(server) { 7 | 8 | // enable authentication 9 | // server.enableAuth(); 10 | }; 11 | -------------------------------------------------------------------------------- /common/models/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user", 3 | "base": "PersistedModel", 4 | "properties": { 5 | "id": { 6 | "id": true, 7 | "type": "string" 8 | }, 9 | "title": "string", 10 | "completed": { 11 | "type": "boolean", 12 | "default": false 13 | }, 14 | "created": { 15 | "type": "number" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/datasources.production.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | // Use the same environment-based configuration as in staging 7 | module.exports = require('./datasources.staging.js'); 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.dat 3 | *.gz 4 | *.log 5 | *.out 6 | *.pid 7 | *.seed 8 | .DS_Store 9 | .tmp 10 | build 11 | global.config.js 12 | bower_components 13 | client/dist 14 | ngapp/config/bundle.js 15 | html5/build 16 | lib-cov 17 | local.config.js 18 | logs 19 | node_modules 20 | npm-debug.log 21 | pids 22 | public/bundle.js 23 | results 24 | server/datasources.local.js 25 | db.json 26 | -------------------------------------------------------------------------------- /client/lbclient/lbclient.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var loopback = require('loopback'); 9 | var boot = require('loopback-boot'); 10 | 11 | var client = module.exports = loopback(); 12 | boot(client); 13 | -------------------------------------------------------------------------------- /client/lbclient/datasources.local.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var GLOBAL_CONFIG = require('../../global-config'); 9 | 10 | module.exports = { 11 | remote: { 12 | url: GLOBAL_CONFIG.restApiUrl 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /server/boot/change-tracking.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | module.exports = function(server) { 9 | 10 | // TODO(ritch) this should be unecessary soon.... 11 | var Todo = server.models.Todo; 12 | server.model(Todo.getChangeModel()); 13 | }; 14 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": "nofunc", 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": "vars", 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "globals": { 21 | "angular": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "strict": "throw", 5 | "persisteUndefinedAsNull": true, 6 | "trackChanges": true, 7 | "properties": { 8 | "id": { 9 | "id": true, 10 | "type": "string", 11 | "defaultFn": "guid" 12 | }, 13 | "title": "string", 14 | "completed": { 15 | "type": "boolean", 16 | "default": false 17 | }, 18 | "created": { 19 | "type": "number" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/boot/livereload.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | module.exports = function(app) { 9 | var livereload = app.get('livereload'); 10 | if (!livereload) return; 11 | 12 | app.middleware('routes:before', require('connect-livereload')({ 13 | port: livereload 14 | })); 15 | }; 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "restApiRoot": "/api", 4 | "host": "0.0.0.0", 5 | "port": 3000, 6 | "remoting": { 7 | "context": false, 8 | "rest": { 9 | "normalizeHttpPath": false, 10 | "xml": false 11 | }, 12 | "json": { 13 | "strict": false, 14 | "limit": "100kb" 15 | }, 16 | "urlencoded": { 17 | "extended": true, 18 | "limit": "100kb" 19 | }, 20 | "cors": false, 21 | "handleErrors": false 22 | }, 23 | "legacyExplorer": false 24 | } 25 | -------------------------------------------------------------------------------- /server/datasources.staging.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | module.exports = { 7 | db: { 8 | connector: 'mongodb', 9 | hostname: process.env.DB_HOST || 'localhost', 10 | port: process.env.DB_PORT || 27017, 11 | user: process.env.DB_USER, 12 | password: process.env.DB_PASSWORD, 13 | database: 'todo-example', 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /server/boot/angular-routes.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | module.exports = function(app) { 9 | var routes = require('../../client/ngapp/config/routes'); 10 | Object 11 | .keys(routes) 12 | .forEach(function(route) { 13 | app.get(route, function(req, res) { 14 | res.sendFile(app.get('indexFile')); 15 | }); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/home.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /** 9 | * @ngdoc function 10 | * @name loopbackExampleFullStackApp.controller:HomeCtrl 11 | * @description 12 | * # HomeCtrl 13 | * Controller of the loopbackExampleFullStackApp 14 | */ 15 | angular.module('loopbackExampleFullStackApp') 16 | .controller('HomeCtrl', function ($scope) { 17 | $scope.foo = Math.random(); 18 | }); 19 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/login.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /** 9 | * @ngdoc function 10 | * @name loopbackExampleFullStackApp.controller:LoginCtrl 11 | * @description 12 | * # LoginCtrl 13 | * Controller of the loopbackExampleFullStackApp 14 | */ 15 | angular.module('loopbackExampleFullStackApp') 16 | .controller('LoginCtrl', function ($scope) { 17 | $scope.foo = Math.random(); 18 | }); 19 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/user.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /** 9 | * @ngdoc function 10 | * @name loopbackExampleFullStackApp.controller:UserCtrl 11 | * @description 12 | * # UserCtrl 13 | * Controller of the loopbackExampleFullStackApp 14 | */ 15 | angular.module('loopbackExampleFullStackApp') 16 | .controller('UserCtrl', function ($scope) { 17 | $scope.foo = Math.random(); 18 | }); 19 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/register.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /** 9 | * @ngdoc function 10 | * @name loopbackExampleFullStackApp.controller:RegisterCtrl 11 | * @description 12 | * # RegisterCtrl 13 | * Controller of the loopbackExampleFullStackApp 14 | */ 15 | angular.module('loopbackExampleFullStackApp') 16 | .controller('RegisterCtrl', function ($scope) { 17 | $scope.foo = Math.random(); 18 | }); 19 | -------------------------------------------------------------------------------- /server/test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": "var", 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "before": false, 25 | "beforeEach": false, 26 | "describe": false, 27 | "it": false 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /server/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": {} 4 | }, 5 | "initial": { 6 | "compression": {}, 7 | "cors": { 8 | "params": { 9 | "origin": true, 10 | "credentials": true, 11 | "maxAge": 86400 12 | } 13 | } 14 | }, 15 | "session": { 16 | }, 17 | "auth": { 18 | }, 19 | "parse": { 20 | }, 21 | "routes": { 22 | "loopback#rest": { 23 | "paths": ["${restApiRoot}"] 24 | } 25 | }, 26 | "files": { 27 | "serve-static": { 28 | "params": "$!../client/ngapp" 29 | } 30 | }, 31 | "final": { 32 | "loopback#urlNotFound": {} 33 | }, 34 | "final:after": { 35 | "strong-error-handler": {} 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/boot/dev-assets.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var path = require('path'); 9 | 10 | module.exports = function(app) { 11 | if (!app.get('isDevEnv')) return; 12 | 13 | var serveDir = app.loopback.static; 14 | 15 | app.use(serveDir(projectPath('.tmp'))); 16 | app.use('/bower_components', serveDir(projectPath('bower_components'))); 17 | app.use('/lbclient', serveDir(projectPath('client/lbclient'))); 18 | }; 19 | 20 | function projectPath(relative) { 21 | return path.resolve(__dirname, '../..', relative); 22 | } 23 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-example-full-stack", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/strongloop/loopback-example-full-stack", 5 | "authors": [ 6 | "Sam Roberts " 7 | ], 8 | "license": "MIT", 9 | "private": true, 10 | "appPath": "client/ngapp", 11 | "testPath": "client/ngapp/test/spec", 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "**/bower_components", 16 | "test", 17 | "tests" 18 | ], 19 | "dependencies": { 20 | "angular": "~1.2.16", 21 | "json3": "~3.3.1", 22 | "es5-shim": "~3.1.0", 23 | "angular-route": "~1.2.16" 24 | }, 25 | "devDependencies": { 26 | "angular-mocks": "~1.2.16", 27 | "angular-scenario": "~1.2.16" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | #### Related issues 5 | 6 | 12 | 13 | - connect to 14 | 15 | ### Checklist 16 | 17 | 22 | 23 | - [ ] New tests added or existing tests modified to cover all changes 24 | - [ ] Code conforms with the [style 25 | guide](http://loopback.io/doc/en/contrib/style-guide.html) 26 | -------------------------------------------------------------------------------- /client/ngapp/config/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/": { 3 | "controller": "HomeCtrl", 4 | "templateUrl": "/views/welcome.html" 5 | }, 6 | "/me": { 7 | "controller": "UserCtrl", 8 | "templateUrl": "/views/user.html" 9 | }, 10 | "/my/todos/:status": { 11 | "controller": "TodoCtrl", 12 | "templateUrl": "/views/todos.html" 13 | }, 14 | "/my/todos": { 15 | "controller": "TodoCtrl", 16 | "templateUrl": "/views/todos.html" 17 | }, 18 | "/login": { 19 | "controller": "LoginCtrl", 20 | "templateUrl": "/views/login.html" 21 | }, 22 | "/register": { 23 | "controller": "RegisterCtrl", 24 | "templateUrl": "/views/register.html" 25 | }, 26 | "/debug": { 27 | "controller": "ChangeCtrl", 28 | "templateUrl": "/views/changes.html" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/ngapp/test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /server/config.local.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var GLOBAL_CONFIG = require('../global-config'); 9 | 10 | var env = (process.env.NODE_ENV || 'development'); 11 | var isDevEnv = env === 'development' || env === 'test'; 12 | 13 | module.exports = { 14 | hostname: GLOBAL_CONFIG.hostname, 15 | restApiRoot: GLOBAL_CONFIG.restApiRoot, 16 | livereload: process.env.LIVE_RELOAD, 17 | isDevEnv: isDevEnv, 18 | indexFile: require.resolve(isDevEnv ? 19 | '../client/ngapp/index.html' : '../client/dist/index.html'), 20 | port: GLOBAL_CONFIG.port, 21 | legacyExplorer: GLOBAL_CONFIG.legacyExplorer 22 | }; 23 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/home.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | describe('Controller: HomeCtrl', function () { 9 | 10 | // load the controller's module 11 | beforeEach(module('loopbackExampleFullStackApp')); 12 | 13 | var HomeCtrl, 14 | scope; 15 | 16 | // Initialize the controller and a mock scope 17 | beforeEach(inject(function ($controller, $rootScope) { 18 | scope = $rootScope.$new(); 19 | HomeCtrl = $controller('HomeCtrl', { 20 | $scope: scope 21 | }); 22 | })); 23 | 24 | it('should attach a `foo` property to the scope', function () { 25 | expect(scope.foo).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/user.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | describe('Controller: UserCtrl', function () { 9 | 10 | // load the controller's module 11 | beforeEach(module('loopbackExampleFullStackApp')); 12 | 13 | var UserCtrl, 14 | scope; 15 | 16 | // Initialize the controller and a mock scope 17 | beforeEach(inject(function ($controller, $rootScope) { 18 | scope = $rootScope.$new(); 19 | UserCtrl = $controller('UserCtrl', { 20 | $scope: scope 21 | }); 22 | })); 23 | 24 | it('should attach a `foo` property to the scope', function () { 25 | expect(scope.foo).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/login.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | describe('Controller: LoginCtrl', function () { 9 | 10 | // load the controller's module 11 | beforeEach(module('loopbackExampleFullStackApp')); 12 | 13 | var LoginCtrl, 14 | scope; 15 | 16 | // Initialize the controller and a mock scope 17 | beforeEach(inject(function ($controller, $rootScope) { 18 | scope = $rootScope.$new(); 19 | LoginCtrl = $controller('LoginCtrl', { 20 | $scope: scope 21 | }); 22 | })); 23 | 24 | it('should attach a `foo` property to the scope', function () { 25 | expect(scope.foo).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | describe('Controller: TodoCtrl', function () { 9 | 10 | // load the controller's module 11 | beforeEach(module('loopbackExampleFullStackApp')); 12 | 13 | var TodoCtrl, 14 | scope; 15 | 16 | // Initialize the controller and a mock scope 17 | beforeEach(inject(function ($controller, $rootScope) { 18 | scope = $rootScope.$new(); 19 | TodoCtrl = $controller('TodoCtrl', { 20 | $scope: scope 21 | }); 22 | })); 23 | 24 | it('should attach a list of Todos to the scope', function () { 25 | expect(scope.todos.length).toBe(0); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/register.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | describe('Controller: RegisterCtrl', function () { 9 | 10 | // load the controller's module 11 | beforeEach(module('loopbackExampleFullStackApp')); 12 | 13 | var RegisterCtrl, 14 | scope; 15 | 16 | // Initialize the controller and a mock scope 17 | beforeEach(inject(function ($controller, $rootScope) { 18 | scope = $rootScope.$new(); 19 | RegisterCtrl = $controller('RegisterCtrl', { 20 | $scope: scope 21 | }); 22 | })); 23 | 24 | it('should attach a `foo` property to the scope', function () { 25 | expect(scope.foo).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/change.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | describe('Controller: ChangeCtrl', function () { 9 | 10 | // load the controller's module 11 | beforeEach(module('loopbackExampleFullStackApp')); 12 | 13 | var ChangeCtrl, 14 | scope; 15 | 16 | // Initialize the controller and a mock scope 17 | beforeEach(inject(function ($controller, $rootScope) { 18 | scope = $rootScope.$new(); 19 | ChangeCtrl = $controller('ChangeCtrl', { 20 | $scope: scope 21 | }); 22 | })); 23 | 24 | it('should attach `clearLocalStorage()` to the scope', function () { 25 | expect(typeof scope.clearLocalStorage).toBe('function'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /client/ngapp/scripts/services/lbclient.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | // load lbclient via browserify's require 9 | var client = (function() { 10 | /*global require:true*/ 11 | return require('lbclient'); 12 | })(); 13 | 14 | /** 15 | * @ngdoc service 16 | * @name loopbackExampleFullStackApp.lbclient 17 | * @description 18 | * # lbclient 19 | * Value in the loopbackExampleFullStackApp. 20 | */ 21 | angular.module('loopbackExampleFullStackApp') 22 | .value('Todo', client.models.LocalTodo) 23 | .value('RemoteTodo', client.models.RemoteTodo) 24 | .value('sync', client.sync) 25 | .value('network', client.network) 26 | .value('getReadableModelId', client.getReadableModelId); 27 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/services/lbclient.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | describe('Service: lbclient', function () { 9 | 10 | // load the service's module 11 | beforeEach(module('loopbackExampleFullStackApp')); 12 | 13 | it('should provide Todo model', function() { 14 | inject(function(Todo) { 15 | expect(Todo).toBeDefined(); 16 | }); 17 | }); 18 | 19 | it('should provide `sync()` function', function() { 20 | inject(function(sync) { 21 | expect(typeof sync).toBe('function'); 22 | }); 23 | }); 24 | 25 | it('should provide `network` object', function() { 26 | inject(function(network) { 27 | expect(network).toBeDefined(); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /client/ngapp/scripts/app.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /** 9 | * @ngdoc overview 10 | * @name loopbackExampleFullStackApp 11 | * @description 12 | * # loopbackExampleFullStackApp 13 | * 14 | * Main module of the application. 15 | */ 16 | angular 17 | .module('loopbackExampleFullStackApp', [ 18 | 'ngRoute' 19 | ]) 20 | .config(function ($routeProvider, $locationProvider) { 21 | Object.keys(window.CONFIG.routes) 22 | .forEach(function(route) { 23 | var routeDef = window.CONFIG.routes[route]; 24 | $routeProvider.when(route, routeDef); 25 | }); 26 | 27 | $routeProvider 28 | .otherwise({ 29 | redirectTo: '/' 30 | }); 31 | 32 | $locationProvider.html5Mode(true); 33 | }); 34 | -------------------------------------------------------------------------------- /global-config.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /* 7 | * Global configuration shared by components. 8 | */ 9 | 'use strict'; 10 | 11 | var url = require('url'); 12 | 13 | var conf = { 14 | hostname: 'localhost', 15 | port: 3000, 16 | restApiRoot: '/api', // The path where to mount the REST API app 17 | legacyExplorer: false 18 | }; 19 | 20 | // The URL where the browser client can access the REST API is available. 21 | // Replace with a full url (including hostname) if your client is being 22 | // served from a different server than your REST API. 23 | conf.restApiUrl = url.format({ 24 | protocol: 'http', 25 | slashes: true, 26 | hostname: conf.hostname, 27 | port: conf.port, 28 | pathname: conf.restApiRoot 29 | }); 30 | 31 | module.exports = conf; 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | # Description/Steps to reproduce 11 | 12 | 16 | 17 | # Link to reproduction sandbox 18 | 19 | 24 | 25 | # Expected result 26 | 27 | 30 | 31 | # Additional information 32 | 33 | 38 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - critical 10 | - p1 11 | - major 12 | # Label to use when marking an issue as stale 13 | staleLabel: stale 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | # Comment to post when closing a stale issue. Set to `false` to disable 20 | closeComment: > 21 | This issue has been closed due to continued inactivity. Thank you for your understanding. 22 | If you believe this to be in error, please contact one of the code owners, 23 | listed in the `CODEOWNERS` file at the top-level of this repository. 24 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var loopback = require('loopback'); 9 | var boot = require('loopback-boot'); 10 | 11 | var app = module.exports = loopback(); 12 | 13 | // boot scripts mount components like REST API 14 | boot(app, __dirname); 15 | 16 | // optionally start the app 17 | app.start = function() { 18 | // start the web server 19 | return app.listen(function() { 20 | app.emit('started'); 21 | var baseUrl = app.get('url').replace(/\/$/, ''); 22 | console.log('Web server listening at: %s', baseUrl); 23 | if (app.get('loopback-component-explorer')) { 24 | var explorerPath = app.get('loopback-component-explorer').mountPath; 25 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 26 | } 27 | }); 28 | }; 29 | 30 | if (require.main === module) { 31 | app.start(); 32 | } 33 | -------------------------------------------------------------------------------- /client/ngapp/config/bundle.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | window.CONFIG = { 7 | "routes": { 8 | "/": { 9 | "controller": "HomeCtrl", 10 | "templateUrl": "/views/welcome.html" 11 | }, 12 | "/me": { 13 | "controller": "UserCtrl", 14 | "templateUrl": "/views/user.html" 15 | }, 16 | "/my/todos/:status": { 17 | "controller": "TodoCtrl", 18 | "templateUrl": "/views/todos.html" 19 | }, 20 | "/my/todos": { 21 | "controller": "TodoCtrl", 22 | "templateUrl": "/views/todos.html" 23 | }, 24 | "/login": { 25 | "controller": "LoginCtrl", 26 | "templateUrl": "/views/login.html" 27 | }, 28 | "/register": { 29 | "controller": "RegisterCtrl", 30 | "templateUrl": "/views/register.html" 31 | }, 32 | "/debug": { 33 | "controller": "ChangeCtrl", 34 | "templateUrl": "/views/changes.html" 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/change.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /** 9 | * @ngdoc function 10 | * @name loopbackExampleFullStackApp.controller:ChangeCtrl 11 | * @description 12 | * # ChangeCtrl 13 | * Controller of the loopbackExampleFullStackApp 14 | */ 15 | angular.module('loopbackExampleFullStackApp') 16 | .controller('ChangeCtrl', function ChangeCtrl($scope, $routeParams, $filter, 17 | Todo, RemoteTodo) { 18 | 19 | Todo.getChangeModel().find(function(err, changes) { 20 | $scope.changes = changes; 21 | $scope.$apply(); 22 | 23 | RemoteTodo.diff(0, changes, function(err, diff) { 24 | $scope.diff = diff; 25 | $scope.$apply(); 26 | }); 27 | }); 28 | 29 | $scope.clearLocalStorage = function() { 30 | localStorage.removeItem('todo-db'); 31 | }; 32 | 33 | Todo.find(function(err, todos) { 34 | $scope.todos = todos; 35 | $scope.$apply(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2014,2017. All Rights Reserved. 2 | Node module: loopback-example-offline-sync 3 | This project is licensed under the MIT License, full text below. 4 | 5 | -------- 6 | 7 | MIT license 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /client/lbclient/build.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var path = require('path'); 9 | var pkg = require('./package.json'); 10 | var fs = require('fs'); 11 | var browserify = require('browserify'); 12 | var boot = require('loopback-boot'); 13 | 14 | module.exports = function buildBrowserBundle(env, callback) { 15 | var b = browserify({ basedir: __dirname }); 16 | b.require('./' + pkg.main, { expose: 'lbclient' }); 17 | 18 | try { 19 | boot.compileToBrowserify({ 20 | appRootDir: __dirname, 21 | env: env 22 | }, b); 23 | } catch(err) { 24 | return callback(err); 25 | } 26 | 27 | var bundlePath = path.resolve(__dirname, 'browser.bundle.js'); 28 | var out = fs.createWriteStream(bundlePath); 29 | var isDevEnv = ['debug', 'development', 'test'].indexOf(env) !== -1; 30 | 31 | b.bundle({ 32 | // TODO(bajtos) debug should be always true, the sourcemaps should be 33 | // saved to a standalone file when !isDev(env) 34 | debug: isDevEnv 35 | }) 36 | .on('error', callback) 37 | .pipe(out); 38 | 39 | out.on('error', callback); 40 | out.on('close', callback); 41 | }; 42 | -------------------------------------------------------------------------------- /common/models/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var async = require('async'); 9 | 10 | module.exports = function(Todo) { 11 | Todo.definition.properties.created.default = Date.now; 12 | 13 | Todo.stats = function(filter, cb) { 14 | var stats = {}; 15 | cb = arguments[arguments.length - 1]; 16 | var Todo = this; 17 | 18 | async.parallel([ 19 | countComplete, 20 | count 21 | ], function(err) { 22 | if (err) return cb(err); 23 | stats.remaining = stats.total - stats.completed; 24 | cb(null, stats); 25 | }); 26 | 27 | function countComplete(cb) { 28 | Todo.count({completed: true}, function(err, count) { 29 | stats.completed = count; 30 | cb(err); 31 | }); 32 | } 33 | 34 | function count(cb) { 35 | Todo.count(function(err, count) { 36 | stats.total = count; 37 | cb(err); 38 | }); 39 | } 40 | }; 41 | 42 | Todo.handleChangeError = function(err) { 43 | console.warn('Cannot update change records for Todo:', err); 44 | }; 45 | 46 | Todo.remoteMethod('stats', { 47 | accepts: {arg: 'filter', type: 'object'}, 48 | returns: {arg: 'stats', type: 'object'}, 49 | http: { path: '/stats' } 50 | }, Todo.stats); 51 | }; 52 | -------------------------------------------------------------------------------- /client/lbclient/boot/replication.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | // TODO(bajtos) Move the bi-di replication to loopback core, 9 | // add model settings to enable the replication. 10 | // Example: 11 | // LocalTodo: { options: { 12 | // base: 'Todo', 13 | // replicate: { 14 | // target: 'Todo', 15 | // mode: 'push' | 'pull' | 'bidi' 16 | // }}} 17 | var proquint = require('proquint'); 18 | 19 | module.exports = function(client) { 20 | var LocalTodo = client.models.LocalTodo; 21 | var RemoteTodo = client.models.RemoteTodo; 22 | 23 | client.network = { 24 | _isConnected: true, 25 | get isConnected() { 26 | console.log('isConnected?', this._isConnected); 27 | return this._isConnected; 28 | }, 29 | set isConnected(value) { 30 | this._isConnected = value; 31 | } 32 | }; 33 | 34 | // setup model replication 35 | var since = { push: -1, pull: -1 }; 36 | function sync(cb) { 37 | LocalTodo.replicate( 38 | since.push, 39 | RemoteTodo, 40 | function pushed(err, conflicts, cps) { 41 | since.push = cps; 42 | RemoteTodo.replicate( 43 | since.pull, 44 | LocalTodo, 45 | function pulled(err, conflicts, cps) { 46 | since.pull = cps; 47 | if (cb) cb(); 48 | }); 49 | }); 50 | } 51 | 52 | // sync local changes if connected 53 | LocalTodo.on('after save', function(ctx, next) { 54 | next(); 55 | sync(); 56 | }); 57 | LocalTodo.on('after delete', function(ctx, next) { 58 | next(); 59 | sync(); 60 | }); 61 | 62 | client.sync = sync; 63 | 64 | client.getReadableModelId = function(modelId) { 65 | return proquint.encode(new Buffer(modelId.substring(0, 8), 'binary')); 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /client/ngapp/views/changes.html: -------------------------------------------------------------------------------- 1 |

Local Change List

2 |

3 | A list of all changes made to models in local storage. 4 |

5 | 6 | No local changes have been made. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Model IDTypeCheckpointRevisionPrev Revision
{{change.modelId}}{{change.type()}}{{change.checkpoint}}{{change.rev}}{{change.prev}}
24 | 25 |

Local to Server Deltas

26 |

27 | Below is list of changes required to replicate local data to the server. 28 |

29 | No changes required to replicate the local data to the server. 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
Model IDRevisionPrev Revision
{{delta.modelId}}{{delta.rev}}{{delta.prev}}
42 | 43 |

Local Storage Data

44 |

45 | Clear Local Storage 46 |

47 | 48 | There is no data in local storage. 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
Todo IDTitleCompleted
{{todo.getId()}}{{todo.title}}{{todo.completed}}
62 | 63 |

Local to Server Conflicts

64 |

65 | Below is list of changes that cannot be replicated to the server. 66 |

67 | No conflicts... 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
Model IDRevisionPrev Revision
{{conflict.modelId}}{{conflict.rev}}{{conflict.prev}}
80 | -------------------------------------------------------------------------------- /client/ngapp/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | // Karma configuration 7 | // http://karma-runner.github.io/0.12/config/configuration-file.html 8 | // Generated on 2014-06-23 using 9 | // generator-karma 0.8.2 10 | 'use strict'; 11 | 12 | module.exports = function(config) { 13 | config.set({ 14 | // enable / disable watching file and executing tests whenever any file changes 15 | autoWatch: true, 16 | 17 | // base path, that will be used to resolve files and exclude 18 | basePath: '../', 19 | 20 | // testing framework to use (jasmine/mocha/qunit/...) 21 | frameworks: ['jasmine'], 22 | 23 | // list of files / patterns to load in the browser 24 | files: [ 25 | '../../bower_components/es5-shim/es5-shim.js', 26 | '../../bower_components/angular/angular.js', 27 | '../../bower_components/angular-mocks/angular-mocks.js', 28 | '../../bower_components/angular-route/angular-route.js', 29 | '../lbclient/browser.bundle.js', 30 | 'config/bundle.js', 31 | 'scripts/**/*.js', 32 | 'test/mock/**/*.js', 33 | 'test/spec/**/*.js' 34 | ], 35 | 36 | // list of files / patterns to exclude 37 | exclude: [], 38 | 39 | // web server port 40 | port: 8080, 41 | 42 | // Start these browsers, currently available: 43 | // - Chrome 44 | // - ChromeCanary 45 | // - Firefox 46 | // - Opera 47 | // - Safari (only Mac) 48 | // - PhantomJS 49 | // - IE (only Windows) 50 | browsers: [ 51 | 'Chrome' 52 | ], 53 | 54 | // Which plugins to enable 55 | plugins: [ 56 | 'karma-chrome-launcher', 57 | 'karma-phantomjs-launcher', 58 | 'karma-jasmine' 59 | ], 60 | 61 | // Continuous Integration mode 62 | // if true, it capture browsers, run tests and exit 63 | singleRun: false, 64 | 65 | colors: true, 66 | 67 | // level of logging 68 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 69 | logLevel: config.LOG_INFO, 70 | 71 | // Uncomment the following lines if you are using grunt's server to run the tests 72 | // proxies: { 73 | // '/': 'http://localhost:9000/' 74 | // }, 75 | // URL root prevent conflicts with the site root 76 | // urlRoot: '_karma_' 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-example-offline-sync", 3 | "version": "0.0.1", 4 | "description": "LoopBack browser and server example", 5 | "main": "server/server.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "install": "bower install", 11 | "test": "grunt test", 12 | "build": "grunt build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/strongloop/loopback-example-offline-sync.git" 17 | }, 18 | "keywords": [ 19 | "loopback", 20 | "example", 21 | "browser", 22 | "server" 23 | ], 24 | "author": "IBM Corp.", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/strongloop/loopback-example-offline-sync/issues" 28 | }, 29 | "homepage": "https://github.com/strongloop/loopback-example-offline-sync", 30 | "devDependencies": { 31 | "bower": "^1.3.8", 32 | "browserify": "~4.2.3", 33 | "chai": "^3.4.1", 34 | "connect-livereload": "^0.4.0", 35 | "grunt": "^0.4.5", 36 | "grunt-autoprefixer": "^0.8.2", 37 | "grunt-cli": "^0.1.13", 38 | "grunt-concurrent": "^0.5.0", 39 | "grunt-contrib-clean": "^0.5.0", 40 | "grunt-contrib-concat": "^0.5.0", 41 | "grunt-contrib-connect": "^0.8.0", 42 | "grunt-contrib-copy": "^0.5.0", 43 | "grunt-contrib-cssmin": "^0.10.0", 44 | "grunt-contrib-htmlmin": "^0.3.0", 45 | "grunt-contrib-imagemin": "^0.8.1", 46 | "grunt-contrib-jshint": "^0.10.0", 47 | "grunt-contrib-uglify": "^0.5.1", 48 | "grunt-contrib-watch": "^0.6.1", 49 | "grunt-filerev": "^0.2.1", 50 | "grunt-google-cdn": "^0.4.0", 51 | "grunt-karma": "^0.8.3", 52 | "grunt-mocha-test": "^0.12.6", 53 | "grunt-newer": "^0.7.0", 54 | "grunt-ng-annotate": "^0.8.0", 55 | "grunt-svgmin": "^0.4.0", 56 | "grunt-usemin": "^2.3.0", 57 | "grunt-wiredep": "^1.8.0", 58 | "jshint": "^2.8.0", 59 | "jshint-stylish": "^0.4.0", 60 | "karma": "^0.12.17", 61 | "karma-chrome-launcher": "^0.1.4", 62 | "karma-jasmine": "^0.1.5", 63 | "karma-phantomjs-launcher": "^0.1.4", 64 | "load-grunt-tasks": "^0.6.0", 65 | "mocha": "^2.1.0", 66 | "supertest": "^1.1.0", 67 | "time-grunt": "^0.4.0" 68 | }, 69 | "dependencies": { 70 | "async": "~0.9.0", 71 | "compression": "^1.0.9", 72 | "cors": "^2.7.1", 73 | "loopback": "^3.0.0", 74 | "loopback-boot": "^2.12.1", 75 | "loopback-component-explorer": "^2.1.0", 76 | "loopback-connector-mongodb": "^1.4.1", 77 | "loopback-datasource-juggler": "^2.0.0", 78 | "proquint": "0.0.1", 79 | "serve-static": "^1.10.0", 80 | "strong-error-handler": "^1.1.1" 81 | }, 82 | "engines": { 83 | "node": ">=0.10.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /client/ngapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo App 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 |

Welcome to the Todo App

32 | 33 |
34 | 39 |
40 | 41 |
42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /server/test/todo.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var lt = require('./helpers/loopback-testing-helpers'); 9 | var assert = require('assert'); 10 | var app = require('../server.js'); //path to app.js or server.js 11 | 12 | describe('Todo', function() { 13 | lt.beforeEach.withApp(app); 14 | lt.beforeEach.givenModel('Todo'); 15 | 16 | // New todo 17 | describe('New todo empty data', function() { 18 | 19 | lt.describe.whenCalledRemotely('POST', '/api/Todos', {}, function() { 20 | lt.beforeEach.cleanDatasource(); 21 | 22 | lt.it.shouldBeAllowed(); 23 | it('should have statusCode 200', function() { 24 | assert.equal(this.res.statusCode, 200); 25 | }); 26 | 27 | it('should respond with a new todo', function() { 28 | var guidRegex = /[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}/; 29 | assert(this.res.body.id.match(guidRegex)); 30 | }); 31 | }); 32 | 33 | }); 34 | 35 | // New todo with id 36 | describe('New todo with id set', function() { 37 | 38 | lt.beforeEach.cleanDatasource(); 39 | lt.describe.whenCalledRemotely('POST', '/api/Todos', { 40 | id: '123', 41 | title: 'Sample', 42 | completed: true, 43 | created: 1024 44 | }, function() { 45 | 46 | it('should respond with given todo', function() { 47 | assert.equal(this.res.body.id, 123); 48 | assert.equal(this.res.body.title, 'Sample'); 49 | assert.equal(this.res.body.completed, true); 50 | assert.equal(this.res.body.created, 1024); 51 | }); 52 | 53 | // Find todo in the list of todos 54 | lt.describe.whenCalledRemotely('GET', '/api/Todos', function() { 55 | 56 | it('should contain the todo', function() { 57 | var found = false; 58 | this.res.body.forEach(function(todo) { 59 | if (todo.id === '123') { 60 | found = true; 61 | } 62 | }); 63 | assert(found); 64 | }); 65 | 66 | }); 67 | 68 | // Get todo stats 69 | lt.describe.whenCalledRemotely('POST', '/api/Todos/stats', function() { 70 | lt.it.shouldBeAllowed(); 71 | it('should respond with 1 total, 0 remaining and 1 completed', function() { 72 | assert.equal(typeof this.res.body, 'object'); 73 | assert.equal(this.res.body.stats.remaining, 0); 74 | assert.equal(this.res.body.stats.completed, 1); 75 | assert.equal(this.res.body.stats.total,1); 76 | }); 77 | }); 78 | 79 | // Set task as not completed 80 | lt.describe.whenCalledRemotely('PUT', '/api/Todos/123', { 81 | id: 123, 82 | completed: false 83 | }, function() { 84 | it('should respond with todo:123 as uncompleted', function() { 85 | assert.equal(typeof this.res.body, 'object'); 86 | assert.equal(this.res.body.completed, false); 87 | }); 88 | }); 89 | 90 | // Get the specific todo 91 | lt.describe.whenCalledRemotely('GET', '/api/Todos/123', function() { 92 | it('should respond with todo:123', function() { 93 | assert.equal(typeof this.res.body, 'object'); 94 | assert.equal(this.res.body.id, 123); 95 | }); 96 | }); 97 | 98 | // Delete the created todo 99 | lt.describe.whenCalledRemotely('DELETE', '/api/Todos/123', function() { 100 | it('should respond with status 200 - todo:123 deleted', function() { 101 | assert.equal(this.res.statusCode, 200); 102 | assert.equal(this.res.body.count, 1); 103 | }); 104 | 105 | // Try to find it -- should return not found 106 | lt.describe.whenCalledRemotely('GET', '/api/Todos/123', function() { 107 | it('should respond with status 404 - todo:123 not found', function() { 108 | assert.equal(this.res.statusCode, 404); 109 | }); 110 | }); 111 | }); 112 | }); 113 | 114 | }); 115 | 116 | }); 117 | -------------------------------------------------------------------------------- /client/ngapp/views/todos.html: -------------------------------------------------------------------------------- 1 |
2 |

Local Conflicts

3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 30 | 50 | 51 |
Local DataRemote Data
11 |
12 | Deleted 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 |
idchangetitle
{{conflict.sourceChange.modelId}}{{conflict.sourceChange.type()}} 24 | {{conflict.source.title}} 25 |
28 | 29 |
31 |
32 | Deleted 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 |
idchangetitle
{{conflict.targetChange.modelId}}{{conflict.targetChange.type()}} 44 | {{conflict.target.title}} 45 |
48 | 49 |
52 |
53 |

Merge Manually

54 | 55 | 56 | 57 |
58 |
59 |
60 | 61 |
62 | 68 |
69 | 70 | 71 |
    72 |
  • 73 |
    74 | 75 | 76 | 77 |
    78 |
    79 | 80 |
    81 |
  • 82 |
83 |
84 |
85 | {{remainingCount}} 86 | 87 | 88 | 99 | 100 |
101 |
102 |
103 |

Double-click to edit a todo

104 |
105 |
106 | 107 | 108 | 109 | Debug 110 | connected: {{ connected() }} 111 |
112 | 113 | -------------------------------------------------------------------------------- /server/test/helpers/loopback-testing-helpers.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var _describe = {}; 7 | var _it = {}; 8 | var _beforeEach = {}; 9 | var helpers = { 10 | describe: _describe, 11 | it: _it, 12 | beforeEach: _beforeEach 13 | }; 14 | module.exports = helpers; 15 | 16 | var assert = require('assert'); 17 | var request = require('supertest'); 18 | var expect = require('chai').expect; 19 | 20 | _beforeEach.withApp = function(app) { 21 | if (app.models.User) { 22 | // Speed up the password hashing algorithm 23 | app.models.User.settings.saltWorkFactor = 4; 24 | } 25 | 26 | beforeEach(function() { 27 | this.app = app; 28 | var _request = this.request = request(app); 29 | this.post = _request.post; 30 | this.get = _request.get; 31 | this.put = _request.put; 32 | this.del = _request.del; 33 | }); 34 | }; 35 | 36 | _beforeEach.cleanDatasource = function(dsName) { 37 | beforeEach(function(done) { 38 | if (!dsName) dsName = 'db'; 39 | 40 | if (typeof this.app === 'function' && 41 | typeof this.app.datasources === 'object' && 42 | typeof this.app.datasources[dsName] === 'object') { 43 | this.app.datasources[dsName].automigrate(); 44 | this.app.datasources[dsName].connector.ids = {}; 45 | } 46 | 47 | done(); 48 | }); 49 | }; 50 | 51 | _beforeEach.withArgs = function() { 52 | var args = Array.prototype.slice.call(arguments, 0); 53 | beforeEach(function() { 54 | this.args = args; 55 | }); 56 | }; 57 | 58 | _beforeEach.givenModel = function(modelName, attrs, optionalHandler) { 59 | var modelKey = modelName; 60 | 61 | if (typeof attrs === 'function') { 62 | optionalHandler = attrs; 63 | attrs = undefined; 64 | } 65 | 66 | if (typeof optionalHandler === 'string') { 67 | modelKey = optionalHandler; 68 | } 69 | 70 | attrs = attrs || {}; 71 | 72 | beforeEach(function(done) { 73 | var test = this; 74 | var app = this.app; 75 | var model = app.models[modelName]; 76 | assert(model, 'cannot get model of name ' + modelName + ' from app.models'); 77 | assert(model.dataSource, 'cannot test model ' + modelName + 78 | ' without attached dataSource'); 79 | assert( 80 | typeof model.create === 'function', 81 | modelName + ' does not have a create method' 82 | ); 83 | 84 | model.create(attrs, function(err, result) { 85 | if (err) { 86 | console.error(err.message); 87 | if (err.details) console.error(err.details); 88 | done(err); 89 | } else { 90 | test[modelKey] = result; 91 | done(); 92 | } 93 | }); 94 | }); 95 | 96 | if (typeof optionalHandler === 'function') { 97 | beforeEach(optionalHandler); 98 | } 99 | 100 | afterEach(function(done) { 101 | this[modelKey].destroy(done); 102 | }); 103 | }; 104 | 105 | _describe.whenCalledRemotely = function(verb, url, data, cb) { 106 | if (cb === undefined) { 107 | cb = data; 108 | data = null; 109 | } 110 | 111 | var urlStr = url; 112 | if (typeof url === 'function') { 113 | urlStr = '/'; 114 | } 115 | 116 | describe(verb.toUpperCase() + ' ' + urlStr, function() { 117 | beforeEach(function(cb) { 118 | if (typeof url === 'function') { 119 | this.url = url.call(this); 120 | } 121 | this.remotely = true; 122 | this.verb = verb.toUpperCase(); 123 | this.url = this.url || url; 124 | var methodForVerb = verb.toLowerCase(); 125 | if (methodForVerb === 'delete') methodForVerb = 'del'; 126 | 127 | if (this.request === undefined) { 128 | throw new Error('App is not specified. Please use lt.beforeEach.withApp to specify the app.'); 129 | } 130 | 131 | this.http = this.request[methodForVerb](this.url); 132 | delete this.url; 133 | this.http.set('Accept', 'application/json'); 134 | if (this.loggedInAccessToken) { 135 | this.http.set('authorization', this.loggedInAccessToken.id); 136 | } 137 | if (data) { 138 | var payload = data; 139 | if (typeof data === 'function') 140 | payload = data.call(this); 141 | this.http.send(payload); 142 | } 143 | this.req = this.http.req; 144 | var test = this; 145 | this.http.end(function(err) { 146 | test.req = test.http.req; 147 | test.res = test.http.res; 148 | delete test.url; 149 | cb(); 150 | }); 151 | }); 152 | 153 | cb(); 154 | }); 155 | }; 156 | 157 | _it.shouldBeAllowed = function() { 158 | it('should be allowed', function() { 159 | assert(this.req); 160 | assert(this.res); 161 | // expect success - status 2xx or 3xx 162 | expect(this.res.statusCode).to.be.within(100, 399); 163 | }); 164 | }; 165 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/todo.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /** 9 | * @ngdoc function 10 | * @name loopbackExampleFullStackApp.controller:TodoCtrl 11 | * @description 12 | * # TodoCtrl 13 | * Controller of the loopbackExampleFullStackApp 14 | */ 15 | angular.module('loopbackExampleFullStackApp') 16 | .controller('TodoCtrl', function TodoCtrl($scope, $routeParams, $filter, Todo, 17 | $location, sync, network, 18 | getReadableModelId) { 19 | $scope.todos = []; 20 | 21 | $scope.newTodo = ''; 22 | $scope.editedTodo = null; 23 | 24 | // sync the initial data 25 | sync(onChange); 26 | 27 | // the location service 28 | $scope.loc = $location; 29 | 30 | function onChange() { 31 | Todo.stats(function(err, stats) { 32 | if(err) return error(err); 33 | $scope.stats = stats; 34 | }); 35 | Todo.find({ 36 | where: $scope.statusFilter, 37 | sort: 'order DESC' 38 | }, function(err, todos) { 39 | $scope.todos = todos; 40 | $scope.$apply(); 41 | }); 42 | } 43 | 44 | function error(err) { 45 | //TODO error handling 46 | throw err; 47 | } 48 | 49 | function errorCallback(err) { 50 | if(err) error(err); 51 | } 52 | 53 | Todo.observe('after save', function(ctx, next) { 54 | next(); 55 | onChange(); 56 | }); 57 | Todo.observe('after delete', function(ctx, next) { 58 | next(); 59 | onChange(); 60 | }); 61 | 62 | // Monitor the current route for changes and adjust the filter accordingly. 63 | $scope.$on('$routeChangeSuccess', function () { 64 | var status = $scope.status = $routeParams.status || ''; 65 | $scope.statusFilter = (status === 'active') ? 66 | { completed: false } : (status === 'completed') ? 67 | { completed: true } : {}; 68 | }); 69 | 70 | $scope.addTodo = function () { 71 | Todo.create({title: $scope.newTodo}) 72 | .then(function() { 73 | $scope.newTodo = ''; 74 | $scope.$apply(); 75 | }); 76 | }; 77 | 78 | $scope.editTodo = function (todo) { 79 | $scope.editedTodo = todo; 80 | }; 81 | 82 | $scope.todoCompleted = function(todo) { 83 | todo.completed = true; 84 | todo.save(); 85 | }; 86 | 87 | $scope.doneEditing = function (todo) { 88 | $scope.editedTodo = null; 89 | todo.title = todo.title.trim(); 90 | 91 | if (!todo.title) { 92 | $scope.removeTodo(todo); 93 | } else { 94 | todo.save(); 95 | } 96 | }; 97 | 98 | $scope.removeTodo = function (todo) { 99 | todo.remove(errorCallback); 100 | }; 101 | 102 | $scope.clearCompletedTodos = function () { 103 | Todo.destroyAll({completed: true}, onChange); 104 | }; 105 | 106 | $scope.markAll = function (completed) { 107 | Todo.find(function(err, todos) { 108 | if(err) return errorCallback(err); 109 | todos.forEach(function(todo) { 110 | todo.completed = completed; 111 | todo.save(errorCallback); 112 | }); 113 | }); 114 | }; 115 | 116 | $scope.sync = function() { 117 | sync(); 118 | }; 119 | 120 | $scope.connected = function() { 121 | return network.isConnected; 122 | }; 123 | 124 | $scope.connect = function() { 125 | network.isConnected = true; 126 | sync(); 127 | }; 128 | 129 | $scope.disconnect = function() { 130 | network.isConnected = false; 131 | }; 132 | 133 | Todo.on('conflicts', function(conflicts) { 134 | $scope.localConflicts = conflicts; 135 | 136 | conflicts.forEach(function(conflict) { 137 | conflict.type(function(err, type) { 138 | conflict.type = type; 139 | conflict.models(function(err, source, target) { 140 | conflict.source = source; 141 | conflict.target = target; 142 | conflict.manual = new conflict.SourceModel(source || target); 143 | $scope.$apply(); 144 | }); 145 | conflict.changes(function(err, source, target) { 146 | source.modelId = getReadableModelId(source.modelId); 147 | conflict.sourceChange = source; 148 | target.modelId = getReadableModelId(target.modelId); 149 | conflict.targetChange = target; 150 | $scope.$apply(); 151 | }); 152 | }); 153 | }); 154 | }); 155 | 156 | $scope.resolveUsingSource = function(conflict) { 157 | conflict.resolveUsingSource(refreshConflicts); 158 | }; 159 | 160 | $scope.resolveUsingTarget = function(conflict) { 161 | conflict.resolveUsingTarget(refreshConflicts); 162 | }; 163 | 164 | $scope.resolveManually = function(conflict) { 165 | conflict.resolveManually(conflict.manual, refreshConflicts); 166 | }; 167 | 168 | function refreshConflicts() { 169 | $scope.localConflicts = []; 170 | $scope.$apply(); 171 | sync(); 172 | } 173 | }); 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-example-offline-sync 2 | 3 | **⚠️ This LoopBack 3 example project is no longer maintained. Please refer to [LoopBack 4 Examples](https://loopback.io/doc/en/lb4/Examples.html) instead. ⚠️** 4 | 5 | An example running LoopBack in the browser and server, demonstrating the 6 | following features: 7 | 8 | - offline data access and synchronization 9 | - routes shared between the AngularJS app and the HTTP server 10 | 11 | ## Install and Run 12 | 13 | 0. You must have `node` and `git` installed. It's recommended to have `mongod` 14 | installed too, so that the data is preserved across application restarts. 15 | 16 | 1. Clone the repo. 17 | 18 | 2. `cd loopback-example-offline-sync` 19 | 20 | 3. `npm install` - install the root package dependencies. 21 | 22 | 4. `npm install grunt-cli -g` - skip if you have Grunt CLI already installed. 23 | 24 | 5. `npm install bower -g` - skip if you already have Bower installed. 25 | 26 | 6. `bower install` - install front-end scripts 27 | 28 | 7. `mongod` - make sure mongodb is running if you want to run with 29 | `NODE_ENV=production`. 30 | 31 | 8. `grunt serve` - build and run the entire project in development mode. 32 | 33 | 9. open `http://localhost:3000` - point a browser at the running application. 34 | 35 | ## Project layout 36 | 37 | The project is composed from multiple components. 38 | 39 | - `common/models/` contains definition of models that are shared by both the server 40 | and the client. 41 | 42 | - `client/lbclient/` provides an isomorphic loopback client with offline synchronization. 43 | The client needs some client-only models for data synchronization. These 44 | models are defined in `client/lbclient/models/`. 45 | 46 | - `client/ngapp/` is a single-page AngularJS application scaffolded using `yo 47 | angular`, with a few modifications to make it work better in the full-stack 48 | project. 49 | 50 | - `server/` is the main HTTP server that brings together all other components. 51 | Also сontains the REST API server; it exposes the shared models via 52 | REST API. 53 | 54 | ## Build 55 | 56 | This project uses [Grunt](http://gruntjs.com) for the build, since that's what 57 | `yo angular` creates. 58 | 59 | There are three major changes from the generic Gruntfile required for this 60 | full-stack example: 61 | 62 | - `grunt serve` uses the `server/` component instead of `grunt connect`. 63 | 64 | - `lbclient` component provides a custom build script (`lbclient/build.js`) 65 | which runs `browserify` to produce a single js file to be used in the 66 | browser. The Gruntfile contains a custom task to run this build. 67 | 68 | - The definition of Angular routes is kept in a standalone JSON file 69 | that is used by the `server/` component too. To make this JSON file 70 | available in the browser, there is a custom task that builds 71 | `ngapp/config/bundle.js`. 72 | 73 | ### Targets 74 | 75 | - `grunt serve` starts the application in development mode, watching for file changes 76 | and automatically reloading the application. 77 | - `grunt test` runs automated tests (only the front-end has tests at the 78 | moment). 79 | - `grunt build` creates the bundle for deploying to production. 80 | - `grunt serve:dist` starts the application serving the production bundle of the 81 | front-end SPA. 82 | - `grunt jshint` checks consistency of the coding style. 83 | 84 | ## Adding more features 85 | 86 | ### Define a new shared model 87 | 88 | The instructions assume the name of the new model is 'MyModel'. 89 | 90 | 1. Create a file `models/my-model.json`, put the model definition there. 91 | Use `models/todo.json` as an example, see 92 | [loopback-boot docs](http://apidocs.strongloop.com/loopback-boot) for 93 | more details about the file format. 94 | 95 | 2. (Optional) Add `models/my-model.js` and implement your custom model 96 | methods. See `models/todo.js` for an example. 97 | 98 | 3. Add an entry to `rest/models.json` to configure the new model in the REST 99 | server: 100 | 101 | ```json 102 | { 103 | "MyModel": { 104 | "dataSource": "db" 105 | } 106 | } 107 | ``` 108 | 109 | 4. Define a client-only model to represent the remote server model in the 110 | client - create `lbclient/models/my-model.json` with the following content: 111 | 112 | ```json 113 | { 114 | "name": "RemoteMyModel", 115 | "base": "MyModel" 116 | } 117 | ``` 118 | 119 | 5. Add two entries to `lbclient/models.json` to configure the new models 120 | for the client: 121 | 122 | ```json 123 | { 124 | "MyModel": { 125 | "dataSource": "local" 126 | }, 127 | "RemoteMyModel": { 128 | "dataSource": "remote" 129 | } 130 | } 131 | ``` 132 | 133 | 6. Register the local model with Angular's injector in 134 | `ngapp/scripts/services/lbclient.js`: 135 | 136 | ```js 137 | .value('MyModel', app.models.LocalMyModel) 138 | ``` 139 | 140 | ### Create a new Angular route 141 | 142 | Since the full-stack example project shares the routes between the client and 143 | the server, the new route cannot be added using the yeoman generator. 144 | 145 | 1. (Optional) Create a new angular controller using yeoman, for example, 146 | 147 | ```sh 148 | $ yo angular:controller MyModel 149 | ``` 150 | 151 | 2. (Optional) Create a new angular view using yeoman, for example, 152 | 153 | ```sh 154 | $ yo angular:view models 155 | ``` 156 | 157 | 3. Add a route entry to `ngapp/config/routes.json`, for example, 158 | 159 | ```json 160 | { 161 | "/models": { 162 | "controller": "MymodelCtrl", 163 | "templateUrl": "/views/models.html" 164 | } 165 | } 166 | ``` 167 | 168 | --- 169 | 170 | [More LoopBack examples](https://loopback.io/doc/en/lb3/Tutorials-and-examples.html) 171 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing ### 2 | 3 | Thank you for your interest in `loopback-example-full-stack`, an open source project 4 | administered by StrongLoop. 5 | 6 | Contributing to `loopback-example-full-stack` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-example-full-stack) 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Contributor License Agreement ### 23 | 24 | ``` 25 | Individual Contributor License Agreement 26 | 27 | By signing this Individual Contributor License Agreement 28 | ("Agreement"), and making a Contribution (as defined below) to 29 | StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and 30 | agree to the following terms and conditions for Your present and 31 | future Contributions submitted to StrongLoop. Except for the license 32 | granted in this Agreement to StrongLoop and recipients of software 33 | distributed by StrongLoop, You reserve all right, title, and interest 34 | in and to Your Contributions. 35 | 36 | 1. Definitions 37 | 38 | "You" or "Your" shall mean the copyright owner or the individual 39 | authorized by the copyright owner that is entering into this 40 | Agreement with StrongLoop. 41 | 42 | "Contribution" shall mean any original work of authorship, 43 | including any modifications or additions to an existing work, that 44 | is intentionally submitted by You to StrongLoop for inclusion in, 45 | or documentation of, any of the products owned or managed by 46 | StrongLoop ("Work"). For purposes of this definition, "submitted" 47 | means any form of electronic, verbal, or written communication 48 | sent to StrongLoop or its representatives, including but not 49 | limited to communication or electronic mailing lists, source code 50 | control systems, and issue tracking systems that are managed by, 51 | or on behalf of, StrongLoop for the purpose of discussing and 52 | improving the Work, but excluding communication that is 53 | conspicuously marked or otherwise designated in writing by You as 54 | "Not a Contribution." 55 | 56 | 2. You Grant a Copyright License to StrongLoop 57 | 58 | Subject to the terms and conditions of this Agreement, You hereby 59 | grant to StrongLoop and recipients of software distributed by 60 | StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, 61 | royalty-free, irrevocable copyright license to reproduce, prepare 62 | derivative works of, publicly display, publicly perform, 63 | sublicense, and distribute Your Contributions and such derivative 64 | works under any license and without any restrictions. 65 | 66 | 3. You Grant a Patent License to StrongLoop 67 | 68 | Subject to the terms and conditions of this Agreement, You hereby 69 | grant to StrongLoop and to recipients of software distributed by 70 | StrongLoop a perpetual, worldwide, non-exclusive, no-charge, 71 | royalty-free, irrevocable (except as stated in this Section) 72 | patent license to make, have made, use, offer to sell, sell, 73 | import, and otherwise transfer the Work under any license and 74 | without any restrictions. The patent license You grant to 75 | StrongLoop under this Section applies only to those patent claims 76 | licensable by You that are necessarily infringed by Your 77 | Contributions(s) alone or by combination of Your Contributions(s) 78 | with the Work to which such Contribution(s) was submitted. If any 79 | entity institutes a patent litigation against You or any other 80 | entity (including a cross-claim or counterclaim in a lawsuit) 81 | alleging that Your Contribution, or the Work to which You have 82 | contributed, constitutes direct or contributory patent 83 | infringement, any patent licenses granted to that entity under 84 | this Agreement for that Contribution or Work shall terminate as 85 | of the date such litigation is filed. 86 | 87 | 4. You Have the Right to Grant Licenses to StrongLoop 88 | 89 | You represent that You are legally entitled to grant the licenses 90 | in this Agreement. 91 | 92 | If Your employer(s) has rights to intellectual property that You 93 | create, You represent that You have received permission to make 94 | the Contributions on behalf of that employer, that Your employer 95 | has waived such rights for Your Contributions, or that Your 96 | employer has executed a separate Corporate Contributor License 97 | Agreement with StrongLoop. 98 | 99 | 5. The Contributions Are Your Original Work 100 | 101 | You represent that each of Your Contributions are Your original 102 | works of authorship (see Section 8 (Submissions on Behalf of 103 | Others) for submission on behalf of others). You represent that to 104 | Your knowledge, no other person claims, or has the right to claim, 105 | any right in any intellectual property right related to Your 106 | Contributions. 107 | 108 | You also represent that You are not legally obligated, whether by 109 | entering into an agreement or otherwise, in any way that conflicts 110 | with the terms of this Agreement. 111 | 112 | You represent that Your Contribution submissions include complete 113 | details of any third-party license or other restriction (including, 114 | but not limited to, related patents and trademarks) of which You 115 | are personally aware and which are associated with any part of 116 | Your Contributions. 117 | 118 | 6. You Don't Have an Obligation to Provide Support for Your Contributions 119 | 120 | You are not expected to provide support for Your Contributions, 121 | except to the extent You desire to provide support. You may provide 122 | support for free, for a fee, or not at all. 123 | 124 | 6. No Warranties or Conditions 125 | 126 | StrongLoop acknowledges that unless required by applicable law or 127 | agreed to in writing, You provide Your Contributions on an "AS IS" 128 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 129 | EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES 130 | OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR 131 | FITNESS FOR A PARTICULAR PURPOSE. 132 | 133 | 7. Submission on Behalf of Others 134 | 135 | If You wish to submit work that is not Your original creation, You 136 | may submit it to StrongLoop separately from any Contribution, 137 | identifying the complete details of its source and of any license 138 | or other restriction (including, but not limited to, related 139 | patents, trademarks, and license agreements) of which You are 140 | personally aware, and conspicuously marking the work as 141 | "Submitted on Behalf of a Third-Party: [named here]". 142 | 143 | 8. Agree to Notify of Change of Circumstances 144 | 145 | You agree to notify StrongLoop of any facts or circumstances of 146 | which You become aware that would make these representations 147 | inaccurate in any respect. Email us at callback@strongloop.com. 148 | ``` 149 | 150 | [Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html 151 | [Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml 152 | -------------------------------------------------------------------------------- /client/ngapp/styles/main.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 | color: inherit; 16 | -webkit-appearance: none; 17 | -ms-appearance: none; 18 | -o-appearance: none; 19 | appearance: none; 20 | } 21 | 22 | body { 23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #eaeaea; 26 | color: #4d4d4d; 27 | width: 550px; 28 | margin: 0 auto; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-font-smoothing: antialiased; 31 | -ms-font-smoothing: antialiased; 32 | -o-font-smoothing: antialiased; 33 | font-smoothing: antialiased; 34 | } 35 | 36 | .header { 37 | border-bottom: 1px solid #e5e5e5; 38 | } 39 | 40 | .header .navigation { 41 | padding-left: 0; 42 | list-style-type: none; 43 | } 44 | 45 | .header .navigation li { 46 | display: inline; 47 | } 48 | 49 | button, 50 | input[type="checkbox"] { 51 | outline: none; 52 | } 53 | 54 | #todoapp { 55 | background: #fff; 56 | background: rgba(255, 255, 255, 0.9); 57 | margin: 130px 0 40px 0; 58 | border: 1px solid #ccc; 59 | position: relative; 60 | border-top-left-radius: 2px; 61 | border-top-right-radius: 2px; 62 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), 63 | 0 25px 50px 0 rgba(0, 0, 0, 0.15); 64 | } 65 | 66 | #todoapp:before { 67 | content: ''; 68 | border-left: 1px solid #f5d6d6; 69 | border-right: 1px solid #f5d6d6; 70 | width: 2px; 71 | position: absolute; 72 | top: 0; 73 | left: 40px; 74 | height: 100%; 75 | } 76 | 77 | #todoapp input::-webkit-input-placeholder { 78 | font-style: italic; 79 | } 80 | 81 | #todoapp input::-moz-placeholder { 82 | font-style: italic; 83 | color: #a9a9a9; 84 | } 85 | 86 | #todoapp h1 { 87 | position: absolute; 88 | top: -120px; 89 | width: 100%; 90 | font-size: 70px; 91 | font-weight: bold; 92 | text-align: center; 93 | color: #b3b3b3; 94 | color: rgba(255, 255, 255, 0.3); 95 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2); 96 | -webkit-text-rendering: optimizeLegibility; 97 | -moz-text-rendering: optimizeLegibility; 98 | -ms-text-rendering: optimizeLegibility; 99 | -o-text-rendering: optimizeLegibility; 100 | text-rendering: optimizeLegibility; 101 | } 102 | 103 | #header { 104 | padding-top: 15px; 105 | border-radius: inherit; 106 | } 107 | 108 | #header:before { 109 | content: ''; 110 | position: absolute; 111 | top: 0; 112 | right: 0; 113 | left: 0; 114 | height: 15px; 115 | z-index: 2; 116 | border-bottom: 1px solid #6c615c; 117 | background: #8d7d77; 118 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); 119 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 120 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 121 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); 122 | border-top-left-radius: 1px; 123 | border-top-right-radius: 1px; 124 | } 125 | 126 | #new-todo, 127 | .edit { 128 | position: relative; 129 | margin: 0; 130 | width: 100%; 131 | font-size: 24px; 132 | font-family: inherit; 133 | line-height: 1.4em; 134 | border: 0; 135 | outline: none; 136 | color: inherit; 137 | padding: 6px; 138 | border: 1px solid #999; 139 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 140 | -moz-box-sizing: border-box; 141 | -ms-box-sizing: border-box; 142 | -o-box-sizing: border-box; 143 | box-sizing: border-box; 144 | -webkit-font-smoothing: antialiased; 145 | -moz-font-smoothing: antialiased; 146 | -ms-font-smoothing: antialiased; 147 | -o-font-smoothing: antialiased; 148 | font-smoothing: antialiased; 149 | } 150 | 151 | #new-todo { 152 | padding: 16px 16px 16px 60px; 153 | border: none; 154 | background: rgba(0, 0, 0, 0.02); 155 | z-index: 2; 156 | box-shadow: none; 157 | } 158 | 159 | #main { 160 | position: relative; 161 | z-index: 2; 162 | border-top: 1px dotted #adadad; 163 | } 164 | 165 | label[for='toggle-all'] { 166 | display: none; 167 | } 168 | 169 | #toggle-all { 170 | position: absolute; 171 | top: -42px; 172 | left: -4px; 173 | width: 40px; 174 | text-align: center; 175 | /* Mobile Safari */ 176 | border: none; 177 | } 178 | 179 | #toggle-all:before { 180 | content: '»'; 181 | font-size: 28px; 182 | color: #d9d9d9; 183 | padding: 0 25px 7px; 184 | } 185 | 186 | #toggle-all:checked:before { 187 | color: #737373; 188 | } 189 | 190 | #todo-list { 191 | margin: 0; 192 | padding: 0; 193 | list-style: none; 194 | } 195 | 196 | #todo-list li { 197 | position: relative; 198 | font-size: 24px; 199 | border-bottom: 1px dotted #ccc; 200 | } 201 | 202 | #todo-list li:last-child { 203 | border-bottom: none; 204 | } 205 | 206 | #todo-list li.editing { 207 | border-bottom: none; 208 | padding: 0; 209 | } 210 | 211 | #todo-list li.editing .edit { 212 | display: block; 213 | width: 506px; 214 | padding: 13px 17px 12px 17px; 215 | margin: 0 0 0 43px; 216 | } 217 | 218 | #todo-list li.editing .view { 219 | display: none; 220 | } 221 | 222 | #todo-list li .toggle { 223 | text-align: center; 224 | width: 40px; 225 | /* auto, since non-WebKit browsers doesn't support input styling */ 226 | height: auto; 227 | position: absolute; 228 | top: 0; 229 | bottom: 0; 230 | margin: auto 0; 231 | /* Mobile Safari */ 232 | border: none; 233 | -webkit-appearance: none; 234 | -ms-appearance: none; 235 | -o-appearance: none; 236 | appearance: none; 237 | } 238 | 239 | #todo-list li .toggle:after { 240 | content: '✔'; 241 | /* 40 + a couple of pixels visual adjustment */ 242 | line-height: 43px; 243 | font-size: 20px; 244 | color: #d9d9d9; 245 | text-shadow: 0 -1px 0 #bfbfbf; 246 | } 247 | 248 | #todo-list li .toggle:checked:after { 249 | color: #85ada7; 250 | text-shadow: 0 1px 0 #669991; 251 | bottom: 1px; 252 | position: relative; 253 | } 254 | 255 | #todo-list li label { 256 | white-space: pre; 257 | word-break: break-word; 258 | padding: 15px 60px 15px 15px; 259 | margin-left: 45px; 260 | display: block; 261 | line-height: 1.2; 262 | -webkit-transition: color 0.4s; 263 | transition: color 0.4s; 264 | } 265 | 266 | #todo-list li.completed label { 267 | color: #a9a9a9; 268 | text-decoration: line-through; 269 | } 270 | 271 | #todo-list li .destroy { 272 | display: none; 273 | position: absolute; 274 | top: 0; 275 | right: 10px; 276 | bottom: 0; 277 | width: 40px; 278 | height: 40px; 279 | margin: auto 0; 280 | font-size: 22px; 281 | color: #a88a8a; 282 | -webkit-transition: all 0.2s; 283 | transition: all 0.2s; 284 | } 285 | 286 | #todo-list li .destroy:hover { 287 | text-shadow: 0 0 1px #000, 288 | 0 0 10px rgba(199, 107, 107, 0.8); 289 | -webkit-transform: scale(1.3); 290 | -ms-transform: scale(1.3); 291 | transform: scale(1.3); 292 | } 293 | 294 | #todo-list li .destroy:after { 295 | content: '✖'; 296 | } 297 | 298 | #todo-list li:hover .destroy { 299 | display: block; 300 | } 301 | 302 | #todo-list li .edit { 303 | display: none; 304 | } 305 | 306 | #todo-list li.editing:last-child { 307 | margin-bottom: -1px; 308 | } 309 | 310 | #footer { 311 | color: #777; 312 | padding: 0 15px; 313 | position: absolute; 314 | right: 0; 315 | bottom: -31px; 316 | left: 0; 317 | height: 20px; 318 | z-index: 1; 319 | text-align: center; 320 | } 321 | 322 | #footer:before { 323 | content: ''; 324 | position: absolute; 325 | right: 0; 326 | bottom: 31px; 327 | left: 0; 328 | height: 50px; 329 | z-index: -1; 330 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 331 | 0 6px 0 -3px rgba(255, 255, 255, 0.8), 332 | 0 7px 1px -3px rgba(0, 0, 0, 0.3), 333 | 0 43px 0 -6px rgba(255, 255, 255, 0.8), 334 | 0 44px 2px -6px rgba(0, 0, 0, 0.2); 335 | } 336 | 337 | #todo-count { 338 | float: left; 339 | text-align: left; 340 | } 341 | 342 | #filters { 343 | margin: 0; 344 | padding: 0; 345 | list-style: none; 346 | position: absolute; 347 | right: 0; 348 | left: 0; 349 | } 350 | 351 | #filters li { 352 | display: inline; 353 | } 354 | 355 | #filters li a { 356 | color: #83756f; 357 | margin: 2px; 358 | text-decoration: none; 359 | } 360 | 361 | #filters li a.selected { 362 | font-weight: bold; 363 | } 364 | 365 | #clear-completed { 366 | float: right; 367 | position: relative; 368 | line-height: 20px; 369 | text-decoration: none; 370 | background: rgba(0, 0, 0, 0.1); 371 | font-size: 11px; 372 | padding: 0 10px; 373 | border-radius: 3px; 374 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); 375 | } 376 | 377 | #clear-completed:hover { 378 | background: rgba(0, 0, 0, 0.15); 379 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); 380 | } 381 | 382 | #info { 383 | margin: 65px auto 0; 384 | color: #a6a6a6; 385 | font-size: 12px; 386 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); 387 | text-align: center; 388 | } 389 | 390 | #info a { 391 | color: inherit; 392 | } 393 | 394 | /* 395 | Hack to remove background from Mobile Safari. 396 | Can't use it globally since it destroys checkboxes in Firefox and Opera 397 | */ 398 | 399 | @media screen and (-webkit-min-device-pixel-ratio:0) { 400 | #toggle-all, 401 | #todo-list li .toggle { 402 | background: none; 403 | } 404 | 405 | #todo-list li .toggle { 406 | height: 40px; 407 | } 408 | 409 | #toggle-all { 410 | top: -56px; 411 | left: -15px; 412 | width: 65px; 413 | height: 41px; 414 | -webkit-transform: rotate(90deg); 415 | -ms-transform: rotate(90deg); 416 | transform: rotate(90deg); 417 | -webkit-appearance: none; 418 | appearance: none; 419 | } 420 | } 421 | 422 | .hidden { 423 | display: none; 424 | } 425 | 426 | hr { 427 | margin: 20px 0; 428 | border: 0; 429 | border-top: 1px dashed #C5C5C5; 430 | border-bottom: 1px dashed #F7F7F7; 431 | } 432 | 433 | .learn a { 434 | font-weight: normal; 435 | text-decoration: none; 436 | color: #b83f45; 437 | } 438 | 439 | .learn a:hover { 440 | text-decoration: underline; 441 | color: #787e7e; 442 | } 443 | 444 | .learn h3, 445 | .learn h4, 446 | .learn h5 { 447 | margin: 10px 0; 448 | font-weight: 500; 449 | line-height: 1.2; 450 | color: #000; 451 | } 452 | 453 | .learn h3 { 454 | font-size: 24px; 455 | } 456 | 457 | .learn h4 { 458 | font-size: 18px; 459 | } 460 | 461 | .learn h5 { 462 | margin-bottom: 0; 463 | font-size: 14px; 464 | } 465 | 466 | .learn ul { 467 | padding: 0; 468 | margin: 0 0 30px 25px; 469 | } 470 | 471 | .learn li { 472 | line-height: 20px; 473 | } 474 | 475 | .learn p { 476 | font-size: 15px; 477 | font-weight: 300; 478 | line-height: 1.3; 479 | margin-top: 0; 480 | margin-bottom: 0; 481 | } 482 | 483 | .quote { 484 | border: none; 485 | margin: 20px 0 60px 0; 486 | } 487 | 488 | .quote p { 489 | font-style: italic; 490 | } 491 | 492 | .quote p:before { 493 | content: '“'; 494 | font-size: 50px; 495 | opacity: .15; 496 | position: absolute; 497 | top: -20px; 498 | left: 3px; 499 | } 500 | 501 | .quote p:after { 502 | content: '”'; 503 | font-size: 50px; 504 | opacity: .15; 505 | position: absolute; 506 | bottom: -42px; 507 | right: 3px; 508 | } 509 | 510 | .quote footer { 511 | position: absolute; 512 | bottom: -40px; 513 | right: 0; 514 | } 515 | 516 | .quote footer img { 517 | border-radius: 3px; 518 | } 519 | 520 | .quote footer a { 521 | margin-left: 5px; 522 | vertical-align: middle; 523 | } 524 | 525 | .speech-bubble { 526 | position: relative; 527 | padding: 10px; 528 | background: rgba(0, 0, 0, .04); 529 | border-radius: 5px; 530 | } 531 | 532 | .speech-bubble:after { 533 | content: ''; 534 | position: absolute; 535 | top: 100%; 536 | right: 30px; 537 | border: 13px solid transparent; 538 | border-top-color: rgba(0, 0, 0, .04); 539 | } 540 | 541 | .learn-bar > .learn { 542 | position: absolute; 543 | width: 272px; 544 | top: 8px; 545 | left: -300px; 546 | padding: 10px; 547 | border-radius: 5px; 548 | background-color: rgba(255, 255, 255, .6); 549 | -webkit-transition-property: left; 550 | transition-property: left; 551 | -webkit-transition-duration: 500ms; 552 | transition-duration: 500ms; 553 | } 554 | 555 | @media (min-width: 899px) { 556 | .learn-bar { 557 | width: auto; 558 | margin: 0 0 0 300px; 559 | } 560 | 561 | .learn-bar > .learn { 562 | left: 8px; 563 | } 564 | 565 | .learn-bar #todoapp { 566 | width: 550px; 567 | margin: 130px auto 40px auto; 568 | } 569 | } 570 | 571 | /* changes */ 572 | 573 | table, table td, table th { 574 | outline: solid 1px #ccc; 575 | padding: 5px; 576 | text-align: center; 577 | } 578 | 579 | /* debug footer */ 580 | 581 | .debug button { 582 | background: #6c615c; 583 | padding: 5px; 584 | color: #fff; 585 | cursor: pointer; 586 | } 587 | 588 | .debug button:active { 589 | background: #000; 590 | } 591 | 592 | 593 | .debug { 594 | border: dashed 2px #6c615c; 595 | padding: 10px; 596 | } 597 | 598 | .conflicts button { 599 | background: #6c615c; 600 | padding: 5px; 601 | color: #fff; 602 | cursor: pointer; 603 | } 604 | 605 | .conflicts button:active { 606 | background: #000; 607 | } 608 | 609 | .deltas {background: red !important;} 610 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved. 2 | // Node module: loopback-example-offline-sync 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | // Generated on 2014-06-23 using generator-angular 0.9.1 7 | 'use strict'; 8 | 9 | var buildClientBundle = require('./client/lbclient/build'); 10 | var fs = require('fs'); 11 | var path = require('path'); 12 | 13 | // # Globbing 14 | // for performance reasons we're only matching one level down: 15 | // 'test/spec/{,*/}*.js' 16 | // use this if you want to recursively match all subfolders: 17 | // 'test/spec/**/*.js' 18 | 19 | module.exports = function (grunt) { 20 | 21 | // Load grunt tasks automatically 22 | require('load-grunt-tasks')(grunt); 23 | 24 | // Time how long tasks take. Can help when optimizing build times 25 | require('time-grunt')(grunt); 26 | 27 | // Configurable paths for the application 28 | var appConfig = { 29 | app: require('./bower.json').appPath || 'app', 30 | dist: 'client/dist' 31 | }; 32 | 33 | // Define the configuration for all the tasks 34 | grunt.initConfig({ 35 | 36 | // Project settings 37 | yeoman: appConfig, 38 | 39 | // Watches files for changes and runs tasks based on the changed files 40 | watch: { 41 | bower: { 42 | files: ['bower.json'], 43 | tasks: ['wiredep'] 44 | }, 45 | js: { 46 | files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], 47 | tasks: ['newer:jshint:all'], 48 | options: { 49 | livereload: '<%= connect.options.livereload %>' 50 | } 51 | }, 52 | jsTest: { 53 | files: ['<%= yeoman.app %>/test/spec/{,*/}*.js'], 54 | tasks: ['newer:jshint:test', 'karma'] 55 | }, 56 | styles: { 57 | files: ['<%= yeoman.app %>/styles/{,*/}*.css'], 58 | tasks: ['newer:copy:styles', 'autoprefixer'] 59 | }, 60 | gruntfile: { 61 | files: ['Gruntfile.js'] 62 | }, 63 | livereload: { 64 | options: { 65 | livereload: '<%= connect.options.livereload %>' 66 | }, 67 | files: [ 68 | '<%= yeoman.app %>/{,*/}*.html', 69 | '.tmp/styles/{,*/}*.css', 70 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 71 | ] 72 | }, 73 | lbclient: { 74 | files: [ 75 | 'lbclient/models/*', 76 | 'lbclient/app*', 77 | 'lbclient/datasources*', 78 | 'lbclient/models*', 79 | 'lbclient/build.js' 80 | ], 81 | tasks: ['build-lbclient'], 82 | options: { 83 | livereload: '<%= connect.options.livereload %>' 84 | }, 85 | }, 86 | config: { 87 | files: ['<%= yeoman.app %>/config/*.json'], 88 | tasks: ['build-config'], 89 | options: { 90 | livereload: '<%= connect.options.livereload %>' 91 | }, 92 | }, 93 | }, 94 | 95 | // The actual grunt server settings 96 | connect: { 97 | options: { 98 | port: 3000, 99 | // Change this to '0.0.0.0' to access the server from outside. 100 | hostname: 'localhost', 101 | livereload: 35729 102 | }, 103 | test: { 104 | options: { 105 | port: 9001, 106 | middleware: function (connect) { 107 | return [ 108 | connect.static('.tmp'), 109 | connect.static('test'), 110 | connect().use( 111 | '/bower_components', 112 | connect.static('./bower_components') 113 | ), 114 | connect().use( 115 | '/lbclient', 116 | connect.static('./lbclient') 117 | ), 118 | connect.static(appConfig.app) 119 | ]; 120 | } 121 | } 122 | } 123 | }, 124 | 125 | // Make sure code styles are up to par and there are no obvious mistakes 126 | jshint: { 127 | options: { 128 | jshintrc: '.jshintrc', 129 | reporter: require('jshint-stylish') 130 | }, 131 | all: { 132 | src: [ 133 | 'Gruntfile.js', 134 | '<%= yeoman.app %>/scripts/{,*/}*.js' 135 | ] 136 | }, 137 | test: { 138 | options: { 139 | jshintrc: '<%= yeoman.app %>/test/.jshintrc' 140 | }, 141 | src: ['test/spec/{,*/}*.js'] 142 | } 143 | }, 144 | 145 | // Empties folders to start fresh 146 | clean: { 147 | dist: { 148 | files: [{ 149 | dot: true, 150 | src: [ 151 | '.tmp', 152 | '<%= yeoman.dist %>/{,*/}*', 153 | '!<%= yeoman.dist %>/.git*' 154 | ] 155 | }] 156 | }, 157 | server: '.tmp', 158 | lbclient: 'lbclient/browser.bundle.js', 159 | config: '<%= yeoman.app %>/config/bundle.js' 160 | }, 161 | 162 | // Add vendor prefixed styles 163 | autoprefixer: { 164 | options: { 165 | browsers: ['last 1 version'] 166 | }, 167 | dist: { 168 | files: [{ 169 | expand: true, 170 | cwd: '.tmp/styles/', 171 | src: '{,*/}*.css', 172 | dest: '.tmp/styles/' 173 | }] 174 | } 175 | }, 176 | 177 | // Automatically inject Bower components into the app 178 | wiredep: { 179 | options: { 180 | cwd: '<%= yeoman.app %>', 181 | bowerJson: require('./bower.json'), 182 | directory: './bower_components' //require('./.bowerrc').directory 183 | }, 184 | app: { 185 | src: ['<%= yeoman.app %>/index.html'], 186 | ignorePath: /..\// 187 | } 188 | }, 189 | 190 | // Renames files for browser caching purposes 191 | filerev: { 192 | dist: { 193 | src: [ 194 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 195 | '<%= yeoman.dist %>/styles/{,*/}*.css', 196 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 197 | '<%= yeoman.dist %>/styles/fonts/*' 198 | ] 199 | } 200 | }, 201 | 202 | // Reads HTML for usemin blocks to enable smart builds that automatically 203 | // concat, minify and revision files. Creates configurations in memory so 204 | // additional tasks can operate on them 205 | useminPrepare: { 206 | html: '<%= yeoman.app %>/index.html', 207 | options: { 208 | dest: '<%= yeoman.dist %>', 209 | flow: { 210 | html: { 211 | steps: { 212 | js: ['concat', 'uglifyjs'], 213 | css: ['cssmin'] 214 | }, 215 | post: {} 216 | } 217 | } 218 | } 219 | }, 220 | 221 | // Performs rewrites based on filerev and the useminPrepare configuration 222 | usemin: { 223 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 224 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 225 | options: { 226 | assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images'] 227 | } 228 | }, 229 | 230 | // The following *-min tasks will produce minified files in the dist folder 231 | // By default, your `index.html`'s will take care of 232 | // minification. These next options are pre-configured if you do not wish 233 | // to use the Usemin blocks. 234 | // cssmin: { 235 | // dist: { 236 | // files: { 237 | // '<%= yeoman.dist %>/styles/main.css': [ 238 | // '.tmp/styles/{,*/}*.css' 239 | // ] 240 | // } 241 | // } 242 | // }, 243 | // uglify: { 244 | // dist: { 245 | // files: { 246 | // '<%= yeoman.dist %>/scripts/scripts.js': [ 247 | // '<%= yeoman.dist %>/scripts/scripts.js' 248 | // ] 249 | // } 250 | // } 251 | // }, 252 | // concat: { 253 | // dist: {} 254 | // }, 255 | 256 | imagemin: { 257 | dist: { 258 | files: [{ 259 | expand: true, 260 | cwd: '<%= yeoman.app %>/images', 261 | src: '{,*/}*.{png,jpg,jpeg,gif}', 262 | dest: '<%= yeoman.dist %>/images' 263 | }] 264 | } 265 | }, 266 | 267 | svgmin: { 268 | dist: { 269 | files: [{ 270 | expand: true, 271 | cwd: '<%= yeoman.app %>/images', 272 | src: '{,*/}*.svg', 273 | dest: '<%= yeoman.dist %>/images' 274 | }] 275 | } 276 | }, 277 | 278 | htmlmin: { 279 | dist: { 280 | options: { 281 | collapseWhitespace: true, 282 | conservativeCollapse: true, 283 | collapseBooleanAttributes: true, 284 | removeCommentsFromCDATA: true, 285 | removeOptionalTags: true 286 | }, 287 | files: [{ 288 | expand: true, 289 | cwd: '<%= yeoman.dist %>', 290 | src: ['*.html', 'views/{,*/}*.html'], 291 | dest: '<%= yeoman.dist %>' 292 | }] 293 | } 294 | }, 295 | 296 | // ngAnnotate tries to make the code safe for minification automatically by 297 | // using the Angular long form for dependency injection. It doesn't work on 298 | // things like resolve or inject so those have to be done manually. 299 | ngAnnotate: { 300 | dist: { 301 | files: [{ 302 | expand: true, 303 | cwd: '.tmp/concat/scripts', 304 | src: '*.js', 305 | dest: '.tmp/concat/scripts' 306 | }] 307 | } 308 | }, 309 | 310 | // Replace Google CDN references 311 | cdnify: { 312 | dist: { 313 | html: ['<%= yeoman.dist %>/*.html'] 314 | } 315 | }, 316 | 317 | // Copies remaining files to places other tasks can use 318 | copy: { 319 | dist: { 320 | files: [{ 321 | expand: true, 322 | dot: true, 323 | cwd: '<%= yeoman.app %>', 324 | dest: '<%= yeoman.dist %>', 325 | src: [ 326 | '*.{ico,png,txt}', 327 | '.htaccess', 328 | '*.html', 329 | 'views/{,*/}*.html', 330 | 'images/{,*/}*.{webp}', 331 | 'fonts/*' 332 | ] 333 | }, { 334 | expand: true, 335 | cwd: '.tmp/images', 336 | dest: '<%= yeoman.dist %>/images', 337 | src: ['generated/*'] 338 | }] 339 | }, 340 | styles: { 341 | expand: true, 342 | cwd: '<%= yeoman.app %>/styles', 343 | dest: '.tmp/styles/', 344 | src: '{,*/}*.css' 345 | } 346 | }, 347 | 348 | // Run some tasks in parallel to speed up the build process 349 | concurrent: { 350 | server: [ 351 | 'copy:styles' 352 | ], 353 | test: [ 354 | 'copy:styles' 355 | ], 356 | dist: [ 357 | 'copy:styles', 358 | 'imagemin', 359 | 'svgmin' 360 | ] 361 | }, 362 | 363 | // Test settings 364 | karma: { 365 | unit: { 366 | configFile: '<%= yeoman.app %>/test/karma.conf.js', 367 | browsers: [ 'PhantomJS' ], 368 | singleRun: true 369 | } 370 | }, 371 | 372 | // Server Tests 373 | mochaTest: { 374 | common: { 375 | options: { 376 | reporter: 'spec', 377 | quiet: false, 378 | clearRequireCache: false 379 | }, 380 | src: ['common/models/test/**/*.js'] 381 | }, 382 | server: { 383 | options: { 384 | reporter: 'spec', 385 | quiet: false, 386 | clearRequireCache: false 387 | }, 388 | src: ['server/test/**/*.js'] 389 | } 390 | } 391 | 392 | }); 393 | 394 | grunt.registerTask('build-lbclient', 'Build lbclient browser bundle', function() { 395 | var done = this.async(); 396 | buildClientBundle(process.env.NODE_ENV || 'development', done); 397 | }); 398 | 399 | grunt.registerTask('build-config', 'Build confg.js from JSON files', function() { 400 | var ngapp = path.resolve(__dirname, appConfig.app); 401 | var configDir = path.join(ngapp, 'config'); 402 | var config = {}; 403 | 404 | fs.readdirSync(configDir) 405 | .forEach(function(f) { 406 | if (f === 'bundle.js') return; 407 | 408 | var extname = path.extname(f); 409 | if (extname !== '.json') { 410 | grunt.warn('Ignoring ' + f + ' (' + extname + ')'); 411 | return; 412 | } 413 | 414 | var fullPath = path.resolve(configDir, f); 415 | var key = path.basename(f, extname); 416 | 417 | config[key] = JSON.parse(fs.readFileSync(fullPath), 'utf-8'); 418 | }); 419 | 420 | var outputPath = path.resolve(ngapp, 'config', 'bundle.js'); 421 | var content = 'window.CONFIG = ' + 422 | JSON.stringify(config, null, 2) + 423 | ';\n'; 424 | fs.writeFileSync(outputPath, content, 'utf-8'); 425 | }); 426 | 427 | grunt.registerTask('run', 'Start the app server', function() { 428 | var done = this.async(); 429 | 430 | var connectConfig = grunt.config.get().connect.options; 431 | process.env.LIVE_RELOAD = connectConfig.livereload; 432 | process.env.NODE_ENV = this.args[0]; 433 | 434 | var keepAlive = this.flags.keepalive || connectConfig.keepalive; 435 | 436 | var server = require('./server'); 437 | server.set('port', connectConfig.port); 438 | server.set('host', connectConfig.hostname); 439 | server.start() 440 | .on('listening', function() { 441 | if (!keepAlive) done(); 442 | }) 443 | .on('error', function(err) { 444 | if (err.code === 'EADDRINUSE') { 445 | grunt.fatal('Port ' + connectConfig.port + 446 | ' is already in use by another process.'); 447 | } else { 448 | grunt.fatal(err); 449 | } 450 | }); 451 | }); 452 | 453 | grunt.registerTask('serve', 'Compile then start the app server', function (target) { 454 | if (target === 'dist') { 455 | return grunt.task.run(['build', 'run:dist:keepalive']); 456 | } 457 | 458 | grunt.task.run([ 459 | 'clean:server', 460 | 'build-lbclient', 461 | 'build-config', 462 | 'wiredep', 463 | 'concurrent:server', 464 | 'autoprefixer', 465 | 'run:development', 466 | 'watch' 467 | ]); 468 | }); 469 | 470 | grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) { 471 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 472 | grunt.task.run(['serve:' + target]); 473 | }); 474 | 475 | grunt.registerTask('test:client', [ 476 | 'clean:server', 477 | 'build-lbclient', 478 | 'build-config', 479 | 'concurrent:test', 480 | 'autoprefixer', 481 | 'connect:test', 482 | 'karma' 483 | ]); 484 | 485 | grunt.registerTask('test:common', [ 486 | 'mochaTest:common' 487 | ]); 488 | 489 | grunt.registerTask('test:server', [ 490 | 'mochaTest:server' 491 | ]); 492 | 493 | grunt.registerTask('test', [ 494 | 'test:server', 495 | 'test:common', 496 | 'test:client' 497 | ]); 498 | 499 | grunt.registerTask('build', [ 500 | 'clean:dist', 501 | 'build-lbclient', 502 | 'build-config', 503 | 'wiredep', 504 | 'useminPrepare', 505 | 'concurrent:dist', 506 | 'autoprefixer', 507 | 'concat', 508 | 'ngAnnotate', 509 | 'copy:dist', 510 | 'cdnify', 511 | 'cssmin', 512 | 'uglify', 513 | 'filerev', 514 | 'usemin', 515 | 'htmlmin' 516 | ]); 517 | 518 | grunt.registerTask('default', [ 519 | 'newer:jshint', 520 | 'test', 521 | 'build' 522 | ]); 523 | }; 524 | --------------------------------------------------------------------------------