Issue with my custom select2 directive for angularjs - angularjs

I have created custom select2 directive for angular it works perfect for my usecase and works like charm when i use template and it sets and get the values from input/ngModel
but when i use it on view page it do not resolve ngModel via scope.$eval
this is something scope issue please help me on this
please find directive mentioned below:
(function () {
'use strict';
var directiveId = 'snSelect2';
angular.module('app').directive(directiveId, ['datacontext', snSelect2]);
function snSelect2(datacontext) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, controller) {
var urlPrefix =datacontext.urlPrefix;
$(function () {
element.select2({
placeholder: element.attr('placeholder'),
multiple: angular.isDefined(attrs.multiple),
minimumInputLength: 3,
blurOnChange: true,
ajax: { // instead of writing the function to execute the request we use Select2's convenient helper
url: urlPrefix + "/Employees",
dataType: 'json',
data: function (term) {
return {
term: term
};
},
results: function (data) { // parse the results into the format expected by Select2.
// since we are using custom formatting functions we do not need to alter remote JSON data
return { results: data };
}
},
initSelection: function (element, callback) {
var id = scope.$eval(attrs.ngModel);//$(element).val();
if (id != "") {
$.ajax(urlPrefix + "/EmployeeById",
{
data: {
id: id,
format: 'json'
},
datatype: 'json'
}).done(function (data) {
if (angular.isDefined(attrs.multiple)) {
callback(data);
}
else {
callback(data[0]);
}
});
}
//var data = {id: element.val(), text: element.val()};
//callback(data);
},
dropdownCssClass: "bigdrop", // apply css that makes the dropdown taller
escapeMarkup: function (m) { return m; } // we do not want to escape markup since we are displaying html in results
}).select2('val', scope.$eval(attrs.ngModel))
.on("change", function (e) {
scope.$apply(function () {
controller.$setViewValue(attrs.ngModel);
});
});
element.bind("change", function (e) {
scope.$apply(function () {
scope[attrs.ngModel] = e.val;
});
});
});
}
}
}})();

http://snag.gy/lcizI.jpg
<!-- html element -->
<input type="hidden" class="form-control" data-ng-model="show" ui-select2="getShow">
$scope.getShow = {
placeholder: "Enter Show Code",
minimumInputLength: 3,
escapeMarkup: function (m) { return m; },
formatSelection: function(obj, container) {
return "ID: " + obj.showid + " - " + obj.name;
},
formatResult: function(obj, container, query) {
var start = obj.startdate ? moment(obj.startdate).format("DD/MM/YYYY") : "Unknown";
var end = obj.enddate ? moment(obj.enddate).format("DD/MM/YYYY") : "Unknown";
return '<div class="list-group-item small">' +
'<i><span class="label label-default pull-right">ID: ' + obj.showid + '</span></i>' +
'<i><span class="label label-info">(' + obj.code + ')</span></i>' +
'<div style="padding-top: 4px"><strong>' + obj.name + '</strong></div>' +
'<i>' + start + " - " + end + '</i>' +
'</div>';
},
query: function(options) {
if (!options.context)
options.context = {};
// status == processing means http request is in process, so don't do it again
if (options.context.status === "processing")
return;
options.context.status = "processing";
// this is just like ajax $http.get("/search").
model.show.search("search", {code: options.term, limit: 10, sort: 'ts desc'} )
.then(function(result) {
// set status = completed to indicate http request finished.
options.context.status = "completed";
// when you get the result from ajax, callback require you to call with
// an object { results: your result } as result
$scope.list.datalist.show = result;
options.callback({
results: result
});
});
}
}
Data from server looks like
[{
code: "MELCCS"
enddate: "2014-03-10T14:00:00.000Z"
id: "5329c28087b375a4d8a2af43"
name: "Melbourne Caravan and Camping Show"
openinghour: ""
showid: 1810
startdate: "2014-03-05T14:00:00.000Z"
state: "VIC"
status: "Research"
ts: 1395245779280
updatedAt: "2014-03-21T09:16:56.859Z"
warehouse: "52ecd53b673a5fba428e21a7"
}]

