SSbits - Home page
Site by Carbon Crayon
Submit a Post >

Tutorials - Big bits of code to help you do more

Create an AJAX Auto-Complete Member Search with SilverStripe and jQuery

SSBautocomplete

 Source files (22 KB)  Demo (admin/pass)

For ages now I have been meaning to figure out how to get an AJAX autocomplete field working with SilverStripe Objects. So when I found this great jQuery Autocomplete Plugin, I thought now would be a good time. As always SilverStripe provides all the tools necessary to complete the task without too much hassle. 

Getting Started

First thing you need to do is download the jQuery AutoComplete plugin and extract it into a folder called 'autocomplete' in the root of your site. You can delete the un-minified version as well as the jquery file, as we will be sourcing our own.

So now let's create our MemberSearchPage class where we can put all our fancy search code and its associated templates:

mysite/MemberSearchPage.php

<?php
class MemberSearchPage extends Page 
{
	
}
class MemberSearchPage_Controller extends Page_Controller 
{

}

themes/myTheme/templates/Layout/MemberSearchPage.ss

<div class="typography">

	<h2>$Title</h2>
	
	$Content
	
</div>

themes/myTheme/templates/Layout/MemberSearchPage_results.ss

<div class="typography">

	<h2>$Title</h2>	
	
</div>

Including the JS and Building the form

So next step is to include the jQuery plugin (along with jQuery it self) and build our form ready to display on the page. So inside mysite/MemberSearchPage.php add the following code:

class MemberSearchPage_Controller extends Page_Controller 
{
	static $allowed_actions = array(
		'MemberSearchForm'
	);
	
	public function init()
	{
		parent::init();

		//CSS
		Requirements::css("autocomplete/css/autocomplete.css");		
		
		//JS
		Requirements::javascript("http://ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js");			
		Requirements::javascript("autocomplete/javascript/jquery.autocomplete-min.js");
		Requirements::CustomScript("
		
			jQuery(document).ready(function()
			{
				var options, a;
				
				jQuery(function(){
				  options = { serviceUrl:'" . $this->Link('results') . "' };
				  a = $('#SearchForm_MemberSearchForm_query').autocomplete(options);
				});						
				
			})
		
		");
	}
	
	public function MemberSearchForm()
	{
		$fields = new FieldSet(
			new TextField('query', 'Search Member name', $this->getSearchQuery())
		);
					
		$form = new SearchForm($this, 'MemberSearchForm', $fields);
		
        $form->setFormAction($this->Link('results'));
				
		return $form;
	}

	function getSearchQuery()
	{
		if($this->request)
			return $this->request->getVar("query");
	}
}

Ok so there is quite a lot of code here, let's go through it. first we define $allowed_actions as a static and give it the name of our form function. This tells silverstripe it is allowed to run a function called MemberSearchForm on this controller.

This is followed by the init() function which is where we include all the JS/CSS files for the autocomplete plugin to work. Nothing you won't have seen before here, the only thing to note is that we tell the plugin to use $this->Link('results')  (line 24) as the service URL to get the JSON suggested results. We will create the 'results' action in a minute.

Then we create our MemberSearchForm() function which is going to return the form it self. We are going to use SilverStripes existing SearchForm class but we don't want the default name for the text field so we define our own as well as setting it's default to be generated by the getSearchQuery() function, before passing it in to the constructor (line 39). We then change the form action to also be $this->Link('results') so that it uses the same function as the plugin to generate results, albeit in a different format. 

Finally we define the getSearchQuery() function which simple checks for a GET variable named 'query' (the same as the text field name) and returns if if it's set. We this means that when the results page loads with the form redrawn, it still has the search text in it. It could also be used for feedback like 'You searched for $SearchQuery' in the template.

Now that we have our form generated function we can add it to the template:

themes/myTheme/templates/Layout/MemberSearchPage.ss

<div class="typography">

	<h2>$Title</h2>
	
	$Content
	
	$MemberSearchForm
	
</div>

Generating the AJAX results

Now, the autocomplete plugin wants its data passed to it in a nicely formatted JSON array. I attempted to do this with SilverStripes built in JSON formatters, Convert::array2json() and JSONDataFormatter, but I couldn't seem to get the format quite right, so I opted to generate it manually in the function. Add the results() function to your MemberSearchPage_Controller class and update the $allowed_actions to include it like so:

mysite/code/MemberSearchPage.php

class MemberSearchPage_Controller extends Page_Controller 
{
	static $allowed_actions = array(
		'MemberSearchForm',
		'results'
	);
	
	.
	.
	.
	.
	.

