asp.net mvc - implementing an “autocomplete” tags selection for a blog?

I'm working on a blog system for my own use, and would like to implement an auto-complete tags selection (similar to stackoverflow), how would I implement something like this? Any example or links to a tutorial would be greatly appreciated.

Thanks.

Answers


See my question here Jquery, Autocomplete using json, id's vs display values

We actually "borrowed" (read copy and pasted) SO's autocomplete javascript and then tweaked it slightly - such as renaming it so it won't interfere with jquery ui's autocomplete.

The two are actually very similar, but we wanted specifically to have a tag system like SO.

you can yank the code I used off http://pastebin.com/t29RCCZg

and here is a sample action we used for tags

public ActionResult ProfileTags(string prefix, int? limit)
    {
        if (!limit.HasValue)
            limit = ConfigurationHelper.Paging.TagList;

        if (String.IsNullOrEmpty(prefix))
            prefix = String.Empty;

        ProfileTagModel model = new ProfileTagModel()
        {
            Tags = profileTagRepository.GetList(new ProfileTagsByPrefixQuery(prefix)).OrderBy(x => x.Name).Take<ProfileTag>(limit.Value)
        };

        return View(model);
    }

And the view looks like this

<%@ Page Language="C#" ContentType="text/html" Inherits="System.Web.Mvc.ViewPage<ProfileTagModel>" %>

<% if(Model.Tags != null) { %>
    <% foreach (ProfileTag tag in Model.Tags) { %>
        <%= tag.Name + ((tag.ProfileCount > 0) ? " (" + tag.ProfileCount.ToString() + ")" : String.Empty) %>
    <% } %>
<% } %>

And then our usage on the page looks something like this

$().ready(function () {
    $("#ProfileTags").troppinautocomplete('<%= Url.Action("ProfileTags", "Filter") %>', {
        max: 10,
        highlightItem: true,
        multiple: true,
        multipleSeparator: " ",
        matchContains: false,
        scroll: true,
        scrollHeight: 300,
        dataType: "html"
    });


})

You don't have to do it this way, though. You can make the action method return an array of objects as json, and then by altering how your autocomplete is declared, you can actually format the tags in to display with an icon or other features.

Heres a sample that took in json for a formatted look

$('#troppinSearch').troppinautocomplete(url, {
        dataType: 'json',
        parse: function (data) {
            var rows = new Array();
            if (data != null) {
                for (var i = 0; i < data.length; i++) {
                    rows[i] = { data: data[i], value: data[i].Id, result: data[i].Title };
                }
            }
            return rows;
        },
        formatItem: function (row, i, n) {

            return '<table><tr><td valign="top"><img height="28" width="28" src="' + row.ImageUrl + '" /></td><td valign="top" style="padding:0px 0px 0px 6px;"><div>' + row.Title + '</div><div><small>' + row.ResultType + '</small></div></td></tr></table>';
        },
        formatResult: function (row, i, n) {
            return row.Id;
        },
        width: 336,
        max: 20,
        highlightItem: true,
        multiple: false,
        matchContains: true,
        scroll: true,
        scrollHeight: 300
    }).result(function (event, data, formatted) {
        var type = data.ResultType.toLowerCase();
        var id = data.Id;

        if (type == "product") {
            window.location.href = '/Shop/Product/' + id;
        } else {
            window.location.href = '/Profile/Index/' + id;
        }
    });

And the action looks like this

public ActionResult Search(string contentType, string prefix, int? limit)
    {
        if (!limit.HasValue)
            limit = ConfigurationHelper.Paging.ProfileList;

        SearchResponse response = GetSearchResults(contentType, prefix);

        var dropDownResults = (from r in response.Results
                              select new
                              {
                                  Id = r.Id,
                                  Title = r.Name,
                                  ImageUrl = r.DefaultImage,
                                  ResultType = r.ResultType.ToString()
                              }).Distinct().Take(limit.Value);

        return Json(dropDownResults.ToList(), JsonRequestBehavior.AllowGet);
    }

When you do it this way, you don't need a view. The autocompleter takes in the json data and does everything magically. The .Result function on the end lets you set up an event that occurs when a selection is made. in this case it's actually sending the user to another page, but we have used it to set a value in a hidden field.

EDIT

I forgot the built in CSS classes for this code. Here's a sample CSS.

.ac_results{
padding:0;
border:1px solid #4c4c4c;
background-color:#ffffff;
overflow:hidden;
z-index:99999;
text-align:left;
font-size: 14px; line-height:14px;
color: #333333;
}

.ac_highlight{
font-weight:bold;
text-decoration:underline;
background-color: #ff6600;
color: #ffffff;
}

.ac_results ul{
width:100%;
list-style-position:outside;
list-style:none;
padding:0;
margin:0;

}

.ac_results li{
margin:0;
padding:3px 6px 3px 6px;
cursor:default;
display:block;
line-height:14px;
overflow:hidden;
}

.ac_loading{
background:#fff url(/Content/images/loading.gif) right center no-repeat;

}

.ac_over{
background-color:#ff6600;
color:#ffffff;

}

I've decided to give jQuery UI Autocomplete a try and it seems to be easy enough :) Here's the javascript code:

