AngularJS $http and logger Circular Dependency

Error

[$injector:cdep] Circular dependency found: $rootScope <- $http <- logger <- $exceptionHandler <- $rootScope <- $location <- routehelper

TLDR:

If you are looking for RequireJS's easy way to late-bind a dependency, here's how you do it in AngularJS.

var $http = angular.injector(['ng']).get('$http');

Story

I imagine this could be a fairly common scenario.

You implemented AngularJS, with a logger service.

 angular
     .module('blocks.logger')
     .factory('logger', logger);

 logger.$inject = ['$log'];
 function logger($log) {
        var service = {
            error   : error,
            log     : $log.log
        };
        return service;

        /////////////////////

        function error(message, data, title) {
            $log.error('Error: ' + message, data);
        }
}

You want to try to catch errors and log it back to your webservice
So add a REST call.

logger.$inject = ['$log', '$http'];
function logger($log, $http) {
    // snip.

    function error(message, data) {
        $log.error('Error: ' + message, data);

        var request = {
            method : 'POST',
            url: _spPageContextInfo.webServerRelativeUrl + '/_layouts/MyLogService.svc/LogMessage',
            data: {
                'log': message,
                'event': 'error',
                'meta': angular.toJson(data)
            },
            dataType: "json",
            headers: {
                "Content-Type": "application/json; charset=utf-8"
            }
        };
        $http(request);           
    }
}


Error!

[$injector:cdep] Circular dependency found: $rootScope <- $http <- logger <- $exceptionHandler <- $rootScope <- $location <- routehelper

I actually quite like this error.  It's telling me the dependency tree starting from routehelper and looping around to $rootScope

Bingle will tell you to re-architect your solution so you have proper separation of concerns.  But to be honest, I just want to log an error…

So here's how you do it.

var ng = angular.injector(['ng']);
var $http = ng.get('$http');

Final Code Summary

logger.$inject = ['$log'];
function logger($log) {
    // snip.

    function error(message, data) {
        $log.error('Error: ' + message, data);
        
        // inject $http here.
        var $http = angular.injector(['ng']).get('$http');
        if (!$http) return;
            var request = {
                method : 'POST',
                url: _spPageContextInfo.webServerRelativeUrl + '/_layouts/MyLogService.svc/LogMessage',
                data: {
                    'log': message,
                    'event': 'error',
                    'meta': angular.toJson(data)
                },
                dataType: "json",
                headers: {
                    "Content-Type": "application/json; charset=utf-8"
                }
            };
            $http(request);           
        }
}

PhantomJS and Office 365 Brisbane

I thoroughly enjoyed my trip up to Brisbane.  I got to see some friends, saw my sister and presented PhantomJS with SharePoint and SharePoint Online.

 

The response was tremendous, those that came to my session seems really interested and gave me more ideas for future discussions. 

The presentation and demo are here

The new business case scenarios:

  • Automated Smoke Testing of sites - randomly check pages and DOM exist - "nothing has so far blown up"
  • Smoke Testing of sites with different credentials!
  • Scanning and scrapping text off dynamic (Single Page Application) pages and feed them to SharePoint's Search Indexing Service.  Thus, you can have textual search linking back to dynamic URLs.

I also really enjoyed Elaine's session on SharePoint 2016.


The next Office 365 Saturday in Australia will be in Melbourne.  The registration is up here:

http://www.o365saturdayaustralia.com/melbourne

AngularJS Extend and Computed Property

This is an Intermediate level discussion on AngularJS Extend, Computed Property in general, and a fairly complicated binding bug that needs some new learning.

 

I came from a KnockoutJS world.  In the KnockoutJS world which I'm familiar with, there is the concept of a Computed Property.

KnockoutJS Computed Property

Because native Javascript objects does not raise OnPropertyChanged events, KnockoutJS wraps properties with an observable wrapper.
A class would look like this:

function PersonViewModel(){
  var self = this;
  
  self.firstName = ko.observable("");
  self.lastName = ko.observable("");
  
  self.fullName = ko.computed(function(){
    return [
      ko.unwrap(self.firstName), 
      ko.unwrap(self.lastName)
    ].join(" ");
  });
}

The developer takes care of the properties firstName and lastName, and KnockoutJS understands when firstName or lastName changes, fullName needs to be re-evaluated.

AngularJS binds Native Javascript Object

In AngularJS, there is no direct concept of a Computed Property, instead, AngularJS watches various binding expressions and automatically re-evaluate expressions.  Since AngularJS binds to native Javascript directly, it employs a dirty checking mechanism.

A native person might look like this:

