requirejs + django_select2 - django

I'm building a wagtail / django app using requirejs as js assets combiner, for the site front end.
I'm using it because I've ever been in a kind of JS dependencies hell, where nothing works because of multiple versions of same libs loaded, from different django apps... (I don't even know if it is a good solution)
I've to tell that I'm not a JS expert, and I've none arround me :(
I'm using the good old templates to render the pages, not using angular, react, riot nor vue : I'm a pretty old school dev :)
I've already adapted some scripts to use require, but I'm stuck for now...
I've installed the django_select2 application, and I'm trying to adapt the django_select2.js asset.
I've loaded select2 through bower, and I've updaetd my config.js:
"shim": {
select2: {
deps: ["jquery"],
exports: "$.fn.select2"
}
},
paths: {
...
select2: "select2/dist/js/select2"
}
Then I'm trying to adapt the django_select2.js:
require(['jquery', 'select2'], function ($, select2) {
return (function ($) {
var init = function ($element, options) {
$element.select2(options);
};
var initHeavy = function ($element, options) {
var settings = $.extend({
ajax: {
data: function (params) {
var result = {
term: params.term,
page: params.page,
field_id: $element.data('field_id')
}
var dependentFields = $element.data('select2-dependent-fields')
if (dependentFields) {
dependentFields = dependentFields.trim().split(/\s+/)
$.each(dependentFields, function (i, dependentField) {
result[dependentField] = $('[name=' + dependentField + ']', $element.closest('form')).val()
})
}
return result
},
processResults: function (data, page) {
return {
results: data.results,
pagination: {
more: data.more
}
}
}
}
}, options);
$element.select2(settings);
};
$.fn.djangoSelect2 = function (options) {
var settings = $.extend({}, options);
$.each(this, function (i, element) {
var $element = $(element);
if ($element.hasClass('django-select2-heavy')) {
initHeavy($element, settings);
} else {
init($element, settings);
}
});
return this;
};
$(function () {
$('.django-select2').djangoSelect2();
});
}($));
});
I'm having a Mismatched anonymous define() when running my page in the browser...
I'me realy not a JS expert, I'm coding by trial and error... Could anyone help me with this ?
Thanks !

OK, I have an auto-response...
I've inherited the mixin:
class _Select2Mixin(Select2Mixin):
def _get_media(self):
"""
Construct Media as a dynamic property.
.. Note:: For more information visit
https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property
"""
return forms.Media(js=('django_select2/django_select2.js', ),
css={'screen': (settings.SELECT2_CSS,)})
media = property(_get_media)
class _Select2MultipleWidget(_Select2Mixin, forms.SelectMultiple):
pass
Then I can use the widget:
class DocumentationSearchForm(forms.Form):
...
document_domains = forms.ModelMultipleChoiceField(required=False,
label=_('Document domains'),
queryset=NotImplemented,
widget=_Select2MultipleWidget)
I've set my config.js file for path:
requirejs.config({
paths: {
jquery: 'jquery/dist/jquery',
select2: "select2/dist/js"
},
shim: {
"select2/select2": {
deps: ["jquery"],
exports: "$.fn.select2"
}
}
});
Then I've overridden the django_select2.js file, to wrap the content in a require satement:
require(['jquery', 'select2/select2'], function ($) {
(function ($) {
var init = function ($element, options) {
$element.select2(options);
};
var initHeavy = function ($element, options) {
var settings = $.extend({
ajax: {
data: function (params) {
var result = {
term: params.term,
page: params.page,
field_id: $element.data('field_id')
}
var dependentFields = $element.data('select2-dependent-fields')
if (dependentFields) {
dependentFields = dependentFields.trim().split(/\s+/)
$.each(dependentFields, function (i, dependentField) {
result[dependentField] = $('[name=' + dependentField + ']', $element.closest('form')).val()
})
}
return result
},
processResults: function (data, page) {
return {
results: data.results,
pagination: {
more: data.more
}
}
}
}
}, options);
$element.select2(settings);
};
$.fn.djangoSelect2 = function (options) {
var settings = $.extend({}, options);
$.each(this, function (i, element) {
var $element = $(element);
if ($element.hasClass('django-select2-heavy')) {
initHeavy($element, settings);
} else {
init($element, settings);
}
});
return this;
};
$(function () {
$('.django-select2').djangoSelect2();
});
}($));
});
That's all, folks !