	public function results()
	{		
		if($query = $this->getSearchQuery())
		{
			$query = Convert::raw2sql($query); 
			
			//Search for our query - Pretty basic example here
			$Results = DataObject::get('Member', 
				"MATCH (FirstName, Surname) AGAINST ('$query') 
				OR FirstName LIKE '%$query%' 
				OR Surname LIKE '%$query%'"
			);

			//For AutoComplete
			if(Director::is_ajax())
			{
				$Members = $Results->map('ID', 'Name');
				
				$Suggestions = "['" . implode("','",$Members) ."']"	;
				
				return $json = "{
					query : '$query',
					suggestions : $Suggestions
	 			}";
			}
		}

		Director::redirect($this->Link());
	}

}

Let's go through the results() function. The first thing we do is check that there is a Search Query, because otherwise there is no point in doing the search. Then we make sure to sanitize the users input using Convert::raw2sql() before passing it into the db query.

Next we do our search, in this case a very simple MAKE/LIKE query which just looks at the FirstName and Surname fields of Member for a match. You can of course extend this to suit your own search needs. Once we have our $Results, we can build the JSON string to pass to the autocomplete plugin. The code to generate it is not exactly pretty, but it gives us total control on the formatting. You will notice that this is wrapped in a if(Director::is_ajax()) condition. This is because the result we want to give the autocomplete field is not the same as the result we want to pass to the results template. Lucky then that SilverStripe gives us this simple way of checking who is asking for the results.

If you type into the search form now, you should get the autocomplete results being generated, but don't try to submit the form yet as we'll be adding the template next.

Returning Results to the Template

Now that we have the AJAX side of things working, we just need to return the results to the template and we'll be done. So well add a second part to our results() function so that it looks like this:

mysite/code/MemberSearchPage.php

class MemberSearchPage_Controller extends Page_Controller 
{
	.
	.
	.

	public function results()
	{		
		if($query = $this->getSearchQuery())
		{
			$query = Convert::raw2sql($query); 
			
			//Search for our query - Pretty basic example here
			$Results = DataObject::get('Member', 
				"MATCH (FirstName, Surname) AGAINST ('$query') 
				OR FirstName LIKE '%$query%' 
				OR Surname LIKE '%$query%'"
			);

			//For AutoComplete
			if(Director::is_ajax())
			{
				$Members = $Results->map('ID', 'Name');
				
				$Suggestions = "['" . implode("','",$Members) ."']"	;
				
				return $json = "{
					query : '$query',
					suggestions : $Suggestions
	 			}";
			}
			//For Results Template
			else
			{
				$Output = new ArrayData(array(
					'Title' => 'Results',
					'Results' => $Results
				));	
				
				return $this->customise($Output);							
			}
		}

		Director::redirect($this->Link());
	}
	
}

Now all we have done here is add the else{...} part (line 33) which returns an ArrayData() object to the template with the Results and a new  Page Title. By calling $this->customise($Output) we tell SilverStripe to re-render the page using our custom $Output data.

So all that is now left is to create our template for the results. To make things easy, we will take advantage of SilverStripes template naming convention, so we call our template MemberSearchPage_results.ss and SilverStripe knows that when we call the results action on this page via the URL (i.e. my-member-search-page/results) that it should render it using that template. The convention is ClassName_Action.ss. You could of course explicitly tell SS which template to use by calling $this->Customise($output)->renderWith(array('Mytemplate', 'Page')); instead.

themes/myTheme/templates/Layout/MemberSearchPage_results.ss

<div id="content" class="typography">
	
	<h2>$Title</h2>
	
	<% if SearchQuery %>
		<% if Results.Count %>
		    <ul id="results">
		      <% control Results %>
		        <li>
					<p>$FirstName, $Surname</p>
		        </li>
		      <% end_control %>
		    </ul>
		 <% else %>
		    <p>Sorry, your search query did not return any results</p>
		 <% end_if %>
	<% end_if %>	

	<h2>Search again</h2>
	$MemberSearchForm

</div>

Anyway, that's it! You should now have a fully functioning Autocomplete field and some code that you can easily extend to help your uses find whatever it is they are looking for :)

Aram Balakjian avatar

Aram Balakjian

