We help IT Professionals succeed at work.

Getting search results to display ul for text field being inputted into

Black Sulfur
Black Sulfur asked
on
410 Views
Last Modified: 2017-03-11
I had this question after viewing Ajax php search with multiple text fields generated by php.

So, I tried to think about what everyone has said and based on that I have tried to come up with a solution that doesn't require using ID's. At this stage I don't even want to add complexity by adding data-id values.

To recap, an unknown number of text fields will be generated by a user. Depending on their input, 1 could be echoed out, 3 could be echoed out etc. All of which will be identical and look pretty much like this: (I have put 2 together for this example)

<div class="form-group">
	<input type="text" name="name" autocomplete="off">
	<div class="result"></div>
</div>
<div class="form-group">
	<input type="text" name="name" autocomplete="off">
	<div class="result"></div>
</div>

Open in new window


When results are displayed and a user clicks on a li, that text displays in the text field closest or associated to that text field which is exactly what I want.

The problem I am having though is getting the ul to only display in the text field the person is typing in. What is happening now is that if I type in text field 1, the ul shows for both text fields which is wrong. I am struggling to get this line right.

$(".result").closest("ul").html(data);

Open in new window


Here is the full jQuery: ( I commented out the line that did work to display records, but for both text fields instead of just the one)

$(document).ready(function() {
    $("input").keyup(function() {
        var input = $(this).val();
        $(".loader").show();
        if (input.length > 3) {
            $.ajax({
                type: 'POST',
                url: 'insert-ajax.php',
                data: {
                    name: input
                },
                success: function(data) {

                    if (!data.error) {
//                        	$(".result").html(data);
                        $(".result").closest("ul").html(data);
                        $(".loader").hide();
                    }
                }

            });
        }

        if (input.length < 1) {
            $(".loader").hide();
            $(".result").html("");
        }
    });

    $(".result").on("click", "li", function() {
        console.log($(this).text());
        $(this).closest(".result").siblings("input").val($(this).text());
    });

});

Open in new window

Comment
Watch Question

Most Valuable Expert 2011
Author of the Year 2014

Commented:
This may not help with the instant problem, but it's worth being aware of the technologies that are on hand today.

What you're asking about here has always been thorny in jQuery (a 12-year old library) and has been perfectly solved by AngularJS (a 5-year old library).  AngularJS gives you two way data bindings between the view and the model.  A change in either is reflected in the other.  So you don't have to deal with the problem of which selector goes with which input - you just bind a new input to the generated input control and bind a new output to the generated response area.  Instead of starting with HTML (which was never designed for dynamic views) and using JavaScript (jQuery) to manipulate named elements, with AngularJS you build the DOM in real time, in the client browser.  Typically server communication is with AJAX requests and JSON.  It's not easy to learn AngularJS, but it's very cool when you see it in action.
https://angularjs.org/
CERTIFIED EXPERT
Expert of the Year 2008
Top Expert 2008

Commented:
>> $(this).closest(".result").siblings("input").val($(this).text());
Based on the markup you supplied, try the following instead:
$('input',$(this).closest(".form-group")).val($(this).text());

Open in new window

Author

Commented:
@ hielo,

I tried that but no results appeared when typing. That part of the code was working as expected though, which was that when a li was clicked, that particular choice then displayed in the text field.

The code that is the issue I believe is

 $("ul").closest(".result").html(data);

Open in new window


This isn't doing anything. Before that, I had:

$(".result").html(data);

Open in new window


But that problem with that is the unordered list showed under both text fields instead of the one I am typing in. I have uploaded a screenshot where you can see I typed "test" in the top field but results are showing under both fields. screenshot

Author

Commented:
@ Ray,

I have seen the name "AngluarJS" around a lot lately but have been too intimidated to look at what it actually is, but your advice is usually always good so I think that since I haven't delved too much into jQuery, perhaps I should rather focus on learning AngularJS instead of jQuery? Or is this another situation where I should try to learn both?

Man, there is a lot to learn out there. This seems like it's going to take a lifetime :P
CERTIFIED EXPERT
Expert of the Year 2008
Top Expert 2008

Commented:
Are you are referring to line 16 of your original code? If so, try:
 $(".result", $(this).closest("form-group")) .html(data);

