reusable modal windows design patterns angularJS - angularjs

I have been trying to create a modal windows that are re-usable with angularJS.
While I am googling I come to know that re-usable modals are created by using
custom directives.
services.
I'm in a confusion which method to follow.
What is the most customized method for creating modal windows in angularJS? and any resources on how to create re-usable modal windows (design patterns for modal windows) in angularJS
Note: Please suggest solutions without angular-bootstrap-ui or bootstrap.
Update:
I am trying to develop a similar kind of screen.
when the user clicks on "select Bus" link a modal window will be displayed.
Title, Content of the modal window is based on the corresponding hyper link.
I've already done this screen by using custom directives with the help of
How to Create a Simple Modal Dialog Directive in Angular.js
But what I am going to rewrite it as a reusable module or directive.
So suggest some design patterns to create re usable custom modal dialog windows using angularJS

Please check the following link for re-usable windows in angular.
That may resolve your issues.
http://www.dwmkerr.com/the-only-angularjs-modal-service-youll-ever-need/

A fairly simple one is ng-modal (https://github.com/adamalbrecht/ngModal), it is a directive, meaning it is highly re-usable. Services are do-able as well, but they are meant to get/set/process data rather than display HTML; hence directives are the way to go.
I've used ng-modal and adding just a little code to it went a long way to make it super-reusable. You can place it in it's own controller and have it always injected so that you can open the modal and show a message or some html.
HTML:
<div ng-controller="modalCtrl">
<modal-dialog show='dialogShown' dialog-title='titleToUse' height='400px' width='75%'>
<p ng-bind="messageToShow"> </p>
<div ng-bind-html="someHTML"></div>
</modal-dialog>
</div>
JS:
myApp.controller('modalCtrl', function($scope){
$scope.titleToUse = "Modal Title";
$scope.messageToShow = "Testing Message"; // default message
$scope.someHTML = "<div>Whoa! A Div!</div>";
$scope.$on('changeMessageEvent', function($event, message){ // listens for change message event and sets new message
$scope.messageToShow = message;
});
});
myApp.controller('someOtherController', function($scope, $rootScope){ // import rootScope
var messageToSendToModal = "New Message!";
$rootScope.$broadcast('changeMessageEvent', messageToSendToModal );
});
Update:
If you want dynamic template and controllers, that is easily to do with ng-modal, you just have to use ng-include:
<div ng-controller="modalCtrl">
<modal-dialog show='dialogShown' dialog-title='titleToUse' height='400px' width='75%'>
<p ng-bind="messageToShow"> </p>
<div ng-bind-html="someHTML"></div>
<!-- some dynamic template -->
<ng-include src="pathToTemplate"></ng-include>
</modal-dialog>
</div>
where in modalCtrl you have
$scope.pathToTemplate = "/path/to/template.html";
that template can contain a controller and can be switched out dynamically via variables.

If anyone is looking for another example I just had a crack at creating a custom modal service and directive myself, with them you can add modals to views like this:
<button ng-click="vm.openModal('custom-modal-1')">Open Modal 1</button>
<modal id="custom-modal-1">
<div class="modal">
<div class="modal-body">
<h1>A Custom Modal!</h1>
<p>
Home page text: <input type="text" ng-model="vm.bodyText" />
</p>
<button ng-click="vm.closeModal('custom-modal-1');">Close</button>
</div>
</div>
<div class="modal-background"></div>
</modal>
Here's the controller that opens and closes the modal:
(function () {
'use strict';
angular
.module('app')
.controller('Home.IndexController', Controller);
function Controller(ModalService) {
var vm = this;
vm.openModal = openModal;
vm.closeModal = closeModal;
initController();
function initController() {
vm.bodyText = 'This text can be updated in modal 1';
}
function openModal(id){
ModalService.Open(id);
}
function closeModal(id){
ModalService.Close(id);
}
}
})();
This is the custom modal service:
(function () {
'use strict';
angular
.module('app')
.factory('ModalService', Service);
function Service() {
var modals = []; // array of modals on the page
var service = {};
service.Add = Add;
service.Remove = Remove;
service.Open = Open;
service.Close = Close;
return service;
function Add(modal) {
// add modal to array of active modals
modals.push(modal);
}
function Remove(id) {
// remove modal from array of active modals
var modalToRemove = _.findWhere(modals, { id: id });
modals = _.without(modals, modalToRemove);
}
function Open(id) {
// open modal specified by id
var modal = _.findWhere(modals, { id: id });
modal.open();
}
function Close(id) {
// close modal specified by id
var modal = _.findWhere(modals, { id: id });
modal.close();
}
}
})();
And this is the custom modal directive:
(function () {
'use strict';
angular
.module('app')
.directive('modal', Directive);
function Directive(ModalService) {
return {
link: function (scope, element, attrs) {
// ensure id attribute exists
if (!attrs.id) {
console.error('modal must have an id');
return;
}
// move element to bottom of page (just before </body>) so it can be displayed above everything else
element.appendTo('body');
// close modal on background click
element.on('click', function (e) {
var target = $(e.target);
if (!target.closest('.modal-body').length) {
scope.$evalAsync(Close);
}
});
// add self (this modal instance) to the modal service so it's accessible from controllers
var modal = {
id: attrs.id,
open: Open,
close: Close
};
ModalService.Add(modal);
// remove self from modal service when directive is destroyed
scope.$on('$destroy', function() {
ModalService.Remove(attrs.id);
element.remove();
});
// open modal
function Open() {
element.show();
$('body').addClass('modal-open');
}
// close modal
function Close() {
element.hide();
$('body').removeClass('modal-open');
}
}
};
}
})();
For more info you can check out this blog post

Related

Angular JS material dialog with multiple options to save

I'm trying to have a dialog with multiple options to save and another option to save and close as well as cancel option, where the (save and close) button will save the data and close the dialog, while the (save) button will save the data in modal then open an empty instance of the modal, the problem is when adding two options with save I see only buttons for save and cancel, here is the angular Material Snippet example I'm modifying:
$scope.showConfirm = function(ev) {
// Appending dialog to document.body to cover sidenav in docs app
var confirm = $mdDialog.confirm()
.title('Would you like to delete your debt?')
.textContent('All of the banks have agreed to forgive you your debts.')
.ariaLabel('Lucky day')
.targetEvent(ev)
.ok('Save and Close')
.ok('Save')
.cancel('Cancel');
When clicking on Confirm Dialog button I would like to see three buttons, below is the modified code:
https://codepen.io/anon/pen/dgWzjw
You can't achieve the dialog presentation you've described using $mdDialog.confirm().
This method provides a preconfigured dialog that can only ever have two action buttons. You can build the dialog you want by providing more configuration parameters to $mdDialog.show().
Here's an example.
You'll need to provide the HTML for your custom dialog:
<script type="text/ng-template" id="custom-confirm.html">
<md-dialog>
<md-dialog-content>
<md-content layout-padding>
<div>...</div>
</md-content>
</md-dialog-content>
<md-dialog-actions>
<md-button ng-click="cancel()">Cancel</md-button>
<md-button ng-click="save()">Save</md-button>
<md-button ng-click="saveAndClose()">Save and Close</md-button>
</md-dialog-actions>
</md-dialog>
</script>
Then provide a custom dialog configuration to $mdDialog.show():
$scope.showCustomConfirm = function () {
$mdDialog.show({
controller: function ($scope, $mdDialog) {
$scope.cancel = function () {
$mdDialog.cancel();
};
$scope.save = function () {
/* ... */
};
$scope.saveAndClose = function () {
/* ... */
};
},
templateUrl: 'custom-confirm.html',
});
};
I've forked your CodePen to include the example described above.
Edit
To have the save button reopen the same dialog, simply chain the the call to open the dialog onto a call to first hide it. You can do this because the return value of $mdDialog.hide() is a promise that resolves once the dialog has hidden itself.
To follow on from the above example, you'll need to do some some slight refactoring to make sure you're not shadowing $scope:
$scope.showCustomConfirm = showCustomConfirm;
function showCustomConfirm() {
$mdDialog.show({
controller: function ($scope, $mdDialog) {
$scope.save = function () {
// Save data...
$mdDialog.hide().then(showCustomConfirm);
};
// Everything else as before...
},
});
}
And here's an updated CodePen fork.

Pass variable to UI-bootstrap modal without using $scope

Since I am a beginner using AngularJS the $scope approach to pass data between different controllers and (in my case) a modal drives me crazy. Due to that reason, I googled around the web and found an interesting blogpost about passing data to a UI-bootstrap modal without using $scope.
I had a deeper look at this blogpost and the delivered plunk which works pretty nice and started to adopt this to my own needs.
What I want to achieve is to open a modal delivering an text input in which the user is able to change the description of a given product. Since this would provide more than a minimal working example I just broke everything down to a relatively small code snippet available in this plunk.
Passing data from the main controller into the modal seems to work as the default product description is displayed in the modal text input as desired. However, passing the data back from the modal to the main controller displaying the data in index.html does not seem to work, since the old description is shown there after it was edited in the modal.
To summarize my two questions are:
What am I doing wrong in oder to achieve a 'two-way-binding' from the main controller into the modal's text input and the whole way back since the same approach works in the mentioned blogpost (well, as the approach shown in the blogpost works there must be something wrong with my code, but I cannot find the mistakes)
How can I implement a proper Accept button in order to accept the changed description only if this button is clicked and discard any changes in any other case (clicking on Cancel button or closing the modal by clicking next to it)?
In your main controller, create two resolver functions: getDescription and setDescription.
In your modal controller, use them.
Your modal HTML
<div class="modal-header">
<h3 class="modal-title">Test Text Input in Modal</h3>
</div>
<div class="modal-body">
Product description:
<input type="text" ng-model="modal.description">
</div>
<div class="modal-footer">
<button ng-click="modal.acceptModal()">Accept</button>
<button ng-click="modal.$close()">Cancel</button>
</div>
Your main controller
function MainCtrl($modal) {
var self = this;
self.description = "Default product description";
self.DescriptionModal = function() {
$modal.open({
templateUrl: 'modal.html',
controller: ['$modalInstance',
'getDescription',
'setDescription',
ModalCtrl
],
controllerAs: 'modal',
resolve: {
getDescription: function() {
return function() { return self.description; };
},
setDescription: function() {
return function(value) { self.description = value; };
}
}
});
};
};
Your modal controller
function ModalCtrl($modalInstance, getDescription, setDescription) {
var self = this;
this.description = getDescription();
this.acceptModal = function() {
setDescription(self.description);
$modalInstance.close();
};
}

Problems Unit Testing ES6 Angular Directive ControllerAs with Karma/Jasmine

I am in the process of writing some unit tests following the conventions in this blog post but I am having trouble accessing my directive's controller functions. I have a directive written with ES6 classes for the directive and the controller. I am using controllerAs to bind my controller class to my directive class. The directive I am trying to write unit tests for looks like so:
// Why is this file included? | Refer to : http://www.michaelbromley.co.uk/blog/350/exploring-es6-classes-in-angularjs-1-x#_section-directives
import directiveFactory from '../../../directivefactory.js';
// ##Directive Definition
class sideNav {
constructor() {
this.template =
`
<!-- SIDENAV -->
<!-- hamburger menu toggle visible when the sidenav menu is toggled shut -->
<span class="glyphicon glyphicon-menu-hamburger side-nav-hamburger dark-hamburger" set-class-when-at-top="fix-to-top" ng-click='vm.test(); vm.toggle();'></span>
<!-- wraps all sidenav menu content -->
<div ng-class='{ show: vm.open }' class="collapsible">
<!-- hamburger menu toggle visible when the sidenav menu is toggled open -->
<span class="glyphicon glyphicon-menu-hamburger side-nav-hamburger light-hamburger" ng-click='vm.test(); vm.toggle();'></span>
<!-- brand-image -->
<div class="side-nav-head" transclude-id="head"></div> <!-- component user content insertion point 1 -->
<!-- navigation links -->
<div class="side-nav-body" transclude-id="body"></div> <!-- component user content insertion point 2 -->
<!-- footer -->
<footer>
</footer>
</div><!-- end collapsible -->
<!-- END SIDENAV -->
`;
this.restrict = 'E';
this.scope = {};
this.bindToController = {
};
this.transclude = true;
this.controller = SideNavController;
this.controllerAs = 'vm';
}
// ###Optional Link Function
link (scope, elem, attrs, ctrl, transclude) {
transclude ((clone) => {
angular.forEach(clone, (cloneEl, value) => {
// If the cloned element has attributes...
if(cloneEl.attributes) {
// Get desired target ID...
var tId = cloneEl.attributes["transclude-to"].value;
// Then find target element with that ID...
var target = elem.find('[transclude-id="' + tId + '"]');
// Append the element to the target
target.append(cloneEl);
}
});
});
}
}
// ###Directive Controller
class SideNavController {
constructor($rootScope) {
this.$rootScope = $rootScope;
// Initiate the menu as closed
this.open = false;
// Upon instantiation setup necessary $rootScope listeners
this.listen();
}
// ####listen()
// *function*
// Setup directive listeners on the $rootScope
listen () {
// Receives an event from the ng-click within the directive template
// for the side-nav-item component
this.$rootScope.$on('navigation-complete', (event) => {
// Upon receiving event, toggle the menu to closed
this.toggle();
});
}
// ####toggle()
// *function*
// Toggle menu open or shut
toggle() {
this.open = !this.open;
}
// ####test()
// *function*
test() { // DEBUG
console.log('tester'); // DEBUG
console.log(this.visible); // DEBUG
console.log(this.open); // DEBUG
}
}
SideNavController.$inject = ['$rootScope'];
export default ['sideNav', directiveFactory(sideNav)];
I take this file and import it along with one other directive component to create a module like this:
import { default as sideNav } from './side-nav/side-nav.js';
import { default as sideNavItem } from './side-nav-item/side-nav-item.js';
let moduleName = 'sideNav';
let module = angular.module(moduleName, [])
// #### Sidebar Nav Components
.directive(...sideNav)
.directive(...sideNavItem);
export default moduleName;
In my unit test I try to mock the controller in beforeEach but no matter if I use the controller name as vm or SideNavController (the former being the controllerAs name and the latter being the actual class nameā€”not truly sure which is the one I want) I still receive the error: Error: [ng:areq] Argument 'vm/SideNavController' is not a function, got undefined.
This is my unit test:
describe('Side Nav Directive', () => {
let elem, scope, ctrl;
// Mock our side-nav directive
beforeEach(angular.mock.module('sideNav'));
beforeEach(angular.mock.inject(($rootScope, $compile, $controller) => {
// Define the directive markup to test with
elem = angular.element(
`
<div>
<!-- side-nav directive component -->
<side-nav>
<!-- content insertion point 1 -->
<div transclude-to="head">
<img src alt="test_image">
</div>
<!-- content insertion point 2 -->
<div transclude-to="body">
Test Link
</div>
</side-nav>
</div>
`
);
scope = $rootScope.$new();
$compile(elem)(scope);
scope.$digest();
ctrl = $controller('vm', scope);
}));
it("should toggle shut when angular view navigation completes", () => {
expect(ctrl).toBeDefined(); // <----- this errors
});
});
I am truly stumped after referring to many tutorials and blog posts and could really use some insight!
I would actually suggest a slightly different approach to the testing. If the goal is to test the the logic in SideNavController I would consider moving that class to it's own file. That way you can import it in both the directive and the test. Structuring it that way gives you a lot easier access to it since you can test it in complete isolation from the directive itself.
Testing this by compiling markup and creating the entire directive basically turns this into an integration test and is a bit more complex to manage.
In general I find that this makes for more maintainable and useful tests - especially if the goal is to test the controller.
It would be similar to my example here: http://www.syntaxsuccess.com/viewarticle/writing-jasmine-unit-tests-in-es6
The solution I implemented is based on the response TGH made to my original question. I now have the controller class stored in a separate file, which creates a separation of concerns between the directive transclusion functionality and the directive controller functionality. The directive controller file looks like this:
const ROOTSCOPE = new WeakMap();
// ###Directive Controller
class SideNavController {
constructor($rootScope) {
ROOTSCOPE.set(this, $rootScope);
// Initiate the menu as closed
this.open = false;
// Upon instantiation setup necessary $rootScope listeners
this.listen();
}
// ####listen()
// *function*
// Setup directive listeners on the $rootScope
listen () {
// Receives an event from the ng-click within the directive template
// for the side-nav-item component
ROOTSCOPE.get(this).$on('navigation-complete', (event) => {
// Upon receiving event, toggle the menu to closed
this.toggle();
});
}
// ####toggle()
// *function*
// Toggle menu open or shut
toggle() {
this.open = !this.open;
}
}
SideNavController.$inject = ['$rootScope'];
export default SideNavController;
The test file imports the class from the appropriate file and mocks the controller for the test in a beforeEach block. The controller's variables and functions are all available:
// Import the controller to be tested
import SideNavController from './SideNavController.js';
describe('SideNavController', () => {
let $rootScope, vm;
beforeEach(angular.mock.inject((_$rootScope_, _$controller_) => {
$rootScope = _$rootScope_.$new();
vm = _$controller_(SideNavController, {} );
}));
it('should be initialize the open variable to false', () => {
expect(vm.open).toBeDefined();
expect(vm.open).toBeFalsy();
});
it('should listen for a navigation event and call the toggle function when the event is caught', () => {
// Create Jasmine spy to watch toggle function
spyOn(vm, 'toggle');
// Simulate navigation event propagation
$rootScope.$emit('navigation-complete');
expect(vm.toggle).toHaveBeenCalled();
});
});

Controller method not found when button inside template is pressed

I'm creating a project using NodeJS, Express and AngularJS that will have a search form (added via custom directive) and a search results that must be loaded only after the search button is pressed.
The problem is that the method I have created inside the controller can't be found from the search form.
Here is a sample of my code:
app.js
(function() {
var app = angular.module('app', ['app-directives']);
app.controller('AppController', function() {
this.buttonClick = function() {
alert('Test');
};
});
})();
directives.js
(function(){
var app = angular.module('app-directives', []);
app.directive('searchForm', function() {
return {
retrict: 'E',
templateUrl: '/partials/search-form.html'
};
});
app.directive('searchResults', function() {
return {
retrict: 'E',
templateUrl: '/partials/search-results.html'
};
});
})();
search-form.html
<input type="text" id="query" />
<button onclick="buttonClick">Search</button>
page-content.html
<section id="mainContent">
<search-form></search-form>
<search-results></search-results>
</section>
UPDATE
The second question will be posted in another thread.
About your first question:
You are using onclick attribute instead angular's 'ng-click' in the button search. This could be the problem. And do not forget to also add the 'ng-app' and 'ng-controller' tags. If not, your method will never be visible.
I also would recommend you to use $scope service instead of 'this' for attaching models and functions you later will use in your views.
Regards

How do I use an Angular directive to show a dialog?

Using Angular, I'm trying to create a directive that will be placed on a button that will launch a search dialog. There are multiple instances of the search button, but obviously I only want a single instance of the dialog. The dialog should be built from a template URL and have it's own controller, but when the user selects an item, the directive will be used to set the value.
Any ideas on how to create the dialog with it's own controller from the directive?
Here's what I've go so far (basically just the directive)...
http://plnkr.co/edit/W9CHO7pfIo9d7KDe3wH6?p=preview
Here is the html from the above plkr...
Find
Here is the code from the above plkr...
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
var person = {};
person.name = 'World';
$scope.person = person;
$scope.setPerson = function(newPerson) {
person = newPerson;
$scope.person = person;
}
});
app.directive('myFind', function () {
var $dlg; // holds the reference to the dialog. Only 1 per app.
return {
restrict: 'A',
link: function (scope, el, attrs) {
if (!$dlg) {
//todo: create the dialog from a template.
$dlg = true;
}
el.bind('click', function () {
//todo: use the dialog box to search.
// This is just test data to show what I'm trying to accomplish.
alert('Find Person');
var foundPerson = {};
foundPerson.name = 'Brian';
scope.$apply(function () {
scope[attrs.myFind](foundPerson);
});
});
}
}
})
This is as far as I've gotten. I can't quite figure out how to create the dialog using a template inside the directive so it only occurs once and then assign it a controller. I think I can assign the controller inside the template, but first I need to figure out how to load the template and call our custom jQuery plugin to generate the dialog (we have our own look & feel for dialogs).
So I believe the question is, how do I load a template inside of a directive? However, if there is a different way of thinking about this problem, I would be interested in that as well.
I will show you how to do it using bootstrap-ui. (you can modify it easily, if it does not suit your needs).
Here is a skeleton of the template. You can normally bound to any properties and functions that are on directive's scope:
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
... // e.g. <div class="button" ng-click=cancel()></div>
</div>
<div class="modal-body">
...
</div>
<div class="modal-footer">
...
</div>
</div>
</div>
Here is how to create/declare directive in your module:
.directive("searchDialog", function ($modal) {
return {
controller: SearchDialogCtrl,
scope : {
searchDialog: '=' // here you will set two-way data bind with a property from the parent scope
},
link: function (scope, element, attrs) {
element.on("click", function (event) { // when button is clicked we show the dialog
scope.modalInstance = $modal.open({
templateUrl: 'views/search.dialog.tpl.html',
scope: scope // this will pass the isoleted scope of search-dialog to the angular-ui modal
});
scope.$apply();
});
}
}
});
Then controller may look something like that:
function SearchDialogCtrl(dep1, dep2) {
$scope.cancel = function() {
$scope.modalInstance.close(); // the same instance that was created in element.on('click',...)
}
// you can call it from the template: search.dialog.tpl.html
$scope.someFunction = function () { ... }
// it can bind to it in the search.dialog.tpl.html
$scope.someProperty;
...
// this will be two-way bound with some property from the parent field (look below)
// if you want to perform some action on it just use $scope.$watch
$scope.searchDialog;
}
Then it your mark-up you can just use it like that:
<div class="buttonClass" search-dialog="myFieldFromScope">search</div>
I recommend this plugin:
https://github.com/trees4/ng-modal
Demo here:
https://trees4.github.io/ngModal/demo.html
Create a dialog declaratively; and it works with existing controllers. The content of the dialog can be styled however you like.

Resources