BForms GroupEditor component is designed to provide rich functionality, cross-browser, cross-device and internationalization support for your group editting needs.

The feature set includes drag & drop functionality to add items to groups, AJAX-enabled editing of the group items, creation and filtering of group items. The UI is touch friendly and HTML5 enhanced featuring edit in place.

On the client side, the GroupEditor is deployed as an AMD jQuery widget that supports theming and templates. The styling is done using bootstrap v3 CSS grid system.


Building a Group Editor

In this tutorial, you will learn how to implement the BForms.GroupEditor from scratch. We start by presenting the models we’ll use. Then will begin building the grid to present our data, step by step, adding more functionality as we go. At this step you will have a grid that displays your data, with sorting and pagination capabilities. Next step is to add editing capabilities to our rows by enabling the details for each row. We’ll also implement the Bulk Actions that will help us modify multiple rows at a time.

Initial Project

We will start from an empty MVC Project. The project will be setup to use RequireJS and BForms. To see a guide on how to setup your initial project please follow this link: Setup BForms for ASP.NET MVC

We will work with mocked data that will fake a menu editor. We create Pages, Links and Categories as menu items. We will also have Public Menu, Users Menu and Admin Menu as groups. The menu items can be added to any of the groups or can be restricted to one or more of the groups.

Menu Item Model Class

public class MenuItem
{
    public int Id { get; set; }
    public string DisplayNameLocal { get; set; }
    public string DisplayNameInternational { get; set; }
    public MenuItemTypes MenuItemType { get; set; }
    public string Link { get; set; }
    public Glyphicon? Icon { get; set; }
    public MenuItemVisibility Visibility { get; set; }
}

            

Our mocked data will consist in a list of MenuItem objects added in the BFormsContext Constructor method. Here is an example of a movie record:

New Menu Item

new MenuItem()
{
    Id = 1,
    DisplayNameLocal = "Home",
    DisplayNameInternational = "Home",
    MenuItemType = MenuItemTypes.Page,
    Link = "/Home.html",
    Icon = Glyphicon.Home,
    Visibility = MenuItemVisibility.Any
};


            

The BFormsContext Object will be instantiated in the BaseController class. All our controllers will inherit from BaseController. This way we will have access to the BFormsContext from inside our controllers.

1. Creating the Models

In this step we will create the models needed to build the group editor. In our project let’s create a folder named ‘Models’. Add a new empty class file to this folder named ‘MenuModels.cs’. This file will contain all our grid related models.

The first model will be the RowForm model. This model is used by the goup editor widget to define the editable form for each group item. The group items can have editable properties. In this case we only want to modify the Local and International DisplayName for each menu item.

Menu Row Form Model Class

public class MenuRowFormModel
{
    [Display(Name = "DisplayNameInternational", ResourceType = typeof(Resource))]
    [Required]
    [BsControl(BsControlType.TextBox)]
    public string DisplayNameInternational { get; set; }

    [Display(Name = "DisplayNameLocal", ResourceType = typeof(Resource))]
    [Required]
    [BsControl(BsControlType.TextBox)]
    public string DisplayNameLocal { get; set; }
}
            

Next step is to create the GroupRowModel. This row model is used to create the grid rows for the tabs and groups. This model should contain the readonly properies that you want to display on each row.

Group Row Model Class

public class MenuGroupRowModel : BsEditorGroupItemModel<MenuRowFormModel>
{
    public int Id { get; set; }
    public string DisplayNameLocal { get; set; }
    public string DisplayNameInternational { get; set; }
    public string Permissions { get; set; }
    public string Link { get; set; }
    public Glyphicon? Icon { get; set; }

    public override object GetUniqueID()
    {
        return this.Id;
    }
}
            

If you want to add filter functionality to the tabs, you must include a search model. This model will be used in the AdvancedSearch form to filter the grid rows.

Search Model Class

public class MenuItemSearchModel
{
    public MenuItemSearchModel()
    {
        Visibility = new BsSelectList<MenuItemVisibility?>();
        Visibility.ItemsFromEnum(typeof(MenuItemVisibility));
    }

    [Display(Name = "DisplayName", ResourceType = typeof(Resource))]
    [BsControl(BsControlType.TextBox)]
    public string DisplayName { get; set; }

    [Display(Name = "Link", ResourceType = typeof(Resource))]
    [BsControl(BsControlType.TextBox)]
    public string Link { get; set; }

    [BsControl(BsControlType.RadioButtonList)]
    [Display(Name = "Visibility", ResourceType = typeof(Resource))]
    public BsSelectList<MenuItemVisibility?> Visibility { get; set; }
}
            


Also you can add new menu items for each tab. For this you need to create a NewMenuItem Model. If the tabs have different menu items, for example pages, links and categories, you should create a model for each.

New Model Class

public class MenuItemNewModel
{
    public MenuItemNewModel()
    {
        Visibility = new BsSelectList<MenuItemVisibility>();
        Visibility.ItemsFromEnum(typeof(MenuItemVisibility));
        Visibility.SelectedValues = MenuItemVisibility.Any;

        Icon = new BsSelectList<Glyphicon?>();
        Icon.ItemsFromEnum(typeof(Glyphicon));
    }

    [Required(ErrorMessageResourceName = "RequiredField", ErrorMessageResourceType = typeof(Resource))]
    [BsControl(BsControlType.RadioButtonList)]
    [Display(Name = "Visibility", ResourceType = typeof(Resource))]
    public BsSelectList<MenuItemVisibility> Visibility { get; set; }

    [Required(ErrorMessageResourceName = "RequiredField", ErrorMessageResourceType = typeof(Resource))]
    [Display(Name = "DisplayNameLocal", ResourceType = typeof(Resource))]
    [BsControl(BsControlType.TextBox)]
    public string DisplayNameLocal { get; set; }

    [Required(ErrorMessageResourceName = "RequiredField", ErrorMessageResourceType = typeof(Resource))]
    [Display(Name = "DisplayNameInternational", ResourceType = typeof(Resource))]
    [BsControl(BsControlType.TextBox)]
    public string DisplayNameInternational { get; set; }

    [Required(ErrorMessageResourceName = "RequiredField", ErrorMessageResourceType = typeof(Resource))]
    [Display(Name = "Link", ResourceType = typeof(Resource))]
    [BsControl(BsControlType.TextBox)]
    public string Link { get; set; }

    [Display(Name = "Icon", Prompt = "PromptIcon", ResourceType = typeof(Resource))]
    [BsControl(BsControlType.DropDownList)]
    public BsSelectList<Glyphicon?> Icon { get; set; }

    public virtual MenuItemTypes MenuItemType { get; set; }
}

public sealed class PageNewModel : MenuItemNewModel
{
    public PageNewModel()
        : base()
    {
        MenuItemType = MenuItemTypes.Page;
    }

    public override MenuItemTypes MenuItemType { get; set; }
}

public sealed class CategoryNewModel : MenuItemNewModel
{
    public CategoryNewModel()
        : base()
    {
        MenuItemType = MenuItemTypes.Category;
    }

    public override MenuItemTypes MenuItemType { get; set; }
}

public sealed class CustomLinkNewModel : MenuItemNewModel
{
    public CustomLinkNewModel()
        : base()
    {
        MenuItemType = MenuItemTypes.CustomLink;
    }

    public override MenuItemTypes MenuItemType { get; set; }
}
            


Now we have all we need to create a BsEditorTabModel or a BsEditorGroupModel. We can now define the GroupEditor model. This model is used by the grid widget to populate the columns and the header. The fields can be annotated with the BsEditorTabAttribute and BsEditorGroupAttribute. This attributes can set some properties for the tabs and groups. For example, the ‘Name’ of the first tab is set to 'Pages', and by setting the 'Selected' property to true you can set this tab to be selected on initialization.

Group Editor Model Class

public class GroupEditorModel
{
    [BsEditorTab(Name = "Pages", Id = MenuItemTypes.Page, Selected = true)]
    public BsEditorTabModel<MenuGroupRowModel, MenuItemSearchModel, PageNewModel> Tab1 { get; set; }

    [BsEditorTab(Name = "Custom Links", Id = MenuItemTypes.CustomLink, Selected = false)]
    public BsEditorTabModel<MenuGroupRowModel, MenuItemSearchModel, CustomLinkNewModel> Tab2 { get; set; }

    [BsEditorTab(Name = "Categories", Id = MenuItemTypes.Category, Selected = false)]
    public BsEditorTabModel<MenuGroupRowModel, MenuItemSearchModel, CategoryNewModel> Tab3 { get; set; }

    [BsEditorGroup(Id = MenuTypes.PublicMenu)]
    public BsEditorGroupModel<MenuGroupRowModel> Group1 { get; set; }

    [BsEditorGroup(Id = MenuTypes.UsersMenu)]
    public BsEditorGroupModel<MenuGroupRowModel> Group2 { get; set; }

    [BsEditorGroup(Id = MenuTypes.AdminMenu)]
    public BsEditorGroupModel<MenuGroupRowModel> Group3 { get; set; }
}

            

Here is how the menu group editor will look for this GroupEditorModel:


Last step in model creation is to define the group editor view model. Our model will have only one property because we have only one Group Editor widget. However you can have multiple group editors on the same page.

Group Editor View Model Class

public class GroupEditorViewModel
{
    public GroupEditorModel Editor { get; set; }
}

            

2. Creating the Repository

The Repository for the GroupEditor component won't have anything special. However, because the GroupEditor componet uses the BsGrid component, the repository must include the methods specified in the grid and toolbar tutorial.

This repository will inherit from BsBaseGridRepository and we will have to override a few methods: Query(), OrderQuery() and MapQuery(). These methods will be used to filter and sort the grid.

Query OredrQuery MapQuery

public override IQueryable<MenuItem> Query()
{
    var query = db.MenuItems.AsQueryable();
    return Filter(query);
}

public override IOrderedQueryable<MenuItem> OrderQuery(IQueryable<MenuItem> query)
{
    var orderedQuery = this.orderedQueryBuilder.Order(query, x => x.OrderBy(y => y.Id));
    return orderedQuery;
}

public override IEnumerable<MenuGroupRowModel> MapQuery(IQueryable<MenuItem> query)
{
    return query.Select(MapMenuItem_SampleGroupRowModel);
}
            

You also need to provide the Filter() method and the mapper.

In this example this is how the Filter method looks like:

Filter Query

public IQueryable<MenuItem> Filter(IQueryable<MenuItem> query)
{

    var settings = this.Settings;

    if (settings != null)
    {
        if(settings.TabId != null)
            query = query.Where(x => x.MenuItemType == settings.TabId);

        if (!string.IsNullOrEmpty(Settings.QuickSearch))
        {
            var searched = settings.QuickSearch.ToLower();

            query = query.Where(x => x.DisplayNameLocal.ToLower().Contains(searched) ||
                                                x.DisplayNameInternational.ToLower().Contains(searched) ||
                                                x.Link.ToLower().Contains(searched));
        }
        else if (settings.Search != null)
        {
            #region DisplayName
            if (!string.IsNullOrEmpty(Settings.Search.DisplayName))
            {
                var displayName = Settings.Search.DisplayName.ToLower();
                query = query.Where(x => x.DisplayNameLocal.ToLower().Contains(displayName) ||
                                         x.DisplayNameInternational.ToLower().Contains(displayName));
            }
            #endregion

            #region Link
            if (!string.IsNullOrEmpty(Settings.Search.Link))
            {
                var link = Settings.Search.Link.ToLower();
                query = query.Where(x => x.Link.ToLower().Contains(link));
            }
            #endregion

            #region DisplayName
            if (Settings.Search.Visibility.SelectedValues.HasValue)
            {
                var visibility = Settings.Search.Visibility.SelectedValues.Value;
                query = query.Where(x => x.Visibility == visibility);
            }
            #endregion
        }
    }

    return query;
}
            

The Filter() method reads the Settings object and uses it to create the QuickSearch and the AdvancedSearch query.

The unique thing about this repository is that we need the Tab information in our queries. For this we need to build a new settings object specific to the GroupEditor that will include also the tab info.

This setting object will inherit from BsGridRepositorySettings.

Filter Query

public class GroupEditorSettings : BsGridRepositorySettings<MenuItemSearchModel>
{
    public MenuItemTypes TabId { get; set; }
}

            

The mapper is simple. We use it to convert from MenuItem to MenuGroupRowModel.

Mapper

public Func<MenuItem, MenuGroupRowModel> MapMenuItem_SampleGroupRowModel = x =>
new MenuGroupRowModel
{
    Id = x.Id,
    DisplayNameLocal = x.DisplayNameLocal,
    DisplayNameInternational = x.DisplayNameInternational,
    Link = x.Link,
    Permissions = x.Visibility.ToString(),
    Icon = x.Icon
};

            

3. Adding the Controller

In this step we will create the GroupEditor Controller. This controller will contain the actions needed to display the tabs, grids and the groups. We will display our group editor on the home page so our controller will be named 'HomeController'.

Create a new controller in the ‘Controllers’ folder. This controller will inherit from the BaseController. This is how it will look like:

Home Controller

public class HomeController : BaseController
{
    private readonly MenuRepository repo;

    public HomeController()
    {
        repo = new MenuRepository(Db);
    }

    public ActionResult Index()
    {
        return View();
    }
}

            

The Index() action of this controller will render the view that will contain the group editor. However we have to do some changes before we can send the model the way our view will be expecting. We have to initialize the GroupEditor Model with some initial settings for the tabs, the groups and the search and new forms. Then we’ll use this model to initialize our View Model. The View Model will be send to the view.

Also we have to use RequireJsOptions.Add() method to send some data to the page. The data we need to send is the name of the ajax action that will be used by the grid pager to return other pages. This will be in the form of a Dictionary object that will contain the urls for the GetTab, Save, Search, NewPage actions that we will define later.

This is how our Index Action will look like at the end:

Index Action

public ActionResult Index()
{
    var bsGridSettings = new GroupEditorSettings
    {
        Page = 1,
        PageSize = 5,
        TabId = MenuItemTypes.Page
    };

    var model = new GroupEditorModel()
    {
        Tab1 = new BsEditorTabModel<MenuGroupRowModel, MenuItemSearchModel, PageNewModel>
        {
            Grid = repo.ToBsGridViewModel(bsGridSettings),
            Search = repo.GetSearchForm(),
            New = repo.GetNewPageForm()
        },

        Group1 = new BsEditorGroupModel<MenuGroupRowModel>
        {
            Items = new List<MenuGroupRowModel>()
        },

        Group2 = new BsEditorGroupModel<MenuGroupRowModel>
        {
            Items = new List<MenuGroupRowModel>()
        },

        Group3 = new BsEditorGroupModel<MenuGroupRowModel>
        {
            Items = new List<MenuGroupRowModel>()
        }
    };

    var viewModel = new GroupEditorViewModel
    {
        Editor = model
    };

    var options = new
    {
        getTabUrl = Url.Action("GetTab"),
        save = Url.Action("Save"),
        advancedSearchUrl = Url.Action("Search"),
        addUrl = Url.Action("NewPage")
    };

    RequireJsOptions.Add("index", options);

    return View(viewModel);
}
            

As you can see in the GroupEditorModel we only initialize the first tab. We do this because this is the tab that has the Selected property set to true.

We initialize the other tabs on the user request with the help of the GetTab() action.

Next we have to implement the RenderTab() nonaction that will be used in the GetTab(), Search and NewPage actions to render the requested tabs.

Render Tab Action

[NonAction]
public string RenderTab(GroupEditorSettings settings, out int count)
{
    var html = string.Empty;
    count = 0;

    var model = new GroupEditorModel();

    switch (settings.TabId)
    {
        case MenuItemTypes.Page:

            var grid1 = repo.ToBsGridViewModel(settings, out count);

            model.Tab1 = new BsEditorTabModel<MenuGroupRowModel, MenuItemSearchModel, PageNewModel>
            {
                Grid = grid1,
                Search = repo.GetSearchForm(),
                New = repo.GetNewPageForm()
            };
            break;

        case MenuItemTypes.CustomLink:

            var grid2 = repo.ToBsGridViewModel(settings, out count);

            model.Tab2 = new BsEditorTabModel<MenuGroupRowModel, MenuItemSearchModel, CustomLinkNewModel>
            {
                Grid = grid2,
                Search = repo.GetSearchForm(),
                New = repo.GetNewLinkForm()
            };
            break;

        case MenuItemTypes.Category:

            var grid3 = repo.ToBsGridViewModel(settings, out count);

            model.Tab3 = new BsEditorTabModel<MenuGroupRowModel, MenuItemSearchModel, CategoryNewModel>
            {
                Grid = grid3,
                Search = repo.GetSearchForm(),
                New = repo.GetNewCategoryForm()
            };
            break;
    }

    var viewModel = new GroupEditorViewModel()
    {
        Editor = model
    };

    html = this.BsRenderPartialView("_Editors", viewModel);

    return html;
}
            

After we implement these actions, our controller is ready. Next we have to create the views.

4. Creating the Views

In this step we will create the GroupEditor Views. The _Editors View will be implemented as a partial view rendered inside the Index View.

Let’s first create the Index View. To do this right click on the Index() Action in the Controller and choose ‘Add View’.

This is how our index.cshtml should look like:

Index View

@model Menu.Models.GroupEditorViewModel

<style type="text/css">
    body {
        max-width: 1400px;
        margin: 50px auto;
    }

    .ui-effects-transfer {
        border: 1px dotted black;
    }
</style>

<div class="grid_container">

    @Html.Partial("_Editors", Model)

</div>
            

This view is just a container for the group editor. Let’s now create the _Editors partial view.

Here we use the BsGroupEditorFor() html helper to create our group editor. This helper returns a BsEditorHtmlBuilder used to build and configure the tabs, groups and forms for the GroupEditor.

This is how the _Editors partial view should look:

Editors View

@model Menu.Models.GroupEditorViewModel
@using BForms.Html
@using Menu.Mock

@{
    var builder = Html.BsGroupEditorFor(x => x.Editor);

    builder.ConfigureTabs(cfg =>
    {
        var tab1 = cfg.For(x => x.Tab1)
            .Template(x => x.Grid, "_TabItem")
            .Template(x => x.Search, "~/Views/Home/_Search.cshtml")
            .Template(x => x.New, "~/Views/Home/_New.cshtml")
            .ConnectsWith(MenuTypes.PublicMenu, MenuTypes.UsersMenu, MenuTypes.AdminMenu);

        var tab2 = cfg.For(x => x.Tab2)
            .Template(x => x.Grid, "_TabItem")
            .ConnectsWith(MenuTypes.PublicMenu);

        var tab3 = cfg.For(x => x.Tab3)
            .Template(x => x.Grid, "_TabItem")
            .ConnectsWith(MenuTypes.UsersMenu);


        tab1.BulkMove = false;
        tab1.Toolbar.QuickSearch = true;
        tab2.Toolbar.QuickSearch = true;
        tab3.Toolbar.QuickSearch = true;

        cfg.Title = "Menu Items";

    })
        .ConfigureGroups(cfg =>
        {
            cfg.For(x => x.Group1)
                .DisplayText("Adauga la Public Menu")
                .DisplayName("Public Menu (FrontEnd)")
                .Template(x => x.Items, "_GroupItem");

            cfg.For(x => x.Group2)
                .DisplayText("Adauga la Users Menu")
                .DisplayName("Users Menu (Authentication Required)")
                .Template(x => x.Items, "_GroupItem");

            cfg.For(x => x.Group3)
                .DisplayText("Adauga la Admin Menu")
                .DisplayName("Admin Menu (BackEnd)")
                .Template(x => x.Items, "_GroupItem");

            cfg.Title = "Menus";
        })
        .IgnoreAjaxRequest(false);

    ViewData["builder"] = builder;
}

@if (Model.Editor != null)
{
    Html.Partial("_GroupEditor", Model.Editor);
}

<script id="js-groupItem" type="text/x-icanhaz">

    <span>{{DisplayNameInternational}}</span>

</script>

            

In this partial view you can set the properties for the tabs and groups. For example you can set the partial view templates used for the Search and New forms or you can specify if a tab has QuickSearch functionality.

For a complete list you can read the GroupEditor documentation.

The _Editors view uses the _GroupEditor partial view to render the GroupEditor widget.

Group Editor View

@model Menu.Models.GroupEditorModel
@using BForms.Html
@using Menu.Models
@using BForms.Editor

@{
    var builder = (BsEditorHtmlBuilder<GroupEditorModel>)ViewData["builder"];
}

@(Html.BsGroupEditorFor(Model, builder)
      .HtmlAttributes(new Dictionary<string, object>() { { "id", "myGroupEditor" } })
)

            

We also need a partial view for the TabItem and one for the GroupItem. These views will be used to render the tabs grid rows and the groups grid rows.

Group Item View

@model Menu.Models.MenuGroupRowModel

<span>@Model.DisplayNameInternational</span>

            


Tab Item View

@using BForms.Html
@model Menu.Models.MenuGroupRowModel


<dl class="to-left">
    <dt>Name:</dt>
    <dd>@if (Model.Icon.HasValue)
        {
            @Html.BsGlyphicon(Model.Icon.Value);
        } @Model.DisplayNameInternational</dd>
    
    <dt>Link:</dt>
    <dd>@Model.Link</dd>

    <dt>Permissions:</dt>
    <dd>@Model.Permissions</dd>

</dl>
            


5. Initializing the GroupEditor Widget

In this step we will create the javascript file that will be executed when the index page loads. There are two ways of working with the BForms JS components. One way is to use RequireJS.NET and the other is to reference the js files directly from the ~/Scripts/BForms/Bundles/js folder. In this example we’ll use the RequireJS way. For this let’s create the folder structure that the RequireJs expects for our views.

Javascript Folder Structure


Inside the Scripts folder create the following folder structure corresponding to our Home Controller, Index page: Controllers/Root/Home/home-index.js

This is how the javascript file will look:

Home Index Js

require([
        'jquery',
        'bforms-namespace',
        'bforms-groupEditor',
        'bforms-initUI',
        'history-js',
        'bforms-ajax'
], function () {
    var homeIndex = function (options) {
        this.options = $.extend(true, {}, options);
        this.init();
    };

    homeIndex.prototype.init = function () {
        $('#myGroupEditor').bsGroupEditor({
            getTabUrl: this.options.getTabUrl,
            buildDragHelper: function (model, tabId, connectsWith) {
                return $('<div class="col-lg-6 col-md-6 bs-itemContent" style="z-index:999"><span>' + model.DisplayNameInternational + '</span></div>');
            },
            buildGroupItem: $.proxy(function (model, group, tabId, objId) {
                return $('<span>' + model.DisplayNameInternational + '</span>');
            }, this),
            validateMove: function (model, tabId, $group) {
                if (model.Permissions == "PublicArea" && ($group.data('groupid') == 2 || $group.data('groupid') == 3)) return false;
                else if (model.Permissions == "UsersArea" && ($group.data('groupid') == 1 || $group.data('groupid') == 3)) return false;
                else if (model.Permissions == "AdminArea" && ($group.data('groupid') == 1 || $group.data('groupid') == 2)) return false;
            },
            onSaveSuccess: $.proxy(function () {
            }, this),
            initEditorForm: $.proxy(function ($form, uid, tabModel) {
                if (uid == "1.Search") {
                   this._initSearchForm($form, uid);
                } else if (uid == "1.New") {
                   this._initAddForm($form, uid);
                }
            }, this),
            validation: {
                required: {
                    unobtrusive: true,
                    message: "Please add at least an item."
                }
            }
        });
    };

    $(document).ready(function () {
        var page = new homeIndex(window.requireConfig.pageOptions.index);
    });
});
            

In this javascript file we require bforms-groupEditor, jquery and a few other files.

We use the jQuery document ready method to initialize the page and to get the options send from the HomeController.

The options sent from the controller via RequireJs will extend and override the default settings.

On the page initialization we find the GroupEditor by the id attribute and initialize it by calling the bsGroupEditor() constructor method.

We can define the html for the dragged and dropped elements in the buildDragHelper and buildGroupItem parameters.

We use the validateMove parameter to provide a function that returns false if a condition is met. If the function returns false, then the move is not validated and the GroupItem will not be added to the Group.

We can also implement a Save method to save the current sate of the groups. If the save is successful then the onSaveSuccess handler is called.

Now we need to initialize the search form and the new form in the initEditorForm handler.

In the Search form we need to initialize the Search and the Reset buttons.

Initialize Search Form

homeIndex.prototype._initSearchForm = function($form, uid) {
    $form.bsForm({
        uniqueName: 'searchForm',
        prefix: 'prefix' + uid + '.',
        actions: [
        {
            name: 'search',
            selector: '.js-btn-search',
            actionUrl: this.options.advancedSearchUrl,
            parse: true,
            handler: $.proxy(function (formData, response) {
                $('#myGroupEditor').bsGroupEditor('setTabContent', response.Html);
            }, this)
        }, {
            name: 'reset',
            selector: '.js-btn-reset',
            handler: $.proxy(function () {
                $form.bsForm('reset');
            }, this)
        }]
    });
};

            

In the New form we need to initialize the Add and the Reset buttons.

Initialize New Form

homeIndex.prototype._initAddForm = function ($form, uid) {
    $form.bsForm({
        uniqueName: 'newForm',
        prefix: 'prefix' + uid + '.',
        actions: [
        {
            name: 'add',
            selector: '.js-btn-save',
            actionUrl: this.options.addUrl,
            parse: true,
            validate: true,
            handler: $.proxy(function (formData, response) {
                var $row = $(response.Row).find('.bs-tabItem');
                $('#myGroupEditor').bsGroupEditor('addTabItem', $row);
            }, this)
        }, {
            name: 'reset',
            selector: '.js-btn-reset',
            handler: $.proxy(function () {
                $form.bsForm('reset');
            }, this)
        }]
    });
};
            

On initialization we can set different properties for the forms. For example we can specify if the form should pe parsed or validated.

Now we have a functional GroupEditor. We can switch tabs, add new MenuItems to the tabs, use QuickSearch and AdvancedSearch to filter the MenuItems, drag and drop MenuItems to the Menus and validate if the move is legal.

Drag And Drop