Very Global Description
You have functions that must exist in the global scope. You want a way to encapsulate the functionality of these functions in a dependency injectable way so they are more testable. What is the correct way to do this?
Somewhat Specific Description
I have a situation where I have an existing javascript library that uses a lot of variables and function in the global scope. The library is a SCORM engine and part of the standard dictates that the functions must be available in the global scope (so user created content can access them).
However, there are a couple of places in various controllers that I must call them as well. At the moment I an just calling them, but this makes testing those lines difficult and seems to violate the angular mindset in general.
Plnkr
A plunk just illustrating how things are
now
var globalVariable = 1;
function globalFunction(){
return "cats";
}
var app = angular.module('plunker', []);
app.controller('MainCtrl', [ '$scope', function($scope) {
$scope.name = 'World';
$scope.cats = globalFunction(); //This line is hard to test and makes me feel dirty.
}]);
A plunk that wraps the function in a factory (maybe what is recommended in comments?)
var globalVariable = 1;
function globalFunction(){
return "cats";
}
var app = angular.module('plunker', []);
app.factory('angularFriendly', function(){
this.globalFunction = globalFunction;
return this;
});
app.controller('MainCtrl', ['$scope', 'angularFriendly',
function($scope, angularFriendly) {
$scope.name = 'World';
$scope.cats = angularFriendly.globalFunction();
}
]);
This is more or less what I mean in my original comment when I said I considered wrapping it in a service. I make the function injectable (which is good and is much more testable) but I am not sure it is good practice (because I have just moved the difficult to test code to a different place).
I prefer wrapping 3rd party non-Angular applications into value objects (which is functionally the same as using a factory and immediately calling it prior to injection, which feels cleaner).
var globalVariable = 1;
function globalFunction(){
return "cats";
}
var app = angular.module('plunker', []);
app.value('external', globalFunction);
app.controller('MainCtrl', ['$scope', 'external',
function($scope, external) {
$scope.name = 'World';
$scope.cats = external(); // Treat the dependency as a service to resolve into the scope
}
]);
Related
I have research through the internet with this popular error and I have found no solution to my problem.
What I have is a jQuery iframe post message function that receive strings from a different domain. When it get the string it will need to store it to Angular and save to the database. What I am having the trouble is, trying to update angular so that recognizes the changes.
So here is my code:
.controller('jobOrderController', function(Jobs, socketio) {
var vm = this;
var myImage;
$.receiveMessage(
function(e) {
myImage = e.data;
vm.$apply(function() {
vm.orderData.guideImage = e.data
});
},
'http://aaa.com'
);
vm.createOrders = function() {
vm.message = '';
Jobs.createOrders(vm.orderData)
.success(function(data) {
vm.orderData = '';
vm.message = data.message;
});
};
})
$.receiveMessage will listen for incoming string data and then when it receive it should just save it to my "controller, vm". I know that my message is being received as I can alert them.
I know that I am going it wrong but everything I read is using $scope.apply so I thought it would work the same way using "this". But it doesn't seem to be updated to angular.
I see that you are using john papa's guideline to avoid the use of $scope.
You just forgot to declare vm (standing for viewmodel) at the beginning of your controller:
var vm = this;
EDIT: the guideline also says :
"Consider using $scope in a controller only when needed. For example
when publishing and subscribing events using $emit, $broadcast, or
$on."
For $apply as well, you need to explicitly use $scope.$apply
You can just do this:
.controller('jobOrderController', function($scope, Jobs, socketio) {
var vm = this;
$.receiveMessage(
function(e) {
myImage = e.data;
$scope.$apply(function() {
vm.orderData.guideImage = e.data
});
},
'http://aaa.com'
);
});
The this works differently inside the Angular's controller. It's not always same as the $scope and $scope is the object which contains $apply method.
For $apply, you can not use this. $scope and this are different instance so this can not access $apply.
Check snippet:
angular.module('myApp', []).controller('MyCtrl', function($scope) {
var vm = this;
setTimeout(function() {
$scope.$apply(function() {
vm.text = 'Submit';
});
}, 0);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="MyCtrl as ctrl">
<button>{{ctrl.text}}</button>
</div>
The controllerAs syntax is just (currently) sugar for $scope.foo (if your controllerAs is set to foo). As a result, this will actually point at $scope.foo in this instance and not $scope, which is why you will be unable to invoke any of $scope's actions through this. To use any of those, you will have to explicitly use $scope.
The above answers all answer with a solution on how to fix the short-term problem, that is, you using $scope.$apply in the controller.
Honestly, the issue here isn't that you can't use $scope.$apply - I'm not sure exactly what you are trying to do but one of the main rules of Angular is that you have to do everything through Angular; $.receiveMessage is decidedly not Angular. Wrap your $.receiveMessage into a service; that service should also handle the $scope.$apply. This will help reduce code duplication and make your controller agnostic to the implementation of the service.
There may already be a library that exists that does this that avoids using the heavy requirement of jQuery.
I have three elements to this program. First, in the service, I have:
$scope.loaded = MySvc.loaded;
$scope.loaded = false;
Then, in the controller which imports this service, I have a series of introductory things. I need to trigger events in the directives once the controller is done with its asycnhronous work, so in the controller, when that trigger is ready, I do
MySvc.loaded = true;
And then in the directive, which also imports the service, I have,
$scope.loaded = MySvc.loaded;
$scope.$watch('loaded', function (newValue, oldValue) {
The directive triggers when loaded is initialized to 'false', but when I change the value to 'true', nothing happens. The watch simply does not trigger. Why not? How do I fix this?
This is a fine way to do things, and I view it as orthogonal to promises (which are indeed extremely useful). Your problem comes from breaking the linkage you've created with the assignment. Instead try:
$scope.data = MySvc.data;
And append to that, e.g. MySvc.data.loaded = false. This way the data variable is never reassigned, so your linkage between controller and service stays intact.
Then you can either watch data.loaded or watch data as a collection by passing true as the 3rd option to $watch.
As I said in comment, you may have problems with yours scopes overridding the loaded property.
Try using the angular events to solve your problem.
angular.module('test', [])
.controller 'MyController', ($scope, $timeout) ->
$timeout ->
$scope.$broadcast('READY')
, 2000
.directive 'myDirective', ->
scope: {}
template: '<span>{{ value }}</span>'
link: ($scope) ->
$scope.value = "I'm waiting to be ready..."
$scope.$on 'READY', ->
$scope.value = "I'm ready!!"
See this in action (CoffeeScript as it's faster for prototyping but should be clear enough).
You're making this complicated. Use promises instead!!! They're sweet. Promises keep state of their resolution.
.factory('hello_world', function ($q, $timeout) {
var promise = $q.defer;
$timeout(function () {
promise.resolve('hello world');
}, 1000)
return promise.promise;
})
.controller('ctrl', function ($scope, hello_world) {
hello_world.then(function (response) {
console.log("I will only be ran when the async event resolves!");
console.log(response);
});
});
As you can see promises are much better, no watches. No weird gates. Simpler, and sexy. ;)
I'm a junior developer and have been learning the ins and outs of Angular.js. I recently started using the resolve feature of the route provider to get my Project data from my service before the page loads. Before that I was calling my service inside the controller to get the data. My problem is that when I use resolve, the data gets to the controller just fine, but my scope doesn't take in my functions present at the bottom of the code and I'm not particularly sure why. If I move the function declarations to the top it works fine. My guess is that since the page now loads with the data, it doesn't have time to check the entire controller and instead just runs down it in order. Can anyone confirm why it's occurring and a solution so I can keep my code nice and readable? Thanks
My Route Provider
.config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/details/:id', {
templateUrl: '/HtmlViews/Details.html',
controller: 'detailController',
resolve: {
project: function (projectService, $rootScope, $route) {
$rootScope.loading = true;
return projectService.getProjectById($route.current.params.id);
}
}
});
}])
My Controller (stripped to just the important stuff) In it's state below, the program can't find function getDateAt in the scope when it needs to.
.controller('detailController', function ($scope, $http, $rootScope, projectService, project) {
$rootScope.loading = false;
$scope.id = project.ID;
$scope.project = project;
$scope.sprintCountRounded = projectService.roundSprintCount($scope.project.SprintCount, $scope.project.RoundUp);
// Check and alter data depending on if a start date is present on the project
$scope.isDateMissing = $scope.project.StartDate === undefined || $scope.project.StartDate === null;
if (!$scope.isDateMissing) {
$scope.startDate = $scope.getDateAt(0);
$scope.finalSprintStart = $scope.getDateAt($scope.sprintCountRounded);
$scope.finalSprintEnd = $scope.getDateAt($scope.sprintCountRounded + 1);
}
$scope.NumberOfLoggedSprints = $scope.project.Sprints.length;
$scope.getDateAt = function (sprintNum) {
return projectService.getDateAt(sprintNum, $scope.project.SprintDuration, $scope.project.StartDate);
}
});
JavaScript processes code 'top down' (somewhat), so to speak, as it is an interpreted language. So as your code is processed line by line, once it reaches the lines inside your conditional that have $scope.getDateAt(), it attempts to call the function on your scope object. But the problem is this: your function hasn't been defined yet, as JavaScript hasn't processed the line where your function is defined.
As you noticed, by placing your function definition at the top, it works perfectly fine, as that code is processed first. This typically happens when you declare inline functions.
The exception to this is explicit function definitions. Were you to do the following code instead, it would work regardless of where you place the getDateAt() function:
function getDateAt(sprintNum) {
return projectService.getDateAt(sprintNum, $scope.project.SprintDuration, $scope.project.StartDate);
}
Before this is marked as duplicate I've read quite of few similar questions, but all the answers I've found seem to use $scope, and after reading the documentation I'm not really sure I understand $scope, or why I'd use it in this situation.
I found this tutorial which describes how to do what I'm trying to do.
However, it's using an array of data. I just need one solid variable. In addition, I don't know why he's declaring an additional object to the factory service he creates; why not just use the factory as the object?
I was thinking I could do something like this, but I'm not sure if it will work or not.
Creating my factory/service:
var demoModule = angular.module("demoModule", []);
demoModule.factory("demoService", function() {
var demoSharedVariable = null;
return demoSharedVariable;
});
Accessing the shared variable in each controller:
var demoControllerOne = demoModule.controller("demoContollerOne", function(demoSharedVariable) {
this.oneFunction = function(oneInput){
demoSharedVariable = oneInput;
};
});
var demoControllerTwo = demoModule.controller("demoContollerTwo", function(demoSharedVariable) {
this.twoFunction = function(twoInput){
demoSharedVariable = twoInput;
};
});
Will this method produced the shared variable I'm after?
You need to inject the service in order to use it, then access the service variable.
demoModule.controller("demoContollerOne", function($scope, demoService) {
$scope.oneFunction = function(){
demoService.demoSharedVariable = $scope.oneInput;
};
});
demoModule.controller("demoContollerTwo", function($scope, demoService) {
$scope.twoFunction = function(){
demoService.demoSharedVariable = $scope.twoInput;
};
});
If you are using controllerAs, you rarely (or shouldn't) need to inject and use $scope. As controllerAs is a relatively newer feature, back then we have no choice but to use $scope, so it is not strange to find example with $scope.
Edit: If you are not using controllerAs (like in this example) you would need $scope to expose functions or variables to the view.
There are several place that are not correct I've found while fiddling with it, I'll edit the code. I don't know how to showcase the effect without using advanced concept like $watch, please provide your own fiddle if you don't understand.
Jsbin
One important thing is if you want to use angular, you have to understand the knowledge of scope.
Since neither you factory or controller is correct, i write a simple example for you to help you understand the service:
detail implementation in this plnkr:
service:
angular.module('myApp').service('MyService', [function() {
var yourSharedVariable; // Your shared variable
//Provide the setter and getter methods
this.setSharedVariable = function (newVal) {
yourSharedVariable = newVal;
};
this.getSharedVariable = function () {
return yourSharedVariable;
};
}
]);
controller:
myApp.controller('Ctrl2', ['$scope', 'MyService', '$window', function($scope, MyService, $window) {//inject MyService into the controller
$scope.setShared = function(val) {
MyService.setSharedVariable(val);
};
$scope.getShared = function() {
return MyService.getSharedVariable();
};
$scope.alertSharedVariable = function () {
$window.alert(MyService.getSharedVariable());
};
}]);
What scope is a function defined inside an angular js controller, part of?
.controller('MyCtrl', ['$scope', '$location', function($scope, $location) {
function log() {
console.log('Vanakkam Ulagam');
}
var functionToCall = 'log';
????????[functionToCall]();
}])
I tried MyCtrl, this and $window. I can access the function by namespacing it like so functions.print = function() and then using functions[functionToCall]().
I am more interested in accessing the function without namespacing it. Is it possible? If not, why?
i think this is solution for this question
var f;
var a = angular.module('MyApp',[]);
a.controller('MyCtrl', ['$scope', function ($scope) {
$scope.log=function(){
console.log('test');
}
f=$scope.log;
});
and this function useable to element html for example
<button ng-click='log()'></button>
or
<button onclick='f()'></button>
If you're just trying to access global functions in your controller code, window (or $window if it's properly injected) should work fine:
$scope.alert = function(text) { window.alert(text) }
To be extra sure this works in other environments you could manually bind it in a self-invoking anonymous function:
(function(global){ //make the global scope assigned to the variable global
app.controller('MyCtrl',['$scope',function($scope){
$scope.alert = global.alert;
}]);
)(this); //this is the global object when this is evaluated
If you want to do anything inside an expression on a DOM element (such as ng-click), it's important to realise that angular expressions are evaluated by Angular's own expression evaluator (namely through $scope.$eval). So if something is not attached to a scope, it cannot be used in an angular expression.
However, if you were to do something like attach the global object to the scope, then everything is fair game. This really isn't recommended (I think there's no real reason to do this, and its better to manually define functions you need), but :
$scope.global = window
Inside your controller definition should work. You can then do things like ng-click="global.alert('Yo SO!')" in your DOM without issues.
For better testability you could use $window (you said you tried it and it didn't work... are you sure you injected it properly? Look at the example in the documentation carefully). But like I said, if you're attaching the global object to the scope you're opening Pandora's Box.