Using AngularJs to implement a $watch feature that enables two-way binding

I am in the process of designing a webpage that includes multiple range sliders that interact with each other based on their settings. Currently, I have a watch function set up that allows this interaction to occur one way. However, I am interested in having the watch function work both ways, but I'm not sure if there is a way to achieve this without creating individual functions for each value (there are approximately 12 sliders).

If you'd like to see what I'm trying to accomplish, check out my Plunker demonstration here: http://plnkr.co/edit/CXMeOT?p=preview

Answer №1

I have successfully found three different methods to achieve bidirectional binding of computed values. My focus was on solving the problem for the assets sliders, rather than the roa sliders. This decision was made based on the fact that assets involves a simple sum calculation, while roa is more complex and unrelated. (To @flashpunk: Feel free to reach out if you require further clarification on this topic.) The main challenge behind this task lies in the inherent imprecision of numerical values in JavaScript. When encountered with such imprecisions, Angular falls into an endless loop of recalculating changes triggered by these inaccuracies.

In essence, the issue at hand is the lossiness of numerical computations; these difficulties would not arise if the calculations were lossless.

Method 1 (visit plunkr for details)

This marks my initial solution, which implements a delicate locking mechanism to switch the primary data source based on the altered value. To strengthen this solution, I suggest developing an Angular module that extends Scope to support more sophisticated and intricate watchers. Should I encounter similar challenges in the future, I may consider initiating a project on GitHub; any updates will be reflected in this response.

// These functions can be substituted with utility functions from libraries like lodash or underscore.
var sum = function(array) { return array.reduce(function(total, next) { return total + (+next); }, 0) };
var closeTo = function(a, b, threshold) { return a == b || Math.abs(a - b) <= threshold; };

$scope.sliders = [
$scope.slide1 = {
  assets: 10,
  roa: 20
}, ... ];

$scope.mainSlide = {
  assets: 0,
  roa: 0
};
// A better method is recommended for managing this "locking" process,
// integrating it seamlessly within the digest cycle
var modifiedMainAssets = false;
var modifiedCollectionAssets = false;
$scope.$watch(function() {
      return sum($scope.sliders.map(function(slider) { return slider.assets; }))
    }, function(totalAssets) {
  if (modifiedCollectionAssets) { modifiedCollectionAssets = false; return; }
  $scope.mainSlide.assets = totalAssets;
  modifiedMainAssets = true;
});
$scope.$watch('mainSlide.assets', function(totalAssets, oldTotalAssets) {
  if (modifiedMainAssets) { modifiedMainAssets = false; return; }
  if (oldTotalAssets === null || closeTo(totalAssets, oldTotalAssets, 0.1)) { return; }

  // Note: Precision issues in floating-point math. Round & accept failures,
  // or utilize a library supporting proper IEEE 854 decimal types
  var incrementAmount = (totalAssets - oldTotalAssets) / $scope.sliders.length;
  angular.forEach($scope.sliders, function(slider) { slider.assets += incrementAmount; });
  modifiedCollectionAssets = true;
});

Method 2

