Pre-populating Standard Page Layouts

detour sign

Warning: This uses undocumented methods. As with any code that relies on anything undocumented it’s not supported and it’s prone to breaking without warning so don’t rely on it working tomorrow!

It’s a known hidden feature of Salesforce that you can pre-populate fields on a standard page layout if you know the name of the fields. There are plenty of blogs on it. For example you can pre-populate the Account Name with the URL:

https://na43.salesforce.com/001/e?acc2=Peace+Without+Walls+Inc&acc5=012345

screenshot-2017-02-28-21-51-44

This gets a bit trickier still on custom page layouts:

https://na43.salesforce.com/a0Q/e?Name=Red%20Widget&00N0G00000DaxKQ=RED

screenshot-2017-02-28-21-53-45

Yeuck! Where did 00N0G00000DaxKQ come from? You have to dig it out using your browser developer tools.

Another blog post on the matter …

So to make this a bit easier, we’re going to write a little VF page and Controller to fathom it all out for us. We’ll only need to remember nice things like Developer Names for stuff.

To do this we need to query two system objects, the EntityDefinition and the FieldDefinition.

The Entity Definition will give us the URL and its Id as contained in the DurableId field.

Select DeveloperName, DurableId, EditUrl, NewUrl From EntityDefinition Where DeveloperName = 'Widget'

DeveloperName: Widget
DurableId: 01I0G000002e6SD
EditUrl: /{ID}/e
NewUrl: /a0Q/e

Using the DurableId from the first Query we can get the Field Definitions:

Select DeveloperName, DurableId, EntityDefinitionId, RunningUserFieldAccessId From FieldDefinition Where EntityDefinitionId = '01I0G000002e6SD' and DeveloperName = 'Colour'

DeveloperName: Colour
DurableId: 01I0G000002e6SD.00N0G00000DaxKQ
EntityDefinitionId: 01I0G000002e6SD

By stripping away everything after and including the . on the DurableId we are left with the Field Id we need to use in our URL.

So lets build something a bit more usable …

Controller:

public without sharing class PreloadPageLayoutController
{
	public String RedirectURL {get; set;}

	private static final String OBJECT_PARAMETER_KEY = 'o';

	/**
	 * Constructor - Builds URL
	 */
	public PreloadPageLayoutController()
	{
		// Page Parameter Map
		Map paramMap = ApexPages.currentPage().getParameters();

		// Get the Object Parameter
		String objectName = paramMap.get(OBJECT_PARAMETER_KEY);

		// Map the Key Pairs
		Map fieldMappings = new Map();
		fieldMappings.putAll(paramMap);
		fieldMappings.remove(OBJECT_PARAMETER_KEY);

		try
		{
			// Query the Entity Definition for the Object
			EntityDefinition ed = [Select DeveloperName, DurableId, EditUrl, NewUrl
									From EntityDefinition
									Where DeveloperName = :objectName];

			Map fieldNameToIdMap = new Map();

			Set fieldNames = fieldMappings.keySet();

			// Query the Field Definition for the Fields and Map the Developer Names to the Field Ids
			for (FieldDefinition fd : [Select DeveloperName, DurableId, EntityDefinitionId
										From FieldDefinition
										Where EntityDefinitionId = :ed.DurableId and
											DeveloperName in :fieldNames])
			{
				String[] idParts = fd.DurableId.split('\\.');

				fieldNameToIdMap.put(fd.DeveloperName, idParts[1]);
			}

			// Build the URL
			RedirectURL = ed.NewUrl;

			String prefix = '?';

			// Loop through the parameters
			for (String key : fieldMappings.keySet())
			{
				String value = fieldMappings.get(key);
				String fieldId = fieldNameToIdMap.get(key);

				String paramTemplate = '{0}{1}={2}';
				String param = String.format(paramTemplate, new String[] { prefix, fieldId == null ? key : fieldId, value });
				RedirectURL += param;

				prefix = '&';
			}

			ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Redirecting to: ' + RedirectURL));
		}
		catch(Exception e)
		{
			ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, e.getMessage() + ' ' + e.getStackTraceString()));
			RedirectURL = null;
		}
	}

	/**
	 * Action Method to Redirect
	 */
	public PageReference redirect()
	{
		PageReference ref;

		if (RedirectURL != null)
		{
			ref = new PageReference(RedirectURL);
			ref.setRedirect(true);
		}

		return ref;
	}
}

Quick Unit Test Class:

@isTest
private class PreloadPageLayoutControllerTest {

	@isTest static void testParams()
	{
		Map paramMap = ApexPages.currentPage().getParameters();
 		paramMap.put('o', 'Account');
 		paramMap.put('Name', 'Test');
 		paramMap.put('acc2', 'Test2');

		PreloadPageLayoutController controller = new PreloadPageLayoutController();
		PageReference ref = controller.redirect();
		Map newParamMap = ref.getParameters();
		System.assert(newParamMap.containsKey('Name'));
		System.assertEquals('Test', newParamMap.get('Name'));
		System.assert(newParamMap.containsKey('acc2'));
		System.assertEquals('Test2', newParamMap.get('acc2'));
	}
}

Visualforce Page: PreloadPageLayout:


		If your page does not redirect automatically click here

We can now pass the parameters to our custom page instead but instead of having to inspect the page and work out the yeuky ids we can use the Developer Names instead:

https://na43.salesforce.com/apex/PreloadPageLayout?o=Widget&Name=Red+Widget&Colour=RED

It also works on Standard Objects for Custom fields only. For Standard fields you still need to use acc1, acc2, acc3 etc:

https://na43.salesforce.com/apex/PreloadPageLayout?o=Account&acc2=Peace+Without+Walls+Inc&acc5=012345&My_Rating=Good

You can also use it from your own Apex Code to do a redirect and pre-load the page:

public PageReference redirect()
{
	PageReference ref = Page.PreloadPageLayout;
	ref.getParameters().put('o', 'Widget');
	ref.getParameters().put('Name', 'Red Widget');
	ref.getParameters().put('Colour', 'RED');

	return ref;
}

Another use is a Custom Button to Add Red Widgets:

screenshot-2017-03-01-19-18-29

which you can then add onto the Search Layout.

Tagged with: , , ,
Posted in apex, code, force.com, salesforce

The Trouble with Dates and Times

SundialDates and DateTimes don’t always play nicely when you’re trying to push them through from environment to environment. You often have to mess about with them a bit to shoehorn them in and out of Salesforce.

I’m going to look here at how you get them to/from Javascript in the case of Visualforce remoting and then to/from JSON Strings if you’re integrating to another service where you need to use REST. There will no doubt be other ways to accomplish this and other scenerios but these are common use cases so this is more a starter for 10.

Javascript to Salesforce via Visualforce Remoting

Let’s begin by looking at going from Javascript to Salesforce via Visualforce Remoting.

Date example:

Javascript in a VF Page:

var dt = new Date();

console.log('dt=' + dt.toUTCString());

Visualforce.remoting.Manager.invokeAction(
    '{!$RemoteAction.RemoteController.sendDate}',
    dt.toUTCString(),
    function(result, event) {
        if (event.status)
        {
            console.log('result=' + result);
        }
    }
);

Apex Controller Methods:

@RemoteAction public static String sendDate(Date dt) 
{
    String r = 'sendDate: ' + dt;
    return r;
}

Results:

dt=Wed, 21 Sep 2016 18:40:43 GMT
result=sendDate: 2016-09-21 18:40:43

Strangely, you’ll notice the resulting date still has the time element preserved when you emit it. It shouldn’t be an issue since in Salesforce you’ll be throwing an instance of Date around. Just be careful when sending Dates back that were constructed in this way. It would probably be safer to use a DateTime in the remoting method and the use date() to return the date portion if that’s what you want.

DateTime example:

Javascript in a VF Page:

var dt = new Date();

console.log('dt=' + dt.toUTCString());

Visualforce.remoting.Manager.invokeAction(
    '{!$RemoteAction.RemoteController.sendDateTime}',
    dt.toUTCString(),
    function(result, event) {
        if (event.status)
        {
            console.log('result=' + result);
        }
    }
);

Apex Controller Methods:

@RemoteAction public static String sendDateTime(DateTime dt) 
{
    String r = 'sendDateTime: ' + dt;
    return r;
}

Results:

dt=Wed, 21 Sep 2016 18:40:43 GMT
result=sendDateTime: 2016-09-21 18:40:43

Salesforce to Javascript via Visualforce Remoting

Date example:

Javascript in a VF Page:

Visualforce.remoting.Manager.invokeAction(
    '{!$RemoteAction.RemoteController.getDate}',
    function(result, event) {
        if (event.status)
        {
            var d = new Date(result);
            console.log('d=' + d);
        }
    }
);

Apex Controller Methods:

@RemoteAction public static Date getDate() 
{
    return System.today();
}

Results:

d=Wed Sep 21 2016 01:00:00 GMT+0100 (BST)

DateTime example:

Javascript in a VF Page:

Visualforce.remoting.Manager.invokeAction(
    '{!$RemoteAction.RemoteController.getDateTime}',
    function(result, event) {
        if (event.status)
        {
            var dt = new Date(result);
            console.log('dt=' + dt);
        }
    }
);

Apex Controller Methods:

@RemoteAction public static DateTime getDateTime() 
{
    return System.now();
}

Results:

dt=Wed Sep 21 2016 20:16:17 GMT+0100 (BST)

Salesforce to JSON String

Date Example:

Apex code:

Date d = System.today();
String ds = JSON.serialize(d);
System.debug('d=' + d);
System.debug('ds=' + ds);

Results:

d=2016-09-21 00:00:00
ds="2016-09-21"

DateTime Example:

Apex code:

DateTime dt = System.now();
String dts = JSON.serialize(dt);
System.debug('ds=' + dt);
System.debug('dts=' + dts);

Results:

ds=2016-09-21 18:51:25
dts="2016-09-21T18:51:25.234Z"

JSON String to Salesforce

Now this is where the fun begins! You’d think that if you serialize a date or datetime, you can simply deserialize it back again wouldn’t you? WRONG!

Date Example:

This won’t work:

Date d = System.today();
String ds = JSON.serialize(d);
Date d2 = (Date)JSON.deserializeUntyped(ds);
System.debug('d=' + d);
System.debug('ds=' + ds);
System.debug('d2=' + d2);

Nor will this:

Date d = System.today();
String ds = JSON.serialize(d);
Date d2 = Date.valueOf(ds);
System.debug('d=' + d);
System.debug('ds=' + ds);
System.debug('d2=' + d2);

You need to do this:

Date d = System.today();
String ds = JSON.serialize(d);
Date d2 = Date.valueOf(ds.replace('"', ''));
System.debug('d=' + d);
System.debug('ds=' + ds);
System.debug('d2=' + d2);

Results:

d=2016-09-21 00:00:00
ds="2016-09-21"
d2=2016-09-21 00:00:00

DateTime Example:

This won’t work:

DateTime dt = System.now();
String dts = JSON.serialize(dt);
DateTime dt2 = (DateTime)JSON.deserializeUntyped(dts);
System.debug('dt=' + dt);
System.debug('dts=' + dts);
System.debug('dt2=' + dt2);

Nor will this:

DateTime dt = System.now();
String dts = JSON.serialize(dt);
DateTime dt2 = DateTime.valueOf(dts);
System.debug('dt=' + dt);
System.debug('dts=' + dts);
System.debug('dt2=' + dt2);

You need to do this:

DateTime dt = System.now();
String dts = JSON.serialize(dt);
DateTime dt2 = DateTime.valueOf(dts.replace('"', ' ').replace('T', ' '));
System.debug('dt=' + dt);
System.debug('dts=' + dts);
System.debug('dt2=' + dt2);

Results:

dt=2016-09-21 19:01:27
dts="2016-09-21T19:01:27.217Z"
dt2=2016-09-21 18:01:27

As I said, there are more ways to crack a nut and there are no doubt more scenarios but hopefully this will help avoid some of the confusion.

Tagged with: , , , , ,
Posted in apex, code, force.com, salesforce

Mocking Multiple HTTP Requests to the Same Endpoint

mockMy Blog posts are like buses. You don’t see one for ages and then two come along at once.

I was musing, like you do, what to do when Unit Testing a service that makes successive call-outs to the same endpoint. If the endpoints were different then you could use the MultiStaticResourceCalloutMock class. But what if you need to make two calls to the same endpoint?

So take this bit of code:

public with sharing class MyCallout
{
	public static Boolean doCallouts()
	{
		Boolean isDog = isDog('Toto');
		Boolean isHuman = isHuman('Dorothy');

		return isDog && isHuman;
	}

	private static Boolean isDog(String name)
	{
		return getSpecies(name) == 'dog';
	}

	private static Boolean isHuman(String name)
	{
		return getSpecies(name) == 'human';
	}

	private static String getSpecies(String name)
	{
		HttpRequest req = new HttpRequest();

		req.setMethod('GET');
		req.setHeader('Content-type', 'application/json');
        req.setHeader('Accept', 'application/json');

		req.setEndpoint('https://somewhereovertherainbow.com/wherestoto');

		req.setBody('{"name" : "' + name + '"}');

		Http http = new Http();

		HttpResponse res = http.send(req);

		if (res.getStatusCode() != 200)
		{
			return null;
		}

		Map marshalledResponse = (Map)JSON.deserializeUntyped(res.getBody());

		String species = (String)marshalledResponse.get('species');

		return species;
	}
}

It makes two successive calls to the same endpoint to resolve the species of a given Wizard Of Oz character. If they both match it returns true. Simple enough, but what about testing it?

In our unit test we create our mock response class (implementing the HttpCalloutMock interface) and add a simple constructor that takes a list of responses.

The respond method then returns the responses in sequence each time it’s called. Easy peasy lemon squeezy.

@isTest
private class MyCalloutTest
{
	@isTest static void testDoCallouts_pass()
	{
		// Create the responses in the correct sequence
		String[] responses = new String[] {
			'{ "species" : "dog" }',
			'{ "species" : "human" }'
		};

        // Set mock callout class
        Test.setMock(HttpCalloutMock.class, new MockResponse(responses));

		Boolean result = MyCallout.doCallouts();

        System.assertEquals(true, result);
	}

	@isTest static void testDoCallouts_fail()
	{
		// Create the responses in the wrong sequence
		String[] responses = new String[] {
			'{ "species" : "human" }',
			'{ "species" : "dog" }'
		};

        // Set mock callout class
        Test.setMock(HttpCalloutMock.class, new MockResponse(responses));

		Boolean result = MyCallout.doCallouts();

        System.assertEquals(false, result);
	}

    public class MockResponse
        implements HttpCalloutMock
    {
    	private Integer m_responseIndex;
    	private String[] m_reponses;

    	public MockResponse(String[] reponses)
    	{
    		m_responseIndex = 0;
    		m_reponses = reponses;
    	}

        public HTTPResponse respond(HTTPRequest req)
        {
            HttpResponse res = new HttpResponse();
            res.setHeader('Content-Type', 'application/json');

            // Return the next response in the list
			String reponse = m_reponses[m_responseIndex];
			m_responseIndex ++;

            res.setBody(reponse);
            res.setStatusCode(200);

            return res;
        }
    }
}

You could adapt this pattern to provide different response codes in the same way.

Tagged with: , , ,
Posted in apex, code, force.com, salesforce, Uncategorized

Wrapping Custom Metadata to Support Unit Testing

Wrapping Paper

Wrapping PaperMy last post looked at using a wrapper class to make Custom Settings more convenient to access from apex code and to ease defaulting and unit testing.

If, like me, you’ve taken the leap to using Custom Metadata instead of List based Custom Settings you may have found yourself with a headache when writing your unit tests. Sure, you don’t need SeeAllData=true to see your metadata from your unit test but you may want to make sure the settings are in order for your given test.

Unlike Custom Settings, you can’t use DML to inject data for your unit tests. Attempting to instantiate a custom metadata object results in the rather friendly:

[OPERATION FAILED]: classes/MyClass.cls: Field is not writeable

To get round this I’ve adapted the pattern from my last post to support Custom Metadata.

Lets use the example Custom Metadata defined here:

Custom Metadata Definition

Nothing too exciting, a checkbox and a text field but it’ll do to demonstrate.

So we’ll start with the wrapper:

/**
 * Wrapper for My_Custom_Metadata_Type__mdt	to ease access and testing
 */
public with sharing class MyCustomMetadataTypes
{
	// Cached Map of metadata
	@TestVisible private static Map s_myCustomMetadataTypeMap = new Map();

	// Public getter
	public static Metadata getMyCustomMetadataType(String developerName)
	{
		// Check the cache
		Metadata myMetadata = s_myCustomMetadataTypeMap.get(developerName);

		// Not found, so query the metadata
		if (myMetadata == null)
		{
			My_Custom_Metadata_Type__mdt myCustomMetadataType;

			try
			{
				myCustomMetadataType = [Select DeveloperName, My_Checkbox__c, My_Text__c
				    					From My_Custom_Metadata_Type__mdt
				     					Where DeveloperName = :developerName];
			}
			catch(System.QueryException sqe)
			{
				throw new MyCustomMetadataTypeException('Could not find My Custom Metadata Type with DeveloperName: ' + developerName);
			}

			// Wrap the metadata using the inner class
			myMetadata = new Metadata(myCustomMetadataType);

			// Add it to the cache
     		s_myCustomMetadataTypeMap.put(myCustomMetadataType.DeveloperName, myMetadata);
		}

		// Return the wrapped metadata
		return myMetadata;
	}

	// Inner class to wrap the metadata
	public class Metadata
	{
		// Properties represent the Fields in the Metadata
		public Boolean My_Checkbox {get; set;}
		public String My_Text {get; set;}

		// Constructor used when creating test values
		public Metadata()
		{
		}

		// Construct from a concrete metadata instance
		private Metadata(My_Custom_Metadata_Type__mdt metadata)
		{
			this.My_Checkbox = metadata.My_Checkbox__c;
			this.My_Text = metadata.My_Text__c;
		}
	}

	public class MyCustomMetadataTypeException extends Exception {}
}

You’ll notice we have an inner class that reflects the fields that we’ve defined in the Custom Metadata.

It has two constructors, a public one we can use for mocking test data and a private one for marshalling an instance of the metadata in the real world. We’ll always use this inner class to get metadata valus and never reference the metadata directly.

A static Map of Developer Names to instances of the inner class provide the way to set values for Unit Tests. It’s set to TestVisible for this purpose. It’s also used to cache values whenever the getter is called. While it’s not strictly necessary to cache the data as Salesforce takes care of that, it does help support the pattern.

The public getter simply takes an argument of a developer name and returns an instance of the inner class populated with the metadata. So in you’re production code you can do things like:

String myString = MyCustomMetadataTypes.getMyCustomMetadataType('indierock').My_Text;

If the developer name doesn’t exist it will throw an exception which you can catch.

I’ll now show you some unit test examples and we’re done:

@isTest
private class MyCustomMetadataTypesTest
{
	@isTest static void thisWontWork()
	{
		// You can't do this as the metadata fields are not writeable!
		// Result: [OPERATION FAILED]: classes/MyCustomMetadataTypesTest.cls: Field is not writeable: My_Custom_Metadata_Type__mdt.DeveloperName

		/*
		My_Custom_Metadata_Type__mdt metadata = new My_Custom_Metadata_Type__mdt(
			DeveloperName = 'test',
			My_Checkbox__c = true,
			My_Text__c = 'some text'
		);

		insert metadata;
		*/
	}

	@isTest static void thisWorks()
	{
		// Create our test metadata
		{
			MyCustomMetadataTypes.Metadata metadata = new MyCustomMetadataTypes.Metadata();
			metadata.My_Checkbox = true;
			metadata.My_Text = 'some text';

			MyCustomMetadataTypes.s_myCustomMetadataTypeMap.put('test', metadata);
		}
		{
			MyCustomMetadataTypes.Metadata metadata = new MyCustomMetadataTypes.Metadata();
			metadata.My_Checkbox = false;
			metadata.My_Text = 'some more text';

			MyCustomMetadataTypes.s_myCustomMetadataTypeMap.put('test2', metadata);
		}

		// Use the wrapper to access the metadata
		MyCustomMetadataTypes.Metadata metadata = MyCustomMetadataTypes.getMyCustomMetadataType('test');

		// Assert the results
		System.assertEquals(true, metadata.My_Checkbox);
		System.assertEquals('some text', metadata.My_Text);

		// Use the wrapper to access the metadata
		MyCustomMetadataTypes.Metadata metadata2 = MyCustomMetadataTypes.getMyCustomMetadataType('test2');

		// Assert the results
		System.assertEquals(false, metadata2.My_Checkbox);
		System.assertEquals('some more text', metadata2.My_Text);

		// This demonstrates the exception thrown when the metadata developer name can't be found
		try
		{
			MyCustomMetadataTypes.Metadata metadata3 = MyCustomMetadataTypes.getMyCustomMetadataType('test3');
			System.assert(false, 'Expected an Exception.');
		}
		catch(Exception e)
		{
			System.assert(e instanceof MyCustomMetadataTypes.MyCustomMetadataTypeException);
		}
	}

	@isTest static void thisAlsoWorks()
	{
		// Using some Custom Metadata already defined in my org. Look, no SeeAllData=true!

		// Use the wrapper to access the metadata
		MyCustomMetadataTypes.Metadata metadata = MyCustomMetadataTypes.getMyCustomMetadataType('indierock');

		// Assert the results
		System.assertEquals(true, metadata.My_Checkbox);
		System.assertEquals('The Cribs', metadata.My_Text);
	}
}

 

 

Tagged with: , , ,
Posted in apex, code, force.com, salesforce

Wrapping Custom Settings to Provide Defaults and Ease Unit Testing

Marconi Valve TunerI have a little pattern I use for wrapping custom settings. It has two uses, first to provide sensible defaults if the settings have not been configured and secondly to make writing unit tests easier with predictable values.

If you’ve ever deployed a new bit of code that runs fine in your sandbox because you have all your custom settings configured but then fails in production as they’ve not been set up then this pattern is for you.

Hierarchy Settings

We’ll start with hierarchy settings since this is the simplest wrapper. Take the custom settings below:

Hierarchy Custom Settings

We have 2 properties, a boolean and a string.

Here’s the wrapper class:

public with sharing class MyHierarchySettings
{
	@TestVisible private static MyHierarchySettings__c s_settings;

	// Cache the settings
	private static MyHierarchySettings__c getSettings()
	{
		if (s_settings ==  null)
		{
			s_settings = MyHierarchySettings__c.getInstance();
		}

		return s_settings;
	}

	public static Boolean getMyBooleanValue()
	{
		return getSettings().My_Boolean_Value__c;
	}

	public static String getMyTextValue()
	{
		MyHierarchySettings__c settings = getSettings();

		// Provide a default if it's not initialised
		if (settings.My_Text_Value__c == null)
		{
			return 'henrietta';
		}
		
		return settings.My_Text_Value__c;
	}
}

You’ll see that we have a static variable to cache the settings and a getSettings() method which caches the custom settings. It’s also cunningly visible to unit tests. We then have a getter for each property and in the case of the Text setting, returns a default. We’ve no need to return a default for the boolean since it will happily return false and that’s a good enough default for me in this scenario. It’s worth noting that the getInstance() method always returns an object for a Hierarchy setting even if it’s not been defined yet. You’ll see later this is not true of List settings.

So to demonstrate we have a unit test:

@isTest
private class MyHierarchySettingsTest 
{	
	@isTest static void test_getMyBooleanValueDefault() 
	{
		System.assertEquals(false, MyHierarchySettings.getMyBooleanValue());
	}
	
	@isTest static void test_getMyTextValueDefault() 
	{
		System.assertEquals('henrietta', MyHierarchySettings.getMyTextValue());
	}
	
	@isTest static void test_setMyBooleanValue() 
	{
		MyHierarchySettings.s_settings = new MyHierarchySettings__c(My_Boolean_Value__c = true);
		System.assertEquals(true, MyHierarchySettings.getMyBooleanValue());
	}
	
	@isTest static void test_setMyTextValue() 
	{
		MyHierarchySettings.s_settings = new MyHierarchySettings__c(My_Text_Value__c = 'scarlet');
		System.assertEquals('scarlet', MyHierarchySettings.getMyTextValue());
	}
}

You’ll see in the unit tests how we can make use of the static variable to initialise the custom setting with our own values. This allows us to write a unit test with predictable data and is much tidier than inserting custom settings data.

List Custom Settings

List custom settings are a little more complicated. Let’s look at the sample settings below:

List Custom Settings

Again I have just a Boolean and a String property. So let’s look at the wrapper class for this:

public with sharing class MyListSettings
{
	@TestVisible private static Map<String, MyListSettings__c> s_settingsMap;

	// Cache the settings
	private static MyListSettings__c getSettings(String key)
	{
		if (s_settingsMap == null)
		{
			s_settingsMap = new Map<String, MyListSettings__c>();
		}

		MyListSettings__c settings = s_settingsMap.get(key);

		if (settings ==  null)
		{
			settings = MyListSettings__c.getInstance(key);
			s_settingsMap.put(key, settings);
		}

		return settings;
	}

	public static Boolean getMyBooleanValue(String key)
	{
		MyListSettings__c settings = getSettings(key);

		// Provide a default if it's not initialised
		if (settings == null)
		{
			return false;
		}

		return settings.My_Boolean_Value__c;
	}

	public static String getMyTextValue(String key)
	{
		MyListSettings__c settings = getSettings(key);

		// Provide a default if it's not initialised
		if (settings == null || settings.My_Text_Value__c == null)
		{
			return 'henrietta';
		}
		
		return settings.My_Text_Value__c;
	}
}

You’ll notice this time the static variable is a map keyed on the setting name. The getSettings() method still caches the data but stores it in the map. The getters for the properties take an argument of the key to the List. You’ll also notice that we have to explicitly supply a default for the Boolean. This is because the getInstance(key) method will return null if there is no List setting with the specified key.

On to the unit test to demonstrate:

@isTest
private class MyListSettingsTest 
{	
	@isTest static void test_getMyBooleanValueDefault() 
	{
		System.assertEquals(false, MyListSettings.getMyBooleanValue('geoff'));
	}
	
	@isTest static void test_getMyTextValueDefault() 
	{
		System.assertEquals('henrietta', MyListSettings.getMyTextValue('geoff'));
	}
	
	@isTest static void test_setMyBooleanValue() 
	{
		MyListSettings.s_settingsMap = new Map<String, MyListSettings__c> {'geoff' => new MyListSettings__c(My_Boolean_Value__c = true)};
		System.assertEquals(true, MyListSettings.getMyBooleanValue('geoff'));
	}
	
	@isTest static void test_setMyTextValue() 
	{
		MyListSettings.s_settingsMap = new Map<String, MyListSettings__c> {'geoff' => new MyListSettings__c(My_Text_Value__c = 'scarlet')};
		System.assertEquals('scarlet', MyListSettings.getMyTextValue('geoff'));
	}
}

In the same way as the hierarchy test we can use the static map variable to initialise it with predictable data.

Tagged with: , , ,
Posted in apex, code, force.com, salesforce

Connecting to Salesforce using OAuth 2.0 from PHP

Dancers

Introduction

Salesforce, with its rich set of APIs allows endless integration with 3rd party systems.

The key to accessing Salesforce from another system, be it a Web App, Mobile device or even a command line script, is logging on and obtaining a Session Token.

There a number of ways to do that. You could, for example, prompt for a Username, Password and Security token and use Salesforce’s Username-Password flow. However that would mean passing user credentials around and is not ideal. If you wanted to avoid the user having to repeatedly log on to Salesforce you would have to store the user credentials somewhere which would be even worse.

A better way of doing this is to use Salesforce’s Web Server Authentication Flow. This lets Salesforce handle the login process and sends back an Authentication Token from which you can request a Session Token. Better still, you can use this method to get a Refresh Token. The Refresh Token can then be stored or used later when the Session Token has expired in order to get a fresh Session Token without having to repeat the login process.

Warning: It’s very important that the Refresh Token (and to a certain extent the Session Token) is treated as if it were a password. Take steps to store it securely as anyone gaining access to this token could log on to your Salesforce org. The code in this example is for illustrative purposes, I don’t recommend spitting your tokens out to the screen.

The OAuth Dance

Sometimes referred to as the OAuth dance, this is the exchange of information between the consumer (in this case a PHP web server) and a provider (in this case Salesforce).

The following diagram outlines the steps involved in getting the all important Session Token:

OAuth Flow

Once you have you a Refresh Token you can hang on to it and use it for logging in again once your Session Token has expired. The process is then much simpler:

OAuth Refresh Token flow

PHP Example

I’ve chosen PHP, it’s fairly ubiquitous and I have it to hand but the steps involved apply to whatever language and framework you choose.

Secure Your Server!

It’s important that your PHP script is running on a secure server. Salesforce will only accept a call back to a URL over HTTPS. If you don’t have a secure server and simply want to try it out in an insecure and non-sensitive environment, I’ve described a Dropbox hack at the end. Doing so will transmit sensitive login information over an insecure connection. It is therefore not recommended, so use it at your own peril!

Create a Connected Application

Firstly you’ll need to login in to Salesforce and navigate to Setup->Create->Apps

Under Connected Apps, click New.

Fill in the mandatory fields.

Check the Enable OAuth Settings box.

In the Callback URL field, enter the URL of your PHP Script:

Eg: https://apps.myphpserver.com/TestOAUTH/sfdc_oauth_web.php

Under Available OAuth Scopes select:

Perform requests on your behalf at any time (refresh_token, offline_access)

Click Save and make a note of the generated Consumer Key and Consumer Secret.

Consumer App

The consumer app is a simple PHP web page that displays the connection state and provides buttons to step through the OAuth process:

OAuth Reset

The Reset button re-initialises the state to what you see above. passthroughState1 and 2 are simply there to demonstrate how to pass state through the authentication request and back. codeVerifier is 128 bytes of random data used to secure the authentication step.

Clicking Authenticate will redirect the user to the salesforce login page and prompt the user to Authenticate the connection before issuing a call back to the PHP script:

OAuth Authorise

OAuth Callback

You’ll see we now have a code passed back from Salesforce. This is the Authentication Code. Both passthroughState1 and 2 have also been sent back in a state parameter. You’ll see how this works in the code later.

Clicking Login via Authentication Code will take that code and attempt to login to Salesforce to get the Session Token and Refresh Token:

OAuth Code Login

You’ll see now we have a Session Token and Refresh Token. Using the Session Token we can now perform a request to get some data. Clicking the Get User button will use the REST API to get some user information:

Get User

Once the Session Token has expired we can use the Refresh Token to obtain a new Session Token without having to authenticate again. In a real world example you may cache this Refresh Token for later use. Clicking Login via Refresh Token with do this:

OAuth Refresh Login

PHP Script

Here’s the PHP Script in full. I’ll describe some of the salient points after. You will need to modify the getClientId() and getClientSecret() methods to to use the Consumer Key and Consumer Secret that belong to the Connected App you created earlier.

<?php
	// Report all errors (ignore Notices)
	error_reporting(E_ALL & ~E_NOTICE);
	ini_set('display_errors', 1);
	session_start();

	// Define our State class
	class State 
	{
		public $passthroughState1;	// Arbitary state we want to pass to the Authentication request
		public $passthroughState2;	// Arbitary state we want to pass to the Authentication request

		public $code;				// Authentication code received from Salesforce
		public $token;				// Session token
		public $refreshToken;		// Refresh token
		public $instanceURL;		// Salesforce Instance URL
		public $userId;				// Current User Id
		
		public $codeVerifier;		// 128 bytes of random data used to secure the request

		public $error;				// Error code
		public $errorDescription;	// Error description

		/**
		 * Constructor - Takes 2 pieces of optional state we want to preserve through the request
		 */
	    function __construct($state1 = "", $state2 = "")
	    {
	    	// Initialise arbitary state
	    	$this->passthroughState1 = $state1;
	    	$this->passthroughState2 = $state2;

	    	// Initialise remaining state
			$this->code = "";
			$this->token = "";
			$this->refreshToken = "";
			$this->instanceURL = "";
			$this->userId = "";
			
			$this->error = "";
			$this->errorDescription = "";

			// Generate 128 bytes of random data
			$this->codeVerifier = bin2hex(openssl_random_pseudo_bytes(128));
	    }

	    /**
	     * Helper function to populate state following a call back from Salesforce
	     */
	    function loadStateFromRequest()
	    {
	    	$stateString = "";

	    	// If we've arrived via a GET request, we can assume it's a callback from Salesforce OAUTH
	    	// so attempt to load the state from the parameters in the request
			if ($_SERVER["REQUEST_METHOD"] == "GET") 
			{
				$this->code = $this->sanitizeInput($_GET["code"]);
				$this->error = $this->sanitizeInput($_GET["error"]);
				$this->errorDescription = $this->sanitizeInput($_GET["error_description"]);
				$stateString = $this->sanitizeInput($_GET["state"]);

				// If we have a state string, then deserialize this into state as it's been passed
				// to the salesforce request and back
				if ($stateString)
				{
					$this->deserializeStateString($stateString);
				}
			}
	    }

	    /**
	     * Helper function to sanitize any input and prevent injection attacks
	     */
	    function sanitizeInput($data) 
	    {
  			$data = trim($data);
  			$data = stripslashes($data);
  			$data = htmlspecialchars($data);
  			return $data;
		}

		/**
		 * Helper function to serialize our arbitary state we want to send accross the request
		 */
		function serializeStateString()
		{
			$stateArray = array("passthroughState1" => $this->passthroughState1, 
								"passthroughState2" => $this->passthroughState2
								);

			return rawurlencode(base64_encode(serialize($stateArray)));
		}

		/**
		 * Helper function to deserialize our arbitary state passed back in the callback
		 */
		function deserializeStateString($stateString)
		{
			$stateArray = unserialize(base64_decode(rawurldecode($stateString)));

			$this->passthroughState1 = $stateArray["passthroughState1"];
			$this->passthroughState2 = $stateArray["passthroughState2"];
		}

		/**
		 * Helper function to generate the code challenge for the code verifier
		 */
		function generateCodeChallenge()
		{
			$hash = pack('H*', hash("SHA256", $this->generateCodeVerifier()));

			return $this->base64url_encode($hash);
		}

		/**
		 * Helper function to generate the code verifier
		 */
		function generateCodeVerifier()
		{
			return $this->base64url_encode(pack('H*', $this->codeVerifier));
		}

		/**
		 * Helper function to Base64URL encode as per https://tools.ietf.org/html/rfc4648#section-5
		 */
		function base64url_encode($string)
		{
			return strtr(rtrim(base64_encode($string), '='), '+/', '-_');
		}

		/**
		 * Helper function to display the current state values
		 */
		function debugState($message = NULL)
		{
			if ($message != NULL)
			{
				echo "<pre>$message</pre>";
			}

			echo "<pre>passthroughState1 = $this->passthroughState1</pre>";
			echo "<pre>passthroughState2 = $this->passthroughState2</pre>";
			echo "<pre>code = $this->code</pre>";
			echo "<pre>token = $this->token</pre>";
			echo "<pre>refreshToken = $this->refreshToken</pre>";
			echo "<pre>instanceURL = $this->instanceURL</pre>";
			echo "<pre>userId = $this->userId</pre>";
			echo "<pre>error = $this->error</pre>";
			echo "<pre>errorDescription = $this->errorDescription</pre>";
			echo "<pre>codeVerifier = $this->codeVerifier</pre>";
		}
	}

	// If we have not yet initialised state, are resetting or are Authenticating then Initialise State
	// and store in a session variable.
	if ($_SESSION['state'] == NULL || $_POST["reset"] || $_POST["authenticate"])
	{
	 	$_SESSION['state'] = new State('ippy', 'dippy');
	}

	$state = $_SESSION['state'];

	// Attempt to load the state from the page request
	$state->loadStateFromRequest();

	// if an error is present, render the error
	if ($state->error != NULL)
	{
		renderError();		
	}

	// Determine the form action
	if ($_POST["authenticate"])	// Authenticate button clicked
	{
		doOAUTH();	
	}
	else if ($_POST["login_via_code"])	// Login via Authentication Code button clicked
	{
		if (!loginViaAuthenticationCode())
		{
			renderError();
			return;
		}

		renderPage();
	}
	else if ($_POST["login_via_refresh_token"])	// Login via Refresh Token button clicked
	{
		if (!loginViaRefreshToken())
		{
			renderError();
			return;
		}

		renderPage();
	}
	else if ($_POST["get_user"])	// Get User button clicked
	{
		// Get the user data from Salesforce
		$userDataHTML = getUserData();

		// Render the page passing in the user data
		renderPage($userDataHTML);
	}
	else	// Otherwise render the page
	{
		renderPage();
	}

	// Render the Page
	function renderPage($userDataHTML = NULL)
	{
		$state = $_SESSION['state'];

		echo "<!DOCTYPE html>";
?>
		<html>
			<head>
			  	<title>SFDC - OAuth 2.0 Web Server Authentication Flow</title>
			  	<meta charset="UTF-8">
			</head>

			<body>
				<h1>SFDC - OAuth 2.0 Web Server Authentication Flow</h1>
<?php
				// Show the current state values
				$state->debugState();

				// If we have some user data to display then do so
				if ($userDataHTML)
				{
					echo $userDataHTML;
				}
?>
				<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">
					<input type="submit" name="reset" value="Reset" />
					<input type="submit" name="authenticate" value="Authenticate" />
					<input type="submit" name="login_via_code" value="Login via Authentication Code" />
					<input type="submit" name="login_via_refresh_token" value="Login via Refresh Token" />
					<input type="submit" name="get_user" value="Get User" />
				</form>

			</body>
		</html>
<?php
	}

	/**
	 * Redirect page to Salesforce to authenticate
	 */
	function doOAUTH()
	{
		$state = $_SESSION['state'];

		// Set the Authentication URL
		// Note we pass in the code challenge
		$href = "https://login.salesforce.com/services/oauth2/authorize?response_type=code" . 
				"&client_id=" . getClientId() . 
				"&redirect_uri=" . getCallBackURL() . 
				"&scope=api refresh_token" . 
				"&prompt=consent" . 
				"&code_challenge=" . $state->generateCodeChallenge() .
				"&state=" . $state->serializeStateString();

		// Wipe out arbitary state values to demonstrate passing additional state to salesforce and back
		$state->passthroughState1 = NULL;
		$state->passthroughState2 = NULL;

		// Perform the redirect
		header("location: $href");
	}

	/**
	 * Login via an Authentication Code
	 */
	function loginViaAuthenticationCode()
	{
		$state = $_SESSION['state'];

		// Create the Field array to pass to the post request
		// Note we pass in the code verifier and the authentication code
		$fields = array('grant_type' => 'authorization_code', 
						'client_id' => getClientId(),
						'client_secret' => getClientSecret(),
						'redirect_uri' => getCallBackURL(),
						'code_verifier' => $state->generateCodeVerifier(),
						'code' => $state->code,
						);
		
		// perform the login to Salesforce
		return doLogin($fields, false);
	}

	/**
	 * Login via a Refresh Token
	 */
	function loginViaRefreshToken()
	{
		$state = $_SESSION['state'];

		// Create the Field array to pass to the post request
		// Note we pass in the refresh token
		$fields = array('grant_type' => 'refresh_token', 
						'client_id' => getClientId(),
						'client_secret' => getClientSecret(),
						'redirect_uri' => getCallBackURL(),
						'refresh_token' => $state->refreshToken,
						);

		// perform the login to Salesforce
		return doLogin($fields, true);
	}

	/**
	 * Login to Salesforce to get a Session Token using CURL
	 */
	function doLogin($fields, $isViaRefreshToken)
	{
		$state = $_SESSION['state'];

		// Set the POST url to call
		$postURL = 'https://login.salesforce.com/services/oauth2/token';

		// Header options
		$headerOpts = array('Content-type: application/x-www-form-urlencoded');

		// Create the params for the POST request from the supplied fields	
		$params = "";
		
		foreach($fields as $key=>$value) 
		{ 
			$params .= $key . '=' . $value . '&';
		}

		$params = rtrim($params, '&');

		// Open the connection
		$ch = curl_init();

		// Set the url, number of POST vars, POST data etc
		curl_setopt($ch, CURLOPT_URL, $postURL);
		curl_setopt($ch, CURLOPT_POST, count($fields));
		curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
		curl_setopt($ch, CURLOPT_HTTPHEADER, $headerOpts);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

		// Execute POST
		$result = curl_exec($ch);

		// Close the connection
		curl_close($ch);

		//record the results into state
		$typeString = gettype($result);
		$resultArray = json_decode($result, true);

		$state->error = $resultArray["error"];
		$state->errorDescription = $resultArray["error_description"];

		// If there are any errors return false
		if ($state->error != null)
		{
			return false;
		}

		$state->instanceURL = $resultArray["instance_url"];
		$state->token = $resultArray["access_token"];

		// If we are logging in via an Authentication Code, we want to store the 
		// resulting Refresh Token
		if (!$isViaRefreshToken)
		{
			$state->refreshToken = $resultArray["refresh_token"];
		}

		// Extract the user Id
		if ($resultArray["id"] != null)
		{
			$trailingSlashPos = strrpos($resultArray["id"], '/');

			$state->userId = substr($resultArray["id"], $trailingSlashPos + 1);
		}

		// verify the signature
		$baseString = $resultArray["id"] . $resultArray["issued_at"];
		$signature = base64_encode(hash_hmac('SHA256', $baseString, getClientSecret(), true));

		if ($signature != $resultArray["signature"])
		{
			$state->error = 'Invalid Signature';
			$state->errorDescription = 'Failed to verify OAUTH signature.';

			return false;
		}

		// Debug that we've logged in via the appropriate method
		echo "<pre>Logged in " . ($isViaRefreshToken ? "via refresh token" : "via authorisation code") . "</pre>";

		return true;
	}

	/**
	 * Get User Data from Salesforce using CURL
	 */
	function getUserData()
	{
		$state = $_SESSION['state'];

		// Set our GET request URL
		$getURL = $state->instanceURL . '/services/data/v20.0/sobjects/User/' . $state->userId . '?fields=Name';

		// Header options
		$headerOpts = array('Authorization: Bearer ' . $state->token);

		// Open connection
		$ch = curl_init();

		// Set the url and header options
		curl_setopt($ch, CURLOPT_URL, $getURL);
		curl_setopt($ch, CURLOPT_HTTPHEADER, $headerOpts);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

		// Execute GET
		$result = curl_exec($ch);

		// Close connection
		curl_close($ch);

		// Get the results
		$typeString = gettype($result);
		$resultArray = json_decode($result, true);

		// Return them as an html String
		$rtnString = '<hr><h2>User Data</h2>';

		foreach($resultArray as $key=>$value) 
		{ 
			$rtnString .= "<pre>$key=$value</pre>";
		}

		return $rtnString;
	}

	/**
	 * Helper function to render an Error
	 */
	function renderError()
	{
		$state = $_SESSION['state'];

		echo '<div class="error"><span class="error_msg">' . $state->error . '</span> <span class="error_desc">' . $state->errorDescription . '</span></div>';
	}

	/**
	 * Get the hard coded Client Id for the Conected Application
	 */
	function getClientId()
	{
		return "xxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyzzzzzzzzzzzzzzzzzzzzzzzzzz";
	}

	/**
	 * Get the hard coded Client Secret for the Conected Application
	 */
	function getClientSecret()
	{
		return "1234567890123456789";
	}

	/**
	 * Get the Call back URL (the current php script)
	 */
	function getCallBackURL()
	{
		$callbackURL = ($_SERVER['HTTPS'] == NULL || $_SERVER['HTTPS'] == false ? "http://" : "https://") .
			$_SERVER['SERVER_NAME'] . "/" . $_SERVER['PHP_SELF'];

		return $callbackURL;
	}
?>

I’m using a class called State to represent the state and storing an instance of it in a SESSION variable.

You’ll notice some helper functions for popuating the state from a GET request following the Salesforce call back.

You’ll also notice some helper functions for serialising and deserialising the state we want to pass through with the authorisation request. While it’s not that useful in this example as we are using PHP’s SESSION variables to manage state it demonstrates how you can preserve data across the request.

In addition there are two helper functions that generate the Code Challenge and Code Verifier used across the authorisation request. While not strictly necessary this adds an additional layer of security. Salesforce will compare the two values when attempting to login using the authentication code. I had a bit of mare getting this to work so I’d like to give a quick nod to Ian Ratcliffe’s forum post that used the PHP bin2hex and pack functions to clinch this. You’ll see these codes used when constructing the authorisation and login requests in the doOAUTH() and loginViaAuthenticationCode() functions.

In the doLogin() function you’ll also notice another security check after the login has succeeded. This verifies the signature passed back in the login response with a hash of the User Id and Issued At values from the response using the Client Secret.

Dropbox Hack

As I mentioned earlier, it’s important that your web server is secured since Salesforce will only authenticate a callback over HTTPS.

I don’t recommend this at all and take no responsibility for using this hack … but if, like me, you’re just hacking around in a developer org, have nothing that you’d want falling into the wrong hands and are just trying stuff out you can use Dropbox to take care of the HTTPS callback.