Related

Dynamically generating widgets from json data without performance losses

Here is the problem I'm trying to solve: generating a form from a variable set of widgets where the exact widgets and their ordering is directed by the data, namely schema. The first approach I've taken looks like (omitting the unnecessary details):
controller.js:
angular.module('app').controller(function($scope) {
$scope.data = {
actions: [{
name: 'Action1',
base: 'nova.create_server',
baseInput: {
flavorId: {
title: 'Flavor Id',
type: 'string'
},
imageId: {
title: 'Image Id',
type: 'string'
}
},
input: [''],
output: [{
type: 'string',
value: ''
}, {
type: 'dictionary',
value: {
key1: '',
key2: ''
}
}, {
type: 'list',
value: ['', '']
}]
}]
};
$scope.schema = {
action: [{
name: 'name',
type: 'string',
}, {
name: 'base',
type: 'string',
}, {
name: 'baseInput',
type: 'frozendict',
}, {
name: 'input',
type: 'list',
}, {
name: 'output',
type: 'varlist',
}
]
};
})
template.html
<div ng-controller="actionCtrl" ng-repeat="item in data.actions">
<div ng-repeat="spec in schema.action" ng-class="{'right-column': $even && isAtomic(spec.type), 'left-column': $odd && isAtomic(spec.type)}">
<typed-field></typed-field>
<div class="clearfix" ng-show="$even"></div>
</div>
</collapsible-panel>
directives.js
.directive('typedField', function($http, $templateCache, $compile) {
return {
restrict: 'E',
scope: true,
link: function(scope, element, attrs) {
$http.get(
'/static/mistral/js/angular-templates/fields/' + scope.spec.type + '.html',
{cache: $templateCache}).success(function(templateContent) {
element.replaceWith($compile(templateContent)(scope));
});
}
}
})
Among the templates located inside '/fields/' the simplest possible template for the string-type field is
<div class="form-group">
<label>{$ spec.title || makeTitle(spec.name) $}</label>
<input type="text" class="form-control" ng-model="item[spec.name]">
</div>
This approach works for - all the widgets are rendered, model bindings work, but on once I type a single letter inside of these widgets, the scope changes and the widgets are redrawn, causing:
* focus losing
* some time delay effective meaning poor performance.
Trying to overcome this drawback, I've rewritten my app in the following way:
template.html
<div ng-controller="actionCtrl" ng-repeat="item in data.actions">
<action></action>
</div>
directives.js
.directive('typedField', function($http, $templateCache, idGenerator, $compile) {
return {
restrict: 'E',
scope: true,
compile: function ($element, $attrs) {
$http.get(
'/static/mistral/js/angular-templates/fields/' + $attrs.type + '.html',
{cache: $templateCache}).success(function (templateContent) {
$element.replaceWith(templateContent);
});
return function (scope, element, attrs) {
scope.title = $attrs.title;
scope.type = $attrs.type;
scope.name = $attrs.name;
}
}
}
})
.directive('action', function($compile, schema, isAtomic) {
return {
restrict: 'E',
compile: function(tElement, tAttrs) {
angular.forEach(
schema.action,
function(spec, index) {
var cls = '', elt;
if ( isAtomic(spec.type) ) {
cls = index % 2 ? 'class="right-column"' : 'class="left-column"';
}
elt = '<div ' + cls + '><typed-field type="' + spec.type + '" name="' + spec.name + '"></typed-field>';
if ( index % 2 ) {
elt += '<div class="clearfix"></div>';
}
elt += '</div>';
tElement.append(elt);
});
return function(scope, element, attrs) {
}
}
}
})
Instead of getting schema from scope, I'm providing it via dependency injection into compile phase of a directive (which is run only the first time - which seemed quite the thing I needed to avoid repetitive full redraw of widgets). But now instead of nicely looking widgets (as before) I get raw html with data bindings not evaluated at all. I guess that I'm doing something wrong, but fail to graps how should I correctly use the compile function to avoid performance issues. Could you please give an advice on what should be fixed?
Finally I have found out what should be returned from the directive's compile function in that case
.directive('action', function($compile, schema, isAtomic) {
return {
restrict: 'E',
compile: function(tElement, tAttrs) {
angular.forEach(
schema.action,
function(spec, index) {
var cls = '', elt;
if ( isAtomic(spec.type) ) {
cls = index % 2 ? 'class="right-column"' : 'class="left-column"';
}
elt = '<div ' + cls + '><typed-field type="' + spec.type + '" name="' + spec.name + '"></typed-field>';
if ( index % 2 ) {
elt += '<div class="clearfix"></div>';
}
elt += '</div>';
tElement.append(elt);
});
var linkFns = [];
tElement.children().each(function(tElem) {
linkFns.push($compile(tElem));
});
return function(scope) {
linkFns.forEach(function(linkFn) {
linkFn(scope);
});
}
}
}
})
What actually $compile does is calling the 'compile' function of each directive it encounters - thus calling $compile on current directive's template lead to infinite recursion, but calling it for each child of this directive worked fine.

