Skip to content

Instantly share code, notes, and snippets.

@bsredbeard
Created March 4, 2016 16:34
Show Gist options
  • Save bsredbeard/78b5f7ec90083e4a5625 to your computer and use it in GitHub Desktop.
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.
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)">&times;</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