Skip to content Skip to sidebar Skip to footer

Creating A Autocomplete Field With Typeahead In The Durandal Framework

I'm trying to achieve a autocomplete input field with typeahead (twitter bootstrap). This autocomplete field should be in a modal but I cant seem to get it working! It also must be

Solution 1:

You would need to bind the typeahead input to an array exposed on your view model. I don't think you are doing either at the moment.

To do the bindings you should use knockout-bootstrap bindings found here: http://billpull.github.io/knockout-bootstrap/

Once you have included the above knockout-bootstrap bindings you can do this in your view:

<input type="text" data-bind="typeahead: repairlines" />

Then make sure you are adding repairlines to your VM instance. Adding it to your this reference should do the trick.

// this will add the repairlines observable array to your VM instance
this.repairlines = repairlines;

Hope this helps and good luck :-)


Solution 2:

I would comment, but can't so here's my note. In your example you have a same origin policy (SOP) issue. So the data from the twitter page isn't getting pulled in. That pretty much kills the process so you don't end up with anything.

I found that if I include the appropriate styles (such as one for tt-dropdown-menu):

<span class="tt-dropdown-menu" style="position: absolute; top: 100%; left: 0px; z-index: 100; display: block; right: auto;"></span>

and have a functioning dataset it works fine. Here's a fiddle of my attempt http://jsfiddle.net/rUdXt/ and here is a great page that helped me (especially with the styles http://twitter.github.io/typeahead.js/examples/).


Solution 3:

I have fixed this problem by adding some extra code to the library: https://github.com/billpull/knockout-bootstrap

This is the knockout-bootstrap.js, where ko.bindingHandlers.typeahead is rewritten so it accept updates, minLength en items. Just load this script in.

//UUID
function s4() {
    return Math.floor((1 + Math.random()) * 0x10000)
        .toString(16)
        .substring(1);
}

function guid() {
    return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}

// Outer HTML
(function($){
  $.fn.outerHtml = function() {
    if (this.length == 0) return false;
    var elem = this[0], name = elem.tagName.toLowerCase();
    if (elem.outerHTML) return elem.outerHTML;
    var attrs = $.map(elem.attributes, function(i) { return i.name+'="'+i.value+'"'; }); 
    return "<"+name+(attrs.length > 0 ? " "+attrs.join(" ") : "")+">"+elem.innerHTML+"</"+name+">";
  };
})(jQuery);

// Bind twitter typeahead
ko.bindingHandlers.typeahead = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var $element = $(element);
        var allBindings = allBindingsAccessor();
        var typeaheadArr = ko.utils.unwrapObservable(valueAccessor());

        $element.attr("autocomplete", "off")
                .typeahead({
                    'source': typeaheadArr,
                    'minLength': allBindings.minLength,
                    'items': allBindings.items,
                    'updater': allBindings.updater
                });
    }
};

/*
ko.bindingHandlers.typeahead = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {


        var $element = $(element);
        var t = valueAccessor();
        var typeaheadArr = ko.utils.unwrapObservable(valueAccessor());
        var k = allBindingsAccessor().v1;

        $element.attr("autocomplete", "off")
                .typeahead({
                    'source' : typeaheadArr
                });
    }
};
*/

// Bind Twitter Progress
ko.bindingHandlers.progress = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var $element = $(element);

        var bar = $('<div/>', {
            'class':'bar',
            'data-bind':'style: { width:' + valueAccessor() + ' }'
        });

        $element.attr('id', guid())
            .addClass('progress progress-info')
            .append(bar);

        ko.applyBindingsToDescendants(viewModel, $element[0]);
    }
}

// Bind Twitter Alert
ko.bindingHandlers.alert = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var $element = $(element);
        var alertInfo = ko.utils.unwrapObservable(valueAccessor());

        var dismissBtn = $('<button/>', {
            'type':'button',
            'class':'close',
            'data-dismiss':'alert'
        }).html('&times;');

        var alertMessage = $('<p/>').html(alertInfo.message);

        $element.addClass('alert alert-'+alertInfo.priority)
                .append(dismissBtn)
                .append(alertMessage);
    }
};

// Bind Twitter Tooltip
ko.bindingHandlers.tooltip = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        // read tooltip options
        var tooltipBindingValues = ko.utils.unwrapObservable(valueAccessor());

        // set tooltip title
        var tooltipTitle = tooltipBindingValues.title;

        // set tooltip placement
        var tooltipPlacement = tooltipBindingValues.placement;

        // set tooltip trigger
        var tooltipTrigger = tooltipBindingValues.trigger;

        var options = {
            title: tooltipTitle
        };

        ko.utils.extend(options, ko.bindingHandlers.tooltip.options);

        if (tooltipPlacement) {
            options.placement = tooltipPlacement;
        }

        if (tooltipTrigger) {
            options.trigger = tooltipTrigger;
        }

        $(element).tooltip(options);
    },
    options: {
        placement: "top",
        trigger: "hover"
    }
};

// Bind Twitter Popover
ko.bindingHandlers.popover = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        // read popover options 
        var popoverBindingValues = ko.utils.unwrapObservable(valueAccessor());

        // set popover title 
        var popoverTitle = popoverBindingValues.title;

        // set popover template id
        var tmplId = popoverBindingValues.template;

        // set popover trigger
        var trigger = 'click';

        if (popoverBindingValues.trigger) {
            trigger = popoverBindingValues.trigger;
        }

        // update triggers
        if (trigger === 'hover') {
            trigger = 'mouseenter mouseleave';
        } else if (trigger === 'focus') {
            trigger = 'focus blur';
        }

        // set popover placement
        var placement = popoverBindingValues.placement;

        // get template html
        var tmplHtml = $('#' + tmplId).html();

        // create unique identifier to bind to
        var uuid = guid();
        var domId = "ko-bs-popover-" + uuid;

        // create correct binding context
        var childBindingContext = bindingContext.createChildContext(viewModel);

        // create DOM object to use for popover content
        var tmplDom = $('<div/>', {
            "class" : "ko-popover",
            "id" : domId
        }).html(tmplHtml);

        // set content options
        options = {
            content: $(tmplDom[0]).outerHtml(),
            title: popoverTitle
        };

        if (placement) {
            options.placement = placement;
        }

        // Need to copy this, otherwise all the popups end up with the value of the last item
        var popoverOptions = $.extend({}, ko.bindingHandlers.popover.options, options);

        // bind popover to element click
        $(element).bind(trigger, function () {
            var popoverAction = 'show';
            var popoverTriggerEl = $(this);

            // popovers that hover should be toggled on hover
            // not stay there on mouseout
            if (trigger !== 'click') {
                popoverAction = 'toggle';
            }

            // show/toggle popover
            popoverTriggerEl.popover(popoverOptions).popover(popoverAction);

            // hide other popovers and bind knockout to the popover elements
            var popoverInnerEl = $('#' + domId);
            $('.ko-popover').not(popoverInnerEl).parents('.popover').remove();

            // if the popover is visible bind the view model to our dom ID
            if($('#' + domId).is(':visible')){
                ko.applyBindingsToDescendants(childBindingContext, $('#' + domId)[0]);
            }

            // bind close button to remove popover
            $(document).on('click', '[data-dismiss="popover"]', function (e) {
                popoverTriggerEl.popover('hide');
            });
        });

        // Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice
        return { controlsDescendantBindings: true };
    },
    options: {
        placement: "right",
        title: "",
        html: true,
        content: "",
        trigger: "manual"
    }
};

In your HTML do this.

<input type="text" data-bind="typeahead: repairlines(), minLength: 2, updater: updateViewAfterSelection, value: Description" required/>

This is my viewmodel:

