AngularJS model introspection/reflection - angularjs

Is it possilbe to introspect/reflect of the model in Angular app, where you can change the scope and traverse it? Something like batarang have but that will allow to change the values.
If not is it possilbe to monkey patch Angular code (by including another script on the page) that will make it possilbe?

1. Accessing/Modyfing scope properties
Angular's $scope objects are plain old JS objects, so thay can be manipulated the standard way.
E.g. someScope.someProp retrieves the value of a property, while someScope.someProp = someValue sets the value of a property.
2. Letting Angular know
Modifying the object's properties is one thing - making Angular aware of the change is another.
Angular will not know about our modifications, until it runs a $digest cycle. If we want to apply the changes right away, we can explicitely trigger a $digest cycle, by using someScope.$apply().
3. Getting hold of a scope
In order to get a reference to the scope associated with a DOM element, we need to have a reference to the corresponding DOM Node object, wrap it in angular.element and execute its scope() method.
Something like this:
<body class="ng-scope">
var someScope = angular.element(document.body).scope();
Furthermore, if we want to access the $rootScope (the parent scope of all scopes), we can use the injector to inject the $rootScope service:
<html ng-app="myApp">
var injector = angular.element(document.documentElement).injector();
var rootScope = injector.get('$rootScope');
4. Traversing the scope-tree
Once we get hold of a scope object, we might want to traverse the scope-tree. Every scope has the following properties (among others):
$parent: The parent scope of this scope (if any).
$$nextSibling: The next sibling scope of this scope (if any).
(Sibling scopes are scopes that have the same $parent)
$$childHead: The first child scope of this scope (if any).
In order to traverse the branch of the scope-tree with someScope as its root:
var scopes = [someScope];
while (scopes.length) {
var scope = scopes.shift();
console.log(scope);
if (scope.$$nextSibling) { scopes.unshift(scope.$$nextSibling); }
if (scope.$$childHead) { scopes.unshift(scope.$$childHead); }
}
E.g. to traverse the whole scope-tree, you can use the following snippet:
var injector = angular.element(document.body).injector();
var rootScope = injector.get('$rootScope');
var scopes = [rootScope];
while (scopes.length) {
var scope = scopes.shift();
report(scope);
if (scope.$$nextSibling) { scopes.unshift(scope.$$nextSibling); }
if (scope.$$childHead) { scopes.unshift(scope.$$childHead); }
}
function report(scope) {
var str = '' + scope.$id;
while (scope.$parent) {
str = scope.$parent.$id + ' => ' + str;
scope = scope.$parent;
}
console.log(str);
}

Found the way, here is a code that change the scope outside of Angular:
var $scope = angular.element($('.section:eq(0)')).scope();
$scope.$apply(function() {
scope.color = "blue";
});
the angular.element().scope() will return scope and can be investigated. Not sure about how to traverse scope tree, but this is good starting point. If there is no scope traverse you could just traverse the DOM and check if there is new scope on that DOM element.

You can get the scope by calling the scope() method on an object returned by angular.element() (or $(), if you also use jQuery), e.g. angular.element(".foo").scope(). There's no direct way of accessing child scopes. But knowing that all elements that have a new scope associated with them have the ng-scope class, you can traverse the element tree instead.
$(".foo").find(".ng-scope").each(function() {
var scope = $(this).scope();
});
This, however can be a bit tricky, as find() will find all nodes below .foo, regardless of their depth. You could use children() instead but it may not yield any results as direct descendants of .foo may not create a new scope.

Related

Wait until require is ready within an angular controller

I need to require a property from a controller from another directive in my main directive, which is easy:
return {
...
require['myOtherController'],
link: function(scope,element,attrs,controller){
scope.propFromOtherCtrl = controller[0].propFromOtherCtrl;
}
But: the controller of my directive gets loaded first.
So at first, propFromOtherCtrl is undefined in the controller until the link function got executed.
Right now, i am $scope.$watching the property in my controller until it is defined and then kick off the initialization of my controller manually.
//directive controller
$scope.$watch(function(){return $scope.propFromOtherCtrl; },function(n,o){
if(n !== o){
// init
}
});
But that seems rather hacky. Is there a more elegant way of doing this?
I can not share the scope between the two directives directly because of the architecture of my app.
If the architecture is the only reason you cannot share the scope between two controllers, you can always use $controller to inherit one controller from the other, then you would have access to its scope regardless of the location in the html dom:
$controller('myOtherController', { $scope: $scope });
Another alternative is inserting the html part that triggers the directive into an ng-if that doesn't get initialized until the other controller is ready:
ng-if="propFromOtherCtrlLoaded"
Finally, in case neither of these suit you, using $watch is not that bad, except a small addition, stopping to listen to the changes, would make it more efficient:
var unreg = $scope.$watch(function(){return $scope.propFromOtherCtrl; },function(n,o){
if(n !== o){
// init
unreg();
}
});

Is there a way to get a scope of a DOM element when debug info is disabled?

I'm writing an directive which need to retrieve a scope of current DOM element. using the non public api angular.element().scope();
It works well until angular 1.3 introduces a new feature $compileProvider.debugInfoEnabled(false); which mainly aims to improve performance to avoid bind data in DOM element. But when debugInfoEnabled() is set to false, angular.element().scope() will return undefined. So I must find another way to get the scope of an DOM element or I have to redesign my code logic.
Is there a way to make this possible?
I just faced a similar problem in our application after compiling our app with $compileProvider.debugInfoEnabled(false);. I needed to later access some of our directive's isolate scope but couldn't use the isolateScope() method. To get around the problem, I created a helper function in a Utils service that looks like this:
this.setElementIsolateScope = function(element, scope) {
element[0].isolateScope = function() {
return scope;
};
};
Then inside any directive where I needed to be able to later access the isolate scope I called this function inside the link() function: Since element is a jqLite object, you need to set the isolateScope() function on element[0]. You should already have the jqLite wrapped element and scope already passed into your link function, which you then just pass to your service method.
Utils.setElementIsolateScope(element, scope);
To then access the isolate scope later, you would get a reference to your element and then do this (assuming child_element is the reference to your element/directive):
var child_iso_scope = _.isFunction(child_element.isolateScope) && child_element.isolateScope();
Depending on how you are getting the reference to your element, you may need to wrap it a jqLite wrapper like this:
child_element = angular.element(child_element);
And then just use the same way as above to get the isolate scope. Hope this helps!

Using AngularJs ControllerAs approach, How can I call a function in a parent controller from the child controller?

I know you may have seen this question before, but is there really an answer for it? I started to doubt it.
Simply, I am using controlleras syntax as recommended, I have no problem accessing parent controller members form within the view, but I cannot do it from the constructor function of my child scope. And here is some code from what I am having right now:
myapp.controller("ParentController", function() {
this.selectedItem = {
Id: 1,
name: 'item1'
};
this.setSelectedItem = function(item) {
this.selectedItem = item;
//do other stuff
}
});
myapp.controller("ChildController", function() {
this.onItemChanged = function(newItem) {
//How can I call the parent controller instance from here
}
});
Also please notice that I want to call the 'updateSelectedItem' function from my child controller in away that the 'this' keyword will refer to the parent controller instance not the child, because I want to change the parent controller instance, so how should I do this?
To answer your question as clearly as possible, you first must have a bit of background on how the Controller-As syntax actually works.
Using Controller-As does not mean that you are not using $scope. In reality, $scope still exists. Controller-As is shorthand which creates an object on $scope and attaches the properties assigned via this to that object. On the view side, this object is explicitly bound to all the controls. you could still reference $scope.vm.property. However, since $scope is implicit in this scenario, it is not necessary to create a dependency to it.
Accessing the properties of the vm object of the parent controller in a nested scenario is still possible, but only if each controller is referenced by a different name. If your objects are outerScope and innerScope, then inside the HTML template of innerScope, you can still refer to outerScope.someProperty.
If, however, all controllers are named the same (i.e. vm), then the only way to access the parent controller would be through a property of the child scope which is aliased to a $scope property, introducing the $scope dependency.
In practice, whenever you have a controller within another controller, it's much cleaner for the innermost item to be a directive which wraps its own content, and explicitly defines which variables it needs through an Isolate Scope. However, whenever this is not necessary, the fallback should be for inner controllers to be named uniquely from outer controllers.

How can I assign a scope to a DOM element from the link function?

I am trying to implement a directive that that functions in a similar but not identical manner to ng-repeat. The details are not important, but at the end of the process I am attempting to pass a child scope to newly created DOM elements.
// Clone template element
var newItem = template.clone();
// Create new scope for element
var childScope = scope.$new();
// Pass relevant file into scope
childScope[indexStr] = member;
// Push new scope into element. <----- How to do this bit?
// Push element onto parent
template.parent().append(newItem);
// Clean up a bit
Naivly I tried:
newItem.scope = childScope;
but that doesn't work. (It just overrides the newItem.scope function to the best of my knowledge)
I also tried
newItem.scope() = childScope;
but apparently that is an illegal construct in JS.
The documentation is somewhat sparse on this, but I will gladly RTFM if someone can point me in the right direction.
I think you are looking for angular $compile functionality. You can use $compile provider to assign scope to a DOM element.
In your case do:-
$compile(newItem)(childScope);
Compiles an HTML string or DOM into a template and produces a template function, which can then be used to link scope and the template together.

Overwriting $scope in controller will not reflect in the DOM

I am trying to persist the whole scope to a service so that it is available and already set up after user navigates away and then returns to the screen.
I have created a service and am storing the current scope there. Later I set the $scope variable passed into the controller with the one stored in my service, but the after inspecting the DOM, I see that it's bound scope is still the scope object that existed before replacing.
How can I replace the scope so that it will also be used for the DOM elements?
Thanks for any help!
the below code tries to see if the local scope variable is initialized and if so it sets the $scope to it, otherwise it continues and wires it all up normally. this.scope is a member variable defined and set in the controller's super class (not shown).
function xyzController($scope, stateService) {
_super.call(this, $scope, stateService);
if (this.scope.hasBeenInitialized) {
$scope = this.scope; // $scope is updated but the DOM's scope never changed
return;
}
$scope.hasBeenInitialized = true;
...
}
You could try:
if (this.scope.hasBeenInitialized) {
angular.extend($scope, this.scope);
return;
}
This would merge the values from this.scope onto your $scope without replacing the variable.
This won't work. Scope is wired up deep inside Angular. To give you an idea, on any element, you can call:
angular.element(someDomElement).scope();
And get its scope. It's really not do-able to replace scopes like you're trying to do. But the more immediate problem is you're just overwriting that particular variable. It's an object passed in. Imagine you have this code:
var myObject = { a: 1 };
function f(obj) {
obj = { a: 2 };
}
f(myObject);
Clearly this doesn't change myObject. It'll replace obj within your function, but the thing about scopes is they're set up for you for all the expression in your views (it's the this in any scope functions for example). You'd need to change it through and through, and I don't see a way to do that.

Resources