In this revised version, the clunky locking mechanism is circumvented, and the watchers are consolidated using existing Angular capabilities. I employ individual slider values as the standard data source and recalculate the total depending on the magnitude of changes. Nevertheless, the built-in features fall short in adequately addressing the scenario, necessitating manual preservation of oldValues. (While the code could be refined, the need to perform the mentioned operation wasn't anticipated).

$scope.calculateSum = function() {
  return sum($scope.sliders.map(function(slider) { return slider.assets; }));
};
$scope.mainSlide.assets = $scope.calculateSum();
var modifiedMainAssets = false;
var modifiedCollectionAssets = false;
var oldValues = [$scope.mainSlide.assets, $scope.mainSlide.assets];
$scope.$watchCollection('[calculateSum(), mainSlide.assets]'
    , function(newValues) {
  var newSum = newValues[0];
  var oldSum = oldValues[0];
  var newAssets = newValues[1];
  var oldAssets = oldValues[1];
  if (newSum !== oldSum) {
    $scope.mainSlide.assets = newSum;
  }
  else if (newAssets !== oldAssets) {
    var incrementAmount = (newAssets - oldAssets) / $scope.sliders.length;
    angular.forEach($scope.sliders, function(slider) { slider.assets += incrementAmount; });
    $scope.mainSlide.assets = $scope.calculateSum();
  }
  oldValues = [$scope.mainSlide.assets, $scope.mainSlide.assets];
});

Method 3: Simplification

This approach reflects what should have been initially suggested. Employing a single canonical data source constituted by individual sliders. For the primary slider, a custom slider directive is fashioned, where events indicating alterations are reported instead of an ng-model usage. (Existing directives can be enhanced or branched to fulfill this purpose.)

$scope.$watch('calculateSum()', function(newSum, oldSum) {
    $scope.mainSlide.assets = newSum;
});
$scope.modifyAll = function(amount) {    
    angular.forEach($scope.sliders, function(slider) { slider.assets += amount; });
};

Revised HTML:

Assets: <span ng-bind="mainSlide.assets"></span>
<button ng-click="modifyAll(1)">Up</button>
<button ng-click="modifyAll(-1)">Down</button>

Evaluation

This appears to be a persistent issue that the Angular team has yet to tackle. Event-driven data binding frameworks (as seen in knockout, ember, and backbone) would navigate this obstacle simply by refraining from triggering change events pertaining to lossy computed changes; in Angular's instance, some modifications within the digest cycle are essential. Although the efficiency of my first two solutions remains uncertain without expert confirmation from Misko or another Angular authority, they prove effective.

For now, my preference leans towards Method 3. If this doesn't meet your requirements, opt for Method 2 and refine it using a lossless decimal or fraction representation, steering clear of the native JavaScript Number type (consider incorporating options like decimal or fractional). -- https://www.google.com/search?q=javascript+number+libaries

Answer №2

I think this is the answer you're looking for.

$scope.itemsToWatch = [$scope.slider1, $scope.slider2];
$scope.$watchCollection('itemsToWatch', function(updatedValues){...});

This method allows you to monitor all your slider-related variables under one watch function.

Answer №3

Is it a good idea to use a $watch in both directions? To avoid potential issues, consider setting up a factory function for the 12 different cases. It's important to ensure that your calculated values stabilize to prevent infinite recursion where each $watch triggers the other.

Based on limited information about slider dependencies, you could try something like the following approach:

function bindSliders() {

    var mainSlider = arguments[0];
    var childSliders = arguments.slice(1);

    angular.forEach(childSliders, function(slider) {

        // Forward Direction
        $scope.$watch(function() {
            return slider.roa + slider.assets;
        }, function (newValue, oldValue) {
            // Update mainSlider based on changes in child sliders
        });

        // Reverse Direction
        $scope.$watch(function() {
            return mainSlider.assets + mainSlider.roa;
        }, function (newValue, oldValue) {
            // Update current `slider` based on changes in main value
        });

    });
}

bindSliders($scope.mainSlide, $scope.slide1, $scope.slide2, $scope.slide3, $scope.slide4);

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for adjusting column positions in a table while maintaining a fixed header and utilizing both horizontal and vertical scrolling

Is there a way to display a table that scrolls both horizontally and vertically, with the header moving horizontally but not vertically while the body moves in both directions? I have been able to shift the position of the columns, however, I am struggling ...

Exploring Vue and Webpack: Optimizing global variables for development and production environments

I have been using vue-cli to create my web application. Throughout the app, I am making use of an API by including it in various places like this: axios.post(API + '/sign-up', data).then(res => { // do something }); The API variable is a c ...

Using Angular to link Google Places API responses to a form: a guide on merging two different length objects with a shared key

I'm struggling with a key concept here regarding displaying the results of a places autocomplete query. I want to replace the types[0] name with more familiar terms such as suburb or city instead of sublocality_level_1 or administrative_area_level_1 ...

What is the best way to handle the select event for a jQuery UI autocomplete when there are images involved?

Looking for help with creating an autocomplete feature with images on this jsfiddle. Despite trying to capture the event when a user selects an image, it doesn't seem to work properly: $("#input").autocomplete({ //source: tags, so ...

Guide for building a Template-driven formArray in Angular

I am currently implementing a fixed number of checkboxes that are being bound using a for loop. <ul> <li *ngFor="let chk of checkboxes"> <input type="checkbox" [id]="chk.id" [value]="chk.value&q ...

Emberjs: Developing a self-focusing button with Views/Handlebar Helpers

Currently, I am using a Handlebars helper view that utilizes Em.Button (although it's deprecated) to create a Deactivate Button. This button is designed to focus on itself when it appears and clicking it triggers the 'delete' action. Additio ...

React Native has encountered an issue with an undefined object, resulting in an error

I have a JSON file with the following structure: { "main": { "first": { "id": "123", "name": "abc" }, "second": { "id": "321", "name": "bca" } } } Before making an AP ...

What is the best way to utilize a function that is housed within a service?

I am facing an issue where I need to access a function globally across controllers. The problem arises when trying to use the function defined within a service in another function. An error is thrown stating that the function cannot be found. Even after at ...

Transforming three items into an array with multiple dimensions

There are 3 unique objects that hold data regarding SVG icons from FontAwesome. Each object follows the same structure, but the key difference lies in the value of the prefix property. The first object utilizes fab as its prefix, the second uses far, and t ...

AngularJS: When the expression is evaluated, it is showing up as the literal {{expression}} text instead of

Resolved: With the help of @Sajeetharan, it was pinpointed that the function GenerateRef was faulty and causing the issue. Although this is a common query, I have not had success in resolving my problem with displaying {{}} to show the outcome. I am atte ...

What is the process for sending tinyEditory's content using ajax?

I recently incorporated the StfalconTinymceBundle into a Symfony2 project and it is functioning perfectly. However, I am encountering a problem when trying to submit the editor's content through AJAX. The editor's content does not seem to be bind ...

Encountering a 405 Error while attempting to perform a search on Twitter

I have been attempting to search Twitter using jQuery, but I am encountering an issue. The response only shows: https://api.twitter.com/1.1/search/tweets.json?q=django 405 (Method Not Allowed) XMLHttpRequest cannot load https://api.twitter.com/1.1/search/ ...

How can PHP information be transmitted to an Ajax response?

I stumbled upon this script online that I'm currently using to identify a visitor's web browser details. This script is triggered when I make an ajax request. At the end of the PHP script, there is an array, return array( 'userA ...

Tips for waiting for a Promise to resolve before returning a new Promise

In the process of developing a web application tailored for crafting retail signage, I have integrated Firestore to manage pertinent information about these signs. One specific functionality I am working on is enabling users to effortlessly remove all exis ...

$routeProvider is not redirecting to the specified path

I am attempting to modify the URL using $routeProvider in AngularJS. $routeProvider.when('/', { templateUrl : 'scripts/login/login.tpl.html', controller : 'LoginCtrl' }); $routeProvider ...

Using ng-repeat data to populate another function

I am looking to transfer the details of an order shown in an ng-repeat to another function within my controller. Currently, it works for the first item in the array. How can I extend this functionality to display any order currently visible on the page? W ...

Why does my array become empty once it exits the useEffect scope?

const [allJobs, setAllJobs] = useState([]); useEffect(() => { axios.get('http://localhost:3002/api/jobs') .then(res => setAllJobs(res.data)); allJobs.map((job, i) => { if (job.language.toLowerCas ...

Looking to replace the cursor on your computer?

Upon exploring the angular-ui-tree, I observed that items are equipped with a drag or move cursor, which is helpful for making items draggable. However, I am curious about how to change the cursor to a normal arrow instead. Can anyone provide guidance on ...

Dividing a string yields varying outcomes when stored in a variable compared to when it is displayed using console.log()

When the `$location` changes, a simple function is executed as shown below. The issue arises when the assignment of `$location.path().split("/")` returns `["browser"]` for `$location.path() == "/browser"`, but when run directly inside the `console.log`, ...

How can you define a variable within the .config service in Angular JS?

Here is the code snippet I have been working on: spa.factory("linkFactory", function() { var linkFactoryObject = {}; linkFactoryObject.currentLink = "home"; return linkFactoryObject; }); This piece of code essentially serves as a global varia ...