ng-switch in custom angular directive breaks two way binding - javascript

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.

Related

How to Pass data from directive template to controller?

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

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

Inside Directive click function add TextBox elements with different scope

I am trying to create a directive , on click of button i need to add text box but when i add 2,3 textbox they all share same scope.
How can i isolate the scope inside directive ??
http://jsfiddle.net/A8Vgk/584/
Code::
angular.module('myApp', []).directive( 'test', function ( $compile ) {
return {
restrict: 'E',
scope: { text: '#' },
template: '<p ng-click="add()">Click me </p>',
controller: function ( $scope, $element ) {
$scope.add = function () {
var el = $compile( "<input type='text' ng-model='user.name' value='hello-World!'>" )( $scope );
$element.parent().append( el );
};
}
};
});
Try $scope.$new() and bind your textbox to this newly created scope:
var el = $compile( "<input type='text' ng-model='user.name' value='hello-World!'>" )( $scope.$new() );
DEMO

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

AngularJS click to edit fields such as dropdown

I stumbled upon this article on how to build a click to edit feature for a form. The author states:
What about if you wanted input type="date" or even a select? This
is where you could add some extra attribute names to the directive’s
scope, like fieldType, and then change some elements in the template
based on that value. Or for full customisation, you could even turn
off replace: true and add a compile function that wraps the necessary
click to edit markup around any existing content in the page.
While looking through the code I cannot seem to wrap my head around how I could manipulate the template in such a way that I could make it apply to any angular component, let alone how I can make it apply to a drop down list. Code from article below:
app.directive("clickToEdit", function() {
var editorTemplate = '<div class="click-to-edit">' +
'<div ng-hide="view.editorEnabled">' +
'{{value}} ' +
'<a ng-click="enableEditor()">Edit</a>' +
'</div>' +
'<div ng-show="view.editorEnabled">' +
'<input ng-model="view.editableValue">' +
'Save' +
' or ' +
'<a ng-click="disableEditor()">cancel</a>.' +
'</div>' +
'</div>';
return {
restrict: "A",
replace: true,
template: editorTemplate,
scope: {
value: "=clickToEdit",
},
controller: function($scope) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
};
}
};
});
My question is, how can we extend the above code to allow for drop down edits? That is being able to change to the values that get selected.
One approach you might consider is using template: function(tElement,tAttrs ).
This would allow you to return appropriate template based on attributes.
app.directive("clickToEdit", function() {
return {
/* pseudo example*/
template: function(tElement,tAttrs ){
switch( tAttrs.type){
case 'text':
return '<input type="text"/>';
break;
}
},....
This is outlined in the $compile docs

Categories

Resources