var person = {
  firstName: "",
  lastName: ""
};

And a View would look like this:

<div>
  First Name: {{ person.firstName }}
  Last Name: {{ person.lastName }}
  Full Name: {{ person.firstName + ' ' + person.lastName }}
</div>

Defining Class for AngularJS

But very quickly, one would find that this gets very unwieldy, and you end up duplicating expressions everywhere in your View.  Duplicated code is never good.  So we need to tidy up the definition of the model a bit more.

function Person(){
  var self = this;
  
  self.firstName = "";
  self.lastName = "";
  
  self.fullName = function(){
    return [self.firstName,self.lastName].join(" ");
  };
}

AngularJS Extend

Such definitions are nice and good, but you will need a way to populate these when you pull your data back from a REST service as JSON.  Luckily, AngularJS provides both angular.extend as well as angular.merge helper methods to get you going quickly. 

If you are familar with jQuery, this is nearly identical to jQuery.extend, it copies the properties from the later objects onto the first one.

var jsonJohn = { firstName: "John", lastName: "Liu" };
var personJohn = angular.extend( new Person(), jsonJohn );

// personJohn should have firstName, lastName 
// and fullName() == 'John Liu'

The updated View is then:

<div>
  First Name: {{ person.firstName }}
  Last Name: {{ person.lastName }}
  Full Name: {{ person.fullName() }}
</div>

Extending Arrays

When you have a collection of people, the code needs to look like this:

var people = [];
var jsonPeople = [
  { firstName: "John", lastName: "Liu" },
  { firstName: "Mark", lastName: "Liu" }
];
people = angular.extend( people, jsonPeople );
<div data-ng-repeat="person in people">
  First Name: {{ person.firstName }}
  Last Name: {{ person.lastName }}
  Full Name: {{ person.fullName() }}
</div>

This looks good initially, except there are a few problems:

  • angular.extend does not know anything about Person class, and fullName is not available
var people = [
  new Person(),
  new Person()
];
var jsonPeople = [
  { firstName: "John", lastName: "Liu" },
  { firstName: "Mark", lastName: "Liu" }
];
people = angular.extend( people, jsonPeople );
  • angular.extend will override people[0] with jsonPeople[0] (not null).  So the result still does not define fullName()

So you need something like this:

var people = [
  new Person() // always show one row by default even if blank
];
var jsonPeople = [
  { firstName: "John", lastName: "Liu" },
  { firstName: "Mark", lastName: "Liu" }
];
var l = jsonPeople.length > people.length ? jsonPeople.length : people.length;
for (var i=0; i < l; i++){
  people[i] = angular.extend( new Person(), people[i], jsonPeople[i] );
  // copy jsonPeople[i] (if not null) onto people[i]
  // copy people[i] onto new Person()
  // store it back on people[i]
}

A subtle bug in copying with extend

The Computed Property doesn't work because the way angular.extend copies references, people[i].fullName is copied to (new Person()).fullName, which means that the (new Person()).fullName is calculating from the previous people[i].fullName, because it uses the 'self' reference.

(let me know if I need to draw a picture)

There are two ways to fix this:

1. We can fix how we extend existing records.

//...
for (var i=0; i < l; i++){
  people[i] = angular.extend( people[i] || new Person(), jsonPeople[i] );
  // OR  people[i] and new Person()
  // so that if there is an existing Person object, it is kept
}

2. We can fix how we define the Person class.  I think this is the better method.

function Person(){
  var self = this;
  
  self.firstName = "";
  self.lastName = "";
  
  Person.prototype.fullName = function(){
    // re-connect 'self' to the current object
    var self = this;
    return [self.firstName,self.lastName].join(" ");
  };
}

This bumps the fullName method to the prototype/supertype, so it is no longer an immediate property defined on the Person object.

Summary

  • Notes on doing computed property in AngularJS
  • How to define a Class for AngularJS
  • How to handle extend and extend an array
  • How to move computed methods to the prototype
  • Plunker http://plnkr.co/edit/HcuxnT3EAVrtz1PyWwXb check out the two Count1 and Count2 computed properties and how they work differently after you clicked 'update'

 

 

 

Notes on KnockoutJS Mapping and ViewModel style guide

I wanted to write down some of the better practices I've settled with since using KnockoutJS for nearly two years now.  A lot of these has to do with just avoiding errors that I see later on, as well as working with the peculiarities of the Knockout Mapping library.

These are not definitive, so let me know if you REALLY disagree!