To do this, in your Public Dropbox folder create a script called redirect.htm that looks like this (change the path to match the URL of your PHP script:

<script>
var href = window.location.href;
var queryParams = href.substring(href.indexOf('?'));
window.location = 'http://apps.myphpserver.com/TestOAUTH/sfdc_oauth_web.php' + queryParams;
</script>

Now copy the Dropbox public link and paste it into the Callback URL of your Connected App definition on Salesforce.

In the function getCallBackURL() of your PHP script assign the same link path to the $callbackURL variable.

Now Salesforce will use call back to the Dropbox script which will then strip the parameters and attach them to a redirect to your PHP Script (all very insecurely, of course!)

Tagged with: , , , , , ,
Posted in apex, code, force.com, PHP, salesforce

Lightning Lookup Component – <aura:if> workaround

For those of you having tried to implement the Lightning Lookup from my post: https://meltedwires.com/2015/10/31/salesforce-lightning-lookup-component-v2/

You may have experienced what appears to be a platform bug surrounding <aura:if> that results in the friendly, yet uninformative error: “Something has gone wrong. Cannot read property ‘childNodes’ of null. Please try again.”  Thanks to Vipul for finding this, it worked when I first published the article but clearly something has changed in the matrix.

After doing a bit of digging it seems to be related to the rerendering of <aura:if> in the AccountLookup.cmp. There’s a whole thread on it here. So until Salesforce apply a fix, I’ll post the workaround here.

Note: The bug doesn’t show up in the lookup component itself but in the example AccountLookup component.

The workaround is to use CSS to show/hide the <force:recordView> rather than an <aura:if>.

To do that we need add some style to the AccountLookup Component as follows by clicking the Style link in the Dev Console or adding the AccountLookupStyle.css to the bundle:

.THIS.hideme {
    display: none;
}

Next, we modify the AccountLookup.cmp to use a <div> instead of an <aura:if>:

<aura:component implements="flexipage:availableForAllPageTypes,force:appHostable">
    <!-- Attributes -->
    <aura:attribute name="recordId" type="Id" description="The current record Id to display" />
    
    <!-- Event handlers -->
    <aura:handler name="updateLookupIdEvent" event="c:UpdateLookupId" action="{!c.handleAccountIdUpdate}"/>
    <aura:handler name="clearLookupIdEvent" event="c:ClearLookupId" action="{!c.handleAccountIdClear}"/>
    
    <!-- Lookup component -->
    <c:LookupSObject label="Account" pluralLabel="Accounts" sObjectAPIName="Account" instanceId="MyAccount" listIconSVGPath="/resource/SLDS092/assets/icons/standard-sprite/svg/symbols.svg#account" listIconClass="slds-icon-standard-account" />

    <!-- Record view -->
    <div aura:id="accountview" class="hideme">
    	<force:recordView recordId="{!v.recordId}" />
    </div>
</aura:component>

Finally we add a Renderer to the AccountLookup and override the rerender method to show/hide the <force:recordView> depending on whether there is an Id present. We can do that by clicking the Renderer link in the Dev Console or adding the AccountLookupRenderer.js to the bundle:

({
	rerender: function(cmp, helper) {
		this.superRerender();

		// Get the Id and Record View
		var accountId = cmp.get('v.recordId');
 		var accountView = cmp.find('accountview');

		// Show or Hide the Account View depending on the Id value
        if (!accountId)
        {
            $A.util.addClass(accountView, 'hideme');
        }
        else
        {
            $A.util.removeClass(accountView, 'hideme');
        }
    },
})

It’s not quite ideal, as the <force:recordView> appears to be rerendered asynchronously and you get the message: “Invalid Record Id or Record not accessible.” briefly appearing.

It should, however, get you round the problem for now.

Posted in apex, code, force.com, lightning, salesforce, Uncategorized

Paris 13-12-2015

Peace For ParisMy girlfriend and I were lucky to escape from Le Bataclan on Friday night in Paris while watching The Eagles of Death Metal.

We survived thanks to the love and compassion of the people of Paris and we thank them from the bottom of our hearts and stand by them, united, in this time of tragedy.

Our thoughts are very much with those that were not as lucky as we were and our love goes out to them and their families.

#LoveNotHate


You can read an interview we did with Louder Than War magazine here along with our return to Paris following the bands return here.


 

Posted in travel, Uncategorized

Salesforce Lightning Lookup Component : Version 2

Lightning Yellow Pages

Version 2

This is Version 2 of the Salesforce Lightning Lookup Component. I had one of those, ‘Why didn’t I do it like that instead?‘ moments in the shower this morning. Essentially, it’s less code as I’ve done away with the rendering of the inner list components and plumped straight for throwing the Apex Controller results into an attribute and using an <aura:iteration> to render them. The original version is here for reference.

Note: There appears to be a bug with using <aura:if> in the AccountLookup component. If you experience difficulty with this, a workaround can be found here

On with the show …

I’ve been playing around with Salesforce Lightning components and one of the obvious things that seems to be missing is a UI component to lookup a record in a Salesforce custom object and return its Id.

This was really simple in Visualforce. You could just use an <apex:inputField> tag and bind it to an objects lookup field in your controller. Salesforce would take care of the rest. Lightning, however, is a different beast and centres around building client side apps or components in Javascript and utilising various API’s for dealing with any server side requests.

There’s quite a bit of code here so I’ve added it all to a github repository. (Links at the bottom)

Design

There were a few things to consider:

  • It should work both within a desktop app and within the Salesforce Lightning mobile app.
  • It should be intuitive and have the look and feel of a lightning app.
  • It should be reusable, easily configured and plumbed into another component or app.

Cake

As I said, there’s quite a bit of code so before we begin, like all good cake recipes, let’s look at the finished result (6Mb Animated GIF file, please be patient while it loads):

Ligntning Lookup Component

Ligntning Lookup Component

Look & Feel (using the Salesforce Lightning Design System)

The look and feel comes from the Salesforce Lightning Design System or SLDS for short. This is a prerequisite, so you’ll need to install this before you press ahead and load any of the code into your org. I’m using SLDS092 but you should be able to a later version if you wish. You can install it into your org using the links here. This library provides a whole plethora of CSS, images to make your app and components look like the real deal. It also covers the UI design and how to structure your markup. For the lookup component I’ve used the markup described in the Lookups component of the SLDS. In some cases I’ve replaced the standard HTML tags with lightning framework markup such as <ui:inputText> instead of <input>. I’ve also had to create an <c:svg> component to use instead of the <svg> tag which is not supported in lightning. This is described in the Trailhead module: Build a Lightning App with the Lightning Design System.

Show me the code!

Lightning component bundle: svg

First things first, I need to show you the svg component. You need this so you can render the icons as nice scalable graphics without using the <svg> tag. Now this is pretty much lifted straight from the trailhead module so I’m not going to bore you with how it works. You can check out the trailhead yourself.

Here’s the component markup:

Component

<aura:component >
  <aura:attribute name="class" type="String" description="CSS classname for the SVG element" />
  <aura:attribute name="xlinkHref" type="String" description="SLDS icon path. Ex: /assets/icons/utility-sprite/svg/symbols.svg#download" />
  <aura:attribute name="ariaHidden" type="String" default="true" description="aria-hidden true or false. defaults to true" />
</aura:component>

It has a renderer defined which looks like this:

Renderer

({
  render: function(component, helper) {
    //grab attributes from the component markup
    var classname = component.get("v.class");
    var xlinkhref = component.get("v.xlinkHref");
    var ariaHidden = component.get("v.ariaHidden");

    //return an svg element w/ the attributes
    var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute('class', classname);
    svg.setAttribute('aria-hidden', ariaHidden);
    svg.innerHTML = '<use xlink:href="'+xlinkhref+'"></use>';
    return svg;
  }
})

Apex Controller: LookupSObjectController

We’re going to need an apex controller on the server side to query the database and return the matching records. To do this create the following aura enabled Apex class:

Apex class

/**
 * Apex Controller for looking up an SObject via SOSL
 */
public with sharing class LookupSObjectController 
{
    /**
     * Aura enabled method to search a specified SObject for a specific string
     */
    @AuraEnabled
    public static Result[] lookup(String searchString, String sObjectAPIName)
    {
        // Sanitze the input
        String sanitizedSearchString = String.escapeSingleQuotes(searchString);
        String sanitizedSObjectAPIName = String.escapeSingleQuotes(sObjectAPIName);

        List<Result> results = new List<Result>();

        // Build our SOSL query
        String searchQuery = 'FIND \'' + sanitizedSearchString + '*\' IN ALL FIELDS RETURNING ' + sanitizedSObjectAPIName + '(id,name) Limit 50'; 

        // Execute the Query
        List<List<SObject>> searchList = search.query(searchQuery);

        // Create a list of matches to return
        for (SObject so : searchList[0])
        {
            results.add(new Result((String)so.get('Name'), so.Id));
        }
        
        return results;
    }
    
    /**
     * Inner class to wrap up an SObject Label and its Id
     */
    public class Result
    {
        @AuraEnabled public String SObjectLabel {get; set;}
        @AuraEnabled public Id SObjectId {get; set;}
        
        public Result(String sObjectLabel, Id sObjectId)
        {
            this.SObjectLabel = sObjectLabel;
            this.SObjectId = sObjectId;
        }
    }
}

Notice the @AuraEnabled keyword. This allows the method to be called from the lightning framework. The lookup method constructs a SOSL query using the passed in search string and the objects API name. It returns a list of Results that contain a label and the Id of the matching SObjects.

Lightning component bundle: LookupSObject

On the client side we have a reusable Lightning component. The component contains attributes to specify the SObjects API Name, the label and plural label to apply along with an optional SVG image path and class from the SLDS. If the latter aren’t specified it uses a default icon, in this case a star.

There are two events registered: updateLookupIdEvent and clearLookupIdEvent. The former is fired when the user chooses an entry from the list, the latter is fired when the user clears a selection (via the cross icon.)

The component uses a keyup event to pass the user entered search string to a controller method. This in turn calls the server side Aura Enabled method.

The markup generated adheres to the SLDS Lookup component design. We’re going to use an <aura:iteration> to bind to the results of the Apex controller search. The matches attribute is where the results will get stored. You’ll notices inside the iteration is <a> element which uses the SObjects Id within the rendered element id so that we can easily capture it within the controller when the item is selected.

The component markup looks like this:

Component

<aura:component controller="LookupSObjectController" >
    <!-- Required Scripts/Styles -->
    <!-- Salesforce Lightning Design System : https://www.lightningdesignsystem.com/ -->
    <ltng:require styles="/resource/SLDS092/assets/styles/salesforce-lightning-design-system-ltng.css" />
    
    <!-- Attributes -->
    <aura:attribute name="sObjectAPIName" type="String" required="true" description="The API name of the SObject to search" />
    <aura:attribute name="label" type="String" required="true" description="The label to assign to the lookup, eg: Account" />
    <aura:attribute name="pluralLabel" type="String" required="true" description="The plural label to assign to the lookup, eg: Accounts" />
    <aura:attribute name="listIconSVGPath" type="String" default="/resource/SLDS092/assets/icons/custom-sprite/svg/symbols.svg#custom11" description="The static resource path to the svg icon to use." />
    <aura:attribute name="listIconClass" type="String" default="slds-icon-custom-11" description="The SLDS class to use for the icon." />
    <aura:attribute name="searchString" type="String" description="The search string to find." />
    <aura:attribute name="matches" type="LookupSObjectController.Result[]" description="The resulting matches returned by the Apex controller." />

    <!-- Events -->
    <aura:registerEvent name="updateLookupIdEvent" type="c:UpdateLookupId"/>    
    <aura:registerEvent name="clearLookupIdEvent" type="c:ClearLookupId"/>    
    
    <!-- Lookup Markup : See https://www.lightningdesignsystem.com/components/lookups -->
    <div class="slds"> 
    <div aura:id="lookup-div" class="slds-lookup" data-select="single" data-scope="single" data-typeahead="true">
        <!-- This is the Input form markup -->
        <div class="slds-form-element">
            <label class="slds-form-element__label" for="lookup">{!v.label}</label>
            <div class="slds-form-element__control slds-input-has-icon slds-input-has-icon--right">
                <c:svg class="slds-input__icon" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#search" />
                <!-- This markup is for when an item is currently selected -->
                <div aura:id="lookup-pill" class="slds-pill-container slds-hide">
                        <span class="slds-pill slds-pill--bare">
                            <span class="slds-pill__label">
                                <c:svg class="{!'slds-icon ' + v.listIconClass + ' slds-icon--small'}" xlinkHref="{!v.listIconSVGPath}" />{!v.searchString}
                            </span>
                            <button class="slds-button slds-button--icon-bare" onclick="{!c.clear}">
                                <c:svg class="slds-button__icon" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#close" />
                                <span class="slds-assistive-text">Remove</span>
                            </button>
                        </span>
                    </div>
                    <!-- This markup is for when searching for a string -->
                    <ui:inputText aura:id="lookup" value="{!v.searchString}" class="slds-input" updateOn="keyup" keyup="{!c.search}" />
                </div>
            </div>
            <!-- This is the lookup list markup. Initially it's hidden -->
            <div aura:id="lookuplist" class="slds-lookup__menu slds-hide" role="listbox">
                <div class="slds-lookup__item">
                    <button class="slds-button">
                        <c:svg class="slds-icon slds-icon-text-default slds-icon--small" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#search" />
                        &quot;{!v.searchString}&quot; in {!v.pluralLabel}
                    </button>
                </div>
                <ul aura:id="lookuplist-items" class="slds-lookup__list" role="presentation">
                    <aura:iteration items="{!v.matches}" var="match">
                        <li class="slds-lookup__item">
                            <a id="{!globalId + '_id_' + match.SObjectId}" role="option" onclick="{!c.select}">
                                <c:svg class="{!'slds-icon ' + v.listIconClass + ' slds-icon--small'}" xlinkHref="{!v.listIconSVGPath}" />{!match.SObjectLabel}
                            </a>
                        </li>
                    </aura:iteration>
                </ul>
            </div>
        </div>
    </div>
</aura:component>

There is an associated controller to handle the events. The search method, as mentioned earlier, is called from the keyup event on the search. The select method is called when the user chooses a result from the rendered list. The clear method is called when the user clears the selected item by clicking the cross icon:

Controller

({
    /**
     * Search an SObject for a match
     */
    search : function(cmp, event, helper) {
        helper.doSearch(cmp);        
    },

    /**
     * Select an SObject from a list
     */
    select: function(cmp, event, helper) {
        helper.handleSelection(cmp, event);
    },
    
    /**
     * Clear the currently selected SObject
     */
    clear: function(cmp, event, helper) {
        helper.clearSelection(cmp);    
    }
})

You’ll notice that each of the methods delegate to a helper. The helper does all the grunt work and looks like this:

Helper

({
    /**
     * Perform the SObject search via an Apex Controller
     */
    doSearch : function(cmp) {
        // Get the search string, input element and the selection container
        var searchString = cmp.get('v.searchString');
        var inputElement = cmp.find('lookup');
        var lookupList = cmp.find('lookuplist');

        // Clear any errors and destroy the old lookup items container
        inputElement.set('v.errors', null);
        
        // We need at least 2 characters for an effective search
        if (typeof searchString === 'undefined' || searchString.length < 2)
        {
            // Hide the lookuplist
            $A.util.addClass(lookupList, 'slds-hide');
            return;
        }

        // Show the lookuplist
        $A.util.removeClass(lookupList, 'slds-hide');

        // Get the API Name
        var sObjectAPIName = cmp.get('v.sObjectAPIName');

        // Create an Apex action
        var action = cmp.get('c.lookup');

        // Mark the action as abortable, this is to prevent multiple events from the keyup executing
        action.setAbortable();

        // Set the parameters
        action.setParams({ "searchString" : searchString, "sObjectAPIName" : sObjectAPIName});
                          
        // Define the callback
        action.setCallback(this, function(response) {
            var state = response.getState();

            // Callback succeeded
            if (cmp.isValid() && state === "SUCCESS")
            {
                // Get the search matches
                var matches = response.getReturnValue();

                // If we have no matches, return nothing
                if (matches.length == 0)
                {
                    cmp.set('v.matches', null);
                    return;
                }
                
                // Store the results
                cmp.set('v.matches', matches);
            }
            else if (state === "ERROR") // Handle any error by reporting it
            {
                var errors = response.getError();
                
                if (errors) 
                {
                    if (errors[0] && errors[0].message) 
                    {
                        this.displayToast('Error', errors[0].message);
                    }
                }
                else
                {
                    this.displayToast('Error', 'Unknown error.');
                }
            }
        });
        
        // Enqueue the action                  
        $A.enqueueAction(action);                
    },

    /**
     * Handle the Selection of an Item
     */
    handleSelection : function(cmp, event) {
        // Resolve the Object Id from the events Element Id (this will be the <a> tag)
        var objectId = this.resolveId(event.currentTarget.id);

        // The Object label is the inner text)
        var objectLabel = event.currentTarget.innerText;

        // Log the Object Id and Label to the console
        console.log('objectId=' + objectId);
        console.log('objectLabel=' + objectLabel);
                
        // Create the UpdateLookupId event
        var updateEvent = cmp.getEvent("updateLookupIdEvent");
        
        // Populate the event with the selected Object Id
        updateEvent.setParams({
            "sObjectId" : objectId
        });

        // Fire the event
        updateEvent.fire();

        // Update the Searchstring with the Label
        cmp.set("v.searchString", objectLabel);

        // Hide the Lookup List
        var lookupList = cmp.find("lookuplist");
        $A.util.addClass(lookupList, 'slds-hide');

        // Hide the Input Element
        var inputElement = cmp.find('lookup');
        $A.util.addClass(inputElement, 'slds-hide');

        // Show the Lookup pill
        var lookupPill = cmp.find("lookup-pill");
        $A.util.removeClass(lookupPill, 'slds-hide');

        // Lookup Div has selection
        var inputElement = cmp.find('lookup-div');
        $A.util.addClass(inputElement, 'slds-has-selection');

    },

    /**
     * Clear the Selection
     */
    clearSelection : function(cmp) {
        // Create the ClearLookupId event
        var clearEvent = cmp.getEvent("clearLookupIdEvent");
        
        // Fire the event
        clearEvent.fire();

        // Clear the Searchstring
        cmp.set("v.searchString", '');

        // Hide the Lookup pill
        var lookupPill = cmp.find("lookup-pill");
        $A.util.addClass(lookupPill, 'slds-hide');

        // Show the Input Element
        var inputElement = cmp.find('lookup');
        $A.util.removeClass(inputElement, 'slds-hide');

        // Lookup Div has no selection
        var inputElement = cmp.find('lookup-div');
        $A.util.removeClass(inputElement, 'slds-has-selection');
    },

    /**
     * Resolve the Object Id from the Element Id by splitting the id at the _
     */
    resolveId : function(elmId)
    {
        var i = elmId.lastIndexOf('_');
        return elmId.substr(i+1);
    },

    /**
     * Display a message
     */
    displayToast : function (title, message) 
    {
        var toast = $A.get("e.force:showToast");

        // For lightning1 show the toast
        if (toast)
        {
            //fire the toast event in Salesforce1
            toast.setParams({
                "title": title,
                "message": message
            });

            toast.fire();
        }
        else // otherwise throw an alert
        {
            alert(title + ': ' + message);
        }
    }
})

The doSearch method takes care of calling the Apex controller and storing the results in the matches attribute.

The handleSelection method takes the selected entry and fires the updateLookupIdEvent before modifying the CSS classes to show the Lookup in its selected state.

The clearSelection method fires the clearLookupIdEvent before modifying the CSS classes to revert back to the search state.

The resolveId method splits the SObject Id from the end of the element id. This is used by the handleSelection method to obtain the SObjects Id. Remember, we hashed it in to the <a> elements id.

The displayToast is simply an error logging method that renders a toast popup in the Salesforce1 app or an alert in the browser.

Lightning event: UpdateLookupId

This is the event fired when the user selects an item from search results. It has a single attribute that gets set with the Id of the selected record. You’ll notice this is a component event rather than an application event. Be careful here if you are nesting your components you’ll only be able to handle it in the outermost component. Consider changing it to an Application event if this is an issue.

Event

<aura:event type="COMPONENT" description="Update Lookup Id" >
    <aura:attribute name="sObjectId" type="Id" required="true" description="The Id of the selected SObject." />
</aura:event>

Lightning event: ClearLookupId

This is the event fired when the user clears the selected item. It has no attributes. Again, you’ll notice this is a component event rather than an application event. Once again, be careful here if you are nesting your components you’ll only be able to handle it in the outermost component. Consider changing it to an Application event if this is an issue.

Event

<aura:event type="COMPONENT" description="Clear the Lookup" />

Ok, great. Now can I see how you use it?

We’re going to wrap this up with an example Account Lookup component which you can plug straight into Salesforce1 via a Component Tab. Since the component implements flexipage:availableForAllPageTypes you could plug it straight into Lightning Builder if you wished. If you wanted to embed it in a Visualforce page you could do that as well. I won’t go into how to do that here but you’ll find details in the Lightning developer documentation.

Lightning component: AccountLookup

First lets look at the component markup. There’s an attribute to hold the Record Id. This is hooked up to the force:recordView component which is only rendered if an Id is present. We have two event handlers listening for the UpdateLookupId and ClearLookupId events. Note the names tie up to the registered events in the LookupSObject component. These event handlers hook up to the components controller via the handleAccountIdUpdate and handleAccountIdClear methods. The LookupSObject component markup takes the labels and icon resources as attributes.

Component

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes">
    <!-- Attributes -->
    <aura:attribute name="recordId" type="Id" description="The current record Id to display" />
    
    <!-- Event handlers -->
    <aura:handler name="updateLookupIdEvent" event="c:UpdateLookupId" action="{!c.handleAccountIdUpdate}"/>
    <aura:handler name="clearLookupIdEvent" event="c:ClearLookupId" action="{!c.handleAccountIdClear}"/>
    
    <!-- Lookup component -->
    <c:LookupSObject label="Account" pluralLabel="Accounts" sObjectAPIName="Account" listIconSVGPath="/resource/SLDS092/assets/icons/standard-sprite/svg/symbols.svg#account" listIconClass="slds-icon-standard-account" />

    <!-- Record view -->
    <aura:if isTrue="{!v.recordId!=null}">
        <force:recordView recordId="{!v.recordId}" />
    </aura:if>
</aura:component>

Now lets take a look at the controller. You’ll see the handleAccountIdUpdate method gets the Id from the Event and updates the recordId attribute. The handleAccountIdClear simply nulls the recordId attribute. And that’s it, simples.

Controller

({
    /**
     * Handler for receiving the updateLookupIdEvent event
     */
    handleAccountIdUpdate : function(cmp, event, helper) {
        // Get the Id from the Event
        var accountId = event.getParam("sObjectId");

        // Set the Id bound to the View
        cmp.set('v.recordId', accountId);
    },

    /**
     * Handler for receiving the clearLookupIdEvent event
     */
    handleAccountIdClear : function(cmp, event, helper) {
        // Clear the Id bound to the View
        cmp.set('v.recordId', null);
    }
})

If you want to plug the component into Lightning builder, you’ll need to include a design in the component bundle. Since there are no attributes that need passing into our Account Lookup component it just looks like this:

Design

<design:component> 
</design:component>

Creating the Lightning Component Tab

  • Navigate to: Setup->Create->Tabs
  • Click on New Lightning Component Tab.
  • Select the AccountLookup from the dropdown list.
  • Enter a Tab Name.
  • Choose a Tab Style.

… and you’re nearly good to go.

All that remains is to add it to the Salesforce1 Navigation.

Navigate to: Setup->Mobile Administration->Salesforce1 Navigation

Add the Component tab to your navigation, fire up the Salesforce1 mobile app and give it a whirl!

Notes:

All the code samples assume the default namespace. If you have your own namespace you’ll need to change the references from c:mything to mynamespace:mything.

If you are using a different version of the SLDS, you’ll need to change the paths in the code from SLDS092 to match your version.

Code and Resources:

The code is based on Version 0.92 of the SLDS: https://www.lightningdesignsystem.com/resources/downloads

Github repository containing all the above code (Note: this includes some updates as per the discussions in the comments thread): https://github.com/tscottdev/Lightning-Lookup

Links and stuff:

Salesforce Lightning Design System

Trailhead: Build a Lightning App with the Lightning Design System

 

Tagged with: , , , ,
Posted in apex, code, force.com, lightning, salesforce

Salesforce Lightning Lookup Component

Lightning Yellow Pages

Note: This article has been superceded by: Version 2

I’ve been playing around with Salesforce Lightning components and one of the obvious things that seems to be missing is a UI component to lookup a record in a Salesforce custom object and return its Id.

This was really simple in Visualforce. You could just use an <apex:inputField> tag and bind it to an objects lookup field in your controller. Salesforce would take care of the rest. Lightning, however, is a different beast and centres around building client side apps or components in Javascript and utilising various API’s for dealing with any server side requests.

There’s quite a bit of code here so I’ve added it all to a github repository. (Links at the bottom)

Design

There were a few things to consider:

  • It should work both within a desktop app and within the Salesforce Lightning mobile app.
  • It should be intuitive and have the look and feel of a lightning app.
  • It should be reusable, easily configured and plumbed into another component or app.

Cake

As I said, there’s quite a bit of code so before we begin, like all good cake recipes, let’s look at the finished result (6Mb Animated GIF file, please be patient while it loads):

Ligntning Lookup Component

Ligntning Lookup Component

Look & Feel (using the Salesforce Lightning Design System)

The look and feel comes from the Salesforce Lightning Design System or SLDS for short. This is a prerequisite, so you’ll need to install this before you press ahead and load any of the code into your org. I’m using SLDS092 but you should be able to a later version if you wish. You can install it into your org using the links here. This library provides a whole plethora of CSS, images to make your app and components look like the real deal. It also covers the UI design and how to structure your markup. For the lookup component I’ve used the markup described in the Lookups component of the SLDS. In some cases I’ve replaced the standard HTML tags with lightning framework markup such as <ui:inputText> instead of <input>. I’ve also had to create an <c:svg> component to use instead of the <svg> tag which is not supported in lightning. This is described in the Trailhead module: Build a Lightning App with the Lightning Design System.

Show me the code!

Lightning component bundle: svg

First things first, I need to show you the svg component. You need this so you can render the icons as nice scalable graphics without using the <svg> tag. Now this is pretty much lifted straight from the trailhead module so I’m not going to bore you with how it works. You can check out the trailhead yourself.

Here’s the component markup:

Component

<aura:component >
  <aura:attribute name="class" type="String" description="CSS classname for the SVG element" />
  <aura:attribute name="xlinkHref" type="String" description="SLDS icon path. Ex: /assets/icons/utility-sprite/svg/symbols.svg#download" />
  <aura:attribute name="ariaHidden" type="String" default="true" description="aria-hidden true or false. defaults to true" />
</aura:component>

It has a renderer defined which looks like this:

Renderer

({
  render: function(component, helper) {
    //grab attributes from the component markup
    var classname = component.get("v.class");
    var xlinkhref = component.get("v.xlinkHref");
    var ariaHidden = component.get("v.ariaHidden");

    //return an svg element w/ the attributes
    var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute('class', classname);
    svg.setAttribute('aria-hidden', ariaHidden);
    svg.innerHTML = '<use xlink:href="'+xlinkhref+'"></use>';
    return svg;
  }
})

Apex Controller: LookupSObjectController

We’re going to need an apex controller on the server side to query the database and return the matching records. To do this create the following aura enabled Apex class:

Apex class

/**
 * Apex Controller for looking up an SObject via SOSL
 */
public with sharing class LookupSObjectController 
{
    /**
     * Aura enabled method to search a specified SObject for a specific string
     */
    @AuraEnabled
    public static Result[] lookup(String searchString, String sObjectAPIName)
    {
        // Sanitze the input
        String sanitizedSearchString = String.escapeSingleQuotes(searchString);
        String sanitizedSObjectAPIName = String.escapeSingleQuotes(sObjectAPIName);

        List<Result> results = new List<Result>();

        // Build our SOSL query
        String searchQuery = 'FIND \'' + sanitizedSearchString + '*\' IN ALL FIELDS RETURNING ' + sanitizedSObjectAPIName + '(id,name) Limit 50'; 

        // Execute the Query
        List<List<SObject>> searchList = search.query(searchQuery);

        // Create a list of matches to return
        for (SObject so : searchList[0])
        {
            results.add(new Result((String)so.get('Name'), so.Id));
        }
        
        return results;
    }
    
    /**
     * Inner class to wrap up an SObject Label and its Id
     */
    public class Result
    {
        @AuraEnabled public String SObjectLabel {get; set;}
        @AuraEnabled public Id SObjectId {get; set;}
        
        public Result(String sObjectLabel, Id sObjectId)
        {
            this.SObjectLabel = sObjectLabel;
            this.SObjectId = sObjectId;
        }
    }
}

Notice the @AuraEnabled keyword. This allows the method to be called from the lightning framework. The lookup method constructs a SOSL query using the passed in search string and the objects API name. It returns a list of Results that contain a label and the Id of the matching SObjects.

Lightning component bundle: LookupSObject

On the client side we have a reusable Lightning component. The component contains attributes to specify the SObjects API Name, the label and plural label to apply along with an optional SVG image path and class from the SLDS. If the latter aren’t specified it uses a default icon, in this case a star.

There are two events registered: updateLookupIdEvent and clearLookupIdEvent. The former is fired when the user chooses an entry from the list, the latter is fired when the user clears a selection (via the cross icon.)

The component uses a keyup event to pass the user entered search string to a controller method. This in turn calls the server side Aura Enabled method.

The markup generated adheres to the SLDS Lookup component design. Note the <ul> tag. This is where the <li> tags will be inserted for the records matching the search.

The component markup looks like this:

Component

<aura:component controller="LookupSObjectController" >
    <!-- Required Scripts/Styles -->
    <!-- Salesforce Lightning Design System : https://www.lightningdesignsystem.com/ -->
    <ltng:require styles="/resource/SLDS092/assets/styles/salesforce-lightning-design-system-ltng.css" />
    
    <!-- Attributes -->
    <aura:attribute name="sObjectAPIName" type="String" required="true" 
        description="The API name of the SObject to search" />
    <aura:attribute name="label" type="String" required="true" 
        description="The label to assign to the lookup, eg: Account" />
    <aura:attribute name="pluralLabel" type="String" required="true" 
        description="The plural label to assign to the lookup, eg: Accounts" />
    <aura:attribute name="listIconSVGPath" type="String" default="/resource/SLDS092/assets/icons/custom-sprite/svg/symbols.svg#custom11" 
        description="The static resource path to the svg icon to use." />
    <aura:attribute name="listIconClass" type="String" default="slds-icon-custom-11" 
        description="The SLDS class to use for the icon." />
    <aura:attribute name="searchString" type="String" 
        description="The search string to find." />

    <!-- Events -->
    <aura:registerEvent name="updateLookupIdEvent" type="c:UpdateLookupId"/>    
    <aura:registerEvent name="clearLookupIdEvent" type="c:ClearLookupId"/>    
    
    <!-- Lookup Markup : See https://www.lightningdesignsystem.com/components/lookups -->
    <div class="slds"> 
        <div aura:id="lookup-div" class="slds-lookup" data-select="single" data-scope="single" data-typeahead="true">
            <!-- This is the Input form markup -->
            <div class="slds-form-element">
                <label class="slds-form-element__label" for="lookup">{!v.label}</label>
                <div class="slds-form-element__control slds-input-has-icon slds-input-has-icon--right">
                    <c:svg class="slds-input__icon" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#search" />
                    <!-- This markup is for when an item is currently selected -->
                    <div aura:id="lookup-pill" class="slds-pill-container slds-hide">
                        <span class="slds-pill slds-pill--bare">
                            <span class="slds-pill__label">
                                <c:svg class="{!'slds-icon ' + v.listIconClass + ' slds-icon--small'}" xlinkHref="{!v.listIconSVGPath}" />{!v.searchString}
                            </span>
                            <button class="slds-button slds-button--icon-bare" onclick="{!c.clear}">
                                <c:svg class="slds-button__icon" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#close" />
                                <span class="slds-assistive-text">Remove</span>
                            </button>
                        </span>
                    </div>
                    <!-- This markup is for when searching for a string -->
                    <ui:inputText aura:id="lookup" value="{!v.searchString}" class="slds-input" updateOn="keyup" keyup="{!c.search}" />
                </div>
            </div>
            <!-- This is the lookup list markup. Initially it's hidden -->
            <div aura:id="lookuplist" class="slds-lookup__menu slds-hide" role="listbox">
                <div class="slds-lookup__item">
                    <button class="slds-button">
                        <c:svg class="slds-icon slds-icon-text-default slds-icon--small" xlinkHref="/resource/SLDS092/assets/icons/utility-sprite/svg/symbols.svg#search" />
                        &quot;{!v.searchString}&quot; in {!v.pluralLabel}
                    </button>
                </div>
                <ul aura:id="lookuplist-items" class="slds-lookup__list" role="presentation">
                </ul>
            </div>
        </div>
    </div>

</aura:component>

There is an associated controller to handle the events. The search method, as mentioned earlier, is called from the keyup event on the search. The select method is called when the user chooses a result from the rendered list. The clear method is called when the user clears the selected item by clicking the cross icon:

Controller

({
    /**
     * Search an SObject for a match
     */
    search : function(cmp, event, helper) {
        helper.doSearch(cmp);        
    },

    /**
     * Select an SObject from a list
     */
    select: function(cmp, event, helper) {
        helper.handleSelection(cmp, event);
    },
    
    /**
     * Clear the currently selected SObject
     */
    clear: function(cmp, event, helper) {
        helper.clearSelection(cmp);    
    }
})

You’ll notice that each of the methods delegate to a helper. The helper does all the grunt work and looks like this:

Helper

({
    /**
     * Perform the SObject search via an Apex Controller
     */
    doSearch : function(cmp) {
        // Get the search string, input element and the selection container
        var searchString = cmp.get("v.searchString");
        var inputElement = cmp.find('lookup');
        var lookupList = cmp.find("lookuplist");
        var lookupListItems = cmp.find("lookuplist-items");

        // Clear any errors and destroy the old lookup items container
        inputElement.set('v.errors', null);
        lookupListItems.set('v.body', new Array());
        
        // We need at least 2 characters for an effective search
        if (typeof searchString === 'undefined' || searchString.length < 2)
        {
            // Hide the lookuplist
            $A.util.addClass(lookupList, 'slds-hide');
            return;
        }

        // Show the lookuplist
        $A.util.removeClass(lookupList, 'slds-hide');

        // Get the API Name
        var sObjectAPIName = cmp.get('v.sObjectAPIName');

        // Create an Apex action
        var action = cmp.get("c.lookup");

        // Mark the action as abortable, this is to prevent multiple events from the keyup executing
        action.setAbortable();

        // Set the parameters
        action.setParams({ "searchString" : searchString, "sObjectAPIName" : sObjectAPIName});
                          
        // Define the callback
        action.setCallback(this, function(response) {
            var state = response.getState();

            // Callback succeeded
            if (cmp.isValid() && state === "SUCCESS")
            {
                // Get the search matches
                var matches = response.getReturnValue();

                // If we have no matches, return
                if (matches.length == 0)
                {
                    return;
                }
                
                // Render the results
                this.renderLookupComponents(cmp, lookupListItems, matches);
            }
            else if (state === "ERROR") // Handle any error by reporting it
            {
                var errors = response.getError();
                
                if (errors) 
                {
                    if (errors[0] && errors[0].message) 
                    {
                        this.displayToast('Error', errors[0].message);
                    }
                }
                else
                {
                    this.displayToast('Error', 'Unknown error.');
                }
            }
        });
        
        // Enqueue the action                  
        $A.enqueueAction(action);                
    },

    /**
     * Render the Lookup List Components
     */    
    renderLookupComponents : function(cmp, lookupListItems, matches)
    {
        // list Icon SVG Path and Class
        var listIconSVGPath = cmp.get('v.listIconSVGPath');
        var listIconClass = cmp.get('v.listIconClass');

        // Array of components to create
        var newComponents = new Array();
         
        // Add a set of components for each match found
        for (var i=0; i<matches.length; i++)
        {
            // li element
            newComponents.push(["aura:html", {
                "tag" : "li",
                "HTMLAttributes" : {
                    "class" : "slds-lookup__item" 
                }
            }]);

            // a element
            newComponents.push(["aura:html", {
                "tag" : "a",
                "HTMLAttributes" : { 
                    "id" : cmp.getGlobalId() + '_id_' + matches[i].SObjectId, 
                    "role" : "option", 
                    "onclick" : cmp.getReference("c.select") 
                }
            }]);

            // svg component
            newComponents.push(["c:svg", {
                "class" : "slds-icon " + listIconClass + " slds-icon--small",
                "xlinkHref" : listIconSVGPath
            }]);

            // output text component
            // For some reason adding an aura:id to this component failed to record the id for subsequent cmp.find requests
            newComponents.push(["ui:outputText", {
                "value" : matches[i].SObjectLabel
            }]);
        }

        // Create the components
        $A.createComponents(newComponents, function(components, status) {
            // Creation succeeded
            if (status === "SUCCESS")
            {
                // Get the List Component Body
                var lookupListItemsBody = lookupListItems.get('v.body');

                // Iterate the created components in groups of 4, correctly parent them and add them to the list body
                for (var i=0; i<components.length; i+=4)
                {
                    // Identify the releated components
                    var li = components[i];
                    var a = components[i+1];
                    var svg = components[i+2];
                    var outputText = components[i+3];

                    // Add the <a> to the <li>
                    var liBody = li.get('v.body');
                    liBody.push(a);
                    li.set('v.body', liBody);

                    // Add the <svg> and <outputText> to the <a>
                    var aBody = a.get('v.body');
                    aBody.push(svg);
                    aBody.push(outputText);
                    a.set('v.body', aBody);

                    // Add the <li> to the container
                    lookupListItemsBody.push(li);
                }

                // Update the list body
                lookupListItems.set('v.body', lookupListItemsBody);
           }
           else // Report any error
           {
                this.displayToast('Error', 'Failed to create list components.');
           }
        });

    },

    /**
     * Handle the Selection of an Item
     */
    handleSelection : function(cmp, event) {
        // Resolve the Object Id from the events Element Id (this will be the <a> tag)
        var objectId = this.resolveId(event.currentTarget.id);

        // The Object label is the 2nd child (index 1)
        var objectLabel = event.currentTarget.children[1].innerText;

        // Log the Object Id and Label to the console
        console.log('objectId=' + objectId);
        console.log('objectLabel=' + objectLabel);
                
        // Create the UpdateLookupId event
        var updateEvent = cmp.getEvent("updateLookupIdEvent");
        
        // Populate the event with the selected Object Id
        updateEvent.setParams({
            "sObjectId" : objectId
        });

        // Fire the event
        updateEvent.fire();

        // Update the Searchstring with the Label
        cmp.set("v.searchString", objectLabel);

        // Hide the Lookup List
        var lookupList = cmp.find("lookuplist");
        $A.util.addClass(lookupList, 'slds-hide');

        // Hide the Input Element
        var inputElement = cmp.find('lookup');
        $A.util.addClass(inputElement, 'slds-hide');

        // Show the Lookup pill
        var lookupPill = cmp.find("lookup-pill");
        $A.util.removeClass(lookupPill, 'slds-hide');

        // Lookup Div has selection
        var inputElement = cmp.find('lookup-div');
        $A.util.addClass(inputElement, 'slds-has-selection');

    },

    /**
     * Clear the Selection
     */
    clearSelection : function(cmp) {
        // Create the ClearLookupId event
        var clearEvent = cmp.getEvent("clearLookupIdEvent");
        
        // Fire the event
        clearEvent.fire();

        // Clear the Searchstring
        cmp.set("v.searchString", '');

        // Hide the Lookup pill
        var lookupPill = cmp.find("lookup-pill");
        $A.util.addClass(lookupPill, 'slds-hide');

        // Show the Input Element
        var inputElement = cmp.find('lookup');
        $A.util.removeClass(inputElement, 'slds-hide');

        // Lookup Div has no selection
        var inputElement = cmp.find('lookup-div');
        $A.util.removeClass(inputElement, 'slds-has-selection');
    },

    /**
     * Resolve the Object Id from the Element Id by splitting the id at the _
     */
    resolveId : function(elmId)
    {
        var i = elmId.lastIndexOf('_');
        return elmId.substr(i+1);
    },

    /**
     * Display a message
     */
    displayToast : function (title, message) 
    {
        var toast = $A.get("e.force:showToast");

        // For lightning1 show the toast
        if (toast)
        {
            //fire the toast event in Salesforce1
            toast.setParams({
                "title": title,
                "message": message
            });

            toast.fire();
        }
        else // otherwise throw an alert
        {
            alert(title + ': ' + message);
        }
    }
})

The doSearch method takes care of calling the Apex controller and handling the results. It calls out to the renderLookupComponents method which creates and injects the <li> elements within the <ul> element of the component markup. We render a <a> element within the <li> element to fire the selection event. We make use of the SObjects Id within the rendered element id so that we can easily capture it when the item is selected. The resolveId method splits the SObject Id from the end of the element id.

The handleSelection method takes the selected entry and fires the updateLookupIdEvent before modifying the CSS classes to show the Lookup in its selected state.

The clearSelection method fires the clearLookupIdEvent before modifying the CSS classes to revert back to the search state.

The displayToast is simply an error logging method that renders a toast popup in the Salesforce1 app or an alert in the browser.

Lightning event: UpdateLookupId

This is the event fired when the user selects an item from search results. It has a single attribute that gets set with the Id of the selected record. You’ll notice this is a component event rather than an application event. Be careful here if you are nesting your components you’ll only be able to handle it in the outermost component. Consider changing it to an Application event if this is an issue.

Event

<aura:event type="COMPONENT" description="Update Lookup Id" >
    <aura:attribute name="sObjectId" type="Id" required="true" description="The Id of the selected SObject." />
</aura:event>

Lightning event: ClearLookupId

This is the event fired when the user clears the selected item. It has no attributes. Again, you’ll notice this is a component event rather than an application event. Once again, be careful here if you are nesting your components you’ll only be able to handle it in the outermost component. Consider changing it to an Application event if this is an issue.

Event

<aura:event type="COMPONENT" description="Clear the Lookup" />

Ok, great. Now can I see how you use it?

We’re going to wrap this up with an example Account Lookup component which you can plug straight into Salesforce1 via a Component Tab. Since the component implements flexipage:availableForAllPageTypes you could plug it straight into Lightning Builder if you wished. If you wanted to embed it in a Visualforce page you could do that as well. I won’t go into how to do that here but you’ll find details in the Lightning developer documentation.

Lightning component: AccountLookup

First lets look at the component markup. There’s an attribute to hold the Record Id. This is hooked up to the force:recordView component which is only rendered if an Id is present. We have two event handlers listening for the UpdateLookupId and ClearLookupId events. Note the names tie up to the registered events in the LookupSObject component. These event handlers hook up to the components controller via the handleAccountIdUpdate and handleAccountIdClear methods. The LookupSObject component markup takes the labels and icon resources as attributes.

Component

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes">
    <!-- Attributes -->
    <aura:attribute name="recordId" type="Id" description="The current record Id to display" />
    
    <!-- Event handlers -->
    <aura:handler name="updateLookupIdEvent" event="c:UpdateLookupId" action="{!c.handleAccountIdUpdate}"/>
    <aura:handler name="clearLookupIdEvent" event="c:ClearLookupId" action="{!c.handleAccountIdClear}"/>
    
    <!-- Lookup component -->
    <c:LookupSObject label="Account" pluralLabel="Accounts" sObjectAPIName="Account" 
        listIconSVGPath="/resource/SLDS092/assets/icons/standard-sprite/svg/symbols.svg#account"
        listIconClass="slds-icon-standard-account"
    />

    <!-- Record view -->
    <aura:if isTrue="{!v.recordId!=null}">
        <force:recordView recordId="{!v.recordId}" />
    </aura:if>
</aura:component>

Now lets take a look at the controller. You’ll see the handleAccountIdUpdate method gets the Id from the Event and updates the recordId attribute. The handleAccountIdClear simply nulls the recordId attribute. And that’s it, simples.

Controller

({
    /**
     * Handler for receiving the updateLookupIdEvent event
     */
    handleAccountIdUpdate : function(cmp, event, helper) {
        // Get the Id from the Event
        var accountId = event.getParam("sObjectId");

        // Set the Id bound to the View
        cmp.set('v.recordId', accountId);
    },

    /**
     * Handler for receiving the clearLookupIdEvent event
     */
    handleAccountIdClear : function(cmp, event, helper) {
        // Clear the Id bound to the View
        cmp.set('v.recordId', null);
    }
})

If you want to plug the component into Lightning builder, you’ll need to include a design in the component bundle. Since there are no attributes that need passing into our Account Lookup component it just looks like this:

Design

<design:component> 
</design:component>

Creating the Lightning Component Tab

  • Navigate to: Setup->Create->Tabs
  • Click on New Lightning Component Tab.
  • Select the AccountLookup from the dropdown list.
  • Enter a Tab Name.
  • Choose a Tab Style.

… and you’re nearly good to go.

All that remains is to add it to the Salesforce1 Navigation.

Navigate to: Setup->Mobile Administration->Salesforce1 Navigation

Add the Component tab to your navigation, fire up the Salesforce1 mobile app and give it a whirl!

Notes:

All the code samples assume the default namespace. If you have your own namespace you’ll need to change the references from c:mything to mynamespace:mything.

If you are using a different version of the SLDS, you’ll need to change the paths in the code from SLDS092 to match your version.

Code and Resources:

The code is based on Version 0.92 of the SLDS: https://www.lightningdesignsystem.com/resources/downloads

Github repository containing all the above code (Note this is now Version 2): https://github.com/tscottdev/Lightning-Lookup

Links and stuff:

Salesforce Lightning Design System

Trailhead: Build a Lightning App with the Lightning Design System

 

Tagged with: , , , ,
Posted in apex, code, force.com, lightning, salesforce
About Me
Product Services Developer at:
FinancialForce.com
All views expressed here are my own. More about me and contact details here.
Please sponsor me …
My wife and I are running the Great North Run to support The Nick Alexander Memorial Trust

If you've found my blog useful, please consider sponsoring us. Sponsor me on BT MyDonate

Enter your email address to follow this blog and receive notifications of new posts by email.

Copyright (there isn’t any, feel free to reuse!)

CC0
To the extent possible under law, Tony Scott has waived all copyright and related or neighboring rights to MeltedWires.com Examples and Code Samples. This work is published from: United Kingdom.

%d bloggers like this: