├── LICENSE ├── README.md ├── angular-360-no-scope.js ├── bower.json ├── demo.html └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Chris Thielen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-360-no-scope 2 | 3 | ## What? 4 | ### `$watch` your controllerAs controller's data, without injecting $scope. 5 | 6 | ## How do I use it? 7 | 8 | - `[npm|bower] install angular-360-no-scope` 9 | - Include angular-360-no-scope.js in your app 10 | - Add a dependency on `angular-360-no-scope` to your app module. 11 | - Write your controller as usual, but avoid `$scope` 12 | - Utilize `this.$watch()` as needed 13 | 14 | ## Live Demo 15 | 16 | http://plnkr.co/edit/WzJCeB4CiEpkFyF81Zez?p=preview 17 | 18 | ## Why? 19 | 20 | ### Preface 21 | When using angular's controllerAs, a controller is given a name and a reference to the controller is placed on the $scope. When writing the logic for the controller, data is typically stored directly on the controller itself, not on the $scope. When referencing the data from a template, the data is namespaced by the controllerAs name. 22 | 23 | This provides various benefits, which help us write cleaner, more maintainable code. See the style guides by [Todd Motto](https://github.com/toddmotto/angularjs-styleguide#controllers) and [John Papa](https://github.com/johnpapa/angularjs-styleguide#controllers) for more details. 24 | 25 | ### Still need $scope capabilities 26 | One oddity of writing ControllerAs code is that we no longer tend to have the `$scope` reference handy. Besides being a tempting dumping ground for data, `$scope` also provides some important funtionality that we occasionally need such as `$watch` (and `$on`, `$broadcast`, and `$emit`). 27 | 28 | A simple mechanism to provide those functions is to inject `$scope` into the controller function. Then, in order to watch your controller data, you may do something like so: 29 | ```javascript 30 | $scope.$watch(function() { return ctrl.someData }, callback) 31 | ``` 32 | This is a little clunkier than watching scope data, i.e., `$scope.$watch("some.scope.variable", callback)`. Additionally, if you want to watch nested attributes whose parents may or may not be initialized, we might need to add yet another dependency on `$parse` so we must do something like: 33 | ```javascript 34 | $scope.$watch(function() { return $parse("some.controller.variable")(ctrl); }, callback); 35 | ``` 36 | 37 | ## 360-no-scope makes this easier 38 | 39 | With **360-no-scope**, your controllers are decorated, and augmented with a `$watch` function. The `$watch` function is bound to the controller instance. This allows you to write `ctrl.$watch("some.controller.variable", callback)`, much like the simple `$scope.$watch` you are already familiar with. 40 | 41 | ### Sample Controller 42 | 43 | ```javascript 44 | app.controller("MyController", function () { // HERE, no $scope is necessary 45 | var ctrl = this; 46 | ctrl.watchCount = 0; 47 | ctrl.foo = {}; 48 | 49 | // Here is the "scope-less" watch registration. Watching "ctrl.foo.bar.baz" 50 | ctrl.$watch("foo.bar.baz", callback); // <-- HERE, $watch something on the controller 51 | function callback (newVal, oldVal) { console.log("WatchCount: " + ctrl.watchCount++, newVal, oldVal); } 52 | } 53 | ``` 54 | 55 | ## How does it work? 56 | 57 | This lib decorates `$controllerProvider.register` and the `$controller` service. When a controller is registered with `$controllerProvider`, or when a controller is instantiated with the `$controller()` service, the controller fn passed in is augmented with a `$watch` function (as well as with `$on`, `$broadcast`, and `$emit`). 58 | 59 | **360-no-scope** augments the controller fn by wrapping it in a surrogate controller which is executed instead. The surrogate is annotated with the same injectable dependencies as the real controller fn. Then, $scope is added to the dependency list. When angular instantiates the controller surrogate, the surrogate always gets `$scope`. It then builds the `$scope` passthrough functions and adds them to the real controller's prototype. Finally, it instantiates and returns the real controller. 60 | 61 | 62 | -------------------------------------------------------------------------------- /angular-360-no-scope.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // https://gist.github.com/dfkaye/6384439 5 | function functionName(fn) { 6 | var f = typeof fn == 'function'; 7 | var s = f && ((fn.name && ['', fn.name]) || fn.toString().match(/function ([^\(]+)/)); 8 | return (!f && 'not a function') || (s && s[1] || 'anonymous'); 9 | } 10 | 11 | var app = angular.module('angular-360-no-scope', []); 12 | 13 | // Decorate both $controllerProvider.register and $controller service fn 14 | // Have them wrap the incoming controller definition in the makeController surrogate 15 | app.config([ '$controllerProvider', '$provide', function($controllerProvider, $provide) { 16 | var realRegister = $controllerProvider.register; 17 | 18 | $controllerProvider.register = function registerDecorator(name, constructor) { 19 | return realRegister(name, makeController(constructor)); 20 | }; 21 | 22 | $provide.decorator('$controller', ['$delegate', function($delegate) { 23 | return function $controllerDecorator() { 24 | if (arguments.length > 0 && typeof arguments[0] === 'function') { 25 | var constructor = arguments[0]; 26 | if (functionName(constructor) !== 'NgModelController') { 27 | arguments[0] = makeController(constructor); 28 | } 29 | } 30 | return $delegate.apply($delegate, arguments); 31 | } 32 | }]); 33 | }]); 34 | 35 | var injector = angular.injector(); 36 | 37 | // This is the heart of this implementation. This function wraps 38 | // a controller inside a surrogate controller function which will 39 | // add $watch capability on the controller. 40 | function makeController(ctrlImpl) { 41 | // Figure out what deps the ctrlImpl requires. 42 | // Append the two injectables we require 43 | var deps = injector.annotate(ctrlImpl).concat([ '$scope', '$injector', '$parse' ]); 44 | 45 | // This is the surrogate controller which is returned 46 | // It wraps the ctrlImpl the app developer defines and adds 47 | // the $watch passthrough. 48 | function surrogateController() { 49 | // When this is invoked later by angular, it will be injected with services/locals 50 | // Snag the injected objects 51 | var injected = arguments, instance = this, locals = {};; 52 | 53 | // We always inject $scope, $injector, and $parse. Snag those three things. 54 | var $scope = arguments[arguments.length - 3]; 55 | var $injector = arguments[arguments.length - 2]; 56 | var $parse = arguments[arguments.length - 1]; 57 | 58 | // Recreate a "locals" array (in case any locals were injected) 59 | // We map the 'deps' and 'injected' arrays back into an assoc-array 60 | for (var i = 0; i < deps.length; i++) { locals[deps[i]] = injected[i]; } 61 | 62 | // Add a $scope.$watch passthrough onto the ctrlImpl's prototype (and 63 | // therefore onto the surrogate controller's prototype as well) 64 | ctrlImpl.prototype.$watch = function(watchExpression, listener, objectEquality) { 65 | if (angular.isFunction(watchExpression)) { 66 | watchExpression = angular.bind(instance, watchExpression); 67 | } 68 | if (angular.isFunction(listener)) { 69 | listener = angular.bind(instance, listener); 70 | } 71 | if (angular.isString(watchExpression)) { 72 | // If watchExpression is a String, set up a $parse fn which evaluates against `instance` 73 | var getExpr = $parse(watchExpression); 74 | watchExpression = function() { return getExpr(instance); }; 75 | } 76 | 77 | return $scope.$watch.call($scope, watchExpression, listener, objectEquality); 78 | }; 79 | // Add a $scope.$watchCollection passthrough onto the ctrlImpl's prototype (and 80 | // therefore onto the surrogate controller's prototype as well) 81 | ctrlImpl.prototype.$watchCollection = function(watchExpression, listener) { 82 | if (angular.isFunction(watchExpression)) { 83 | watchExpression = angular.bind(instance, watchExpression); 84 | } 85 | if (angular.isFunction(listener)) { 86 | listener = angular.bind(instance, listener); 87 | } 88 | if (angular.isString(watchExpression)) { 89 | // If watchExpression is a String, set up a $parse fn which evaluates against `instance` 90 | var getExpr = $parse(watchExpression); 91 | watchExpression = function() { return getExpr(instance); }; 92 | } 93 | 94 | return $scope.$watchCollection.call($scope, watchExpression, listener); 95 | }; 96 | 97 | ctrlImpl.prototype.$on = function(name, listener) { 98 | if (angular.isFunction(listener)) { 99 | listener = angular.bind(instance, listener); 100 | } 101 | return $scope.$on.call($scope, name, listener); 102 | }; 103 | // Add some other $scope passthroughs to ctrlImpl prototype; just because. 104 | angular.forEach(['$broadcast', '$emit', '$apply'], function(fnName) { 105 | ctrlImpl.prototype[fnName] = function() { 106 | $scope[fnName].apply($scope, arguments); 107 | }; 108 | }); 109 | 110 | // This worked in 1.2.x but 1.3.0 introduced invoking controllers 'later' 111 | // return instance = $injector.instantiate(ctrlImpl, locals); 112 | 113 | // Finally, return the result of calling "ctrlImpl(injectedVars...)" with 114 | // `this` bound to the surrogate instance. This will let the controller code 115 | // use 'this' and apply it to the surrogate. 116 | $injector.invoke(ctrlImpl, instance, locals); 117 | } 118 | 119 | // Annotate the surrogateController surrogate function with required injectables 120 | surrogateController.$inject = deps; 121 | // Make the surrogate's protoype the ctrlImp's prototype because the surrogate 122 | // is what is actually returned and set into the $scope. 123 | surrogateController.prototype = ctrlImpl.prototype; 124 | 125 | // return the surrogate fn here 126 | return surrogateController; 127 | } 128 | })(); 129 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-360-no-scope", 3 | "version": "0.1.2", 4 | "authors": [ 5 | "Chris Thielen " 6 | ], 7 | "description": "", 8 | "keywords": [ 9 | "angular", "angularjs", "controllerAs" 10 | ], 11 | "license": "MIT", 12 | "main": "./angular-360-no-scope.js", 13 | "ignore": [ 14 | "**/.*", 15 | "pages", 16 | "build", 17 | "node_modules", 18 | "bower_components", 19 | "ui-router-versions", 20 | "src", 21 | "test", 22 | "tests", 23 | "Gruntfile.js", 24 | "files.js", 25 | "package.json", 26 | ".bower.json", 27 | "*.iml", 28 | "*.ipr", 29 | "*.iws" 30 | ], 31 | "dependencies": { 32 | "angular": "^1.2.0" 33 | }, 34 | "devDependencies": { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 39 | 40 | 41 | 42 | 43 |

angular-360-no-scope

44 | 45 |
46 | 50 | 51 | 52 | 53 |
54 | 55 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Christopher Thielen", 3 | "name": "angular-360-no-scope", 4 | "version": "0.1.2", 5 | "description": "$watch your controllerAs controller's data without injecting $scope", 6 | "homepage": "https://github.com/christopherthielen/angular-360-no-scope", 7 | "dependencies": { 8 | }, 9 | "devDependencies": { 10 | "angular": "^1.2.0" 11 | }, 12 | "main": "angular-360-no-scope.js", 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:christopherthielen/angular-360-no-scope.git" 16 | }, 17 | "bugs": "https://github.com/christopherthielen/angular-360-no-scope/issues" 18 | } 19 | --------------------------------------------------------------------------------