AngularJS: Why is my directive's link function not being called? - javascript

I am using ngTagsInput and would like to perform some validation on my models as they change. Here is a Plunker of what I'd like to achieve.
Markup:
<div ng-repeat="field in fields">
<tags-input ng-model="field.selectedData" max-tags="{{field.maxTags}}" enforce-max-tags placeholder="{{option.placeholder}}">
<auto-complete source="updateAutocomplete($query)"></auto-complete>
</tags-input>
</div>
Fields / models:
$scope.fields = [
{
name: 'assay',
placeholder: 'Select one assay...',
maxTags: 1,
selectedData: [] // These are the models
},
{
name: 'cellLines',
placeholder: 'Select cell line(s)...',
maxTags: 5,
selectedData: []
},
...
]
Finally, my enforceMaxTags directive:
.directive('enforceMaxTags', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$parsers.push(function(value) {
console.log('link called');
});
}
};
})
The enforceMaxTags directive is being compiled, but the link function is not called even when the models change. The data binding does appear to work, though, because if I console.log the selectedData on form submission, it is filled with the correct objects. What am I missing?
Thanks in advance.

Related

AngularJS: ngChange not called from directive

Brief intro to my problem
I have a directive that dynamically shows a list of checkboxes. It has a parameter called options that should be an array like the following, in order to show the list of checkboxes correctly. For example:
var options = [
{
id: 1,
label: 'option #1'
},
{
id: 2,
label: 'option #2'
},
{
id: 3,
label: 'option #3'
}
];
So, by passing this array to my directive, a group of three checkboxes would be shown.
Also, the directive requires ngModel that will have the result of checking/unchecking the checkboxes (this object is always passed initialized). For example:
var result = {
"1": true,
"2": true,
"3": false
};
This case means that the first and second checkboxes (options with id=1 and id=2) are checked and the third (option with id=3) is unchecked.
My directive
template.html
<div ng-repeat="option in options track by $index">
<div class="checkbox">
<label>
<input type="checkbox"
ng-model="result[option.id]">
{{ ::option.label }}
</label>
</div>
</div>
directive.js
angular
.module('myApp')
.directive('myDirective', myDirective);
function myDirective() {
var directive = {
templateUrl: 'template.html',
restrict: 'E',
require: 'ngModel',
scope: {
options: '='
},
link: linkFunc
};
return directive;
function linkFunc(scope, element, attrs, ngModel) {
scope.result;
ngModel.$render = setResult;
function setResult() {
scope.result = ngModel.$viewValue;
};
};
};
What I want to achieve
Wherever I use my directive, I want to be able to trigger a function whenever the ngModel changes. Of course, I would like to achieve this using ngChange. So far I have the following:
<my-directive
name="myName"
options="ctrlVM.options"
ng-model="ctrlVM.result"
ng-change="ctrlVM.selectionChanged()">
</my-directive>
but the .selectionChanged() function is not triggered whenever the model changes. Anyone has any idea why this is not working as I am expecting it to work?
First thing first, please try to provide jsfiddle, codepen etc code snippet link so that it will be easy for others to answer your question.
The problem in your case is that you are never updating the ctrlVM.result object as you are passing the object's reference and that reference never change even if you manually update the model by calling ngModel.$setViewValue().
To solve the problem, just update the model by manually calling ngModel.$setViewValue() and pass in the new Object so that the reference changes and that will trigger the ngChange directives logic.
I've added the logic to do that and it will successfully trigger the change. Look at the code below:
angular
.module('myApp', [])
.directive('myDirective', myDirective)
.controller('MyController', function($timeout) {
var vm = this;
vm.options = [{
id: 1,
label: 'option #1'
}, {
id: 2,
label: 'option #2'
}, {
id: 3,
label: 'option #3'
}];
vm.result = {
"1": true,
"2": true,
"3": false
};
vm.selectionChanged = function() {
vm.isChanged = true;
$timeout(function() {
vm.isChanged = false;
}, 500)
}
});
function myDirective() {
var directive = {
templateUrl: 'template.html',
restrict: 'E',
require: 'ngModel',
scope: {
options: '='
},
link: linkFunc
};
return directive;
function linkFunc(scope, element, attrs, ngModel) {
scope.result;
ngModel.$render = setResult;
function setResult() {
scope.result = ngModel.$viewValue;
};
scope.updateValue = function(val) {
ngModel.$setViewValue(Object.assign({}, val))
}
};
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular.min.js"></script>
<div ng-app="myApp">
<script type="text/ng-template" id="template.html">
<div ng-repeat="option in options track by $index">
<div class="checkbox">
<label>
<input type="checkbox"
ng-model="result[option.id]" ng-click="updateValue(result)">
{{ ::option.label }}
</label>
</div>
</div>
</script>
<div ng-controller="MyController as ctrlVM">
<my-directive name="myName" options="ctrlVM.options" ng-model="ctrlVM.result" ng-change="ctrlVM.selectionChanged()">
</my-directive>
<div> Data: {{ctrlVM.result}} </div>
<div> isChanged: {{ctrlVM.isChanged}} </div>
</div>
</div>
#Gaurav correctly identified the problem (ng-change is never called because the object reference does not change). Here is a simpler solution that doesn't require manually cloning into the controller's model:
Add a binding for the ng-change attribute:
scope: {
options: '=',
ngChange: '&' // Add this, creates binding to `ctrlVM.selectionChanged()`
}
Add an ng-change to your checkbox template:
<input type="checkbox"
ng-model="result[option.id]" ng-change="ngChange()">
Now, when any checkbox changes it will automatically call the outer ng-change function without the intermediate step of cloning into the model.

Angular Directive Element dynamic Template fields with its updated model value from controller?

I have <superhero> directive which has two directive
web-buttons to take of the form validation and post the updated
ngModel value to respective controller
fieldMap directive to generate the dynamic fields by object we are passing from respective controller
Here is the example which i have worked
directive attribute called saveFormFn will tell the button to call which function to invoked using enter attribute directive.
For example. After click save button it will call the function 'Ctrl1saveFormFn' from controller Ctrl1 .This function will make ajax post to save the form fields.
After updating the text fields with some content and click save,I have passed the current scope of the directive to respective controller (see console log). i could not get the updated fielddata value from current Scope.
$scope.Ctrl1saveFormFn = function(item){
_.each(item,function(currentScope){
console.log(currentScope)
// here i want to collect the form data with updated fielddata values
})
}
I am beginner.Am i on right path? Please advice
I've re-written your code because it was pretty hard to understand.
I would do it like this:
Use ng-include to load the template of your buttons. That's loading the control buttons edit and save.
Save your data in a variable in the superhero directive. Maybe it would be even better to store it in a separate service/factory.
Create a directive customForm that will create a form based on the supplied model that you're passing to its scope.
The main application logic is in the superhero directive because it is adding the controls save/edit to the DOM. If saving/editing is not only related to the superhero it would be better to do it in your main controller.
Please have a look at the demo below or in this jsfiddle.
angular.module('demoApp', [])
.directive('superhero', Superhero)
.directive('customForm', CustomForm)
.controller('mainController', MainController);
function Superhero() {
return {
restrict: 'E',
scope: {
formModel: "=",
},
template: '<div class="hero"><div ng-include="\'web-buttons.html\'"></div><custom-form model="formModel"></custom-form></div>',
controllerAs: 'superHeroCtrl',
controller: function ($scope) {
var self = this;
console.log('controller directive');
angular.extend(this, {
abilities: [],
editMode: false,
addStrength: function (data) {
self.abilities.push(data);
},
getStrength: function () {
return self.abilities;
},
showSave: function() {
self.editMode = true;
$scope.formModel.editMode = true;
},
hideSave: function() {
self.editMode = false;
$scope.formModel.editMode = false;
},
save: function() {
self.addStrength('can fly');
console.log(self.getStrength());
console.log('saving data now of form now...', $scope.formModel.data);
alert('saving data of form now: ' + self.getStrength()[0] + ' - ' + JSON.stringify( $scope.formModel.data, null, 2));
self.hideSave();
}
});
}
}
}
function CustomForm() {
return {
restrict: 'EA',
scope: {
model: '='
},
template: '<div ng-if="model.editMode" ng-repeat="formElement in model.fields" ng-include="formElement.template.url"></div>'
}
}
function MainController() {
this.normalForm = {
editMode: false,
data: {
},
fields: {
'NAME':{
template: {
url: 'customForms/text.html',
type: 'edit' // not sure for what it is needed
},
label: 'First name',
id: "NAME",
placeholder : "First Name",
fieldData: "NAME",
key : 'first_name'
},
'LNAME': {
template: {
url: 'customForms/text.html',
type: 'edit' // not sure for what it is needed
},
label: "Last Name",
placeholder : "Last Name",
id: "LNAME",
key : 'last_name'
}
}
};
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="demoApp" ng-controller="mainController as mainCtrl">
<script type="text/ng-template" id="customForms/text.html">
<label for="{{formElement.id}}">{{formElement.label}}</label>
<input ng-model="model.data[formElement.key]" placeholder="{{formElement.placeholder}}" id="formElement.id"/>
</script>
<script type="text/ng-template" id="web-buttons.html">
<button ng-click="superHeroCtrl.showSave()" ng-if="!superHeroCtrl.editMode">edit</button>
<button ng-if="superHeroCtrl.editMode" ng-click="superHeroCtrl.save()">save</button>
</script>
<superhero form-model="mainCtrl.normalForm"></superhero>
<h3>debug output:</h3>
<pre>
{{mainCtrl.normalForm |json}}
</pre>
</div>

AngularJS - Fill input text field with dropdown menu

How can I fill an input text field with a dropdown-menu ?
Text Input:
<input type="text" ng-model="storagePlaceModel" lms-dropdown class="form-control lms-input-text-disable lms-dropdown-toggle" id="storagePlace" placeholder="Standort" autocomplete="off" readonly>
Own written dropdown:
<div class="lms-dropdown">
<div class="lms-dropdown-scrollbar">
<li ng-repeat="data in data_input_fields.input_cat_music_book_storage_place">
<span>{{data.input_value}}</span>
<i ng-show="storagePlaceModel == data.input_value" class="glyphicon glyphicon-ok"></i>
</li>
</div>
</div>
If I select an li element I want to update the storagePlaceModel with the li value.
Updated question:
Because I have more than one of these text inputs with dropdowns I need a solution where the conroller/directive does not know the model name exactly.
Directive could look like:
lmsApp.directive('updateInputField', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
$(elem).click(function(e) {
// Read out the model name of the html text input field
});
}
};
});
Thank you for your help! I would appreciate every answer.
I've edited the entire question to create a directive to wrap your desired structure. You'll pass to the directive the model you want, and that way, each model will be independent on different directive usages:
myApp.directive('myDirective', function() {
return {
restrict: "E",
scope: {
model: "=",
datas: "="
},
templateUrl: "directive.html",
link: function(scope, element, attr) {
scope.updateValue = function(val) {
scope.model.storagePlaceModel = val;
}
}
}
});
The directive.html contains your text input and the dropdown.
Controller:
function MyCtrl($scope) {
$scope.wrapper = {};
$scope.wrapper2 = {};
$scope.wrapper3 = {};
$scope.datas = [
{ "input_value" : "1" },
{ "input_value" : "2" },
{ "input_value" : "3" },
{ "input_value" : "4" }
];
}
HTML usage:
<div ng-app="myApp" ng-controller="MyCtrl">
<my-directive model="wrapper" datas="datas"></my-directive>
<my-directive model="wrapper2" datas="datas"></my-directive>
<my-directive model="wrapper3" datas="datas"></my-directive>
</div>
Working Fiddle

Wrapping Angular.js ui-bootstrap or ui-select directives in own directive

I am creating a big Angular.JS application which uses some third party modules like ui-select and ui-bootstrap.
To avoid repeating myself I started to create directives which are wrapping for example ui-select code and the logic to retrieve / search for data.
Goal:
The goal was to create a directive which can be used in the template like that, without duplicating code in controllers:
<tq-car-select ng-model="model.car"></tq-car-select>
What I try to avoid:
<select ng-options="car.id as car.name for car in cars"></select>
and duplicate following code in all controllers which are using the select:
$scope.cars = carResource.query();
$scope.$watch('model'), function (newValue, oldValue) {
$scope.cars = carResource.query({model: $scope.model});
});
I created directives for that kind of select fields.
Actual Example with ui-select:
tq-lead-select.html:
<ui-select ng-model="$parent.tqModel" style="display: block">
<ui-select-match placeholder="tippen ...">{{$select.selected.bezeichnung}}</ui-select-match>
<ui-select-choices repeat="lead in leads | filter:{bezeichnung: $select.search}">
<div ng-bind-html="lead.bezeichnung | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
tqLeadSelect.ts (TypeScript):
export function tqLeadSelect(tqLeadSelects): ng.IDirective {
var dir: ng.IDirective = {};
dir.scope = {
tqModel: '=',
tqCompany: '='
};
dir.restrict = 'E';
dir.templateUrl = '/js/templates/leadApp/tq-lead-select.html';
dir.replace = false;
dir.controller = function ($scope: any) {
if (tqLeadSelects != null && $scope.tqCompany != null) {
$scope.leads = tqLeadSelects.getLeadsFromFirma({ id: $scope.tqCompany });
}
$scope.$watch('tqCompany', (newValue, oldValue) => {
if (newValue === oldValue) return;
$scope.leads = tqLeadSelects.getLeadsFromFirma({ id: $scope.tqCompany });
}, true);
}
return dir;
}
tqLeadSelect.$inject = ['tqLeadSelects'];
Problems:
I need isolated scope, because some views use multiple instances of one field.
I am using the isolated scope variable tqModel which is set by the ngModel of the ui-select directive
I would like to use ng-required without creating a tq-required scope variable on tqLeadSelect directive
Questions:
Am I doing it right? Are there better ways achieving my goals?
how are you defining select fields with supporting controller code for retrieving data and additional functions?
One solution would be to add a directive which extends the existing directive.
I created a Plunker with an example: http://plnkr.co/edit/9IZ0aW?p=preview
Following Code:
HTML:
<ui-select ng-model="address.selected" theme="bootstrap" ng-disabled="disabled" reset-search-input="false" style="width: 300px;">
<ui-select-match placeholder="Enter an address...">{{$select.selected.formatted_address}}</ui-select-match>
<ui-select-choices repeat="address in addresses track by $index" refresh="refreshAddresses($select.search)" refresh-delay="0">
<div ng-bind-html="address.formatted_address | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
Controller:
$scope.address = {};
$scope.refreshAddresses = function(address) {
var params = {
address: address,
sensor: false
};
return $http.get(
'http://maps.googleapis.com/maps/api/geocode/json', {
params: params
}
).then(function(response) {
$scope.addresses = response.data.results
});
};
can be simplified by using a configuration directive:
<ui-select ng-model="adress.selected" tq-select></ui-select>
Controller is now empty!
Directive:
app.directive("tqSelect", function($http) {
return {
restrict: "A", // Attribute
require: ["uiSelect", "ngModel"],
compile: function compile(tElement, tAttrs, transclude) {
// Add the inner content to the element
tElement.append('<ui-select-match placeholder="Enter an address...">{{$select.selected.formatted_address}}</ui-select-match>\
<ui-select-choices repeat="address in addresses track by $index" refresh="refreshAddresses($select.search)" refresh-delay="0">\
<div ng-bind-html="address.formatted_address | highlight: $select.search"></div>\
</ui-select-choices>');
return {
pre: function preLink(scope, iElement, iAttrs, controller) {},
post: function postLink(scope, iElement, iAttrs, controller) {
// logic from controller
scope.address = {};
scope.refreshAddresses = function(address) {
var params = {
address: address,
sensor: false
};
return $http.get(
'http://maps.googleapis.com/maps/api/geocode/json', {
params: params
}
).then(function(response) {
scope.addresses = response.data.results
});
};
}
}
}
}
});
The directive is the actual tricky part. I am using a not trivial logic in the compile function. First I add the required markup for the ui-select directive.
Then in the post-link function I added the logic which is normally in the controller (or in the link()-function).

angular: how to handle code provided by attributes

As an example of what I want, consider the following example
<select ng-options="option.text for option in options"></select>
In my directive I want to use something similar to ngOptions, because I need to create a list
For example, assume I have a directive barFoo, called as follows:
<bar-foo options="options"></bar-foo>
with a template/html as follows:
<ol>
<li ng-repeat="option in options" ng-bind="option.text"></li>
</ol>
What is needed to change all this into a call like
<bar-foo options="option.text for option in options"></bar-foo>
The main reason I need this is because I don't know the property name holding the label text (in this case it is text)
I provided a fiddle and see whether this helps. Instead of passing in "options.text for option in options", I set it up such that you pass the "options" array and then the field you want. I assumed the field will be set up as a variable; if it hard-coded, then you can just do field='someFieldName' instead.
http://jsfiddle.net/y376K/1/
HTML
<body ng-app='testApp'>
<div ng-controller='TestCtrl'>
<bar-foo options='options' field='{{optionsField}}'></bar-foo>
</div>
</body>
JS
angular.module('testApp', [])
.controller('TestCtrl', function($scope) {
$scope.options = [
{
text: 'Node.js rocks my socks',
language: 'Node.js',
},
{
text: 'Angular is hot',
language: 'Angular.js',
},
{
text: 'Backbone.js is mmmm',
language: 'Backbone.js',
}
];
$scope.optionsField = 'text';
})
.directive('barFoo', function() {
return {
restrict: 'E',
scope: {
options: '=',
field: '#'
},
template: '<ol><li ng-repeat="option in options" ng-bind="option[field]"></li>'
};
})
You can do this by parsing the attribute. The other solution would be to pass it as two attributes (see the other answer)
You should probably use a regexp for this, but I coded this quickly:
app.directive('barFoo',function($parse) {
return {
restrict: 'E',
scope: {},
templateUrl: "template.html",
link: function(scope,element,attrs) {
var splitOptions = attrs.options.split(' for ');
scope.fieldName = splitOptions[0].split('.')[1];
var repeatExp = splitOptions[1];
scope.valueName = repeatExp.split(' in ')[0];
var collectionName = repeatExp.split(' in ')[1];
scope.values = $parse(collectionName)(scope.$parent);
}
};
});
See this plnkr

Categories