How to change AngularJS data outside the scope?

Problem

After hours of frustrating searches I feel I need to submit my question here. I apologize in advance if this question is somehow answered before but none of my searches has helped so far. So here's my question:

My JavaScript code is creating an object, which is modified and monitored by AngularJS. On some events (like loading a previous setting of the object), I wish to change the properties of this object from outside the scope. The problem is that the inputs does not change...

Here's an example of how I wish to perform these changes:


HTML code:

<div ng-app="myApp" ng-controller="FirstCtrl">
<input type="number" ng-model="data.age">
<h1>{{data.age}}</h1>

<input type="button" value="Change to 20" ng-model="data" onclick="change()">

JavaScript Code:

var person = {
    age: 16
};

// Create module
var myApp = angular.module('myApp', []);
myApp.factory('Data', function() {
    return person;
});

function FirstCtrl($scope, Data) {
    $scope.data = Data;
}

function change() {
    person.age = 20;
}

When I now press the "Change to 20" button, nothing happens. How can I modify the person's age from the change function?

Problem courtesy of: Birger

Solution

MaxPRafferty's answer is correct - using a function in the scope is often the nicer way to do this - but there is another option. You can use the angular.element(...).scope() method to access an Angular scope from unrelated JavaScript. Select the top-level scope for the app by targeting the element that has the ng-app attribute specified, with something like in your click handler:

function change() {
    var appElement = document.querySelector('[ng-app=myApp]');
    var $scope = angular.element(appElement).scope();
    $scope.$apply(function() {
        $scope.data.age = 20;
    });
}

Try it out in this Fiddle.

Shaun just pointed out that Angular will only process any "watches" or "bindings" during a $digest() call. If you just modify the properties of the $scope directly, the changes may not be reflected immediately and you may gets bugs.

To trigger this you can call $scope.$apply() which will check for dirty scopes and update anything bound correctly. Passing a function that does the work inside $scope.$apply will allow Angular to catch any exceptions as well. This behaviour is explained in the documentation for Scope.

Solution courtesy of: Jeremy Banks

Discussion

Jeremy's answer is really good, though now Angular has changed, and will no longer work, unless you add this line of code:

$scope = $scope.$$childHead;

So, the changed function should look like this

function change() {
    var appElement = document.querySelector('[ng-app=myApp]');
    var $scope = angular.element(appElement).scope();
    $scope = $scope.$$childHead; // add this and it will work
    $scope.$apply(function() {
        $scope.data.age = 20;
    });
}
Discussion courtesy of: Aleksandrus

Simply use a short $timeout

var whatever = 'xyz';
$timeout(function(){
    $scope.yourModel.yourValue = whatever;
}, 0);

and you are done.

I tried all those $apply hacks from around the www before and they all didn't work. But this $timeout works always like a charme and in every case. All you need is a reference of your $scope and $timeout.

Wy and how it works:

// Imagine an angular buildin-function
angular.$queue = function(fn){
    $timeout(fn, 0);
}

It works basically like a queue. But be aware that it is async.

Discussion courtesy of: Steffomio

http://jsfiddle.net/MaxPRafferty/GS6Qk/

You want set your ng-click attribute to a function in your scope, as follows:

var person = {
    age: 16
};

// Create module
var myApp = angular.module('myApp', []);
myApp.factory('Data', function() {
    return person;
});

function FirstCtrl($scope, Data) {
    $scope.data = Data;
    $scope.update = function(){
        $scope.data.age = 20;
    }
}
Discussion courtesy of: MaxPRafferty

With $render method is called by the ng-model directive when the value has been modified outside the directive. Get new value by reading the $viewValue property. Look: https://jsfiddle.net/cesar_ade/g5ybs6ne/

ctrl.$render=function(){
    setSelected(ctrl.$viewValue || 'Not Sure');
}
Discussion courtesy of: César Alcívar

Using Jeremy Banks' perfect answer, done in one line, though I'm referencing the controller vs the app:

angular.element('[ng-controller=myController]').scope().$apply(function(x){ x.foo = "bar"; });
Discussion courtesy of: Bryan

This recipe can be found in it's original form on Stack Over Flow.