AngularUI – benutzerdefinierte Direktive für eine Multiselect Dropdownliste im Bootstrap Style inkl. AngularJS Form Validation


Leider war ich vergeblich auf der Suche nach einer brauchbaren Lösung für eine Multiselect Dropdownliste für AngularJS im Twitter Bootstrap Style die meinen Anforderungen entspricht. Daher habe ich AngularUI als Vorlage für meine “eigene” Mutliselect Dropdownliste genommen und von AngularUI die Dropdown Komponente als Grundlage verwendet. Denn die Dropdownkomponente bietet schon alle wichtigen Grundlagen für eine Multiselect Dropdownliste. Das ganze habe ich dann in eine eigene Direktive verpackt und kann entsprechend in AngularJS verwendet werden.

Voraussetzungen und was die Multiselect Dropdownliste alles können soll:

  • Benötigt wird AngularJS und  AngularUI als Grundlage (daher wird auch kein jQuery benötigt!)
  • Das Basic Styling übernimmt Twitter Bootstrap, aber die Icons verwende ich von Font Awesome
  • Ein paar Custom Styles um die Dropdown Komponenten entsprechend anzupassen für die Multiselect Liste
  • Automatisches Anpassen der Breite des Controls an z.B. “col-md-12” von Bootstrap
  • eine eigene Direktive inkl. Template für die Multiselect Liste
  • Integrierte Form Validation für das “required” Attribut sowie “$dirty” und “$pristine” Modus des Formulars in dem die Liste verwendet wird
  • Die Möglichkeit alle Elemente an und abzuwählen
  • Über Änderungen der Auswahl “bescheid” geben wie bei “ng-change”

1. Custom Styles

/* Multiselect Dropdown Styles - Start */
.sq-dropdown .dropdown-multiselect {
	max-height: 300px; 
	overflow-y: auto; 
	overflow-x: hidden;
	width: 100%;
}

.sq-dropdown .ddl-icon {
    display: inline-block;
    margin-top: 2.5px;
    position: absolute;
    right: 15px;
}

.sq-dropdown.bootstrap-select:not([class*="span"]):not([class*="col-"]):not([class*="form-control"]), .bootstrap-select > .btn {
    width: 100%;
}

.sq-dropdown.bootstrap-select.btn-group .btn .filter-option {
    left: 12px;
    overflow: hidden;
    position: absolute;
	text-overflow:ellipsis;
    right: 25px;
    text-align: left;
}

.sq-dropdown.bootstrap-select.btn-group .btn .caret {
    margin-top: -2px;
    position: absolute;
    right: 12px;
    top: 50%;
    vertical-align: middle;
}
/* Multiselect Dropdown Styles - Ende */

2. Benutzerdefinierte Direktive

wichtig ist hier, das die Liste die angezeigt werden soll, die Properties “Selected” und “Text” enthält. Denn das Property “Text” wird angezeigt und in “Selected” merken wir uns welches Element ausgewählt wurde. Die Funktionen “validators” und das $watch für “listEntries” wird beides benötigt, damit das Control mit der Form Validierung von AngularJS funktioniert. Denn wenn sich die Auswahl ändert oder das “required” Attribut gesetzt wird, kann man über die Standard AngularJS Methoden prüfen ob die Eingaben valide sind z.B. “frm.$valid” oder “frm.$dirty” oder auch “frm.$pristine” oder auch direkt das Control ansprechen. Damit die Validierung überhaupt verwendet werden kann, muss auch “required: ‘ngModel’ gesetzt, werden denn nur dann steht uns das Model in der Link Funktion zur Verfügung und wir können auf “$setValidity” zugreifen für die “required” Validierung.

Damit das alles Funktioniert, darf man natürlich nicht vergessen, dem Control das “name” Attribut zu verpassen inkl. eines passenden Namens.