$(document).ready(function () {
    function split(val) {
        return val.split(/,\s*/);
    }
    function extractLast(term) {
        return split(term).pop();
    }

    $("#TagsString")
    // don't navigate away from the field on tab when selecting an item
            .bind("keydown", function (event) {
                if (event.keyCode === $.ui.keyCode.TAB &&
                        $(this).data("autocomplete").menu.active) {
                    event.preventDefault();
                }
            })
            .autocomplete({
                source: function (request, response) {
                    $.get("/Blog/GetTags", { term: extractLast(request.term) }, function (data) {
                        response($.map(data.tags, function (item) {
                            return {
                                label: item.Name,
                                value: item.Id
                            }
                        }))
                    }, "json");
                },
                minLength: 2,
                dataType: 'json',
                focus: function () {
                    // prevent value inserted on focus
                    return false;
                },
                select: function (event, ui) {
                    var terms = split(this.value);
                    // remove the current input
                    terms.pop();
                    // add the selected item
                    terms.push(ui.item.label);
                    // add placeholder to get the comma-and-space at the end
                    terms.push("");
                    this.value = terms.join(", ");
                    return false;
                }
            });
});

Here's the HTML:

<p>
            @Html.TextBoxFor(Model => Model.TagsString, new { @tabindex = "2", @size = "22", @value = "", @class = "text_input" })
            <label for="TagsString">
                <strong class="leftSpace">Tags</strong></label></p>
<style>
    .ui-autocomplete-loading
    {
        background: white url('/Content/Images/ui-anim_basic_16x16.gif') right center no-repeat;
    }
</style>

And here's the action:

[HttpGet]
        public virtual JsonResult GetTags(string term)
        {
            var getTags = _tag.All().Where(t => t.Name.ToLower().Contains(term.ToLower())).OrderBy(t => t.Name).ToList();

            TagViewModel model = new TagViewModel()
            {
                Tags = Mapper.Map<List<Tag>, List<TagModel>>(getTags)
            };

            return Json(new
            {
                tags = model.Tags
            }, JsonRequestBehavior.AllowGet);
        }

Works really well :)


I use jQuery UI's autocomplete but I load the data beforehand;

View:

@Html.TextBoxFor(Model => Model.Tags, new { @class = "txtbox-long" })
@Html.Resource(@<link href="@Url.Content("~/Content/CSS/flick/jquery-ui-1.8.11.css")" rel="stylesheet" type="text/css" />, "css")
@Html.Resource(@<script src="@Url.Content("~/Content/JS/jquery-ui-1.8.11.min.js")" type="text/javascript" language="javascript"></script>, "js")
@Html.Resource(
    @<script type="text/javascript" language="javascript">
         $(document).ready(function () {
             var tags; $.getJSON("/Thread/GetTags", function (data) { tags = data; });

             function split(val) { return val.split(/ \s*/); }
             function extractLast(term) { return split(term).pop(); }

             $("#Tags")
             // don't navigate away from the field on tab when selecting an item
                .bind("keydown", function (event) {
                    if (event.keyCode === $.ui.keyCode.TAB && $(this).data("autocomplete").menu.active) event.preventDefault();
                })
                .autocomplete({
                    delay: 0,
                    minLength: 0,
                    source: function (request, response) {
                        response($.ui.autocomplete.filter(tags, extractLast(request.term)));
                    },
                    focus: function () {
                        // prevent value inserted on focus
                        return false;
                    },
                    select: function (event, ui) {
                        var terms = split(this.value);
                        // remove the current input
                        terms.pop();
                        // add the selected item
                        terms.push(ui.item.value);
                        // add placeholder to get the space at the end
                        terms.push("");
                        this.value = terms.join(" ");

                        return false;
                    }
                });
         });
    </script>
, "js")

Controller:

[Classes.Attributes.Ajax]
public JsonResult GetTags()
{
    return Json(
        TagService.GetTags().Select(x => x.Name),
        "text/plain",
        JsonRequestBehavior.AllowGet
    );
}

Works really well and saves multiple calls to the database as it uses client side searching. I'm using it in a small project so there won't be that many tags.


You could call an Action Method that would take the text they're currently entering and return a View with a populated list of possible tags.

Your Action Method could look like this:

public ActionResult GetTags(string tag)
{
    List<string> tags = // get AutoComplete data from somewhere

    return View(tags);
}

And your Autocomplete View could simply be:

<%@ Page Language="C#" Inherits="ViewPage<IList<string>>" %>

<ul>
    <% foreach(string tag in Model) { %>
    <li><%=tag %></li>
    <% } %>
</ul>

And if you're using jQuery, you could try:

$.ajax({
    url: "Autocomplete/GetTags/" + tag,
    cache: false,
    success: function(html) {
        $("#autocomplete").html(html);
    }
});

Need Your Help

JList is not updating correctly when action listner is a thread

java swing actionlistener jlist event-dispatch-thread

I have a button ActionListener that is a Thread. Within this thread's run method I try to update a JList setListData() method and it does not update. It worked with a combobox but not a JList.

Override for fluent NHibernate for long text strings nvarchar(MAX) not nvarchar(255)

c# nhibernate fluent-nhibernate automapping sharp-architecture

When ever you set a string value in fluent NHibernate it alwasy sets the DB vales to Nvarchar(255), I need to store quite a lot of long string which are based on user inputs and 255 is impractical.

About UNIX Resources Network

Original, collect and organize Developers related documents, information and materials, contains jQuery, Html, CSS, MySQL, .NET, ASP.NET, SQL, objective-c, iPhone, Ruby on Rails, C, SQL Server, Ruby, Arrays, Regex, ASP.NET MVC, WPF, XML, Ajax, DataBase, and so on.