Demo jQuery Autocomplete Suitelet

jQuery autocomplete is a powerful tool that can be overlooked or underutilized. Here is an example that demonstrates not only how jQuery autocomplete works, but some of its hidden potential.

In this example I demonstrate applying the contents of multiple HTML controls to filter results of a single textbox’s autocomplete results. Here is what it looks like in action.

My data is articles of clothing. A garment’s category only includes ‘tops’ and ‘bottoms’. It’s pretty sparse for an autocomplete example, but you get the idea.

My example includes the following scripts: Suitelet, Restlet, Client, and an HTML file. The JSON file takes the place of a database. I created my JASONized data using vsCode’s Copilot. That’s a handy tip!

Here is the HTML file’s contents.

<label for="filters">Additional Filters</label><br />
<div id="filters" style="border: solid 2px blue; background-color: lightblue; padding: 10px; width: 40%;">
    <table>
        <tr>
            <td><label for="garment_type">Garment Type:</label></td>
            <td><label for="garment_color">Garment Color:</label></td>
            <td><label for="garment_size">Garment Size:</label></td>
            <td><label for="garment_manufacturer">Garment Manufacturer:</label></td>
            <td><label id="hover_sku_heading" style="display: none;">SKU:</label></td>
        </tr>
        <tr>
            <td><select id="garment_type" name="garment_type">
                    <option value="">-- Select Type --</option>
                    <option value="shirt">Shirt</option>
                    <option value="tank">Tank</option>
                    <option value="sweater">Sweater</option>
                    <option value="pants">Pants</option>
                    <option value="shorts">Shorts</option>
                </select></td>
            <td><select id="garment_color" name="garment_color">
                    <option value="">-- Select Color --</option>
                    <option value="blue">Blue</option>
                    <option value="gray">Gray</option>
                    <option value="black">Black</option>
                    <option value="kakhi">Kakhi</option>
                </select></td>
            <td><select id="garment_size" name="garment_size">
                    <option value="">-- Select Size --</option>
                    <option value="Small">Small</option>
                    <option value="Medium">Medium</option>
                    <option value="Large">Large</option>
                    <option value="XL">XL</option>
                    <option value="30x32">30x32</option>
                    <option value="32x32">32x32</option>
                    <option value="34x32">34x32</option>
                    <option value="36x32">36x32</option>
                    <option value="34x30">34x30</option>
                    <option value="36x34">36x34</option>
                </select>
            </td>
            <td><select id="garment_manufacturer" name="garment_manufacturer">
                    <option value="">-- Select Manufacturer --</option>
                    <option value="LL Bean">LL Bean</option>
                    <option value="Polo">Polo</option>
                    <option value="Ralph Loren">Ralph Loren</option>
                    <option value="Levi's">Levi's</option>
                    <option value="Wrangler">Wrangler</option>
                    <option value="Fruit of the Loom">Fruit of the Loom</option>
                </select>
            </td>
            <td></td>
        </tr>
        <tr>
            <td id="hover_type"></td>
            <td id="hover_color"></td>
            <td id="hover_size"></td>
            <td id="hover_manufacturer"></td>
            <td id="hover_sku"></td>
        </tr>
    </table>
