Created
March 4, 2016 16:34
-
-
Save bsredbeard/78b5f7ec90083e4a5625 to your computer and use it in GitHub Desktop.
A non-drag & drop list allowing manual sorting. Currently relies on Font Awesome for up/down icons. JsDoc/NgDoc may be incorrect because nothing makes those tools tolerable.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import angular from 'angular'; | |
let listTemplate = `<ul ng-show="sortable.data.length"> | |
<li class="sortable-entry" ng-repeat="entry in sortable.data"> | |
<button ng-disabled="$first" ng-click="sortable.move($index, -1)"><i class="fa fa-arrow-circle-up"></i></button> | |
<button ng-disabled="$last" ng-click="sortable.move($index, 1)"><i class="fa fa-arrow-circle-down"></i></button> | |
<span class="entry-text" sortable-item></span> | |
<button ng-click="sortable.remove($index)">×</button> | |
</li> | |
</ul>`; | |
/** | |
* @class sortableController | |
* @param $scope {$rootScope.Scope} - the current scope of the sortable list directive | |
* @param $transclude {Function} - the pre-bound transclusion function | |
* @param $q - The angular $q service | |
* @description | |
* This controller provides functions to render the model list and manage the ordering | |
* and removal of items within that list. | |
*/ | |
sortableController.$inject = ['$scope', '$transclude', '$q']; | |
function sortableController($scope, $transclude, $q){ | |
/** | |
* @member data {Object[]} - The data managed by the controller | |
*/ | |
this.data = [] | |
/** | |
* @member onChanged {Function} - The function called when the model has been changed externally | |
*/ | |
this.onChanged = () => {}; | |
/** | |
* @member setModel {Function} - api method to bind this controller to a specific ngModel controller | |
*/ | |
this.setModel = (ngModel) => { | |
//provide a callback to the model's render function | |
ngModel.$render = () => { | |
this.data = ngModel.$viewValue; | |
}; | |
this.onChanged = () => { | |
//notify the model that the data in this list has changed | |
ngModel.$setViewValue(this.data); | |
//notify the model that the state of the associated model data is now dirty | |
ngModel.$setDirty(); | |
}; | |
}; | |
/** | |
* @member renderTemplate {Function} - api method to render items with the transcluded template. Returns a Promise({ content, scope }) | |
*/ | |
this.renderTemplate = (element, item) => { | |
let deferred = $q.defer(); | |
//create a new scope as a sibling to this control's scope | |
let childScope = $scope.$parent.$new(false); | |
//inject the $item scope variable | |
childScope.$item = item; | |
//call the transclude function with the new scope | |
$transclude(childScope,(clone, scope) => { | |
//empty out the child item of any contents it may have | |
element.empty(); | |
//append the new clone to the child item element | |
element.append(clone); | |
//pass the cloned element and scope back out to the child-item | |
//so that they may be properly tracked in the angular lifecycle | |
deferred.resolve({ content: clone, scope: scope }); | |
}); | |
return deferred.promise; | |
} | |
/** | |
* @member move {Function} - view method to move items higher and lower in the data stack | |
*/ | |
this.move = (index, offset) => { | |
let data = this.data || []; | |
if(index >= 0){ | |
let desiredIndex = index + offset; | |
if(desiredIndex >= 0 && desiredIndex < data.length){ | |
//make sure to access the item in the array returned out of splice | |
let item = data.splice(index, 1)[0]; | |
//splice the removed item back into the array at the appropriate index | |
data.splice(desiredIndex, 0, item); | |
this.onChanged(); | |
} | |
} | |
}; | |
/** | |
* @member remove {Function} - view method to remove an item from the data stack | |
*/ | |
this.remove = (index) => { | |
let data = this.data || []; | |
data.splice(index, 1); | |
this.onChanged(); | |
}; | |
} | |
/** | |
* @ngdoc directive | |
* @module utilities.sortableListModule | |
* @name sortableList | |
* @restrict E | |
* @scope | |
* @description | |
* Provides a manually sortable list of items in the attached ngModel. Items | |
* can be sorted up, down, and removed from the list. If the model is updated | |
* outside of the component (add/remove/reorder items), the changes will be | |
* picked up automatically. | |
* @param ngModel {Object[]} - the data backing this sortableList | |
*/ | |
function sortableListDirective(){ | |
return { | |
restrict: 'E', | |
scope: true, | |
controller: sortableController, | |
controllerAs: 'sortable', | |
require: 'ngModel', | |
template: listTemplate, | |
transclude: true, | |
link: function(scope, elem, attr, ngModel){ | |
scope.sortable.setModel(ngModel); | |
} | |
}; | |
} | |
/** | |
* @ngdoc directive | |
* @name sortableItem | |
* @description Transcludes the item template into the sortable list, exposing the $item scope variable for use in the template. | |
* @restrict A | |
*/ | |
function sortableItemDirective(){ | |
return { | |
restrict: 'A', | |
require: '^sortableList', | |
scope: false, | |
link: (scope, element, attr, sortable) => { | |
let myRender = null; | |
//call sortableList controller's renderTemplate function | |
//store the results into the myRender scoped variable | |
sortable | |
.renderTemplate(element, scope.entry) | |
.then((result) => myRender = result); | |
//watch for destruction of this item's scope | |
scope.$on('$destroy', () => { | |
if(myRender){ | |
//destroy the transclusion content | |
if(myRender.content) | |
myRender.content.remove(); | |
//destroy the transclusion scope | |
if(myRender.scope) | |
myRender.scope.$destroy(); | |
} | |
}); | |
} | |
}; | |
} | |
export default angular.module('utilities.sortableListModule', []) | |
.directive('sortableList', sortableListDirective) | |
.directive('sortableItem', sortableItemDirective); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment