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

LVL 1
Black SulfurAsked:
Who is Participating?

[Product update] Infrastructure Analysis Tool is now available with Business Accounts.Learn More

x
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

Ray PaseurCommented:
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/
0
hieloCommented:
>> $(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

0
Black SulfurAuthor 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
0
JavaScript Best Practices

Save hours in development time and avoid common mistakes by learning the best practices to use for JavaScript.

Black SulfurAuthor 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
0
hieloCommented:
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

0
Black SulfurAuthor Commented:
@ hielo,

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

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

Open in new window

0
hieloCommented:
Do you have a link to your page?
0
Black SulfurAuthor Commented:
Hi,

Unfortunately not, it's running on localhost. Will it help you if I can get it on a live server?
0
Ray PaseurCommented:
perhaps I should rather focus on...
Learn JavaScript to the point that you know it blindfolded.  Then the frameworks will all make sense.  There are two JavaScript books I would recommend, and if anyone else has written a book and they want my opinion, I'll read it and if it's in any way better than these two, I'll recommend it, too.  But for now, these.
https://www.amazon.com/JavaScript-Definitive-Guide-Activate-Guides/dp/0596805527
https://www.amazon.com/JavaScript-Good-Parts-Douglas-Crockford/dp/0596517742

Will it help you if I can get it on a live server?
Yes, yes, a thousand times yes!  That is one of the wonderful things about JavaScript -- it runs in the browser and the source code is visible, so if you put it on a live server, we can all see the code, copy the code, test in our own browsers, watch the console, clone-hack-repeat, etc.

Many of my colleagues have dev environments that run on a single desktop. Their Mac (usually Mac) is both the client and the server, and they use something like MAMP.  I've done that, and it works well, in isolation.  But when you want to teach or write or share ideas, there is nothing that is more valuable than a public-facing server that exposes a URL and/or your APIs.  To that end, I have a demonstration site online all the time.  You can get something like this from various "fiddle" sites, but I like the freedom to be able to publish both client and server-side scripts in the same environment.  All it takes is a hosting account and a copy of WinSCP.  And Git, especially if you collaborate on projects.

To the question of jQuery vs AngularJS, stick with jQuery for now.  You're already on that path.  But take a moment, maybe a weekend, to vacation from jQuery and just try some of the "hello world" stuff in AngularJS.  It's really remarkable.  I think of jQuery as better than struggling with cross-browser JavaScript, but still a spaghetti-mess of untestable code.  AngularJS is built with object-oriented principles in mind and AngularJS gives you the ability to do formal testing.  When I started with AngularJS there was almost no documentation and little in the way of online learning resources, and it was painful.  But that is all behind us now and anyone with some time and concentration can find the good parts of AngularJS and exploit them for fun and profit.
1
Julian HansenCommented:
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
1
Black SulfurAuthor 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.
0
Julian HansenCommented:
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[]
0
Black SulfurAuthor 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.
0
Julian HansenCommented:
$(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)
0
Julian HansenCommented:
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.
0
Black SulfurAuthor Commented:
Ah, I see!
0
Julian HansenCommented:
In case you missed it check the post before my last one for the solution on how to clear the <ul>
0
Black SulfurAuthor Commented:
I did see that, thanks so much! :)

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

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

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

0
Black SulfurAuthor 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

0
Julian HansenCommented:
did you make .form-group position: relative?
0
Black SulfurAuthor 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.
0
Julian HansenCommented:
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.
0
Black SulfurAuthor 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
0
Black SulfurAuthor Commented:
This is what it currently does, lol.
wrong.jpg
0
Black SulfurAuthor Commented:
If you check this comment and click on the gif, you will see it spinning.
0
Julian HansenCommented:
Got it ... working on it.
0
Black SulfurAuthor Commented:
Awesome, thanks!
0
Julian HansenCommented:
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.
0
Julian HansenCommented:
Here is a much better attempt - I got distracted by a loader <div> when a much simpler solution is to just add a class to the input that adds the loader.
jQuery
CSS
<style type="text/css">
.form-group {
	position: relative;
	width: 25%;
}
input {
	width: 100%;
	height: 30px;
}
input.loader {
	background: rgba(0,0,0,0.15) url(images/loading2.gif) no-repeat right center;
}
</style>

Open in new window

HTML unchanged
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) {
      jqo.addClass('loader').prop({disabled: true});
      // 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);
		jqo.removeClass('loader').prop({disabled: false});
      })
    }
    // 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

With this approach we simply add / remove a class to the <input> which in turn puts a background on the input (we also disable the input).

This has the advantage that you don't have to rely on the input being the same dimensions as the container - the background will position relative to the input itself.
0

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
Black SulfurAuthor 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

0
Julian HansenCommented:
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
0
Black SulfurAuthor 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.
0
Julian HansenCommented:
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.
0
Black SulfurAuthor 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.
0
Julian HansenCommented:
You are welcome.
0
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
PHP

From novice to tech pro — start learning today.