define(function (require) {
    var dataservice = require('services/dataservice'),
        allCustomers = ko.observableArray([]),
        repairlinesRaw = ko.observableArray([]),
        Description = ko.observable(),
        InternalCode = ko.observable(),
        Mat = ko.observable(),
        Location = ko.observable(),
        Rep = ko.observable(),
        Dum = ko.observable(),
        Dam = ko.observable(),
        Qty = ko.observable(0).extend({ numeric: 2 }),
        Hours = ko.observable(0).extend({ numeric: 2 }),
        Tariff = ko.observable(0).extend({ numeric: 2 }),
        Costs = ko.observable(0).extend({ numeric: 2 });

    function init() {
        dataservice = new dataservice('api/data');
        dataservice.getAllRows('AllCustomers').then(function (data) {
            data.results.forEach(function (item) {
                allCustomers.push(item);
            });
        }).fail(function () {
        });

        dataservice.getAllRows('EntireRepairLineLib').then(function (data) {
            data.results.forEach(function (item) {
                repairlinesRaw.push(item);
            });
        }).fail(function () {
        });
    }

    init();

    AddRepairOrderLineModal = function (loggedInEmployee) {
        //later ook customer en repairorder meegeven!
        this.allCustomers = allCustomers;
        this.choosenCustomerId = ko.observable(); //holds the Id of the chosen customer
        this.Description = Description;
        this.InternalCode = InternalCode;
        this.Mat = Mat;
        this.Location = Location;
        this.Rep = Rep;
        this.Dam = Dam;
        this.Qty = Qty;
        this.Hours = Hours;
        this.Tariff = Tariff;
        this.Costs = Costs;
        //this.CreationDate = (new Date().getMonth() + 1) + "-" + new Date().getDate() + "-" + new Date().getFullYear();
        this.repairlines = function () {
            var repairlinesName = [];
            map = {};
            var data = repairlinesRaw();
            $.each(data, function (i, repairline) {
                map[repairline.Description()] = repairline;
                repairlinesName.push(repairline.Description());
            });
            return repairlinesName;
        };

        this.IsAgreement = ko.observable(true);
        this.IsAuthorized = ko.observable(true);
        this.DoRepair = ko.observable(true);
        this.selectedEmployee = loggedInEmployee;
    };

    AddRepairOrderLineModal.prototype.updateViewAfterSelection = function(item) {
        //map can be found in the repairlines function
        Description(map[item].Description());
        InternalCode(map[item].InternalCode());
        Mat(map[item].MaterialCode());
        Location(map[item].LocationCode());
        Rep(map[item].RepairCode());
        Dam(map[item].DamageCode());
        Qty(map[item].Quantity());
        Hours(map[item].Hours());
        Costs(map[item].Costs());
        return item;
    };

    AddRepairOrderLineModal.prototype.ok = function () {
        var jsonObj = [];
        jsonObj.push({
            Description: this.Description(), InternalCode: this.InternalCode(),
            Mat: this.Mat(), Location: this.Location(),
            Rep: this.Rep(), Dam: this.Dam(),
            CustomerId: this.choosenCustomerId(), Qty: this.Qty(), Hours: this.Hours(),
            Tariff: this.Tariff(), Costs: this.Costs(),
            IsAgreement: this.IsAgreement(), IsAuthorized: this.IsAuthorized(), DoRepair: this.DoRepair(),
        });

        //empty all fields after JSON 
        //Otherwise the values would still be there when a new modal is opened again.
        Description(null);
        InternalCode(null);
        Mat(null);
        Location(null);
        Rep(null);
        Dum(null);
        Dam(null);
        Qty(0);
        Hours(0);
        Tariff(0);
        Costs(0);

        this.modal.close(jsonObj);
    };

    AddRepairOrderLineModal.prototype.closeModal = function () {
        return this.modal.close();
    };
    return AddRepairOrderLineModal;
});

Hope people will understand this ;), hope it helps, alot of credits go to Bill Pull ;) and Alex Preston


Solution 4:

Cannot understand this as it is TOO messy. That is, the typeahead bit is mix in with all your other stuff. What about a simple example that has only typeahead stuff and maybe is composed to a view/viewmodel like a widget so it can be reused.

Then we could have a chance to understand it


Post a Comment for "Creating A Autocomplete Field With Typeahead In The Durandal Framework"