Directive not updateing (re-render) with data change

I can't get my angularJS module to update when the provider keeping the data is updated.
The console.logs are being written out, but html doesn't update.
Directive:
.directive('logBar', ['loggingService', function(loggingService) {
var template = '<div class="floatLeft" ><div class="arrow"></div></div>' +
'<ul class="floatLeft">' +
'<li class="logHolder" tabindex="-1" ng-repeat="log in logs | reverse">' +
'<label class="label label-{{log.type}}" ng-click="alert(log.message);" tabindex="-1">{{log.date | date:"HH:mm, dd.MMM" }} - {{log.message}}</label>' +
'</li>' +
'</ul>';
return {
restrict: 'E',
template: template,
scope: {},
link: function($scope, $elem, $attrs) {
$scope.$watch(function() {
console.log("logs updating");
$scope.logs = loggingService.getLogs();
console.log($scope.logs);
});
}
};
}]);
provider:
provider('loggingService', function() {
var logs = [{"date": Date.now(), "origin": "Logbar", "message": "Welcome", "type": "info"}];
var maxLogs = 15;
var types = "default,primary,success,info,warning,danger";
this.resetLog = function() {
logs = [];
};
this.Removelog = function(message, type, alert) {
};
this.$get = function() {
return {
logs: logs,
getLogs: function() {
return logs;
},
log: function(message, type, alert) {
if (types.indexOf(type) === -1) {
if (type !== "error") {
type = "default";
} else {
type = "danger";
}
}
logs.push({"message": message, "type": type, "date": Date.now(), origin: "User"});
if (logs.length > maxLogs) {
logs.shift();
}
}
};
};
})
controller:
Works with applys, but without them, it does not. Is there anyway to insert the $apply into the provider so I don't have to write them everywhere?
$http.post("/competition/" + $scope.key, angular.toJson($scope.draws)).success(function(data) {
$scope.fixDraws();
loggingService.log("Saved", "success", false);
$scope.$apply();
}).error(function(data) {
loggingService.log("Error", "danger", true);
$scope.fixDraws();
$scope.$apply();
});
From what I read, you're trying to update your scope from the watchExpression, not from the $watch listener.
Try this:
link: function($scope, $elem, $attrs) {
$scope.$watch(
// watch expression
function() {
return loggingService.getLogs();
},
// listener
function(newVal){
$scope.logs = newVal;
}
);
}
Refer to the docs.
It was fixed by adding the $apply to the http calls. And I could not reproduce the error in another project.

How to manually add a datasource to Angularjs-select2 when select2 is set up as <input/> to load remote data?

