Reputation: 3569
Given json data provided by an angular service:
{
"editableData": {
"test": {
"value": "Never gonna give you up"
}
},
"hiddenData": {
"test": {
"value": "Never gonna let you down"
}
}
}
... I want to let the user modify editableData.test.value
, but continuously synchronise that value across to private hiddenData.test.value
. The part that has me stumped is the best way to ensure this update is always triggered when the first value is changed, and that it happens as soon as possible.
Real-world complications
Must work in IE8+ (Angular 1.2.x; es5-shim allowed), and not break or overcomplicate two-way binding.
The 'public' property is simultaneously used by multiple controllers, directives and intermediary services, so as per DRY it would be better to handle this in the core service than in every other place it's used.
It's undesirable/unmaintainable to implement a solution that would involve declaring rules for developers like 'whenever this property gets changed, you must remember to call xyz() afterwards to refresh any mirroring'.
The actual data structure is much larger, and may have multiple properties that must be mirrored. Any solution should be fairly easy to scale up.
Possible Solution 1: $rootScope.$watch()
Within the service, I could use $rootScope.$watch(funcThatRunsOnEveryDigest)
to fire on every $digest
and continually copy values across.
However, I'm uneasy because:
I know it can lead to issues when you start giving $rootScope
to services... I think this may be a situation that warrants it, but it feels like a sin.
This would run on every single $digest
, regardless of whether the properties have changed. But this would be the case with any watcher, right (that is, the watcher expression/function is always run)? And better than having dozens of watchers, one per mirrored property, each dirty checking?
Am I going to run into issues if editableData.test.value
isn't currently exposed on $scope
somewhere, but it gets modified behind-the-scenes by code running in response to some other user action, or resolution of an async op?
Possible Solution 2: Link by reference
Simply linking the properties by reference:
//linked at the parent obj containing the .value property
_data.hiddenData.test = _data.editableData.test;
There are still some further implications to consider, including how easily this reference may be broken, and that it's kind of 'sneaky' and seems like it could surprise maintenance devs.
Better answers or insights into implications I haven't considered greatly appreciated!
http://plnkr.co/edit/mOhFBFfKfqDiHBFEgEpH?p=preview
(function() {
"use strict";
angular.module("myApp", ["myServices"])
.controller("Controller1", ["$scope", "dataServiceFacade1", Controller1])
.controller("Controller2", ["$scope", "dataServiceFacade2", Controller2])
.controller("Controller3", ["$scope", "dataServiceCore", Controller3]);
angular.module("myServices", [])
.service("dataServiceCore", ["$rootScope", DataServiceCore])
.service("dataServiceFacade1", ["dataServiceCore", DataServiceFacade1])
.service("dataServiceFacade2", ["dataServiceCore", DataServiceFacade2]);
/* myApp controllers */
function Controller1($scope, dataServiceFacade1) {
$scope.data = dataServiceFacade1.data; //using facade1 which returns editableData.test.value as test1.value
}
function Controller2($scope, dataServiceFacade2) {
$scope.data = dataServiceFacade2.data; //using facade2 which returns editableData.test.value as test2.value
}
function Controller3($scope, dataServiceCore) {
$scope.data = dataServiceCore.data; //no facade, raw data straight from the core
$scope.isWatching = dataServiceCore.mirrorByRootScopeWatch; // for toggling the $rootScope.$watch on and off
$scope.isReferencing = dataServiceCore.mirrorByRef; // for toggling ref on and off
$scope.reset = dataServiceCore.reset;
}
/* myServices services */
function DataServiceCore($rootScope) {
var _data,
_isWatching,
_watcherDereg,
_isReferencing;
_init();
//##################################################
//# Mirroring by updating from within the service, #
//# listening to every digest... #
//##################################################
function _watcherFireOnEveryDigest() {
_data.hiddenData.test.value = _data.editableData.test.value; //mirroring the value
}
//_isWatching flag getter/setter
function _mirrorByRootScopeWatch(value) {
if(typeof value !== "undefined") {
_isWatching = value;
if(_isWatching) {
_mirrorByRef(false);
_watcherDereg = $rootScope.$watch(_watcherFireOnEveryDigest); //no listener function
} else if(typeof _watcherDereg === "function") {
_watcherDereg();
_watcherDereg = null;
}
}
return _isWatching;
}
function _mirrorByRef(value) {
if(typeof value !== "undefined") {
_isReferencing = value;
if(_isReferencing) {
_mirrorByRootScopeWatch(false);
//##################################################
//# Mirroring by creating reference from one prop #
//# to the other... #
//##################################################
_data.hiddenData.test = _data.editableData.test; //linking by ref
} else {
_data.hiddenData.test = JSON.parse(JSON.stringify(_data.hiddenData.test)); //set to a de-ref'd copy of itself
}
}
return _isReferencing;
}
function _init() {
if(_data) {
//if _data already exists, merge (deep copy / recursive extend) so we update without breaking existing ref's
merge(_data, _getData());
} else {
_data =_getData();
}
_mirrorByRootScopeWatch(false);
_mirrorByRef(false);
}
//return a clone of the original data
function _getData() {
return JSON.parse(JSON.stringify({
"editableData": {
"test": {
"value": "Never gonna give you up"
}
},
"hiddenData": {
"test": {
"value": "Never gonna let you down"
}
}
}));
}
//merge function adapted from angular.merge (angular 1.4+) as per http://stackoverflow.com/a/29003438/446030
function merge(dst){
var slice = [].slice;
var isArray = Array.isArray;
function baseExtend(dst, objs, deep) {
for (var i = 0, ii = objs.length; i < ii; ++i) {
var obj = objs[i];
if (!angular.isObject(obj) && !angular.isFunction(obj)) continue;
var keys = Object.keys(obj);
for (var j = 0, jj = keys.length; j < jj; j++) {
var key = keys[j];
var src = obj[key];
if (deep && angular.isObject(src)) {
if (!angular.isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
baseExtend(dst[key], [src], true);
} else {
dst[key] = src;
}
}
}
return dst;
}
return baseExtend(dst, slice.call(arguments, 1), true);
}
return {
data: _data,
mirrorByRootScopeWatch: _mirrorByRootScopeWatch,
mirrorByRef: _mirrorByRef,
reset: _init
};
}
function DataServiceFacade1(dataServiceCore) {
var _data = {
"test1": dataServiceCore.data.editableData.test
};
return {
data: _data
};
}
function DataServiceFacade2(dataServiceCore) {
var _data = {
"test2": dataServiceCore.data.editableData.test
};
return {
data: _data
};
}
})();
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js@*" data-semver="1.2.28" src="https://code.angularjs.org/1.2.28/angular.js"></script>
<style type="text/css">
body {font: 0.9em Arial, Verdana, sans-serif;}
div {margin-bottom: 4px;}
label {margin-right: 8px;}
p {font-size: 0.9em; color: #999;}
code {color:#000; background-color: #eee}
pre code {display:block;}
</style>
<script src="script.js"></script>
</head>
<body>
<div ng-app="myApp">
<div ng-controller="Controller1">
<h4>Controller1</h4>
<p>This value is linked to <code>editableData.test.value</code> via
reference in its facade service.</p>
<label for="test1">test1.value</label>
<input ng-model="data.test1.value" id="test1" />
<pre><code>{{data|json}}</code></pre>
</div>
<div ng-controller="Controller2">
<h4>Controller2</h4>
<p>This value is <em>also</em> linked to
<code>editableData.test.value</code> via reference in its facade
service.</p>
<label for="test2">test2.value</label>
<input ng-model="data.test2.value" id="test2" />
<pre><code>{{data|json}}</code></pre>
</div>
<div ng-controller="Controller3">
<h4>Core Data</h4>
<p>'Mirroring' the value of <code>editableData.test.value</code> to
<code>hiddenData.test.value</code> by listening for every
<code>$rootScope.$digest</code> from within the service, and copying
between them.</p>
<p>Enable/Disable mirroring with the button below, and type in the
input fields above.</p>
<button ng-click="isWatching(!isWatching());"><strong>
{{isWatching() ? "Disable" : "Enable"}}</strong> mirroring with
<code>$rootScope.$watch</code></button>
<button ng-click="isReferencing(!isReferencing());"><strong>
{{isReferencing() ? "Disable" : "Enable"}}</strong> mirroring by ref
</button>
<button ng-click="reset()">Reset</button>
<pre><code>{{data|json}}</code></pre>
</div>
</div>
</body>
</html>
Update: Based on accepted answer, have made some further modifications to a fork of the Plnkr to play with encapsulating to a separate service. More complicated than necessary, really, so that I can still test out By Ref vs By $rootScope.$watch():
http://plnkr.co/edit/agdBWg?p=preview
Upvotes: 2
Views: 1616
Reputation: 5827
EDIT: It seems I misunderstood the question, since it was about how to watch for changes in the data object, you could add a separate service/controller whose ONLY job is to watch for the change. For me that sounds enough of a separation of concerns to make it quite ok. Here is an example using a DataWatcher controller.
function DataWatcher($scope, dataServiceCore) {
function _watcherHasChanged() {
return dataServiceCore.data.editableData;
}
function _watcherFireOnEveryChange() {
dataServiceCore.data.hiddenData.test.value = dataServiceCore.data.editableData.test.value; //mirroring the value
}
$scope.$watch(_watcherHasChanged, _watcherFireOnEveryChange, true);
}
http://plnkr.co/edit/PWPUPyEXGN5hcc9JANBo?p=preview
OLD ANSWER:
According to What is the most efficient way to deep clone an object in JavaScript? JSON.parse(JSON.stringify(obj))
is the most efficient way to clone an object. Which is essentially what you're trying to do here.
So something like
myObject.hiddenData = JSON.parse(JSON.stringify(myObject.editableData);
executed whenever editableData
is changed in myObject
might be what you're looking for.
Upvotes: 1