How to Pass data from directive template to controller? - javascript

I am building some kind of date-picker, which is actually 2 date pickers.
one for start date and the other for end date.Every datepicker element generate a template of 2 input tags(). I want to pass data from input's value attribute to the controller.
I have tried to define fields in the inner scope which are 2-way data binding(dateOne and dateTwo) but apparently no effect and no real data is pass between the 2 fields.
My other approach is using ng-model, but I have little exprience with this feature and I don't know the rules for that.
Here is my code
angular.module('directives', [])
.directive('datepicker', ['$timeout',function ($timeout) {
// Runs during compile
return {
scope: {
id: '#',
"class": '#',
dateOne: '=',
dateTwo: '='
},
restrict: 'E', // E = Element, A = Attribute, C = Class, M = Comment
template: '<div id="{{id}}" class="{{class}}">'+
'<div class="date-wrapper">'+
'<label for="datepicker-start">From:</label>'+
'<div class="fieldWrapper">'+
'<input id="datepicker-start" type="date" placeholder="Select date" value={{dateOne}} />'+
'<a class="calendar"></a>'+
'</div>'+
'</div>'+
'<div class="date-wrapper">' +
'<label for="datepicker-end">To:</label>' +
'<div class="fieldWrapper">' +
'<input id="datepicker-end" type="date" placeholder="Select date" value={{dateTwo}}/>' +
'<a class="calendar"></a>' +
'</div>' +
'</div>'+
'</div>'
,
replace: true,
link: function($scope, iElm, iAttrs, controller) {
console.log('directive link function');
console.log('directive iAttrs', iAttrs);
$(".date-wrapper").each(function (index) {
console.log('directive index', index);
$input = $(this).find('input');
$btn = $(this).find('.calendar');
console.log('input', $input[0]);
console.log('btn', $btn[0]);
$input.attr('type', 'text');
var pickerStart = new Pikaday({
field: $input[0],
trigger: $btn[0],
container: $(this)[0],
format: 'DD/MM/YYYY',
firstDay: 1
});
$btn.show();
});
}
};
}]);
------------------------Updated Code -----------------------------------
angular.module('directives', [])
.directive('datepicker', ['$timeout',function ($timeout) {
// Runs during compile
return {
scope: {
id: '#',
"class": '#',
dateOne: '=',
dateTwo: '='
},
restrict: 'E', // E = Element, A = Attribute, C = Class, M = Comment
template: '<div id="{{id}}" class="{{class}}">'+
'<div class="date-wrapper">'+
'<label for="datepicker-start">From:</label>'+
'<div class="fieldWrapper">'+
'<input id="datepicker-start" type="date" placeholder="Select date" ng-model=dateOne />' +
'<a class="calendar"></a>'+
'</div>'+
'</div>'+
'<div class="date-wrapper">' +
'<label for="datepicker-end">To:</label>' +
'<div class="fieldWrapper">' +
'<input id="datepicker-end" type="date" placeholder="Select date" ng-model=dateTwo />' +
'<a class="calendar"></a>' +
'</div>' +
'</div>'+
'</div>'
,
replace: true,
link: function($scope, iElm, iAttrs, controller) {
console.log('directive iAttrs', iAttrs);
$(".date-wrapper").each(function (index) {
console.log('directive index', index);
$input = $(this).find('input');
$btn = $(this).find('.calendar');
console.log('input', $input[0]);
console.log('btn', $btn[0]);
$input.attr('type', 'text');
var pickerStart = new Pikaday({
field: $input[0],
trigger: $btn[0],
container: $(this)[0],
format: 'DD/MM/YYYY',
firstDay: 1
});
$btn.show();
});
$scope.$watch(iAttrs.dateOne, function (newValue, oldValue) {
console.log('newValue', newValue);
console.log('oldValue', oldValue);
}, true);
}
};

Actually you are almost there, I've done something very similar to what you have described and here was my approach to solve it (I used the UI-Bootstrap Date picker in my case).
The way you would send data from your directive to your controller is by using callbacks, rather than simple watches. If you would have used = you would have to set watches in your controller (and directive) to watch for value changes, it's bad practice overall and extra code.
So basically what you need to do is
In you directive definition object bind a callback method/function using the & sign like so
scope: {
onSelect: "&" // onSelect is our callback function in the ctrl
}
You then supply the callback attribute a function bound to the controller's $scope, but you pass it a function reference (not a function call as you would in something like ng-changed). like so
<my-directive on-selected="onSelected"></my-directive>
Then you define what onSelected should do, lets say I want to print the selected date
// inside controller
$scope.onSelected = function(time) {
console.log("Time selected: ", time);
}
Note that we pass the time argument from the directive to the controller like so, scope.onSelect() is actually a curried function, meaning it will return a function once called (that is, if you provided it with a function, you could test it using angular.isFunction), so you should call the curried function and provide it your argument, scope.onSelect()(time).
scope.selectDate = function(time) {
if (angular.isFunction(scope.onSelect())) {
// we use isFunction to test if the callback function actually
// points to a valid function object
scope.onSelect()(time); // we pass out new selected date time
}
}
Here is a plunk that shows what I mean.

Replace value in the template with ng-model=dateOne and the same with dateTwo.
I suggest to use a dedicated controller for this directive and not doing the logic inside the link function.
app.directive('someDirective', function () {
return {
restrict: 'A',
controller: 'SomeController',
controllerAs: 'ctrl',
template: '{{ctrl.foo}}'
};
});
Read more here http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html

Related

jQuery doesn't select the element with dynamic ID, how to do that?

I am having a problem when I pass the ID through a directive. I can't obtain the element using jQuery inside the Link function, and the element is using the correct dynamic ID coming as parameter:
The Directive:
(function(angular) {
var app = angular.module('pi.core');
app.directive('piSearch', function() {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
idelement: '#'
},
link: function(scope, element, attrs, controller, transcludeFn) {
var idelement = scope.idelement;
console.log('idElement: ' + idelement);
console.log($('#' + idelement + ' .typeahead'));
},
template: '<div id="{{idelement}}"></div>'
};
});
})(angular);
var myApp = angular.module('piCore', []);
myApp.directive("piSearch", function() {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
idelement: '#'
},
link: function(scope, element, attrs, controller, transcludeFn) {
var idelement = scope.idelement;
scope.elementSelected = $('#' + idelement + ' .typeahead');
console.log('idElement: ' + idelement);
console.log($('#' + idelement + ' .typeahead'));
},
template: '<div id="{{idelement}}"></div>'
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body ng-app="piCore">
<pi-search></pi-search>
{{$scope.elementSelected}}
</body>
Any hints? Thanks in advance for your help!
I'll refactor your link function, remember angular has their own life cycle, and you need to make sure that your template compiles when your model has a value, wrap your logic in a watch
app.directive('piSearch', function() {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
idelement: '#'
},
link: function(scope, element, attrs, controller, transcludeFn) {
var idelement = scope.idelement;
scope.$watch('idelement',function(){
scope.elementSelected = $('#' + idelement + ' .typeahead');
console.log('idElement: ' + idelement);
console.log($('#' + idelement + ' .typeahead'));
});
},
template: '<div id="{{idelement}}"></div>'
};
});
try to use angular.element("#"+ idelement);
and also make sure this template: '<div id="{{idelement}}"></div>' is not generated multiple times
One thing you must know regarding dynamic ID's and elements in global:
1) First you must generate your dynamic HTML inside javascript
2) Print it, append or just insert somwhere in the DOM
3) Now to can access to that new element you must use document to read that new element like this:
document.getElementById('new-generic-id');
or
$(document).on('click touchstart select', '#new-generic-id', function(){});
I hope you understand.

ng-switch in custom angular directive breaks two way binding

I created my custom directive to encapsulate an uib-datepicker-popup:
'use strict';
angular.module( 'frontendApp' )
.directive( 'inputDate', function(){
var controller = function(){
var vm = this;
function init() {
vm.formats = [ 'dd.MMMM yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate' ];
vm.format = vm.formats[ 0 ];
vm.altInputFormats = [ 'M!/d!/yyyy' ];
vm.dateOptions = {
datepickerMode: 'day',
formatYear: 'yy',
maxDate: new Date(),
minDate: new Date( 1900, 1, 1 ),
startingDay: 1
};
vm.datepicker = {
opened: false
};
};
init();
vm.showDatePicker = function(){
vm.datepicker.opened = true;
};
};
var template = '<div ng-switch on="readonly" >' +
'<div ng-switch-when="true" class="form-control" readonly>' +
'<div readonly name="readonlyText">{{ngModel | date : \'d.MMMM yyyy\'}}</div>' +
'</div>' +
'<div ng-switch-default class="input-group">' +
'<input class="form-control" type="text" uib-datepicker-popup="{{vm.format}}" ng-model="ngModel" ng-model-options="{timezone:\'UTC\'}" is-open="vm.datepicker.opened" datepicker-options="vm.dateOptions" ng-required="true" show-button-bar="false" alt-input-formats="vm.altInputFormats" />' +
'<span class="input-group-btn">' +
'<button type="button" class="btn btn-default" ng-click="vm.showDatePicker()"><i class="glyphicon glyphicon-calendar"></i></button>' +
'</span>' +
'</div>' +
'</div>';
return{
controller: controller,
controllerAs: 'vm',
bindToController: true,
template: template,
restrict: 'EA',
scope :true,
require:'ngModel',
link: function( scope, element, attrs, ngModel ){
// Bring in changes from outside:
scope.$watch( 'ngModel', function(){
if( ngModel ) {
scope.$eval( attrs.ngModel + ' = ngModel' );
}
} );
// Send out changes from inside:
scope.$watch( attrs.ngModel, function( val ){
if( val ) {
scope.ngModel = val;
}
} );
if( attrs.readonly === 'true' ) {
scope.readonly = true;
}
}
};
} );
The html part then is:
<input-date ng-model="form.flight.date"></input-date>
The problem: if the popup shows up, scope.ngModel is initialized correctly from attrs.ngModel. I had a log inside the watcher that showed me that watching attrs.ngModel works perfecly, but watching 'ngModel' or scope.ngModel does only work until i use the datepicker. It works perfectly as long as the datepicker is not triggered.
Just discovered that it works perfectly if i remvoe the
"ng-switch-default". Replacing it with ng-show/ng-hide makes the directive work completely as expected.
Can anyone explain why?
The behavior you see is absolutely correct. When you use structural directives like ng-if, ng-switch, ng-repeat etc. it creates a new scope and copies all attributes of the parent scope. Your model is a primitive (string), so it is fully copied to the new scope and changed within this scope without propagation to the parent one.
What you can do is:
Use object instead of string to pass the ng-model, what I personally find here very awkward
Use ng-model from controller object and not from the scope
Going on with the second approach: you already use bindToController and an isolated scope by scope: true, so just instead of tracking the model with watcher bind it to the controller:
return {
bindToController: true,
scope: {
ngModel: '='
},
...
so ideally you won't even need your link function and in the template instead of
'<div readonly name="readonlyText">{{ngModel | date : \'d.MMMM yyyy\'}}</div>'
use
'<div readonly name="readonlyText">{{vm.ngModel | date : \'d.MMMM yyyy\'}}</div>'
Why ng-hide still works? It does not create a new scope.

Is the way of creating a directive scope the same in versions 1.3 and 2 of AngularJS?

I have the following directive that my application is using. I was under the impression that my application was working fine with AngularJS 1.3 but after a lot of changes including a move to the latest version, the removal of jQuery, and also the use of controller as then now this directive is giving me errors:
app.directive('pagedownAdmin', function ($compile, $timeout) {
var nextId = 0;
var converter = Markdown.getSanitizingConverter();
converter.hooks.chain("preBlockGamut", function (text, rbg) {
return text.replace(/^ {0,3}""" *\n((?:.*?\n)+?) {0,3}""" *$/gm, function (whole, inner) {
return "<blockquote>" + rbg(inner) + "</blockquote>\n";
});
});
return {
require: 'ngModel',
replace: true,
scope: {
modal: '=modal'
},
template: '<div class="pagedown-bootstrap-editor"></div>',
link: function (scope, iElement, attrs, ngModel) {
var editorUniqueId;
if (attrs.id == null) {
editorUniqueId = nextId++;
} else {
editorUniqueId = attrs.id;
}
var newElement = $compile(
'<div>' +
'<div class="wmd-panel">' +
'<div data-ng-hide="modal.wmdPreview == true" id="wmd-button-bar-' + editorUniqueId + '"></div>' +
'<textarea data-ng-hide="modal.wmdPreview == true" class="wmd-input" id="wmd-input-' + editorUniqueId + '">' +
'</textarea>' +
'</div>' +
'<div data-ng-show="modal.wmdPreview == true" id="wmd-preview-' + editorUniqueId + '" class="pagedownPreview wmd-panel wmd-preview">test div</div>' +
'</div>')(scope);
iElement.html(newElement);
var help = function () {
alert("There is no help");
}
var editor = new Markdown.Editor(converter, "-" + editorUniqueId, {
handler: help
});
var $wmdInput = iElement.find('#wmd-input-' + editorUniqueId);
var init = false;
editor.hooks.chain("onPreviewRefresh", function () {
var val = $wmdInput.val();
if (init && val !== ngModel.$modelValue) {
$timeout(function () {
scope.$apply(function () {
ngModel.$setViewValue(val);
ngModel.$render();
});
});
}
});
ngModel.$formatters.push(function (value) {
init = true;
$wmdInput.val(value);
// editor.refreshPreview();
return value;
});
editor.run();
}
}
});
Can someone explain to me what the following is doing:
scope: {
modal: '=modal'
},
and also the
)(scope);
Here is how I am calling this directive:
<textarea id="modal-data-text"
class="pagedown-admin wmd-preview-46"
data-modal="modal"
data-pagedown-admin
ng-model="home.modal.data.text"
ng-required="true"></textarea>
If anyone can see anything that may not work in 2 then I would much appreciate some help. In particular it seems that the following code returns null:
var $wmdInput = iElement.find('#wmd-input-' + editorUniqueId);
You dropped jQuery, so your code now relies on jQLite. Functions of element objects support less functionality when using jqLite. See the full details in the doc:
https://docs.angularjs.org/api/ng/function/angular.element
var $wmdInput = iElement.find('#wmd-input-' + editorUniqueId);
Under jqLite, the find function only support searching by tag names, ids will not work. You can use the following tricks from ( AngularJS: How to .find using jqLite? )
// find('#id')
angular.element(document.querySelector('#wmd-input-' + editorUniqueId))
$compile is a service that will compile a template and link it to a scope.
https://docs.angularjs.org/api/ng/service/$compile
scope: {
modal: '=modal'
}
allows you to define a isolated scope for the directive with some bindings to the scope in which the directive is declared. '=' is used for two-way data bindings. Other options are '# and &' for strings and functions.
https://docs.angularjs.org/guide/directive

AngularJS input ng-model not updating

I am trying to create a simple pagination directive with an isolated scope. For some reason when I manually change the value it gets a bit finnicky. Here is my problem:
When I page forward and backward, it works great. Awesome
When I enter a page into the field it works. Great
However, if I enter a page into the field and then try to go forward and backward, the ng-model seems to break after I enter a page into the field. I had it working when I did not isolate my scope but I am confused as to why it would break it. Here is my code:
HTML:
<paginate go-to-page="goToPage(page)" total-pages="results.hits.pages" total-hits="results.hits.total"></paginate>
Directive:
'use strict';
angular.module('facet.directives')
.directive('paginate', function(){
return {
restrict: 'E',
template: '<div class="pull-right" ng-if="(totalPages !== undefined) && (totalPages > 0)">'+
'<span class="left-caret hoverable" ng-click="changePage(current-1)" ng-show="current > 1"></span> Page'+
' <input type="number" ng-model="current" class="pagination-input" ng-keypress="enterPage($event)"/> of'+
' {{totalPages}} '+
'<span class="right-caret hoverable" ng-click="changePage(current+1)" ng-show="current < totalPages"></span>'+
'</div>',
scope: {
goToPage: '&',
totalPages: '=',
totalHits: '='
},
link: function(scope) {
scope.current = 1;
scope.changePage = function(page) {
scope.current = page;
window.scrollTo(0,0);
scope.goToPage({page:page});
};
scope.enterPage = function(event) {
if(event.keyCode == 13) {
scope.changePage(scope.current);
}
}
}
}
});
What am I doing wrong?
Beware of ng-if - it creates a new scope. If you change it to just ng-show, your example would work fine. If you do want to use ng-if, create a object to store the scope variable current. Maybe something like scope.state.current?
scope.state = {
current: 1
};
To avoid confusion like this, I always keep my bindings as something.something and never just something.
Edit: Good explanation here - http://egghead.io/lessons/angularjs-the-dot
Please always try to use model rather than using primitive types while using the ng-model because of the javascript's prototypical hierarchies.
angular.module('facet.directives').directive('paginate', function () {
return {
restrict: 'E',
replace: true,
template: '<div class="pull-right discovery-pagination" ng-if="(totalPages !== undefined) && (totalPages > 0)">' +
'<span class="left-caret hoverable" ng-click="changePage(current-1)" ng-show="current > 1"></span> Page' +
' <input type="number" ng-model="current.paging" class="pagination-input" ng-keypress="enterPage($event)"/> of' +
' {{totalPages}} ' +
'<span class="right-caret hoverable" ng-click="changePage(current+1)" ng-show="current < totalPages"></span>' +
'</div>',
scope: {
goToPage: '&',
totalPages: '=',
totalHits: '='
},
link: function(scope) {
scope.current = {paging:1};
scope.changePage = function(page) {
scope.current.paging = page;
window.scrollTo(0, 0);
scope.goToPage({ page: page });
};
scope.enterPage = function(event) {
if (event.keyCode == 13) {
scope.changePage(scope.current.paging);
}
};
}
};
});
Hope this will solve your problem :)
For detail about this, please go through Understanding-Scopes

How can I use ng-repeat and a previously-declared function within the template of an AngularJS directive?

Using AngularJS I'm attempting to create a directive to templatize questions in a form. For a particular type of question, I want to create radio buttons for each number within a certain range. I have an external function that returns an array of numbers given a lower and upper bound.
How can I use this external function, together with ng-repeat, to templatize this type of question? Here's the code I've tried so far...
HTML:
<qtnrange qtn-variable="cxnTestVarA" first-num="1" last-num="5">
This is a test question. Pick a number between 1 and 5.</qtnrange>
<hr>
You picked {{cxnTestVarA}}.
JS:
var module = angular.module('app', [])
.directive('qtnrange', function() {
return {
scope: {
qtnVariable: '=',
firstNum: '=',
lastNum: '=',
bounds: '&range',
},
template:
'<div class=question>' +
'<label ng-transclude></label>' +
'<label class="radio inline" ng-repeat="iter in bounds(firstNum, lastNum)"><input type="radio" value="{{iter}}" ng-model="qtnVariable">{{iter}} </label>' +
'<hr></div>',
restrict: 'E', // must be an HTML element
transclude: true,
replace: true,
};
});
var range = function(start, end, step) {
... function that returns an array []
}
Here's a working example of your jsFiddle: http://jsfiddle.net/daEmw/3/
What I did was moving the range function onto the scope, and to use ng-click on the radio inputs instead of binding them with ng-model.
Basically, changing this:
module.directive('qtnrange', function() {
return {
scope: {
qtnVariable: '=',
firstNum: '=',
lastNum: '=',
bounds: '&range',
},
template:
'<div class=question>' +
'<label ng-transclude></label>' +
'<label class="radio inline" ng-repeat="iter in bounds(firstNum, lastNum)"><input type="radio" value="{{iter}}" ng-model="qtnVariable">{{iter}} </label>' +
'<hr></div>',
restrict: 'E', // must be an HTML element
transclude: true,
replace: true,
};
});
var range = function() {
}
into this:
module.directive('qtnrange', function() {
return {
scope: {
qtnVariable: '=',
firstNum: '=',
lastNum: '='
},
template:
'<div class=question>' +
'<label ng-transclude></label>' +
'<label class="radio inline" ng-repeat="iter in range(firstNum, lastNum)"><input type="radio" value="{{iter}}" ng-click="setNum(item)">{{iter}} </label>' +
'<hr></div>',
restrict: 'E', // must be an HTML element
transclude: true,
replace: true,
controller: function($scope) {
$scope.setNum = function(num) {
$scope.qtnVariable = num;
}
$scope.range = function() {
// ...
}
}
};
});

Categories

Resources