I am using the Select2 as a typeahead control. The code below works very well when the user types in the search term.
However, when loading data into the page, I need to be able to manually set the value of the search box.
Ideally something like: $scope.selectedProducerId = {id:1, text:"Existing producer}
However, since no data has been retrieved the Select2 data source is empty.
So what I really need to be able to do is to add a new array of data to the datasource and then set the $scope.selectedProducerId, something like: $scope.producersLookupsSelectOptions.addNewData({id:1, text:"Existing producer}) and then
$scope.selectedProducerId = 1;
Researching this I have seen various suggestions to use initSelection(), but I can't see how to get this to work.
I have also tried to set createSearchChoice(term), but the term is not appearing in the input box.
I would be most grateful for any assistance.
Thanks
This is the html
<div class="col-sm-4">
<input type="text" ui-select2="producersLookupsSelectOptions" ng- model="selectedProducerId" class="form-control" placeholder="[Produtor]" ng-change="selectedProducerIdChanged()"/>
</div>
This is the controller
angular.module("home").controller("TestLookupsCtrl", [
"$scope", "$routeParams", "AddressBookService",
function($scope, $routeParams, AddressBookService) {
$scope.producersLookupsSelectOptions = AddressBookService.producersLookupsSelectOptions();
}
]);
This is the service:
angular.module("addressBook").service("AddressBookService", [
"$http", "$q", function($http, $q) {
var routePrefix = "/api/apiAddressBook/";
//var fetchProducers = function(queryParams) {
// return $http.get(routePrefix + "GetClientsLookup/" + queryParams.data.query).then(queryParams.success);
//};
var _getSelectLookupOptions = function(url, minimumInputLength, idField, textField) {
var _dataSource = [];
var _queryParams;
return {
allowClear: true,
minimumInputLength: minimumInputLength || 3,
ajax: {
data: function(term, page) {
return {
query: term
};
},
quietMillis: 500,
transport: function(queryParams) {
_queryParams = queryParams;
return $http.get(url + queryParams.data.query).success(queryParams.success);
},
results: function(data, page) {
var firstItem = data[0];
if (firstItem) {
if (!firstItem[idField]) {
throw "[id] " + idField + " does not exist in datasource";
}
if (!firstItem[textField]) {
throw "[text] " + textField + " field does not exist in datasource";
}
}
var arr = [];
_.each(data, function(returnedData) {
arr.push({
id: returnedData[idField],
text: returnedData[textField],
data: returnedData
});
});
_dataSource = arr;
return { results: arr };
}
},
dataSource: function() {
return _dataSource;
},
getText: function (id) {
if (_dataSource.length === 0) {
throw ("AddressBookService.getText(): Since the control was not automatically loaded the dataSource has no content");
}
return _.find(_dataSource, { id: id }).text;
}
//initSelection: function(element, callback) {
// callback($(element).data('$ngModelController').$modelValue);
//},
//createSearchChoice:function(term) {
// return term;
//},
addNewData:function(data) {
this.ajax.results(data,1);
};
};
return {
producersLookupsSelectOptions: function() {
var url = routePrefix + "GetClientsLookup/";
return _getSelectLookupOptions(url, 2, "Id", "Name");
},
}
}
]);

How to create a tree list in Angular with ajax loading of each new layer?

I'm new to Angular and trying to wrap my brain around how to do stuff.
I'm trying to create a list that's populated on page load by an ajax call to a REST API service, and the elements of that list would fire ajax calls to that same service when clicked on that populated sub-lists below those elements, and so on to depth n.
The initial population of the list is easy: the controller makes the ajax call, gets the JSON object, assigns it to scope and the DOM is handled with a ng-repeat directive. I'm having trouble with the subsequent loading of the sub-lists.
In jQuery, I would have a function tied to each appropriately classed element clicked upon via onClick which would get the required parameters, take the JSON output, parse it into HTML and append that HTML after the element which fired the event. This is direct DOM manipulation and therefore Angular heresy.
I've already looked at this question here, but I still don't quite understand how to implement something like this "the Angular way".
Help?
Edit: Solved this problem by making a recursive directive. Instructions from here: http://jsfiddle.net/alalonde/NZum5/light/.
Code:
var myApp = angular.module('myApp',[]);
myApp.directive('uiTree', function() {
return {
template: '<ul class="uiTree"><ui-tree-node ng-repeat="node in tree"></ui-tree-node></ul>',
replace: true,
transclude: true,
restrict: 'E',
scope: {
tree: '=ngModel',
attrNodeId: "#",
loadFn: '=',
expandTo: '=',
selectedId: '='
},
controller: function($scope, $element, $attrs) {
$scope.loadFnName = $attrs.loadFn;
// this seems like an egregious hack, but it is necessary for recursively-generated
// trees to have access to the loader function
if($scope.$parent.loadFn)
$scope.loadFn = $scope.$parent.loadFn;
// TODO expandTo shouldn't be two-way, currently we're copying it
if($scope.expandTo && $scope.expandTo.length) {
$scope.expansionNodes = angular.copy($scope.expandTo);
var arrExpandTo = $scope.expansionNodes.split(",");
$scope.nextExpandTo = arrExpandTo.shift();
$scope.expansionNodes = arrExpandTo.join(",");
}
}
};
})
.directive('uiTreeNode', ['$compile', '$timeout', function($compile, $timeout) {
return {
restrict: 'E',
replace: true,
template: '<li>' +
'<div class="node" data-node-id="{{ nodeId() }}">' +
'<a class="icon" ng-click="toggleNode(nodeId())""></a>' +
'<a ng-hide="selectedId" ng-href="#/assets/{{ nodeId() }}">{{ node.name }}</a>' +
'<span ng-show="selectedId" ng-class="css()" ng-click="setSelected(node)">' +
'{{ node.name }}</span>' +
'</div>' +
'</li>',
link: function(scope, elm, attrs) {
scope.nodeId = function(node) {
var localNode = node || scope.node;
return localNode[scope.attrNodeId];
};
scope.toggleNode = function(nodeId) {
var isVisible = elm.children(".uiTree:visible").length > 0;
var childrenTree = elm.children(".uiTree");
if(isVisible) {
scope.$emit('nodeCollapsed', nodeId);
} else if(nodeId) {
scope.$emit('nodeExpanded', nodeId);
}
if(!isVisible && scope.loadFn && childrenTree.length === 0) {
// load the children asynchronously
var callback = function(arrChildren) {
scope.node.children = arrChildren;
scope.appendChildren();
elm.find("a.icon i").show();
elm.find("a.icon img").remove();
scope.toggleNode(); // show it
};
var promiseOrNodes = scope.loadFn(nodeId, callback);
if(promiseOrNodes && promiseOrNodes.then) {
promiseOrNodes.then(callback);
} else {
$timeout(function() {
callback(promiseOrNodes);
}, 0);
}
elm.find("a.icon i").hide();
var imgUrl = "http://www.efsa.europa.eu/efsa_rep/repository/images/ajax-loader.gif";
elm.find("a.icon").append('<img src="' + imgUrl + '" width="18" height="18">');
} else {
childrenTree.toggle(!isVisible);
elm.find("a.icon i").toggleClass("icon-chevron-right");
elm.find("a.icon i").toggleClass("icon-chevron-down");
}
};
scope.appendChildren = function() {
// Add children by $compiling and doing a new ui-tree directive
// We need the load-fn attribute in there if it has been provided
var childrenHtml = '<ui-tree ng-model="node.children" attr-node-id="' +
scope.attrNodeId + '"';
if(scope.loadFn) {
childrenHtml += ' load-fn="' + scope.loadFnName + '"';
}
// pass along all the variables
if(scope.expansionNodes) {
childrenHtml += ' expand-to="expansionNodes"';
}
if(scope.selectedId) {
childrenHtml += ' selected-id="selectedId"';
}
childrenHtml += ' style="display: none"></ui-tree>';
return elm.append($compile(childrenHtml)(scope));
};
scope.css = function() {
return {
nodeLabel: true,
selected: scope.selectedId && scope.nodeId() === scope.selectedId
};
};
// emit an event up the scope. Then, from the scope above this tree, a "selectNode"
// event is expected to be broadcasted downwards to each node in the tree.
// TODO this needs to be re-thought such that the controller doesn't need to manually
// broadcast "selectNode" from outside of the directive scope.
scope.setSelected = function(node) {
scope.$emit("nodeSelected", node);
};
scope.$on("selectNode", function(event, node) {
scope.selectedId = scope.nodeId(node);
});
if(scope.node.hasChildren) {
elm.find("a.icon").append('<i class="icon-chevron-right"></i>');
}
if(scope.nextExpandTo && scope.nodeId() == parseInt(scope.nextExpandTo, 10)) {
scope.toggleNode(scope.nodeId());
}
}
};
}]);
function MyCtrl($scope, $timeout) {
$scope.assets = [
{ assetId: 1, name: "parent 1", hasChildren: true},
{ assetId: 2, name: "parent 2", hasChildren: false}
];
$scope.selected = {name: "child 111"};
$scope.hierarchy = "1,11";
$scope.loadChildren = function(nodeId) {
return [
{assetId: parseInt(nodeId + "1"), name: "child " + nodeId + "1", hasChildren: true},
{assetId: parseInt(nodeId + "2"), name: "child " + nodeId + "2"}
];
}
$scope.$on("nodeSelected", function(event, node) {
$scope.selected = node;
$scope.$broadcast("selectNode", node);
});
}
Template:
<div ng-controller="MyCtrl">
<ui-tree ng-model="assets" load-fn="loadChildren" expand-to="hierarchy" selected-id="111" attr-node-id="assetId"></ui-tree>
<div>selected: {{ selected.name }}</div>
</div>
Here's a solution which I prototyped for my own use.
https://embed.plnkr.co/PYVpWYrduDpLlsvto0wR/
Link updated

Angularjs directive TypeError: object is not a function

I'm new at Angularjs... Just got mad making this to work:
angular.module('app', ['ui.select2']).directive("selectCompany", function($timeout) {
return {
restrict: 'A',
replace: true,
template: '<input type="text" name="company_id" ng-model="companySelected" />',
scope: {},
link: function (scope, element, attrs, ctrl) {
$timeout(element.select2({
placeholder : "Buscar empresa", minimumInputLength : 3, allowClear : true,
ajax: {
url : 'http://' + window.location.host + '/ajax/module/company/load-companies',
dataType : 'json',
type : 'post',
quietMillis : '250',
data : function (term, page) { return { name: term }; },
results : function (data, page) { return { results : data }; }
},
formatResult : function(item) { return item.name; },
formatSelection : function(item) { return item.name; },
escapeMarkup : function (m) { return m; },
}));
},
};
});
This is my Angular directive which makes a <div select-company></div> into a select2 control. It works at the moment, it also return the details but I'm getting this error:
Any idea?
Normally, you'd want to work with non-minified versions of scripts during development, because they give more descriptive stack traces...
Hard to say what exactly is going on here, but try:
$timeout(function () {
element.select2({
placeholder: "Buscar empresa",
minimumInputLength: 3,
allowClear: true,
ajax: {
url: 'http://' + window.location.host + '/ajax/module/company/load-companies',
dataType: 'json',
type: 'post',
quietMillis: '250',
data: function (term, page) {
return {
name: term
};
},
results: function (data, page) {
return {
results: data
};
}
},
formatResult: function (item) {
return item.name;
},
formatSelection: function (item) {
return item.name;
},
escapeMarkup: function (m) {
return m;
},
})
});

Resources