Open in new window

Author

Commented:
@ hielo,

Yes, that's the exact line. I tried your new code and still no search results/list items appear.
CERTIFIED EXPERT
Expert of the Year 2008
Top Expert 2008

Commented:
>>  I tried your new code and still no search results
I missed a period on my last post.  It should be ".form-group" (notice the period to the left of "form-group").

Author

Commented:
Ah, okay. I tried that and still nothing.

 $(".result", $(this).closest(".form-group")) .html(data);

Open in new window

CERTIFIED EXPERT
Expert of the Year 2008
Top Expert 2008

Commented:
Do you have a link to your page?

Author

Commented:
Hi,

Unfortunately not, it's running on localhost. Will it help you if I can get it on a live server?
Most Valuable Expert 2011
Author of the Year 2014
Commented:
This problem has been solved!
(Unlock this solution with a 7-day Free Trial)
UNLOCK SOLUTION
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
AngularJS is a framework
jQuery is a library

Important to understand the difference between the two. jQuery is fine for this setup.

Next lets look at some issues with your code
//$(document).ready(function() {
// I prefer $(function 
$(function() {
    $("input").keyup(function() {
        var input = $(this).val();
        $(".loader").show();
        if (input.length > 3) {
            $.ajax({
                type: 'POST',
                url: 'insert-ajax.php',
                data: {
                    name: input
                },
                success: function(data) {
// PROBLEM: THE if SAYS YOU ARE EXPECTING AN OBJECT BACK - SOMETHING LIKE
// {
//     error: 'Something here'
// }
// BUT ...
                    if (!data.error) {
//                        	$(".result").html(data);
// OVER HERE YOU ARE USING IT AS AN HTML RETURN
// ALSO .result IS GOING TO RETURN THE FIRST .result IT FINDS
// ON THE PAGE - NOT RELATIVE TO THE INPUT
                        $(".result").closest("ul").html(data);
                        $(".loader").hide();
                    }
                }

            });
        }

        if (input.length < 1) {
            $(".loader").hide();
            $(".result").html("");
        }
    });

    $(".result").on("click", "li", function() {
        console.log($(this).text());
        $(this).closest(".result").siblings("input").val($(this).text());
    });

});

Open in new window


Now lets look at a solution that works
<script>
$(function() {
  $("input").keyup(function() {
    // GET A REFERENCE TO THE <input> THAT TRIGGERED THE EVENT
    var jqo = $(this);
	
	// GET THE VALUE
    var input = jqo.val();
	
	// FIND THE result <div> FOR THIS <input>
    var result = jqo.closest('div').find('.result');

    if (input.length > 3) {
      // GET DATA FROM SERVER
      $.ajax({
        type: 'POST',
        url: 't2185.php',
        data: {
          name: input
        }
      })
      // .success IS ON ITS WAY OUT
      // I PREFER promise INTERFACE
      .done(function(data) {
        // data IS PLAIN HTML SO ADD IT TO OUR
        // result WINDOW
        result.html(data);
      })
    }
	// THE REST IS THE SAME
    if (input.length < 1) {
      result.html("");
    }
  });

  $(".result").on("click", "li", function() {
    console.log($(this).text());
    $(this).closest(".result").siblings("input").val($(this).text());
  });
});
</script>

Open in new window


Working sample here

Once you get that working we can look at adding the loader

Also note - your <input>'s have the same name attributes - you might need to change these of you need to use them on the server - in my code I have used arrays to get around the dynamic creation issue

Author

Commented:
Thanks Julian! I got that working now. I also added (hopefully the correct code) a line to remove the UL list once an option has been selected otherwise it still hangs around after making a selection.

$(this).closest(".result").hide()

Open in new window


My idea was that they all have the same name (text inputs) as I am using the class and not ID for the jQuery and then when I insert the records I will just use a foreach loop. So, "name" would have to be "name[]".

I did try play around with some code now for the loader. I can get it to show up but not in the right place. Ideally I would like it to show in the right hand side of the text field.
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
So, "name" would have to be "name[]"
Firstly, name <> name[] - the former will only come through in PHP $_POST as the last value submitted.
Consider this example
<form action="reflect.php" method="post">
<input name="name" value="fred" />
<input name="name" value="frank" />
<input name="name" value="frida" />
<input type="submit" />
</form>

Open in new window

Result
$_POST
Array
(
    [name] => frida
)

Open in new window

However if we do the follwoing
echo file_get_contents('php://input');

Open in new window

We get this
name=fred&name=frank&name=frida

Open in new window

PHP takes the last value for the $_POST['name']

name[] on the other hand
<form action="reflect.php" method="post">
<input name="name[]" value="fred" />
<input name="name[]" value="frank" />
<input name="name[]" value="frida" />
<input type="submit" />
</form>

Open in new window

Gives us
$_POST
Array
(
    [name] => Array
        (
            [0] => fred
            [1] => frank
            [2] => frida
        )
)

Open in new window


Secondly, if you look at the sample I posted you will see I used name[]

Author

Commented:
Hi Julian,

Sorry, maybe I didn't explain what I was trying to say properly but I was agreeing with you. I needed to change my input name from just name to name[] so I could use foreach when doing the insert otherwise it would only insert one record instead of all of them.
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
$(this).closest(".result").hide()

Open in new window

I would rather do this
<script>
$(function() {
  $("input").keyup(function() {
    // GET A REFERENCE TO THE <input> THAT TRIGGERED THE EVENT
    var jqo = $(this);
	
	// GET THE VALUE
    var input = jqo.val();
	
	// FIND THE result <div> FOR THIS <input>
    var result = jqo.closest('div').find('.result');
    if (input.length > 3) {
      // GET DATA FROM SERVER
      $.ajax({
        type: 'POST',
        url: 't2185.php',
        data: {
          name: input
        }
      })
      // .success IS ON ITS WAY OUT
	  // I PREFER promise INTERFACE
	  .done(function(data) {
	    // data IS PLAIN HTML SO ADD IT TO OUR
		// result WINDOW
        result.html(data);
        result.data('input', jqo);
      })
    }
	// THE REST IS THE SAME
    if (input.length < 1) {
      result.html("");
    }
  });
  $('.result').on('click','li', function() {
	var res = $(this).closest('.result');
	res.data('input').val(this.textContent);
	res.html('');
  });
});
</script>

Open in new window

Breaking it down
First up this line
result.data('input', jqo);

Open in new window

This is taking advantage of jQuery objects. I assign the input control for the .result container to a data attribute on the element. The jQuery data() method allows you to assign a scalar value (string, number) or an object. The jqo is the jQuery object we created at the beginning so we could access the input val.
Now in our event handler
  $('.result').on('click','li', function() {
	var res = $(this).closest('.result');
	res.data('input').val(this.textContent);
	res.html('');
  });

Open in new window

The input is available on the result container - so we save ourselves an extra find.
We can even take this one step further
  $('.result').on('click', function(evt) {
 	$(this).data('input').val(evt.target.textContent);
	this.textContent = '';
  });

Open in new window

Here we bind to the .result container - not the li. We know the children will be li's so we can use this code.
We use the event.target to get the content we want to send to the input - but because the event is firing on the .result container we don't have to use a .closest() to find the container to get the input data attribute and clear the contents.

I have updated the sample online with the above (using the last method)
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
Sorry, maybe I didn't explain what I was trying to say properly but I was agreeing with you.
No, I did understand - I just provided that post for background because there is often a misconception that when using a non array duplicate data is lost. It is only lost when accessing the $_POST array but you can still get the data using php://input - and then parse it yourself.

Author

Commented:
Ah, I see!
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
In case you missed it check the post before my last one for the solution on how to clear the <ul>

Author

Commented:
I did see that, thanks so much! :)

Should I post a related question if I get stuck with trying to show the loader?
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
Up to you - the loader depends on whether you want one per input or a global loader. The latter is dead easy you just hide and show it. The former is also easy but have to reference it relative to its container.

Author

Commented:
If you wouldn't mind, let's try add the loader in this question and I will ask a related question for the next part ;)

Ideally, I would like the loader to display in the text field I am typing in (right hand side of text field)

Not sure if I will be able to get it right, but I will give it a go.
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
What does the loader look like - can you post it.

Author

Commented:
Hi Julian,

Will do so this evening as soon as I am back in front of my development machine.

Author

Commented:
What does the loader look like - can you post it.

I have uploaded it for you to this post.
ajax-loader.gif

Author

Commented:
I tried it out myself and I got the loader to display for the correct text fields but it is showing under the text fields instead of in them. But I assume this is because I have the loader div underneath the text field div. I am going to try see if I can use CSS to position it but not sure if that would be the right way of positioning it.

I added this code:

 // FIND THE CLOSEST LOADER FOR THIS INPUT
	var loader = jqo.closest('div').find('.loader');
	  $(loader).show();

Open in new window


and then changed the HTML to:

		<div class="form-group">
			<input type="text" name="name" autocomplete="off">
			<div style="display:none" class="loader">
		        <img src="../build/css/ajax-loader.gif" />
			</div>
			<div class="result"></div>
		</div>

Open in new window

Author

Commented:
Hmm. CSS doesn't work because the loader is always in exactly the same place.

	.loader {
		position: absolute;
		top: 0%;
		left: 18.5%;
	}

Open in new window

CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
did you make .form-group position: relative?

Author

Commented:
I didn't. But I just tried that now (I think). Like this:

.loader {
		position: absolute;
		top: 0%;
		left: 18.5%;
	}
	
	.form-group {
		position: relative;
	}

Open in new window


This makes the positioning weird and not in the right place.
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
Explain to me again exactly how you want the loader to look
Do you have a screen shot?
Are you using an image / spinner (.gif) as part of the loader?
I can help you but I need to know what I am aiming at.

Author

Commented:
For sure. Sorry for not being more specific. I have done this in photoshop quickly.

The loader should show in this location when you type in that specific input. I have just pasted it into both so you can see where it should go in each one.

Yes, it is an animated gif and it spins round and round.
example-loader.jpg

Author

Commented:
This is what it currently does, lol.
wrong.jpg

Author

Commented:
If you check this comment and click on the gif, you will see it spinning.
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
Got it ... working on it.

Author

Commented:
Awesome, thanks!
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
Here is a first attempt
CSS
<style type="text/css">
.form-group {
	position: relative;
	width: 25%;
}
input {
	width: 100%;
	height: 30px;
}
.loader {
	display: none;
	position: absolute;
	right: 0;
	top: 0;
	height: 100%;
	background: rgba(0,0,0,0.15) url(images/loading2.gif) no-repeat right center;
	width: 100%;
}
</style>

Open in new window

HTML
<div class="form-group">
  <input type="text" name="name[]" autocomplete="off">
  <div class="loader"></div>
  <div class="result"></div>
</div>
<div class="form-group">
  <input type="text" name="name[]" autocomplete="off">
  <ul class="result"></ul>
</div>

Open in new window

jQuery
<script>
$(function() {
  $("input").keyup(function() {
    // GET A REFERENCE TO THE <input> THAT TRIGGERED THE EVENT
    var jqo = $(this);
  
    // GET THE VALUE
    var input = jqo.val();
  
    // FIND THE result <div> FOR THIS <input>
	var parent = jqo.closest('div');
    var result = parent.find('.result');
    if (input.length > 3) {
		var loader = parent.find('.loader').show();
      // GET DATA FROM SERVER
      $.ajax({
        type: 'POST',
        url: 't2185.php',
        data: {
          name: input
        }
      })
      // .success IS ON ITS WAY OUT
      // I PREFER promise INTERFACE
      .done(function(data) {
        // data IS PLAIN HTML SO ADD IT TO OUR
        // result WINDOW
        result.html(data);
        result.data('input', jqo);
		loader.hide();
      })
    }
    // THE REST IS THE SAME
    if (input.length < 1) {
      result.html("");
    }
  });
  
  $('.result').on('click', function(evt) {
    $(this).data('input').val(evt.target.textContent);
    this.textContent = '';
  });
});
</script>

Open in new window


Updated sample here

The jQuery changes are arbitrary - the main changes are the styling to the HTML to position the loader. You can play around with the styling. There are different options depending on the look you want to achieve.

I have added a sleep to the PHP script to simulate a server delay so you can see the spinner.
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019
Commented:
This problem has been solved!
(Unlock this solution with a 7-day Free Trial)
UNLOCK SOLUTION

Author

Commented:
This is pretty darn awesome. There is only one problem I can see at this point though. The inputs are going to be generated by php meaning that they are going to be identical. The HTML you changed shows that the first input field is different to the second, third, fourth or however many are generated. They all need to be exactly the same as in my particular use case.

For example, if the simple equation calculated from a user input is 3, then 3 inputs will be echoed. If the result is 4, 4 text fields will be echoed etc.

<div class="form-group">
	<input type="text" name="name[]" autocomplete="off">
	<div class="loader"></div>
	<div class="result"></div>
</div>
<div class="form-group">
	<input type="text" name="name[]" autocomplete="off">
	<ul class="result"></ul>
</div>

Open in new window


Only the first input here has the loader div. Where if PHP was echoing it out, both would have it like this:

<div class="form-group">
	<input type="text" name="name[]" autocomplete="off">
	<div class="loader"></div>
	<div class="result"></div>
</div>
<div class="form-group">
	<input type="text" name="name[]" autocomplete="off">
	<div class="loader"></div>
	<div class="result"></div>
</div>

Open in new window

CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
That was a mistake - the HTML should be
<div class="form-group">
  <input type="text" name="name[]" autocomplete="off">
  <ul class="result"></ul>
</div>
<div class="form-group">
  <input type="text" name="name[]" autocomplete="off">
  <ul class="result"></ul>
</div>

Open in new window

Note the latest sample does not have a .loader DIV because we are styling the <input> directly.

If you look at the sample link I posted above and go to the HTML tab of the source window you will see the HTML for the blocks is identical

Author

Commented:
Ah.

So, I changed it a little because I didn't want the text field to be disabled. It is working pretty much exactly how I had imagined it in my mind which is great, thanks so much! I just wanted to ask you about something though if you don't mind. Firstly, what do you mean by this:

.success IS ON ITS WAY OUT

And secondly,

why does this remove the loader and the text field

jqo.removeClass('loader').hide();

Open in new window


but this just removes the loader:

jqo.removeClass('loader').prop({disabled: false});

Open in new window


I thought the disabled: false bit just meant that you were able to type in the text field again as disabled:true is what disabled the text field.
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
jqo refers to the <input> so when you say
jqo.removeClass('loader')

Open in new window

- it removes the class that adds the spinner to the <input>
when you say
jqo.removeClass('loader').hide()

Open in new window

you are saying hide the <input>.
Remember the new solution uses CSS to style the <input> with the loader overlay - there is no element that represents the loader so there is nothing to hide - all we are doing is adding / removing a class that adds / removes styling to the <input>

The disabled bit I added on to prevent users from typing in the box while a lookup was being done.

.success() is being deprecated in favour of the promise return
Refer to this link http://api.jquery.com/jQuery.ajax/#jqXHR
Deprecation Notice: The jqXHR.success(), jqXHR.error(), and jqXHR.complete() callbacks are removed as of jQuery 3.0. You can use jqXHR.done(), jqXHR.fail(), and jqXHR.always() instead.

To summarise:

The solution above uses CSS to style the <input> to give the appearance of having a loader overlaid on top of it when in reality all we are doing is adding a class that has a background-image positioned right.

Author

Commented:
Thanks for the explanation, it helps a lot. Well, this is working perfectly now. I just want to style the unordered list and then it's on to the next part! Thanks a million, this was really giving me a headache.
CERTIFIED EXPERT
Most Valuable Expert 2017
Distinguished Expert 2019

Commented:
You are welcome.

Gain unlimited access to on-demand training courses with an Experts Exchange subscription.

Get Access
Why Experts Exchange?

Experts Exchange always has the answer, or at the least points me in the correct direction! It is like having another employee that is extremely experienced.

Jim Murphy
Programmer at Smart IT Solutions

When asked, what has been your best career decision?

Deciding to stick with EE.

Mohamed Asif
Technical Department Head

Being involved with EE helped me to grow personally and professionally.

Carl Webster
CTP, Sr Infrastructure Consultant
Empower Your Career
Did You Know?

We've partnered with two important charities to provide clean water and computer science education to those who need it most. READ MORE

Ask ANY Question

Connect with Certified Experts to gain insight and support on specific technology challenges including:

  • Troubleshooting
  • Research
  • Professional Opinions