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>
AngularJS profile editor showing a saved model after form updates
The served profile editor updates the preview and preserves a saved copy.

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.

Terminal output showing the AngularJS profile editor browser test passing
The browser test exercises binding, save isolation, and reset behavior.

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.

Pankaj Kumar
Pankaj Kumar

Pankaj Kumar is the founder and CEO of CodeForGeek, with more than 14 years in IT. He is an open-source enthusiast who enjoys sharing what he learns through CodeForGeek and YouTube, with a focus on Python, data analytics, machine learning, Angular, Node.js, and Kafka.

Articles: 335