I've got two RequireJS modules, one for fetching data from an external service, one in charge of passing a callback to the first module.
Here is the first very basic module:
define(["jquery"], function($) {
return {
/**
* Retrieves all the companies that do not employs the provided employee
* #param employeeId ID of the employee
* #param successCallback callback executed on successful request completion
* #return matching companies
*/
fetchCompanies: function(employeeId, successCallback) {
var url = '/employees/' + employeeId + '/nonEmployers';
return $.getJSON(url, successCallback);
}
};
});
And the most interesting one, that will generate a new drop-down and inject it into the specified DOM element (this is the one under test):
define([
'jquery',
'vendor/underscore',
'modules/non-employers',
'text!tpl/employeeOption.tpl'], function($, _, nonEmployers, employeeTemplate) {
var updateCompanies = function(selectedEmployeeId, companyDropDownSelector) {
nonEmployers.fetchCompanies(selectedEmployeeId, function(data) {
var template = _.template(employeeTemplate),
newContents = _.reduce(data, function(string,element) {
return string + template({
value: element.id,
display: element.name
});
}, "<option value='-1'>select a client...</option>\n");
$(companyDropDownSelector).html(newContents);
});
};
return {
/**
* Updates the dropdown identified by companyDropDownSelector
* with the companies that are non employing the selected employee
* #param employeeDropDownSelector selector of the employee dropdown
* #param companyDropDownSelector selector of the company dropdown
*/
observeEmployees: function(employeeDropDownSelector, companyDropDownSelector) {
$(employeeDropDownSelector).change(function() {
var selectedEmployeeId = $(employeeDropDownSelector + " option:selected").val();
if (selectedEmployeeId > 0) {
updateCompanies(selectedEmployeeId, companyDropDownSelector);
}
});
}
};
});
I'm trying to test this last module, using Jasmine-fixtures and using waitsFor, to asynchronously check that the set-up test DOM structure has been modified. However, the timeout is always reached.
If you can spot what's wrong in the following test, I'd be most grateful (gist:https://gist.github.com/fbiville/6223bb346476ca88f55d):
define(["jquery", "modules/non-employers", "modules/pages/activities"], function($, nonEmployers, activities) {
describe("activities test suite", function() {
var $form, $employeesDropDown, $companiesDropDown;
beforeEach(function() {
$form = affix('form[id=testForm]');
$employeesDropDown = $form.affix('select[id=employees]');
$employeesDropDown.affix('option[selected=selected]');
$employeesDropDown.affix('option[value=1]');
$companiesDropDown = $form.affix('select[id=companies]');
$companiesDropDown.affix('option');
});
it("should update the company dropdown", function() {
spyOn(nonEmployers, "fetchCompanies").andCallFake(function(employeeId, callback) {
callback([{id: 42, name: "ACME"}, {id: 100, name: "OUI"}]);
});
activities.observeEmployees('#employees', '#companies');
$('#employees').trigger('change');
waitsFor(function() {
var companiesContents = $('#companies').html(),
result = expect(companiesContents).toContain('<option value="42">ACME</option>');
return result && expect(companiesContents).toContain('<option value="100">OUI</option>');
}, 'DOM has never been updated', 10000);
});
});
});
Thanks in advance!
Rolf
P.S.: replacing $(employeeDropDownSelector).change by $(employeeDropDownSelector).on('change', and/or wrapping the activities.observeEmployees call (and $('#employees').trigger('change');) with a domReady yields the same result
P.P.S.: this error is the cause -> SEVERE: runtimeError: message=[An invalid or illegal selector was specified (selector: '[id='employees'] :selected' error: Invalid selector: *[id="employees"] *:selected).] sourceName=[http://localhost:59811/src/vendor/require-jquery.js] line=[6002] lineSource=[null] lineOffset=[0].
P.P.P.S.: it seems HtmlUnit doesn't support CSS3 selectors (WTF?), and even forcing the latest published version as jasmine-maven-plugin dependency won't change anything...
Is there any way to change jasmine plugin runner ?
OK guys.
Solution found:
upgrade (if not already) to jasmine-maven-plugin v1.3.1.1 (or later)
configure phantomjs instead of this crappy HtmlUnit (add PhantomJS binaries to your project)
if you've got use of ':focus' selector in your code, beware of this bug, replace it with $(mySelector).get(0) == document.activeElement
also, do not forget to wrap your code blocks by run(function() { /* expect */ }) if they are positioned after and depend on your waitsFor condition.
Finally, all should be well.
See how is the test now:
define(["jquery",
"modules/nonEmployers",
"modules/pages/activities"], function($, nonEmployers, activities) {
describe("activities test suite", function() {
var $form, $employeesDropDown, $companiesDropDown;
beforeEach(function() {
$form = affix('form[id=testForm]');
$employeesDropDown = $form.affix('select[id=employees]');
$employeesDropDown.affix('option[selected=selected]');
$employeesDropDown.affix('option[value=1]');
$companiesDropDown = $form.affix('select[id=companies]');
$companiesDropDown.affix('option');
spyOn(nonEmployers, "fetchCompanies").andCallFake(function(employeeId, callback) {
callback([{id: 42, name: "ACME"}, {id: 100, name: "OUI"}]);
});
});
it("should update the company dropdown", function() {
$(document).ready(function() {
activities.observeEmployees('#employees', '#companies');
$('#employees option[selected=selected]').removeAttr("selected");
$('#employees option[value=1]').attr("selected", "selected");
$('#employees').trigger('change');
waitsFor(function() {
var dropDown = $('#companies').html();
return dropDown.indexOf('ACME') > 0 && dropDown.indexOf('OUI') > 0;
}, 'DOM has never been updated', 500);
runs(function() {
var dropDown = $('#companies').html();
expect(dropDown).toContain('<option value="42">ACME</option>');
expect(dropDown).toContain('<option value="100">OUI</option>');
});
});
});
});
});
Creating modules this way is really difficult. I'd recommend not using fixtures and not rendering anywhere actually. Instead using detached DOM elements to do all the work is much easier.
Imagine if your code looked closer to this:
define([
'jquery',
'vendor/underscore',
'modules/non-employers',
'text!tpl/employeeOption.tpl'], function($, _, nonEmployers, employeeTemplate) {
return {
init: function() {
this.$companies = $('<select class="js-companies"></select>');
},
render: function(data) {
var template = _.template(employeeTemplate),
newContents = _.reduce(data, function(string,element) {
return string + template({
value: element.id,
display: element.name
});
}, "<option value='-1'>select a client...</option>\n");
this.$companies.empty().append(newContents);
return this;
});
observeEmployees: function(employeeDropDownSelector) {
$(employeeDropDownSelector).change(function() {
var selectedEmployeeId = $(employeeDropDownSelector + " option:selected").val();
if (selectedEmployeeId > 0) {
nonEmployers.fetchCompanies(selectedEmployeeId, function(data) {
this.render(data);
}
}
});
}
};
});
The above is not complete. It is just to give you an idea of another way to approach your problem. Now instead of a fixture all you need to do is inspect this.$companies and you will be done. I think the main problem though is that your functions are not simple enough. The concern of each function should be extremely specific. Your updateCompanies function is doing things like creating a template, fetching data then passing it to an anonymous function, which can't be spied on, that anonymous function iterates on an object, then you change some already existing DOM element. That sounds exhausting. All that function should do is look at some precompiled template send it an object. The template should loop on the object using {{each}} then return. Your function then empties and append the newContents and returns it self so the next function down can choose what it should do with this.$companies. Or if this.$companies has already been append to the page nothing needs to be done at all.
Related
I am currently in the process of working on the app which is through modular pattern.
The problem i am currently getting is that once the Ajax is complete, i want to be able to fire a function within the object. The object i can see but when i specify a function, it fails and comes back as Undefined.
JS
var TestCase = {
settings: {
cu: $('.select'),
},
init: function() {
se = this.settings;
},
windowsReady: function() {
TestCase.init();
if ($.fn.selectBox) {
TestCase.selectBind();
}
},
ajaxComp: function() {
TestCase.init();
TestCase.selectBind();
},
selectBind: function(){
se.cu.selectBox();
},
};
JS Fire - The selectBind works fine when its loaded through the ready call. However as mentioned before, the ajaxcomplete keeps coming back as Undefined for TestCase.ajaxComp(); or a direct call for TestCase.selectBind(); Please note that when i console.log(TestCase) it lists all the objects.
$(document).ready(function () {
TestCase.windowsReady();
});
$(document).ajaxSuccess(function() {
console.log(TestCase);
TestCase.ajaxComp();
console.log('completed');
});
This is happenning because of this line:
se = this.settings;
The this in the scope of your $(document).ajaxSuccess(...) method will be the jQuery object, not TestCase object.
Try changing it to se = TestCase.settings;
I am making two different app's with Meteor. In first app, witch you can see here, I am using ... template.текст.set( true ); ... and everything is working fine. Now in second app I got error
ReferenceError: template is not defined
So, what is the problem? I Checked, packages are same.
Here is the code of second app:
Template.body.onCreated(function bodyOnCreated() {
this.TrenutniKorisnik = new ReactiveVar(true);
});
Template.PrijavaKorisnika.events({
'submit .Prijava': function(event) {
event.preventDefault();
var korisnik = event.target.КорисничкоИме.value;
var šifra = event.target.Лозинка.value;
if (Korisnici.findOne({КорисничкоИме: korisnik, Шифра: šifra})) { template.TrenutniKorisnik.set( false )};
event.target.КорисничкоИме.value = "";
event.target.Лозинка.value = "";
}
});
Template.body.helpers({
TrenutniKorisnik: function() {
return Template.instance().TrenutniKorisnik.get();
},
});
The template instance is the second parameter in an event handler. Simply change this:
'submit .Prijava': function(event) {
to this:
'submit .Prijava': function(event, template) {
so template will be defined in the function body.
Once you solve that, however you'll find that TrenutniKorisnik isn't defined because it's on the body template and not the current template. One way to solve that is to use a file-scoped variable rather than a template one. Here's an example:
var TrenutniKorisnik = new ReactiveVar(true);
Template.PrijavaKorisnika.events({
'submit .Prijava': function (event) {
...
if (Korisnici.findOne({ КорисничкоИме: korisnik, Шифра: šifra })) {
TrenutniKorisnik.set(false);
}
...
},
});
Template.body.helpers({
TrenutniKorisnik: function () {
return TrenutniKorisnik.get();
},
});
I have a script (below) that asynchronously updates markup on setInterval; markup which is generated with jQuery from XML data. This is my attempt at creating a UI in which users can view to see changes happen to the XML data in real-time. However, this is seeming like a round about way of acheiving the desired effect compared to Web Workers API; I am finding out that my AJAX script and setInterval function are unreliable; the script appears to freeze or not respond at certain initial loads and after running for long periods of time points . How can I modify my code to use workers instead of AJAX or setInterval?
setInterval(refreshXml, 1500);
function refreshXml() {
var req = $.get('Administration/data/people.xml');
req.done(function(xml) {
// Update the global XML variable used to create buttons.
window.peopleXml = xml;
// Clear existing buttons.
$('#loadMe').empty();
// Display a button for each XML person entity.
$(xml).find('fullName').each(function(index) {
var fullName = $(this).text();
$('<button>', {
'class': 'mybutton',
value: $(this).siblings('id').text(),
text: fullName
}).appendTo('#loadMe');
});
// Update any person divs that were already visible.
$('#toadMe .person').each(function() {
// Grabs the ID from data-person-id set earlier.
var id = $(this).data('person-id');
show_person(id);
});
});
}
function show_person(id) {
$('#person-detail-' + id).remove();
get_person(id).appendTo('#toadMe');
}
function get_person(id) {
var $person = $(window.peopleXml).find('id:contains(' + id + ')').parent();
var $div = $('<div>', {
'class': 'person',
'data-person-id': id,
id: 'person-detail-' + id
});
$('<h1>', { text: $person.find('firstName').text() }).appendTo($div);
$('<h1>', { text: $person.find('lastName').text() }).appendTo($div);
$('<h1>', { text: $person.find('age').text() }).appendTo($div);
$('<h1>', { text: $person.find('hometown').text() }).appendTo($div);
$('<h1>', { text: $person.find('job').text() }).appendTo($div);
return $div;
}
$(document).on('click', '.mybutton', function() {
$('#toadMe').empty();
show_person(this.value);
});
The name of the above script is home.js and here is an example of an index page (index.html) and a worker (my_task.js):
// index.html
<script>
var myWorker = new Worker("my_task.js");
myWorker.onmessage = function (oEvent) {
console.log("Worker said : " + oEvent.data);
};
myWorker.postMessage("ali");
// my_task.js
postMessage("I\'m working before postMessage(\'ali\').");
onmessage = function (oEvent) {
postMessage("Hi " + oEvent.data);
};
How can I implement home.js in a way in which index.html and my_task.js are implemented? Thanks a ton, I am really just looking for a way to get starting using workers as the next level up since I just recently learned AJAX. Also, I know this could possibly be seen as a broad question so I am willing to improve my question upon request and suggestions.
I am using Knockout to display some tags for an array of images. each tag will have a popup that gives more information about the tag. The elements is registered with popup class the following way:
function RegisterCharacterPopups() {
$('[data-characterid]').each(function() {
var cId = $(this).data('characterid');
var placement = $(this).data('position');
if (placement == null || placement == undefined) {
placement = "top-center";
}
$(this).PopUp({
url: "/Ajax/CharacterPop/" + cId,
position: placement,
});
});
}
And i have added this to my constructor of the view model that contains the tags:
// Hook on to update of Tags
ko.computed(() => {
var test = this.Tags();
RegisterCharacterPopups();
console.log("Tags updated");
});
I can see the methods is executed, but the tags do not register with the popup. if it force the Tags to update again, will it work though!
I think the problem is that this method in executed the first time, before the elements are in the html.
How can I fix this, so it will wait for the elements to be inserted before it executes it?
Solution: a custom binding
ko.bindingHandlers['tagpop'] = {
init: function (element, valueAccessor, allBindings, vm, context) {
var data = valueAccessor();
$(element).PopUp({
url: "/Ajax/CharacterPop/" + data.id,
position: data.placement,
});
},
update: function (element, valueAccessor) {
}
};
A bit of an architectural question...
I originally created a Javascript singleton to house methods needed to operate a photo gallery module in a template file for a CMS system. The original specification only called for one instance of this photo gallery module on a page. (The code below is a gross simplification of what I actually wrote.)
Shortly after releasing the code, it dawned on me that even though the specification called for one instance of this module, this code would fall apart if a page had two instances of the module (i.e. the user adds two photo galleries to a page via the CMS). Now, the HTML markup is safe, because I used class names, but how would I go about restructuring my Javascript and jQuery event listeners to be able to handle multiple modules? You may assume that each photo gallery has its own JSON-P file (or you may assume a single JSON-P file if you think it can be handled more elegantly with one JSON-P file).
I think my original jQuery event listeners might have to be converted to $.delegate(), but I have no clue what to do after that and what to do about converting my singleton. Any leads would be appreciated. If you offer code, I prefer readability over optimization.
I'm not asking this question, because I have an immediate need to solve the problem for work. I am asking this question to be forward-thinking and to be a better Javascript developer, because I am expecting to run into this problem in the future and want to be prepared.
Thank you for reading.
HTML
<div class="photoGalleryMod">
<div class="photoGalleryImgBox"><img src="http://www.test.org/i/intro.jpg" alt="Intro Photo" /></div>
<div class="photoGalleryImgCap"><p>Caption</p></div>
</div>
The Javascript is an external static file and makes a call to a JSON-P file via $.getSCript(), created by the CMS.
Javascript/jQuery
(function($) {
photoGalleryModule = {
json: '',
numSlidesInJson: '',
currentSlide: '',
updateSlide: function (arg_slidNum) {
/* Update the slide here */
},
init: function (arg_jsonObj) {
this.json = arg_jsonObj;
this.numSlidesInJson = this.json.photoGallerySlides.length;
this.currentSlide = 0;
}
};
$(document).ready(function() {
$.getScript('./photogallery.json');
$('.photoGalleryPrevImgLnk').live('click', function(event) {
photoGalleryModule.currentSlide = photoGalleryModule.currentSlide - 1;
photoGalleryModule.updateSlide(photoGalleryModule.currentSlide);
event.preventDefault();
});
$('.photoGalleryNextImgLnk').live('click', function(event) {
photoGalleryModule.currentSlide = photoGalleryModule.currentSlide + 1;
photoGalleryModule.updateSlide(photoGalleryModule.currentSlide);
event.preventDefault();
});
});
})(jQuery);
Contents of photo-gallery.json
photoGalleryModule.init(
{
photoGallerySlides:
[
{
type: 'intro',
pageTitle: 'Intro Photo',
imgUrl: 'http://www.test.org/i/intro.jpg',
imgAltAttr: 'Intro photo',
captionText: 'The intro photo',
},
{
type: 'normal',
pageTitle: 'First Photo',
imgUrl: 'http://www.test.org/i/img1.jpg',
imgAltAttr: 'First photo',
captionText: 'the first photo',
},
{
type: 'normal',
pageTitle: 'Second Photo',
imgUrl: 'http://www.test.org/i/img2.jpg',
imgAltAttr: 'Second photo',
captionText: 'the second photo',
}
]
});
I think the easiest way is to just turn your code into a plugin. So for the following HTML:
<div id="photoGallery1">
<div class="photoGalleryImgBox"></div>
<div class="photoGalleryImgCap"></div>
</div>
<div id="photoGallery2">
...
</div>
<div id="photoGallery3">
...
</div>
You would create the plugin with $.fn.photoGallery where you pass in an index as a parameter:
$.fn.photoGallery = function (index) {
var $this = this,
module = {
json: '',
numSlidesInJson: '',
currentSlide: '',
updateSlide: function (arg_slidNum) {
/* Update the slide here */
},
init: function (arg_jsonObj) {
module.json = arg_jsonObj;
module.numSlidesInJson = module.json.photoGallerySlides.length;
module.currentSlide = 0;
}
},
events = {
prev: function(e) {
module.currentSlide = module.currentSlide - 1;
module.updateSlide(module.currentSlide);
e.preventDefault();
},
next: function(e) {
module.currentSlide = module.currentSlide + 1;
module.updateSlide(module.currentSlide);
e.preventDefault();
}
};
$.getScript('./photogallery' + index + '.json');
$this.find('.photoGalleryPrevImgLnk').live('click', events.prev);
$this.find('.photoGalleryNextImgLnk').live('click', events.next);
};
And then initiate each gallery like so:
$(document).ready(function(){
$('#photoGallery1').photoGallery(1);
$('#photoGallery2').photoGallery(2);
$('#photoGallery3').photoGallery(3);
});
Where you have the files photogallery1.json, photogallery2.json and photogallery3.json that each invoke module.init({ ... }); with the necessary object data.
Something like this should do the trick: (untested)
// jquery plugin: jquery.photogallery.js
$.fn.photoGallery = (function($){
var PhotoGalleryModule = function(el, opts){
$.extend(this, opts);
this.el = $(el);
// if there are multiple on the page do not re-bind or re-init
if(!!this.el.data('photoGallery')) return el;
this.numSlidesInJson = this.json.photoGallerySlides.length;
this.bind();
};
PhotoGalleryModule.prototype = {
updateSlide: function (arg_slidNum) {
/* Update the slide here */
},
bind: function(){
var self = this;
this.el.find('.photoGalleryPrevImgLnk')
.live('click', function(event) {
self.currentSlide = self.currentSlide - 1;
self.updateSlide(self.currentSlide);
event.preventDefault();
});
this.el.find('.photoGalleryNextImgLnk')
.live('click', function(event) {
self.currentSlide = self.currentSlide + 1;
self.updateSlide(self.currentSlide);
event.preventDefault();
});
}
};
return function (opts) {
return this.each(function () {
$(this).data('photoGallery',
new PhotoGalleryModule(this, opts));
});
};
})(jQuery);
// activate
$(function(){
var ready = function(){
$('div.photoGalleryMod').photoGallery({
// similar technique as below to load json
json: { photoGallerySlides: { /*...*/} },
currentSlide: 0
});
};
// load script dynamically when needed
('photoGallery' in $.fn) ? ready() :
$.getScript('/scripts/jquery.photogallery.js', ready);
});