$scope.$watch doesn't fire when I update from a directive - javascript

I have the following code snippets:
HTML:
<div data-filedrop data-ng-model="file"></div>
Controller:
$scope.$watch('file', function(newVal) {
if (newVal) {
alert("File",newVal);
}, false);
}
Directive:
angular.module('app').directive('filedrop', function () {
return {
restrict: 'A',
templateUrl: './directives/filedrop.html',
replace: true,
scope: {
ngModel: '=ngModel'
},
link: function (scope, element) {
var dropzone = element[0];
dropzone.ondragover = function () {
this.className = 'hover';
return false;
};
dropzone.ondragend = function () {
this.className = '';
return false;
};
dropzone.ondrop = function (event) {
event.preventDefault();
this.className = '';
scope.$apply(function () {
scope.ngModel = event.dataTransfer.files[0];
});
return false;
};
}
};
});
The $watch function is never triggered when I update the $scope.
Any Ideas?? Might be an isolated scope issue? It used to work until yesterday... when I had to redo
bower install && npm install
I can confirm:
dropzone.ondrop is fired
event.dataTransfer.files[0] does contain the file being dropped
because of the bower install I also tried angular 2.1.14, 2.1.15 and 2.1.16 (current) but none are working
Thanks!
Sander

ngModel is a controller/provider, it's not a scope. It's not identical to using a scope like in a controller in any way whatsoever. You have to use ngModel.$setViewValue('some value') to manipulate the value. You also have to add the ngModel like this:
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
// do some stuff
ngModel.$setViewValue(element.html()); // example
}
I found a good tutorial which describes this perfectly: http://suhairhassan.com/2013/05/01/getting-started-with-angularjs-directive.html#.U1jme-aSzQ4
Another option would of course be to just pass a scope variable like this:
Directive:
scope: {
'someAttribute': '='
},
link: function(scope, element) {
dropzone.ondrop = function(event) {
scope.$apply(function() {
scope.someAttribute = event.dataTransfer.files[0];
});
}
}
Controller View:
<div filedrop some-attribute="mymodel"></div>
Controller:
$scope.$watch('mymodel', function(newVal) {
// yeah
});

It seems that you are not modifying the value of scope.ngModel. Instead you are overwriting variable scope.ngModel so that it points to the different object, namely: event.dataTransfer.files[0]

Related

AngularJS directive that will look at the value of my ngModel then fire off a function in the controller and be available immediately in view

I am trying to use a $scope.quickText(data) function in my controller. The function reviews the parameter 'data' and looks for any codes (ie: .smoke) and then adds that text to the value of the model.
For instance, if the ngModel value was "Completed smoke assessment" and someone types into the 'textarea' or 'text' input .smoke, it would add "patient smokes. Completed smoke assessment". This would be available to see in the view instantly as the user is typing .smoke. The function works but my directive does not.
myApp.directive('gmaEvalQuickText1', ['$timeout', function ($timeout) {
'use strict';
return {
restrict: 'A',
require: 'ngModel',
scope: {
quickTextEvaluate: '&',
},
bindToController: true,
controller: 'gmaController',
controllerAs: 'gc',
link: function ($elem, $ctrl,controller) {
$elem.on('input keyup change', function () {
var val = $elem.val().toString();
var newVal = gc.quickText(val).toString();
$ctrl.$setViewValue(newVal);
$timeout(function () {
$ctrl.$render();
});
});
}
}
}]);
I am very new to AngularJS so I am sure I am doing something wrong.
I figured out how to make it work :)
For those who need the answer:
Directive:
myApp.directive('evalQuickText', ['$timeout', function ($timeout) {
'use strict';
return {
restrict: 'A',
require: 'ngModel',
scope: {
quicktextevalfct: '='
},
link: function ($scope, $elem, attrs, $ctrl) {
$elem.on("keydown keypress", function (event) {
if(event.which === 13) {
var val = $elem.val().toString();
var newVal = $scope.quicktextevalfct(val);
$ctrl.$setViewValue(newVal + "\n");
$timeout(function () {
$ctrl.$render();
});
event.preventDefault();
}
if(event.which === 9) {
var val = $elem.val().toString();
var newVal = $scope.quicktextevalfct(val);
$ctrl.$setViewValue(newVal);
$timeout(function () {
$ctrl.$render();
});
event.preventDefault();
}
});
}
};
}]);
HTML:
eval-quick-text quicktextevalfct="quickTextEvaluate"

