├── .eslintrc ├── .gitignore ├── README.md ├── app ├── components │ ├── FilterLinks.js │ ├── Footer.js │ ├── TextEditor.js │ ├── TodoApp.html │ ├── TodoApp.js │ └── TodoView.js ├── directives │ └── FocusOn.js ├── index.js └── services │ ├── Filters.js │ └── TodoStore.js ├── dist ├── css │ ├── base.css │ └── index.css └── index.html ├── gulpfile.babel.js ├── karma.conf.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser" : "babel-eslint", 3 | "rules" : { 4 | "strict" : 0, 5 | "quotes" : [1, "single"], 6 | "new-cap" : 0, 7 | "eol-last" : 0, 8 | "no-shadow" : 0, 9 | "no-trailing-spaces" : 0, 10 | "no-underscore-dangle" : 0, 11 | "no-unused-vars" : [2, { 12 | "args" : "none" 13 | }] 14 | }, 15 | "globals" : { 16 | "window" : false, 17 | "mina" : false, 18 | "protractor" : false, 19 | "require" : false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | .DS_Store 4 | .hold 5 | .sass-cache 6 | *.swp 7 | .build 8 | dist/bundle.js 9 | npm-debug.log 10 | busters.json 11 | coverage 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ng-forward-playground 2 | A playground for testing early builds of ng-forward 3 | 4 | To get started, clone this repository and ng-forward 5 | 6 | 1. In your terminal, go to the folder where you cloned ng-forward 7 | 2. Run `npm link` to begin the linking process 8 | 3. Go to the folder where you cloned ng-forward-playground 9 | 4. Run `npm install` 10 | 5. Run `npm link ng-forward` to finish linking ng-forward to the playground 11 | 6. Run `gulp` to start a development server 12 | -------------------------------------------------------------------------------- /app/components/FilterLinks.js: -------------------------------------------------------------------------------- 1 | import {Inject, Component} from 'ng-forward'; 2 | import Filters from '../services/Filters'; 3 | 4 | @Component({ 5 | selector: 'filterLinks', 6 | template: 7 | ` 8 | 13 | ` 14 | }) 15 | @Inject(Filters) 16 | export default class FilterLinks { 17 | 18 | constructor(filters) { 19 | this.filters = filters; 20 | } 21 | } -------------------------------------------------------------------------------- /app/components/Footer.js: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter} from 'ng-forward'; 2 | import FilterLinks from './FilterLinks'; 3 | 4 | @Component({ 5 | selector: 'footer', 6 | inputs: ['remainingCount', 'completedCount'], 7 | outputs: ['clearCompleted'], 8 | directives: [FilterLinks], 9 | template: 10 | ` 11 | {{footer.remainingCount}} 12 | 13 | 14 | 15 | 16 | ` 17 | }) 18 | export default class Footer { 19 | constructor() { 20 | this.clearCompleted = new EventEmitter(); 21 | } 22 | } -------------------------------------------------------------------------------- /app/components/TextEditor.js: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Inject} from 'ng-forward'; 2 | import FocusOn from '../directives/FocusOn'; 3 | 4 | const ESC_KEY = 27; 5 | const ENTER_KEY = 13; 6 | 7 | @Component({ 8 | selector: 'text-editor', 9 | inputs: ['value', 'placeholder', 'inputClasses', 'focusOn'], 10 | outputs: ['start', 'enter', 'end', 'abort'], 11 | directives: [FocusOn], 12 | template: 13 | `` 19 | }) 20 | @Inject('$element') 21 | export default class TextEditor { 22 | constructor($element) { 23 | this.$input = $element.find('input')[0]; 24 | this.start = new EventEmitter(); 25 | this.enter = new EventEmitter(); 26 | this.end = new EventEmitter(); 27 | this.abort = new EventEmitter(); 28 | } 29 | 30 | keyup(keyCode) { 31 | if(keyCode === ENTER_KEY) { 32 | this.enter.next(); 33 | } 34 | if(keyCode === ESC_KEY) { 35 | this.abort.next(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/components/TodoApp.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

todos

5 | 11 |
12 | 13 |
14 | 15 | 19 | 20 | 21 | 32 | 33 |
34 | 35 | 41 |
-------------------------------------------------------------------------------- /app/components/TodoApp.js: -------------------------------------------------------------------------------- 1 | import {Inject, Component} from 'ng-forward'; 2 | import {TodoStore} from '../services/TodoStore'; 3 | import Footer from './Footer'; 4 | import TextEditor from './TextEditor'; 5 | import TodoView from './TodoView'; 6 | import Filters from '../services/Filters'; 7 | 8 | @Component({ 9 | selector: 'todo-app', 10 | providers: [TodoStore, Filters], 11 | directives: [TextEditor, TodoView, Footer], 12 | template: require('./TodoApp.html') 13 | }) 14 | @Inject(TodoStore, Filters) 15 | export default class TodoApp { 16 | 17 | todoStore: TodoStore; 18 | newTodoTitle: string; 19 | 20 | constructor(todoStore, filters) { 21 | this.filters = filters; 22 | this.todoStore = todoStore; 23 | this.newTodoTitle = ''; 24 | } 25 | 26 | addTodo() { 27 | let newTodoTitle = this.newTodoTitle && this.newTodoTitle.trim(); 28 | if (newTodoTitle) { 29 | this.todoStore.add(newTodoTitle); 30 | this.newTodoTitle = ''; 31 | } 32 | } 33 | 34 | getFilteredTodos() { 35 | return this.todoStore.todos.filter(this.filters.current()); 36 | } 37 | } -------------------------------------------------------------------------------- /app/components/TodoView.js: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter} from 'ng-forward'; 2 | 3 | @Component({ 4 | selector: 'todo-view', 5 | inputs: ['todo'], 6 | outputs: ['titleChange', 'completedChange', 'remove'], 7 | template: 8 | ` 9 |
10 | 11 | 12 | 13 |
14 | 23 | ` 24 | }) 25 | export default class TodoView { 26 | constructor() { 27 | this.titleChange = new EventEmitter(); 28 | this.completedChange = new EventEmitter(); 29 | this.remove = new EventEmitter(); 30 | } 31 | 32 | updateTitle() { 33 | this.titleChange.next(); 34 | this.todo.editing = false; 35 | } 36 | 37 | saveOriginal() { 38 | this.originalTitle = this.todo.title; 39 | } 40 | 41 | resetTodo() { 42 | this.todo.title = this.originalTitle; 43 | this.todo.editing = false; 44 | } 45 | } -------------------------------------------------------------------------------- /app/directives/FocusOn.js: -------------------------------------------------------------------------------- 1 | import {Directive, Inject} from 'ng-forward'; 2 | 3 | /** 4 | * Directive that places focus on the element it is applied to when the 5 | * expression it binds to evaluates to true 6 | */ 7 | @Directive({ selector: '[focus-on]' }) 8 | @Inject('$timeout', '$scope', '$attrs', '$element') 9 | export default class FocusOn { 10 | constructor($timeout, $scope, $attrs, $element) { 11 | $scope.$watch($attrs.focusOn, newVal => { 12 | if (newVal) { 13 | $timeout(() => $element[0].focus(), 0, false); 14 | } 15 | }); 16 | } 17 | } -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import 'babel/polyfill'; 2 | import 'angular'; 3 | import 'zone.js'; 4 | import {bootstrap} from 'ng-forward'; 5 | import TodoApp from './components/TodoApp'; 6 | 7 | bootstrap(TodoApp); -------------------------------------------------------------------------------- /app/services/Filters.js: -------------------------------------------------------------------------------- 1 | import {Injectable, Inject} from 'ng-forward'; 2 | 3 | @Injectable() 4 | @Inject('$location') 5 | export default class Filters { 6 | constructor($location) { 7 | this.$location = $location; 8 | 9 | this.filters = { 10 | '': () => true, 11 | 'active': todo => !todo.completed, 12 | 'completed': todo => todo.completed 13 | }; 14 | } 15 | 16 | currentName() { 17 | // strip off leading forward slash 18 | return this.$location.path().replace(/^\//, ''); 19 | } 20 | 21 | current() { 22 | return this.filters[this.currentName()]; 23 | } 24 | 25 | isCurrent(filterName) { 26 | return (filterName) === this.currentName(); 27 | } 28 | } -------------------------------------------------------------------------------- /app/services/TodoStore.js: -------------------------------------------------------------------------------- 1 | import {Injectable} from 'ng-forward'; 2 | 3 | /* global localStorage */ 4 | export class Todo { 5 | completed: Boolean; 6 | title: String; 7 | constructor(title: String, completed = false) { 8 | this.completed = completed; 9 | this.title = title.trim(); 10 | } 11 | } 12 | 13 | @Injectable() 14 | export class TodoStore { 15 | 16 | todos: Array; 17 | 18 | constructor() { 19 | // Load the todos from local storage 20 | let persistedTodos = JSON.parse(localStorage.getItem('todos') || '[]'); 21 | this.todos = persistedTodos.map( (todo) => { 22 | return new Todo(todo.title, todo.completed); 23 | }); 24 | } 25 | 26 | countCompleted() { 27 | let count = this.todos.reduce((count, todo) => todo.completed ? count + 1 : count, 0); 28 | return count; 29 | } 30 | 31 | allCompleted() { 32 | return this.todos.length === this.countCompleted(); 33 | } 34 | 35 | countRemaining() { 36 | return this.todos.length - this.countCompleted(); 37 | } 38 | 39 | setAllTo(completed) { 40 | this.todos.forEach((t: Todo) => t.completed = completed); 41 | this.save(); 42 | } 43 | 44 | removeCompleted() { 45 | this.todos = this.todos.filter((todo: Todo) => !todo.completed); 46 | this.save(); 47 | } 48 | 49 | remove(todoToRemove: Todo) { 50 | this.todos = this.todos.filter((todo: Todo) => todoToRemove !== todo); 51 | this.save(); 52 | } 53 | 54 | add(title: String, completed: boolean) { 55 | this.todos.push(new Todo(title, completed)); 56 | this.save(); 57 | } 58 | 59 | save() { 60 | localStorage.setItem('todos', JSON.stringify(this.todos)); 61 | } 62 | } -------------------------------------------------------------------------------- /dist/css/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /dist/css/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | .todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | .todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | .todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | .todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | .todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | .new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | .new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | .main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | .toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | .toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | .toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | .todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | .todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | .todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | .todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | .todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | .todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | .todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | .todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | .todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li label { 200 | white-space: pre; 201 | word-break: break-word; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | .todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | .todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | .todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | .todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | .todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | .todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | .todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | .footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | .footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | .todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | .todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | .filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | .filters li { 291 | display: inline; 292 | } 293 | 294 | .filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | .filters li a.selected, 304 | .filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | .filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | .clear-completed, 313 | html .clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | .clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | .info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | .info p { 335 | line-height: 1; 336 | } 337 | 338 | .info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | .info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | .toggle-all, 354 | .todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | .todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | .toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | .footer { 372 | height: 50px; 373 | } 374 | 375 | .filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /*global __dirname */ 2 | import babelify from 'babelify'; 3 | import browserify from 'browserify'; 4 | import buffer from 'vinyl-buffer'; 5 | import eslint from 'gulp-eslint'; 6 | import gulp from 'gulp'; 7 | import gulpwatch from 'gulp-watch'; 8 | import gutil from 'gulp-util'; 9 | import historyApiFallback from 'connect-history-api-fallback'; 10 | import sequence from 'run-sequence'; 11 | import source from 'vinyl-source-stream'; 12 | import stringify from 'stringify'; 13 | import watchify from 'watchify'; 14 | import {server as karma} from 'karma'; 15 | import browsersync from 'browser-sync'; 16 | 17 | function handleError(task){ 18 | return function(err) { 19 | gutil.beep(); 20 | gutil.log(err); 21 | this.emit('end'); 22 | }; 23 | } 24 | 25 | function Build(watch, done){ 26 | var b; 27 | function transform(){ 28 | if( !b ){ 29 | b = browserify('./app/index.js', { 30 | debug: true, 31 | paths: ['./node_modules', './app/'], 32 | cache: {}, 33 | packageCache: {}, 34 | fullPaths: true 35 | }); 36 | 37 | if(watch){ 38 | b = watchify(b); 39 | } 40 | 41 | b.transform(babelify.configure({ 42 | stage: 0 43 | })); 44 | 45 | b.transform(stringify(['.html'])); 46 | } 47 | 48 | function bundle(){ 49 | let stream = b.bundle() 50 | .on('error', handleError('Browserify')) 51 | .pipe(source('bundle.js')) 52 | .pipe(buffer()) 53 | .pipe(gulp.dest('./dist')); 54 | 55 | stream.on('end', browsersync.reload); 56 | 57 | return stream; 58 | } 59 | 60 | return bundle(); 61 | } 62 | 63 | gulp.task('bundle', ['lint'], function bundleTask(){ 64 | return transform(); 65 | }); 66 | 67 | gulp.task('bundle-light', function bundleLightTask(){ 68 | return transform(); 69 | }); 70 | 71 | gulp.task('test', function(done){ 72 | karma.start({ 73 | configFile: __dirname + '/karma.conf.js', 74 | singleRun: true 75 | }, done); 76 | }); 77 | 78 | function tdd(){ 79 | karma.start({ 80 | configFile: __dirname + '/karma.conf.js', 81 | singleRun: false, 82 | reporters: ['mocha'] 83 | }); 84 | } 85 | 86 | gulp.task('setup-watchers', function(){ 87 | gulpwatch(['app/**/*.js', 'app/**/*.html', 'node_modules/ng-forward/**/*.js'], function(){ 88 | gulp.run('bundle'); 89 | }); 90 | 91 | gulp.run('serve'); 92 | tdd(); 93 | }); 94 | 95 | if(watch) 96 | { 97 | sequence('bundle', 'setup-watchers', done); 98 | } 99 | else 100 | { 101 | sequence('bundle', 'test', done); 102 | } 103 | } 104 | 105 | function serve(){ 106 | browsersync.init({ 107 | port: 2000, 108 | ui: { 109 | port: 2001 110 | }, 111 | server: { 112 | baseDir: './dist', 113 | middleware: [historyApiFallback] 114 | } 115 | }); 116 | } 117 | 118 | gulp.task('serve', serve); 119 | 120 | gulp.task('default', function(done){ 121 | Build(true, done); 122 | }); 123 | 124 | gulp.task('build', function(done){ 125 | Build(false, done); 126 | }); 127 | 128 | gulp.task('lint', function(){ 129 | return gulp.src(['app/**/*.js', '!app/**/*.spec.js', '!app/**/*.e2e.js']) 130 | .pipe(eslint()) 131 | .pipe(eslint.format()); 132 | }); 133 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | module.exports = function(karma) { 3 | karma.set({ 4 | autoWatch: true, 5 | basePath: '', 6 | frameworks: ['mocha', 'browserify'], 7 | files: [ 'app/**/*.spec.js' ], 8 | exclude: [], 9 | port: 9018, 10 | runnerPort: 9101, 11 | browsers: [ 12 | 'Chrome' 13 | ], 14 | reporters: ['mocha'], 15 | singleRun: false, 16 | colors: true, 17 | logLevel: karma.LOG_INFO, 18 | preprocessors: { 19 | 'app/**/*.js': ['browserify'] 20 | }, 21 | browserify: { 22 | debug: true, 23 | transform: [ 24 | ['stringify'], 25 | ['babelify', { 26 | stage: 0 27 | }] 28 | ] 29 | } 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-forward-playground", 3 | "author": "Mike Ryan", 4 | "repository": "https://github.com/ngUpgraders/ng-forward-playground", 5 | "version": "0.1.0", 6 | "devDependencies": { 7 | "angular-mocks": "^1.4.3", 8 | "babel": "^5.8.19", 9 | "babel-eslint": "^4.0.5", 10 | "babelify": "^6.1.3", 11 | "browser-sync": "^2.8.1", 12 | "browserify": "^11.0.1", 13 | "chai": "^2.2.0", 14 | "chai-as-promised": "^4.3.0", 15 | "chai-jquery": "^2.0.0", 16 | "connect-history-api-fallback": "0.0.5", 17 | "eslint": "^0.24.0", 18 | "gulp": "^3.9.0", 19 | "gulp-eslint": "^0.13.2", 20 | "gulp-util": "^3.0.4", 21 | "gulp-watch": "^4.2.4", 22 | "karma": "^0.12.31", 23 | "karma-browserify": "^4.1.2", 24 | "karma-chrome-launcher": "^0.1.5", 25 | "karma-mocha": "^0.1.10", 26 | "karma-mocha-reporter": "^1.0.2", 27 | "mocha": "^2.2.4", 28 | "run-sequence": "^1.0.2", 29 | "sinon": "^1.14.1", 30 | "sinon-chai": "^2.7.0", 31 | "sinon-promise": "^0.1.3", 32 | "stringify": "^3.1.0", 33 | "vinyl-buffer": "^1.0.0", 34 | "vinyl-source-stream": "^1.1.0", 35 | "watchify": "^3.3.1" 36 | }, 37 | "scripts": { 38 | "postinstall": "npm dedupe" 39 | }, 40 | "dependencies": { 41 | "angular": "^1.4.3", 42 | "angular-decorators": "^1.0.0", 43 | "angular-ui-router": "^0.2.15", 44 | "lodash": "^3.9.3", 45 | "rx": "^2.5.3", 46 | "todomvc-app-css": "^2.0.1", 47 | "todomvc-common": "^1.0.2", 48 | "zone.js": "^0.5.2" 49 | } 50 | } 51 | --------------------------------------------------------------------------------