Related

Decouple Knockout dependencies in Jasmine unit tests

I am introducing javascript unit tests to my company's application. I've worked predominantly with AngularJS, but their framework of choice is Knockout. The code is relatively modular in design (thanks to Knockout), so I assumed it would be easy to add unit tests around the code (much like Angular is).
However, there is a further complication: the system uses require.js as an IoC container. Also, my test runner is Chutzpah, which is headless/in the Visual Studio Test Explorer. I have tried to mock out the dependencies using SquireJS, but I run into a snag when trying to instantiate the ViewModel: it wants certain dependencies that are set up in the Component and inherited. I try to load the component, which has further dependencies, and so on and so on. Below I have a dumbed-down version of the setup that outlines the point. What is the best way to instantiate this view model so that I can mock out the dependencies with Squire and run my tests?
Component
define(function (require) {
var Boiler = require('Boiler'),
BaseViewModel = require('../../../../core/baseClasses/BaseViewModel'),
viewTemplate = require('text!./view.html'),
viewModel = require('./viewModel'),
nls = require('i18n!./nls/resources');
var Component = function (initializingParameters) {
//...blah blah blah setup things
};
return Component;
});
ViewModel
define(function (require) {
var Constants = require('../../../../core/constants');
var momDate = moment().format('MMM DD, YYYY');
var Utils = require("../../../../model/utils");
var utils = new Utils();
var otherComponentUtils = require("../../../otherModule/otherComponent/OtherComponentUtils");
var otherComponentUtils = new OtherComponentUtils();
var ViewModel = function (globalContext, moduleContext, dataContext, domElement) {
var Self = this;
Self.super(moduleContext, dataContext, resPickerContext);
var constants = new Constants();
var utils = require("../../../../model/utils");
var Utils = new utils();
Self.Items = ko.observableArray().extend({ throttle: 500 });
//a bunch more Knockout object setup
Self.GetAuthorizationTypesArray = function (itemId) {
dataContext.dataRequestHandler(dataContext.serviceConstants.GetTransactionTypes, { ItemId: itemId },
function (data) {
if (data != null && data.length !== 0) {
Self.TransactionTypeArray(data);
var transactionTypeArray = Self.TransactionTypeArray();
if (transactionTypeArray.length) {
var paymentInfo = Self.PaymentInfo();
if (paymentInfo !== null && paymentInfo !== undefined && paymentInfo.IsSpecial) {
var childThing = Self.ChildThing();
dataContext.dataRequestHandler(dataContext.serviceConstants.GetChild, { ChildId: childThing.ChildId, SpecialId: childThing.SpecialID }, function (data) {
var child = data[0];
var specialTypeId = child.ListId;
if (specialTypeId === 13)
Self.BigThing.Type(1);
else
Self.BigThing.Type(2);
}, Self.ShowError);
}
}
}
},
Self.ShowError);
}
return ViewModel;
});
chutzpah.json
{
"Framework": "jasmine",
"TestHarnessReferenceMode": "AMD",
"TestHarnessLocationMode": "SettingsFileAdjacent",
"RootReferencePathMode": "SettingsFileDirectory",
"References": [
{ "Path": "../../myWebApp/Scripts/libs/assets/plugins/jquery-1.10.2.min.js" },
{ "Path": "../../myWebApp/scripts/libs/moment/moment.min.js" },
{ "Path": "../../myWebApp/Scripts/libs/require/require.js" },
{ "Path": "unittest.main.js" }
],
"Tests": [
{
"Path": "app",
"Includes": [ "*.tests.js" ]
}
]
}
unittest.main.js
"use strict";
require.config({
paths: {
'jasmine': ['jasmine/jasmine'],
'jasmine-html': ['jasmine/jasmine-html'],
'jasmine-boot': ['jasmine/boot'],
squire: 'Squire'
},
shim: {
'jasmine-html': {
deps : ['jasmine']
},
'jasmine-boot': {
deps : ['jasmine', 'jasmine-html']
},
'squire': {
exports: 'squire'
}
},
config: {
text: {
useXhr: function (url, protocol, hostname, port) { return true },
//Valid values are 'node', 'xhr', or 'rhino'
env: 'xhr'
}
},
waitSeconds: 0
});
viewModel.tests.js
define(function (require) {
var testTargetPath = '../../myWebApp/Scripts/app/modules/thisModule/myViewModel';
describe('My View Model', function () {
var mockDataContext;
var mockResponseData;
var injector;
var viewModel;
beforeEach(function () {
var Squire = require('Squire');
injector = new Squire();
});
beforeEach(function () {
mockResponseData = {};
mockDataContext = {
dataRequestHandler: function (url, data, onSuccess) {
onSuccess(mockResponseData[url]);
},
serviceConstants: {
GetTransactionTypes: 'getTransactionTypes',
GetChild: 'getNewPolicy'
}
};
injector.mock("dataContext", mockDataContext);
});
beforeEach(function (done) {
injector.require([testTargetPath], function (ViewModel) {
viewModel = new ViewModel();
done();
});
});
it('gets authorization type array', function () {
spyOn(mockDataContext, 'dataRequestHandler').and.callThrough();
mockResponseData = {
'getTransactionTypes': [
{ name: 'auth type 1', TransactionTypeId: 90210 },
{ name: 'auth type 2', TransactionTypeId: 42 },
]
};
viewModel.GetAuthorizationTypesArray(9266);
expect(mockDataContext.dataRequestHandler).toHaveBeenCalledWith('getTransactionTypes', { ItemId: 9266 });
expect(viewModel.TransactionTypeArray()).toBe(mockResponseData);
});
});
});
To be specific, in the ViewModel, when the tests run, it complains that super is undefined.
Alright, turns out there were a number of issues I had to deal with:
At first I thought the failing on super was because of an ES6
compatibility issue (since PhantomJS only supports ES5). However,
it turns out that the project is not using ES6, but
inheritance.js, which manually builds the super dependencies.
That setup is done within the component section of Knockout in our
solution, so I replicated it within the unit tests.
I was not properly setting up Squire to inject my dependencies, and
corrected that.
I did not make any changes to my component or view model, and my Chutzpah configuration stayed the same. However, unittest.main.js was updated, as were my tests:
unittest.main.js
"use strict";
require.config({
paths: {
'jasmine': ['jasmine/jasmine'],
'jasmine-html': ['jasmine/jasmine-html'],
'jasmine-boot': ['jasmine/boot'],
squire: 'Squire',
"baseViewModel": '../../myWebApp/Scripts/app/core/baseClasses/BaseViewModel'
},
shim: {
'jasmine-html': {
deps : ['jasmine']
},
'jasmine-boot': {
deps : ['jasmine', 'jasmine-html']
},
'squire': {
exports: 'squire'
}
},
config: {
text: {
useXhr: function (url, protocol, hostname, port) { return true },
//Valid values are 'node', 'xhr', or 'rhino'
env: 'xhr'
}
},
waitSeconds: 0
});
viewModel.tests.js
define(function (require) {
var testTargetPath = '../../myWebApp/Scripts/app/modules/thisModule/myViewModel';
describe('View Model', function () {
var mockGlobalContext;
var mockModuleContext;
var mockDataContext;
var mockResponseData;
var injector;
var viewModel;
var mockUtils;
beforeEach(function () {
var Squire = require('Squire');
injector = new Squire();
});
beforeEach(function () {
mockResponseData = {};
mockGlobalContext = {};
mockUtils = {};
mockModuleContext = {};
mockDataContext = {
dataRequestHandler: function (url, data, onSuccess) {
onSuccess(mockResponseData[url]);
},
serviceConstants: {
GetTransactionTypes: 'getTransactionTypes',
GetChild: 'getNewPolicy'
}
};
injector.mock("../../../../model/utils", function () { return mockUtils; });
spyOn(mockDataContext, 'dataRequestHandler').and.callThrough();
});
beforeEach(function (done) {
injector.require([testTargetPath], function (ViewModel) {
var BaseViewModel = require('baseViewModel');
BaseObject.Create(ViewModel, BaseViewModel, [mockGlobalContext, mockModuleContext, mockDataContext]);
viewModel = new ViewModel(mockGlobalContext, mockModuleContext, mockDataContext);
done();
});
});
it('gets authorization type array', function () {
mockResponseData = {
'getGatewayTransactionTypes': [
{ name: 'auth type 1', TransactionTypeId: 90210 },
{ name: 'auth type 2', TransactionTypeId: 42 },
]
};
viewModel.GetAuthorizationTypesArray(9266);
expect(mockDataContext.dataRequestHandler).toHaveBeenCalledWith('getGatewayTransactionTypes', { ItemId: 9266 }, jasmine.any(Function), viewModel.ShowError);
expect(viewModel.TransactionTypeArray()).toEqual(mockResponseData['getGatewayTransactionTypes']);
});
});
});
With these changes, the test runs successfully and passes.

angular-ui-bootstrap causes unknown provider error when minified

After adding angular-ui-bootstrap and running grunt serve on my yeoman app, it runs perfectly and the modal I want to show is displayed correctly, but once I do a grunt build, I get an unknown provider error in my console.
<!-- This is what I added in my index.html -->
<script src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
// In app.js I have
angular.module('yeomanApp', [
'ngCookies',
'ngResource',
'ngSanitize',
'ngRoute',
'ui.bootstrap'
])
and in the controller,
.controller('myCntrl', function ($modal) {
$scope.items = ['item1', 'item2', 'item3'];
$scope.showDeleteWarning = function () {
var modalInstance = $modal.open({
templateUrl: 'deleteWarning.html',
controller: ModalInstanceCtrl,
resolve: {
items: function () {
return $scope.items;
}
}
});
modalInstance.result.then(function (selectedItem) {
$scope.selected = selectedItem;
}, function () {});
};
// Please note that $modalInstance represents a modal window (instance) dependency.
// It is not the same as the $modal service used above.
var ModalInstanceCtrl = function ($scope, $modalInstance, items) {
$scope.items = items;
$scope.selected = {
item: $scope.items[0]
};
$scope.ok = function () {
$modalInstance.close($scope.selected.item);
deleteVent();
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
};
};
Likely that you need to inject your controller dependency...
https://docs.angularjs.org/tutorial/step_05#a-note-on-minfication
.controller('myCntrl', ['$modal', function ($modal) {
/* Controller Code Here... */
}]);
I know this is an old question, but I'll post my answer here for people who come across this problem in the future.
I came across this exact problem before. The cause of your errors during minification is most likely your 'var ModalInstanceCtrl'.
Here's how I got my code to work:
var modalInstance = $modal.open({
templateUrl: 'deleteWarning.html',
controller: 'ModalInstanceCtrl', //change this to a string
resolve: {
items: function () {
return $scope.items;
}
}
});
and this line:
var ModalInstanceCtrl = function ($scope, $modalInstance, items) {
to:
angular.module('myModule').controller('ModalInstanceCtrl', function ($scope, $modalInstance, items) {
For anyone who just encountered this problem, maybe this will help.
We use customModalDefaults and customModalOptions, so we had to turn the whole return $modal.open(tempModalDefaults).result; in the show function to the following:
this.show = function (customModalDefaults, customModalOptions) {
//Create temp objects to work with since we're in a singleton service
var tempModalDefaults = {};
var tempModalOptions = {};
//Map angular-ui modal custom defaults to modal defaults defined in service
angular.extend(tempModalDefaults, modalDefaults, customModalDefaults);
//Map modal.html $scope custom properties to defaults defined in service
angular.extend(tempModalOptions, modalOptions, customModalOptions);
return $modal.open({
backdrop: customModalDefaults.backdrop,
keyboard: customModalDefaults.keyboard,
modalFade: customModalDefaults.modalFade,
templateUrl: customModalDefaults.templateUrl,
size: customModalDefaults.size,
controller: ['$scope', '$modalInstance', function ($scope, $modalInstance) {
$scope.modalOptions = tempModalOptions;
$scope.modalOptions.ok = function (result) {
$modalInstance.close(result);
};
$scope.modalOptions.close = function (result) {
$modalInstance.dismiss('cancel');
};
} ]
}).result;
};
I just ran into this problem on only one of many modals used throughout my application, and it turned out my problem was not using explicit function annotation in the resolve block of the modal configuration.
var modalInstance = $uibModal.open({
templateUrl: 'preferences.html',
controller: 'preferencesCtrl as ctrl', // this external controller was using explicit function annotation...
resolve: {
parent: [function() {
return ctrl;
}],
sectorList: ['preferencesService', function(preferencesService) { // but this was not!
return preferencesService.getSectors();
}]
}
});
Hope this saves someone else a gray hair or two...

How to define Handlebar.js templates in an external file

I am currently defining my Handlebars templates within the same HTML file in which they will be used.
Is it possible to define them as external templates, which can be called when the page is loaded?
For those that get here through Google search like I did, I finally found the perfect answer in this post, check it out :
http://berzniz.com/post/24743062344/handling-handlebarsjs-like-a-pro
Basically you need to implement a method, getTemplate :
Handlebars.getTemplate = function(name) {
if (Handlebars.templates === undefined || Handlebars.templates[name] === undefined) {
$.ajax({
url : 'templatesfolder/' + name + '.handlebars',
success : function(data) {
if (Handlebars.templates === undefined) {
Handlebars.templates = {};
}
Handlebars.templates[name] = Handlebars.compile(data);
},
async : false
});
}
return Handlebars.templates[name];
};
and then call your template with it :
var compiledTemplate = Handlebars.getTemplate('hello');
var html = compiledTemplate({ name : 'World' });
That way, if you precompiled the templates (great for production use) it will find it directly, otherwise it will fetch the template and compile it in the browser (that's how you work in development).
You can use AJX to load them.
Here's an example function which accepts a URL to a Handlebars template, and a callback function. The function loads the template, and calls the callback function with the compiled Handlebars template as a parameter:
function loadHandlebarsTemplate(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
var raw = xhr.responseText;
var compiled = Handlebars.compile(raw);
callback(compiled);
}
};
xhr.send();
}
For example, assume you have a Handlebars template defined in a file named /templates/MyTemplate.html, such as:
<p>The current date is {{date}}</p>
You can then load that file, compile it and render it to the UI, like this:
var url = '/templates/MyTemplate.html';
loadHandlebarsTemplate(url, function(template) {
$('#container').html(template({date: new Date()}));
});
An even better solution, would be to pre-compile the templates which would speed up the time they take to load. More info on precompilation can be found here.
Edited solution from Jeremy Belolo for working more templates in directories, if i do not want to have all templates in templates directory.
Handlebars.getTemplate = function(name, dir) {
if (dir === undefined) //dir is optional
dir = "";
if (Handlebars.templates === undefined || Handlebars.templates[name] === undefined) {
$.ajax({
url : 'templates/' + dir+'/' + name, //Path, dir is optional
success : function(data) {
if (Handlebars.templates === undefined) {
Handlebars.templates = {};
}
if (Handlebars.templates[dir] === undefined) {
Handlebars.templates[dir] = {};
}
if (dir === undefined)
Handlebars.templates[name] = Handlebars.compile(data);
else
Handlebars.templates[dir][name] = Handlebars.compile(data);
},
async : false
});
}
if (dir === undefined)
return Handlebars.templates[name];
else
return Handlebars.templates[dir][name];
};
Using async: false; is far away from being the perfect solution. With jQuery in your toolbox, just make use of $.Deferred.
I came up with this solution:
;(function ($, Handlebars) {
'use strict';
var namespace = window,
pluginName = 'TemplateEngine';
var TemplateEngine = function TemplateEngine(options) {
if(!(this instanceof TemplateEngine)) {
return new TemplateEngine(options);
}
this.settings = $.extend({}, TemplateEngine.Defaults, options);
this._storage = {};
return this;
};
TemplateEngine.Defaults = {
templateDir: './tpl/',
templateExt: '.tpl'
};
TemplateEngine.prototype = {
constructor: TemplateEngine,
load: function(name, $deferred) {
var self = this;
$deferred = $deferred || $.Deferred();
if(self.isCached(name)) {
$deferred.resolve(self._storage[name]);
} else {
$.ajax(self.urlFor(name)).done(function(raw) {
self.store(name, raw);
self.load(name, $deferred);
});
}
return $deferred.promise();
},
fetch: function(name) {
var self = this;
$.ajax(self.urlFor(name)).done(function(raw) {
self.store(name, raw);
});
},
isCached: function(name) {
return !!this._storage[name];
},
store: function(name, raw) {
this._storage[name] = Handlebars.compile(raw);
},
urlFor: function(name) {
return (this.settings.templateDir + name + this.settings.templateExt);
}
};
window[pluginName] = TemplateEngine;
})(jQuery, Handlebars);

Backbone collection parse never fires, collection.models should have 2 items but is 0

Here is my MemberView.js ...
define([
'jquery',
'underscore',
'backbone',
'collections/MembersCollection',
'text!templates/memTemplate.html'
], function($, _, Backbone, MembersCollection, memTemplate) {
var MembersView = Backbone.View.extend({
el: $("#page"),
initialize: function() {
var that = this;
this.collection = new MembersCollection([]);
this.collection.fetch({
success : function(collection, response, options) {
that.render();
},
error: function(collection, response, options) {
console.log('members fetch error: '+response.responseText);
alert(response.responseText);
}
});
},
render: function() {
var data = { members : this.collection.models };
var compiledTemplate = _.template( memTemplate, data );
this.$el.html( compiledTemplate );
}
});
return MembersView;
});
Here is my MemberCollection.js ...
define([
'jquery',
'underscore',
'backbone',
'models/Member'
], function($, _, Backbone, Member) {
var MembersCollection = Backbone.Collection.extend({
model: Member,
initialize : function(models, options) { },
url : '/modular-backbone/server/member',
parse: function (response) {
console.log("In Parse=" + response.length);
return response;
}
});
return MembersCollection;
});
There is never a "In Parse=?" in the console so I have to assume collection.parse is not fireing. Also, if I put a break in the view.render method, collection.models is always a zero length array even though I can clearly see 2 Member records in the fetch success response. What am I missing?
Thanks a lot for your advice :-)
Unbelievable...
I went back and cleaned up a bunch of commented out lines in a few js files, ran the app again and now it's working perfectly. This makes no sense at all.

What is the best approach to use Disqus in a single page application?

What is the best approach to use Disqus in a single page application?
I see that the angular js docs has implemented it successfully.
Currently our approach looks like is this in our AngularJS app, but it seems unstable, is hard to test, and loads wrong thread ids (the same thread gets loaded almost everywhere).
'use strict';
angular.module('studentportalenApp.components')
.directive('disqusComponent',['$log', '$rootScope', function($log, $rootScope) {
var _initDisqus = function _initDisqus(attrs)
{
if(window.DISQUS) {
DISQUS.reset({
reload: true,
config: function () {
this.page.identifier = attrs.threadId;
this.disqus_container_id = 'disqus_thread';
this.page.url = attrs.permalinkUrl;
}
});
}
else
{
$log.error('window.DISQUS did not exist before directive was loaded.');
}
}
//Destroy DISQUS bindings just before route change, to properly dispose of listeners and frame (postMessage nullpointer exception)
$rootScope.$on('$routeChangeStart', function() {
if(window.DISQUS) {
DISQUS.reset();
}
});
var _linkFn = function link(scope, element, attrs) {
_initDisqus(attrs);
}
return {
replace: true,
template: '<div id="disqus_thread"></div>',
link: _linkFn
};
}]);
I also wanted to include Disqus on my AngularJS-powered blog. I found the existing solutions a bit unwieldy so I wrote my own directive:
.directive('dirDisqus', function($window) {
return {
restrict: 'E',
scope: {
disqus_shortname: '#disqusShortname',
disqus_identifier: '#disqusIdentifier',
disqus_title: '#disqusTitle',
disqus_url: '#disqusUrl',
disqus_category_id: '#disqusCategoryId',
disqus_disable_mobile: '#disqusDisableMobile',
readyToBind: "#"
},
template: '<div id="disqus_thread"></div>comments powered by <span class="logo-disqus">Disqus</span>',
link: function(scope) {
scope.$watch("readyToBind", function(isReady) {
// If the directive has been called without the 'ready-to-bind' attribute, we
// set the default to "true" so that Disqus will be loaded straight away.
if ( !angular.isDefined( isReady ) ) {
isReady = "true";
}
if (scope.$eval(isReady)) {
// put the config variables into separate global vars so that the Disqus script can see them
$window.disqus_shortname = scope.disqus_shortname;
$window.disqus_identifier = scope.disqus_identifier;
$window.disqus_title = scope.disqus_title;
$window.disqus_url = scope.disqus_url;
$window.disqus_category_id = scope.disqus_category_id;
$window.disqus_disable_mobile = scope.disqus_disable_mobile;
// get the remote Disqus script and insert it into the DOM
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + scope.disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
}
});
}
};
});
Advantages
The main advantage of this approach, I think, is that it keeps things simple. Once you have registered the directive with your app, you don't need to write any JavaScript or set any config values in your JavaScript. All configuration is handled by passing attributes in the directive tag like so:
<dir-disqus disqus-shortname="YOUR_DISQUS_SHORTNAME"
disqus-identifier="{{ article.id }}"
disqus-title="{{ article.title }}"
...>
</dir-disqus>
Also, you don't need to alter your index.html file to include the Disqus .js file - the directive will dynamically load it when it is ready. This means that all that extra .js will only get loaded on those pages that actually use the Disqus directive.
You can see the full source and documentation here on GitHub
Caveat
The above will only work properly when your site is in HTML5Mode, i.e. not using the "#" in your URLs. I am updating the code on GitHub so the directive will work when not using HTML5Mode, but be warned that you must set a hashPrefix of "!" to make "hashbang" URLs - e.g. www.mysite.com/#!/page/123. This is a limitation imposed by Disqus - see http://help.disqus.com/customer/portal/articles/472107-using-disqus-on-ajax-sites
I know nothing about Disqus, but according to the AngularJS Documentation source code:
They bind a load function to afterPartialLoaded:
$scope.afterPartialLoaded = function() {
var currentPageId = $location.path();
$scope.partialTitle = $scope.currentPage.shortName;
$window._gaq.push(['_trackPageview', currentPageId]);
loadDisqus(currentPageId);
};
Then, they simply add the html to the page:
function loadDisqus(currentPageId) {
// http://docs.disqus.com/help/2/
window.disqus_shortname = 'angularjs-next';
window.disqus_identifier = currentPageId;
window.disqus_url = 'http://docs.angularjs.org' + currentPageId;
// http://docs.disqus.com/developers/universal/
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = 'http://angularjs.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] ||
document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
angular.element(document.getElementById('disqus_thread')).html('');
}
This is how we solved it.
We load DISQUS in the body of index.html, and resets it whenever there is a directive using it.
Directive:
'use strict';
angular.module('fooApp.directives')
.directive('disqusComponent',['$window', '$log', function($window, $log) {
var _initDisqus = function _initDisqus(scope)
{
if($window.DISQUS) {
$window.DISQUS.reset({
reload: true,
config: function () {
this.page.identifier = scope.threadId;
this.disqus_container_id = 'disqus_thread';
}
});
}
else
{
$log.error('window.DISQUS did not exist before directive was loaded.');
}
}
var _linkFn = function link(scope, element, attrs) {
element.html('<div id="disqus_thread"></div>');
_initDisqus(scope);
}
return {
replace: true,
template: 'false',
scope: {
threadId: '#'
},
link: _linkFn
};
}]);
This is how it can be tested:
'use strict';
describe('Directive: Disqus', function() {
var element, $window, $rootScope, $compile;
beforeEach(function() {
module('fooApp.directives', function($provide) {
$provide.decorator('$window', function($delegate) {
$delegate.DISQUS = {
reset: jasmine.createSpy()
};
return $delegate;
});
});
inject(function(_$rootScope_, _$compile_, _$window_) {
$window = _$window_;
$rootScope = _$rootScope_;
$compile = _$compile_;
});
});
it('should place a div with id disqus_thread in DOM', function() {
element = angular.element('<disqus-component></disqus-component>');
element = $compile(element)($rootScope);
expect(element.html()).toBe('<div id="disqus_thread"></div>');
});
it('should do a call to DISQUS.reset on load', function() {
element = angular.element('<disqus-component thread-id="TESTTHREAD"></disqus-component>');
element = $compile(element)($rootScope);
var resetFn = $window.DISQUS.reset;
expect(resetFn).toHaveBeenCalled();
});
});