Aram is a web developer running London based agency Aab Web. He has a strong passion for developing attractive, usable sites around the SilverStripe CMS.

  • Bart van Irsel
    10/05/2011 6:16pm (3 years ago)

    Hi Aram,

    Thanks for this tutorial. The last links still points to your local machine :)

    Cheers,

    Bart

  • Aram Balakjian
    10/05/2011 6:19pm (3 years ago)

    Ahh cheers Bart! Fixed :)

  • MisterAC
    10/05/2011 7:59pm (3 years ago)

    Hi Aram. Nice article - I'll be needing something similar shortly, so the timing is great.

    There seems to be a small issue in the code for template templates/Layout/MemberSearchPage.ss - the div is wrapped in a strong with some inline styling. Perhaps it's TinyMCE's doing.

  • Bauke
    10/05/2011 10:48pm (3 years ago)

    Hi Aram,
    thanks for the nice article, working on something similar right now, so this comes in really helpfull!

  • Aram Balakjian
    11/05/2011 10:04am (3 years ago)

    Thanks guys!

    @MisterAC - Thanks fixed now, the joys of TinyMCE!

  • Trskldn
    12/05/2011 1:50pm (3 years ago)

    Thanks !I love SS!

  • Paltat
    26/05/2011 11:02am (3 years ago)

    Sweet article Aram! I just noticed the explanation for sanitizing the user input by using raw2xml did not match the code you've written because in your code you are using raw2sql. :)

  • Aram Balakjian
    26/05/2011 1:50pm (3 years ago)

    Thanks Paltat, fixed :)

  • MRKDevelopment
    01/06/2011 7:15am (3 years ago)

    This article really helped me. I actually used these technique to make a location search for a bread maker.

    It really worked great for just a how to make filtered results ontop of a data object.

    Thanks SS for saving me yet again when it comes to SilverStripe website dev.

  • SamTheJarvis
    15/06/2011 4:07pm (3 years ago)

    I take it the same principles outlined in this can be used to request whole pages via AJAX also?

    I'm looking to implement an ajax search results page + ajax pagination (however I've found search to be really.. limited).

    Should be fun!

  • einsteinsboi
    15/06/2011 8:39pm (3 years ago)

    Very nice, can't wait to try this! Excellent resources here

  • William Melbourne
    18/06/2011 1:18am (3 years ago)

    another great ssbits post, you guys and unclecheese are doing a huge help to the SS community.

    one thing I think might needed to be added to this you don't mention the AutocompleteMemberDecorator in the tutorial but it is in the downloaded source code, I was trying to follow the tutorial through and couldn't get the search to work so tried downloading the source and realise it was the indexes which were missing.

    also I couldn't get the autocomplete to work and was messing around with jquery for a while then realized that i needed to have the prototype turned off so in case it helps someone else

    Validator::set_javascript_validation_handler('none');

  • alex li
    15/08/2011 5:39am (3 years ago)

    Great Silverstirpe Ajax tutoria, may just save my day!

  • Victor Hernandez
    08/09/2011 11:47pm (3 years ago)

    How would I go about doing this for a list of locations added through the CMS instead of Members.

    For Example I want to add locations in the admin with a Name, City and State. I want to have a form with 3 fields: 1) Name, 2) City, and 3) State. When the user begins to fill out the Name field I want it to begin displaying the name of the locations added to the admin. When 1 is chosen I want the City and State fields to get automatically populated with the data in the admin that's associated to the name.

    Is this even possible with SilverStripe?

  • Aram Balakjian
    09/09/2011 9:19am (3 years ago)

    Hi Victor,

    It's possible, but too much for me to explain right now. Effectively you need to trigger an ajax call to a function which get's the second dropdowns items based on the first, so the URL might be yoursite.com/getdropdown/city/{name} and the same for the stage. The autocomplete would work in much the same way but you would need to adapt the results() function to get the Location objects and search their Name fields.

  • Victor Hernandez
    19/10/2011 5:07pm (3 years ago)


    Hi Aram,

    I have a dilemma. I was able to modify the code so that this works for me on a regular form with a thank you message.

    I also want the form to save the data as a record but once I add the $form to results($data, $form) the autocomplete stops working. Any ideas?

    <?php
    class MemberSearchPage extends Page {}

    class MemberSearchPage_Controller extends Page_Controller {
    static $allowed_actions = array(
    'SignUpForm',
    'results'
    );

    public function init() {
    parent::init();

    //CSS
    Requirements::css("autocomplete/css/autocomplete.css");

    //JS
    //Requirements::javascript("http://ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js");
    Requirements::javascript("autocomplete/javascript/jquery.autocomplete-min.js");
    Requirements::CustomScript("

    jQuery(document).ready(function() {
    var options, a;

    jQuery(function(){
    options = { serviceUrl:'" . $this->Link('results') . "' };
    a = jQuery('#Form_SignUpForm_credit_union').autocomplete(options);
    });

    })

    ");
    }

    public function SignUpSuccess() {
    return isset($_REQUEST['signupsuccess']) && $_REQUEST['signupsuccess'] == "1";
    }

    public function SignUpForm() {
    $fields = new FieldSet(
    new TextField('first_name','First Name*'),
    new TextField('last_name','Last Name*'),
    new TextField('credit_union', 'Credit Union*', $this->getSearchQuery()),
    new EmailField('email','E-mail address*')
    );

    $actions = new FieldSet(
    new FormAction('results', 'Submit')
    );

    $validator = new RequiredFields('first_name', 'last_name', 'credit_union', 'email');

    $form = new Form($this, 'SignUpForm', $fields, $actions, $validator);

    return $form;
    }

    function results($data, $form) {
    if($credit_union = $this->getSearchQuery()) {
    $credit_union = Convert::raw2xml($credit_union);

    //Search for our query - Pretty basic example here
    $Results = DataObject::get('CreditUnion',
    "MATCH (Name) AGAINST ('$credit_union')
    OR Name LIKE '%$credit_union%'"
    );

    //For AutoComplete
    if(Director::is_ajax()) {
    $Members = $Results->map('ID', 'Name');

    $Suggestions = "['" . implode("','",$Members) ."']" ;

    return $json = "{
    query : '$credit_union',
    suggestions : $Suggestions
    }";
    }
    }

    // Store in DB
    $submission = new SignUpSubmission();
    $form->saveInto($submission);
    $submission->write();

    // E-mail it
    $From = $data['email'];
    $To = 'email@test.com';
    $Subject = 'Form Submission';
    $email = new Email($From, $To, $Subject);
    $email->setTemplate('Template');
    $email->populateTemplate($data);
    $email->send();

    // Auto Reply Email
    $autoFrom = 'test@email.com';
    $autoTo = $data['email'];
    $autoSubject = 'Form Submission';
    $autoEmail = new Email($autoFrom, $autoTo, $autoSubject);
    $autoEmail->setTemplate('EmailSignUpTHINK12Reply');
    $autoEmail->send();

    Director::redirect(Director::baseURL(). $this->URLSegment . "/?signupsuccess=1");
    }

    function getSearchQuery() {
    if($this->request)
    return $this->request->getVar("query");
    }
    }

  • Victor Hernandez
    19/10/2011 5:16pm (3 years ago)

    Adam I was able to figure it out I just separated the functions like below.

    All I need now is to be able to populate other fields based on the selected credit union from the auto complete. Any help would be appreciated.

    function results() {
    if($credit_union = $this->getSearchQuery()) {
    $credit_union = Convert::raw2xml($credit_union);

    //Search for our query - Pretty basic example here
    $Results = DataObject::get('CreditUnion',
    "MATCH (Name) AGAINST ('$credit_union')
    OR Name LIKE '%$credit_union%'"
    );

    //For AutoComplete
    if(Director::is_ajax()) {
    $Members = $Results->map('ID', 'Name');

    $Suggestions = "['" . implode("','",$Members) ."']" ;

    return $json = "{
    query : '$credit_union',
    suggestions : $Suggestions
    }";
    }
    }
    }

    function doSignUp($data, $form) {

    // Store in DB
    $submission = new SignUpSubmission();
    $form->saveInto($submission);
    $submission->write();

    // E-mail it
    $From = $data['email'];
    $To = 'test@email.com';
    $Subject = 'Form Submission';
    $email = new Email($From, $To, $Subject);
    $email->setTemplate('Template');
    $email->populateTemplate($data);
    $email->send();

    // Auto Reply Email
    $autoFrom = 'test@email.com';
    $autoTo = $data['email'];
    $autoSubject = 'Form Submission';
    $autoEmail = new Email($autoFrom, $autoTo, $autoSubject);
    $autoEmail->setTemplate('Template');
    $autoEmail->send();

    Director::redirect(Director::baseURL(). $this->URLSegment . "/?signupsuccess=1");
    }

  • ss_user
    01/06/2012 1:04pm (2 years ago)

    Hi Aram,

    Thanks for the nice tutorial. . but the query isnt working for me havent played with MATCH and AGAINST much i usually go with like

    getting "#1191 - Can't find FULLTEXT index matching the column list" error when i try to run it through PHPMYADMIN

    how can i fix this ?

  • Berry
    21/08/2012 2:10pm (2 years ago)

    Hi Aram,

    I really would like to use this script on my site but I am unable to locate where I need to put my dbsettings, so where to connect to and I can also not find the actual query that I want to perform on my database. Can you help me getting this info?

    Cheers,
    Berry

Post a comment ...

You cannot post comments until you have logged in. Login Here.

Advertisement

Site of the Month

Find SSbits on

Top Contributers

Rank Avatar Name
1 article image Aram Balakjian
2 article image Daniel Hensby
3 article image Marcus Dalgren
4 article image Hamish Campbell
5 article image njorndare
6 article image Ty Barho
7 article image Martijn van Nieuwenhoven
8 article image Darren-Lee
9 article image Roman Schmid
10 article image Matt Clegg

View full leaderboard


Advertisement