Does anyone of you think that the form in angular 1.3 is causing tightly coupling between the controller and the DOM? This is an anti pattern of Angular.
For example, when giving a form the name='formExample' attribute, to set it to dirty or invalid programmily, in the controller we have to do $scope.formExample.$setDirty()
This is a bad practice!
Waiting to hear your thoughts!
Example :
this.onSaveClicked = function () {
that.saveMessage = that.SAVING_IN_PROGRESS;
//Update project with changes
ProjectsBLL.update($scope.entities.project.Model, function (data) {
$scope.entities.project.Model = data;
$scope.configurationForm.$setPristine();
that.saveMessage = that.SAVING_FINISHED;
}, null)
}
It sounds like you don't want the following line in the controller
$scope.configurationForm.$setPristine();
To achieve this, one way is to:
Change ProjectsBLL.update to return a promise rather than accept a callback (how to do this is probably beyond the scope of this question). Then also return the derived promise from then from the function in the controller:
this.onSaveClicked = function () {
that.saveMessage = that.SAVING_IN_PROGRESS;
//Update project with changes
return ProjectsBLL.update($scope.entities.project.Model).then(function (data) {
$scope.entities.project.Model = data;
that.saveMessage = that.SAVING_FINISHED;
});
}
And then use a custom directive rather than ng-click to call the save function. There are probably a few ways of doing this, but one is to add one that takes a function as an attribute:
<button type="button" my-submit="onSaveClicked()">Save</button>
which adds a manual click listener, calls the passed function on click, and after its returned promise is resolved, calls $setPristine() on a requireed form:
app.directive('mySubmit', function($parse) {
return {
require: '^form',
link: function(scope, element, attrs, formController) {
var callback = $parse(attrs.mySubmit);
element.on('click', function() {
callback(scope, {}).then(function() {
formController.$setPristine();
});
});
}
};
});
A version of this, just using a $timeout to simulate a call to $http can be seen at http://plnkr.co/edit/IdcfQ4N9MhDKdvyorzeO?p=preview
Related
I've rendered a chart using highchart.js using solutions from a couple questions. I understand the basic use of directives. However, in the case of highchart.js, I don't quite understand this code here:
app.directive('highchart', function () {
var direc = {};
var link = function (scope, element, attributes) {
scope.$watch(function () {
return attributes.chart;
}, function () {
var charts = JSON.parse(attributes.chart);
$(element[0]).highcharts(charts);
})
}
direc.restrict = 'E';
direc.link = link;
direc.template = '<div></div>';
//the replace method replaces the content inside the element it is called
direc.replace = true;
direc.scope = {};
return direc;
})
The charts attribute will accept a JSON array of chart attributes.
Can someone explain what's happening inside the function? Thank you for reading.
The $watch is used to monitor the changes on a specific field. In the above case the attributes.chart is being watched for changes in the first argument in the $watch function and the second argument works with actually checking the modified data and performing manipulation on it.
You can also find further options that can be used by the $watch in the official angular docs: https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch
$watch monitor the model chnanges, once the model chnages , updated values get from below code and as per requirement, required action can perform.
$scope.$watch('ng-model-name', function (newval, oldval) {
if (newval< 0) {
$('#modelCustomer').modal('show');
}
});
I'm torn between using $rootScope and a global service.
$rootScope feels dirty and comes with all the problem of global variables, so I wanted to use a global service but the Angular FAQ says:
Of course, global state sucks and you should use $rootScope sparingly,
like you would (hopefully) use with global variables in any language.
In particular, don't use it for code, only data. If you're tempted to
put a function on $rootScope, it's almost always better to put it in a
service that can be injected where it's needed, and more easily
tested.
Conversely, don't create a service whose only purpose in life is to
store and return bits of data.
...and that's what I want to use the service for.
For example, I have a navbar that when toggled on mobile screens should add a class to the body and html elements. I handle this with a directive and a controller.
NavbarController:
function NavbarController($rootScope) {
var vm = this;
vm.toggleNav = toggleNav;
$rootScope.navCollapsed = false;
function toggleNav() {
$rootScope.navCollapsed = !$rootScope.navCollapsed;
}
}
As you can see there, I'm using rootScope for the navCollapsed value and in my directive I do this:
function navOpen($rootScope) {
return {
restrict: 'A',
link: function(scope, element) {
scope.$watch(function () {
return $rootScope.navCollapsed;
}, function () {
if ($rootScope.navCollapsed)
element.addClass('nav-open');
else
element.removeClass('nav-open');
});
}
};
}
It works fine, but using $rootScope feels dirty. I would much rather do something like this:
NavbarController:
function NavbarController(globalService) {
var vm = this;
vm.toggleNav = toggleNav;
globalService.navCollapsed = false;
function toggleNav() {
globalService.navCollapsed = !globalService.navCollapsed;
}
}
And in the directive:
function navOpen(globalService) {
return {
restrict: 'A',
link: function(scope, element) {
scope.$watch(function () {
return globalService.navCollapsed;
}, function () {
if (globalService.navCollapsed)
element.addClass('nav-open');
else
element.removeClass('nav-open');
});
}
};
}
Which does the exact same thing except now the value is in a single location and can't be unintentionally altered elsewhere which is better, but according to AngularJS team that is bad practice because it's "bits of data".
What is the best way of going about this? I have other variables with similar functionality that need to be global (able to be accessed in any controller for manipulation), without having to use $rootScope.
New to creating custom directives. It renders fine on the initial render. However, I am trying to $watch for changes to the original data, and then, trigger an update.
As a quick test, I created a button and used jQuery to update the costPerDay.costs array (by hand)...but the $watch still doesn't fire & my breakpoint wasn't reached.
Thanks for the help...
MY CONTROLLER LOOKS LIKE:
The GET is mocked to return an object, not a promise, so ignore that particular line. Once I get the $watch working, I will update this part of the code accordingly.
// CONTROLLER
application.controller('HomeIndexController', function ($scope, costPerDayDataService) {
var vm = this;
// Internal
vm.on = {
databind: {
costPerDay: function () {
// The GET is mocked to return an object, not a promise, so ignore this line
var costPerDay = costPerDayDataService.get();
$scope.data.costPerDay = costPerDay;
}
}
};
vm.databind = function () {
vm.on.databind.costPerDay();
};
// Scope
$scope.data = {
costPerDay: {}
};
$scope.on = {
alterCosts: function (e) {
var costs = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300];
$scope.data.costPerDay.costs = costs;
}
}
// Databind
vm.databind();
});
MY ELEMENT LOOKS LIKE:
This renders fine initially. I need to automate updates.
<ul id="sparks" class="pull-down pull-left">
<li cost-per-day-sparkline costperday="data.costPerDay">
</li>
</ul>
MY DIRECTIVE LOOKS LIKE:
I am just trying to get ONE of them to work...I will obviously remove the others when I get a working example. And yes, I am aware you should NOT update the $parent directly. I'm just trying to find a combination that works before I get fancy.
define([], function () {
'use strict';
function CostPerDaySparklineDirective() {
return {
replace: true,
restrict: "AE",
scope: {
costperday: "=costperday"
},
templateUrl: '/modules/templates/sparklines/costperdaysparklinetemplate.html',
link: function (scope, elem, attrs) {
// This fails
scope.$watch('costperday', function (newval) {
// ... code to update will go here
}, true);
// This fails
scope.$watch('costperday', function (newval) {
// ... code to update will go here
});
// This fails
scope.$parent.$watch('data.costPerDay.costs', function (newval) {
// ... code to update will go here
});
// This renders initially, but fails to fire again
scope.$watch('scope.$parent.data.costPerDay.costs', function (newval) {
var eleSparkline = $('.sparkline', elem);
eleSparkline.sparkline(scope.costperday.costs, { type: "bar" });
});
}
};
}
return CostPerDaySparklineDirective;
});
UPDATE:
Even using ng-click to test the $watch fails to hit the breakpoint...
<a ng-click="on.alterCosts()">Change Costs</a>
In this case I'd run $scope.$apply(); in your alterCosts method to trigger a template digest. This will update the value in the DOM, which your directive catches, and subsequently triggers the $watch.
For more information on $apply(), https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply
"$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events.."
In this particular scenario you're changing the value from a DOM event.
in this situation I would watch the actual get of the costPerDayDataService vs listening to the controllers scope variable. so in your controller you would 'set' the variable in costPerDayDataService and in your directive you would just inject your service and watch the get function. OR if you are using 1.3.x > you can use bindToController which I believe eliminates the whole need for watches.
bindToController: {
costperday: '='
}
I've written a pretty simple test app as follows:
angular.module('tddApp', [])
.controller('MainCtrl', function ($scope, $rootScope, BetslipService) {
$scope.displayEvents = [
{
id: 1,
name: 'Belarus v Ukraine',
homeTeam: 'Belarus',
awayTeam: 'Ukraine',
markets: {home: '2/1', draw: '3/2', away: '5/3'},
display: true
}
];
$scope.betslipArray = BetslipService.betslipArray;
$scope.oddsBtnCallback = BetslipService.addToBetslip;
$scope.clearBetslip = BetslipService.clearBetslip;
})
.directive('oddsButton', function () {
return {
template: '<div class="odds-btn">{{market}}</div>',
replace: true,
scope: {
market: '#',
marketName: '#',
eventName: '#',
callback: '&'
},
link: function (scope, element) {
element.on('click', function() {
scope.callback({
name: scope.eventName,
marketName: scope.marketName,
odds:scope.market
});
});
}
};
})
.factory ('BetslipService', function ($rootScope) {
var rtnObject = {};
rtnObject.betslipArray = [];
rtnObject.addToBetslip = function (name, marketName, odds) {
rtnObject.betslipArray.push({
eventName: name,
marketName: marketName,
odds: odds
});
};
rtnObject.clearBetslip = function () {
rtnObject.betslipArray = [];
};
return rtnObject;
});
I've assigned an array to a controller variable. I've also assigned functions to modify the array. To add an object to the array the callback is called by a directive with isolate scope. There's some strange behaviour happening that I don't quite understand:
=> clicking the directive runs the callback in the service. I've done some debugging and it seems that the controller variable is updated but it doesn't show in the html.
=> clicking the button to clear the array isn't working as expected. The first time it's causing an element to display, after which it has no effect.
I think that this may have to do with the nested ng-repeats creating their own scopes
NB
I fixed the array not clearing by changing the function in the service to:
while (rtnObject.betslipArray.length > 0) {
rtnObject.betslipArray.pop();
}
// instead of
rtnObject.betslipArray = [];
This makes sense as the service variable was being pointed at a new object while the old reference would persist in the controller.
I got the html to update by wrapping the callback call in the directive in a scope.$apply().
This part I dont really understand. How can scope.$apply() called in the directive have an effect on the controller scope when the directive has an isolate scope? updated fiddle: http://jsfiddle.net/b6ww0rx8/7/
Any thought's greatly appreciated
jsfiddle: http://jsfiddle.net/b6ww0rx8/5/
C
I got it working here: http://jsfiddle.net/b6ww0rx8/8/
Added $q, $scope.$emit and $timeout clauses to help with communications between your directive / service and controller.
I would like to also say that I wouldn't assign service functions to a controller $scope, You should define functions in the controller that call service functions.
Instead of this:
$scope.clearBetslip = BetslipService.clearBetslip;
Do this:
$scope.clearBetslip = function(){
BetslipService.clearBetslip().then(function(){
$scope.betslipArray = BetslipService.getBetslipArray();
});
};
There are a lot of questions/answers here on stackoverflow and out on google about this topic ($apply), and I feel like I have read every one and followed them, but to no avail. All my searches on google now return purple links.
Here's the issue I'm facing (trying to be specific without overkill):
I have an app which pulls data via a webservice, storing the records in an array. I have created a drag/drop target to upload an excel file with changes/additions to the data. I have created a directive for the drop target, which binds the event listeners to the element. I have isolated the scope, using & to bind a function from the controller into the directive. The function in the controller processes the file dropped and updates the model. Here is the setup:
HTML
<div ng-controller="myController as vm">
<div class="row">
<div class="col-lg-12">
<div drop-target drop="vm.drop(files)">
<p>Drag an XLSX file here to import.</p>
</div>
</div>
</div>
</div>
I also have a table afterward with ng-repeat to display the records.
Controller
app.controller('myController', ['dataService', '$scope', function (data, $scope) {
var vm = this;
vm.data = data;
vm.drop = function (files) {
var reader = new FileReader();
reader.onload = function (e) {
... Read and parse excel sheet into array of objects ...
importLines(wbJson); //call function to update model with objects
};
reader.readAsBinaryString(files[0]);
}
function importLines(lines) {
//do a bunch of validation and update model data
}
}
Directive
app.directive('dropTarget', function () {
return {
restrict: 'A',
scope: {
drop: '&'
},
link: function (scope, el, attrs, controller) {
el.bind("dragover", function (e) {
...
});
el.bind("dragenter", function (e) {
...
});
el.bind("dragleave", function (e) {
...
});
el.bind("drop", function (e) {
if (e.preventDefault) { e.preventDefault(); } // Necessary. Allows us to drop.
if (e.stopPropogation) { e.stopPropogation(); } // Necessary. Allows us to drop.
var files = e.originalEvent.dataTransfer.files;
scope.$apply(function () {
scope.drop({ files: files });
});
});
}
}
});
So everything I have read online seems to indicate that I should wrap my call to the controller function in $apply(), as you see I have done. All of the functionality of the drag/drop and updating the model works fine. However, the view does not update. The model is updated--I can see it in the console. And when I trigger any other Angular activity (clicking some button that has an ng-click or checking a checkbox that has ng-change, etc.), the whole UI updates and I can see all the updates to the model.
If I wrap the call to importLines in the controller with $apply(), it works great. But my understanding is the $apply() call should be done within the directive...try to avoid it in the controller.
I can't for the life of me figure out why it doesn't work. It seems to follow the literally dozens and dozens of examples on forums and blogs about $apply() that I have read. I am still fairly new to angular--I know I don't fully understand some of the forums/blogs when they start discussing $digest cycles and such (but I know a lot more than I did a couple days ago). I understand that some changes to the model are done outside of angular's context, so you have to call $apply(). Some answers have said that the link function of a directive is within angular's context, others no. I don't get any errors (no $apply within $apply). Everything runs smoothly...it just doesn't update the view.
What am I missing? Any help would be appreciated.
UPDATE:
Thanks Erti-Chris for the answer. I ended up putting a $q promise in the vm.drop function, resolving it after the importLines function finishes. In the directive, when I left the scope.$apply(), I was getting an error that an $apply was already in process. But I still had to have a .then on the function call, even though it's empty. Without it, it wouldn't work right.
Controller
app.controller('myController', ['dataService', '$scope', function (data, $scope) {
var vm = this;
vm.data = data;
vm.drop = function (files) {
var deferred = $q.defer(); //Added
var reader = new FileReader();
reader.onload = function (e) {
... Read and parse excel sheet into array of objects ...
importLines(wbJson);
deferred.resolve(); //Added
};
reader.readAsBinaryString(files[0]);
return deferred.promise; //Added
}
function importLines(lines) {
//do a bunch of validation and update model data
}
}
Directive
scope.drop({ files: files }).then(function (r) {
// Do nothing here, but doesn't work without .then
});
It updates right away now. Thanks for the help!
I would expect the FileReader class to be asynchronous. It doesn't make sense that it would block in JS. Thus that would explain why your scope.apply is not working in directive. The reason is: scope apply runs BEFORE importLines has chance to run(as it is asynchronous).
You can look into $q(a promise object, which will solve it nicely) or you can create a callback function which will "notify" directive that importLines has finally been done.
vm.drop = function (files, doneCallback) {
var reader = new FileReader();
reader.onload = function (e) {
... Read and parse excel sheet into array of objects ...
importLines(wbJson); //call function to update model with objects
if(doneCallback)
doneCallback();
};
reader.readAsBinaryString(files[0]);
}
and in directive:
scope.drop({ files: files }, scope.$apply);
Directives can have an isolated scope or it can share the scope with the parent scope. However in your case you are making use of the isolated scope by declaring
scope: {
drop: '&'
},
in your directive. & allows the directive's isolate scope to pass values into the parent scope for evaluation in the expression defined in the attribute. Instead, = sets up a two-way binding expression between the directive's isolate scope and the parent scope. If you make use of
scope: {
drop: '='
},
in your directive then it will pass that scope object completely and make use of the two way binding.
You can make use of ngModel in your directive.
app.directive('dropTarget', function () {
return {
restrict: 'A',
require:'^ngModel',
link: function (scope, el, attrs, controller,ngModel) {
el.bind("dragover", function (e) {
...
});
el.bind("dragenter", function (e) {
...
});
el.bind("dragleave", function (e) {
...
});
el.bind("drop", function (e) {
if (e.preventDefault) { e.preventDefault(); } // Necessary. Allows us to drop.
if (e.stopPropogation) { e.stopPropogation(); } // Necessary. Allows us to drop.
var files = e.originalEvent.dataTransfer.files;
scope.$apply(function () {
ngModel.$setViewValue({ files: files });
});
});
}
}
});