Created
March 23, 2015 12:19
-
-
Save bennadel/3425dc3614a51aa0f3e6 to your computer and use it in GitHub Desktop.
Creating A Simple Modal System In AngularJS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html ng-app="Demo"> | |
<head> | |
<meta charset="utf-8" /> | |
<title> | |
Creating A Simple Modal System In AngularJS | |
</title> | |
<link rel="stylesheet" type="text/css" href="./demo.css"></link> | |
</head> | |
<body ng-controller="AppController"> | |
<h1> | |
Creating A Simple Modal System In AngularJS | |
</h1> | |
<ul> | |
<li> | |
<a ng-click="alertSomething()">Alert something</a> | |
</li> | |
<li> | |
<a ng-click="confirmSomething()">Confirm something</a> | |
</li> | |
<li> | |
<a ng-click="promptSomething()">Prompt something</a> | |
</li> | |
</ul> | |
<!-- | |
BEGIN: Modals Layout. | |
-- | |
Our simple modal system makes the assumption that only one modal window can be | |
open at a time. I find this to be a very reasonable assumption for 99% of use- | |
cases. And, I'd rather keep it simple and work around the rare use-case in some | |
other fashion (such as a multi-state modal). | |
To remove any "magic", I'm explicitly presenting the control properties rather | |
than trying to hide them behind a compile() step. Notice that rendering a modal | |
is JUST LIKE RENDERING ANY OTHER VIEW - we have a subview variable which we are | |
using to swap modals, or remove modals altogether. | |
The big take-away in the following code is that modal windows aren't a *special* | |
concept. They are views or components that work the same exact way that the rest | |
of your application works! Sure, there are a few more rules, like only one open | |
at a time; but, other than that, there's nothing here that's any different than | |
any other view you will build. It has a Controller, it has a View, and it works | |
with other services to execute its functionality. | |
--> | |
<div | |
bn-modals | |
ng-show="subview" | |
class="m-modals" | |
ng-switch="subview"> | |
<!-- BEGIN: Alert Modal. --> | |
<div | |
ng-switch-when="alert" | |
ng-controller="AlertModalController" | |
class="modal"> | |
<p> | |
{{ message }} | |
</p> | |
<p> | |
<a ng-click="close()">Ok, I got it!</a> | |
</p> | |
<!-- This is to demonstrate that we can jump from one modal to another. --> | |
<p> | |
<a ng-click="jumpToConfirm()" class="jump">Jump over to the confirm modal</a> | |
</p> | |
</div> | |
<!-- END: Alert Modal. --> | |
<!-- BEGIN: Confirm Modal. --> | |
<div | |
ng-switch-when="confirm" | |
ng-controller="ConfirmModalController" | |
class="modal"> | |
<p> | |
{{ message }} | |
</p> | |
<p> | |
<a ng-click="confirm()">{{ confirmButton }}</a> | |
— | |
<a ng-click="deny()">{{ denyButton }}</a> | |
</p> | |
</div> | |
<!-- END: Confirm Modal. --> | |
<!-- BEGIN: Prompt Modal. --> | |
<form | |
ng-switch-when="prompt" | |
ng-controller="PromptModalController" | |
ng-submit="submit()" | |
class="modal"> | |
<p> | |
{{ message }} | |
</p> | |
<p ng-if="errorMessage"> | |
<strong>Sorry:</strong> {{ errorMessage }} | |
</p> | |
<p> | |
<input type="text" ng-model="form.input" /> | |
</p> | |
<p> | |
<a ng-click="submit()">Submit</a> | |
— | |
<a ng-click="cancel()">Cancel</a> | |
</p> | |
</form> | |
<!-- END: Prompt Modal. --> | |
</div> | |
<!-- END: Modals Layout. --> | |
<!-- Load scripts. --> | |
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.15.min.js"></script> | |
<script type="text/javascript" src="../../vendor/angularjs/angular-animate-1.3.15.min.js"></script> | |
<script type="text/javascript"> | |
// Create an application module for our demo. | |
var app = angular.module( "Demo", [ "ngAnimate" ] ); | |
// -------------------------------------------------- // | |
// -------------------------------------------------- // | |
// I control the root of the application. | |
app.controller( | |
"AppController", | |
function( $scope, modals ) { | |
// I open an Alert-type modal. | |
$scope.alertSomething = function() { | |
// The .open() method returns a promise that will be either | |
// resolved or rejected when the modal window is closed. | |
var promise = modals.open( | |
"alert", | |
{ | |
message: "I think you are kind of beautiful!" | |
} | |
); | |
promise.then( | |
function handleResolve( response ) { | |
console.log( "Alert resolved." ); | |
}, | |
function handleReject( error ) { | |
console.warn( "Alert rejected!" ); | |
} | |
); | |
}; | |
// I open a Confirm-type modal. | |
$scope.confirmSomething = function() { | |
// The .open() method returns a promise that will be either | |
// resolved or rejected when the modal window is closed. | |
var promise = modals.open( | |
"confirm", | |
{ | |
message: "Are you sure you want to taste that?!" | |
} | |
); | |
promise.then( | |
function handleResolve( response ) { | |
console.log( "Confirm resolved." ); | |
}, | |
function handleReject( error ) { | |
console.warn( "Confirm rejected!" ); | |
} | |
); | |
}; | |
// I open a Prompt-type modal. | |
$scope.promptSomething = function() { | |
// The .open() method returns a promise that will be either | |
// resolved or rejected when the modal window is closed. | |
var promise = modals.open( | |
"prompt", | |
{ | |
message: "Who rocks the party the rocks the body?", | |
placeholder: "MC Lyte." | |
} | |
); | |
promise.then( | |
function handleResolve( response ) { | |
console.log( "Prompt resolved with [ %s ].", response ); | |
}, | |
function handleReject( error ) { | |
console.warn( "Prompt rejected!" ); | |
} | |
); | |
}; | |
} | |
); | |
// -------------------------------------------------- // | |
// -------------------------------------------------- // | |
// I control the Alert modal window. | |
// -- | |
// NOTE: This controller gets "modals" injected; but, it is in no way | |
// different than any other Controller in your entire AngularJS application. | |
// It takes services, manages the view-model, and knows NOTHING about the DOM. | |
app.controller( | |
"AlertModalController", | |
function( $scope, modals ) { | |
// Setup default values using modal params. | |
$scope.message = ( modals.params().message || "Whoa!" ); | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// Wire the modal buttons into modal resolution actions. | |
$scope.close = modals.resolve; | |
// I jump from the current alert-modal to the confirm-modal. | |
$scope.jumpToConfirm = function() { | |
// We could have used the .open() method to jump from one modal | |
// to the next; however, that would have implicitly "rejected" the | |
// current modal. By using .proceedTo(), we open the next window, but | |
// defer the resolution of the current modal until the subsequent | |
// modal is resolved or rejected. | |
modals.proceedTo( | |
"confirm", | |
{ | |
message: "I just came from Alert - doesn't that blow your mind?", | |
confirmButton: "Eh, maybe a little", | |
denyButton: "Oh please" | |
} | |
) | |
.then( | |
function handleResolve() { | |
console.log( "Piped confirm resolved." ); | |
}, | |
function handleReject() { | |
console.warn( "Piped confirm rejected." ); | |
} | |
); | |
}; | |
} | |
); | |
// -------------------------------------------------- // | |
// -------------------------------------------------- // | |
// I control the Confirm modal window. | |
// -- | |
// NOTE: This controller gets "modals" injected; but, it is in no way | |
// different than any other Controller in your entire AngularJS application. | |
// It takes services, manages the view-model, and knows NOTHING about the DOM. | |
app.controller( | |
"ConfirmModalController", | |
function( $scope, modals ) { | |
var params = modals.params(); | |
// Setup defaults using the modal params. | |
$scope.message = ( params.message || "Are you sure?" ); | |
$scope.confirmButton = ( params.confirmButton || "Yes!" ); | |
$scope.denyButton = ( params.denyButton || "Oh, hell no!" ); | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// Wire the modal buttons into modal resolution actions. | |
$scope.confirm = modals.resolve; | |
$scope.deny = modals.reject; | |
} | |
); | |
// -------------------------------------------------- // | |
// -------------------------------------------------- // | |
// I control the Prompt modal window. | |
// -- | |
// NOTE: This controller gets "modals" injected; but, it is in no way | |
// different than any other Controller in your entire AngularJS application. | |
// It takes services, manages the view-model, and knows NOTHING about the DOM. | |
app.controller( | |
"PromptModalController", | |
function( $scope, modals ) { | |
// Setup defaults using the modal params. | |
$scope.message = ( modals.params().message || "Give me." ); | |
// Setup the form inputs (using modal params). | |
$scope.form = { | |
input: ( modals.params().placeholder || "" ) | |
}; | |
$scope.errorMessage = null; | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// Wire the modal buttons into modal resolution actions. | |
$scope.cancel = modals.reject; | |
// I process the form submission. | |
$scope.submit = function() { | |
// If no input was provided, show the user an error message. | |
if ( ! $scope.form.input ) { | |
return( $scope.errorMessage = "Please provide something!" ); | |
} | |
modals.resolve( $scope.form.input ); | |
}; | |
} | |
); | |
// -------------------------------------------------- // | |
// -------------------------------------------------- // | |
// I manage the modals within the application. | |
app.service( | |
"modals", | |
function( $rootScope, $q ) { | |
// I represent the currently active modal window instance. | |
var modal = { | |
deferred: null, | |
params: null | |
}; | |
// Return the public API. | |
return({ | |
open: open, | |
params: params, | |
proceedTo: proceedTo, | |
reject: reject, | |
resolve: resolve | |
}); | |
// --- | |
// PULBIC METHODS.s | |
// --- | |
// I open a modal of the given type, with the given params. If a modal | |
// window is already open, you can optionally pipe the response of the | |
// new modal window into the response of the current (cum previous) modal | |
// window. Otherwise, the current modal will be rejected before the new | |
// modal window is opened. | |
function open( type, params, pipeResponse ) { | |
var previousDeferred = modal.deferred; | |
// Setup the new modal instance properties. | |
modal.deferred = $q.defer(); | |
modal.params = params; | |
// We're going to pipe the new window response into the previous | |
// window's deferred value. | |
if ( previousDeferred && pipeResponse ) { | |
modal.deferred.promise | |
.then( previousDeferred.resolve, previousDeferred.reject ) | |
; | |
// We're not going to pipe, so immediately reject the current window. | |
} else if ( previousDeferred ) { | |
previousDeferred.reject(); | |
} | |
// Since the service object doesn't (and shouldn't) have any direct | |
// reference to the DOM, we are going to use events to communicate | |
// with a directive that will help manage the DOM elements that | |
// render the modal windows. | |
// -- | |
// NOTE: We could have accomplished this with a $watch() binding in | |
// the directive; but, that would have been a poor choice since it | |
// would require a chronic watching of acute application events. | |
$rootScope.$emit( "modals.open", type ); | |
return( modal.deferred.promise ); | |
} | |
// I return the params associated with the current params. | |
function params() { | |
return( modal.params || {} ); | |
} | |
// I open a modal window with the given type and pipe the new window's | |
// response into the current window's response without rejecting it | |
// outright. | |
// -- | |
// This is just a convenience method for .open() that enables the | |
// pipeResponse flag; it helps to make the workflow more intuitive. | |
function proceedTo( type, params ) { | |
return( open( type, params, true ) ); | |
} | |
// I reject the current modal with the given reason. | |
function reject( reason ) { | |
if ( ! modal.deferred ) { | |
return; | |
} | |
modal.deferred.reject( reason ); | |
modal.deferred = modal.params = null; | |
// Tell the modal directive to close the active modal window. | |
$rootScope.$emit( "modals.close" ); | |
} | |
// I resolve the current modal with the given response. | |
function resolve( response ) { | |
if ( ! modal.deferred ) { | |
return; | |
} | |
modal.deferred.resolve( response ); | |
modal.deferred = modal.params = null; | |
// Tell the modal directive to close the active modal window. | |
$rootScope.$emit( "modals.close" ); | |
} | |
} | |
); | |
// I manage the views that are required to render the modal windows. I don't | |
// actually define the modals in anyway - I simply decide which DOM sub-tree | |
// should be linked. The means by which the modal window is defined is | |
// entirely up to the developer. | |
app.directive( | |
"bnModals", | |
function( $rootScope, modals ) { | |
// Return the directive configuration. | |
return( link ); | |
// I bind the JavaScript events to the scope. | |
function link( scope, element, attributes ) { | |
// I define which modal window is being rendered. By convention, | |
// the subview will be the same as the type emitted by the modals | |
// service object. | |
scope.subview = null; | |
// If the user clicks directly on the backdrop (ie, the modals | |
// container), consider that an escape out of the modal, and reject | |
// it implicitly. | |
element.on( | |
"click", | |
function handleClickEvent( event ) { | |
if ( element[ 0 ] !== event.target ) { | |
return; | |
} | |
scope.$apply( modals.reject ); | |
} | |
); | |
// Listen for "open" events emitted by the modals service object. | |
$rootScope.$on( | |
"modals.open", | |
function handleModalOpenEvent( event, modalType ) { | |
scope.subview = modalType; | |
} | |
); | |
// Listen for "close" events emitted by the modals service object. | |
$rootScope.$on( | |
"modals.close", | |
function handleModalCloseEvent( event ) { | |
scope.subview = null; | |
} | |
); | |
} | |
} | |
); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment