AngularJS Digest Cycle
The digest compares watched expressions and updates their consumers until the model stabilizes. Changing a field outside AngularJS does not guarantee a digest will run immediately.
How it works
This controlled demonstration attaches a native listener through a directive and changes a controller field without scheduling an AngularJS update. The separate Refresh view button runs an ordinary AngularJS event expression, which triggers the digest that reveals the earlier change. That stale intermediate display is intentional here so the mechanism is visible, not a recommended integration pattern.
Run the code
AngularJS 1.8.3 is a legacy framework whose official support ended in January 2022. These examples teach maintenance, not a recommendation for a new application. They need a separate local browser page; Bhylu has no AngularJS playground.
In an empty folder save the HTML below as digest-cycle.html and the server as lab-server.mjs. Make a vendor folder and save the four pinned libraries there, retaining their MIT notices. With Node 22 or later run node lab-server.mjs digest-cycle.html, then open http://127.0.0.1:8136/. Stop that lab before running a different lesson. The server supplies fixed read-only HTTP fixtures; no data is saved and it does not connect to SQL. No PHP, Apache or MySQL configuration changes are needed.
Example
<!doctype html>
<html lang="en" ng-app="repairLab" ng-strict-di>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>AngularJS Digest Cycle</title>
<style>
:root {
font:
18px/1.55 system-ui,
sans-serif;
color: #0f3464;
background: #fff;
color-scheme: light;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
main {
max-width: 50rem;
margin: auto;
padding: 1.2rem;
}
h1 {
font-size: 1.65rem;
}
label {
display: block;
margin: 0.6rem 0;
}
input,
select,
textarea,
button {
font: inherit;
max-width: 100%;
padding: 0.35rem;
margin: 0.2rem;
}
button {
border: 1px solid #034fa0;
border-radius: 0.25rem;
background: #034fa0;
color: white;
}
button:disabled {
opacity: 0.55;
}
output,
[role="status"] {
display: block;
min-height: 1.5rem;
}
table {
border-collapse: collapse;
width: 100%;
}
td,
th {
padding: 0.4rem;
border: 1px solid #a5bbd4;
text-align: left;
}
pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
:focus-visible {
outline: 3px solid #0078d4;
outline-offset: 3px;
}
[ng-cloak] {
display: none !important;
}
.marked {
font-weight: 700;
}
.error {
color: #a02424;
}
:root[data-theme="dark"] {
color: #d6e7fb;
background: #061d30;
color-scheme: dark;
}
:root[data-theme="dark"] .error {
color: #ffb4b4;
}
</style>
</head>
<body>
<main ng-controller="DeskController as vm" ng-cloak>
<h1>Community repair desk</h1>
<p>AngularJS 1.8.3 · Legacy maintenance lab · Local demonstration</p>
<button type="button" outside-change>Change outside AngularJS</button
><button type="button" ng-click="vm.refresh()">Refresh view</button>
<p data-result>{{ vm.count }}</p>
</main>
<script src="/vendor/angular.min.js"></script>
<script>
(function () {
"use strict";
var app = angular.module("repairLab", []);
app.directive("outsideChange", [
function () {
return {
link: function (scope, element) {
function change() {
scope.vm.count = 5;
}
element.on("click", change);
scope.$on("$destroy", function () {
element.off("click", change);
});
},
};
},
]);
app.controller("DeskController", [
function () {
var vm = this;
vm.count = 0;
vm.refresh = function () {};
},
]);
})();
</script>
</body>
</html>
Focus on the bindings and controller
This excerpt leaves out the document shell, styles and library files. Use the complete download to run it.
<button type="button" outside-change>Change outside AngularJS</button
><button type="button" ng-click="vm.refresh()">Refresh view</button>
<p data-result>{{ vm.count }}</p>
<script>
(function () {
"use strict";
var app = angular.module("repairLab", []);
app.directive("outsideChange", [
function () {
return {
link: function (scope, element) {
function change() {
scope.vm.count = 5;
}
element.on("click", change);
scope.$on("$destroy", function () {
element.off("click", change);
});
},
};
},
]);
app.controller("DeskController", [
function () {
var vm = this;
vm.count = 0;
vm.refresh = function () {};
},
]);
})();
</script>
Expected result
0
The first click leaves the paragraph at zero. The refresh reveals five, proving the model changed before the screen caught up. The next lesson repairs the integration with $applyAsync. If repeated digest passes never stabilize, investigate state-changing watchers rather than merely increasing the iteration limit.