Skip to content

Commit

Permalink
Merge pull request #505 from marmelab/select2
Browse files Browse the repository at this point in the history
[RFR] Use UI-Select for Choice and Choices fields
  • Loading branch information
ThieryMichel committed Jun 15, 2015
2 parents 388cfc6 + 670933c commit 43c650d
Show file tree
Hide file tree
Showing 19 changed files with 353 additions and 293 deletions.
2 changes: 1 addition & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ module.exports = function (grunt) {
karma: {
unit: {
configFile: 'src/javascripts/test/karma.conf.js',
singleRun: true
singleRun: process.env.KARMA_SINGLE_RUN !== 'false'
}
},
protractor: {
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,12 @@ ng-admin has unit tests (powered by karma) and end to end tests (powered by prot
make test
```

**Tip:** if you are working on Karma tests, you can prevent from relaunching the whole process by disabling single-run:

```
KARMA_SINGLE_RUN=false grunt karma:unit
```

## License

ng-admin is licensed under the [MIT Licence](LICENSE), courtesy of [marmelab](http://marmelab.com).
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"style-loader": "^0.12.2",
"superagent": "^0.18.2",
"textangular": "^1.3.11",
"ui-select": "^0.11.2",
"underscore": "^1.8.3",
"url-loader": "^0.5.5",
"webpack": "^1.9.4",
Expand Down
6 changes: 5 additions & 1 deletion src/javascripts/ng-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ var Factory = require('admin-config/lib/Factory');
var factory = angular.module('AdminDescriptionModule', []);
factory.constant('AdminDescription', new Factory());

var ngadmin = angular.module('ng-admin', ['main', 'crud', 'AdminDescriptionModule']);
var ngadmin = angular.module('ng-admin', ['ui.select', 'main', 'crud', 'AdminDescriptionModule']);
ngadmin.config(function(NgAdminConfigurationProvider, AdminDescription) {
NgAdminConfigurationProvider.setAdminDescription(AdminDescription);
});

ngadmin.config(function(uiSelectConfig) {
uiSelectConfig.theme = 'bootstrap';
});
1 change: 1 addition & 0 deletions src/javascripts/ng-admin/Crud/CrudModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ CrudModule.directive('maCheckboxField', require('./field/maCheckboxField'));
CrudModule.directive('maTextField', require('./field/maTextField'));
CrudModule.directive('maWysiwygField', require('./field/maWysiwygField'));
CrudModule.directive('maTemplateField', require('./field/maTemplateField'));
CrudModule.directive('uiSelectRequired', require('./field/uiSelectRequired'));

CrudModule.provider('FieldViewConfiguration', require('./fieldView/FieldViewConfiguration'));

Expand Down
88 changes: 45 additions & 43 deletions src/javascripts/ng-admin/Crud/field/maChoiceField.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,50 @@
/*global define*/
function maChoiceField($compile) {
return {
scope: {
'field': '&',
'value': '=',
'entry': '=?',
'datastore': '&?'
},
restrict: 'E',
compile: function() {
return {
pre: function(scope, element) {
var field = scope.field();
scope.name = field.name();
scope.v = field.validation();

define(function (require) {
'use strict';
var template = `
<ui-select ng-model="$parent.value" ng-required="v.required" id="{{ name }}" name="{{ name }}">
<ui-select-match allow-clear="{{ !v.required }}" placeholder="Filter values">{{ $select.selected.label }}</ui-select-match>
<ui-select-choices repeat="item.value as item in getChoices(entry) | filter: {label: $select.search}">
{{ item.label }}
</ui-select-choices>
</ui-select>`;

/**
* Edition field for an element in a list - a select.
*
* @example <ma-choice-field entry="entry" field="field" value="value"></ma-choice-field>
*/
function maChoiceField() {
return {
scope: {
'field': '&',
'value': '=',
'entry': '=?',
'datastore': '&?'
},
restrict: 'E',
link: function(scope, element) {
var field = scope.field();
scope.name = field.name();
scope.v = field.validation();
var choices;
if (field.type() === 'reference' || field.type() === 'reference_many') {
choices = scope.datastore().getChoices(field);
} else {
choices = field.choices();
}
scope.getChoices = typeof(choices) === 'function' ? choices : function() { return choices; };
var select = element.children()[0];
var attributes = field.attributes();
for (var name in attributes) {
select[name] = attributes[name];
var choices;
if (field.type() === 'reference' || field.type() === 'reference_many') {
choices = scope.datastore().getChoices(field);
} else {
choices = field.choices();
}
scope.getChoices = typeof(choices) === 'function' ? choices : function() { return choices; };
element.html(template);

var select = element.children()[0];
var attributes = field.attributes();
for (var name in attributes) {
select.setAttribute(name, attributes[name]);
}

$compile(element.contents())(scope);
}
},
template:
'<select ng-model="value" ng-required="v.required" id="{{ name }}" name="{{ name }}" class="form-control"' +
' ng-options="item.value as item.label for item in getChoices(entry)">' +
'<option value="">-- select a value --</option>' +
'</select>'
};
}
};
}
};
}

maChoiceField.$inject = ['$compile'];

maChoiceField.$inject = [];
module.exports = maChoiceField;

return maChoiceField;
});
93 changes: 51 additions & 42 deletions src/javascripts/ng-admin/Crud/field/maChoicesField.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,56 @@
/*global define*/

define(function (require) {
/**
* Edition field for a selection of elements in a list - a multiple select.
*
* @example <ma-choices-field entry="entry" field="field" value="value"></ma-choices-field>
*/
function maChoicesField($compile) {
'use strict';

/**
* Edition field for a selection of elements in a list - a multiple select.
*
* @example <ma-choices-field entry="entry" field="field" value="value"></ma-choices-field>
*/
function maChoicesField() {
return {
scope: {
'field': '&',
'value': '=',
'entry': '=?',
'datastore': '&?'
},
restrict: 'E',
link: function(scope, element) {
var field = scope.field();
scope.name = field.name();
scope.v = field.validation();
var choices;
if (field.type() === 'reference' || field.type() === 'reference_many') {
choices = scope.datastore().getChoices(field);
} else {
choices = field.choices();
}
scope.getChoices = typeof(choices) === 'function' ? choices : function() { return choices; };
var select = element.children()[0];
var attributes = field.attributes();
for (var name in attributes) {
select[name] = attributes[name];
return {
scope: {
'field': '&',
'value': '=',
'entry': '=?',
'datastore': '&?'
},
restrict: 'E',
compile: function() {
return {
pre: function(scope, element) {
var field = scope.field();
scope.name = field.name();
scope.v = field.validation();

var template = `
<ui-select ${scope.v.required ? 'ui-select-required' : ''} multiple ng-model="$parent.value" ng-required="v.required" id="{{ name }}" name="{{ name }}">
<ui-select-match placeholder="Filter values">{{ $item.label }}</ui-select-match>
<ui-select-choices repeat="item.value as item in getChoices(entry) | filter: {label: $select.search}">
{{ item.label }}
</ui-select-choices>
</ui-select>`;

var choices;
if (field.type() === 'reference' || field.type() === 'reference_many') {
choices = scope.datastore().getChoices(field);
} else {
choices = field.choices();
}
scope.getChoices = typeof(choices) === 'function' ? choices : function() { return choices; };
element.html(template);

var select = element.children()[0];
var attributes = field.attributes();
for (var name in attributes) {
select.setAttribute(name, attributes[name]);
}

$compile(element.contents())(scope);
}
},
template:
'<select multiple ng-model="value" id="{{ name }}" name="{{ name }}" class="form-control" ng-required="v.required"' +
' ng-options="item.value as item.label for item in getChoices(entry)">' +
'</select>'
};
}
};
}
};
}

maChoicesField.$inject = [];
maChoicesField.$inject = ['$compile'];

return maChoicesField;
});
module.exports = maChoicesField;
30 changes: 30 additions & 0 deletions src/javascripts/ng-admin/Crud/field/uiSelectRequired.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Directive created to fix a bug with ui-select and multiple required values.
* @see https://github.com/angular-ui/ui-select/issues/258
*/
function uiSelectRequired() {
'use strict';

return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$validators.uiSelectRequired = function(modelValue, viewValue) {
var determineVal;
if (angular.isArray(modelValue)) {
determineVal = modelValue;
} else if (angular.isArray(viewValue)) {
determineVal = viewValue;
} else {
return false;
}

return determineVal.length > 0;
};
}
};
}

uiSelectRequired.$inject = [];

module.exports = uiSelectRequired;
28 changes: 10 additions & 18 deletions src/javascripts/ng-admin/Crud/fieldView/ChoiceFieldView.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
define(function(require) {
"use strict";

function getReadWidget() {
module.exports = {
getReadWidget: function() {
return '<ma-string-column value="::field.getLabelForChoice(entry.values[field.name()], entry)"></ma-string-column>';
}
function getLinkWidget() {
return '<a ng-click="gotoDetail()">' + getReadWidget() + '</a>';
}
function getFilterWidget() {
},
getLinkWidget: function() {
'<a ng-click="gotoDetail()">' + getReadWidget() + '</a>'
},
getFilterWidget: function() {
return '<ma-choice-field field="::field" value="values[field.name()]"></ma-choice-field>';
}
function getWriteWidget() {
},
getWriteWidget: function() {
return '<ma-choice-field field="::field" entry="entry" value="entry.values[field.name()]"></ma-choice-field>';
}
return {
getReadWidget: getReadWidget,
getLinkWidget: getLinkWidget,
getFilterWidget: getFilterWidget,
getWriteWidget: getWriteWidget,
}
});
};
75 changes: 39 additions & 36 deletions src/javascripts/ng-admin/Crud/filter/maFilter.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
/*global define*/
var FilterController = require('./maFilterController');
var _ = require('lodash');

define(function (require) {
function maFilterDirective(FieldViewConfiguration) {
'use strict';

var FilterController = require('./maFilterController');
var _ = require('lodash');
var filterWidgetTypes = _(FieldViewConfiguration)
.map(function(fieldView, field) {
return '<span ng-switch-when="' + field + '">' + fieldView.getFilterWidget() +'</span>';
}).join('');

function maFilterDirective(FieldViewConfiguration) {
var filterWidgetTypes = _(FieldViewConfiguration)
.map(function(fieldView, field) {
return '<span ng-switch-when="' + field + '">' + fieldView.getFilterWidget() +'</span>';
}).join('');
var template =
'<form class="filters navbar-form well well-sm" ng-if="filterCtrl.shouldFilter()" ng-submit="filterCtrl.filter()">' +
'<div class="filter form-group" ng-repeat="field in filters track by $index" ng-class="{\'input-group\':field.label()}">' +
'<label for="{{ field.name() }}" ng-if="field.label() && field.type() != \'boolean\'" class="input-group-addon">' +
'{{ field.label() }}<span ng-if="field.validation().required">&nbsp;*</span>&nbsp;' +
'</label>' +
'<div ng-switch="field.type()" ng-class="field.getCssClasses(entry)">' +
filterWidgetTypes +
'</div>' +
'</div>' +
'<button class="btn btn-default" type="submit"><span class="glyphicon glyphicon-search"></span> Filter</button> ' +
'<button ng-if="!filterCtrl.isFilterEmpty" class="btn btn-default" type="button" ng-click="filterCtrl.clearFilters()"><span class="glyphicon glyphicon-remove"></span> Clear</button>' +
'</form>';
return {
restrict: 'E',
template: template,
scope: {
filters: '&',
datastore: '&'
},
controllerAs: 'filterCtrl',
controller: FilterController
};
}
var template = `
<form class="filters navbar-form well well-sm" ng-if="filterCtrl.shouldFilter()" ng-submit="filterCtrl.filter()">
<div class="filter form-group input-{{ field.type() }}" ng-repeat="field in filters track by $index" ng-class="{\'input-group\':field.label()}">
<label for="{{ field.name() }}" ng-if="field.label() && field.type() != \'boolean\'" class="input-group-addon">
{{ field.label() }}<span ng-if="field.validation().required">&nbsp;*</span>&nbsp;
</label>
<div ng-switch="field.type()" ng-class="field.getCssClasses(entry)">
${filterWidgetTypes}
</div>
</div>
<button class="btn btn-default" type="submit">
<span class="glyphicon glyphicon-search"></span> Filter
</button>
<button ng-if="!filterCtrl.isFilterEmpty" class="btn btn-default" type="button" ng-click="filterCtrl.clearFilters()">
<span class="glyphicon glyphicon-remove"></span> Clear
</button>
</form>
`;

maFilterDirective.$inject = ['FieldViewConfiguration'];
return {
restrict: 'E',
template: template,
scope: {
filters: '&',
datastore: '&'
},
controllerAs: 'filterCtrl',
controller: FilterController
};
}

return maFilterDirective;
});
maFilterDirective.$inject = ['FieldViewConfiguration'];

module.exports = maFilterDirective;
Loading

0 comments on commit 43c650d

Please sign in to comment.