Skip to content

Add an instant search (via AJAX) #173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/Resources/translations/messages.en.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@
<source>post.deleted_successfully</source>
<target>Post deleted successfully!</target>
</trans-unit>
<trans-unit id="post.search_for">
<source>post.search_for</source>
<target>Search for...</target>
</trans-unit>

<trans-unit id="notification.comment_created">
<source>notification.comment_created</source>
Expand Down
4 changes: 4 additions & 0 deletions app/Resources/translations/messages.ru.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@
<source>post.deleted_successfully</source>
<target>Запись успешно удалена!</target>
</trans-unit>
<trans-unit id="post.search_for">
<source>post.search_for</source>
<target>Искать запись...</target>
</trans-unit>

<trans-unit id="notification.comment_created">
<source>notification.comment_created</source>
Expand Down
18 changes: 17 additions & 1 deletion app/Resources/views/base.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<header>
<div class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<div class="navbar-header col-md-3 col-lg-2">
<a class="navbar-brand" href="{{ path('homepage') }}">
Symfony Demo
</a>
Expand All @@ -37,6 +37,13 @@
<span class="icon-bar"></span>
</button>
</div>
<div class="search-bar col-sm-5">
<form action="{{ path('blog_search') }}" method="get">
<div class="input-group-sm">
<input name="q" type="text" class="form-control" placeholder="{{ 'post.search_for'|trans }}" autocomplete="off">
</div>
</form>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">

Expand Down Expand Up @@ -135,6 +142,15 @@
<script src="{{ asset('build/manifest.js') }}"></script>
<script src="{{ asset('build/js/common.js') }}"></script>
<script src="{{ asset('build/js/app.js') }}"></script>
<script>
(function($) {
$(function() {
$('.search-bar input[name="q"]').instantSearch({
noItemsFoundMessage: '{{ 'post.no_posts_found'|trans }}'
});
});
})(window.jQuery);
</script>
{% endblock %}

{# it's not mandatory to set the timezone in localizeddate(). This is done to
Expand Down
6 changes: 6 additions & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// Exposes jQuery as a global variable
global.$ = global.jQuery = require('jquery');

// loads the Bootstrap jQuery plugins
import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown.js';
import 'bootstrap-sass/assets/javascripts/bootstrap/modal.js';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition.js';

// loads the code syntax highlighting library
import './highlight.js';

// loads the instant search library
import './jquery.instantSearch.js';
100 changes: 100 additions & 0 deletions assets/js/jquery.instantSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* jQuery plugin for an instant searching.
*
* @author Oleg Voronkovich <[email protected]>
*/
(function($) {
$.fn.instantSearch = function(config) {
return this.each(function() {
initInstantSearch(this, $.extend(true, defaultConfig, config || {}));
});
};

var defaultConfig = {
minQueryLength: 2,
maxPreviewItems: 10,
previewDelay: 500,
noItemsFoundMessage: 'No items found'
};

function debounce(fn, delay) {
var timer = null;
return function () {
var context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}

var initInstantSearch = function(el, config) {
var $input = $(el);
var $form = $input.closest('form');
var $preview = $('<ul class="search-preview list-group">').appendTo($form);

var setPreviewItems = function(items) {
$preview.empty();

$.each(items, function(index, item) {
if (index > config.maxPreviewItems) {
return;
}

addItemToPreview(item);
});
}

var addItemToPreview = function(item) {
var $link = $('<a>').attr('href', item.url).text(item.result);
var $li = $('<li class="list-group-item">').append($link);

$preview.append($li);
}

var noItemsFound = function() {
var $li = $('<li class="list-group-item">').text(config.noItemsFoundMessage);

$preview.empty();
$preview.append($li);
}

var updatePreview = function() {
var query = $.trim($input.val()).replace(/\s{2,}/g, ' ');

if (query.length < config.minQueryLength) {
$preview.empty();
return;
}

$.getJSON($form.attr('action') + '?' + $form.serialize(), function(items) {
if (items.length === 0) {
noItemsFound();
return;
}

setPreviewItems(items);
});
}

$input.focusout(function(e) {
$preview.fadeOut();
});

$input.focusin(function(e) {
$preview.fadeIn();
updatePreview();
});

$input.keyup(debounce(updatePreview, config.previewDelay));
}
})(window.jQuery);
14 changes: 14 additions & 0 deletions assets/scss/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ header .locales a {
padding-right: 10px
}

header .search-bar {
padding: 0.8em 0;
}

header .search-bar form {
position: relative;
}

header .search-preview {
position: absolute;
width: 100%;
top: 100%;
}

.body-container {
flex: 1;
/* needed to prevent pages with a very small height and browsers not supporting flex */
Expand Down
39 changes: 39 additions & 0 deletions src/AppBundle/Controller/BlogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

Expand Down Expand Up @@ -150,4 +151,42 @@ public function commentFormAction(Post $post)
'form' => $form->createView(),
]);
}

/**
* @Route("/search", name="blog_search")
* @Method("GET")
*
* @return JsonResponse
*/
public function searchAction(Request $request)
{
$query = $request->query->get('q', '');

// Sanitizing the query: removes all non-alphanumeric characters except whitespaces
$query = preg_replace('/[^[:alnum:] ]/', '', trim(preg_replace('/[[:space:]]+/', ' ', $query)));

// Splits the query into terms and removes all terms which
// length is less than 2
$terms = array_unique(explode(' ', mb_strtolower($query)));
$terms = array_filter($terms, function ($term) {
return 2 <= mb_strlen($term);
});

$posts = [];

if (!empty($terms)) {
$posts = $this->getDoctrine()->getRepository(Post::class)->findByTerms($terms);
}

$results = [];

foreach ($posts as $post) {
array_push($results, [
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just $results[] = xxx ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Koc, I don't know :) Anyway, this code was changed in #613.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We finally used $results[] = xxx ... mostly because PHPStorm displayed a pretty annoying message suggesting that 😄

'result' => htmlspecialchars($post->getTitle()),
'url' => $this->generateUrl('blog_post', ['slug' => $post->getSlug()]),
]);
}

return new JsonResponse($results);
}
}
18 changes: 18 additions & 0 deletions src/AppBundle/Repository/PostRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,22 @@ private function createPaginator(Query $query, $page)

return $paginator;
}

public function findByTerms(array $terms, $limit = Post::NUM_ITEMS)
{
$queryBuilder = $this->createQueryBuilder('p');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about to shorten it with $qb?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its really a bad practice to get variable names under 4 characters so IMO that's definitely not something that should be encouraged in this project

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symfony Best Practices don't mention about short variable's names, so I prefer to base on Symfony docs. If it'll be changed - so we could change it in Symfony demo too

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a Symfony best practice, it's a PHP one. Having $em as a variable is more a legacy from Doctrine than a Symfony convention.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@theofidry, @bocharsky-bw I think it's better to create a particular issue to discuss code style for this project. What do you think?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I think now we should following styles already used in this project.


foreach ($terms as $key => $term) {
$queryBuilder
->orWhere('p.title LIKE :t_'.$key)
->setParameter('t_'.$key, '%'.$term.'%')
;
}

return $queryBuilder
->orderBy('p.publishedAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
}
2 changes: 1 addition & 1 deletion web/build/css/app.css

Large diffs are not rendered by default.

18 changes: 12 additions & 6 deletions web/build/js/app.js

Large diffs are not rendered by default.