function PersonViewModel(data) {
  var self = this;
  
  self.firstName = ko.observable();
  self.lastName = ko.observable();
    
  ko.mapping.fromJS(data, {}, self);

  self.name = ko.computed(function(){
    return ko.unwrap(self.firstName) + " " + ko.unwrap(self.lastName);
  });
}

Class Definition

  • Define a class, I always name the class CapitalizedCase, as opposed to camelCase for variables.  Makes it clear when I'm about to instantiate a variable
  • I also always name ViewModel separately from just normal classes.  ViewModel are special, they are for data-binding.  To me, PersonViewModel is different from Person.

You can now create a view model like this.

var personVM = new PersonViewModel();

You can also define a class this way

var PersonViewModel = function(data){
  var self = this;
  
  // ...
};
  • But if you do it this way, understand that Javascript Hoisting is now in effect, and the class is not available until this line.
  • Also, remember the trailing semi-colon if you go for this assign variable syntax.

Property Definition & Knockout Mapping

ko.mapping.fromJS can be used to instantiate properties as a constructor.  But there are potential issues.  Consider:

function PersonViewModel(data) {
  var self = this;
  
  //self.firstName = ko.observable();
  //self.lastName = ko.observable();
  //if we don't define these...
    
  ko.mapping.fromJS(data, {}, self);

  self.name = ko.computed(function(){
    return ko.unwrap(self.firstName) + " " + ko.unwrap(self.lastName);
  });
}

var johnVM = new PersonViewModel( { firstName: "John", lastName: "Liu" });
var marcVM = new PersonViewModel( { firstName: "Marc" });
  • In marcVM, self.lastName is not created by ko.mapping and does not exist, and some binding statements will fail
  • So always declare (annoying) all the observable properties that you need.  Don't trust some REST response to always return data that you need.

Knockout Mapping Definition

I go with this style for defining Knockout Mapping definitions.

function PersonViewModel(data) {
  var self = this;
  self.firstName = ko.observable();
  //...
}
PersonViewModel.mapping = {
  create: function(options){
    return new PersonViewModel(options.data);
  },
  key: function(data) {
    return ko.unwrap(data.firstName);
  }
};
  • The reason is that as you'll see in next example, you need the mapping from time to time, might as well keep it in one place that you can access easily.
  • I have also seen people put mapping on ViewModel.prototype.mapping - the benefit I see with this is that within the class, you can refer to it via self.mapping (but I think that lacks clarity).

You can now do this:

var data = {firstName:"John"};
var person = ko.mapping.fromJS(data, PersonViewModel.mapping);

This gets more fun when you have nested Observable Arrays of Objects

function PeopleViewModel(data) {
  var self = this;
  self.teachers = ko.mapping.fromJS([], PersonViewModel.mapping);
  self.students = ko.mapping.fromJS([], PersonViewModel.mapping);
  
  ko.mapping.fromJS(data, {}, self);
}
PeopleViewModel.mapping = {
  create: function(options){
    return new PeopleViewModel(options.data);
  },
  teachers: PersonViewModel.mapping,
  students: PersonViewModel.mapping
  // Reuse the mapping defined above!
};

var data = { 
  teachers: [
    { firstName: "John" },
    { firstName: "Marc" }
  ],
  students: {
    { firstName: "Bob"}
  }
};
var peopleVM = new PeopleViewModel(data);
var index = peopleVM.teachers.mappedIndexOf({firstName:"Marc"});

// index == 1

Summary

Some notes on structuring your ViewModel classes for readability, avoiding binding issues and Knockout Mapping gotchas.

You can try the example in this Plunker

Clearing SPD Cache from SharePoint Designer

Our good old friend SharePoint Designer is old, but I still find it useful for editing JavaScript files in development for quick tests.

Because SharePoint Designer caches what it thinks the site's contents look like, sometimes, this cache gets out of date with reality and you will see issues when SPD thinks you have some files checked out, but you don't.

And in the past, I would go and Bing and search "clear SharePoint Designer cache" and this will net you:

So there's no shortage of finding the command to clear the folder.

Until Today

I came across this part of the SPD UI by accident.

  • Select Home in the current site. 
  • Go to Site Options.
  • Select Advanced Tab
  • Click Delete Files (temporary files)

You will see that any currently Opened Folders views will be empty. 

As the cache for the site has been dumped.  You need to refresh from the Ribbon Bar and you'll see the status of the items has updated correctly.

SharePoint Designer 2013

The same UI exists for SharePoint Designer 2013 as well.

No more command line, perhaps.

Let me know in the comments if this was something you've ALWAYS known about, or whether it also made you feel foolish that you've been using the command-line or deleting cryptic user profile temporary folders for years, literally.  When there's a button that does this for me.