In AngularJS with Ionic, I would like to be able to call one modal from different controllers without having to duplicate the code related to the modal.
Here's how to create a modal (abbreviated from http://learn.ionicframework.com/formulas/making-modals/).
HTML:
<div class="card" ng-controller='MainCtrl' ng-click="openModal()">
Click here to open the modal
</div>
JS:
app.controller('MainCtrl', function($scope, $ionicModal)
{
$ionicModal.fromTemplateUrl('contact-modal.html', {
scope: $scope,
animation: 'slide-in-up'
}).then(function(modal) {
$scope.modal = modal
})
$scope.openModal = function() {
$scope.modal.show()
}
// functions for this modal
// ...
})
Now that's all fine an good, but if I want to open the same modal with the same functionality from a different controller, I would have to copy all the code related to it.
How can I abstract this to make my modals reusable and callable from different controllers?
Ideally, I would like each modal to have it's own "controller" (or similar concept), rather than having to put all of its code into the controller of whatever wants to open it.
This is a perfect scenario for a Directive.
Directive Code:
app.directive('myPopUp', ['$ionicModal', function($ionicModal) {
return {
restrict: 'E',
scope: {
externalScope : "="
}
replace: true,
templateUrl: 'path/to/your/template',
link: function($scope, $element, $attrs) {
$ionicModal.fromTemplateUrl('contact-modal.html', {
scope: $scope.externalScope,
animation: 'slide-in-up'
}).then(function(modal) {
$scope.modal = modal
});
$scope.externalScope.openModal = function() {
$scope.modal.show()
};
}
};
}]);
And Your Controller(s):
app.controller('MainCtrl', ['$scope', function($scope) {
$scope.externalScope = {}
});
Whenever you want to include this in a partial just add:
<my-pop-up externalScope="externalScope"></my-pop-up>
The directive will have access to the controller and vice versa via the externalScope attribute. You can call $scope.externalScope.openModal() from your controller and it will trigger your directive modal to open.
Hope this was helpful.
The way i do it is a service
app.service('ModalService', function($ionicModal, $rootScope) {
var init = function(tpl, $scope) {
var promise;
$scope = $scope || $rootScope.$new();
promise = $ionicModal.fromTemplateUrl(tpl, {
scope: $scope,
animation: 'slide-in-up'
}).then(function(modal) {
$scope.modal = modal;
return modal;
});
$scope.openModal = function() {
$scope.modal.show();
};
$scope.closeModalService = function() {
$scope.modal.hide();
//$scope.modal.remove();
};
$scope.$on('$destroy', function() {
//$scope.modal.remove();
});
return promise;
}
return {
init: init
}
})
How to use it in a controller
app.controller('editMyProfileCtrl', function($scope,ModalService) {
$scope.openModal = function() {
ModalService
.init('my-modal.html', $scope)
.then(function(modal) {
modal.show();
});
};
$scope.closeModal = function() {
$scope.closeModalService();
};
})
Related
I am trying to use a directive, click-anywhere-but-here, in my header HTML, using controller navCtrl. Angular is throwing error:
Unknown provider: clickAnywhereButHereProvider <-
I'm thinking this has to do with how I'm using gulp to concatenate the JS files. I checked the concatenated main.js files with all JS, and see that navCtrl is defined above the clickAnywhereButHere directive. Not sure if this matters at all since the controller isn't using the directive at all, only the header.html file.
<header ng-controller="navCtrl">
<a click-anywhere-but-here="clickedSomewhereElse()" ng-click="clickedHere()">
<li>study</li>
</a>
</header>
How can I force the header to wait until clickAnywhereButHere directive is loaded before complaining?
Edit: Code:
navCtrl.js: I've gutted out a lot of the unrelated code
angular
.module('DDE')
.controller('navCtrl', ['$rootScope', '$location', '$scope', 'Modal', 'Auth', '$window', '$state', 'deviceDetector',
function($rootScope, $location, $scope, Modal, Auth, $window, $state, deviceDetector) {
$scope.clicked = '';
$scope.clickedHere = function(){
$scope.clicked = 'stop that';
console.log('clicked on element');
};
$scope.clickedSomewhereElse = function(){
console.log('clicked elsewhere');
$scope.clicked = 'thanks';
};
$scope.headings = [
{page: 'contact', route: '#/contact'}
];
}
]);
clickAnywhereButHere.js directive:
angular.module('DDE')
.directive('clickAnywhereButHere', function($document, clickAnywhereButHereService){
return {
restrict: 'A',
link: function(scope, elem, attr, ctrl) {
var handler = function(e) {
e.stopPropagation();
};
elem.on('click', handler);
scope.$on('$destroy', function(){
elem.off('click', handler);
});
clickAnywhereButHereService(scope, attr.clickAnywhereButHere);
}
};
});
clickAnywhereButHereService.js Service:
angular.module('DDE')
.factory('clickAnywhereButHereService', function($document){
var tracker = [];
return function($scope, expr) {
var i, t, len;
for(i = 0, len = tracker.length; i < len; i++) {
t = tracker[i];
if(t.expr === expr && t.scope === $scope) {
return t;
}
}
var handler = function() {
$scope.$apply(expr);
};
$document.on('click', handler);
// IMPORTANT! Tear down this event handler when the scope is destroyed.
$scope.$on('$destroy', function(){
$document.off('click', handler);
});
t = { scope: $scope, expr: expr };
tracker.push(t);
return t;
};
});
Both the directive and service are present in my min file:
You need to take into account the fact that your JS is minified.
So change this
.directive('clickAnywhereButHere', function($document, clickAnywhereButHereService){
to this
.directive('clickAnywhereButHere',
['$document', 'clickAnywhereButHereService',
function($document, clickAnywhereButHereService){
//...
}])
I have the following directive. When I trigger the open function and get to the debugger I get an error message in the console that says Uncaught ReferenceError: $scope is not defined(…).
How is it possible for $scope.open to be called when $scope is undefined?
app.directive('photo', ['$http', 'modal', function($http, modal) {
return {
replace: true,
templateUrl: '/assets/photo.html',
transclude: false,
scope: {
result: '=',
index: '#'
},
controller: ['$scope', '$http', 'modal', function($scope, $http, modal) {
$scope.prev = $scope.index - 1;
$scope.open = function() {
debugger;
};
}]
}
}]);
Here is my DOM:
<div ng-repeat="r in results" photo result="r" index="$index"></div>
If I insert console.log($scope) just before my open function, and then again right before the debugger in that function, I get the following results. Left is before open is called, right is after open is called.
You inject the $http and modal in the directive definition (as you did), no need to in the controller function, just do:
controller: function($scope) {
$scope.prev = $scope.index - 1;
$scope.open = function() {
debugger;
};
}
Try adding a statement that uses $scope in $scope.open. Chrome has probably optimized $scope away when you're in $scope.open because you're not using it.
$scope.open = function() {
console.log($scope);
debugger; //now you should see $scope.
};
its worked for me
var app = angular.module("moduleTest",[]);
app.directive("testDirective",function(){
return {
restrict: "A",
scope: true,
link: function(scope, element){
//code
//and $scope is scope
}
}
});
This should work:
app.directive('photo', ['$http', 'modal', function($http, modal) {
return {
replace: true,
templateUrl: '/assets/photo.html',
transclude: false,
scope: {
result: '=',
index: '#'
},
controller: function($scope, $http, modal) {
$scope.prev = $scope.index - 1;
$scope.open = function() {
debugger;
};
}
}
}]);
You need to define the $Scope at the top i.e.:
app.directive('photo', ['$http', '$Scope','modal', function($http, $Scope, modal)
It will work fine now.
I know how to use Angular's array notation for controllers when these are created "within an app", like this:
angular.module('appName').controller('controllerName', ['$scope', '$http', function ($scope, $http) {
}]);
But what if I have a controller that is used "on the fly"?
I have a directive that creates a bootstrap-ui modal controlled by a controller created "in the moment", this is the code:
angular.module('appName').directive('ngConfirmClick', ['$modal', function($modal) {
// Controller "on the fly"
var modalController = function($scope, $modalInstance) {
$scope.ok = function() {
$modalInstance.close();
};
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
};
return {
restrict: 'A',
scope: {
ngConfirmClick:"&"
},
link: function(scope, element, attrs) {
element.bind('click', function() {
var message = attrs.ngConfirmMessage || "Really?";
var modalHtml = '<div class="modal-header"><h3 class="modal-title">Confirm</h3></div>'
+ '<div class="modal-body">' + message + '</div>'
+ '<div class="modal-footer"><button class="btn btn-default" ng-click="ok()">OK</button>'
+ '<button class="btn btn-default" ng-click="cancel()">Cancel</button></div>';
var modalInstance = $modal.open({
template: modalHtml,
controller: modalController, // Controller reference
backdrop: false,
});
modalInstance.result.then(function() {
scope.ngConfirmClick();
}, function() {
});
});
}
}
}]);
Question is... how do I use array notation in here?
// Controller "on the fly"
var modalController = function($scope, $modalInstance) {
...
};
I know I could register the controller as I usually do with reusable controllers (see my first block of code)... but: should I? is it the only way?
This question can be abstracted and be helpful to others if stated as this:
If a directive needs to create/use a controller and this controller is used only by this directive and nobody else... how should the controller be created? within the app? within what?
You can add an $inject property...
// Controller "on the fly"
var modalController = function($scope, $modalInstance) {
...
};
modalController.$inject = ['$scope', '$modalInstance'];
What you're doing is already fine, you just need to assign modalController using the array notation. You don't need to add it as a reusable controller in the app since it's only used inside the directive.
JAVASCRIPT
var modalController = ['$scope', '$modalInstance', function($scope, $modalInstance) {
$scope.ok = function() {
$modalInstance.close();
};
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
}];
Alternatively, you can also add the dependencies using the $inject property.
var modalController = function($scope, $modalInstance) {
$scope.ok = function() {
$modalInstance.close();
};
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
};
modalController.$inject = ['$scope', '$modalInstance'];
Well, I'm having some problems updating a progress bar (which is in a directive) from a controller.
here are some code snippets:
my directive:
angular.module('TestApp').directive('orderProgress', ['$window', OrderProgress]);
function OrderProgress($window) {
var directive = {
link: link,
restrict: 'A',
templateUrl: 'OrderProgress.html',
controller: 'ProgressController',
replace: true
};
return directive;
function link(scope, element, attrs) {}
}
controller for directive:
function ProgressController($scope, progressNumberService) {
$scope.progress = progressNumberService.getProgress();
}
progressNumberService just hides the detail for the amount of "progress":
var progress = 20;
var progressServiceInstance = {
incProgress: function() {
progress += 20;
},
decProgress: function() {
progress -= 20;
},
getProgress: function() {
return progress;
}
};
App.value('progressNumberService', progressServiceInstance);
of course the controller:
function Controller($scope, progressNumberService) {
$scope.nextStep = function() {
progressNumberService.incProgress();
};
$scope.prevStep = function() {
progressNumberService.decProgress();
};
}
I've created an example:
http://plnkr.co/edit/LtY4ZUG591Kd3mUKEmEF?p=catalogue
So why doesn't the directive get the update from the 'Controller', when the Next/Prev buttons are pressed?
So your issue is that the value is being updated in your .value module, but your directive controller is never calling getProgress once the values are updated. I would suggest using $broadcast and $on to send a message saying that the progress was updated. I tested this and it seemed to do the trick.
Controller:
angular.module('TestApp').controller(controllerId2, ['$scope', '$rootScope', 'progressNumberService', ProgressController]);
function ProgressController($scope, $rootScope, progressNumberService) {
$scope.progress = progressNumberService.getProgress();
$rootScope.$on("event:progress-change", function() {
$scope.progress = progressNumberService.getProgress();
});
}
And change your .value to a factory so you can use $rootScope to broadcast
App.factory('progressNumberService', function($rootScope) {
return {
incProgress: function() {
progress += 20;
$rootScope.$broadcast("event:progress-change");
},
decProgress: function() {
progress -= 20;
$rootScope.$broadcast("event:progress-change");
},
getProgress: function() {
return progress;
}
}
});
Here is the updated Plunker DEMO
When I require a controller in a directive, I am getting error saying that, not able to find the controller.
Please see the code with the issue below.
http://plnkr.co/edit/NzmQPA?p=preview
Can someone please have a look at it?
Thanks
You should use a service to communicate between them. Exactly how/what you do depends on your exact needs (there's not enough info in your post).
Side note, I changed your click handler to an ng-click.
Here's an example:
http://plnkr.co/edit/I2TvvV?p=preview
<div search-result-filter></div>
<div search-result-header ng-click="doClick()"></div>
angular.module('mymodule', [])
.controller('mainCtrl', ['$scope',
function($scope) {
$scope.test = "main angular is working";
}
]).controller('searchResultFilterController', ['$scope', 'myService',
function($scope, myService) {
//do something with 'myService'
}
])
.directive('searchResultFilter', [
function() {
return {
replace: true,
controller: 'searchResultFilterController',
template: '<h1>this is the first directive</h1>'
};
}
])
.directive('searchResultHeader', ['myService',
function(myService) {
return {
replace: true,
template: '<button>clickme</button>',
link: function($scope, $elem, $attrs) {
$scope.doClick = function() {
myService.someFn();
};
}
};
}
])
.service('myService', function() {
this.someFn = function() {
alert('this is working');
};
});
You should use require when your directives are related: like an accordion and accordion items.
To communicate between scopes, you should try $on, $emit, $broadcast. In your case, you need to inject rootScope into your directive, and broadcast an event from rootScope:
.directive('searchResultHeader',
function($rootScope) { //inject rootScope
return {
replace: true,
template: '<button>clickme</button>',
link: function($scope, $elem, $attrs) {
$elem.on('click', function() {
$rootScope.$broadcast("someEvent"); //broadcast an event to all child scopes.
});
}
};
}
);
Any scopes interested in the event can subscribe to it using $on:
function($scope) {
$scope.$on("someEvent", function() {
alert('this is working');
});
}
Using events is a way to create decoupled systems.
DEMO