Angular Directive With Binding to Hash of Scope Objects - angularjs

I'm trying to create a directive that will allow me to bind to hash of other scope properties.
HTML
<div lookup lookup-model="data.countryId"></div>
<div lookup lookup-model="data.stateId" lookup-params="{countryId: data.countryId}"></div>
What I would like to be able to do is every time a value in lookup-params is updated to refresh the lookup with the model of data.stateId. I'm trying to keep this generic since there is likely a variety of different lookup-params I'll want to have.
Is there a way to do this in Angular?
Update
I certainly didn't provide enough detail. Clicked submit too soon. Here is my solution based off feedback from #Olivvv. The suggestion led me to the scope.$eval function.
The goal here was to create a directive that would allow us to use a select with a $http get to seed the options within the select. Some of the $http requests will need a parameter since their is a dependency on another value. For example, a set of states can only be provided when a country value is provided.
The following is the code I pulled together. I'm sure it can be improved, but it is doing the trick at the moment. Please note, I'm using Lodash for some utility functions. You'll also see a scope object "lookupModelObject". This was purely to meet a design need for styling selects. It probably can be ignored if you are only interested in the lookupParams.
HTML Snippet
<div select-lookup lookup-value="block.data.countryId" lookup-type="countries" lookup-placeholder="Select a Country"></div>
<div select-lookup lookup-value="block.data.stateId" lookup-type="states" lookup-params="{countryId: block.data.countryId}" lookup-placeholder="Select a State"></div>
Directive
The important part to point out is how I'm evaluating the attrs.lookupParams. If the attribute exists I use scope.$eval to evaluate the attribute. You'll later see how I added each parameter to scope and added a watcher in case one of the params changed. This would allow me to reset the "state" to null if a different country was selected.
angular.module("foo").directive('selectLookup', ['$http', '$q', function($http, $q) {
return {
restrict: 'A',
replace: true,
templateUrl: 'common/lookups/partials/selectLookupPartial.html',
scope: {
id: "#",
lookupValue: "=",
lookupParams: "="
},
link: function(scope, element, attrs) {
// Initialize the options.
scope.options = [];
var optionsLoaded = false;
var resetLookupModel = function() {
scope.lookupModelObject = {
id: null,
text: attrs.lookupPlaceholder
};
};
// Evaluate and obtain the lookup parameters for this lookup.
var lookupParams = {};
if (attrs.lookupParams) {
lookupParams = scope.$eval(attrs.lookupParams);
}
var updateLookupModelObject = function(value) {
// This function is only relevant if the options have been loaded.
if (optionsLoaded) {
if (value === undefined || value === null) {
resetLookupModel();
}
else {
var item = _.findWhere(scope.options, {id: value});
if (item) {
scope.lookupModelObject = item;
}
else {
resetLookupModel();
}
}
}
};
var fetchValues = _.throttle(function() {
var deferred = $q.defer(),
fetchUrl = "/api/lookup/" + attrs.lookupType,
keys = _.keys(lookupParams);
_.each(keys, function(key, index) {
if (index === 0) {
fetchUrl += "?";
}
else {
fetchUrl += "&";
}
fetchUrl += key + "=" + lookupParams[key];
});
// Empty the options.
scope.options.splice(0, scope.options.length);
$http.get(fetchUrl).then(function(response) {
scope.options = response.data;
optionsLoaded = true;
updateLookupModelObject(scope.lookupValue);
deferred.resolve(scope.options);
});
return deferred.promise;
}, 150);
// Setup the watchers
// If there are lookup params add them to scope so we can watch them.
var keys = _.keys(lookupParams);
_.each(keys, function(key) {
scope[key] = lookupParams[key];
// Setup watchers for each param.
scope.$watch(key, function() {
fetchValues();
});
});
scope.$watch('lookupParams', function(newValue, oldValue) {
if (!_.isEqual(newValue, oldValue)) {
lookupParams = newValue;
fetchValues().then(resetLookupModel);
}
});
scope.$watch('lookupValue', updateLookupModelObject);
scope.$watch('lookupModelObject', function(newValue, oldValue) {
if (!_.isEqual(newValue, oldValue)) {
scope.lookupValue = newValue.id;
}
});
fetchValues();
}
};
}]);
Template
We had a design constraint that forced us to introduce a "select-placeholder". Outside of that the select is the "typical" way to setup a select in Angular.
<div class="container select-container">
<div class="select-placeholder">
<span class="select-placeholder-text">{{ lookupModelObject.text }}</span>
<select class="select-input" data-ng-model="lookupModelObject" id="{{ id }}" data-ng-options="option as option.text for option in options"></select>
</div>
</div>

I solved it with the following function in a "utils" service:
function utils ($parse) {
this.$params = function(attrs, attrName, allowedParams){
var out = {},
parsed = $parse(attrs[attrName])();
if (typeof parsed === 'object'){
angular.forEach(parsed, function(val, key){
if (allowedParams.indexOf(key) !== -1){
this[key] = val;
} else {
//do some logging. i.e ('parameter not recognized :', key, ' list of the allowed params:', allowedParams)
}
}, out);
}else{
out[allowedParams[0]] = attrs[attrName];
}
return out;
};
}
use it that way in your template:
lookup-params="{countryId: '{{data.countryId}}'}"
and in your directive :
var lookupParams = utils.$params(attrs, 'lookup-params', ['countryId','anotherParams', 'etcetera']);
The first of the allowed params can be passed directly as string instead of an object:
lookup-params="{'{{data.countryId}}'}"
will work

Related

Angular Select Option Class Expression

I cant change the option class attribute. OptionClass actually work. I founded it from the this site. But my real code not work. I dont know where is the bug.
I only want to change the options background-color with expression, for example if id>100 then background color is red etc.
My Select
<select ng-model="firstSection" ng-options="o.label for o in options" multiple
class="invoiceSelect" options-class="{ 'is-eligible' : Id<100, 'not-eligible': !Id<100 }"></select>
My Filling methods
var myApp = angular.module('app', []);
myApp.controller('myCtrl', function ($scope, $http, $filter) {
$scope.getInvoiceByRfiPolicyId = function (rfiPolicyId) {
$http.get('http://somesitelink.com/' + rfiPolicyId)
.then(function (response) {
$scope.options = response.data.map(o=> {
return { label: 'Numbers:' + o.InvoiceNumber + ' InvDate:' + $scope.dateParse(o.InvoiceDate), value: o.InvoiceId, Id:o.InvoiceId, eligible: true }
});
});
}
});
Here is my optionClass function
myApp.directive('optionsClass', function ($parse) {
return {
require: 'select',
link: function (scope, elem, attrs, ngSelect) {
var parts = attrs.ngOptions.split(' ');
var optionsSourceStr = parts[parts.indexOf('in') + 1];
var getOptionsClass = $parse(attrs.optionsClass);
scope.$watchCollection(optionsSourceStr, function (items) {
scope.$$postDigest(function () {
angular.forEach(items, function (item, index) {
var classes = getOptionsClass(item),
option = elem.find('option[value="' + item.id + '"]');
angular.forEach(classes, function (add, className) {
if (add) {
angular.element(option).addClass(className);
}
});
});
});
});
}
};
});
A working example is given in the embedded code snippet below.
You must have got the solution from here. There are two issues in the code used:
1. Finding option element
find method in this statement option = elem.find('option[value="' + item.id + '"]') tries to look using attribute selector but find method of jqLite (which is embedded in AngularJS) is limited to look up by tag names only so this doesn't work in your code. Added a simple method to find the required option.
It is working in the question that you referred because code pasted there had an assumption to be working with jQuery and find method in jQuery allows you to find based on a variety of selectors.
2. Expressions in options-class directive
Group the expressions using (<expression>) as shown here options-class="{ 'is-eligible' : (Id<2), 'not-eligible': !(Id<2) }", otherwise ! negation operator will first negate the Id value and then perform the comparison.
angular.module('ExampleApp', [])
.controller('ExampleController', function($scope) {
$scope.options = [{
label: 'Numbers: Invoice001',
value: 1,
Id: 1,
eligible: true
},
{
label: 'Numbers: Invoice002',
value: 2,
Id: 2,
eligible: true
},
{
label: 'Numbers: Invoice003',
value: 3,
Id: 3,
eligible: true
}
];
})
.directive('optionsClass', function($parse) {
return {
require: 'select',
link: function(scope, elem, attrs, ngSelect) {
function findOptionByValueAttr(options, findByValue) {
for (var i = 0; i < options.length; i++) {
var option = angular.element(options[i]);
if (option.attr('value') === findByValue.toString()) {
return option;
}
}
}
var parts = attrs.ngOptions.split(' ');
var optionsSourceStr = parts[parts.indexOf('in') + 1];
console.log(attrs.optionsClass);
var getOptionsClass = $parse(attrs.optionsClass);
scope.$watchCollection(optionsSourceStr, function(items) {
angular.forEach(items, function(item, index) {
console.log(getOptionsClass(item));
var classes = getOptionsClass(item),
/*option = elem.find('option[value="' + item.Id + '"]');*/
option = findOptionByValueAttr(elem.children(), item.Id);
console.log(item);
angular.forEach(classes, function(add, className) {
if (add) {
option.addClass(className);
}
});
});
});
} // link function ends
}; // return value ends
}); // directive ends
.is-eligible {
background-color: lightgreen;
}
.not-eligible {
background-color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="ExampleApp" ng-controller="ExampleController">
<select ng-model="firstSection" ng-options="o.label for o in options track by o.Id" multiple class="invoiceSelect" options-class="{ 'is-eligible' : (Id<2), 'not-eligible': !(Id<2) }"></select>
</div>
Design Suggestions
Styling the options is not recommended as this behavior is not consistent across browsers and option elements are rendered differently on mobile.
You may check following design considerations to avoid styling:
Modify the label and/or disable those options which you eventually want the user to NOT select. This is possible using ng-options.
On selecting an option, highlight the external surrounding parent area enclosing the select element to alert the user to understand the difference and may accompany with a supporting text, disabling the relevant areas, submit buttons etc.
Hope these will be helpful.

md-select component loses ng-selected value

I work with Angular 1.5.4 & material 1.0.6. Everything of the following example is working fine if I use chrome. But if I use safari no value is preselected.
The template of the component looks like
<md-select ng-model="$ctrl.selectedFruit">
<md-option ng-repeat="fruit in $ctrl.fruits" ng-value="fruit" required
ng-selected="$first" >
{{fruit.name}} {{fruit.weight}}
</md-option>
</md-select>
the component looks like
.component('applicantAccount', {
// #ngInject
bindings: {
selectedFruit: "="
},
controller: function (accountService) {
var ctrl = this;
fruitService.initFruitBefore(function () {
ctrl.fruits= fruitService.getFruits();
});
},
templateUrl: 'views/templates/selectFruitTemplate.html'
});
fruitService:
var fruits = [];
var initFruits = function () {
return $http.get(configuration.apiUrl + "fruits")
.then(function success(response) {
fruits = response.data || []
return response;
})
};
var getFruits = function () {
var filterEx = {type: "fresh"};
return $filter('filter')(fruits , filterEx);
};
var initFruitBefore = function (callback) {
if (!areFruitsAvailable()) {
initFruits().then(callback);
}
else{
callback();
}
};
var areFruitsAvailable = function () {
return typeof fruits[0] !== 'undefined'
};
var getFreshFruit = function () {
var filterEx = {type: "fresh"};
var filtered = $filter('filter')(fruits, filterEx);
return filtered[0] || null;
};
controller:
fruitService.initFruitBefore(function () {
ctrl.selectedFruit = accountService.getFreshFruit();
ctrl.fruits = accountService.getFruits();
});
The odd thing(only safari) is, that ctrl.fruits gets initialized before selectedFruit is initialized (outside of the component). If this happens the value was shown for a short term.
Has anyone an idea whats going on? Why behaves the safari different?
Thank you & sorry for my bad english
The Solution
I added ng-model-options={trackBy:'$value.id'}.
Angular Material:
To determine whether something is selected, ngModelController is looking at whether $scope.selectedUser == (any user in $scope.users);;
Javascript's == operator does not check for deep equality (ie. that all properties on the object are the same), but instead whether the objects are the same object in memory. In this case, we have two instances of identical objects, but they exist in memory as unique entities. Because of this, the select will have no value populated for a selected user.
For futher description look at https://material.angularjs.org/latest/api/directive/mdSelect

Angular directive using value from database, interpolate and filter

I have a directive which I've used for texts in my app:
module.directive("enaText", function (textService) {
return {
restrict: "AE",
link: function (scope, element, attributes) {
// Catching <enaText key="[key]"> and <div ena-text="[key]">
scope.$watchCollection(function () {
return [attributes.key, attributes.enaText];
}, function (values) {
var key = values[0] || values[1];
if (!key) {
return;
}
var text = textService.get(key) || key;
// Not using a template to easier support HTML in text value
element.html(text || "");
}, true);
}
};
});
My textService helps with getting the text in the current language from sessionStorage (initially from a database). This version of the directive works just as intended:
<div ena-text="page_title"></div>
Which gets the text with name/key "page_title" and puts it in the div.
Now I want to extend the directory to be able to use scope variables in the text strings from textService and possibly also filters. This is what I have so far:
module.directive("enaTextNew", function (textService, $interpolate, $parse, $compile) {
return {
restrict: "AE",
link: function (scope, element, attributes) {
var regex = /^([^\|]*)(\|.+)?/;
// Catching <enaText key="[key]"> and <div ena-text="[key]">
scope.$watchCollection(function () {
return [attributes.key, attributes.enaTextNew];
}, function (values) {
var expression = values[0] || values[1];
if (!expression) {
return;
}
var match = expression.match(regex);
var key = match[1].trim();
var filter = match[2];
var text = textService.get(key) || key;
text = $interpolate(text)(scope);
if (filter) {
text = scope.$eval("'" + text + "'" + filter);
}
// Not using a template to easier support HTML in text value
element.html(text || "");
}, true);
}
};
});
This works fine when I use it in:
<div ena-text-new="character_count|uppercase"></div>
Which gets the text "{{count}} characters of max {{max}}", uses variables count and max from scope and then adds the uppercase filter. The result is for example: "0 CHARACTERS OF MAX 100".
The only problem is that even though scope.count (or scope.max) is changed, it's not reflected in the result of the directive.
This specific string and the filter is just an example. Filters will propably not be necessary, I've tried without it but it didn't do any difference. But the important thing is the scope variables.
The user PSL helped me get on the right track using http://plnkr.co/edit/ir9Ews. I made some changes to it, because a new watch would else be created every time the attributes values changed. This is what I use now:
module.directive("enaText", function (textService, $interpolate) {
return {
restrict: "AE",
link: function (scope, element, attributes) {
var keyFilterRegex = /^([^\|]*)(\|.+)?/;
var originalText;
var filter;
// Catching <enaText key="[key]"> and <div ena-text="[key]">
scope.$watchCollection(function () {
return [attributes.key, attributes.enaText];
}, function (values) {
var expression = values[0] || values[1];
if (!expression) {
originalText = undefined;
filter = undefined;
return;
}
var match = expression.match(keyFilterRegex);
originalText = textService.get(match[1]);
filter = match[2];
});
scope.$watch(function () {
return $interpolate(originalText || "")(scope);
}, function (text) {
if (filter) {
text = scope.$eval("'" + text + "'" + filter);
}
// Not using a template to easier support HTML in text value
element.html(text);
});
}
};
});

Angular ui-router's ui-sref created dynamically in a directive?

I am working on a line of business application that is using Angular to create a SPA around a Node.js api server. I decided on using ui-router cause of the state-machine and their intuitive way of embedding urls but I chanced upon a slight challenge when creating dynamic URLs within a directive.
I am using jQuery Datatables for my data grid as a directive but any action links generated using 'fnRender' don't seem to compile the 'ui-sref' to their respective 'href'links. The directive code is as follows:
app.directive('datatable', function ($http) {
return {
restrict: 'A',
link: function ($scope, $elem, attrs) {
var responsiveHelper;
var breakpointDefinition = {
tablet: 1024,
phone : 480
};
var options = {
bDeferRender: true,
sPaginationType: "full_numbers",
oLanguage: {
sEmptyTable: "No records returned",
sSearch: "<span>Search:</span> ",
sInfo: "Showing <span>_START_</span> to <span>_END_</span> of <span>_TOTAL_</span> entries",
sLengthMenu: "_MENU_ <span>entries per page</span>",
sProcessing: "Loading..."
},
sDom: "lfrtip",
oColVis: {
buttonText: "Change columns <i class='icon-angle-down'></i>"
},
oTableTools: {
sSwfPath: "js/plugins/datatable/swf/copy_csv_xls_pdf.swf"
},
bAutoWidth : false,
fnPreDrawCallback: function () {
if (!responsiveHelper) {
responsiveHelper = new ResponsiveDatatablesHelper($elem, breakpointDefinition);
}
},
fnRowCallback : function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
responsiveHelper.createExpandIcon(nRow);
},
fnDrawCallback : function (oSettings) {
responsiveHelper.respond();
}
};
if (typeof $scope.dtOptions !== 'undefined') {
angular.extend(options, $scope.dtOptions);
}
if (attrs['dtoptions'] === undefined) {
for (property in attrs) {
switch (property) {
case 'sajaxsource':
options['sAjaxSource'] = attrs[property];
break;
case 'sajaxdataprop':
options['sAjaxDataProp'] = attrs[property];
break;
}
}
} else {
angular.extend(options, $scope[attrs['dtoptions']]);
}
if (typeof options['sAjaxSource'] === 'undefined') {
throw "Ajax Source not defined! Use sajaxsource='/api/v1/*'";
}
if (typeof options['fnServerData'] === 'undefined') {
options['fnServerData'] = function (sSource, aoData, resultCb) {
$http.get(sSource, aoData).then(function (result) {
resultCb(result.data);
});
};
}
options.aoColumnDefs = [];
$elem.find('thead th').each(function() {
var colattr = angular.element(this).data();
if (colattr.mdata) {
if (colattr.mdata.indexOf("()") > 1) {
var fn = $scope[colattr.mdata.substring(0, colattr.mdata.length - 2)];
if (typeof fn === 'function') {
options.aoColumnDefs.push({
mData: fn,
sClass: colattr.sclass,
aTargets: [colattr.atargets]
});
} else {
throw "mData function does not exist in $scope.";
}
} else {
options.aoColumnDefs.push({
mData: colattr.mdata,
sClass: colattr.sclass,
bVisible: colattr.bvisible,
aTargets: [colattr.atargets]
});
}
} else {
if (colattr.fnrender.indexOf("()") > 1) {
var fn = $scope[colattr.fnrender.substring(0, colattr.fnrender.length - 2)];
if (typeof fn === 'function') {
options.aoColumnDefs.push({
fnRender: fn,
sClass: colattr.sclass,
aTargets: [colattr.atargets]
});
} else {
throw "fnRender function does not exist in $scope.";
}
} else {
options.aoColumnDefs.push({
fnRender: function (oObj) {
return "<a tooltip class='btn' title='View' ui-sref=\""+colattr.tag+".show({slug:\'"+oObj.aData._id+"\'})\"><center class=\"icon-search\"></center></a>";
},
sClass: colattr.sclass,
bVisible: colattr.bvisible,
aTargets: [colattr.atargets]
});
}
}
});
$elem.dataTable(options);
$(".dataTables_length select").wrap("<div class='input-mini'></div>").chosen({disable_search_threshold: 9999999 });
}
}
});
It runs with out complications and even generates the following anchor tag:
<a ui-sref="organisation.show({slug:'527a44c02aa9ce3a1c3fbc17'})"></a>
However, ui-router doesn't compile it to a respective state url. What could be the issue? Is there some configuration I may have missed?
Thanks
Are you using any other directives or expressions within your data table? I'm guessing those wouldn't work either, because it looks like Angular never gets the opportunity to compile any of the HTML you're generating.
This has nothing to do with uiSref and everything to do with writing directives correctly. In terms of best practices, this directive has way too much code. You should look at decomposing it into multiple nested directives and straight HTML. I'd suggest spending some time learning about transclusion and doing nested directives with directive controllers.
Leaving aside best practice, I just encountered and resolved this issue myself. Because DataTables is modifying the DOM outside of Angular's event loop, the ui-sref attributes don't get compiled. It's a simple fix: you need to call $compile on each row as it's created.
Here's my (much simpler) directive:
function datatable($compile) {
return {
restrict: "A",
link: (scope, elem, attrs) => {
// THE IMPORTANT BIT
scope.options.createdRow = function (row) {
$compile(row)(scope);
};
var api = elem.DataTable(scope.options);
var handleUpdates = function (newData) {
var data = newData || null;
if (data) {
api.clear();
api.rows.add(data).draw();
}
};
scope.$watch("options.data", handleUpdates, true);
},
scope: { options: "=" }
};
}

AngularJS Passing Variable to Directive

I'm new to angularjs and am writing my first directive. I've got half the way there but am struggling figuring out how to pass some variables to a directive.
My directive:
app.directive('chart', function () {
return{
restrict: 'E',
link: function (scope, elem, attrs) {
var chart = null;
var opts = {};
alert(scope[attrs.chartoptions]);
var data = scope[attrs.ngModel];
scope.$watch(attrs.ngModel, function (v) {
if (!chart) {
chart = $.plot(elem, v, opts);
elem.show();
} else {
chart.setData(v);
chart.setupGrid();
chart.draw();
}
});
}
};
});
My controller:
function AdListCtrl($scope, $http, $rootScope, $compile, $routeParams, AlertboxAPI) {
//grabing ad stats
$http.get("/ads/stats/").success(function (data) {
$scope.exports = data.ads;
if ($scope.exports > 0) {
$scope.show_export = true;
} else {
$scope.show_export = false;
}
//loop over the data
var chart_data = []
var chart_data_ticks = []
for (var i = 0; i < data.recent_ads.length; i++) {
chart_data.push([0, data.recent_ads[i].ads]);
chart_data_ticks.push(data.recent_ads[i].start);
}
//setup the chart
$scope.data = [{data: chart_data,lines: {show: true, fill: true}}];
$scope.chart_options = {xaxis: {ticks: [chart_data_ticks]}};
});
}
My Html:
<div class='row-fluid' ng-controller="AdListCtrl">
<div class='span12' style='height:400px;'>
<chart ng-model='data' style='width:400px;height:300px;display:none;' chartoptions="chart_options"></chart>
{[{ chart_options }]}
</div>
</div>
I can access the $scope.data in the directive, but I can't seem to access the $scope.chart_options data.. It's definelty being set as If I echo it, it displays on the page..
Any ideas what I'm doing wrong?
UPDATE:
For some reason, with this directive, if I move the alert(scope[attrs.chartoptions]); to inside the $watch, it first alerts as "undefined", then again as the proper value, otherwise it's always undefined. Could it be related to the jquery flot library I'm using to draw the chart?
Cheers,
Ben
One problem I see is here:
scope.$watch(attrs.ngModel, function (v) {
The docs on this method are unfortunately not that clear, but the first argument to $watch, the watchExpression, needs to be an angular expression string or a function. So in your case, I believe that you need to change it to:
scope.$watch("attrs.ngModel", function (v) {
If that doesn't work, just post a jsfiddle or jsbin.com with your example.

Resources