Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Phil Blad
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Phil Blad

Using CMD+Enter To Submit A Form In AngularJS

By
Published Comments (7)

On many online forms, the CMD+Enter (Command key plus Enter key) combination is being used to submit the form while the user is still focused on a textarea input. This allows the user to quickly submit a form without having to use the mouse (or the Tab key) to click on the Submit button. I love this user interaction pattern and cringe anytime it's not available. As such, I thought it would be fun to create an AngularJS directive that makes tapping into the CMD+Enter key combination as easy as possible.

NOTE: If you are focused in a simple text input, the Enter key will automatically submit a form under must "normal" circumstances.

Run this demo in my JavaScript Demos project on GitHub.

Most event-related directives, like ngClick, just take an expression and evaluate it when the event is triggered. And, the CDM+Enter directive works in the same way. But, since the CMD+Enter interaction is almost always used in the context of a form submission, I thought it would also be helpful to have the directive conditionally tap into the parent form behavior as well. This way, you wouldn't need to duplicate the expression that needs be evaluated.

So, the way I created this directive is that if you provide an expression, it will evaluate that expression when the user enters the CMD+Enter key combination. However, if you omit an expression, the directive will travel up the DOM (Document Object Model) tree and find the closest Form element. It will then use the triggerHandler() jQLite method to trigger the "submit" event on that Form element. This way, you can use the CMD+Enter directive as an arbitrary event handler; or, as an alternate means of form submission.

To see this in action, I've created a tiny demo in which you have a chat form. The chat form has a textarea input that we can submit using the CMD+Enter key combination. In this case, I am omitting an explicit expression and just letting the directive - bn-cmd-enter - tap into the natural form-submission processing.

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		Using CMD+Enter To Submit A Form In AngularJS
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">

	<h1>
		Using CMD+Enter To Submit A Form In AngularJS
	</h1>

	<form ng-submit="processMessage()">
		<!--
			The bn-cmd-enter directive can take an expression to evaluate upon
			CMD+Enter; or, if the expression is omitted, it will travel up the
			DOM and trigger the "submit" event on the closest form element that
			it finds.
			--
			NOTE: When triggering the event, it uses the triggerHandler() method
			to trigger "submit" event, not the trigger() method. This means that
			only the first bound event handler will be invoked.
		-->
		<textarea
			ng-model="form.message"
			bn-cmd-enter
			placeholder="Use CDM+Enter to submit form.">
		</textarea>

		<button type="submit">Send Message</button>
	</form>

	<ul class="chat">
		<li
			ng-repeat="message in messages track by message.id"
			class="chat-item">
			{{ message.text }}
		</li>
	</ul>


	<!-- Load scripts. -->
	<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.min.js"></script>
	<script type="text/javascript">

		// Create an application module for our demo.
		angular.module( "Demo", [] );


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I control the root of the application.
		angular.module( "Demo" ).controller(
			"AppController",
			function AppController( $scope ) {

				// I am the collection of historical chat items.
				$scope.messages = [{
					id: 1,
					text: "Good morning!"
				}];

				// I hold the form data for the ng-model bindings.
				$scope.form = {
					message: ""
				};


				// ---
				// PUBLIC METHODS.
				// ---


				// I process the chat form.
				$scope.processMessage = function() {

					if ( $scope.form.message ) {

						$scope.messages.unshift({
							id: ( new Date() ).getTime(),
							text: $scope.form.message
						});

						$scope.form.message = "";

					}

				};

			}
		);


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I evaluate the given expression, or submit the parent form, when the user
		// enters the CMD+Enter key combination on the current element.
		angular.module( "Demo" ).directive(
			"bnCmdEnter",
			function cmdEnterDirective() {

				// Return the directive configuration object.
				// --
				// NOTE: The formController is optional and is only used to determine
				// if there is a parent form. And, this is only checked if an explicit
				// expression is not provided.
				return({
					link: link,
					require: "^?form",
					restrict: "A"
				});


				// I bind the JavaScript events to the view-model.
				function link( scope, element, attributes, formController ) {

					// If there is neither an expression nor a parent form (as determined
					// by the existence of a form controller), then just exit out of the
					// directive; there are no actions that we can take.
					if ( ! attributes.bnCmdEnter && ! formController ) {

						return;

					}

					// Start listening for the CMD+Enter event.
					element.on( "keydown", handleKeyEvent );


					// ---
					// PRIVATE METHODS.
					// ---


					// I travel up the DOM looking for the closest form element.
					function closestForm() {

						var parent = element.parent();

						while ( parent.length && ( parent[ 0 ].tagName !== "FORM" ) ) {

							parent = parent.parent();

						}

						return( parent );

					}


					// I listen for key events, looking for the CMD+Enter combination.
					function handleKeyEvent( event ) {

						// Is CMD+Enter event.
						if ( ( event.which === 13 ) && event.metaKey ) {

							event.preventDefault();

							attributes.bnCmdEnter
								? triggerExpression( event )
								: triggerFormEvent()
							;

						}

					}


					// I evaluate the expression passed-into the directive.
					// --
					// NOTE: The event is exposed as "$event". While I don't think the
					// calling context should have to know about the event object, this
					// seems to be a pattern that AngularJS uses.
					function triggerExpression( keyEvent ) {

						scope.$apply(
							function changeViewModel() {

								scope.$eval(
									attributes.bnCmdEnter,
									{
										$event: keyEvent
									}
								);

							}
						);

					}


					// I find the closest form element and trigger the "submit" event.
					function triggerFormEvent() {

						closestForm().triggerHandler( "submit" );

					}

				}

			}
		);

	</script>

</body>
</html>

Since we are using the ngSubmit directive and a Submit button, it means that the user can always submit the form using the button. However, the bn-cmd-enter directive also allows the user to submit the form by providing an alternate means of triggering the "submit" event.

Because I'm using the triggerHandler() method internally, it does mean that only the native AngularJS Form directive will see the event (it always binds first in the pre-linking phase). But, I don't see that as a big problem. More often than not, you're using the ngSubmit directive to process the form data anyway. It's unlikely that there is another custom directive also listening for the "submit" event.

I love the CMD+Enter user interaction pattern. And, hopefully, directives likes this make it completely painless to integrate into your AngularJS applications.

Want to use code from this post? Check out the license.

Reader Comments

5 Comments

This is really cool code, thanks. I probably need to read through the directive a few times before I can understand it though... or maybe I'll watch the video.

2 Comments

Hey Ben,

that's super cool! Thanks for sharing.

Do you have plans to publish it as a module? I think a lot of people are looking for something like that. ;)

15,848 Comments

@Stefan,

That's a really good question. I've never officially published anything before. I'm not against it, just not sure how to start. I'm just now trying to learn about things like WebPack and using npm inside front-end development.

2 Comments

@Ben,

cool.

That's not a problem at all. If you want, you can set up a repo on GitHub and I'll help out with making it ready for publishing. :)

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel