Link to home
Start Free TrialLog in
Avatar of Crazy Horse
Crazy HorseFlag for South Africa

asked on

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

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

Avatar of Ray Paseur
Ray Paseur
Flag of United States of America image

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/
Avatar of hielo
>> $(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

Avatar of Crazy Horse

ASKER

@ 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. User generated image
@ 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
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

@ hielo,

Yes, that's the exact line. I tried your new code and still no search results/list items appear.
>>  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").
Ah, okay. I tried that and still nothing.

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

Open in new window

Do you have a link to your page?
Hi,

Unfortunately not, it's running on localhost. Will it help you if I can get it on a live server?
SOLUTION
Avatar of Ray Paseur
Ray Paseur
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
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
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.
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[]
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.
$(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)
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.
Ah, I see!
In case you missed it check the post before my last one for the solution on how to clear the <ul>
I did see that, thanks so much! :)

Should I post a related question if I get stuck with trying to show the loader?
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.
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.
What does the loader look like - can you post it.
Hi Julian,

Will do so this evening as soon as I am back in front of my development machine.
What does the loader look like - can you post it.

I have uploaded it for you to this post.
ajax-loader.gif
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

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

did you make .form-group position: relative?
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.
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.
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
This is what it currently does, lol.
wrong.jpg
If you check this comment and click on the gif, you will see it spinning.
Got it ... working on it.
Awesome, thanks!
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.
ASKER CERTIFIED SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
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

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
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.
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.
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.
You are welcome.