├── .babelrc ├── .eslintrc ├── .gitignore ├── README.md ├── app ├── config.js ├── images │ ├── favicon.png │ └── logo.png ├── index.html ├── index.js ├── modules │ ├── comment │ │ ├── config │ │ │ └── routes.js │ │ ├── controller │ │ │ ├── comment.js │ │ │ └── comments.js │ │ ├── index.js │ │ ├── sass │ │ │ ├── comment.scss │ │ │ └── comments.scss │ │ ├── service │ │ │ └── service.js │ │ └── view │ │ │ ├── comment.html │ │ │ └── comments.html │ ├── home │ │ ├── config │ │ │ └── routes.js │ │ ├── controller │ │ │ ├── __tests__ │ │ │ │ └── home-test.js │ │ │ └── home.js │ │ ├── directive │ │ │ ├── footer.js │ │ │ └── menu.js │ │ ├── index.js │ │ ├── sass │ │ │ ├── footer.scss │ │ │ ├── home.scss │ │ │ └── menu.scss │ │ └── view │ │ │ ├── footer.html │ │ │ ├── home.html │ │ │ └── menu.html │ ├── post │ │ ├── config │ │ │ └── routes.js │ │ ├── controller │ │ │ ├── __tests__ │ │ │ │ └── post-test.js │ │ │ ├── post.js │ │ │ └── posts.js │ │ ├── directive │ │ │ ├── social.js │ │ │ └── vote.js │ │ ├── filter │ │ │ └── reply.js │ │ ├── index.js │ │ ├── sass │ │ │ ├── post.scss │ │ │ ├── posts.scss │ │ │ ├── social.scss │ │ │ └── vote.scss │ │ ├── service │ │ │ └── service.js │ │ └── view │ │ │ ├── post.html │ │ │ └── posts.html │ └── user │ │ ├── config │ │ └── routes.js │ │ ├── controller │ │ ├── user.js │ │ └── users.js │ │ ├── index.js │ │ ├── sass │ │ ├── user.scss │ │ └── users.scss │ │ ├── service │ │ └── service.js │ │ └── view │ │ ├── user.html │ │ └── users.html └── sass │ ├── color.scss │ └── style.scss ├── karma.conf.js ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 6 3 | sourceType: module 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | build/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Webpack Angular ES6](http://geniuscarrier.me/images/webpack-angular-es6.png) 2 | # Webpack Angular ES6 3 | A boilerplate for writing modular Angular 1.X in ES6 using Webpack. 4 | 5 | ## Quick start 6 | 7 | ### Install dependencies 8 | ``` 9 | npm install 10 | ``` 11 | #### Dev 12 | ``` 13 | npm run dev 14 | ``` 15 | In your browser, navigate to: http://localhost:8080/ 16 | #### Test 17 | ``` 18 | npm run test 19 | ``` 20 | 21 | #### Production 22 | ``` 23 | npm run build 24 | ``` 25 | Copy everything in `build/` folder to the server. 26 | 27 | #### Demo 28 | 29 | [Demo](http://geniuscarrier.com/demo/webpack-angular-es6) 30 | 31 | ## Angular Modules 32 | 33 | Instead of using the great [AngularJS Seed Project](https://github.com/angular/angular-seed), which is using Horizontal Modules, provided by the AngularJS team, I am using Vertical Modules, which is highly inspired by [MEAN.JS](https://github.com/meanjs/mean). This helps us divide the project logic into modules that represent individual logical units and scale well for bigger projects that are more maintainable in the long term. The following Module structure and folder structure use demo example to demonstrate how it works. 34 | 35 | ### Modules Structure 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 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 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
Application
HomeModulePostModuleCommentModuleUserModule
HomeControllerPostControllerCommentControllerUserController
HomeDirectivePostDirectiveCommentDirectiveUserDirective
HomeServicePostServiceCommentServiceUserService
HomeFilterPostFilterCommentFilterUserFilter
HomeRoutesPostRoutesCommentRoutesUserRoutes
HomeViewPostViewCommentViewUserView
89 | 90 | ### Folder Structure 91 | 92 | This structure allows clear separation of functionality and concerns. 93 | 94 | ``` 95 | |-sass 96 | |-images 97 | |-modules 98 | |---home 99 | |-----config 100 | |-----controller 101 | |-------tests 102 | |-----directive 103 | |-------tests 104 | |-----service 105 | |-------tests 106 | |-----filter 107 | |-------tests 108 | |-----view 109 | |-----sass 110 | |---post 111 | |-----config 112 | |-----controller 113 | |-------tests 114 | |-----directive 115 | |-------tests 116 | |-----service 117 | |-------tests 118 | |-----filter 119 | |-------tests 120 | |-----view 121 | |-----sass 122 | |---comment 123 | |-----config 124 | |-----controller 125 | |-------tests 126 | |-----directive 127 | |-------tests 128 | |-----service 129 | |-------tests 130 | |-----filter 131 | |-------tests 132 | |-----view 133 | |-----sass 134 | |---users 135 | |-----config 136 | |-----controller 137 | |-------tests 138 | |-----directive 139 | |-------tests 140 | |-----service 141 | |-------tests 142 | |-----filter 143 | |-------tests 144 | |-----view 145 | |-----sass 146 | ``` 147 | 148 | ## Basic Usage 149 | 150 | This boilerplate comes with a blog example. I am taking post module as an example to illustrate how the Angular work with Webpack in ES6. 151 | 152 | 153 | ### API 154 | 155 | Using [JSONPlaceholder](http://jsonplaceholder.typicode.com/) for fake Online REST API for Testing and Prototyping 156 | 157 | ### Controller 158 | 159 | ``` 160 | export default class PostController { 161 | constructor($stateParams, $location, post, user, comments) { 162 | this.$stateParams = $stateParams; 163 | this.$location = $location; 164 | this.post = post; 165 | this.user = user; 166 | this.comments = comments; 167 | this.author = 'Yang Zhao'; 168 | } 169 | } 170 | 171 | PostController.$inject = ['$stateParams', '$location', 'post', 'user', 'comments']; 172 | 173 | ``` 174 | 175 | ### Directive 176 | 177 | ``` 178 | /*@ngInject*/ 179 | export default class Menu { 180 | constructor() { 181 | this.template = require('../view/menu.html'); 182 | this.restrict = 'E'; 183 | this.scope = {}; 184 | this.controller = HomeController; 185 | } 186 | 187 | // optional compile function 188 | compile(tElement) { 189 | return this.link.bind(this); 190 | } 191 | 192 | // optional link function 193 | link(scope, element, attributes, controller) { 194 | scope.isActive = function(viewLocation) { 195 | return viewLocation === controller.$location.path(); 196 | }; 197 | } 198 | } 199 | 200 | ``` 201 | 202 | ### Service 203 | 204 | ``` 205 | /*@ngInject*/ 206 | export default class PostService { 207 | constructor($http) { 208 | this.$http = $http; 209 | } 210 | 211 | getPosts() { 212 | return this.$http.get('http://jsonplaceholder.typicode.com/posts'); 213 | } 214 | 215 | getPost(postId) { 216 | return this.$http.get('http://jsonplaceholder.typicode.com/posts/' + postId); 217 | } 218 | 219 | getUser(usreId) { 220 | return this.$http.get('http://jsonplaceholder.typicode.com/users/' + usreId); 221 | } 222 | 223 | getComments(postId) { 224 | return this.$http.get('http://jsonplaceholder.typicode.com/posts/' + postId + '/comments'); 225 | } 226 | } 227 | 228 | ``` 229 | 230 | ### Filter 231 | 232 | ``` 233 | export default () => { 234 | return (input) => { 235 | return input + ' '; 236 | } 237 | } 238 | 239 | ``` 240 | 241 | ### Routes 242 | 243 | ``` 244 | /*@ngInject*/ 245 | export default ($stateProvider) => { 246 | $stateProvider 247 | .state('listPosts', { 248 | url: '/posts', 249 | template: require('../view/posts.html'), 250 | controller: 'PostsController', 251 | controllerAs: 'posts', 252 | resolve: { 253 | posts: (PostService) => { 254 | return PostService.getPosts().then((object) => { 255 | return object.data; 256 | }); 257 | } 258 | } 259 | }) 260 | .state('post', { 261 | url: '/posts/:postId', 262 | template: require('../view/post.html'), 263 | controller: 'PostController', 264 | controllerAs: 'post', 265 | resolve: { 266 | post: (PostService, $stateParams) => { 267 | return PostService.getPost($stateParams.postId).then((object) => { 268 | return object.data; 269 | }); 270 | }, 271 | user: (PostService, post) => { 272 | return PostService.getUser(post.userId).then((object) => { 273 | return object.data; 274 | }); 275 | }, 276 | comments: (PostService, post) => { 277 | return PostService.getComments(post.id).then((object) => { 278 | return object.data; 279 | }); 280 | } 281 | } 282 | }); 283 | } 284 | ``` 285 | 286 | ### Test (Controller) 287 | 288 | ``` 289 | describe('PostController', () => { 290 | let controller; 291 | let service; 292 | beforeEach(() => { 293 | angular.mock.module(post); 294 | angular.mock.inject(($injector, $controller, $stateParams, $location) => { 295 | service = $injector.get('PostService'); 296 | controller = $controller('PostController', { 297 | $stateParams: $stateParams, 298 | $location: $location, 299 | post: service, 300 | user: service, 301 | comments: service 302 | }); 303 | }) 304 | }); 305 | 306 | 307 | it('should have correct author name', () => { 308 | assert.equal(controller.author, 'Yang Zhao', 'PostController has correct author name'); 309 | }); 310 | }); 311 | 312 | 313 | ``` 314 | ### Index (Post Module) 315 | 316 | ``` 317 | import routes from './config/routes'; 318 | import PostService from './service/service'; 319 | import PostController from './controller/post'; 320 | import PostDirective from './directive/post'; 321 | import PostFilter from './filter/post'; 322 | 323 | export default angular.module('post') 324 | .config(routes) 325 | .service('PostService', PostService) 326 | .controller('PostController', PostController) 327 | .directive('PostDirective', () => new PostDirective()) 328 | .filter('PostFilter', PostFilter) 329 | .name; 330 | ``` 331 | 332 | ### Application 333 | 334 | ``` 335 | import home from './modules/home'; 336 | import post from './modules/post'; 337 | import comment from './modules/comment'; 338 | import user from './modules/user'; 339 | 340 | /*@ngInject*/ 341 | angular.module('app', [home, post, comment, user]); 342 | ``` 343 | -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | /*@ngInject*/ 2 | export default ($urlRouterProvider, $locationProvider) => { 3 | $locationProvider.html5Mode(false); 4 | $urlRouterProvider.otherwise('/'); 5 | } 6 | -------------------------------------------------------------------------------- /app/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geniuscarrier/webpack-angular-es6/d5c4ebc9b6ab6604c47ec878801183472741f8d4/app/images/favicon.png -------------------------------------------------------------------------------- /app/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geniuscarrier/webpack-angular-es6/d5c4ebc9b6ab6604c47ec878801183472741f8d4/app/images/logo.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Webpack + Angular + ES6 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import '../node_modules/bootstrap/dist/js/bootstrap.min.js'; 2 | import angular from 'angular'; 3 | import uirouter from 'angular-ui-router'; 4 | import sanitize from 'angular-sanitize'; 5 | if (TEST) { 6 | require('angular-mocks'); 7 | } 8 | 9 | import './sass/style.scss'; 10 | import routing from './config'; 11 | import home from './modules/home'; 12 | import post from './modules/post'; 13 | import comment from './modules/comment'; 14 | import user from './modules/user'; 15 | 16 | /*@ngInject*/ 17 | angular.module('app', [uirouter, sanitize, home, post, comment, user]) 18 | .config(routing) 19 | .run(['$rootScope', ($root) => { 20 | $root.$on('$stateChangeStart', (e, newUrl, oldUrl) => { 21 | if (newUrl !== oldUrl) { 22 | $root.loadingView = true; 23 | } 24 | }); 25 | $root.$on('$stateChangeSuccess', () => { 26 | $root.loadingView = false; 27 | }); 28 | $root.$on('$stateChangeError', () => { 29 | $root.loadingView = false; 30 | }); 31 | }]); 32 | -------------------------------------------------------------------------------- /app/modules/comment/config/routes.js: -------------------------------------------------------------------------------- 1 | /*@ngInject*/ 2 | export default ($stateProvider) => { 3 | $stateProvider 4 | .state('listComments', { 5 | url: '/comments', 6 | template: require('../view/comments.html'), 7 | controller: 'CommentsController', 8 | controllerAs: 'comments', 9 | resolve: { 10 | comments: (CommentService) => { 11 | return CommentService.getComments().then((object) => { 12 | return object.data; 13 | }); 14 | } 15 | } 16 | }) 17 | .state('comment', { 18 | url: '/comments/:commentId', 19 | template: require('../view/comment.html'), 20 | controller: 'CommentController', 21 | controllerAs: 'comment', 22 | resolve: { 23 | comment: (CommentService, $stateParams) => { 24 | return CommentService.getComment($stateParams.commentId).then((object) => { 25 | return object.data; 26 | }); 27 | } 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /app/modules/comment/controller/comment.js: -------------------------------------------------------------------------------- 1 | import '../sass/comment.scss'; 2 | export default class CommentController { 3 | constructor($stateParams, $location, comment) { 4 | this.$stateParams = $stateParams; 5 | this.$location = $location; 6 | this.comment = comment; 7 | } 8 | } 9 | 10 | CommentController.$inject = ['$stateParams', '$location', 'comment']; 11 | -------------------------------------------------------------------------------- /app/modules/comment/controller/comments.js: -------------------------------------------------------------------------------- 1 | import '../sass/comments.scss'; 2 | export default class CommentsController { 3 | constructor($stateParams, $location, comments) { 4 | this.$stateParams = $stateParams; 5 | this.$location = $location; 6 | this.comments = comments; 7 | } 8 | } 9 | 10 | CommentsController.$inject = ['$stateParams', '$location', 'comments']; 11 | -------------------------------------------------------------------------------- /app/modules/comment/index.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import uirouter from 'angular-ui-router'; 3 | 4 | import routes from './config/routes'; 5 | import CommentService from './service/service'; 6 | import CommentsController from './controller/comments'; 7 | import CommentController from './controller/comment'; 8 | 9 | export default angular.module('comment', [uirouter]) 10 | .config(routes) 11 | .service('CommentService', CommentService) 12 | .controller('CommentsController', CommentsController) 13 | .controller('CommentController', CommentController) 14 | .name; 15 | -------------------------------------------------------------------------------- /app/modules/comment/sass/comment.scss: -------------------------------------------------------------------------------- 1 | #comment { 2 | section { 3 | margin: 50px 0; 4 | } 5 | ul { 6 | list-style: none; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | h2 { 11 | text-transform: capitalize; 12 | } 13 | .divider { 14 | width: 100%; 15 | text-align: center; 16 | border-bottom: 1px solid #eeeeee; 17 | line-height: 0.1em; 18 | margin: 10px 0 20px; 19 | span { 20 | background-color: #ffffff; 21 | padding: 0 20px; 22 | } 23 | } 24 | .post-excerpt { 25 | margin: 0; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/modules/comment/sass/comments.scss: -------------------------------------------------------------------------------- 1 | #comments { 2 | ul { 3 | list-style: none; 4 | margin: 0; 5 | padding: 0; 6 | li { 7 | article { 8 | margin: 40px 0; 9 | } 10 | } 11 | } 12 | h2 { 13 | text-transform: capitalize; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/modules/comment/service/service.js: -------------------------------------------------------------------------------- 1 | /*@ngInject*/ 2 | class CommentService { 3 | constructor($http) { 4 | this.$http = $http; 5 | } 6 | 7 | getComments() { 8 | return this.$http.get('http://jsonplaceholder.typicode.com/comments'); 9 | } 10 | 11 | getComment(commentId) { 12 | return this.$http.get('http://jsonplaceholder.typicode.com/comments/' + commentId); 13 | } 14 | } 15 | 16 | export default CommentService; 17 | -------------------------------------------------------------------------------- /app/modules/comment/view/comment.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{comment.comment.name}}

4 |

{{comment.comment.body}}

5 |

Commented by: {{comment.comment.email}}

6 |
7 |

See other comments on this post.

8 |
9 |
10 | -------------------------------------------------------------------------------- /app/modules/comment/view/comments.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 18 |
19 | -------------------------------------------------------------------------------- /app/modules/home/config/routes.js: -------------------------------------------------------------------------------- 1 | /*@ngInject*/ 2 | export default ($stateProvider) => { 3 | $stateProvider 4 | .state('home', { 5 | url: '/', 6 | template: require('../view/home.html'), 7 | controller: 'HomeController', 8 | controllerAs: 'home' 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /app/modules/home/controller/__tests__/home-test.js: -------------------------------------------------------------------------------- 1 | import home from '../../index'; 2 | import 'angular-mocks'; 3 | describe('HomeController', () => { 4 | let controller; 5 | beforeEach(() => { 6 | angular.mock.module(home); 7 | angular.mock.inject(($controller, $location) => { 8 | controller = $controller('HomeController', { $location: $location }); 9 | }) 10 | }); 11 | 12 | 13 | it('should initiate with correct home title and body', () => { 14 | assert.equal(controller.title, 'WebPack Angular ES6', 'HomeController has correct title'); 15 | assert.equal(controller.description, 'This blog example is a quick exercise to illustrate how the Angular work with Webpack in ES6.', 'HomeController has correct description'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app/modules/home/controller/home.js: -------------------------------------------------------------------------------- 1 | import '../sass/home.scss'; 2 | export default class HomeController { 3 | constructor($location) { 4 | this.$location = $location; 5 | this.title = 'WebPack Angular ES6'; 6 | this.description = 'This blog example is a quick exercise to illustrate how the Angular work with Webpack in ES6.'; 7 | } 8 | } 9 | 10 | HomeController.$inject = ['$location']; 11 | -------------------------------------------------------------------------------- /app/modules/home/directive/footer.js: -------------------------------------------------------------------------------- 1 | import '../sass/footer.scss'; 2 | 3 | /*@ngInject*/ 4 | export default class Footer { 5 | constructor() { 6 | this.template = require('../view/footer.html'); 7 | this.restrict = 'E'; 8 | this.scope = {}; 9 | } 10 | 11 | // optional compile function 12 | compile(tElement) { 13 | return this.link.bind(this); 14 | } 15 | 16 | // optional link function 17 | link(scope, element, attributes) { 18 | scope.year = new Date().getFullYear(); 19 | element.bind('click', (event) => { 20 | if (event.target && event.target.matches('a.backToTop')) { 21 | window.scrollTo(0, 0); 22 | } 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/modules/home/directive/menu.js: -------------------------------------------------------------------------------- 1 | import HomeController from '../controller/home'; 2 | import '../sass/menu.scss'; 3 | 4 | /*@ngInject*/ 5 | export default class Menu { 6 | constructor() { 7 | this.template = require('../view/menu.html'); 8 | this.restrict = 'E'; 9 | this.scope = {}; 10 | this.controller = HomeController; 11 | } 12 | 13 | // optional compile function 14 | compile(tElement) { 15 | return this.link.bind(this); 16 | } 17 | 18 | // optional link function 19 | link(scope, element, attributes, controller) { 20 | scope.isActive = function(viewLocation) { 21 | return viewLocation === controller.$location.path(); 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/modules/home/index.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import uirouter from 'angular-ui-router'; 3 | 4 | import routes from './config/routes'; 5 | import Menu from './directive/menu'; 6 | import Footer from './directive/footer'; 7 | import HomeController from './controller/home'; 8 | 9 | export default angular.module('home', [uirouter]) 10 | .config(routes) 11 | .controller('HomeController', HomeController) 12 | .directive('ngMenu', () => new Menu()) 13 | .directive('ngFooter', () => new Footer()) 14 | .name; 15 | -------------------------------------------------------------------------------- /app/modules/home/sass/footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | margin: 1em 0 2em; 3 | .backToTop { 4 | cursor: pointer; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/modules/home/sass/home.scss: -------------------------------------------------------------------------------- 1 | .jumbotron { 2 | margin-top: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /app/modules/home/sass/menu.scss: -------------------------------------------------------------------------------- 1 | .navbar-brand { 2 | padding: 0; 3 | } 4 | -------------------------------------------------------------------------------- /app/modules/home/view/footer.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /app/modules/home/view/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{home.title}}

4 |

{{home.description}}

5 |

6 | View posts 7 |

8 |
9 |
10 | -------------------------------------------------------------------------------- /app/modules/home/view/menu.html: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /app/modules/post/config/routes.js: -------------------------------------------------------------------------------- 1 | /*@ngInject*/ 2 | export default ($stateProvider) => { 3 | $stateProvider 4 | .state('listPosts', { 5 | url: '/posts', 6 | template: require('../view/posts.html'), 7 | controller: 'PostsController', 8 | controllerAs: 'posts', 9 | resolve: { 10 | posts: (PostService) => { 11 | return PostService.getPosts().then((object) => { 12 | return object.data; 13 | }); 14 | } 15 | } 16 | }) 17 | .state('post', { 18 | url: '/posts/:postId', 19 | template: require('../view/post.html'), 20 | controller: 'PostController', 21 | controllerAs: 'post', 22 | resolve: { 23 | post: (PostService, $stateParams) => { 24 | return PostService.getPost($stateParams.postId).then((object) => { 25 | return object.data; 26 | }); 27 | }, 28 | user: (PostService, post) => { 29 | return PostService.getUser(post.userId).then((object) => { 30 | return object.data; 31 | }); 32 | }, 33 | comments: (PostService, post) => { 34 | return PostService.getComments(post.id).then((object) => { 35 | return object.data; 36 | }); 37 | } 38 | } 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /app/modules/post/controller/__tests__/post-test.js: -------------------------------------------------------------------------------- 1 | import post from '../../index'; 2 | describe('PostController', () => { 3 | let controller; 4 | let service; 5 | beforeEach(() => { 6 | angular.mock.module(post); 7 | angular.mock.inject(($injector, $controller, $stateParams, $location) => { 8 | service = $injector.get('PostService'); 9 | controller = $controller('PostController', { 10 | $stateParams: $stateParams, 11 | $location: $location, 12 | post: service, 13 | user: service, 14 | comments: service 15 | }); 16 | }) 17 | }); 18 | 19 | 20 | it('should have correct author name', () => { 21 | assert.equal(controller.author, 'Yang Zhao', 'PostController has correct author name'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /app/modules/post/controller/post.js: -------------------------------------------------------------------------------- 1 | import '../sass/post.scss'; 2 | export default class PostController { 3 | constructor($stateParams, $location, post, user, comments) { 4 | this.$stateParams = $stateParams; 5 | this.$location = $location; 6 | this.post = post; 7 | this.user = user; 8 | this.comments = comments; 9 | this.author = 'Yang Zhao'; 10 | } 11 | } 12 | 13 | PostController.$inject = ['$stateParams', '$location', 'post', 'user', 'comments']; 14 | -------------------------------------------------------------------------------- /app/modules/post/controller/posts.js: -------------------------------------------------------------------------------- 1 | import '../sass/posts.scss'; 2 | export default class PostControllers { 3 | constructor($stateParams, $location, posts) { 4 | this.$stateParams = $stateParams; 5 | this.$location = $location; 6 | this.posts = posts; 7 | } 8 | } 9 | 10 | PostControllers.$inject = ['$stateParams', '$location', 'posts']; 11 | -------------------------------------------------------------------------------- /app/modules/post/directive/social.js: -------------------------------------------------------------------------------- 1 | import toastr from 'toastr'; 2 | import '../sass/social.scss'; 3 | 4 | /*@ngInject*/ 5 | export default class Social { 6 | constructor() { 7 | this.template = '
' + 8 | '' + 9 | '' + 10 | '' + 11 | '
'; 12 | this.restrict = 'E'; 13 | this.scope = {}; 14 | this.transclude = true; 15 | } 16 | 17 | // optional compile function 18 | compile(tElement) { 19 | return this.link.bind(this); 20 | } 21 | 22 | // optional link function 23 | link(scope, element, attributes) { 24 | element.bind('click', (event) => { 25 | if (event.target && event.target.matches('i.fa-facebook-square')) { 26 | toastr.info('Thanks for sharing on Facebook'); 27 | } else if (event.target && event.target.matches('i.fa-twitter-square')) { 28 | toastr.info('Thanks for sharing on Twitter'); 29 | } else if (event.target && event.target.matches('i.fa-google-plus-square')) { 30 | toastr.info('Thanks for sharing on Google Plus'); 31 | } 32 | }); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/modules/post/directive/vote.js: -------------------------------------------------------------------------------- 1 | import toastr from 'toastr'; 2 | import '../sass/vote.scss'; 3 | 4 | /*@ngInject*/ 5 | export default class Vote { 6 | constructor() { 7 | this.template = '
|
'; 8 | this.restrict = 'E'; 9 | this.scope = {}; 10 | this.transclude = true; 11 | } 12 | 13 | // optional compile function 14 | compile(tElement) { 15 | return this.link.bind(this); 16 | } 17 | 18 | // optional link function 19 | link(scope, element, attributes) { 20 | element.bind('click', (event) => { 21 | if (event.target && event.target.matches('i.fa-thumbs-up')) { 22 | this.upVote(); 23 | } else if (event.target && event.target.matches('i.fa-thumbs-down')) { 24 | this.downVote(); 25 | } 26 | }); 27 | } 28 | 29 | upVote() { 30 | toastr.success('You up voted!'); 31 | } 32 | 33 | downVote() { 34 | toastr.error('You down voted!'); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/modules/post/filter/reply.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return (input) => { 3 | return input + ' '; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/modules/post/index.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import uirouter from 'angular-ui-router'; 3 | 4 | import routes from './config/routes'; 5 | import Vote from './directive/vote'; 6 | import Social from './directive/social'; 7 | import PostService from './service/service'; 8 | import PostsController from './controller/posts'; 9 | import PostController from './controller/post'; 10 | import replay from './filter/reply'; 11 | 12 | export default angular.module('post', [uirouter]) 13 | .config(routes) 14 | .service('PostService', PostService) 15 | .controller('PostsController', PostsController) 16 | .controller('PostController', PostController) 17 | .filter('reply', replay) 18 | .directive('vote', () => new Vote()) 19 | .directive('social', () => new Social()) 20 | .name; 21 | -------------------------------------------------------------------------------- /app/modules/post/sass/post.scss: -------------------------------------------------------------------------------- 1 | #post { 2 | section { 3 | margin: 60px 0; 4 | } 5 | h2 { 6 | text-transform: capitalize; 7 | } 8 | ul { 9 | list-style: none; 10 | margin: 0; 11 | padding: 0; 12 | li { 13 | article { 14 | margin: 30px 0; 15 | } 16 | } 17 | } 18 | .divider { 19 | width: 100%; 20 | text-align: center; 21 | border-bottom: 1px solid #eeeeee; 22 | line-height: 0.1em; 23 | margin: 10px 0 60px; 24 | span { 25 | background-color: #ffffff; 26 | padding: 0 20px; 27 | } 28 | } 29 | .comment-excerpt { 30 | margin: 0; 31 | } 32 | } 33 | 34 | @media (min-width: 768px) { 35 | .post-author { 36 | .col-sm-6 { 37 | &:first-child { 38 | text-align: left; 39 | } 40 | &:last-child { 41 | text-align: right; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/modules/post/sass/posts.scss: -------------------------------------------------------------------------------- 1 | #posts { 2 | ul { 3 | list-style: none; 4 | margin: 0; 5 | padding: 0; 6 | li { 7 | article { 8 | margin: 40px 0; 9 | } 10 | } 11 | } 12 | h2 { 13 | text-transform: capitalize; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/modules/post/sass/social.scss: -------------------------------------------------------------------------------- 1 | .social { 2 | i { 3 | margin: 0 0.3em; 4 | cursor: pointer; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/modules/post/sass/vote.scss: -------------------------------------------------------------------------------- 1 | .vote { 2 | i { 3 | cursor: pointer; 4 | } 5 | span { 6 | margin: 0 1em; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/modules/post/service/service.js: -------------------------------------------------------------------------------- 1 | /*@ngInject*/ 2 | export default class PostService { 3 | constructor($http) { 4 | this.$http = $http; 5 | } 6 | 7 | getPosts() { 8 | return this.$http.get('http://jsonplaceholder.typicode.com/posts'); 9 | } 10 | 11 | getPost(postId) { 12 | return this.$http.get('http://jsonplaceholder.typicode.com/posts/' + postId); 13 | } 14 | 15 | getUser(usreId) { 16 | return this.$http.get('http://jsonplaceholder.typicode.com/users/' + usreId); 17 | } 18 | 19 | getComments(postId) { 20 | return this.$http.get('http://jsonplaceholder.typicode.com/posts/' + postId + '/comments'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/modules/post/view/post.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{post.post.title}}

4 |

{{post.post.body}}

5 |
6 |
7 |

Author

8 |
9 |
10 |

{{post.user.name}}

11 |

Read more posts by this author.

12 |
13 |
14 |

Share this post

15 | 16 |
17 |
18 |
19 |
20 |

Comments

21 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /app/modules/post/view/posts.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 18 |
19 | -------------------------------------------------------------------------------- /app/modules/user/config/routes.js: -------------------------------------------------------------------------------- 1 | /*@ngInject*/ 2 | export default ($stateProvider) => { 3 | $stateProvider 4 | .state('listUsers', { 5 | url: '/users', 6 | template: require('../view/users.html'), 7 | controller: 'UsersController', 8 | controllerAs: 'users', 9 | resolve: { 10 | users: (UserService) => { 11 | return UserService.getUsers().then((object) => { 12 | return object.data; 13 | }); 14 | } 15 | } 16 | }) 17 | .state('user', { 18 | url: '/users/:userId', 19 | template: require('../view/user.html'), 20 | controller: 'UserController', 21 | controllerAs: 'user', 22 | resolve: { 23 | user: (UserService, $stateParams) => { 24 | return UserService.getUser($stateParams.userId).then((object) => { 25 | return object.data; 26 | }); 27 | }, 28 | posts: (UserService, $stateParams) => { 29 | return UserService.getUserPosts($stateParams.userId).then((object) => { 30 | return object.data; 31 | }); 32 | } 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /app/modules/user/controller/user.js: -------------------------------------------------------------------------------- 1 | import '../sass/user.scss'; 2 | export default class UserController { 3 | constructor($stateParams, $location, user, posts) { 4 | this.$stateParams = $stateParams; 5 | this.$location = $location; 6 | this.user = user; 7 | this.posts = posts; 8 | } 9 | } 10 | 11 | UserController.$inject = ['$stateParams', '$location', 'user', 'posts']; 12 | -------------------------------------------------------------------------------- /app/modules/user/controller/users.js: -------------------------------------------------------------------------------- 1 | import '../sass/users.scss'; 2 | export default class UserController { 3 | constructor($stateParams, $location, users) { 4 | this.$stateParams = $stateParams; 5 | this.$location = $location; 6 | this.users = users; 7 | } 8 | } 9 | 10 | UserController.$inject = ['$stateParams', '$location', 'users']; 11 | -------------------------------------------------------------------------------- /app/modules/user/index.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import uirouter from 'angular-ui-router'; 3 | 4 | import routes from './config/routes'; 5 | import UserService from './service/service'; 6 | import UsersController from './controller/users'; 7 | import UserController from './controller/user'; 8 | 9 | export default angular.module('user', [uirouter]) 10 | .config(routes) 11 | .service('UserService', UserService) 12 | .controller('UsersController', UsersController) 13 | .controller('UserController', UserController) 14 | .name; 15 | -------------------------------------------------------------------------------- /app/modules/user/sass/user.scss: -------------------------------------------------------------------------------- 1 | #user { 2 | h2 { 3 | text-transform: capitalize; 4 | } 5 | .divider { 6 | width: 100%; 7 | text-align: center; 8 | border-bottom: 1px solid #eeeeee; 9 | line-height: 0.1em; 10 | margin: 10px 0 20px; 11 | span { 12 | background-color: #ffffff; 13 | padding: 0 20px; 14 | } 15 | } 16 | .user-info { 17 | margin: 100px 0; 18 | img { 19 | margin-bottom: 20px; 20 | } 21 | } 22 | address { 23 | margin-bottom: 0; 24 | } 25 | .posts { 26 | margin: 50px 0; 27 | ul { 28 | list-style: none; 29 | margin: 0; 30 | padding: 0; 31 | li { 32 | article { 33 | margin: 40px 0; 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/modules/user/sass/users.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/modules/user/service/service.js: -------------------------------------------------------------------------------- 1 | /*@ngInject*/ 2 | class UserService { 3 | constructor($http) { 4 | this.$http = $http; 5 | } 6 | 7 | getUsers() { 8 | return this.$http.get('http://jsonplaceholder.typicode.com/users'); 9 | } 10 | 11 | getUser(usreId) { 12 | return this.$http.get('http://jsonplaceholder.typicode.com/users/' + usreId); 13 | } 14 | 15 | getUserPosts(usreId) { 16 | return this.$http.get('http://jsonplaceholder.typicode.com/posts/?userId=' + usreId); 17 | } 18 | } 19 | 20 | export default UserService; 21 | -------------------------------------------------------------------------------- /app/modules/user/view/user.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 140x140 6 |
7 |
8 |
9 |
10 |

{{user.user.name}}

11 |
12 |
13 |
14 |
15 | Email 16 |
17 | 20 |
21 |
22 |
23 | Company 24 |
25 |
26 | {{user.user.company.name}} 27 |
28 |
29 |
30 |
31 | Website 32 |
33 |
34 | {{user.user.website}} 35 |
36 |
37 |
38 |
39 | Address 40 |
41 |
42 |
43 | {{user.user.address.street}}, {{user.user.address.suite}} 44 |
{{user.user.address.city}}, {{user.user.address.zipcode}} 45 |
46 |
47 |
48 |
49 |
50 | Phone 51 |
52 |
53 | {{user.user.phone}} 54 |
55 |
56 |
57 |
58 |
59 |
60 |

{{user.posts.length}} Posts

61 |
62 |
63 | 76 |
77 |
78 | -------------------------------------------------------------------------------- /app/modules/user/view/users.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 18 |
19 | -------------------------------------------------------------------------------- /app/sass/color.scss: -------------------------------------------------------------------------------- 1 | $grey: #eeeeee; 2 | -------------------------------------------------------------------------------- /app/sass/style.scss: -------------------------------------------------------------------------------- 1 | @import "../../node_modules/bootstrap/dist/css/bootstrap.min.css"; 2 | @import "../../node_modules/font-awesome/css/font-awesome.min.css"; 3 | @import "../../node_modules/toastr/build/toastr.min.css"; 4 | @import "./color.scss"; 5 | 6 | .content { 7 | margin-top: 51px; 8 | } 9 | 10 | .spinner { 11 | margin: 100px auto 0; 12 | width: 70px; 13 | text-align: center; 14 | } 15 | 16 | .spinner > div { 17 | width: 18px; 18 | height: 18px; 19 | background-color: $grey; 20 | 21 | border-radius: 100%; 22 | display: inline-block; 23 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 24 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 25 | } 26 | 27 | .spinner .bounce1 { 28 | -webkit-animation-delay: -0.32s; 29 | animation-delay: -0.32s; 30 | } 31 | 32 | .spinner .bounce2 { 33 | -webkit-animation-delay: -0.16s; 34 | animation-delay: -0.16s; 35 | } 36 | 37 | @-webkit-keyframes sk-bouncedelay { 38 | 0%, 80%, 100% { -webkit-transform: scale(0) } 39 | 40% { -webkit-transform: scale(1.0) } 40 | } 41 | 42 | @keyframes sk-bouncedelay { 43 | 0%, 80%, 100% { 44 | -webkit-transform: scale(0); 45 | transform: scale(0); 46 | } 40% { 47 | -webkit-transform: scale(1.0); 48 | transform: scale(1.0); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpackConfig = require('./webpack.config'); 3 | var entry = './app/modules/**/**/__tests__/*.js'; 4 | var preprocessors = {}; 5 | preprocessors[entry] = ['webpack']; 6 | 7 | // Karma configuration 8 | // Generated on Fri Apr 29 2016 13:59:09 GMT-0700 (PDT) 9 | 10 | module.exports = function(config) { 11 | config.set({ 12 | 13 | // base path that will be used to resolve all patterns (eg. files, exclude) 14 | basePath: '', 15 | 16 | 17 | // frameworks to use 18 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 19 | frameworks: ['mocha', 'chai'], 20 | 21 | 22 | // list of files / patterns to load in the browser 23 | files: [entry], 24 | 25 | webpack: webpackConfig, 26 | 27 | // list of files to exclude 28 | exclude: [], 29 | 30 | 31 | // preprocess matching files before serving them to the browser 32 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 33 | preprocessors: preprocessors, 34 | 35 | 36 | // test results reporter to use 37 | // possible values: 'dots', 'progress' 38 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 39 | reporters: ['progress'], 40 | 41 | 42 | // web server port 43 | port: 9876, 44 | 45 | 46 | // enable / disable colors in the output (reporters and logs) 47 | colors: true, 48 | 49 | 50 | // level of logging 51 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 52 | logLevel: config.LOG_INFO, 53 | 54 | 55 | // enable / disable watching file and executing tests whenever any file changes 56 | autoWatch: true, 57 | 58 | 59 | // start these browsers 60 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 61 | browsers: ['Chrome'], 62 | 63 | 64 | // Continuous Integration mode 65 | // if true, Karma captures browsers, runs the tests and exits 66 | singleRun: false, 67 | 68 | // Concurrency level 69 | // how many browser should be started simultaneous 70 | concurrency: Infinity, 71 | 72 | plugins: [ 73 | require('karma-webpack'), 74 | 'karma-chai', 75 | 'karma-mocha', 76 | 'karma-chrome-launcher' 77 | ] 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Webpack-Angular", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run lint && NODE_ENV=test ./node_modules/.bin/karma start", 8 | "lint": "./node_modules/.bin/eslint app", 9 | "dev": "./node_modules/.bin/webpack-dev-server --content-base app", 10 | "build": "NODE_ENV=production node ./node_modules/.bin/webpack && cp app/index.html build/index.html && cp -r app/images build/images" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "angular": "^1.5.5", 16 | "angular-mocks": "^1.5.5", 17 | "angular-sanitize": "^1.5.5", 18 | "angular-ui-router": "^0.3.2", 19 | "autoprefixer-loader": "^3.2.0", 20 | "babel-core": "^6.7.7", 21 | "babel-loader": "^6.2.4", 22 | "babel-preset-es2015": "^6.6.0", 23 | "bootstrap": "^3.3.6", 24 | "chai": "^3.5.0", 25 | "css-loader": "^0.23.1", 26 | "eslint": "^2.2.0", 27 | "file-loader": "^0.8.5", 28 | "font-awesome": "^4.5.0", 29 | "jquery": "^2.2.3", 30 | "karma": "^0.13.22", 31 | "karma-chai": "^0.1.0", 32 | "karma-chrome-launcher": "^0.2.3", 33 | "karma-mocha": "^0.2.2", 34 | "karma-webpack": "^1.7.0", 35 | "mocha": "^2.4.5", 36 | "ng-annotate-loader": "^0.1.0", 37 | "ng-resource": "^1.3.2", 38 | "node-sass": "^3.6.0", 39 | "raw-loader": "^0.5.1", 40 | "sass-loader": "^3.2.0", 41 | "style-loader": "^0.13.1", 42 | "toastr": "^2.1.2", 43 | "url-loader": "^0.5.7", 44 | "webpack": "^1.13.0", 45 | "webpack-dev-server": "^1.14.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var DefinePlugin = require('webpack').DefinePlugin; 2 | var ProvidePlugin = require('webpack').ProvidePlugin; 3 | var optimize = require('webpack').optimize; 4 | 5 | var definePlugins = new DefinePlugin({ 6 | TEST: process.env.NODE_ENV === 'test' 7 | }); 8 | var providePlugins = new ProvidePlugin({ 9 | $: 'jquery', 10 | jQuery: 'jquery', 11 | 'window.jQuery': 'jquery', 12 | 'windows.jQuery': 'jquery', 13 | }) 14 | 15 | var config = { 16 | context: __dirname + '/app', 17 | entry: './index.js', 18 | output: { 19 | path: __dirname + '/app', 20 | filename: 'bundle.js' 21 | }, 22 | devtool: 'source-map', 23 | module: { 24 | loaders: [{ 25 | test: /\.js$/, 26 | exclude: /(node_modules)/, 27 | loader: 'ng-annotate!babel' 28 | }, { 29 | test: /\.s?css$/, 30 | exclude: /(node_modules)/, 31 | loaders: ['style', 'css?sourceMap', 'autoprefixer', 'sass?sourceMap'] 32 | }, { 33 | test: /\.html$/, 34 | loader: 'raw' 35 | }, { 36 | test: /\.(jpe?g|png|gif)$/, 37 | exclude: /(node_modules)/, 38 | loader: 'url?limit=10000' 39 | }, { 40 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 41 | loader: 'url?limit=10000&minetype=application/font-woff' 42 | }, { 43 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 44 | loader: 'url' 45 | }] 46 | }, 47 | plugins: [ 48 | definePlugins, 49 | providePlugins 50 | ], 51 | resolve: { 52 | extensions: ['', '.js', '.css'] 53 | } 54 | }; 55 | 56 | if (process.env.NODE_ENV === 'production') { 57 | config.output.path = __dirname + '/build'; 58 | config.plugins.push(new optimize.UglifyJsPlugin()); 59 | } 60 | 61 | module.exports = config; 62 | --------------------------------------------------------------------------------