Background

Prior to OpenMRS version 1.8, the search widgets were written with dojo which involved writing a separate javascript file where you would have to  extended the parent OpenmrsSearch for each Openmrs Object if it were to be searchable in the webapp. The  widgets would fetch all hits in one ajax query which would take a while to return all the hits in case there are many making the widgets slow. In 1.8, this was changed by introducing a single and more generic search widget written with jquery, the widget is faster from a user's stand point in that it fetches just the exact number of results to display on the first page and then continue to query the server for the rest in the background while updating the table until all results are returned. The reason behind this is that it should take way less time to display the first N results to display on the first page rather waiting for all the matching hits to be returned in one call in case there are many. The objective is to increase the perceived speed from a user's point of view since they get back results for the first page in a nick of time even if it isn't all.

How to include a search widget for a domain object in a jsp

This can be relatively simple if the domain objects to search already have the required methods in the API. Currently the required methods have been added for Concepts, Encounters, Patients, Users, Locations, Providers, Drugs and ConceptReferenceTerms. Let's assume you wish to add an encounter search widget to your jsp, add the snippet below to your jsp (you can leave out the javascript and css files that you already have in the parent jsp).

<%-- (OPTIONAL) This should be the target DWR service that processes the http requests for results in case you fetch them via DWR --%>
<openmrs:htmlInclude file="/dwr/interface/DWREncounterService.js"/>

<%-- (OPTIONAL) Include this to apply css to improve the look and feel of the widget if the containing page doesn't include it --%>
<openmrs:htmlInclude file="/scripts/jquery/dataTables/css/dataTables_jui.css"/>

<%-- This is required if the containing page doesn't include it --%>
<openmrs:htmlInclude file="/scripts/jquery/dataTables/js/jquery.dataTables.min.js"/>

<%-- REQUIRED --%>
<openmrs:htmlInclude file="/scripts/jquery-ui/js/openmrsSearch.js" />

<script type="text/javascript">
	var lastSearch;
	$j(document).ready(function() {
		new OpenmrsSearch("findEncounter", true, doEncounterSearch, doSelectionHandler,
				[	{fieldName:"personName", header:"Patient Name"},
					{fieldName:"encounterType", header:"Encounter Type"},
					{fieldName:"formName", header:"Encounter Form"},
					{fieldName:"providerName", header:"Encounter Provider"},
					{fieldName:"location", header:"Encounter Location"},
					{fieldName:"encounterDateString", header:"Encounter Date"}
				],
                {
                    searchLabel: '<spring:message code="Encounter.search" javaScriptEscape="true"/>',
                    searchPlaceholder:'<spring:message code="Encounter.search.placeholder" javaScriptEscape="true"/>'
                });
	});

        //The action to take when the user selects an item from the hits in the widget
	function doSelectionHandler(index, data) {
		document.location = "encounter.form?encounterId=" + data.encounterId + "&phrase=" + lastSearch;
	}

	//Contains the logic that fetches the results from the server,, should return a map of the form <String, Object>
	function doEncounterSearch(text, resultHandler, getMatchCount, opts) {
		lastSearch = text;
		DWREncounterService.findCountAndEncounters(text, opts.includeVoided, opts.start, opts.length, getMatchCount, resultHandler);
	}
</script>

The first four html includes are required because they contain the necessary scripts used by the widgets and the css file for improved styling via datatables' support for jquery-ui themes so that the widgets match the active theme on the page.

You call to the server needs to return a map of the results with the following key names:

Next thing is to initialize the widget and there are 2 ways to do it, the one used above is the one which delegates to a helper function that takes in 6 arguments, the arguments are;

the id of the element inside which the widget should be embedded, the rest of them are configuration properties that are explained below under the 'widget properties' section.

Below is the other way of how to initialize the widget:

$j(document).ready(function() {
  		$j("#elementId").openmrsSearch({
  			searchLabel:'<spring:message code="General.search"/>',
  			searchPlaceholder: '<spring:message code="Encounter.search.placeholder" javaScriptEscape="true"/>',
  			searchHandler: doSearchHandler,
  			selectionHandler: doSelectionHandler,
  			fieldsAndHeaders: [	{fieldName:"personName", header:"Patient Name},
					{fieldName:"encounterType", header:"Encounter Type},
					{fieldName:"formName", header:"Encounter Form},
					{fieldName:"providerName", header:"Encounter Provider},
					{fieldName:"location", header:"Encounter Location},
					{fieldName:"encounterDateString", header:"Encounter Date}
				]
  		});
  	});

The arguments passed to the widgets are to used to customize the widget properties, in the example above, 'searchLabel' specifies the label that appears to the left of the search input box, 'searchPlaceHolder' specifies the text to display inside the input box as a place holder, 'searchHandler' specifies the javascript function to be called to fetch the hits from the server, and 'fieldsAndHeaders' specifies the properties of the returned objects to display in the table of results along with their respective column headers, see below for the full list of the widget properties. 'SelectionHandler' specifies the function to be called when the user selects a row in the table, in our example above, we open the encounter form and the encounter to edit becomes the selected one.

'SearchHandler' specifies the function that will get called by the script to fetch results from the server, the function gets called at least once once the search is triggered. The first call is expected to return a map with 2 key-value pairs i.e the expected total count of results and results to display on the first page. Typically, you should have logic on the server that get called by this function via ajax and it is important to make sure it returns the expected map. The keys in the returned map are 'count' and 'objectList' and their values should be the total count of expected results and the results to display on the first page respectively. Any subsequent calls to the search handler  after the first call don't require that the count gets returned.

In this example, we call the DWREncounterService.findCountAndEncounters(String, boolean, Integer, Integer, boolean) method and below is how the code could like like:

public Map<String, Object> findCountAndEncounters(String phrase, boolean includeVoided, Integer start, Integer length,
	        boolean getMatchCount) throws APIException {
		//Map to return
		Map<String, Object> resultsMap = new HashMap<String, Object>();
		Vector<Object> objectList = new Vector<Object>();
		try {
			EncounterService es = Context.getEncounterService();
			int encounterCount = 0;
			if (getMatchCount)
				encounterCount += es.getCountOfEncounters(phrase, includeVoided);

			//If we have any matches, fetch them or if this is not the first ajax call
			//for displaying the results on the first page, the getMatchCount is expected to be zero
			if (encounterCount > 0 || !getMatchCount)
				objectList = findBatchOfEncounters(phrase, includeVoided, start, length);

			resultsMap.put("count", encounterCount);
			resultsMap.put("objectList", objectList);
		}
		catch (Exception e) {
			objectList.clear();
			objectList.add("Error while searching for encounters");
			resultsMap.put("count", 0);
			resultsMap.put("objectList", objectList);

			//you can opt to pass in a new phrase which will tell the core search widget to rerun the
			//search but for your new phrase and this will lead to ignoring the results you send back
			resultsMap.put("searchAgain", "newphrase");
		}
		return resultsMap;
	}

The implementation of the server side logic should call the API methods that return the count and the appropriate search method that supports paging for the given domain object, in this case they would be EncounterService.getCountOfEncounters(String, boolean) and EncounterService.getEncounters(String, Integer, Integer, boolean). In the DAO layer, it's highly recommended that these methods use the same criteria object and the difference should be that the one that gets the count by setting a row count projection while the other should return the actual objects matching the criteria. This implies that the total number of hits fetched when the search is done should always match the value returned by the get Count method otherwise the scripts in the widgets will fail with the assumption that there is either more hits to fetch. For an example implementation of the methods in the DAO layer, you can look at HibernateEncounterDAO.getCountOfEncounters(String, boolean) and HibernateEncounterDAO.getEncounters(String, Integer, Integer, boolean).

If you don't wish to support paging in your widgets and want to have all results returned in one request just set the value of the length argument to null when calling your server side code in the search handler.

Widget properties