Welcome to this review of the Pluralsight course Advanced Techniques in JavaScript and JQuery by Kevin Murray.
Kevin is a lifelong learner with over 30 years in the IT industry. He works on web and mobile applications as well as the databases and web services to support them. With a gift for learning new languages, he is able to rapidly apply his broad experience in new environments.
Kevin has also produced courses on Making Work from Home Work for You, Managing Developers and a Developer’s Guide to SQL Server CLR Integration
Introduction
Traditional JavaScript Functions
gobj is bad name
function1();
function2();
obj.function3();
gobj.function4();
Draw Version 1
HTML5 Canvas Draw Function
+ Contained in a global namespace
+ Doesn’t accept parameters
+ Verifies the canvas context is accessible
Extending Draw – Two Parameters
Add some parameters
+ Left Position
+ Right Position
Stub Functions
Stub Downsides:
+ Documentation needed
+ Multiple versions of legacy code not supported
+ Wrong result if calling wrong function
Default Parameters
Provide defaults within the function when parameters are missing
+ Useful when one or two parameters are optional
+ Optional parameters should be at the end of the parameters list
+ If more than one parameters is optional, certain assumptions must be made if only one is supplied
All Default Values
Provide defaults for remaining parameters
- All parameters are optional
- Order of parameters is still important
- May have to pass “undefined” as a position holder
Changing to an Object Parameter
Use one object parameter instead of multiple native types
draw: function(options)
{
…
if (typeof options !== ‘object’)
options = {};
…
}
- Objects are just named value pairs
- Properties of an object are optional by nature
- The order of object properties does not matter
- Object parameters are passed by reference
Legacy Support with Object
Honor contract with legacy code
draw: function(options)
{
…
if (typeof left !== ‘undefined’)
if (typeof left === ‘object’)
options = left;
else
options = { left: left,
top: top,
width: width,
height: height,
stroke: stroke,
fill: fill };
…
}
Default Values in an Object
// Setting canvas width will clear its contents
if (options.clear)
…
drawOptions:
{ left: 10, top: 10,
width: 100, height: 100,
stroke: ‘black’, fill: ‘silver’,
clear: false
}
Function Summary
Object Parameters are clean and future proof your code
Event Handling
Shorthand Methods
Handling standard events
- Usually use an anonymous function
- For common events, shorthand method can be used
.blur()
.change()
.click()
.dblclick()
.focus(), focusin(0,.focusout()
.hover()
.keydown(),.keypress(), .keyup()
.load()
.mousedown(), .mouseenter(), .mouseleave(), .mousemove(), .mouseout(), .mouseover(), .mouseup()
etc.
Work Area
$(function()
{ $(‘.clickable’).click(function()
{ $(‘#Messages’).append(‘click occurred
‘);
});
});
Message Function
- Centralize message handling
- Use an object parameter
- Explicitly set what “this” references
$(function()
{ var showEventMessage = function(options)
{ options = $.extend(
{ eventType: ‘CLICK’,
eventTarget: this,
suffix: ‘
‘
}, options);
var message = options.eventType + ‘: ‘ +
(options.eventTarget.nodeName || ‘unknown’) +
options.suffix;
$(‘#Messages’).append(message);
};$(‘.clickable’).click(function()
{ showEventMessage.call(this, { suffix: ‘!
‘ });
});
});
Making use of call, and its close relative apply, can help simplify numerous check within a function
that try to determine what this references. In our case, we can be sure that this will refer to
More Elements
- Add clickable to more elements
- Add more event handlers
- Preventing default behaviour
- Internet Explorer is different
Chasm between user and developer expectations
$(‘.clickable’)
.click(function()
{ showEventMessage.call(this);
}}
.dblclick(function()
{ showEventMessage.call(this, { eventType: ‘double click’}};
}}
.mousedown(function()
{ showEventMessage.call(this, { eventType: ‘mouse down’}};
return false;
});
}};
Anonymous function passed in is not executed right away. It is just attached as a mousedown event handler to
the element
IE and Firefox handle events differently
Propagation
+ Events bubble up the DOM until handled
Ways to cancel event processing:
+ Return false from event handler (any version)
+ Call preventDefault() on the event (IE9+ when native)
+ Call stopPropagation() on the event (jQuery)
+ Call stopImmediatePropagation() on the event (jQuery)
jQuery provides methods to determine if the state of the event has been changed
+ isPropagationStopped()
+ isImmediatePropagationStopped()
+ isDefaultPrevented()
Some browsers may not support returning false from event handlers.
W3C spec doesn’t require it
$(‘.clickable’)
.click(function(event)
{ showEventMessage.call(this);
}}
.dblclick(function(event)
{ showEventMessage.call(this, { eventType: ‘double click’}};
}}
.mousedown(function(event)
{ showEventMessage.call(this, { eventType: ‘mouse down’}};
event.stopPropagation(); //or stopImmediatePropagation
event.preventDefault(); //call method on the event object
})
.mousedown(function(event)
{ showEventMessage.call(this, { eventType: event.type,
suffix: ‘#2
‘ });
});
}};
For stopPropagation this outputs:
mousedown: H5
mousedown: H5#2
For stopImmediatePropagation it outputs:
mousedown: H5
To manage the event without returning false, use stopPropagation()
it will NOT be passed up the DOM tree. Mouse event is only processed one time.
No Shorthand
Event Handlers Without Shorthand
.bind()
.unbind()
.live()
.die()
.delegate()
.undelegate()
.on() //supercedes bind, live and delegate
.off() //supercedes unbind, die, and undelegate
.one() //attach event handler that will only be processed one time
Summary
- Shorthand methods ultimately reference the .on() method
- Event bubble up the DOM from the innermost elements
- .bind(), .live(), and .on() are other ways to attach events
- .on is the preferred attachment method
Advanced Event Handling
Event Handler Methods
- Change shorthand methods to use .on() method
- Consolidate event handler functions
- Pay attention when using .off()
Named Functions
- Use a named function as the event handler
- Easy to remove
- Can be centralized in a library
- Updating one handler function has global impact
Namespace
Event namespace doesn’t interfere with processing of system-generated events
Its part of how jQuery manages events
$(‘.clickable’)
.on(‘click demo dblclick.demo mousedown.demo’, function(event)
{ if(!event.isPropoagationStopped() &&
!event.isImmediatePropagationStopped() &&
!event.isDefaultPrevented())
{ …
Using a namespace makes it easy to target specific event handlers for removal
Delegation
+ Bind event handlers higher in the DOM
+ Efficiency
+ Centralized processing
$(‘li’).bind(‘click’, function(event)
{ //process click event
})
Custom Events
+ Create new events for DOM elements
+ Create events for regular object
+ Initiate a custom event with .trigger()
$(‘.clickable’)
.on(‘click’, function(event)
{ var $this = $(this),
clickCount = ($this.data(‘clickcount’) || 0) +1;$this.data(‘clickcount’, clickCount);
showEventMessage.call(this,
{ eventType: ” + clickCount + ‘) ‘ + event.type });if (clickCount === 3)
$this.trigger(‘click3’);
})
.on(‘click3’, function(event)
{ event.stopPropagation();
showEventMessage.call(this, {eventType: event.type});
$(this).addClass(‘highlight’); //highlighted in yellow
});
Can attach event handler to internal object stored in memory just as we can with objects in the DOM:
$(internalObject)
.on(‘recordsloaded’, function(event)
{ showEventMessage.call(this, {eventType: event.type});
$.each(internalObject.records, function()
{ $(‘#Messages’).append(‘ — ‘ +
this.description + ‘: ‘ + this.value + ‘
‘);
});
});
Event Parameters
$(notifyObject)
.on(‘recordsloaded’, function(event)
{ showEventMessage.call(this, {eventType: event.type});
$.each(internalObject.records, function()
{ $(‘#Messages’).append(‘ — ‘ +
this.description + ‘: ‘ + this.value + ‘
‘);
});
});
Using jQuery Deferred Objects
Kevin says most JavaScript developers have been conditioned to use callback functions and many aren’t aware of jQuery’s deferred objects.
jQuery Deferred Processing
This is an implementation of the JavaScript promise, and Kevin explains how these work in this clip.
A promise states that the result of an asynchronous process will be reported to the calling code at some point in the future.
Kevin says the Promise pattern is underused because many developers are unaware of it.
Traditional AJAX Processing
$.get(‘SomeFile.html’, function(result)
{ // This is the callback function
//Process the result and set some flag to indicate completion
});
Kevin briefly discusses how traditional AJAX processing gets problematic
Loading Dynamic Content – Layout
As an example, let’s see if we can deliver the following requirements:
- Load content into three <div> elements – asynchronously
- When all content is loaded, enable another element
- Only load content when user clicks the “Load” button
We see the layout, and we’re using CSS to style the divs and position them side-by-side.
Kevin also shows us the HTML and the CSS that generates our prototype webpage. The only JavaScript so far is an empty document ready function.
Section 1 Content
First we use jQuery to listen for the click on the Load button, and run an anonymous function in response to the click.
$(function()
{
$(‘#Load’).click(function()
{
$(‘#Section1’).load(‘Content1.html’, function()
{ $(‘#Proceed’).removeAttr(‘disabled’);
}
});
});
Everything seems to work properly at first, but there are a couple of problems. Kevin says we can’t trust our eyes.
Loading All Content
Debugging timing problems can be very troublesome. Adding tracing logic could make everything work correctly, and it could fail to work again when the tracing code is removed.
Common Pattern
Using nested callbacks. Bleugh!:
$(function()
{
$(‘#Load’).click(function()
{
$(‘#Section1’).load(‘Content1.html’, function()
{
$(‘#Section2’).load(‘Content2.html’, function()
{
$(‘#Section3’).load(‘Content3.html’, function()
{ $(‘#Proceed’).removeAttr(‘disabled’);
});
});
});
});
This is the Christmas tree effect that was covered in Wee Higbee’s Reasoning About Asynchronous JavaScript course.
We can do better.
Deferred Object
Kevin says many people are left scratching their heads even after reading the documentation.
When all content is loaded then we want to enable the proceed button. So this suits the jQuery syntax:
$(‘#Load’).click(function()
{ $.when{$.get(‘Content1.html’, function(result)
{ $(‘#Section1’).html(result);
}, ‘html’),
$.get(‘Content2.html’, function(result)
{ $(‘#Section2’).html(result);
}, ‘html’),
$.get(‘Content3.html’, function(result)
{ $(‘#Section3’).html(result);
}, ‘html’)
}
.then(function()
{ $(‘#Proceed’).removeAttr(‘disabled’);
});
});
});
Kevin runs through how this code works. Basically it implements the when-then statement above.
We see that this works as expected. The code is more extensible and maintainable then using callbacks.
When/Then pattern
- Can include as many gets as desired
- Then processing only occurs once all When methods complete
- Extensible, but readability could be improved
LoadSection Function
Create a function to load content
- Load specified URL into specified element
- Return the results of a “get” method
- Needs two parameters – for now
Negative Testing
What happens if we have
- Check invalid URL
- Check blank section element
- Alternate syntax for When/Then
Proceed button isn’t enabled if invalid URL
$(function()
{ var loadSection = function(options)
{
…
}$(‘#Load’).click(function()
{ $.when(
loadSection({ selector: ‘#Section1’, url: ‘Content11.html’ }),
loadSection({ selector: ‘#Section2’, url: ‘Content2.html’ }),
loadSection({ selector: ‘#Section3’, url: ‘Content3.html’ }),
).then(function()
{ $(‘#Proceed’).removeAttr(‘disabled’);
},function(result)
{ $(‘#Messages’).append(‘Failure!
‘)
.append(‘Result:’ + result.statusText + ‘
‘);
});
});
});
This above code doesn’t handle multiple errors. Try using the .fail method using chaining:
…
.then(function()
{ $(‘#Proceed’).removeAttr(‘disabled’); //success
})
.fail(function(result)
{ $(‘#Messages’).append(‘Failure!
‘)
.append(‘Result:’ + result.statusText + ‘
‘);
});
});
});
The when, then and fail methods all execute almost immediately.
They setup event handlers for deferred processing.
Deferred Methods
Some methods available on deferred objects
Return deferred object for a process flow
- when
- promise
(the promise object returns just a subset of the deferred object that lets us attach handlers like we’ve been doing, but doesn’t allow the state of the deferred object that lets us attach handlers like we’ve been doing, but doesn’t allow the state of the deferred object to be changed by us)
Attach handler functions to deferred object
- then
- done – This method attaches a success handler function. The function will only execute when all processing completes successfully.
- fail – Attaches a failure handler function. Only called once, regardless of number of errors
- progress – Attaches a progress handler function. Most commonly for updating progress bars
- always – like finally after a try/catch
(these functions will be called when the event associated with a name occurs for that object.)
Change state of deferred object
- resolve – change state to indicate successful completion
- reject – change state to indicate failure
$(function()
{ var loadSection = function(options)
{
…
}$(‘#Load’).click(function()
{ var myDefer = $.when(
loadSection({ selector: ‘#Section1’, url: ‘Content11.html’ }),
loadSection({ selector: ‘#Section2’, url: ‘Content2.html’ }),
loadSection({ selector: ‘#Section3’, url: ‘Content3.html’ }),
) //this returned object can have it’s state changed
.promise() //make sure nothing we do accidentally changes the state
.done(function()
{ myDefer.done(function()
{ $(‘#Messages’).append(’embedded done handler
‘);
});
})
.done(function()
{ $(‘#Proceed’).removeAttr(‘disabled’);
})
.fail(function(result)
{ $(‘#Messages’).append(‘Failure!
‘)
.append(‘Result:’ + result.statusText + ‘
‘);
})
.always(function()
{ $(‘#Proceed’).removeAttr(‘disabled’);
});
});
});
There are other AJAX methods within jQuery that also return a deferred object.
Dynamic Pages
The dynamically loaded pages had static content
Dynamic pages vs static pages
- Scripts execute on loaded page
- Loaded page also loads embedded elements
- Loaded page may wait for user interaction
Our previous logic could fail because we only monitor the successful loading of content and not whether any embedded scripts are executed.
Content1a.html
$(function()
{ setTimeout(function()
{ $(‘#Content1’)
.append(”)
.append(‘more content loaded dynamically’);
}, 2000);
});
Creating a Deferred Object
A self-managed deferred object is useful when
- Writing long running processes
- Loading content that contains embedded content
- Wrapping a process flow into a single process
$(function()
{ var loadSection = function(options)
{ var defer = $.Deferred(),
$div;if (typeof options !== ‘object’)
return defer.reject({ statusText: ‘Expecting parameter object’ });
//This will trigger the fail method using the statusText objectoptions.selector = options.selector || ”;
options.url = options.url || ”;if (options.url === ”)
return defer.reject({ statusText: ‘Missing URL’ });$div = $(options.selector)
if ($div.length > 0)
{ $div.load(options.url, function()
{ defer.resolve();
});
}
else
//we didn’t find the selector element
defer.reject({ statusText: ‘Error in selector’});return defer; //immediately return this.
//It will remain in a pending state until we either resolve or reject it
//may have succeeded or failed
}
Defer will get resolved even if the load method fails. jQuery calls a success callback upon completion of the load method regardless of whether it is truly successful or not.
jQuery defer method can only have one failure. If we need to know more, we have to do it on each deferred object we create.
Current state of affairs
- Proceed button enabled before dynamic content is loaded
- Meaningful error messages
- Only one error message per process flow
Using Deferred Objects
$(function()
{ var showMessage = function(options)
{ if (typeof options !== ‘object’)
options = { message: ‘Parameter not an object’, error: true };options.message = options.message || false;
options.error = options.error || false;$(‘#Messages’)
.append(options.error ? ‘Error: ‘ : ”)
.append(options.message)
.append(‘
‘);
},
loadSection = function(options)
{ var defer = $.Deferred(),
$div, msg;if (typeof options !=== ‘object’)
{ msg = ‘Expecting parameter object’;
showMessage({ message: msg, error: true });
return defer.reject({ statusText: msg });
}options.selector = options.selector || ”;
options.url = options.url || ”;
options.dynamic = options.dynamic || false;if (options.url === ”)
{ msg = ‘Missing URL’;
showMessage({ message: msg, error: truew });
return defer.reject({ statusText: msg });
}$div = $(options.selector)
if ($div.length > 0)
{ $.get(options.url, function(){}, ‘html’)
.done(function(result)
{ $div.html(result);
if (!options.dynamic)
{ defer.resolve();
}
})
.fail(function(result)
{ msg = ‘Could not load URL:’ + options.url;
showMessage({ message: msg, error: true });
defer.reject(result);
});
if (options.dynamic)
$div.off(‘complete, failure’)
.on(‘complete’, function(event)
{ defer.resolve();
})
.on(‘failure’, function(event, result)
//event e.g. click, blur, mouseover
//additional info in result parameter
{ msg = ‘Dynamic content: ‘ + result.statusText;
showMessage({ message: msg, error: true });
defer.reject(result);
});
}
else
{ msg = ‘Error in selector’;
showMessage({ message: msg, error: true });
defer.reject({ statusText: msg });
}return defer.promise();
}$(‘#Load’).click(function()
{ $.when(
showMessage({ message: ‘Started processing’ }),
//additional dynamic property
loadSection({ selector: ‘#Section1’, url: ‘Content1b.html’,
dynamic: true }),
loadSection({ selector: ‘#Section2’, url: ‘Content2.html’ }),
dynamic: true }),
loadSection({ selector: ‘#Section3’, url: ‘Content3a.html’,
dynamic: true }),
showMessage({ message: ‘Done with processing’ })
)
.done(function()
{
})
.fail(function(result)
{ $(‘#Messages’).append(‘Failure!
‘)
.append(‘Result:’ + result.statusText + ‘
‘);
})
.always(function()
{ $(‘#Proceed’).removeAttr(‘disabled’);
});
});
We can get two error messages returned.
The second message is in response to our fail method that’s part of the when block.
Takeaway:
When we reject our deferred object, that immediately rejects the deferred object used by the when block.
That causes the failed process and also causes the always method to immediately process.
Once the state of a deferred object is established, it can’t be changed.
Our current logic
When processing these embedded functions, run the done function immediately upon success, or run the fail function immediately upon failure.
That’s exactly what is happening in this case. Since we’ve purposely introduced negative test into this process, we’re encountering something that may not become apparent for a very long time. Under normal circumstances, this situation may not present itself for months.
What we really want our logic to be
When processing the embedded functions, report errors to the message area and keep processing. When everything is processed, enable the Proceed button.
The key point of that logic is that we’re handling the errors directly in the loadSection function.
There is no need to ever reject the deferred object that we’re using.
Once we report the error message we can resolve the deferred.
The only thing we’re doing in the fail logic is duplicating the error message.
[Changes all reject methods to resolve methods]
Now the Proceed button doesn’t enable until after the dynamic content is loaded.
Even we an error is present.
We must consider WHEN, not IF we get an error. Logic must hold up in unexpected circumstances.
Using Deferred Objects
Kevin explains the pro’s and cons of deferred Objects:
- Use a deferred object in a function we create
- Notify when dynamic content is loaded
- Resolve or Reject deferred object as appropriate
- Combining deferred objects with custom events give maximum flexibility in process control
- Functions that don’t return a deferred object are resolved immediately – when embedded in a WHEN block
- An embedded deferred object that is rejected will cause the containing deferred object to be rejected, also
- Negative testing is very important