angular.module("select.directives", [])
    /*
    Direktive zum Anzeigen einer MultiSelect Box als Dropdownliste. Achtung Es wird hier ebenfalls AnguluarUi benötigt!
    Es werden die Properties "Selected" und "Text" benötigt in der übergebenen Anzeigeliste, damit die Multiselect Liste richtig funktioniert.

    Verwendung:

    html ...
    <div sq-multiselect ng-model="ModelData.Listendaten" value-changed="LogChange(selectedValue)" required></div>
    ... html
    */
    .directive("sqMultiselect", function () {
        return {
            restrict: 'AE',
            replace: true,
            require: 'ngModel',
            scope: {
                listEntries: "=ngModel", //unsere Liste mit den Einträgen
                valueChanged: "&" //Funktion die aufgerufen wird, wenn sich der Wert geändert hat.
            },
            //ACHTUNG funktioniert nur in Verbindung mit AngularUi und dem dort enthaltenen "dropdown" Control!
            //dies muss auch mit in den Abhängigkeiten geladen werden mit "ui.bootstrap".
            template: '<div class="sq-dropdown bootstrap-select btn-group" dropdown>' +
                '<button type="button" class="btn btn-default dropdown-toggle" title="{{Fct.selectedString(listEntries)}}">' +
                '<span class="filter-option" ng-bind="Fct.selectedString(listEntries)"></span>&nbsp;' +
                '<span class="caret"></span></button>' +
                '<ul class="dropdown-menu dropdown-multiselect" role="menu">' +
                '<li ng-show="listEntries.length > 0"><a href="" ng-click="Fct.selectAll(listEntries)">' +
                '<span style="margin-right: 30px;"><strong ng-bind="Locals.selectAll"></strong></span>' +
                '<i ng-show="Fct.allSelected(listEntries)" class="fa fa-check ddl-icon"></i>' +
                '</a>' +
                '</li>' +
                '<li ng-repeat="item in listEntries">' +
                '<a ng-click="item.Selected = !item.Selected" href="">' +
                '<span style="margin-right: 30px;" ng-bind="item.Text"></span> ' +
                '<i ng-show="item.Selected" class="fa fa-check ddl-icon"></i>' +
                '</a>' +
                '</li>' +
                '</ul>' +
                '</div>',
            link: function(scope, elem, attr, ngModel) {
                scope.Fct = {};
                scope.Locals = {};
                scope.Locals.selectAll = "Alle auswählen";

                //String zusammenbauen für die ausgewählten Elemente
                scope.Fct.selectedString = function(data) {
                    var anz = "-- Bitte wählen --", count = 0;
                    if (data !== undefined && data !== null) {
                        for (var i = 0; i < data.length; i++) {
                            //Nur wenn das Element auch ausgewählt wurde anzeigen.
                            if (data[i].Selected) {
                                if (count === 0) {
                                    anz = "";
                                }
                                if (anz.length > 0) {
                                    anz += "; ";
                                }
                                //Text zusammenbauen
                                anz += data[i].Text;
                                count++;
                            }
                        }
                    }
                    return anz;
                };

                //Funktion die alle Werte auswählt und wenn alle Werte ausgewählt wurden, dann
                //werden alle wieder abgewählt
                scope.Fct.selectAll = function(data) {
                    var select = true;
                    //Wenn alle ausgewählt sind, dann alle abwählen.
                    if (scope.Fct.allSelected(data)) {
                        select = false;
                    }
                    if (data !== undefined && data !== null) {
                        for (var i = 0; i < data.length; i++) {
                            data[i].Selected = select;
                        }
                    }
                };

                //Gibt zurück ob alle Werte in der Liste ausgewählt wurden.
                scope.Fct.allSelected = function(data) {
                    if (data !== undefined && data !== null) {
                        for (var i = 0; i < data.length; i++) {
                            if (!data[i].Selected) {
                                return false;
                            }
                        }
                    }
                    return true;
                };

                //Gibt True zurück, wenn kein Wert in der Liste ausgewählt wurde.
                scope.Fct.noneSelected = function(data) {
                    if (data !== undefined && data !== null) {
                        for (var i = 0; i < data.length; i++) {
                            if (data[i].Selected) {
                                return false;
                            }
                        }
                    }
                    return true;
                }

                //Custom Validation einbinden für Required - Das value ist hier ngModel was immer übergeben wird.
                //vom $parsers bzw. $formatters
                var validator = function (value) {
                    //Einbinden einer Required Validierung, es wird das "required" Attribut unterstützt
                    //und nur wenn auch ein Wert im ngModel (selectedValue) gesetzt ist, ist die Validation True!
                    if (attr.required !== undefined) {
                        if (scope.Fct.noneSelected(value)) {
                            ngModel.$setValidity("required", false);
                        } else {
                            ngModel.$setValidity("required", true);
                        }
                    }
                    //WICHTIG den Eingabewert auch zurückgeben, sonst landet im Model kein Wert mehr!
                    return value;
                }

                //Unseren Validator den passenden Listen hinzufügen.
                ngModel.$parsers.unshift(validator);    //view-to-model direction
                ngModel.$formatters.unshift(validator); //model-to-view direction

                //Prüfen ob sich die Liste ändert
                //http://www.bennadel.com/blog/2566-scope-watch-vs-watchcollection-in-angularjs.htm
                scope.$watch('listEntries', function (newVal, oldVal) {
                    if (newVal !== undefined && newVal !== oldVal) {
                        //Wenn die Werte unterschiedlich sind, dann ebenfalls die Methode für Change aufrufen!
                        scope.valueChanged();
                        //Wenn der Wert einfach nur "geändert" wird, muss auch $dirty und $pristine "gesetzt" werden
                        //dies passiert automatisch über "ngModel.$setViewValue(ngModel.$viewValue);"
                        ngModel.$setViewValue(ngModel.$viewValue);
                    }
                }, true); //TRUE NOTWENDIG!
            }
        };
    })

3. Verwendung

JavaScript

var app = angular.module("app.ddl", ["ui.bootstrap", "select.directives"]);

app.controller("ddlCtrl", function ($scope, $log) {
    $scope.multiSelectItems = {
        users: [
            {
                Text: "Ray Jones",
                Selected: true,
                joined: 2012
            },
            {
                Text: "Lana Lane",
                Selected: false,
                joined: 2001
            },
            {
                Text: "Titus Jonas",
                Selected: true,
                joined: 1995
            },
            {
                Text: "Serena Wuh",
                Selected: true,
                joined: 1990
            }
        ]
    };

    $scope.Fct = {};
    $scope.Fct.LogChange = function(entry) {
        $log.log(entry);
    }

});

HTML

<div ng-app="app.ddl" ng-controller="ddlCtrl">
    <form name="frm">
        <div class="row">
            <div class="col-md-12"><strong>Multiselect mit AngularUi</strong></div>
        </div>
        <div class="row">
            <div class="col-md-3">
                <div required name="multiPersonSelect" sq-multiselect ng-model="multiSelectItems.users" value-changed="Fct.LogChange(multiSelectItems.users)"></div>
            </div>
        </div>
    </form>
</div>

 

Das ganze kann natürlich beliebig erweitert werden. Auf meinem CodePlex Account findet Ihr die Quellcodes und eine funktionierende MVC Anwendung. Außerdem ist dort auch noch eine Implementierung einer einfachen Select Dropdown Liste im Bootstrap Style wie die Multiselect Liste vorhanden.

Ich weiß leider nicht ob meine Umsetzung dem Standard entspricht, denn ich habe leider kein ähnliches Control gefunden, welches ebenfalls die Form Validation von AngularJS unterstützt. Über Vorschläge und Hinweise freue ich mich sehr :-)

Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s