├── .gitignore ├── README.md ├── karma.conf.js ├── package.json ├── src ├── app.config.js ├── app.css ├── app.js ├── asset │ └── image │ │ └── components.jpg ├── common │ ├── common.module.js │ └── component │ │ └── user-info-component.js ├── feature-a │ ├── feature-a.config.js │ ├── feature-a.ctrl.js │ ├── feature-a.module.js │ └── feature-a.tpl.html ├── feature-b │ ├── feature-b.config.js │ ├── feature-b.module.js │ └── some-component │ │ ├── some-component.js │ │ └── some-component.tpl.html ├── index.html └── tests.webpack.js ├── webpack.build.js ├── webpack.config.js ├── webpack.make.js └── webpack.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | build 4 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### ATTENTION 2 | This is one of the first Angular JS repositories I created, if you like component pattern 3 | and want a seed project use [Angular JS 1.X ES6 seed](https://github.com/tomastrajan/angular-js-es6-testing-example) 4 | which implements `component pattern`, contains better `webpack` build and advanced 5 | testing support out of the box, happy hacking! 6 | 7 | # Component Pattern for Angular JS 1.X 8 | 9 | * original blog post describing [Component Pattern](https://medium.com/@tomastrajan/component-paradigm-cf32e94ba78b) 10 | * presentation describing history of Angular JS concepts and [Component Pattern](https://slides.com/tomastrajan/component-pattern-for-angular-js-1-x) 11 | 12 | ![Components](/src/asset/image/components.jpg?raw=true "Component Patternf for Angular JS 1.X") 13 | 14 | ## Features 15 | 16 | * standard implementation of a state using `ui-router` 17 | * component implementation used as inline template in `ui-router` state definition 18 | * ES6, Webpack 19 | 20 | ## Installation 21 | 22 | To use it, just clone this repo and install the npm dependencies: 23 | 24 | ```shell 25 | $ git clone https://github.com/tomastrajan/component-pattern-for-angular-js-1-x component_pattern_example 26 | $ cd component_pattern_example 27 | $ npm install 28 | ``` 29 | 30 | ## Scripts 31 | 32 | All scripts are run with `npm run [script]`, for example: `npm run test`. 33 | 34 | * `build` - generate a minified build to dist folder 35 | * `dev` - start development server, try it by opening `http://localhost:8080/` 36 | * `test` - run all tests 37 | * `test:live` - continuously run unit tests watching for changes 38 | 39 | ## Credits 40 | 41 | This example uses build process from [angular-webpack-workflow](https://github.com/Foxandxss/angular-webpack-workflow), 42 | so check it out for more information if needed. -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require('./webpack.test'); 2 | 3 | // Reference: http://karma-runner.github.io/0.12/config/configuration-file.html 4 | module.exports = function karmaConfig (config) { 5 | config.set({ 6 | frameworks: [ 7 | // Reference: https://github.com/karma-runner/karma-jasmine 8 | // Set framework to jasmine 9 | 'jasmine' 10 | ], 11 | 12 | reporters: [ 13 | // Reference: https://github.com/mlex/karma-spec-reporter 14 | // Set reporter to print detailed results to console 15 | 'spec', 16 | 17 | // Reference: https://github.com/karma-runner/karma-coverage 18 | // Output code coverage files 19 | 'coverage' 20 | ], 21 | 22 | files: [ 23 | // Grab all files in the app folder that contain .test. 24 | 'src/tests.webpack.js' 25 | ], 26 | 27 | preprocessors: { 28 | // Reference: http://webpack.github.io/docs/testing.html 29 | // Reference: https://github.com/webpack/karma-webpack 30 | // Convert files with webpack and load sourcemaps 31 | 'src/tests.webpack.js': ['webpack', 'sourcemap'] 32 | }, 33 | 34 | browsers: [ 35 | // Run tests using PhantomJS 36 | 'PhantomJS' 37 | ], 38 | 39 | singleRun: true, 40 | 41 | // Configure code coverage reporter 42 | coverageReporter: { 43 | dir: 'build/coverage/', 44 | type: 'html' 45 | }, 46 | 47 | webpack: webpackConfig 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "component-pattern-for-angular-js-1-x", 3 | "version": "1.0.0", 4 | "description": "Example of how to implement Component Pattern in Angular JS 1.X", 5 | "scripts": { 6 | "build": "webpack --config webpack.build.js --bail -p", 7 | "dev": "webpack-dev-server --history-api-fallback --hot --inline --progress", 8 | "test": "karma start", 9 | "test:live": "karma start --auto-watch --no-single-run" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/tomastrajan/component-pattern-for-angular-js-1-x" 14 | }, 15 | "author": "Tomas Trajan", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/tomastrajan/component-pattern-for-angular-js-1-x/issues" 19 | }, 20 | "homepage": "https://github.com/tomastrajan/component-pattern-for-angular-js-1-x", 21 | "dependencies": { 22 | "angular": "1.3.16", 23 | "angular-ui-router": "0.2.15", 24 | "autoprefixer-core": "5.2.0", 25 | "babel-core": "5.5.8", 26 | "babel-loader": "5.1.4", 27 | "babel-runtime": "5.5.8", 28 | "bootstrap": "3.3.5", 29 | "css-loader": "0.15.5", 30 | "extract-text-webpack-plugin": "0.8.2", 31 | "file-loader": "0.8.4", 32 | "html-webpack-plugin": "1.5.2", 33 | "jquery": "2.1.4", 34 | "node-libs-browser": "0.5.2", 35 | "postcss-loader": "0.5.1", 36 | "raw-loader": "0.5.1", 37 | "style-loader": "0.12.3", 38 | "webpack": "1.9.11", 39 | "webpack-dev-server": "1.9.0" 40 | }, 41 | "devDependencies": { 42 | "angular-mocks": "1.4.1", 43 | "isparta-instrumenter-loader": "0.2.1", 44 | "jasmine-core": "2.3.4", 45 | "karma": "0.13.2", 46 | "karma-coverage": "0.4.2", 47 | "karma-jasmine": "0.3.5", 48 | "karma-phantomjs-launcher": "0.2.0", 49 | "karma-sourcemap-loader": "0.3.5", 50 | "karma-spec-reporter": "0.0.20", 51 | "karma-webpack": "1.5.1", 52 | "null-loader": "0.1.1", 53 | "phantomjs": "1.9.17" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app.config.js: -------------------------------------------------------------------------------- 1 | export function routing($urlRouterProvider, $stateProvider) { 2 | 3 | $urlRouterProvider.otherwise('/feature-a'); 4 | 5 | $stateProvider 6 | .state('app', { 7 | abstract: true, 8 | template: '
' 9 | }) 10 | 11 | } 12 | 13 | export function routingEventsLogger($rootScope) { 14 | 15 | const ROUTING_EVENTS = [ 16 | '$stateChangeStart', 17 | '$stateChangeSuccess', 18 | '$stateChangeError' 19 | ]; 20 | 21 | const VIEW_EVENTS = [ 22 | '$viewContentLoading', 23 | '$viewContentLoaded' 24 | ]; 25 | 26 | ROUTING_EVENTS.forEach(function(routingEvent) { 27 | $rootScope.$on(routingEvent, function(event, toState, toParams, fromState, fromParams, error){ 28 | console.log(routingEvent, event, toState, toParams, fromState, fromParams); 29 | }); 30 | }); 31 | 32 | VIEW_EVENTS.forEach(function(viewEvent) { 33 | $rootScope.$on(viewEvent, function(event, viewConfig){ 34 | console.log(viewEvent, event, viewConfig); 35 | }); 36 | }); 37 | 38 | } -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 80px; 3 | } -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.min.css'; 2 | import './app.css'; 3 | 4 | import bootstrap from 'bootstrap'; 5 | 6 | import angular from 'angular'; 7 | import uirouter from 'angular-ui-router'; 8 | 9 | import { routing, routingEventsLogger } from './app.config'; 10 | 11 | import common from './common/common.module'; 12 | 13 | import featureA from './feature-a/feature-a.module'; 14 | import featureB from './feature-b/feature-b.module'; 15 | 16 | const DEBUG = false; 17 | 18 | const app = angular 19 | .module('app', [uirouter, common, featureA, featureB]) 20 | .config(routing); 21 | 22 | if (DEBUG) { 23 | app 24 | .run(routingEventsLogger) 25 | ; 26 | } 27 | -------------------------------------------------------------------------------- /src/asset/image/components.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomastrajan/component-pattern-for-angular-js-1-x/7f58f40400a5faef0f838e8bdcc82bc9194f5f02/src/asset/image/components.jpg -------------------------------------------------------------------------------- /src/common/common.module.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | 3 | import userInfoComponent from './component/user-info-component'; 4 | 5 | export default angular 6 | .module('app.common', []) 7 | .directive('userInfoComponent', userInfoComponent) 8 | .name; -------------------------------------------------------------------------------- /src/common/component/user-info-component.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | 3 | return { 4 | scope: {}, 5 | controller: UserInfoComponent, 6 | controllerAs: 'ctrl', 7 | bindToController: true, 8 | template: 'Hi {{ctrl.name}}!' 9 | }; 10 | 11 | } 12 | 13 | class UserInfoComponent { 14 | 15 | constructor() { 16 | this.name = 'Tomas'; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/feature-a/feature-a.config.js: -------------------------------------------------------------------------------- 1 | import template from './feature-a.tpl.html' 2 | 3 | export function routing($stateProvider) { 4 | 5 | $stateProvider 6 | .state('app.feature-a', { 7 | url: '/feature-a', 8 | controller: 'FeatureACtrl', 9 | controllerAs: 'ctrl', 10 | template: template 11 | }); 12 | } -------------------------------------------------------------------------------- /src/feature-a/feature-a.ctrl.js: -------------------------------------------------------------------------------- 1 | export default function FeatureACtrl() { 2 | 3 | const vm = this; 4 | 5 | vm.property = 'My Controller Property'; 6 | 7 | } -------------------------------------------------------------------------------- /src/feature-a/feature-a.module.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | 3 | import { routing } from './feature-a.config.js'; 4 | import FeatureACtrl from './feature-a.ctrl.js'; 5 | 6 | export default angular.module('app.feature-a', []) 7 | .config(routing) 8 | .controller('FeatureACtrl', FeatureACtrl) 9 | .name; -------------------------------------------------------------------------------- /src/feature-a/feature-a.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |

Feature A!

3 |

Feature A is implemented using controller and template which are bound together in ui-router state definition

4 |
5 |

Feature is so great that you won't believe! It also contains: {{ctrl.property}}

6 | 7 | 8 |
-------------------------------------------------------------------------------- /src/feature-b/feature-b.config.js: -------------------------------------------------------------------------------- 1 | export function routing($stateProvider) { 2 | 3 | $stateProvider 4 | .state('app.feature-b', { 5 | url: '/feature-b', 6 | template: '
' 7 | }); 8 | } -------------------------------------------------------------------------------- /src/feature-b/feature-b.module.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | 3 | import { routing } from './feature-b.config.js'; 4 | 5 | import someComponent from './some-component/some-component'; 6 | 7 | export default angular 8 | .module('app.feature-b', []) 9 | .config(routing) 10 | .directive('someComponent', someComponent) 11 | .name; -------------------------------------------------------------------------------- /src/feature-b/some-component/some-component.js: -------------------------------------------------------------------------------- 1 | import template from './some-component.tpl.html' 2 | 3 | export default function() { 4 | 5 | return { 6 | scope: {}, 7 | controller: SomeComponent, 8 | controllerAs: 'ctrl', 9 | bindToController: true, 10 | template: template 11 | }; 12 | 13 | } 14 | 15 | class SomeComponent { 16 | 17 | constructor() { 18 | this.count = 0; 19 | this.property = 'My Component Property'; 20 | } 21 | 22 | increment() { 23 | this.count++; 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/feature-b/some-component/some-component.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |

Feature B!

3 |

Feature B is implemented as Component using "Component Pattern"

4 |
5 |

Feature is so great that it even contains counter functionality! It also contains: {{ctrl.property}}

6 |

Counter: {{ctrl.count}}

7 |

Oh, and don't forget about user-info-component which we just reused here... Sick!

8 | 9 | 10 | 11 |
-------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Component Pattern for Angular 1.X 6 | 7 | 8 | 9 | 10 | 32 | 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /src/tests.webpack.js: -------------------------------------------------------------------------------- 1 | // This file is an entry point for angular tests 2 | // Avoids some weird issues when using webpack + angular. 3 | 4 | import 'angular'; 5 | import 'angular-mocks/angular-mocks'; 6 | 7 | var testsContext = require.context(".", true, /.test$/); 8 | testsContext.keys().forEach(testsContext); -------------------------------------------------------------------------------- /webpack.build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for builds 3 | */ 4 | module.exports = require('./webpack.make')({ 5 | BUILD: true, 6 | TEST: false 7 | }); 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for development 3 | */ 4 | module.exports = require('./webpack.make')({ 5 | BUILD: false, 6 | TEST: false 7 | }); 8 | -------------------------------------------------------------------------------- /webpack.make.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Modules 4 | var webpack = require('webpack'); 5 | var autoprefixer = require('autoprefixer-core'); 6 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | 9 | module.exports = function makeWebpackConfig(options) { 10 | /** 11 | * Environment type 12 | * BUILD is for generating minified builds 13 | * TEST is for generating test builds 14 | */ 15 | var BUILD = !!options.BUILD; 16 | var TEST = !!options.TEST; 17 | 18 | /** 19 | * Config 20 | * Reference: http://webpack.github.io/docs/configuration.html 21 | * This is the object where all configuration gets set 22 | */ 23 | var config = {}; 24 | 25 | /** 26 | * Entry 27 | * Reference: http://webpack.github.io/docs/configuration.html#entry 28 | * Should be an empty object if it's generating a test build 29 | * Karma will set this when it's a test build 30 | */ 31 | if (TEST) { 32 | config.entry = {} 33 | } else { 34 | config.entry = { 35 | app: './src/app.js' 36 | } 37 | } 38 | 39 | /** 40 | * Output 41 | * Reference: http://webpack.github.io/docs/configuration.html#output 42 | * Should be an empty object if it's generating a test build 43 | * Karma will handle setting it up for you when it's a test build 44 | */ 45 | if (TEST) { 46 | config.output = {} 47 | } else { 48 | config.output = { 49 | // Absolute output directory 50 | path: __dirname + '/public', 51 | 52 | // Output path from the view of the page 53 | // Uses webpack-dev-server in development 54 | publicPath: BUILD ? '/' : 'http://localhost:8080/', 55 | 56 | // Filename for entry points 57 | // Only adds hash in build mode 58 | filename: BUILD ? '[name].[hash].js' : '[name].bundle.js', 59 | 60 | // Filename for non-entry points 61 | // Only adds hash in build mode 62 | chunkFilename: BUILD ? '[name].[hash].js' : '[name].bundle.js' 63 | } 64 | } 65 | 66 | /** 67 | * Devtool 68 | * Reference: http://webpack.github.io/docs/configuration.html#devtool 69 | * Type of sourcemap to use per build type 70 | */ 71 | if (TEST) { 72 | config.devtool = 'inline-source-map'; 73 | } else if (BUILD) { 74 | config.devtool = 'source-map'; 75 | } else { 76 | config.devtool = 'eval'; 77 | } 78 | 79 | /** 80 | * Loaders 81 | * Reference: http://webpack.github.io/docs/configuration.html#module-loaders 82 | * List: http://webpack.github.io/docs/list-of-loaders.html 83 | * This handles most of the magic responsible for converting modules 84 | */ 85 | 86 | // Initialize module 87 | config.module = { 88 | preLoaders: [], 89 | loaders: [{ 90 | // JS LOADER 91 | // Reference: https://github.com/babel/babel-loader 92 | // Transpile .js files using babel-loader 93 | // Compiles ES6 and ES7 into ES5 code 94 | test: /\.js$/, 95 | loader: 'babel?optional[]=runtime', 96 | exclude: /node_modules/ 97 | }, { 98 | // ASSET LOADER 99 | // Reference: https://github.com/webpack/file-loader 100 | // Copy png, jpg, jpeg, gif, svg, woff, woff2, ttf, eot files to output 101 | // Rename the file using the asset hash 102 | // Pass along the updated reference to your code 103 | // You can add here any file extension you want to get copied to your output 104 | test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/, 105 | loader: 'file' 106 | }, { 107 | // HTML LOADER 108 | // Reference: https://github.com/webpack/raw-loader 109 | // Allow loading html through js 110 | test: /\.html$/, 111 | loader: 'raw' 112 | }] 113 | }; 114 | 115 | // ISPARTA LOADER 116 | // Reference: https://github.com/ColCh/isparta-instrumenter-loader 117 | // Instrument JS files with Isparta for subsequent code coverage reporting 118 | // Skips node_modules and files that end with .test.js 119 | if (TEST) { 120 | config.module.preLoaders.push({ 121 | test: /\.js$/, 122 | exclude: [ 123 | /node_modules/, 124 | /\.test\.js$/ 125 | ], 126 | loader: 'isparta-instrumenter' 127 | }) 128 | } 129 | 130 | // CSS LOADER 131 | // Reference: https://github.com/webpack/css-loader 132 | // Allow loading css through js 133 | // 134 | // Reference: https://github.com/postcss/postcss-loader 135 | // Postprocess your css with PostCSS plugins 136 | var cssLoader = { 137 | test: /\.css$/, 138 | // Reference: https://github.com/webpack/extract-text-webpack-plugin 139 | // Extract css files in production builds 140 | // 141 | // Reference: https://github.com/webpack/style-loader 142 | // Use style-loader in development for hot-loading 143 | loader: ExtractTextPlugin.extract('style', 'css?sourceMap!postcss') 144 | }; 145 | 146 | // Skip loading css in test mode 147 | if (TEST) { 148 | // Reference: https://github.com/webpack/null-loader 149 | // Return an empty module 150 | cssLoader.loader = 'null' 151 | } 152 | 153 | // Add cssLoader to the loader list 154 | config.module.loaders.push(cssLoader); 155 | 156 | /** 157 | * PostCSS 158 | * Reference: https://github.com/postcss/autoprefixer-core 159 | * Add vendor prefixes to your css 160 | */ 161 | config.postcss = [ 162 | autoprefixer({ 163 | browsers: ['last 2 version'] 164 | }) 165 | ]; 166 | 167 | /** 168 | * Plugins 169 | * Reference: http://webpack.github.io/docs/configuration.html#plugins 170 | * List: http://webpack.github.io/docs/list-of-plugins.html 171 | */ 172 | config.plugins = [ 173 | // Reference: https://github.com/webpack/extract-text-webpack-plugin 174 | // Extract css files 175 | // Disabled when in test mode or not in build mode 176 | new ExtractTextPlugin('[name].[hash].css', { 177 | disable: !BUILD || TEST 178 | }), 179 | 180 | new webpack.ProvidePlugin({ 181 | $: "jquery", 182 | jQuery: "jquery" 183 | }) 184 | ]; 185 | 186 | // Skip rendering index.html in test mode 187 | if (!TEST) { 188 | // Reference: https://github.com/ampedandwired/html-webpack-plugin 189 | // Render index.html 190 | config.plugins.push( 191 | new HtmlWebpackPlugin({ 192 | template: './src/index.html', 193 | inject: 'body', 194 | minify: BUILD 195 | }) 196 | ) 197 | } 198 | 199 | // Add build specific plugins 200 | if (BUILD) { 201 | config.plugins.push( 202 | // Reference: http://webpack.github.io/docs/list-of-plugins.html#noerrorsplugin 203 | // Only emit files when there are no errors 204 | new webpack.NoErrorsPlugin(), 205 | 206 | // Reference: http://webpack.github.io/docs/list-of-plugins.html#dedupeplugin 207 | // Dedupe modules in the output 208 | new webpack.optimize.DedupePlugin(), 209 | 210 | // Reference: http://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin 211 | // Minify all javascript, switch loaders to minimizing mode 212 | new webpack.optimize.UglifyJsPlugin() 213 | ) 214 | } 215 | 216 | /** 217 | * Dev server configuration 218 | * Reference: http://webpack.github.io/docs/configuration.html#devserver 219 | * Reference: http://webpack.github.io/docs/webpack-dev-server.html 220 | */ 221 | config.devServer = { 222 | contentBase: './public', 223 | stats: { 224 | modules: false, 225 | cached: false, 226 | colors: true, 227 | chunk: false 228 | } 229 | }; 230 | 231 | return config; 232 | }; -------------------------------------------------------------------------------- /webpack.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for tests 3 | */ 4 | module.exports = require('./webpack.make')({ 5 | BUILD: false, 6 | TEST: true 7 | }); 8 | --------------------------------------------------------------------------------