Search code examples
razorknockout.jstag-it

multiple tagit and knockout.js


I am trying to create a page where a user can add multiple products, and each product can have multiple competitors. The Competitors piece is required to be generated with tagit. For some reason the tagit is not working... This is the knockout template:

<script type="text/html" id="product-template">
<tr>
    <td style="width: 5%">
        <input type="hidden" name="Products.Index" data-bind="value: Id"/>
    </td>
    <td style="width: 30%">
        Product:<br/>
        <select data-bind='options: ProductSource, optionsText: "Product", optionsCaption: "", value: Product, attr: { name: "Products[" + Id + "].Product" }'></select>
    </td>
    <td style="width: 30%; line-height: 100%; margin-top: 0; padding-top: 0">
        Competitors:<br/>
        <div id="tags_error" style="display: none;" class="ui-state-error-text">entries must be less than 50 characters each</div>
        <ul class="tags" style="margin-top: 2px; padding-top: 0" data-bind="foreach: Competitors">
            <li data-bind="text: $data" >
            </li >
        </ul>
        <div style="font-size: 10px; color: GrayText; font-style: italic;">please separate tags using the comma or enter key</div>
        @* <input type="text" value="" data-bind='attr: { name: "Products[" + $parent.Id + "].Competitors" }' />*@
    </td>
    <td style="width: 30%">
        Positioning:<br/>
        <select data-bind='options: PositioningSource, optionsText: "Positioning", optionsCaption: "", value:Positioning, attr: { name: "Products[" + Id + "].Positioning" }'></select>
    </td>
    <td style="width: 5%">
        <a href="#" data-bind="click: $parent.removeProduct">
            <img src="@Links.Content.Images.DeleteAlert_gif"/>
        </a>
    </td>
</tr>

This is my Knockout Model View:

 $(document).ready(function () {

    function ProductItem(id, product, positioning, competitors) {
        var self = this;
        self.Id = id;
        self.Product = ko.observable(product);
        self.Positioning = ko.observable(positioning);
        self.Competitors = ko.observableArray(competitors);
        self.Competitors.push( '1', '2', '3' );
        self.errors = ko.validation.group(self);
    }

    function ProductsViewModel() {
        var self = this;
        var id = 0;
        self.Products = [];
        self.Products = ko.observableArray();

        @if (Model.Products != null)
        {
            foreach (var item in Model.Products)
            {
                <text>self.Products.push(new ProductItem(id++, '@item.Product', '@item.Positioning', '@item.Competitors'));</text>
            }
        }

        self.selectedProduct = ko.observable();
        self.addProduct = function () {
            self.Products.push(new ProductItem(id++));
        };
        self.removeProduct = function (product) {
            self.Products.remove(product);
        };
    }
    ko.validation.init({ insertMessages: true, decorateElement: true, errorClass: "error" });
    ko.applyBindingsWithValidation(new ProductsViewModel());
});

And this is the tagit setup:

  $(function () {
    var changed = function (e1, e2) {
        var item = $(e2);
        if (item.find('input').val().length > 50) {
            setTimeout(function () { item.remove(); }, 50);
            $('#tags_error').show();
            setTimeout(function () { $('#tags_error').fadeOut(1000); }, 10000);
        } else {
            $('.tags, .tags input').addClass('ui-state-highlight');
        }
    };
    $('.tags').tagit({
        tagSource: '@Html.Raw(Url.Action(MVC.Marketing.Campaign.SearchTags()))',
        removeConfirmation: true,
        allowSpaces: true,
        caseSensitive: false,
        onTagAdded: changed,
        onTagRemoved: changed,
        placeholderText: 'enter a tag'
    });
});

The adding of products works well, just that the competitors show up as a list instead of an input where I can add tags...


