Direct answer
Use an isolate scope and never write to the bound property. Expose data to the parent with one-way input and an explicit output callback, and keep an internal working copy for all local edits.
- Define isolate scope:
scope: {} for a directive or use component().
- Prefer one-way input binding for data coming in. In AngularJS 1.5+ components use
<, otherwise emulate one-way with = plus a local copy.
- Initialize a local copy from the input on init and never mutate the bound reference directly:
$scope.local = angular.copy($scope.modelIn).
- Emit changes back to the parent only via an
& callback, e.g. onSave({value: $scope.local}). The parent decides whether to update its model.
If you must support a two-way API, keep the two-way binding read-only inside the directive and route all writes through the callback. Do not reference $parent or $rootScope in templates or controller logic.
Confirmed facts
- An isolate scope is created by
scope: {} or component(). It does not prototypically inherit parent properties, so assignments to unbound scope properties stay local.
- Binding symbols control data flow:
= two-way reference, @ string, & expression callback. AngularJS 1.5+ component bindings add < for one-way parent-to-component binding.
- AngularJS has no built-in runtime guard that throws when a directive writes to a parent scope.
$parent and $rootScope can still be accessed explicitly.
ngModelOptions controls when ngModel writes back, e.g. updateOn and debounce. It delays propagation timing but does not isolate object references.
Likely explanation for accidental mutation
Isolate scope prevents prototype inheritance, not reference sharing. With = or < binding to an object or array, the directive receives a reference to the same object. Mutating nested fields obj.prop = ... mutates the parent object because JavaScript objects are passed by reference. This is the common source of inadvertent parent changes.
Steps for this case
- Inspect the directive definition for
scope: {} and the bindings object. Confirm which symbols are used.
- Replace two-way
= inputs with one-way < where possible and add an explicit & output.
- In controller init or link, create a local working copy:
var vm = this; vm.local = angular.copy(vm.modelIn);. Use shallow copy for performance if only top-level fields change.
- Bind the template to
vm.local only. Never assign to vm.modelIn.
- On save/cancel, call the output callback with the local copy. Parent updates its model only when the callback runs.
- Verification: log object identity in parent and directive to confirm reference sharing is broken after copying. Temporarily swap
= for < + callback and assert parent data changes only on callback invocation.
Detection and tooling
There is no AngularJS compiler flag that warns on parent writes. Practical detection is code review and linting. Community ESLint rules such as eslint-plugin-angular can flag $parent and $rootScope usage. Unit tests that snapshot parent scope before and after directive interaction catch accidental writes.
ngModelOptions note
ngModelOptions can debounce or restrict when ngModel writes back to the model, which reduces the frequency of parent updates but does not prevent mutation of a shared object reference. It is not a substitute for copying and explicit callbacks.
One missing detail that changes the recommendation: which AngularJS version and whether the directive is a 1.5+ component or a classic directive. < one-way binding and component bindings are only available in 1.5+. If you are on an earlier version, one-way behavior must be emulated manually with = plus a copy.