Setting Up Two-Way Data Binding with ng-model in AngularJS 1.x Forms
A practical guide to wiring ng-model two-way binding in AngularJS 1.x forms: the dot rule, input types, $apply for external updates, and digest-cycle performance limits.
14 Jul 2025, 03:17 UTC

What you're building and why
You have an AngularJS 1.x form where the input fields and the JavaScript model must stay in sync: when the user types, the scope property updates; when your code changes the scope property, the input updates. The ng-model directive does exactly this by wiring a bidirectional link between a form control and a scope variable, with updates propagated through AngularJS's digest cycle (its internal change-detection pass).
One important caveat before you start: AngularJS 1.x reached end of life in December 2021 and no longer receives security patches. This guide is for maintaining existing applications. New projects should use a supported framework.
Prerequisites
- An existing AngularJS 1.x application (this guide assumes 1.6+ behavior; earlier 1.x versions behave the same for basic
ng-modelusage). - A controller attached to the view, or at minimum
ng-appon the page. - Browser DevTools for verification.
Procedure: bind an input to the scope
Declare the model on the scope in your controller, then reference it with ng-model on the input:
<div ng-app="app" ng-controller="UserCtrl">
<input type="text" ng-model="user.name">
<p>Hello, {{ user.name }}</p>
</div>angular.module('app', [])
.controller('UserCtrl', function ($scope) {
$scope.user = { name: '' };
});Two details matter here. First, bind to a property of an object (user.name), not a bare primitive (name). Child scopes created by directives like ng-if or ng-repeat can shadow primitives and silently break the binding — the classic "always use a dot" rule. Second, the interpolation {{ user.name }} is your free verification display: it proves the model is updating without any extra code.
Other input types
ng-model adapts to the control type:
<input type="checkbox" ng-model="user.active"> Active
<input type="radio" ng-model="user.plan" value="pro"> Pro
<select ng-model="user.role" ng-options="r for r in roles"></select>The checkbox binds a boolean, the radio binds the value of the selected option, and the select binds whichever option matches. If you need a non-string value on a radio or option, use ng-value instead of value so the expression is evaluated rather than treated as literal text.
Updating the model from code
Inside AngularJS-aware code (controllers, $http callbacks, $timeout), just assign the property — the next digest cycle refreshes the view:
$scope.user.name = 'Ada';If you mutate the scope from outside AngularJS's context — a native DOM event, a third-party plugin callback, setTimeout — the digest cycle doesn't run and the view goes stale. Wrap the change:
somePlugin.on('change', function (val) {
$scope.$apply(function () {
$scope.user.name = val;
});
});Risk: calling $apply while a digest is already running throws a $digest already in progress error. If you're unsure, use $timeout(fn) or $scope.$evalAsync(fn), which schedule safely.
Expected checks
- Type in the input and confirm the
{{ user.name }}paragraph updates on each keystroke. - In DevTools console, run
angular.element(document.querySelector('[ng-controller]')).scope().userand confirm the property matches the input. (This debug helper requires debug info to be enabled; if your app calls$compileProvider.debugInfoEnabled(false), re-enable it in development.) - Toggle the checkbox and confirm
user.activeflips betweentrueandfalse.
Performance limitation worth knowing
Every ng-model and interpolation registers a watcher, and each digest cycle re-evaluates all of them. A page with thousands of bindings can feel sluggish on input. If profiling shows digest cost is the problem, reduce bindings with one-time binding ({{ ::staticValue }}) for read-only data, or debounce model updates with ng-model-options="{ debounce: 300 }" (available in AngularJS 1.3+).
Recovery
Nothing here changes state outside the page, so recovery is simply reverting your markup. If a binding misbehaves after changes, check the two most common causes first: a shadowed primitive (missing dot in the model path) and a scope mutation made outside AngularJS context without $apply.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.