UTILITY

U-TYPE-AHEAD

Demo Section

Each variation will be presented in the following sections.

Default

    Technical Details

    Bower versionGitter Chat

    TypeAhead

    This utility offers a type-ahead function that can be applied to input elements.

    It generates a list with keywords fetched from a server.


    Requirements

    • Veams >= v5.0.0 - Veams Framework.

    Installation

    Installation with Veams

    veams install vu type-ahead

    Installation with Bower

    bower install veams-utility-type-ahead --save


    Fields

    u-type-ahead.hbs

    Settings

    • settings.typeAheadClasses {String} - Modifier classes.
    • settings.typeAheadJsOptions {Object} - Options object which gets stringified.
    • settings.height {Number} - Height of suggestion list.

    JavaScript Options

    The module gives you the possibility to override default options:

    • currentValueClass {String} [‘type-ahead__current-value’] - Current value class.
    • inputSel {String} [[data-js-item=“type-ahead__input”]] - Element selector for input.
    • minLength {Number} [2] - Minimal length of input value before suggestions will be shown.
    • minLoadTime {Number} [0] - Minimal time that must have passed before new request is sent to server.
    • suggestionItemSel {String} [[data-js-item=“suggestion-item”]] - Element selector for suggestion item.
    • suggestionItemSel {String} [[data-js-item=“type-ahead__list”]] - Element selector for type-ahead list.
    • templates.tplSuggestionsA11y {String} [‘SUGGESTIONS__A11Y’] - Template name for suggestion (list item).
    • templates.tplSuggestionsOpt {String} [‘SUGGESTIONS__OPTIONS’] - Template name for option.
    {
    	"variations": {
    		"default": {
    			"docs": {
    				"variationName": "Default"
    			},
    			"settings": {
    				"typeAheadClasses": false,
    				"id": "type-ahead",
    				"height": "400px",
    				"type": "text",
    				"attributes": [
    					{
    						"attrKey": "data-js-item",
    						"attrValue": "type-ahead__input"
    					},
    					{
    						"attrKey": "autocomplete",
    						"attrValue": "off"
    					}
    				],
    				"autocomplete": "off",
    				"typeAheadJsOptions": {
    					"serviceOptions": {
    						"url": "/ajax/example-ajax.json",
    						"method": "get",
    						"type": "json",
    						"contentType": "application/json",
    						"X-CSRFToken":"475438ff641b168e1f7e1a06c0fea6eb"
    					}
    				}
    			},
    			"content": {}
    		}
    	}
    }
    

    u-type-ahead-usage

    <div class="u-type-ahead{{#if settings.typeAheadClasses}} {{settings.typeAheadClasses}}{{/if}}"
         data-js-module="type-ahead"{{#if settings.typeAheadJsOptions}}
         data-js-options='{{stringify settings.typeAheadJsOptions}}'{{/if}}>
    	{{! WrapWith START:  }}
    	{{#wrapWith "c-form"}}
    		{{> c-form__input }}
    	{{/wrapWith}}
    	{{! WrapWith END:  }}
    	<ul class="type-ahead__list" role="listbox" data-js-item="type-ahead__list" data-js-height="{{ settings.height }}"></ul>
    </div>
    
    
    /* ===================================================
    utility: type-ahead
    =================================================== */
    
    /* ---------------------------------------------------
    Global Variables
    --------------------------------------------------- */
    $type-ahead-animation-duration-std: 200ms;
    $type-ahead-animation-easing-std: ease-in-out;
    
    
    .u-type-ahead {
    	width: 100%;
    	text-align: center;
    
    	datalist {
    		display: none;
    	}
    
    	.type-ahead__list {
    		position: absolute;
    		bottom: 0;
    		left: 0;
    		z-index: 100;
    		width: 100%;
    		background-color: #fff;
    		transform: translate3d(0, 90%, 0);
    		padding: 0;
    		margin: 0;
    		height: auto !important; // TO OVERWRITE JS TOGGLER BEHAVIOUR - OTHERWISE JS-LOGIC HAS TO BE REFACTORED - TODO
    
    		/*
    		MODIFIERS
    		----------------------- */
    		&.is-open {
    			box-shadow: 0 10px 25px 0 rgba(0, 0, 0, .1);
    			padding-top: 4px;
    			padding-bottom: 4px;
    
    			@include bp($bp-desktop-l) {
    				padding-top: 8px;
    				padding-bottom: 8px;
    			}
    
    			&:empty {
    				display: none;
    			}
    		}
    	}
    
    	.type-ahead__suggestion-item {
    		width: 100%;
    		display: block;
    	}
    
    	.type-ahead__suggestion-link {
    		display: block;
    		text-decoration: none;
    		padding: 10px 0;
    		font-size: 1.8rem;
    		line-height: 22px;
    		font-family: $font-family;
    		color: #000;
    		transition: color $type-ahead-animation-duration-std, background-color $type-ahead-animation-duration-std;
    
    		@include bp($bp-tablet-p) {
    			padding: 18px 0;
    			font-size: 3rem;
    			line-height: 3rem;
    		}
    
    		@include bp($bp-tablet-l) {
    
    		}
    
    		@include bp($bp-desktop-m) {
    
    		}
    
    		@include bp($bp-desktop-l) {
    			font-size: 3.4rem;
    			line-height: 3.4rem;
    			padding: 20px 0;
    		}
    
    		&:hover,
    		&:focus,
    		&.a11y-focus-key {
    			background-color: #f2f7f9;
    			color: #055d8d;
    			outline: 0;
    
    			em {
    				color: #055d8d !important;
    			}
    		}
    	}
    
    	.type-ahead__current-value {
    		color: #b1b1b1;
    	}
    }
    

    type-ahead.js

    /**
     * This module offers type-ahead functionality for input fields.
     *
     * @module TypeAhead
     * @version v3.1.4
     *
     * @author José Medina
     */
    
    // Imports
    import {Veams} from 'app';
    
    import VeamsComponent from 'veams/lib/common/component';
    import AjaxService from 'veams/lib/services/http';
    import Toggler from '../../../components/toggler/js/toggler';
    
    // Variables
    const $ = Veams.$;
    
    class TypeAhead extends VeamsComponent {
    
    	/**
    	 * Constructor for our class
    	 *
    	 * @see module.js
    	 *
    	 * @param {Object} obj - Object which is passed to our class
    	 * @param {Object} obj.el - element which will be saved in this.el
    	 * @param {Object} obj.options - options which will be passed in as JSON object
    	 */
    	constructor(obj) {
    		let options = {
    			currentValueClass: "type-ahead__current-value",
    			inputSel: '[data-js-item="type-ahead__input"]',
    			minLength: 2,
    			minLoadTime: 0,
    			suggestionItemSel: '[data-js-item="suggestion-item"]',
    			suggestionListSel: '[data-js-item="type-ahead__list"]',
    			templates: {
    				tplSuggestionsA11y: "SUGGESTIONS__A11Y",
    				tplSuggestionsOpt: "SUGGESTIONS__OPTIONS"
    			}
    		};
    
    		super(obj, options);
    	}
    
    
    	/**
    	 * Get module information
    	 */
    	static get info() {
    		return {
    			version: '3.1.4',
    			vc: true,
    			mod: false // set to true if source was modified in project
    		};
    	}
    
    
    	/**
    	 * Subscribe handling
    	 */
    	get subscribe() {
    		return {}
    	}
    
    
    	/**
    	 * Initialize the view
    	 *
    	 */
    	initialize() {
    		this.$suggestionList = this.$el.find(this.options.suggestionListSel);
    		this.$input = this.$el.find(this.options.inputSel);
    		this.currentValue = this.$input.val();
    
    		this.suggestionList = new Toggler({
    			el: this.$suggestionList[0],
    			namespace: 'Toggler',
    			appInstance: Veams
    		});
    
    		this.suggestionList.close();
    
    		this.service = new AjaxService(this.options.serviceOptions);
    
    		this.service.requestDidOpen = (request, obj) => {
    			if (this.options.serviceOptions.contentType) {
    				request.setRequestHeader("Content-type", this.options.serviceOptions.contentType);
    			}
    			request.setRequestHeader("X-CSRFToken", this.options.serviceOptions["X-CSRFToken"]);
    		}
    	}
    
    
    	/**
    	 * Bind Vent events for the resize events and the load more button
    	 */
    	bindEvents() {
    		this.$input.on(Veams.EVENTS.keyup, this.onKeyup.bind(this));
    		$(window).on(Veams.EVENTS.keydown, this.onWindowKeydown.bind(this));
    		this.$input.on(Veams.EVENTS.blur, this.onItemBlur.bind(this));
    	}
    
    
    	/**
    	 * Set focus when needed
    	 *
    	 * @param {Object} obj - CTA event object
    	 */
    	setFocus(obj) {
    
    		if (obj.isActive) {
    
    			setTimeout(() => {
    				this.$input[0].focus();
    			}, 1000);
    		}
    	}
    
    
    	/**
    	 * Handle window key down event to control the module with arrow keys, enter and escape
    	 *
    	 * @param {Object} event - Event object
    	 */
    	onWindowKeydown(event) {
    		if (this.el.contains(event.target)) {
    			let getFocusableElements = () => {
    				return Array.prototype.slice.call($("a, input", this.$el));
    			};
    			let focusableEls, index;
    
    			switch (event.keyCode) {
    				case 38:
    					event.preventDefault();
    					focusableEls = getFocusableElements();
    					index = focusableEls.indexOf(document.activeElement);
    
    					if (index > -1 && index - 1 >= 0) {
    						focusableEls[index - 1].focus();
    					}
    					break;
    				case 40:
    					event.preventDefault();
    					focusableEls = getFocusableElements();
    					index = focusableEls.indexOf(document.activeElement);
    
    					if (index > -1 && index + 1 < focusableEls.length) {
    						focusableEls[index + 1].focus();
    					}
    					break;
    				case 27:
    					this.closeSuggestionList();
    					break;
    				case 13:
    					this.triggerChosenSuggestion(event.target);
    					this.closeSuggestionList();
    					break;
    			}
    		}
    	}
    
    
    	/**
    	 * Handle input key up event to control the module with arrow keys, enter and escape
    	 *
    	 * @param {Object} event - Event object
    	 */
    	onKeyup(event) {
    		switch (event.keyCode) {
    			case 38:
    			case 40:
    			case 27:
    			case 13:
    				event.preventDefault();
    				break;
    			default:
    				this.currentValue = this.$input.val();
    				this.render(this.generateTimeOfRequest());
    		}
    	}
    
    
    	/**
    	 * Bind filter items events as soon as render occurs
    	 *
    	 */
    	bindItemEvents() {
    		this.$suggestionList.find(this.options.suggestionItemSel)
    			.on(Veams.EVENTS.touchstart + ' ' + Veams.EVENTS.click, this.onChosenSuggestion.bind(this))
    			.on(Veams.EVENTS.blur, this.onItemBlur.bind(this))
    			.on(Veams.EVENTS.focus, this.onItemFocus.bind(this))
    			.on(Veams.EVENTS.mouseenter, this.onItemMouseEnter.bind(this))
    			.on(Veams.EVENTS.mouseleave, this.onItemMouseLeave.bind(this));
    	}
    
    
    	/**
    	 * Handle touch and click event
    	 *
    	 * @param {Object} event - Event object
    	 * @param {object} [currentTarget] - Target to which listener was attached (used in VeamsQuery).
    	 */
    	onChosenSuggestion(event, currentTarget) {
    		let target = currentTarget || event.currentTarget;
    
    		event.preventDefault();
    
    		this.triggerChosenSuggestion(target);
    
    		this.closeSuggestionList();
    	}
    
    
    	/**
    	 * Handle item focus event
    	 *
    	 * @param {Object} event - Event object
    	 */
    	onItemFocus(event) {
    		this.$input.val($(event.target).closest(this.options.suggestionItemSel).attr('data-value'));
    	}
    
    
    	/**
    	 * Handle item mouse enter event
    	 *
    	 * @param {Object} event - Event object
    	 */
    	onItemMouseEnter(event) {
    		this.onItemFocus.apply(this, arguments);
    	}
    
    
    	/**
    	 * Handle item mouse leave event
    	 */
    	onItemMouseLeave() {
    		this.$input.val(this.currentValue);
    	}
    
    
    	/**
    	 * Handle item blur event
    	 *
    	 * @param {Object} event - Event object
    	 */
    	onItemBlur(event) {
    		this.$input.val(this.currentValue);
    
    		if (!!event.relatedTarget && !this.$suggestionList[0].contains(event.relatedTarget) &&
    			event.relatedTarget !== this.$input[0]) {
    			this.closeSuggestionList();
    		}
    	}
    
    
    	/**
    	 * Helper function that looks for the current input value and transmit it
    	 *
    	 * @param {Object} target - DOM Node
    	 */
    	triggerChosenSuggestion(target) {
    		this.currentValue = $(target).attr('data-value') || $(target).val();
    		this.$input.val(this.currentValue);
    
    		this.$el.trigger(Veams.EVENTS.typeAhead.suggestionChoosen, {
    			search: this.currentValue
    		});
    	}
    
    
    	/**
    	 * Render class
    	 *
    	 * Perform and server request to get the suggestions to a given value
    	 *
    	 * @param {Number} timeOfRequest - Time of request
    	 */
    	render(timeOfRequest) {
    		if (this.currentValue.length < this.options.minLength || !this.$el[0].contains(document.activeElement)) {
    			this.closeSuggestionList();
    			return false;
    		}
    
    		return new Promise((resolve, reject) => {
    			this.service[this.options.serviceOptions.method]({
    				data: JSON.stringify({}),
    				url: this.options.serviceOptions.url + "?type-ahead=" + this.currentValue
    			}).then((data) => {
    				setTimeout(() => {
    					if (timeOfRequest !== this.timeOfRequest) {
    						return false;
    					}
    
    					this.$suggestionList.html(Veams.templater.render(this.options.templates.tplSuggestionsA11y,
    						this.formatResponseDataHelper(data)));
    
    					this.bindItemEvents();
    
    					this.suggestionList.open();
    
    					resolve();
    				}, Math.max(this.options.minLoadTime - (Date.now() - timeOfRequest), 0));
    			}, (error) => {
    				reject();
    				throw new Error("Typeahead render() :: could not resolve server request :: ", error);
    			});
    		});
    	}
    
    
    	/**
    	 * Cache the current timestamp to figure out which was the last request
    	 *
    	 * If 2 delta time between request is less than minLoadTime, they will be rejected
    	 * and the one with the latest timestamp will be sent
    	 *
    	 * @return {Number} - Time of request
    	 */
    	generateTimeOfRequest() {
    		this.timeOfRequest = Date.now();
    
    		return this.timeOfRequest;
    	}
    
    
    	/**
    	 * Data formatter that prepares the data for the template to render
    	 *
    	 * @param {Object} data - Data to be prepared for rendering
    	 * @return {Object} - Prepared data (ready for rendering)
    	 */
    	formatResponseDataHelper(data) {
    		data.suggestions = data.suggestions.map((item) => {
    			item.suggestionText = this.getSuggestionTextHelper(item.name);
    			item.searchVal = this.currentValue;
    
    			return item;
    		});
    
    		return data;
    	}
    
    
    	/**
    	 * Data formatter that prepares the data for the template to render
    	 *
    	 * @param {String} name - Item name
    	 * @return {String} - Modified data
    	 */
    	getSuggestionTextHelper(name) {
    		let currentValueCaseInsensitive = new RegExp(this.currentValue, "ig");
    
    		return name.replace(currentValueCaseInsensitive, '<em class="' + this.options.currentValueClass + '">$&</em>');
    	}
    
    
    	/**
    	 * Close the suggestion list and empty the list
    	 *
    	 */
    	closeSuggestionList() {
    		this.suggestionList.close();
    		this.$suggestionList.empty();
    	}
    }
    
    export default TypeAhead;
    
    

    Default

    <div class="u-type-ahead" data-js-module="type-ahead" data-js-options='{&quot;serviceOptions&quot;:{&quot;url&quot;:&quot;/ajax/example-ajax.json&quot;,&quot;method&quot;:&quot;get&quot;,&quot;type&quot;:&quot;json&quot;,&quot;contentType&quot;:&quot;application/json&quot;,&quot;X-CSRFToken&quot;:&quot;475438ff641b168e1f7e1a06c0fea6eb&quot;}}'>
    	<form class="c-form--default
    	" action="//" method="" data-css="c-form">
    		<div class="form__input">
    			<div class="form__input-wrapper">
    				<input id="type-ahead" name="type-ahead" type="text" data-js-item="type-ahead__input" autocomplete="off" class="form__input-text" />
    			</div>
    		</div>
    	</form>
    	<ul class="type-ahead__list" role="listbox" data-js-item="type-ahead__list" data-js-height="400px"></ul>
    </div>