</div><br />
<br />
<label for="garment_id">Unique ID (can be a hidden field):</label><br />
<input type="text" id="garment_id" name="garment_id" placeholder="SKU" disabled /><br />
<br />
<label for="garment_category">Garment Category (with autocomplete):</label><br />
<input type="text" id="garment_category" name="garment_category" placeholder="Type either 'top' or 'bottom'" /><br />
<br />
<p>
    This demonstrates the use of the <strong>jQuery autocomplete</strong> feature in a NetSuite Suitelet.
    Key takeaways include:
    <ul>
        <li>Incorporates additional filters beyond a textbox control (Type, Color, Size, Manufacturer)</li>
        <li>Displays multiple Meta Data in the autocomplete list beyond values typed in the textbox (separated by "|").</li>
        <li>Returns a unique key associated with the selected entry (The garment's SKU).</li>
        <li>Exposes extensive Meta Data as the user hovers over possible selections.</li>
    </ul>
</p>

Here is the client script’s contents.

/**
 * demo_client_autocomplete.js
* @NApiVersion 2.1
* @NScriptType ClientScript
*/

/*********************************************************************************
 *********************************************************************************
 FILE: demo_client_autocomplete.js
 *********************************************************************************
 *********************************************************************************/

//@ sourceURL=demo_client_autocomplete.js
define(['N/url', 'N/https'],
    (url, https) => {
        function pageInit(context) {
            $(document).ready(function () {

                let garment_id_field = $('input[name="garment_id"]');
                let garment_category_field = $('input[name="garment_category"]');

                restlet_base_url = url.resolveScript({
                    scriptId: 'customscript_demo_restlet_autocomplete',
                    deploymentId: 'customdeploy_demo_restlet_autocomplete',
                    returnExternalUrl: false
                });

                /**
                 * What's going on here is making functions available in the global scope.
                 * You can see that it also keeps them unique by exposing them inside a
                 * JavaScript namespace called demo_autocomplete_client.
                 */
                demo_autocomplete_client.restletGet = restletGet;
                demo_autocomplete_client.restletPost = restletPost;

                /**
                 * You turn on autocomplete via the autocomplete method associated with
                 * the input[type=text] control.
                 */
                garment_category_field.autocomplete({
                    source: function (request, response) {
                        let type = $('select[name="garment_type"]').val();
                        let color = $('select[name="garment_color"]').val();
                        let size = $('select[name="garment_size"]').val();
                        let manufacturer = $('select[name="garment_manufacturer"]').val();
                        /**
                         * In addition to typed text, you can send additional parameters to act as
                         * filters in the autocomplete responses.
                         */
                        restletGet(
                            'method=autocomplete' +
                            '&category=' + request.term + // <<<=== request term contains the typed text
                            '&type=' + type +
                            '&color=' + color +
                            '&size=' + size +
                            '&manufacturer=' + manufacturer
                            ,
                            function (data) {
                                response(JSON.parse(data));
                            }
                        );
                    },
                    minLength: 3,
                    /**
                     * This event fires on selection of one of the rows in the autocomplete list.
                     * Note the ui.item contains all properties you wish to send back.
                     * 
                     * Properties that must be returned are Label and Value.
                     * Label: is what displays in the selectable list.
                     * Value: would typically represent an associated value.
                     */
                    select: function (event, ui) {
                        event.preventDefault();
                        garment_id_field.val(ui.item.value);
                        garment_category_field.val(ui.item.name);
                    },
                    /**
                     * This event fires whenever the user hovers over a possble selection.
                     */
                    focus: function (event, ui) {
                        $('#hover_size').text(ui.item.size);
                        $('#hover_color').text(ui.item.color);
                        $('#hover_type').text(ui.item.type);
                        $('#hover_manufacturer').text(ui.item.manufacturer);
                        $('#hover_sku_heading').show();
                        $('#hover_sku').text(ui.item.sku);
                        event.preventDefault();
                    },
                    close: function (event, ui) {
                        event.preventDefault();
                    },
                    change: function (event, ui) {
                        event.preventDefault();
                    }
                });

                $('input[name="garment_category"]').focus(
                    function () {
                        $('input[name="garment_category"]').val('');
                        $('input[name="garment_id"]').val('');
                    }
                )
            });
        }

        let restlet_base_url;

        function restletGet(parameters, callback) {
            return https.get.promise({
                url: restlet_base_url + '&' + parameters
            })
                .then(response => {
                    callback(response.body);
                })
                .catch(
                    reason => {
                        alert('error reason: ' + reason);
                    }
                )
        }

        function restletPost(parameters, callback) {
            return https.post.promise({
                url: restlet_base_url,
                body: parameters
            })
                .then(response => {
                    callback(response.body);
                })
                .catch(
                    reason => {
                        alert('Error: ' + reason);
                        callback('fail');
                    }
                )
        }

        return {
            pageInit: pageInit
        };
    }
);
var demo_autocomplete_client = {
    restletGet: () => { return false; }
    ,
    restletPost: () => { return false; }   
}

And finally, here is the Restlet’s contents.

/**
 * @NApiVersion 2.1
 * @NScriptType restlet
 */
define(['N/file'],
    (file) => {

        function GET(context) {
            if (context.hasOwnProperty('method')) {
                let method = context.method;
                switch (method.toLowerCase().trim()) {
                    case 'autocomplete':
                        if (context.hasOwnProperty('category')) {
                            return auto_complete(context.category, context.type, context.color, context.size, context.manufacturer);
                        }
                        break;
                }
            }
            return 'Method not found';
        }

        function auto_complete(garment_category, garment_type, garment_color, garment_size, garment_manufacturer) {
            let sample_data = file.load({
                id: './demo_suitelet_autocomplete.json'
            }).getContents();
            let sample_data_json = JSON.parse(sample_data);

            return JSON.stringify(
                sample_data_json.filter(
                    (item) => {
                        return item.category.toLowerCase().indexOf(garment_category.toLowerCase()) !== -1;
                    }
                )
                    .filter(
                        (item) => {
                            return !garment_type || (garment_type == item.type);
                        }
                    )
                    .filter(
                        (item) => {
                            return !garment_color || (garment_color == item.color);
                        }
                    )
                    .filter(
                        (item) => {
                            return !garment_size || (garment_size == item.size);
                        }
                    )
                    .filter(
                        (item) => {
                            return !garment_manufacturer || (garment_manufacturer == item.manufacturer);
                        }
                    )
                    .map(
                        (item) => {
                            let label = `${item.category} | ${item.name}`;
                            return {
                                label: label,
                                value: item.sku,
                                name: item.name,
                                color: item.color,
                                size: item.size,
                                type: item.type,
                                manufacturer: item.manufacturer,
                                sku: item.sku
                            }
                        }
                    )
                    .sort(
                        (a, b) => {
                            return a.label.localeCompare(b.label);
                        }
                    )
                    .slice(0, 10) // limit to 10 results
            );

        }

        return {
            get: GET
        }
    }
)

I’ve left out 2 of the five files. This post was getting long and those should be relatively straightforward.

Cheers!