Lesson 5: Scope

The scope is an object that acts as a shared context between the view and the controller that allows these layers to exchange information related to the application model. Both sides are kept synchronized along the way through a mechanism called two-way data binding.

In this Lesson, we are going to cover the following topics:

  • Two-way data binding
  • Best practices using the scope
  • The $rootScope object
  • Broadcasting the scope

Two-way data binding

Traditional web applications are commonly developed through a one-way data binding mechanism. This means there is only a rendering step that attaches the data to the view. This is done with the following code snippet in the index.html file:

<input id="plate" type="text"/>
<button id="showPlate">Show Plate</button>

Consider the following code snippet in the render.js file:

var plate = "AAA9999";
$("#plate").val(plate);

$("#showPlate").click(function () {
  alert(plate);
});

What happens when we change the plate and click on the button? Unfortunately, nothing.

In order to reflect the changes on the plate, we need to implement the binding in the other direction, as shown in the following code snippet (in the render.js file):

var plate = "AAA9999";
$("#plate").val(plate);

$("#showPlate").click(function () {
    plate = $("#plate").val();
    alert(plate);
});

Every change that occurs in the view needs to be explicitly applied to the model, and this requires a lot of boilerplate code, which means snippets of code that have to be included in many places just to keep everything synchronized. The highlighted sections in the following code snippet of the render.js file comprise the boilerplate code:

var plate = "AAA9999";
$("#plate").val(plate);

$("#showPlate").click(function () {
    plate = $("#plate").val();
    alert(plate);
});

To illustrate these examples, we used the jQuery library that can be easily obtained through its website at www.jquery.com.

With two-way data binding, the view and controller are always kept synchronized without any kind of boilerplate code, as we will learn in the next topics.

$apply and $watch

During the framework initialization, the compiler walks through the DOM tree looking for directives. When it finds the ngModel directive attached to any kind of input field, it binds its own scope's $apply function to the onkeydown event. This function is responsible for invoking the notification process of the framework called the digest cycle.

This cycle is responsible for the notification process by looping over all the watchers, keeping them posted about any change that may occur in the scope. There are situations where we might need to invoke this mechanism manually by calling the $apply function directly, as follows:

$scope.$apply(function () {
  $scope.car.plate = '8AA5678';
});

On the other side, the components responsible for displaying the content of any element present inside the scope use their scope's $watch function to be notified about the changes on it. This function observes whether the value of a provided scope property has changed. To illustrate the basic usage of the $watch function, let's create a counter to track the number of times the value of a scope property has changed. Consider the following code snippet in the parking.html file:

<input type="text" ng-model="car.plate" placeholder="What's the plate?"/>
<span>{{plateCounter}}</span>

Also, consider the following code snippet in the controllers.js file:

parking.controller("parkingCtrl", function ($scope) {
  $scope.plateCounter = -1;
  
  $scope.$watch("car.plate", function () {
    $scope.plateCounter++;
  });
});

Every time the plate property changes, this watcher will increment the plateCounter property, indicating the number of times it has changed. You may wonder why we are using -1 instead of 0 to initialize the counter, when the value starts with 0 in the view. This is because the digest cycle is called during the initialization process and updates the counter to 0.

To figure it out, we can use some parameters inside the $watch function to know what has changed. When the $watch function is being initialized, newValue will be equal to oldValue, as shown in the following code snippet (the controllers.js file):

parking.controller("parkingCtrl", function ($scope) {
  $scope.plateCounter = 0;
  
  $scope.$watch("car.plate", function (newValue, oldValue) {
    if (newValue == oldValue) return;
    $scope.plateCounter++;
  });
});

Best practices using the scope

The scope is not the model itself—it's just a way to reach it. Thus, the view and controller layers are absolutely free to share any kind of information, even those that are not related to the model, and they only exist to fulfill specific layout matters such as showing or hiding a field under a determined condition.

Be careful about falling into a design trap! The freedom provided by the scope can lead you to use it in a wrong way. Keep the following advice in mind:

"Treat scope as read-only inside the view and write-only inside the controller as possible."

Also, we will go through some important advice about using the scope:

Avoid making changes to the scope directly from the view

This means that though it is easy, we should avoid making changes to the scope by creating or modifying its properties directly inside the view. At the same time, we need to take care about reading the scope directly everywhere inside the controller.

The following is an example from the faq.html file where we can understand these concepts in more detail:

<button ng-click="faq = true">Open</button>
<div ng-modal="faq">
  <div class="header">
    <h4>FAQ</h4>
  </div>
  <div class="body">
    <p>You are in the Frequently Asked Questions!</p> 
  </div>
  <div class="footer">
    <button ng-click="faq = false">Close</button>
  </div>
</div>

In the previous example, we changed the value of the dialog property directly from the ngClick directive declaration. The best choice in this case would be to delegate this intention to the controller and let it control the state of the dialog, such as the following code in the faq.html file:

<button ng-click="openFAQ()">Open</button>
<div ng-modal="faq">
  <div class="header">
    <h4>FAQ</h4>
  </div>
  <div class="body">
    <p>You are in the Frequently Asked Questions!</p> 
  </div>
  <div class="footer">
    <button ng-click="closeFAQ()">Close</button>
  </div>
</div>

Consider the following code snippet in the controllers.js file:

parking.controller("faqCtrl", function ($scope) {
  $scope.faq = false;

  $scope.openFAQ = function () {
    $scope.faq = true;
  }

  $scope.closeFAQ = function () {
    $scope.faq = false;
  }
});

The idea to spread a variable across the whole view is definitely dangerous. It contributes to reducing the flexibility of the code and also increases the coupling between the view and the controller.

Avoid reading the scope inside the controller

Reading the $scope object inside the controller instead of passing data through parameters should be avoided. This increases the couple between them and makes the controller much harder to test. In the following code snippet of the login.html file, we will call the login function and access its parameters directly from the $scope object:

<div ng-controller="loginCtrl">
  <input 
    type="text" 
    ng-model="username" 
    placeholder="Username"
  />
  <input 
    type="password" 
    ng-model="password" 
    placeholder="Password"/>
  <button ng-click="login()">Login</button>
</div>

Consider the following code snippet in the controllers.js file:

parking.controller("loginCtrl", function ($scope, loginService) {
  $scope.login = function () {
    loginService.login($scope.username, $scope.password);
  }
});

Do not let the scope cross the boundary of its controller

We should also take care about not allowing the $scope object to be used far a way from the controller's boundary. In the following code snippet from the login.html file, there is a situation where loginCtrl is sharing the $scope object with loginService:

<div ng-controller="loginCtrl">
  <input 
    type="text" 
    ng-model="username" 
    placeholder="Username"
  />
  <input 
    type="password" 
    ng-model="password" 
    placeholder="Password"/>
  <button ng-click="login()">Login</button>
</div>

Consider the following code snippet in the controllers.js file:

parking.controller("loginCtrl", function ($scope, loginService) {
  $scope.login = function () {
    loginService.login($scope);
  }
});

Consider the following code snippet in the services.js file:

parking.factory("loginService", function ($http) {
  var _login = function($scope) {
    var user = {
      username: $scope.username, 
      password: $scope.password
    };
    return $http.post('/login', user);
  };
  
  return {
    login: _login
  };
});

Use a '.' inside the ngModel directive

The framework has the ability to create an object automatically when we introduce a period in the middle of the ngModel directive. Without that, we ourselves would need to create the object every time by writing much more code.

In the following code snippet of the login.html file, we will create an object called user and also define two properties, username and password:

<div ng-controller="loginCtrl">
  <input 
    type="text" 
    ng-model="user.username" 
    placeholder="Username"
  />
  <input 
    type="password" 
    ng-model="user.password" 
    placeholder="Password"
  />
  <button ng-click="login(user)">Login</button>
</div>

Consider the following code snippet of the controllers.js file:

parking.controller("loginCtrl", function ($scope, loginService) {
  $scope.login = function (user) {
    loginService.login(user);
  }
});

Consider the following code snippet of the services.js file:

services.js

parking.factory("loginService", function ($http) {
  var _login = function(user) {
    return $http.post('/login', user);
  };
  
  return {
    login: _login
  };
});

Now, the login method will be invoked just by creating a user object, which is not coupled with the $scope object anymore.

Avoid using scope unnecessarily

As we saw in Lesson 3, Data Handling, the framework keeps the view and the controller synchronized using the two-way data binding mechanism. Because of this, we are able to increase the performance of our application by reducing the number of things attached to $scope.

With this in mind, we should use $scope only when there are things to be shared with the view; otherwise, we can use a local variable to do the job.

The $rootScope object

The $rootScope object is inherited by all of the $scope objects within the same module. It is very useful and defines global behavior. It can be injected inside any component such as controllers, directives, filters, and services; however, the most common place to use it is through the run function of the module API, shown as follows (the run.js file):

parking.run(function ($rootScope) {
  $rootScope.appTitle = "[Packt] Parking";
});

Scope Broadcasting

The framework provides another way to communicate between components by the means of a scope, however, without sharing it. To achieve this, we can use a function called $broadcast.

When invoked, this function dispatches an event to all of its registered child scopes. In order to receive and handle the desired broadcast, $scope needs to call the $on function, thus informing you of the events you want to receive and also the functions that will be handling it.

For this implementation, we are going to send the broadcast through the $rootScope object, which means that the broadcast will affect the entire application.

In the following code, we created a service called TickGenerator. It informs the current date every second, thus sending a broadcast to all of its children (the services.js file):

parking.factory("tickGenerator", function($rootScope, $timeout) {
  var _tickTimeout;

  var _start = function () {
    _tick();
  };

  var _tick = function () {
    $rootScope.$broadcast("TICK", new Date());	
    _tickTimeout = $timeout(_tick, 1000);	
  };

  var _stop = function () {
    $timeout.cancel(_tickTimeout);
  };

  var _listenToStop = function () {
    $rootScope.$on("STOP_TICK", function (event, data) {
      _stop();
    });
  };

  _listenToStop();

  return {
    start: _start,
    stop: _stop
  };
});

Now, we need to start tickGenerator. This can be done using the run function of the module API, as shown in the following code snippet of the app.js file:

parking.run(function (tickGenerator) {
  tickGenerator.start();
});

To receive the current date, freshly updated, we just need to call the $on function of any $scope object, as shown in the following code snippet of the parking.html file:

{{tick | date:"hh:mm"}}

Consider the following code snippet in the controllers.js file:

parking.controller("parkingCtrl", function ($scope) {
  var listenToTick = function () {
    $scope.$on('TICK', function (event, tick) {
      $scope.tick = tick;
    });
  };
  listenToTick();
});

From now, after the listenToTick function is called, the controller's $scope object will start to receive a broadcast notification every 1000 ms, executing the desired function.

To stop the tick, we need to send a broadcast in the other direction in order to make it arrive at $rootScope. This can be done by means of the $emit function, shown as follows in the parking.html file:

{{tick | date:"hh:mm"}}
<button ng-click="stopTicking()">Stop</button>

Consider the following code snippet in the controllers.js file:

parking.controller("parkingCtrl", function ($scope) {
  $scope.stopTicking = function () {
    $scope.$emit("STOP_TICK");
  };

  var listenToTick = function () {
    $scope.$on('TICK', function (event, tick) {
      $scope.tick = tick;
    });
  };
  listenToTick();
});

Be aware that depending on the size of the application, the broadcast through $rootScope may become too heavy due to the number of objects listening to the same event.

There are a number of libraries that implement the publish and subscribe pattern in JavaScript. Of them, the most famous is AmplifyJS, but there are others such as RadioJS, ArbiterJS, and PubSubJS.

Scope Broadcasting
Scope Broadcasting
Scope Broadcasting
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.76.166