Reacting to changes in AngularJS

by Michael Hladky

3 Parts

Watchers

Events

Channels

Code sections

Service

View

Controller


.service('valueService', function() {
    var val =  'value';
    
    var getValue = function() {
        return val;
    };
			   
    var setValue = function(newValue) {
        return val = newValue;
    };
			
    return {
        getValue: getValue,
        setValue: setValue
    };
		
});

Service


<div ng-controller="parentCtrl">
							
    <div ng-controller="child1Ctrl">
        {{data.val}}
        <input type="text" ng-model="data.val">
        <button ng-click="setValue(data.val)">Set</button>
    </div>
    		
    <div ng-controller="child2Ctrl">
        {{data.val}}
        <input type="text" ng-model="data.val">
        <button ng-click="setValue(data.val)">Set</button>
    </div>
    
    //...
			
</div>

View


    .controller('parentCtrl',function() { }) 
		
    .controller('child1Ctrl',function($scope, valueService) {
        
        $scope.val = valueService.getValue();
			
	$scope.setValue = function(newVlaue) {
	    valueService.setValue(newVlaue);
	};
			
    })
		
    .controller('child2Ctrl',function($scope, valueService) {
		
        $scope.val = valueService.getValue();
			
        $scope.setValue = function(newVlaue) {
	    valueService.setValue(newVlaue);
	};
			
    }) 

    //...

Controller

Controller-Tree || Scope-Tree

Rendered

ui-router states => controller tree 

Using $watch to react to changes

$watch

watchExpression (string, function)
is called on every digest, returns the watched value

listener (function)
Is called when new and old values are not equal

objectEquality (boolean)
Compare for object equality instead of reference equality

Returns a unregister function


var unregister = $scope.$watch(
    'val', 
    function(newValue, oldValue) {
        //some logic
        if (newValue !== oldValue) {
            //actions
            $scope.myVal = newValue;	
        }
    }
);
unregister = $scope.$watch(watchExpression, listener, [objectEquality]);

.controller('parentCtrl',function() { }) 

.controller('child1Ctrl',function($scope, valueService) {

	$scope.val = valueService.getValue();
	
	$scope.setValue = function(newVlaue) {
		valueService.setValue(newVlaue);
	};
	
	$scope.$watch(
            valueService.getValue, 
            function(newValue, oldValue) {
	        if (newValue !== oldValue) {
	    	    $scope.val = valueService.getValue();
	        }
	});

}) 

.controller('child2Ctrl', function($scope, valueService) { 
	
	//same here
	
});

Controller


.service('valueService', function() {

	var val =  'Value';
	
	var getValue = function() {
  		return val;
  	};
    
  	var setValue = function(newValue) {
  		val = newValue;
  	};

  	return {
	  getValue: getValue,
	  setValue: setValue
  	};

});

Service

Should we use $watch for this purpose?

Very inefficient

Hard to test

Bad Performance

Using events to react to changes

Methods

$emit (publish)

$broadcast (publish)

$on (subscribe)

$emit

$broadcast

$rootScope.$broadcast

				
.controller('child1Ctrl',function($scope, valueService) { 
		
    $scope.val = 'test';
			
    $scope.setValue = function(newValue) {
        valueService.setValue(newVlaue);
    }
		
    var onValueChangedHandler = function (value) {
        if(value !== $scope.data.val) {
            $scope.data.val = valueService.getValue(); 
	}   	
    }
		
    $scope.$on('valueChanged', function(event, args) {
        onValueChangedHandler(args.value);
    });
		
}) 
	
.controller('child2Ctrl',function(valueService) {

    //same here
}) 

Controller


.service('valueService', function($rootScope) {

	var val =  'Value';
		
	var getValue = function() {
	    return val;
	};
	    
	var setValue = function(newValue) {
	val = newValue;
        var args = {value: val};
        $rootScope.$broadcast('valueChanged', args);
    };
	
    return {
        getValue: getValue,
        setValue: setValue
    };
	
})

Service

Is it really better now?

Debug on many places in the application

Easy to get unstructured

Event-Callback-Hell

