├── .travis.yml ├── .gitignore ├── .jshintrc ├── .editorconfig ├── bower.json ├── package.json ├── LICENSE ├── gulpfile.js ├── dist ├── angular-component.min.js └── angular-component.js ├── README.md └── src └── angular-component.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.11" 5 | before_script: 6 | - npm install -g gulp 7 | script: gulp 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | 4 | ## OS X 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | ._* 10 | .Spotlight-V100 11 | .Trashes 12 | 13 | ## Windows 14 | Thumbs.db 15 | ehthumbs.db 16 | Desktop.ini 17 | $RECYCLE.BIN/ 18 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": false, 8 | "eqeqeq": true, 9 | "indent": 2, 10 | "loopfunc": true, 11 | "immed": true, 12 | "latedef": true, 13 | "newcap": true, 14 | "noarg": true, 15 | "quotmark": "single", 16 | "regexp": true, 17 | "undef": true, 18 | "unused": false, 19 | "trailing": false, 20 | "sub": true, 21 | "globals": { 22 | "angular": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-component.js", 3 | "version": "0.0.7", 4 | "main": "./dist/angular-component.min.js", 5 | "dependencies": { 6 | "angular": ">=1.3.0 <=1.5" 7 | }, 8 | "homepage": "https://github.com/toddmotto/angular-component", 9 | "authors": [ 10 | "Todd Motto " 11 | ], 12 | "description": "Angular component() polyfill for v1.3+", 13 | "moduleType": [], 14 | "keywords": [ 15 | "Angular", 16 | "Component", 17 | "Directives" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | "**/.*", 22 | "node_modules", 23 | "bower_components", 24 | "test", 25 | "tests" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-component", 3 | "version": "0.1.3", 4 | "main": "./dist/angular-component.min.js", 5 | "description": "Angular component() polyfill", 6 | "author": "@toddmotto", 7 | "license": "MIT", 8 | "homepage": "https://github.com/toddmotto/angular-component", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/toddmotto/angular-component.git" 12 | }, 13 | "devDependencies": { 14 | "gulp": "~3.9.0", 15 | "gulp-uglify": "~0.3.0", 16 | "gulp-rename": "~1.1.0", 17 | "gulp-clean": "^0.2.4", 18 | "gulp-plumber": "~0.6.2", 19 | "gulp-header": "^1.0.2", 20 | "gulp-jshint": "^1.6.1", 21 | "jshint-stylish": "^0.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2015 Todd Motto 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | jshint = require('gulp-jshint'), 3 | stylish = require('jshint-stylish'), 4 | header = require('gulp-header'), 5 | uglify = require('gulp-uglify'), 6 | plumber = require('gulp-plumber'), 7 | clean = require('gulp-clean'), 8 | rename = require('gulp-rename'), 9 | package = require('./package.json'); 10 | 11 | var paths = { 12 | output : 'dist/', 13 | scripts : [ 14 | 'src/angular-component.js' 15 | ], 16 | test: [ 17 | 'test/spec/**/*.js' 18 | ] 19 | }; 20 | 21 | var banner = [ 22 | '/*! ', 23 | '<%= package.name %> ', 24 | 'v<%= package.version %> | ', 25 | '(c) ' + new Date().getFullYear() + ' <%= package.author %> |', 26 | ' <%= package.homepage %>', 27 | ' */', 28 | '\n' 29 | ].join(''); 30 | 31 | gulp.task('scripts', ['clean'], function() { 32 | return gulp.src(paths.scripts) 33 | .pipe(plumber()) 34 | .pipe(header(banner, { package : package })) 35 | .pipe(gulp.dest('dist/')) 36 | .pipe(rename({ suffix: '.min' })) 37 | .pipe(uglify()) 38 | .pipe(header(banner, { package : package })) 39 | .pipe(gulp.dest('dist/')); 40 | }); 41 | 42 | gulp.task('lint', function () { 43 | return gulp.src(paths.scripts) 44 | .pipe(plumber()) 45 | .pipe(jshint()) 46 | .pipe(jshint.reporter('jshint-stylish')); 47 | }); 48 | 49 | gulp.task('clean', function () { 50 | return gulp.src(paths.output, { read: false }) 51 | .pipe(plumber()) 52 | .pipe(clean()); 53 | }); 54 | 55 | gulp.task('default', [ 56 | 'lint', 57 | 'clean', 58 | 'scripts' 59 | ]); 60 | -------------------------------------------------------------------------------- /dist/angular-component.min.js: -------------------------------------------------------------------------------- 1 | /*! angular-component v0.1.3 | (c) 2017 @toddmotto | https://github.com/toddmotto/angular-component */ 2 | !function(){function n(n,t){if(t&&"string"==typeof t)return t;if("string"==typeof n){var r=/^(\S+)(\s+as\s+(\w+))?$/.exec(n);if(r)return r[3]}}function t(){function t(t,r){function o(e){function o(n){var t=angular.isArray(n);return angular.isFunction(n)||t?function(r,o){return e.invoke(t?n:["$element","$attrs",n],this,{$element:r,$attrs:o})}:n}function a(n){var t={};for(var r in n){var e=n[r];if("<"===e.charAt(0)){var o=e.substring(1);i.unshift({local:r,attr:""===o?r:o})}else t[r]=e}return t}var i=[],l=a(r.bindings),u=[t],c=[];if(angular.isObject(r.require))for(var f in r.require)u.push(r.require[f]),c.push(f);return{controller:r.controller||angular.noop,controllerAs:n(r.controller)||r.controllerAs||"$ctrl",template:o(r.template||r.templateUrl?r.template:""),templateUrl:o(r.templateUrl),transclude:r.transclude,scope:l||{},bindToController:!!l,restrict:"E",require:u,link:{pre:function(n,t,r,e){function o(){l.$onChanges(f),f=void 0}function a(n,t,r,e){"function"==typeof l.$onChanges&&(f||(f={}),f[n]&&(r=f[n].currentValue),f[n]={currentValue:t,previousValue:r},e&&o())}for(var l=e[0],u=0;u [View live demo in v1.3.0](https://jsfiddle.net/vz53audf) 6 | 7 | angular-component.js is a `~1kb` script that adds `component()` support in Angular 1.5 to all Angular versions above `1.3`, so you can begin using the new method now. 8 | 9 | _Note_: you must include this script straight after `angular.js` and before your application code to ensure the `component()` method has been added to the `angular.module` global. 10 | 11 | > Read about the Angular 1.5 `component()` method [here](http://toddmotto.com/exploring-the-angular-1-5-component-method) 12 | 13 | ### Polyfill support 14 | 15 | This polyfill supports the following feature set of the `.component()` method: 16 | 17 | | Feature | Supports | 18 | |----------------------------------------------------------------|-----------| 19 | | `angular.module.component()` method | Yes | 20 | | `bindings` property | Yes | 21 | | `'E'` restrict default | Yes | 22 | | `$ctrl` controllerAs default | Yes | 23 | | `transclude` property | Yes | 24 | | `template` support | Yes | 25 | | `templateUrl` injectable for `$element` and `$attrs` | Yes | 26 | | One-way data-binding emulated | Yes | 27 | | Lifecycles: `$onInit`, `$onChanges`m `$postLink`, `$onDestroy` | Yes | 28 | | `require` Object for parent Component inheritance | Yes | 29 | | `$` prefixed properties such as `$canActivate` | Yes | 30 | 31 | ### Component method usage 32 | 33 | ```html 34 |
35 |
36 | 37 |
38 |
39 | 40 | 89 | ``` 90 | 91 | ## Installing with NPM 92 | 93 | ``` 94 | npm install angular-component --save 95 | ``` 96 | 97 | ## Installing with Bower 98 | 99 | ``` 100 | bower install angular-component.js --save 101 | ``` 102 | 103 | ## Manual installation 104 | Ensure you're using the files from the `dist` directory (contains compiled production-ready code). Ensure you place the script before the closing `` tag. 105 | 106 | ```html 107 | 108 | 109 | 110 | 111 | 112 | 113 | ``` 114 | 115 | ## Contributing 116 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using Gulp. 117 | 118 | ## Release history 119 | 120 | - 0.1.2 121 | - Version fixes 122 | - 0.1.1 123 | - Version fixes 124 | - 0.1.0 125 | - Add one-way data-binding and $onChanges lifecycle, stable release 126 | - 0.0.8 127 | - Add new lifecycle hooks ($onInit, $onDestroy, $postLink) 128 | - 0.0.7 129 | - Add `require` property from 1.5 stable, `restrict: 'E'` as default 130 | - 0.0.6 131 | - Add automated `$` prefixed properties to factory Object 132 | - 0.0.5 133 | - Add automatic function annotations 134 | - 0.0.4 135 | - Fix Array dependency annotations [#11](https://github.com/toddmotto/angular-component/issues/11) 136 | - 0.0.3 137 | - Fix bug caused by bugfix (aligns with new 1.5 changes) 138 | - 0.0.2 139 | - Bugfix isolate scope when `bindings` is omitted, short-circuit if .component() is already supported 140 | - 0.0.1 141 | - Initial release 142 | -------------------------------------------------------------------------------- /src/angular-component.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var ng = angular.module; 4 | 5 | function identifierForController(controller, ident) { 6 | if (ident && typeof ident === 'string') return ident; 7 | if (typeof controller === 'string') { 8 | var match = /^(\S+)(\s+as\s+(\w+))?$/.exec(controller); 9 | if (match) return match[3]; 10 | } 11 | } 12 | 13 | function module() { 14 | 15 | var hijacked = ng.apply(this, arguments); 16 | 17 | if (hijacked.component) { 18 | return hijacked; 19 | } 20 | 21 | function component(name, options) { 22 | 23 | function factory($injector) { 24 | 25 | function makeInjectable(fn) { 26 | var closure; 27 | var isArray = angular.isArray(fn); 28 | if (angular.isFunction(fn) || isArray) { 29 | return function (tElement, tAttrs) { 30 | return $injector.invoke((isArray ? fn : [ 31 | '$element', 32 | '$attrs', 33 | fn 34 | ]), this, { 35 | $element: tElement, 36 | $attrs: tAttrs 37 | }); 38 | }; 39 | } else { 40 | return fn; 41 | } 42 | } 43 | 44 | var oneWayQueue = []; 45 | 46 | function parseBindings(bindings) { 47 | var newBindings = {}; 48 | for (var prop in bindings) { 49 | var binding = bindings[prop]; 50 | if (binding.charAt(0) === '<') { 51 | var attr = binding.substring(1); 52 | oneWayQueue.unshift({ 53 | local: prop, 54 | attr: attr === '' ? prop : attr 55 | }); 56 | } else { 57 | newBindings[prop] = binding; 58 | } 59 | } 60 | return newBindings; 61 | } 62 | 63 | var modifiedBindings = parseBindings(options.bindings); 64 | 65 | var requires = [name]; 66 | var ctrlNames = []; 67 | if (angular.isObject(options.require)) { 68 | for (var prop in options.require) { 69 | requires.push(options.require[prop]); 70 | ctrlNames.push(prop); 71 | } 72 | } 73 | 74 | return { 75 | controller: options.controller || angular.noop, 76 | controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl', 77 | template: makeInjectable( 78 | !options.template && !options.templateUrl ? '' : options.template 79 | ), 80 | templateUrl: makeInjectable(options.templateUrl), 81 | transclude: options.transclude, 82 | scope: modifiedBindings || {}, 83 | bindToController: !!modifiedBindings, 84 | restrict: 'E', 85 | require: requires, 86 | link: { 87 | pre: function ($scope, $element, $attrs, $ctrls) { 88 | var self = $ctrls[0]; 89 | for (var i = 0; i < ctrlNames.length; i++) { 90 | self[ctrlNames[i]] = $ctrls[i + 1]; 91 | } 92 | if (typeof self.$onDestroy === 'function') { 93 | $scope.$on('$destroy', function () { 94 | self.$onDestroy.call(self); 95 | }); 96 | } 97 | var changes; 98 | function triggerOnChanges() { 99 | self.$onChanges(changes); 100 | changes = undefined; 101 | } 102 | function updateChangeListener(key, newValue, oldValue, flush) { 103 | if (typeof self.$onChanges === 'function') { 104 | if (!changes) { 105 | changes = {}; 106 | } 107 | if (changes[key]) { 108 | oldValue = changes[key].currentValue; 109 | } 110 | changes[key] = { 111 | currentValue: newValue, 112 | previousValue: oldValue 113 | }; 114 | if (flush) { 115 | triggerOnChanges(); 116 | } 117 | } 118 | } 119 | if (oneWayQueue.length) { 120 | var destroyQueue = []; 121 | for (var q = oneWayQueue.length; q--;) { 122 | (function(current){ 123 | self[current.local] = $scope.$parent.$eval($attrs[current.attr]); 124 | var unbindParent = $scope.$parent.$watch($attrs[current.attr], function (newValue, oldValue) { 125 | self[current.local] = newValue; 126 | updateChangeListener(current.local, newValue, oldValue, true); 127 | }); 128 | destroyQueue.unshift(unbindParent); 129 | var unbindLocal = $scope.$watch(function () { 130 | return self[current.local]; 131 | }, function (newValue, oldValue) { 132 | updateChangeListener(current.local, newValue, oldValue, false); 133 | }); 134 | destroyQueue.unshift(unbindLocal); 135 | })(oneWayQueue[q]); 136 | } 137 | $scope.$on('$destroy', function () { 138 | for (var i = destroyQueue.length; i--;) { 139 | destroyQueue[i](); 140 | } 141 | }); 142 | } 143 | if (typeof self.$onInit === 'function') { 144 | self.$onInit(); 145 | } 146 | }, 147 | post: function ($scope, $element, $attrs, $ctrls) { 148 | var self = $ctrls[0]; 149 | if (typeof self.$postLink === 'function') { 150 | self.$postLink(); 151 | } 152 | } 153 | } 154 | }; 155 | } 156 | 157 | for (var key in options) { 158 | if (key.charAt(0) === '$') { 159 | factory[key] = options[key]; 160 | } 161 | } 162 | 163 | factory.$inject = ['$injector']; 164 | 165 | return hijacked.directive(name, factory); 166 | 167 | } 168 | 169 | hijacked.component = component; 170 | 171 | return hijacked; 172 | 173 | } 174 | 175 | angular.module = module; 176 | 177 | })(); 178 | -------------------------------------------------------------------------------- /dist/angular-component.js: -------------------------------------------------------------------------------- 1 | /*! angular-component v0.1.3 | (c) 2017 @toddmotto | https://github.com/toddmotto/angular-component */ 2 | (function () { 3 | 4 | var ng = angular.module; 5 | 6 | function identifierForController(controller, ident) { 7 | if (ident && typeof ident === 'string') return ident; 8 | if (typeof controller === 'string') { 9 | var match = /^(\S+)(\s+as\s+(\w+))?$/.exec(controller); 10 | if (match) return match[3]; 11 | } 12 | } 13 | 14 | function module() { 15 | 16 | var hijacked = ng.apply(this, arguments); 17 | 18 | if (hijacked.component) { 19 | return hijacked; 20 | } 21 | 22 | function component(name, options) { 23 | 24 | function factory($injector) { 25 | 26 | function makeInjectable(fn) { 27 | var closure; 28 | var isArray = angular.isArray(fn); 29 | if (angular.isFunction(fn) || isArray) { 30 | return function (tElement, tAttrs) { 31 | return $injector.invoke((isArray ? fn : [ 32 | '$element', 33 | '$attrs', 34 | fn 35 | ]), this, { 36 | $element: tElement, 37 | $attrs: tAttrs 38 | }); 39 | }; 40 | } else { 41 | return fn; 42 | } 43 | } 44 | 45 | var oneWayQueue = []; 46 | 47 | function parseBindings(bindings) { 48 | var newBindings = {}; 49 | for (var prop in bindings) { 50 | var binding = bindings[prop]; 51 | if (binding.charAt(0) === '<') { 52 | var attr = binding.substring(1); 53 | oneWayQueue.unshift({ 54 | local: prop, 55 | attr: attr === '' ? prop : attr 56 | }); 57 | } else { 58 | newBindings[prop] = binding; 59 | } 60 | } 61 | return newBindings; 62 | } 63 | 64 | var modifiedBindings = parseBindings(options.bindings); 65 | 66 | var requires = [name]; 67 | var ctrlNames = []; 68 | if (angular.isObject(options.require)) { 69 | for (var prop in options.require) { 70 | requires.push(options.require[prop]); 71 | ctrlNames.push(prop); 72 | } 73 | } 74 | 75 | return { 76 | controller: options.controller || angular.noop, 77 | controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl', 78 | template: makeInjectable( 79 | !options.template && !options.templateUrl ? '' : options.template 80 | ), 81 | templateUrl: makeInjectable(options.templateUrl), 82 | transclude: options.transclude, 83 | scope: modifiedBindings || {}, 84 | bindToController: !!modifiedBindings, 85 | restrict: 'E', 86 | require: requires, 87 | link: { 88 | pre: function ($scope, $element, $attrs, $ctrls) { 89 | var self = $ctrls[0]; 90 | for (var i = 0; i < ctrlNames.length; i++) { 91 | self[ctrlNames[i]] = $ctrls[i + 1]; 92 | } 93 | if (typeof self.$onDestroy === 'function') { 94 | $scope.$on('$destroy', function () { 95 | self.$onDestroy.call(self); 96 | }); 97 | } 98 | var changes; 99 | function triggerOnChanges() { 100 | self.$onChanges(changes); 101 | changes = undefined; 102 | } 103 | function updateChangeListener(key, newValue, oldValue, flush) { 104 | if (typeof self.$onChanges === 'function') { 105 | if (!changes) { 106 | changes = {}; 107 | } 108 | if (changes[key]) { 109 | oldValue = changes[key].currentValue; 110 | } 111 | changes[key] = { 112 | currentValue: newValue, 113 | previousValue: oldValue 114 | }; 115 | if (flush) { 116 | triggerOnChanges(); 117 | } 118 | } 119 | } 120 | if (oneWayQueue.length) { 121 | var destroyQueue = []; 122 | for (var q = oneWayQueue.length; q--;) { 123 | (function(current){ 124 | self[current.local] = $scope.$parent.$eval($attrs[current.attr]); 125 | var unbindParent = $scope.$parent.$watch($attrs[current.attr], function (newValue, oldValue) { 126 | self[current.local] = newValue; 127 | updateChangeListener(current.local, newValue, oldValue, true); 128 | }); 129 | destroyQueue.unshift(unbindParent); 130 | var unbindLocal = $scope.$watch(function () { 131 | return self[current.local]; 132 | }, function (newValue, oldValue) { 133 | updateChangeListener(current.local, newValue, oldValue, false); 134 | }); 135 | destroyQueue.unshift(unbindLocal); 136 | })(oneWayQueue[q]); 137 | } 138 | $scope.$on('$destroy', function () { 139 | for (var i = destroyQueue.length; i--;) { 140 | destroyQueue[i](); 141 | } 142 | }); 143 | } 144 | if (typeof self.$onInit === 'function') { 145 | self.$onInit(); 146 | } 147 | }, 148 | post: function ($scope, $element, $attrs, $ctrls) { 149 | var self = $ctrls[0]; 150 | if (typeof self.$postLink === 'function') { 151 | self.$postLink(); 152 | } 153 | } 154 | } 155 | }; 156 | } 157 | 158 | for (var key in options) { 159 | if (key.charAt(0) === '$') { 160 | factory[key] = options[key]; 161 | } 162 | } 163 | 164 | factory.$inject = ['$injector']; 165 | 166 | return hijacked.directive(name, factory); 167 | 168 | } 169 | 170 | hijacked.component = component; 171 | 172 | return hijacked; 173 | 174 | } 175 | 176 | angular.module = module; 177 | 178 | })(); 179 | --------------------------------------------------------------------------------