I have the following directive for a popup with multiple templates:
app.directive('popup', function ($http, $rootScope, $templateCache, $compile, $parse, Popup) {
return {
link: function ($scope, $element, $attrs) {
$element.bind('click', function () {
var data = {
index: $attrs.index
}
$http.get('/partial/' + $attrs.popup + '.html', {cache: $templateCache}).success(function (tplContent) {
var mainElement = angular.element(document.getElementById('main'));
mainElement.append($compile(tplContent)($scope));
Popup.show(data);
});
});
}
}
});
I've pinned a visibility flag to $rootScope (to make the popup visible by css) along with index and item that came in data object. and the popup template looks like this:
<section class="popup" ng-class="{'show': popup.visibility}">
<h1>{{data[popup.index].title}}<h1>
<p>{{data[popup.index].message}}<p>
</section>
The directive loads the popup template and appends it to my mainElement, but doesn't apply scope to. so popup doesn't appear and no data is bound to it.
you need to pass $scope.$parent instead of $scope for compiling. because you are in the child $scope.
Related
In the interest of abstraction, I'm trying to pass a scope between directives with little success... Basically, this is a modal type scenario:
Directive A - handles click function of on screen element:
.directive('myElement', ['pane', function(pane){
return {
restrict: 'A',
scope: {},
link: function(scope,elem,attrs){
//im going to try and call the form.cancel function from a template compiled in another directive
scope.form = {
cancel: function(){
pane.close();
}
};
scope.$watch(function(){
var w = elem.parent()[0].clientWidth;
elem.css('height',(w*5)/4+'px');
});
elem.on('click', function(){
//this calls my service which communicates with my other directive to 1) display the pane, and 2) pass a template compiled with this directive's scope
pane.open({
templateUrl: 'views/forms/edit.html',
scope: scope //I pass the scope to the service API here
});
});
}
}
}])
I have a service called 'Pane' to handle the API:
.service('pane',['$rootScope', function($rootScope){
var open = function(data){
$rootScope.$broadcast('openPane',data); //this broadcasts my call to open the pane with the template url and the scope object
};
var close = function(){
$rootScope.$broadcast('closePane');
};
return {
open: open,
close: close
}
}]);
Finally, directive B is lying in wait for the 'openPane' broadcast which includes the template url and the scope:
.directive('pane',['$compile','$templateRequest','$rootScope', function($compile,$templateRequest,$rootScope){
return {
restrict: 'A',
link: function(scope,elem,attrs){
var t;
scope.$on('openPane', function(e,data){ //broadcast is received and pane is displayed with template that gets retrieved
if(data.templateUrl){
$templateRequest(data.templateUrl).then(function(template){
//this is where the problem seems to be. it works just fine, and the data.scope object does include my form object, but calling it from the template that opens does nothing
t = $compile(template)(data.scope);
elem.addClass('open');
elem.append(t);
}, function(err){
console.log(JSON.stringify(err));
});
}
else if(data.template){
t = $compile(angular.element(data.template))(data.scope);
elem.addClass('open');
elem.append(t);
}
else console.log("Can't open pane. No templateUrl or template was specified.")
});
scope.$on('closePane', function(e,data){
elem.removeClass('open');
t.remove();
});
}
}
}])
The problem is that when the last directive, 'pane', receives the 'openPane' broadcast, it opens and appends the template just fine, but when i call the function 'form.cancel()' defined in my original directive like so:
<button type="button" ng-click="form.cancel()">Cancel</button>
... nothing happens. Truth is, I'm not sure what I'm doing is legit at all, but i want to understand why it isn't working. The ultimate goal here is to be able to pass the scope of one directive, along with a form template, to the Pane directive, so all my forms (which are controlled by their own directives) can be 'injected' into the pane.
Without a running example I'm suspecting the likely cause to be the scope of your scope when passed to your pane template. The scope itself does get passed and used when you compile your pane template, but its closure is lost along the way, so you likely can't see pane service which is part of the directive factory closure and form.cancel uses.
I've written a simplified example that does work and doesn't rely on closures bt rather on local variables. You could accomplish a similar thing if you called .bind(pane) on your scope.form.cancel function and within replace pane by this.
So here's a working example and this is its code:
/* ************ */
/* Pane service */
class PaneService {
constructor($rootScope) {
console.log('pane service instantiated.', this);
this.$rootScope = $rootScope;
}
open(template, scope) {
this.$rootScope.$emit('OpenPane', template, scope);
}
close(message) {
this.$rootScope.$emit('ClosePane', message);
}
}
PaneService.$inject = ['$rootScope'];
/* ************************* */
/* Pane directive controller */
class PaneController {
constructor($rootScope, $compile, $element) {
console.log('pane directive instantiated.', this);
this.$compile = $compile;
this.$element = $element;
$rootScope.$on('OpenPane', this.open.bind(this));
$rootScope.$on('ClosePane', this.close.bind(this));
}
open(event, template, scope) {
console.log('pane directive opening', template, scope);
var t = this.$compile(template)(scope);
this.$element.empty().append(t);
}
close(evet, message) {
console.log('pane directive closing', message);
this.$element.empty().append('<strong>' + message + '</strong>');
}
}
PaneController.$inject = ['$rootScope', '$compile', '$element'];
var PaneDirective = {
restrict: 'A',
controller: PaneController,
controllerAs: 'pane',
bindToController: true
}
/* *************** */
/* Page controller */
class PageController {
constructor(paneService, $scope) {
console.log('page controller instantiated.', this);
this.paneService = paneService;
this.$scope = $scope;
}
open() {
console.log('page controller open', this);
this.paneService.open('<button ng-click="page.close(\'Closed from pane\')">Close from pane</button>', this.$scope);
}
close(message) {
console.log('page controller close');
this.paneService.close(message);
}
}
PageController.$inject = ['paneService', '$scope'];
angular
.module('App', [])
.service('paneService', PaneService)
.directive('pane', () => PaneDirective)
.controller('PageController', PageController);
And page template is very simple:
<body ng-app="App">
<h1>Hello Plunker!</h1>
<div ng-controller="PageController as page">
<button ng-click="page.open()">Open pane</button>
<button ng-click="page.close('Closed from page')">Close pane</button>
</div>
<div pane></div>
</body>
I've got an Angular view thusly:
<div ng-include="'components/navbar/navbar.html'" class="ui centered grid" id="navbar" onload="setDropdown()"></div>
<div class="sixteen wide centered column full-height ui grid" style="margin-top:160px">
<!-- other stuff -->
<import-elements></import-elements>
</div>
This is controlled by UI-Router, which is assigning the controller, just FYI.
The controller for this view looks like this:
angular.module('pcfApp')
.controller('ImportElementsCtrl', function($scope, $http, $location, $stateParams, $timeout, Framework, OfficialFramework) {
$scope.loadOfficialFrameworks();
// other stuff here
});
The <import-elements> directive, looks like this:
angular.module('pcfApp').directive('importElements', function($state, $stateParams, $timeout, $window, Framework, OfficialFramework) {
var link = function(scope, el, attrs) {
scope.loadOfficialFrameworks = function() {
OfficialFramework.query(function(data) {
scope.officialFrameworks = data;
$(".ui.dropdown").dropdown({
onChange: function(value, text, $item) {
loadSections($item.attr("data-id"));
}
});
window.setTimeout(function() {
$(".ui.dropdown").dropdown('set selected', data[0]._id);
}, 0);
});
}
return {
link: link,
replace: true,
templateUrl: "app/importElements/components/import_elements_component.html"
}
});
I was under the impression that I'd be able to call the directive's loadOfficialFrameworks() method from my controller in this way (since I'm not specifying isolate scope), but I'm getting a method undefined error on the controller. What am I missing here?
The problem is that your controller function runs before your link function runs, so loadOfficialFrameworks is not available yet when you try to call it.
Try this:
angular.module('pcfApp')
.controller('ImportElementsCtrl', function($scope, $http, $location, $stateParams, $timeout, Framework, OfficialFramework) {
//this will fail because loadOfficialFrameworks doesn't exist yet.
//$scope.loadOfficialFrameworks();
//wait until the directive's link function adds loadOfficialFrameworks to $scope
var disconnectWatch = $scope.$watch('loadOfficialFrameworks', function (loadOfficialFrameworks) {
if (loadOfficialFrameworks !== undefined) {
disconnectWatch();
//execute the function now that we know it has finally been added to scope
$scope.loadOfficialFrameworks();
}
});
});
Here's a fiddle with this example in action: http://jsfiddle.net/81bcofgy/
The directive scope and controller scope are two differents object
you should use in CTRL
$scope.$broadcast('loadOfficialFrameworks_event');
//And in the directive
scope.$on('loadOfficialFrameworks_event', function(){
scope.loadOfficialFrameworks();
})
I have an angular modal that works with a separate button, but need to have it popup when a row in the slickgrid is double clicked. How should I structure this code:
grid.onDblClick.subscribe(function (e, args) {
//make the angular modal popup
});
With an angular modal, I would normally just select the dom element, then add an ng-click="openModal()" into the div tag but I'm having trouble even locating the div in chrome dev tools. Any ideas?
Currently, I have the slickgrid wrapped in a directive, meaning that my html looks like this:
<div class="gridContainer" ng-controller="EditModalCtrl">
<div class="modalTest" style='width:100%;'>
<div style='width:100%;height:500px;' all-batches-grid></div>
</div>
</div>
In my slickgrid directive, I have:
angular.module('epDefaultApp.allBatchesDirective', [])
.directive('allBatchesGrid', [function () {
return {
restrict: 'EA',
link : function(scope, element, attrs){
grid.onDblClick.subscribe(function (e, args) {
$('.addButton').removeClass('addText');
$('#addEditButton').addClass('editButton');
$('.addButton').addClass('editText');
var item = grid.getDataItem(args.row);
scope.openPrefill('lg', item);
});
}
}
}]);
Here is my controller as well:
angular.module('epDefaultApp.editModalController', [])
.controller('EditModalCtrl', ['$scope', '$modal', '$log', 'toastr', function ($scope, $modal, $log, toastr) {
$scope.openPrefill = function (size, selectedRowObject) {
$scope.selectedRow = {
vendorName: selectedRowObject[0],
purchaseNumber: selectedRowObject[3],
invoiceAmount: selectedRowObject[6],
invoiceDate: selectedRowObject[10],
invoiceNumber: selectedRowObject[1],
dueDate: selectedRowObject[9],
checkCode: '0',
memo: selectedRowObject[5]
};
}]);
The reason that this works is that the scope variable within the directive is actually linked to $scope in the EditModalCtrl. This happens when we first initialize the grid directive in the HTML file.
So I want to create a directive that outputs a global message.
Requirements of directive...
This directive message can be updated from any controller.
When the message is updated from any controller so is the directive consequently the view.
Clear the Message after the view is destoryed
So far I am doing this by creating a directive and service that work together.
Problem is Im not able to update the view when the message is update from inside other controllers
If someone could direct me on how to proceed that would be swell.
What about using $rootScope and broadcasting?
app.directive("alertMsg", ['MsgService', function(MsgService) {
return {
restrict: "E",
scope: true,
template: '{{msg}}', // this string is the html that will be placed inside the <alert-msg></alert-msg> tags.
link: function (scope, $element, attrs) {
scope.msg = MsgService.getAlertMsg(); //set msg to be available to the template above <alert-msg>{{msg}}</alert-msg>
scope.$on("$destroy", function(){ //when the <alert-msg> view is destroyed clear the alert message
MsgService.clearAlertMsg();
});
}
};
}]);
app.service('MsgService', function() {
this.alertMsg = '';
this.getAlertMsg = function(){
return this.alertMsg;
};
this.setAlertMsg = function(string) {
this.alertMsg = string;
};
this.clearAlertMsg = function(){
this.alertMsg = '';
};
});
app.controller('NewPlateController', ['urlConfig', '$scope', '$http', '$location', 'MsgService', '$routeParams', function(urlConfig, $scope, $http, $location, MsgService, $routeParams) {
$scope.plate = {license_plate: $routeParams.plate, state: 'default-state'};
// create new plate via json request
$scope.createPlate = function(){
$http.post(urlConfig.rootUrl+"/plates.js", $scope.plate).success(function(data) {
$scope.plateInfo = data;
MsgService.setAlertMsg('Plate Sucessfully Created'); //Need to update the directive to actual show this update
$location.path('/plate/'+$scope.plateInfo.plate_id);
// http error: display error messages
}).error(function(data,status,headers,config) {
$scope.errors = data;
$('#new-plate-errors').slideDown('fast');
});
};
}]);
Use $rootscope.$emit to send messages from your controllers (and even services) and use $rootScope.$on to receive them in your directive.
You must remove the listener on the directive's scope destruction or you will have a memory leak.
app.directive("alertMsg", ['$rootScope', function($rootScope) {
return {
restrict: "E",
scope: true,
template: '{{msg}}', // this string is the html that will be placed inside the <alert-msg></alert-msg> tags.
link: function (scope, $element, attrs) {
var _unregister; // store a reference to the message event listener so it can be destroyed.
_unregister = $rootScope.$on('message-event', function (event, message) {
scope.msg = message; // message can be value, an object, or an accessor function; whatever meets your needs.
});
scope.$on("$destroy", _unregister) //when the <alert-msg> view is destroyed remove the $rootScope event listener.
}
};
}]);
app.controller('NewPlateController', ['urlConfig', '$scope', '$http', '$location', '$rootScope', '$routeParams', function(urlConfig, $scope, $http, $location, $rootScope, $routeParams) {
$scope.plate = {license_plate: $routeParams.plate, state: 'default-state'};
// create new plate via json request
$scope.createPlate = function(){
$http.post(urlConfig.rootUrl+"/plates.js", $scope.plate).success(function(data) {
$scope.plateInfo = data;
$rootScope.$emit('message-event', 'Plate Sucessfully Created'); //use $emit, not $broadcast. Only $rootscope listeners are called.
scope.$on("$destroy", function() { // remove the message when the view is destroyed.
$rootScope.$emit('message-event', "");
});
$location.path('/plate/'+$scope.plateInfo.plate_id);
// http error: display error messages
}).error(function(data,status,headers,config) {
$scope.errors = data;
$('#new-plate-errors').slideDown('fast');
});
};
}]);
The message is not persisted outside of the directive so it will be removed when its scope is destroyed.
Edit: Adding a JSFiddle showing a working example: http://jsfiddle.net/kadm3zah/
Edit 2: I missed the requirement to remove the remove the message added when the view is destroyed. With this method you could add a second emit to the message on the NewPlateController scope's destruction with an empty string message.
This does not cover dynamically adding or removing the directive to the DOM. For that you could use a service to add and later remove the directive tag. This is how module's like ngToast and ui.boostrap's modal service work. Using one of them may be more appropriate for what you want to accomplish.
I have a strange issue when using $compile(element)(scope) to bind some data to a dom element manually.
Please check the example code here.
In the example preview, when you click the button "Bind Template", the template is compiled with some data and rendered into a dom element. But when the button is clicked again, only the template is shown. Any idea what could be the problem here.
I don't know what you are trying to do doing it this way, but you should probably use a directive instead.
Here is your plunker working with a directive
app.controller('MainCtrl', function ($scope, $http, $templateCache, $compile) {
$scope.name = 'World';
$scope.result = '<empty>';
$scope.bindTemplate = function () {
$scope.item = {
CaseNumber: 1234,
CandidateName: 'Joe Smith',
CandidateEmail: 'joe#smith.com',
IsActive: true
}
};
});
app.directive('caseDetails', function() {
return {
templateUrl: "template.html"
}
})