change directive when scope variable changes

I am using http request to get data from json file which I than use in controller.
app.controller('mainCtrl', ['$scope', 'loaderService', function ($scope, loaderService) {
//gets data from service
loaderService.getLoadedHtml().then(function (result) {
$scope.fields = result.data;
});
}]);
I need to update directive when this $scope.fields change as
app.directive('dform', function () {
return {
scope: {
action: '#',
method: '#',
html: '='
},
link: function (scope, elm, attrs) {
var config = {
"html": scope.fields
};
scope.$watch('fields', function (val) {
elm.dform(config);
});
//console.log(config);
//elm.dform(config);
}
};
})
and here is how I am using this directive
<div html="fields" dform></div>
But in my case when $scope.fields changes, i get scope as undefined in my directive $watch function.
Question:
How can I get the updated value for scope.fields in scope.$watch function?
You need to give the directive access to fields by adding a binding for it:
scope: {
action: '#',
method: '#',
html: '=',
fields: '='
}
And HTML:
<dform fields="fields" ...
The value might be undefined the first time, then you don't want to call dform:
scope.$watch('fields', function(newValue, oldValue) {
if (newValue === oldValue) return;
var config = {
"html": newValue
};
elm.dform(config);
});
Update
With this HTML:
<div html="fields" dform></div>
You just need to watch html instead, no need for $parent or adding fields as a binding:
scope.$watch('html', ...
Usually for directives that should be as transparent as possible, no new scope is supposed be used. Having a new scope also prevents other directives from requesting a new scope on the same element.
If only one of the attributes is supposed to be dynamic, it is as simple as
scope: false,
link: function (scope, elm, attrs) {
scope.$watch(function () { return scope[attrs.html] }, function (val) {
if (val === undefined) return;
var config = {
action: attrs.action,
method: attrs.method,
html: val
};
elm.dform(config);
});
}
Alternatively, bindToController can be used in more modern, future-proof fashion (depending on what happens with html, $scope.$watch can be further upgraded to self.$onChanges hook).
scope: true,
bindToController: {
action: '#',
method: '#',
html: '='
},
controller: function ($scope, $element) {
var self = this;
$scope.$watch(function () { return self.html }, function (val) {
if (val === undefined) return;
var config = {
action: attrs.action,
method: attrs.method,
html: val
};
$element.dform(config);
});
}
Considering that html="fields", the code above will watch for fields scope property.
use $parent.fields instead of fields

Is there any way to do this with a directive?

I have no problem getting the $scope function referenced in the directive attribute to run. The problem is I need to capture the element that was touched in the directive and pass it to the function in the attribute. I haven't figure out how to do that. Isolating the scope in the directives will throw an error because there's 2 directives trying to act on the same element.
With the below code I am getting $event as undefined and an error of course. Any way to achieve what I want? Or maybe there's a better way?
<li class="list-group-item row" data-touchstart="darkenBackground($event)" data-touchend="lightenBackground($event)">
..
angular.module('myNotes')
.directive('touchstart', [function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
elem.bind('touchstart', function(e) {
e.preventDefault();
scope.$apply(attrs.touchstart);
});
}
};
}]);
angular.module('myNotes')
.controller('notesCtrl', ['$scope', 'Site',
function($scope, Site) {
Site.setTitle('My Notes');
Site.setActiveNavLink('myNotes');
$scope.darkenBackground = function($event) {
angular.element($event.currentTarget)
.css('background-color', '#eee');
};
$scope.lightenBackground = function($event) {
angular.element($event.currentTarget)
.css('background-color', '#fff');
}
}]);
Here's the issue:
scope.$apply(attrs.touchstart);
When you call scope.$apply directly on an angular expression (which is what attrs.touchstart is), it will automatically evaluate that expression against scope, so scope.$event (which is undefined) is passed to the callback.
To pass the event to the callback, you can use the second parameter (called "locals") of scope.$eval to temporarily include an $event property on the scope while evaluating the expression.
return {
restrict: 'A',
link: function (scope, elem, attrs) {
elem.bind('touchstart', function (e) {
e.preventDefault();
scope.$apply(function () {
scope.$eval(attrs.touchstart, {$event: e});
});
});
}
};
I got it working like so, but I'm not sure if it's the best/proper approach:
<li class="list-group-item row" data-touchstart="darkenBackground(event)">
...
angular.module('myNotes')
.directive('touchstart', [function () {
return {
restrict: 'A',
scope: {
touchstart: '&'
},
link: function (scope, elem, attrs) {
elem.bind('touchstart', function(e) {
e.preventDefault();
scope.$apply(scope.touchstart({event: e}));
});
}
};
}]);
angular.module('myNotes')
.controller('notesCtrl', ['$scope', 'Site',
function($scope, Site) {
Site.setTitle('My Notes');
Site.setActiveNavLink('myNotes');
$scope.darkenBackground = function(event) {
var elem = angular.element(event.currentTarget);
elem.css('background-color', '#eee');
elem.bind('touchend', function (e) {
$scope.lightenBackground(e);
})
};
$scope.lightenBackground = function(event) {
angular.element(event.currentTarget)
.css('background-color', '#fff');
};
}]);

angular js directive post link can't access generated element id

I have this directive:
/*html, enclosed in a ng-repeat directive*/
<textarea name="alternativaHtml" id="questao_alternativa_{{$index}}" data-ng-model="alternativa.TextoHtml" data-ck-editor></textarea>
/*javascript*/
angular
.module("fluxo_itens.directives")
.directive('ckEditor', [function () {
return {
require: '?ngModel',
link: {
"post": PostLink
}
};
}]);
function PostLink($scope, elm, attr, ngModel) {
var ck = CKEDITOR.replace(attr.id);
ck.on('pasteState', function () {
$scope.$apply(function () {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function (value) {
ck.setData(ngModel.$modelValue);
};
}
the problem is that when CKEDITOR tries to create the editor instance, it can't find the element, which has its id property dinamycally generated.
Just guessing , but you can try this :
<textarea name="alternativaHtml" id="questao_alternativa_{{$index}}" data-ng-model="alternativa.TextoHtml" data-ck-editor editor-id="questao_alternativa_{{$index}}"></textarea>
Directive
angular
.module("fluxo_itens.directives")
.directive('ckEditor', [function () {
return {
scope : {
'editorId' : '='
}
require: '?ngModel',
link: {
"post": PostLink
}
};
}]);
PostLink
function PostLink($scope, elm, attr, ngModel) {
var ck = CKEDITOR.replace($scope.editorId);
ck.on('pasteState', function () {
$scope.$apply(function () {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function (value) {
ck.setData(ngModel.$modelValue);
};
}
The problem was that CkEditor wasn't able to find the element because the id of the element was not set in the DOM. So I've relied on jQuery to set de element DOM property so the CkEditor can find the textarea.
I've changed this in the PostLink function
var ck = CKEDITOR.replace(attr.id);
to this:
$(elm).attr("id", attr.id).prop("id", attr.id);
var ck = CKEDITOR.replace(elm[0].id);
Now everything works fine

Change a scope value in directive

Whats the best way to assign a new value through a directive? A two way databinding.
I have a fiddle here where i tried. http://jsfiddle.net/user1572526/grLfD/2/ . But it dosen't work.
My directive:
myApp.directive('highlighter', function () {
return {
restrict: 'A',
replace: true,
scope: {
activeInput: '='
},
link: function (scope, element, attrs) {
element.bind('click', function () {
scope.activeInput = attrs.setInput
})
}
}
});
And my controller:
function MyCtrl($scope) {
$scope.active = {
value : true
};
}
And my view:
<h1 highlighter active-input="active.value" set-input="false">Click me to update Value in scope: {{active}}</h1>
So what i wanna do is update the scope.active with the given attribute setInput.
Any ideas what I'm doing wrong here?
With element.bind you leave the realm of Angular, so you need to tell Angular that something had happened. You do that with the scope.$apply function:
scope.$apply(function(){
scope.activeInput = attrs.setInput;
});
here is an updated jsfiddle.

Categories

Resources