Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created March 23, 2015 12:19
Show Gist options
  • Save bennadel/3425dc3614a51aa0f3e6 to your computer and use it in GitHub Desktop.
Save bennadel/3425dc3614a51aa0f3e6 to your computer and use it in GitHub Desktop.
Creating A Simple Modal System In AngularJS
<!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>
&mdash;
<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>
&mdash;
<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