Here is a something cool you can add to your web site. You have a registration form that asks for a username. In order to save the user time and a trip to the server, you want to see if the username exists as they type it in. Let's look at how Spry could handle this.
First, let me build a simple form:
<form id="userform" action="null.html" method="post">
<h2>Register</h2>
<table width="600" border="0">
<tr valign="top">
<td align="right" width="200">username (min 4 characters)</td>
<td width="400"><input type="text" id="username" name="username" onKeyUp="checkUsername()">
<span id="resultblock" class="error"></span></td>
</tr>
<tr>
<td align="right">password</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td align="right">confirm password</td>
<td><input type="password2" name="password2"></td>
</tr>
<tr>
<td> </td>
<td><input type="button" value="Fake Submit"></td>
</tr>
</table>
</form>
The only thing you want to pay attention to here are these two lines:
<input type="text" id="username" name="username" onKeyUp="checkUsername()">
<span id="resultblock" class="error"></span>
What I've done here is used a bit of JavaScript to execute when the form field is changed. (And I know onKeyUp has a few issues. Can folks recommend a more well rounded approach?) As the user types, it will call my function, checkUsername():
function checkUsername() {
var uvalue = document.getElementById("username").value;
if(uvalue.length < 4) { status(''); return; }
Spry.Utils.loadURL("GET","userchecker.cfm?username="+encodeURIComponent(uvalue), false, usercheck);
}
In this code I grab the value of the form field. If the size is less than 4, I clear my result message and leave the function (the status function will be described in a bit). If we have enough characters, I then use loadURL, from the Spry.Utils package, to call a server side file. (Here is my earlier entry on loadURL.) I fire off the event and wait for the result. (At the end I'll talk about how to modify it to not wait.) Lastly, a function named usercheck will be called with the result. Let's take a look at that function:
function usercheck(request) {
var result = request.xhRequest.responseText;
if(result == 0) status("Username not available!");
else status('');
}
When the result returns from the request, I have an object that contains information returned in the result. In this case, my server side script will return either a 1 or a 0. 0 is the flag for the username not being available, so I use my status function to write that result. Here is the status function in case you are curious:
function status(str) {
var resultblock = document.getElementById("resultblock");
resultblock.innerHTML = str;
}
As you can see, it is just using a bit of DHTML to update the span block next to my form field. Last but not least, here is the ColdFusion code running behind the scenes. Obviously it is not hooked up to anything real:
<cfsetting showdebugoutput=false>
<cfparam name="url.username" default="bob">
<cfset takennames="victor,jack,ashley,gloria,nikki">
<cfif listFindNoCase(takennames, url.username)>
<cfset available = 0>
<cfelse>
<cfset available = 1>
</cfif>
<cfoutput>#available#</cfoutput>
You can test this out here, and be sure to view source on the HTML page for a more complete view.
So I mentioned earlier that if I made my request not wait (asynch), then I'd have to modify things a bit. Because the user could keep on typing, I would need to return both the result and the username in my server side code. I'd then need to check and see if the username in the form field was still the same. I'll post a followup showing an asynch version later on.
Edited on August 26: Rob Brooks-Bilson raises some very good points about the security of this. Please be sure you read the comments below.
Archived Comments
Nice. Works good.
Can you explain this call:
request.xhRequest.responseText
Ray, you might be able to use Prototype's formObserver instead of onkeyup. If I wasn't on vacation typing this from my laptop lying on the hotel bed, I might give a rip at a sample for you, but at the very least I'll bookmark this for a look when I get back!
Yeah - You keep raving about it and I can't wait to see it at the UG meeting. You know - something we should consider - maybe doing a joint preso on Breeze sometime from my office. If our meeting goes well "live", lets talk about it more. I bet my readers would love to see me do Spry again, _and_ see an alternative.
BL: That is just the API I found in other examples. I don't believe it is properly documented it yet though.
I see examples like this just-in-time lookup in AJAX libraries, but I'm not convinced. Even at my modest typing speed, the HTTP calls are so far behind me that I'm done and on to the next field before the first of 5 to 7 calls returns. For that reason, I'm still a big fan of the onBlur event. Nonetheless, thanks for the Spry example!
I think the type ahead idea is cool, but I don't think I'd ever use it to show usernames for an application. That seems to me to be inviting trouble as you're essentially giving people (potentially the wrong people) one component of the login. It's a security issue. A person with less than the best of intentions could essentially hit the letter "a" and get a list of all your "a" users, etc.
For "sensitive" sites such as customer portals, you may be giving away more information than that to competing customers, or competitors.
You're blog reaches a lot of people, so I'd hate to see any of them adopt the type ahead technique for this exact purpose, knowing that it creates a security concern.
Edward: As I mentioned, I was using a synch approach. This slows down the lookup. This is NOT optimal and should be switched to an asynch approach. There was another reason why I didn't do that, but that reason no longer applies so I'm going to post a follow up (because it shows another technique as well.)
Rob: Rob, was that to me? I don't use the type ahead to reveal usernames, I just use it to reveal dupes. So you can't type A and see Andy, Andrew, etc. The code only fires when usernames are more than 3 characters and to find a dupe you would need to know the name of the dupe. You _could_ use it to get a list of usernames, but only a bit faster than you could w/o it. If I wanted to sniff usernames w/o AJAX, I'd fill the form out, click submit, and go Back and tweak the username. I _do_ see your point, but I don't believe this would help hackers very much. Now - one thing I should point - and I just realized this, the CFM file does NOT check the length, so it would be a security hole. It should also check the referer (which can be faked, but would stop most script kiddies).
See what I get for skimming the post! My appologies Ray, you're right. I missed the part about pulling back just the dupes.
I still don't think it's a good idea to show them if a username exists, though. Same for a bad login. I know some sites will tell you "username not found", but in my book, sites shouldn't give back any more information than "invalid login".
Rob, this isn't a logon though, it is a registration (and if that isn't clear, it is my fault).
However - all in all - I feel strongly enough about your comments that I'm going to ban you - no wait, I mean I'm going to edit the entry to make sure folks read your comments below. You know - if you have any doubt when it comes to security crap, it is probably almost always a good idea to lean towards the more secure option. (Entry will be edited in 5 mins.)
Ok, I obviously shouldn't post this early in the morning on a Sat, and definitely not before a cup of coffee.
Ray, just ignore my comments. Right after I posted the last one I realized you weren't checking on a login form, but rather an account sign-up form.
Rob, absolutely not! If there is ANYTHING to be anal or 'sensitive' about, this is exactly it. I'm very happy you made these comments. I'm still going to ban you, but thanks. ;)
Seriously though - folks may be so turned on by AJAX that they may forget security issues. This is something folks should definitely keep in mind.
Is this supposed to be "Check if user exists"? it/if
thanks for the examples.
Yes. Was it not clear?
just referring to the potential typo in the heading.
Oops. Thanks.
Back to security issues, doing this does open the door a bit for a hacker. Right now as this script stands, it's very easy for a hacker to start posting data to the userchecker.cfm template to harvest a list of valid usernames.
It's always important to be things in place to monitor for suspicious looking traffic.
I definitely agree that the CFM file, as stands, is too insecure. It should have a length check, like the front end. It should check REFERER, which can be faked, but it would be nice to check.
I'm planning a followup to this article to show using XML results in Spry (again, non-dataset ones). In my followup, I'm going to change it to a simple string lookup type function. For example, you can imagine a CMS that demands you use unique page names.
JUST TESTING YOUR COMMENTS VIA BLOGCFC FLEX
I am not able to get the demo included here to work. I also tried to copy the code and test. Nothing happens - I even tried with a onBlur event - same results. Could some one tell me what I am doing wrong and point me to a working version? Thanks for all the great work.
The code I tested with:
REGISTER.CFM
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1...">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:spry="http://ns.adobe.com/spry">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"><title>Spry Test</title>
<script language="JavaScript" type="text/javascript" src="../SpryAssets/xpath.js"></script>
<script language="JavaScript" type="text/javascript" src="../SpryAssets/SpryData.js"></script>
<script language="JavaScript" type="text/javascript" src="../SpryAssets/SpryUtils.js"></script>
<script language="JavaScript" type="text/javascript">
<!--
function checkUsername() {
var uvalue = document.getElementById("username").value;
if(uvalue.length < 4) { status(''); return; }
Spry.Utils.loadURL("GET","registerChecker.cfm?username="+encodeURIComponent(uvalue), false, usercheck);
}
function usercheck(request) {
var result = request.xhRequest.responseText;
if(result == 0) status("Username not available!");
else status('');
}
function status(str) {
var resultblock = document.getElementById("resultblock");
resultblock.innerHTML = str;
}
-->
</script>
</head>
<body>
<form id="userform" action="null.html" method="post">
<h2>Register</h2>
<table width="600" border="0">
<tr valign="top">
<td align="right" width="200">username (min 4 characters)</td>
<td width="400"><input type="text" id="username" name="username" onBlur="checkUsername()">
<span id="resultblock" class="error"></span></td>
</tr>
<tr>
<td align="right">password</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td align="right">confirm password</td>
<td><input type="password2" name="password2"></td>
</tr>
<tr>
<td> </td>
<td><input type="button" value="Fake Submit"></td>
</tr>
</table>
</form>
</body>
</html>
REGISTERCHECKER.CFM
<cfsetting showdebugoutput=false>
<cfparam name="url.username" default="bob">
<cfset takennames="victor,jack,ashley,gloria,nikki">
<cfif listFindNoCase(takennames, url.username)>
<cfset available = 0>
<cfelse>
<cfset available = 1>
</cfif>
<cfoutput>#available#</cfoutput>
For the demo, try here:
http://www.coldfusionjedi.c...
As for your own code - do you get a JS error?
No JavaScript error. Now that I checked, it is actually sending back the value of available - the issue is it is not displaying this either in the resultblock div or in an alert box (i just get a blank alert box). When i check the response on my Firebug console, i do see the value as 1.
I need to capture the values returned and update the form fields as well as the dataset in the master detail set up that i am planning to use this for.
Thanks,
-Xavier
I am testing this on a form I am developing. I have put the REGISTERCHECKER.CFM in the form of a component.
I am having trouble with the loadURL return (var result = request.xhRequest.responseText;).
I got tons of html. The cfc returns the proper result if I enter the url directly ( http://76.12.43.89/cfc/reg.cfc?method=userCheck&uvalue=dthardy ).
I have an alert that pops up with the var result @ http://76.12.43.89/_action/registration_tmp2.cfm
I am testing it on the "Invitation Code" textfield. I have been struggling with this all day. Any ideas how to fix?
Actually, you aren't. View source on the URL and you see a lot of stuff on top. Both comments and the fact that the CFC wddx-encoded the response. In order to use a CFC and not have stuff around it, you need to use returnFormat, which will work in CF8. See the docs for more info on that.
Looked through livedocs for CF8. Saw where invoking a cfc by url "returns a result using the cfreturn tag, ColdFusion converts the text to HTML edit format (with special characters replaced by their HTML escape sequences), puts the result in a WDDX packet, and includes the packet in the HTML that it returns to the client." I couldn't find specific instructions to get rid of said HTML!
I came up with returnformat="plain". This removed the wddx packet stuff but all the HTML info is still showing up before the 0 or 1.
How do you get rid of the rest of the HTML?
If you view source, you will see it's a lot of html comments. That's being generated by something else on your server, most likely an Application.cfm or Application.cfc file.
It was in the application.cfm file. I commented out all the cfoutput html. Hopefully I won't need any of it.
I meant to conclude with "It works now!"
I am now trying to get "check if user exists" to work with spry textfield validation for required, minchar, and maxchar. I have written a javascript function that runs through each form field to check validation status before performing the next process. I am having trouble adding checkUsername process to the validation function. I tried making 'result' a global variable but it caused both the usercheck and validation function to fail. What is a better method? Here is the key elements of what I have now.
**************My function*****************
function userValidate(){
userNameObj.validate();
firstNameObj.validate();
lastNameObj.validate();
genderObj.validate();
zipCodeObj.validate();
if ((userNameObj.validate() && firstNameObj.validate() && lastNameObj.validate() && genderObj.validate() && zipCodeObj.validate() === true) && (result != 0)) {
sp1.showPanel('findCause');
}else{
return false;
}
***********What I changed in your example***********
var result = "";
function usercheck(request) {
result = request.xhRequest.responseText;
if(result == 0) status("Username not available!");
else status('');
}
The issue is that my code uses an asynch process to check the value. Ie, fire and call some func when done. You would need to change it to a synch process in your validation. Check the docs for Spry.Utils.loadURL to see if that is possible. It should be.
I am trying to use your script to check for existing product title in the DB, which works well except when i click on another tab in firefox, then it gives me a following spry error (pops up in the lowest right corner of the browser):
Exception caught while loading agentSites/userchecker.cfm?productname=hiking%20in%20cuba: Function expected
I tried googling it and changing all the possible things in the script, but the error still exists.
Maybe you can shine the light on the problem?
i am using onBlur() to trigger the check, can that be an issue?
Well in theory, if onBlur was the issue, then you could click anywhere on the page to force it. To be honest, I've _never_ seen a JS bug thrown by clicking on another tab. Do you also get it if you click in the chrome some place?
Thanks a lot for answer!
It seems like clicking on another tab (or anywhere else for that matter, since onblur triggers anyways) causes that error most of the time, though it happens when i just click off the field too, just more rarely i guess.
What really throws me off is that it is such a random error... It pops up both when it finds the same product name and when there is no same name in db.
Here is the code:
<script>
Spry.Data.Region.debug = true;
function checkProductname() {
var uvalue = document.getElementById("productTitle").value;
Spry.Utils.loadURL("GET","agentSites/userchecker.cfm?productname="+encodeURIComponent(uvalue), false, productcheck);
}
function productcheck(request) {
var result = request.xhRequest.responseText;
if(result == 0){
status("<br />This title already exists! Please choose another one."); document.getElementById("productTitle").focus(); document.getElementById("productTitle").select();
}else{
status('');
}
}
function status(str) {
var resultblock = document.getElementById("resultblock");
resultblock.innerHTML = str;
}
</script>
And here is the usechecker.cfm:
<cfsetting showdebugoutput=false>
<cfparam name="url.productname" default="">
<cfquery name="checkName" datasource="agent_sites">
SELECT productTitle
FROM products
</cfquery>
<!--- Create a list of existing products --->
<cfset productList = valueList(checkName.productTitle, ",")>
<cfif listFindNoCase(productList, url.productname)>
<cfset available = 0>
<cfelse>
<cfset available = 1>
</cfif>
<cfoutput>#available#</cfoutput>
I mean if your example works fine for me, than its gotta be something with backend...
Also, that page uses spry for form validation as well, maybe its two spry scripts colliding???
Thanks again for help, i am so desperate already, i am trying to punish this error for like ever now ;)
Well it says that productcheck doesn't exist. productcheck is the call back function you wrote to handle the result. I don't see any possible way for your code to accidentally overwrite it. Honestly I have no idea unfortunately. Is it online where I can see?
Its on the company's internal website, so i can't really give access to that :(
Thank you very much for trying to help out, i really appreciate it.
I will let you know if i find the solution (the weirdest thing is that when i google something like "spry function expected" it doesnt even give me any good results, so it must be something wrong with my setup...)
This script is within another massive website, which i havent built, so its possible that something there is causing the problems. So i guess i should really start with isolating your script and seeing how it works standalone...
As i promised, i would post the result of my fight with the script. Well it seems like it's fixed now.
What i did is get rid of status(str) function (since that is what was giving me a hard time) and in the end have that:
if(result == 0){
document.getElementById("resultblock").innerHTML = "<br />This title already exists! Please choose another one.";
}else{
document.getElementById("resultblock").innerHTML = "";
}
Not sure if anyone else will have a problem like that, but i thought i would just share it.
Thank you again for trying to help out and mostly for the script itself!
Hello Raymond, not the first time I write to you and always with great attention from you.
Raymond, I wasn't able to make your demo (not the code, but the demo) to work. The link on your post doesn't work, and the link you gave to Xavier on June 25, 2007 doesn't respond to the validation.
I'm creating a form where designers add collections to the database. Basically I need to avoid the same collection is entered again and I would love to do it without submitting the form (real time username validation). Do you have a sample for this?
Thanks so much, as usual.
I just tested and it worked for me in FF3.5. I entered "victor" and it told me the user was taken. Did you enter that?
My mistake. I apologize.
Let me ask you something else: my hosting provider needs to have what installed? Just CF8 or something else?
Thanks again.
Not even CF8 - this should work with ... shoot, CF1 even. It's mainly the Spry code. CF is just returning a 1 or a 0.
Hello again Ray, I have a question about this: I'm doing some tests with two different names. It works with one of them but not with the other one. I've tried putting a text message in case the name is available and tried entering the one taken and it shows it as available.
Do you know why this could be happening?
Thanks, as usual.
Something new just discovered: It will respect the latest value. Let's say I enter a new username, so if I try to re-enter that one, it will display the warning. Now, if I enter another one, the the previous one will show as available....checking the code to see where I'm telling my query to check the latest record...
I think it's solved with Daniii comment from October 27 2008. Almost a year ago. Thanks Daniii, thanks Ray.
Catching up now. Glad you got it. :)