Encapsulating the pub/sub-logic into a channel


.controller('child1Ctrl',
    function($scope, valueService) { 
	
    $scope.val = 'test';
		
    $scope.setValue = function(newValue) {
        valueService.setValue(newVlaue);
    }
	
    var onValueChangedHandler = function (value) {
        if(value !== $scope.val) {
            $scope.data.val = valueService.getValue(); 
        }   	
    }
	
    $scope.$on('valueChanged', 
        function(event, args) {
            onValueChangedHandler(args.value);
        }
    );
	
})

Controller


.service('valueService',
    function($rootScope) {
        
    var val =  'Value';
	
    var getValue = function() {
        return val;
    };
    
    var setValue = function(newValue) {
		val = newValue;
		var args = {value: val};
        $rootScope.$broadcast(
            'valueChanged', 
            args
        );
    };

    return {
        getValue: getValue,
        setValue: setValue
    };

})

Service

How to improve this?

Channel


//???

Controller

Service

Constants for the event names

Channel


.controller('child1Ctrl',
    function($scope, valueService, myEvents) { 
	
    $scope.val = 'test';
		
    $scope.setValue = function(newValue) {
        valueService.setValue(newVlaue);
    }
	
    var onValueChangedHandler = function (value) {
        if(value !== $scope.val) {
            $scope.data.val = valueService.getValue(); 
        }   	
    }
	
    $scope.$on(myEvents.valueChanged, 
        function(event, args) {
            onValueChangedHandler(args.value);
        }
    );
	
})

.service('valueService',
    function($rootScope, myEvents) {
        
    var val =  'Value';
	
    var getValue = function() {
        return val;
    };
    
    var setValue = function(newValue) {
	val = newValue;	
        var args = {value: val};
        $rootScope.$broadcast(
            myEvents.valueChanged, 
            args
        );
    };

    return {
        getValue: getValue,
        setValue: setValue
    };

})

.config('myEvents', function() {
    valueChanged : 'valueChanged'
});

Controller

Service

Channel for pub/sub the events

Channel


.controller('child1Ctrl',
    function($scope, valueService, myEvents) { 
	
    $scope.val = 'test';
		
    $scope.setValue = function(newValue) {
        valueService.setValue(newVlaue);
    }
	
    var onValueChangedHandler = function (value) {
        if(value !== $scope.val) {
            $scope.data.val = valueService.getValue(); 
        }   	
    }
	
    $scope.$on(myEvents.valueChanged, 
        function(event, args) {
            onValueChangedHandler(args.value);
        }
    );
	
})

.service('valueService',
    function($rootScope, myEvents) {
        
    var val =  'Value';
	
    var getValue = function() {
        return val;
    };
    
    var setValue = function(newValue) {
	val = newValue;
	var args = {value: val};
        $rootScope.$broadcast(
            myEvents.valueChanged, 
            args
        );
    };

    return {
        getValue: getValue,
        setValue: setValue
    };

})

.config('myEvents', function() {
    valueChanged : 'valueChanged'
});

.config('myEvents', function() {
	valueChanged : 'valueChanged'
});

.service('myChannel',
    function($rootScope, myEvents) {
	
    // Publish to value changed event
    var pubValueChanged = function (newVlaue) {};
    // Subscribe to value changed event
    var subValueChanged = function($scope, handler) {};
	
    return {
        publishValueChanged	: publishValueChanged,
        onValueChanged		: onValueChanged
    };
	
});

Controller

Service

Moving pub logic

Channel


.controller('child1Ctrl',
    function($scope, valueService, myEvents) { 
	
    $scope.val = 'test';
		
    $scope.setValue = function(newValue) {
        valueService.setValue(newVlaue);
    }
	
    var onValueChangedHandler = function (value) {
        if(value !== $scope.val) {
            $scope.data.val = valueService.getValue(); 
        }   	
    }
	
    $scope.$on(myEvents.valueChanged, 
        function(event, args) {
            onValueChangedHandler(args.value);
        }
    );
	
})

