New to Rust? Grab our free Rust for Beginners eBook Get it free →
Two-Way Data Binding in AngularJS with ng-model

AngularJS two-way data binding keeps a form control and its model value synchronized through ng-model. The profile editor below proves both directions in a served browser: form edits update the preview, Save copies the model, later edits leave that copy unchanged, and Reset restores the initial values.
Keep the AngularJS boundary explicit
AngularJS support ended in January 2022, so this example is for maintaining an existing application rather than starting a new one.
For new work, choose a supported framework, while the AngularJS tutorial hub remains useful when you need to understand an older codebase.
Within the running page, two-way binding connects a view value to a model property before any server request exists. It does not send data.
Install and serve the example
In an empty directory with Node.js and npm, run the commands below to fetch available package releases without recreating an old dependency set.
npm init -y
npm install angular @playwright/test http-server
npm pkg set scripts.start="http-server . -p 4173 -c-1 --silent"
npm pkg set scripts.test="playwright test --reporter=line"
Create index.html, app.js, styles.css, and the test file shown below. Run npm run start, then open http://127.0.0.1:4173 to inspect the form and preview.
Bind the form and preview to one object
Each form control writes to vm.profile, and the preview reads that same object. A named form lets AngularJS expose validation state, so Save stays unavailable while the name is invalid.
<!doctype html>
<html lang="en" ng-app="profileApp">
<head>
<meta charset="utf-8">
<title>AngularJS profile editor</title>
<link rel="stylesheet" href="styles.css">
<script src="node_modules/angular/angular.min.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="ProfileController as vm" ng-cloak>
<main class="shell">
<form name="profileForm" ng-submit="vm.save(profileForm)" novalidate>
<label for="name">Display name</label>
<input id="name" name="name" ng-model="vm.profile.name" ng-minlength="3" required>
<p class="error" ng-if="profileForm.name.$touched && profileForm.name.$invalid">Enter a display name with at least three characters.</p>
<label for="role">Role</label>
<select id="role" ng-model="vm.profile.role">
<option>Developer</option>
<option>Designer</option>
<option>Product manager</option>
</select>
<label for="bio">Short bio</label>
<textarea id="bio" ng-model="vm.profile.bio"></textarea>
<button type="submit" ng-disabled="profileForm.$invalid">Save profile</button>
<button type="button" ng-click="vm.reset(profileForm)">Reset</button>
</form>
<section class="preview" aria-live="polite">
<h2>{{ vm.profile.name || 'Unnamed profile' }}</h2>
<p>{{ vm.profile.role }}</p>
<p>{{ vm.profile.bio }}</p>
</section>
<section ng-if="vm.savedProfile">
<pre data-testid="saved-output">{{ vm.savedProfile | json }}</pre>
</section>
</main>
</body>
</html>

Copy the model when you save
Copy vm.profile on Save so later form edits cannot mutate the saved result.
(function () {
'use strict';
angular.module('profileApp', []).controller('ProfileController', ProfileController);
function ProfileController() {
var vm = this;
var initial = {
name: 'Ava Patel',
role: 'Developer',
bio: 'Maintaining a customer dashboard built with AngularJS.'
};
vm.profile = copy(initial);
vm.savedProfile = null;
vm.save = function (form) {
if (!form.$invalid) vm.savedProfile = copy(vm.profile);
};
vm.reset = function (form) {
vm.profile = copy(initial);
vm.savedProfile = null;
form.$setPristine();
form.$setUntouched();
};
function copy(source) {
return { name: source.name, role: source.role, bio: source.bio };
}
}
}());
Follow a value through ng-model
When you type a valid value, ng-model updates vm.profile.name during the framework update cycle, and the preview expression reads that property without an event listener or DOM assignment. AngularJS initializes the input from the same model path.
When a timer changes a model value, it needs the same lifecycle to appear in the view. The AngularJS interval refresh tutorial shows the framework-aware update path.
Test the browser behavior
Write the browser test before you add the controller behavior. The watched red run in the execution receipt timed out while the page had no controls, and the same test passed after the form and controller existed.
const { test, expect } = require('@playwright/test');
test('keeps input, model, preview, saved copy, and reset state aligned', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Display name').fill('Nina Shah');
await page.getByLabel('Role').selectOption('Product manager');
await page.getByLabel('Short bio').fill('Maintaining a customer dashboard built with AngularJS.');
await expect(page.getByRole('heading', { name: 'Nina Shah' })).toBeVisible();
await page.getByRole('button', { name: 'Save profile' }).click();
const saved = page.getByTestId('saved-output');
await expect(saved).toContainText('Nina Shah');
await page.getByLabel('Display name').fill('Changed after save');
await expect(saved).toContainText('Nina Shah');
await expect(saved).not.toContainText('Changed after save');
await page.getByRole('button', { name: 'Reset' }).click();
await expect(page.getByLabel('Display name')).toHaveValue('Ava Patel');
await expect(saved).toHaveCount(0);
});
Run npm test after the files are in place. The tested run below passed from a fresh workspace using the author-matched CodeForGeek terminal prompt.

Binding is not persistence
This example saves a copied object in browser memory, so a page reload removes it. A production save needs an HTTP request, authorization, server-side validation, and durable storage.
Build and test that network boundary separately with the Angular POST request example. If the application is still large enough to maintain, the single-page application tutorial also helps you identify the routing and view layers that sit around a form like this.
Troubleshoot a stale preview
If template braces appear in the page, first confirm that node_modules/angular/angular.min.js loads over HTTP. If the preview does not reflect an edit, compare the ng-model path and the preview expression. They must read the same object property.
Use this editor to isolate the binding boundary before you debug a server save. That sequence gives you one observable result at a time and avoids blaming persistence for a form-model mismatch.




