Tuesday 9 June 2015

Lightning Components and Unobtrusive JavaScript

Lightning Components and Unobtrusive JavaScript

Overview

Recently I’ve been experimenting with wrapping some of my existing HTML components as Lightning Components - you might have spotted my tweet around the multi-date picker :

Screen Shot 2015 06 09 at 07 57 17

When writing JavaScript I try to adhere to the principles of Unobtrusive JavaScript, specifically around the separation of behaviour from markup. This means that rather than using inline JavaScript of the form:

<button id=“clickme” onclick=“clicked();">Click Me</button>

I just provide the markup in HTML:

<button id=“clickme”>Click Me</button>

and then bind the click handler as part of JavaScript initialisation (usually via jQuery):

<script type="text/javascript">
  $(document).ready(function(){
    $(‘#clickme').click(clicked);
  });
</script>

The Code

Component

The component makes use of the jQuery JavaScript library:

<aura:component controller="UnobtrusiveController">
    <ltng:require scripts="/resource/jQuery_2_1_0"
                  afterScriptsLoaded="{!c.doInit}" />
    <button id="clickme">Click Me</button>
    <div id="results"></div>
</aura:component>

Controller

The controller contains a single method invoked after the jQuery library is loaded, which delegates to the helper:

({
	doInit : function(component, event, helper) {
		helper.doInit(component);
	}
})

Helper

The helper carries out the heavy lifting (such as it is in this noddy example), binding the click handler to the button and defining the function that will be executed in response to a click event, which executes a server side method in the Apex controller.

({
	doInit : function(cmp) {
		var self=this;
		$('#clickme').click(function (ev) {self.clicked(cmp, ev);});
	},
	clicked: function(cmp, ev) {
		var action = cmp.get("c.Method1");

		action.setCallback(this, function(response) {
			if (response.getState() === "SUCCESS") {
				$('#results').html(response.getReturnValue());
			} else if (a.getState() === "ERROR") {
				console.log("Errors", response.getError());
			}
		});
		$A.enqueueAction(action);
	}
})

Apex Controller

The Apex controller contains a single method that returns a string showing that it was executed.

public class UnobtrusiveController {
	@AuraEnabled
	public static String Method1()
	{
		return 'Method 1 called';
	}
}

Application

A minimal lightning application is used to surface the component.

<aura:application >
    <bblightning:Unobtrusive />
</aura:application>

The Challenge

Adopting this approach with Lightning Components brings an additional challenge, in that when I click on my button, nothing appears to happen.  As I’m not using Lightning all day every day, my initial assumption whenever this happens (and its usually correct) is that I’ve got something wrong about my action or response handler, but the error is being swallowed.  Usually a few lines of debug logging to the console will show this, so I update my helper accordingly:

clicked: function(cmp, ev) {
	var action = cmp.get("c.Method1");
	console.log('Action = ' + JSON.stringify(action));
	action.setCallback(this, function(response) {
		console.log('In callback response = ' + JSON.stringify(response));
		if (response.getState() === "SUCCESS") {
			console.log('Return value = ' + response.getReturnValue());
			$('#results').html(response.getReturnValue());
		} else if (a.getState() === "ERROR") {
			console.log("Errors", response.getError());
		}
		console.log('Updated results');
	});
	console.log('Enqueuing action');
	$A.enqueueAction(action);
	console.log('Action enqueued');
}

Note that I’m using JSON.stringify() to output the action - this is an excellent utility, although if there are any circular references it will fail. Clicking the button now shows the following output in the console:

"Action = {"id":"6;a","descriptor":"apex://bblightning.UnobtrusiveController/ACTION$Method1","params":{}}" app.js:35:0
"Enqueuing action" app.js:36:131
"Action enqueued"

so no errors, the code proceeded as expected, but no sign of the callback being executed.  Turning on debug logging on the server also showed no sign of the Apex controller.  

As this was happening the day after my Salesforce org had been upgraded to Summer 15, I started to wonder if something had gone awry there, but trying out some of my other lightning components in the same instance showed that this wasn’t the case. My next though was the jQuery library namespace interfering with the lightning framework, but enabling no conflict mode didn’t help either. I then executed the clicked() function upon initialisation:

doInit : function(cmp) {
	var self=this;
	$('#clickme').click(function (ev) {self.clicked(cmp, ev);});
	this.clicked(cmp);
}

This time everything worked as expected, and the page contents were updated when the action completed, and all expected debug appeared.

The Solution

The issue was something to do with the way that the action was created and queued. The obvious candidate was the fact that the event handler function was bound to the element via unobtrusive JavaScript, rather than binding a component controller method via markup. After some digging around in the Lightning Developer's Guide, it became apparent that by doing this I'd stepped outside of the normal component lifecycle, so there was nothing in place to execute the queued action once my method completed. In a situation where I didn’t need immediate feedback to an event this would be fine, as it would get ‘boxcar’-d with the next action(s) that the framework sent. However, in this case I wanted it to fire immediately.

Luckily the architects of Lightning have thought about this, step up “Modifying Components Outside the Framework Lifecycle” and specifically the $A.run() method, which ensures the framework processes any queued actions - kind of like Test.stopTest() processes all queued asynchronous methods when unit testing.  Changing my code to execute this after queuing the action meant that my action executed as expect:

console.log('Enqueuing action');
$A.enqueueAction(action);
console.log('Action enqueued');
$A.run();

Related Posts