.service('valueService',
    function(myChannel) {
        
    var val =  'Value';
	
    var getValue = function() {
        return val;
    };
    
    var setValue = function(newValue) {
        val = newValue;
	myChannel.pubValueChanged(newValue);	
    };

    return {
        getValue: getValue,
        setValue: setValue
    };

})

.config('myEvents', function() {
    valueChanged : 'valueChanged'
});

.config('myEvents', function() {
	valueChanged : 'valueChanged'
});

.service('myChannel',
    function($rootScope, myEvents) {
	
    // Publish to value changed event
    var pubValueChanged = function (newVlaue) {
        var args = {value: val};
        $rootScope.$broadcast(
            myEvents.valueChanged, 
            args
        );
    };
    // Subscribe to value changed event
    var subValueChanged = function($scope, handler) {};
	
    return {
        publishValueChanged	: publishValueChanged,
        onValueChanged		: onValueChanged
    };
	
});

Controller

Service

Moving sub logic

Channel


.controller('child1Ctrl',
    function($scope, valueService, myChannel) { 
	
    $scope.val = 'test';
		
    $scope.setValue = function(newValue) {
        valueService.setValue(newVlaue);
    }
	
    var onValueChangedHandler = function (value) {
        if(value !== $scope.val) {
            $scope.data.val = valueService.getValue(); 
        }   	
    }

    myChannel
        .subValueChanged($scope, onValueChangedHandler);
	
})

.service('valueService',
    function(myChannel) {
        
    var val =  'Value';
	
    var getValue = function() {
        return val;
    };
    
    var setValue = function(newValue) {
        val = newValue;
	myChannel.pubValueChanged(newValue);	
    };

    return {
        getValue: getValue,
        setValue: setValue
    };

})

.config('myEvents', function() {
    valueChanged : 'valueChanged'
});

.config('myEvents', function() {
	valueChanged : 'valueChanged'
});

.service('myChannel',
    function($rootScope, myEvents) {
	
    // Publish to value changed event
    var pubValueChanged = function (newVlaue) {
        var args = {value: val};
        $rootScope.$broadcast(
            myEvents.valueChanged, 
            args
        );
    };
    // Subscribe to value changed event
    var subValueChanged = function($scope, handler) {
        $scope.$on(myEvents.valueChanged, 
            function(event, args) {
                handler(args.value);
            }
        );
    };
	
    return {
        publishValueChanged	: publishValueChanged,
        onValueChanged		: onValueChanged
    };
	
});

What's better now?

Good separation of concerns

Moved code away from the controller

Extentd/change the logic in a single place

Single place for debugging

Further options

Log fired events (debugging)

Split/merge something (events)

Debunk something (clicks, keydowns)

...

We can also communicate

between directives => can have a $scope

between services => $fakeScope = $rootScope.newScope()

Performance???

$rootScope.$emit

pub and sub on the $rootScope => sub returns unsub function

just pass in the callback from the subscribing $scope => ;-)

unsure after $scope destruction => memory leaks

$emit brings 100% more performance

Channel


.config('myEvents', function() {
    valueChanged : 'valueChanged'
});

.service('myChannel', function($rootScope, myEvents) {

    var publishValueChanged = function (newVlaue) {
        var args = {value: newVlaue};
	//$rootScope.$broadcast(myEvents.valueChanged, args);
	$rootScope.$emit(myEvents.valueChanged, args);
    };
	
    var onValueChanged = function($scope, handler) {
        //$scope.$on(myEvents.valueChanged, function(event, args) {
	var unsubToValueChanged = $rootScope.$on(myEvents.valueChanged, function(event, args) {
	    //;-)
	    handler(args.value);
        });
        //
        $scope.$on('$destroy', function() {
            //memory leaks
            unsubToValueChanged();
        });
    };

    return {
    	publishValueChanged	: publishValueChanged,
    	onValueChanged		: onValueChanged
    };
	
});

3 lines, 1 file, +100% => :-)

michael@hladky.at

Reacting to changes in AngularJs

By Michael Hladky

Reacting to changes in AngularJs

How to encapsulate your event pub/sub into a separate service

  • 955