Refactoring Autocomplete JS & CSS
We still have work to do to get the new.html.twig
template working:
// ... lines 1 - 2 | |
{% block javascripts %} | |
// ... lines 4 - 5 | |
<script src="https://cdn.jsdelivr.net/autocomplete.js/0/autocomplete.jquery.min.js"></script> | |
<script src="{{ asset('js/algolia-autocomplete.js') }}"></script> | |
// ... line 8 | |
{% endblock %} | |
// ... lines 10 - 25 |
we have a script tag for this external autocomplete library and one for our own public/js/algolia-autocomplete.js
file... which is our last JavaScript file in the public/
directory! Woo!
$(document).ready(function() { | |
$('.js-user-autocomplete').each(function() { | |
var autocompleteUrl = $(this).data('autocomplete-url'); | |
$(this).autocomplete({hint: false}, [ | |
{ | |
source: function(query, cb) { | |
$.ajax({ | |
url: autocompleteUrl+'?query='+query | |
}).then(function(data) { | |
cb(data.users); | |
}); | |
}, | |
displayKey: 'email', | |
debounce: 500 // only request every 1/2 second | |
} | |
]) | |
}); | |
}); |
This holds code that adds auto-completion... on this author box... which, yes, is totally broken.
Installing the Autocomplete Library
To start, remove the CDN link to this autocomplete library:
// ... lines 1 - 2 | |
{% block javascripts %} | |
// ... lines 4 - 5 | |
<script src="https://cdn.jsdelivr.net/autocomplete.js/0/autocomplete.jquery.min.js"></script> | |
// ... lines 7 - 8 | |
{% endblock %} | |
// ... lines 10 - 25 |
And, at your terminal, install it properly!
yarn add autocomplete.js --dev
Organizing our Autocomplete into a Component
Next, you know the drill, take the algolia-autocomplete.js
file and move it into the assets/js/
directory. But I'm not going to make this a new entry point. We could do that, but really, we already have an entry file that's included on this page: admin_article_form
:
// ... lines 1 - 2 | |
{% block javascripts %} | |
// ... lines 4 - 7 | |
{{ encore_entry_script_tags('admin_article_form') }} | |
{% endblock %} | |
// ... lines 10 - 25 |
So really, admin_article_form.js
should probably just use the code from algolia-autocomplete.js
.
So, move that file into the components/ directory... which is kind of meant for reusable modules. And... well, this isn't really written like a re-usable module yet because it just executes code instead or returning something, like a function. But, we'll work on that later.
Let's also take the algolia-autocomplete.css
file and move that all the way up here into assets/css/
. And just because we can, I'll make it an SCSS file!
Okay! Back in admin_article_form.js
, let's bring in this code: import './components/algolia-autocomplete'
:
// ... lines 1 - 4 | |
import './components/algolia-autocomplete'; | |
// ... lines 6 - 157 |
We don't need an import from
yet... because that file doesn't actually export anything. For the CSS: import '../css/algolia-autocomplete.scss'
:
// ... lines 1 - 4 | |
import './components/algolia-autocomplete'; | |
import '../css/algolia-autocomplete.scss'; | |
// ... lines 7 - 157 |
Back in new.html.twig
, the great thing is, we don't need to import this CSS file anymore or any of these script files. This is really how we want our templates to look: a single a call to {{ encore_entry_script_tags() }}
and a single call to {{ encore_entry_link_tags() }}
:
// ... lines 1 - 2 | |
{% block javascripts %} | |
{{ parent() }} | |
{{ encore_entry_script_tags('admin_article_form') }} | |
{% endblock %} | |
{% block stylesheets %} | |
{{ parent() }} | |
{{ encore_entry_link_tags('admin_article_form') }} | |
{% endblock %} | |
// ... lines 14 - 22 |
So if we refresh right now, not surprisingly, it still won't work! And it's our favorite error!
$ is undefined
from algolia-autocomplete.js
. Yes, this is the error I see when I close my eyes at night.
Using the autocomplete.js Library
Let's get to work. Of course, we are referencing $
. So, import $ from 'jquery'
:
import $ from 'jquery'; | |
// ... lines 2 - 23 |
We're also using the autocomplete library in here. No problem: import autocomplete from 'autocomplete.js'
:
import $ from 'jquery'; | |
import autocomplete from 'autocomplete.js'; | |
// ... lines 3 - 23 |
Wait... that's not quite right. This autocomplete.js
library is a standalone JavaScript library that can be used with anything - jQuery, React, whatever. But... our existing code isn't using the "standalone" version of the library. It's using a jQuery plugin - this .autocomplete()
function - that comes with that package:
// ... lines 1 - 3 | |
$(document).ready(function() { | |
$('.js-user-autocomplete').each(function() { | |
// ... lines 6 - 7 | |
$(this).autocomplete({hint: false}, [ | |
// ... lines 9 - 19 | |
]) | |
}); | |
}); |
So, we could refactor our code down here to use the, kind of, official way of using this library - independent of jQuery. But... that's the easy way out! Let's see if we can get this to work as a jQuery plugin.
Finding and Using the jQuery Plugin
I'll hold Command
or Control
and click into autocomplete.js
. Then double-click the directory to zoom us there. The "main" file is this index.js
at the root of the directory. But if you look in dist/
, hey! autocomplete.jquery.js
! That's what we were including before via the <script>
tag!
So instead of importing the main file, let's import autocomplete.js/dist/autocomplete.jquery
:
import $ from 'jquery'; | |
import 'autocomplete.js/dist/autocomplete.jquery'; | |
// ... lines 3 - 23 |
And remember, we don't use import from with jQuery plugins... because they don't return anything: they modify the jQuery object.
Ok, I think we're great and I think we're ready. Move over, refresh and... huh:
jQuery is not defined
Notice it doesn't say "$ is not defined": it says "jQuery is not defined"... and it's coming from autocomplete.jquery.js
! It's coming from the third party package!
This... is tricky. Plain and simple, that file is written incorrectly. Yea, it only works if jQuery is a global variable! And in Webpack... it's not! Let's talk more about this and fix it with some black magic, next.
I have a question regarding JS/jQuery plugins written correctly (like bootstrap) and "badly" (like autocomplete.js): I checked out the bootstrap source code and found out (as I understood) that bootstrap uses a <strong>getjQuery()</strong> util function to get jQuery and attach plugin functionality to it:
But here you clearly see that <strong>getjQuery</strong> only checks for jQuery to be available on window...
How does then the final transpiled/built bootstrap (the one we import in our code using <strong>import 'bootstrap'</strong>, i.e. "dist/js/bootstrap.js")
know that it has to attach the jQuery plugin to the "local" jQuery peer dependency we installed with <strong>yarn add jquery --dev</strong> if the bootstrap's source only uses jQuery through the getjQuery() util function and therefore through window? Hope I correctly formulated my question.
I mean, I have written a jQuery plugin called jquery-multiselect-checkbox (you can check it out here: https://github.com/tonix-tuft/jquery-multiselect-checkbox/), and there in my index.js source file I explicitly do <strong>import $ from 'jquery'</strong> (which I marked as a peer dep in package.json) and I also check for window.jQuery and attach my plugin to both if available (if the imported $ is different from window.$, i.e. <strong>$ !== window.jQuery</strong>). But maybe this is not the way to do it when authoring libraries. I don't know...