Solution

  • And the solution. Template:

    <script type="text/html" id="product-template">
    <tr>
        <td style="width: 5%">
            <input type="hidden" name="Products.Index" data-bind="value: Id"/>
        </td>
        <td style="width: 30%">
            Product:<br/>
            <select data-bind='options: ProductSource, optionsText: "Product", optionsCaption: "", value:Product'></select>
            <input data-bind='attr: { name: "Products[" + Id + "].Product", value: (Product() !== undefined) ? Product().Product : "" }' type="hidden" value="">
        </td>
        <td style="width: 30%; line-height: 100%; margin-top: 0; padding-top: 0">
            Competitors:<br/>
            <ul style="margin-top: 2px; padding-top: 0" data-bind="tagit:Competitors, productId:$data.Id"></ul>
            <div style="font-size: 10px; color: GrayText; font-style: italic;">please separate competitors using the comma or enter key</div>
        </td>
        <td style="width: 30%">
            Positioning:<br/>
            <select data-bind='options: PositioningSource, optionsText: "Positioning", optionsCaption: "", value:Positioning'></select>
            <input data-bind='attr: { name: "Products[" + Id + "].Positioning", value: (Positioning() !== undefined) ? Positioning().Positioning : "" }' type="hidden" value="">
        </td>
        <td style="width: 5%">
            <a href="#" data-bind="click: $parent.removeProduct">
                <img src="@Links.Content.Images.DeleteAlert_gif"/>
            </a>
        </td>
    </tr>
    

    Knockout model:

    <script type="text/javascript">
    var ProductSource = [];
    var index = 0;
    $.ajax({
        type: "GET",
        url: '@Html.Raw(Url.Action(MVC.UserChoice.ForName()))',
        data: { userChoiceName: '@UserChoiceKey.OpportunityProductInterest' },
        cache: false,
        success: function (data) {
            data.forEach(function (item)
            { ProductSource.push({ Product: item.Text }); });
        }
    });
    
    var PositioningSource = [];
    var index = 0;
    $.ajax({
        type: "GET",
        url: '@Html.Raw(Url.Action(MVC.UserChoice.ForName()))',
        data: { userChoiceName: '@UserChoiceKey.WorkRequestProductPositioning' },
        cache: false,
        success: function (data) {
            data.forEach(function (item)
            { PositioningSource.push({ Positioning: item.Text }); });
        }
    });
    
    $(document).ready(function () {
    
        function ProductItem(id, product, positioning, competitors) {
            var self = this;
            self.Id = id;
            self.Product = ko.observable(product);
            self.Positioning = ko.observable(positioning);
            self.Competitors = ko.observableArray(competitors);
            self.errors = ko.validation.group(self);
        }
    
        function ProductsViewModel() {
            var self = this;
            var id = 0;
            self.Products = [];
            self.Products = ko.observableArray();
    
            @if (Model.Products != null)
            {
                foreach (var item in Model.Products)
                {
                    <text>self.Products.push(new ProductItem(id++, '@item.Product', '@item.Positioning', '@item.Competitors'));</text>
                }
            }
    
            self.selectedProduct = ko.observable();
            self.addProduct = function () {
                self.Products.push(new ProductItem(id++));
            };
            self.removeProduct = function (product) {
                self.Products.remove(product);
            };
        }
    
        ko.bindingHandlers.tagit = {
            //https://github.com/aehlke/tag-it
            init: function (element, valueAccessor, allBindingsAccessor) {
    
                var options = {
                    allowSpaces: true,
                    caseSensitive: false,
                    showAutocompleteOnFocus: true,
                    tagSource: '@Html.Raw(Url.Action(MVC.Marketing.Campaign.SearchTags()))',
                    itemName: "Products[" + allBindingsAccessor.get('productId') + "].Competitors",
                    fieldName:""
                };
    
                var tags = allBindingsAccessor()["tagsSource"];
                if (tags)
                    $.extend(options, {
                        autocomplete: { source: tags, delay: 0, minLength: 0 }
                    });
                $(element).tagit(options);
            },
            update: function (element, valueAccessor) {
                var value = ko.utils.unwrapObservable(valueAccessor());
                var tags = value;
    
                $(element).tagit("removeAll");
                for (var x = 0; x < tags.length; x++) {
                    $(element).tagit("createTag", tags[x]);
                }
            }
        };
    
        ko.validation.init({ insertMessages: true, decorateElement: true, errorClass: "error" });
        ko.applyBindingsWithValidation(new ProductsViewModel());
    });