<!doctype html> <html ng-app="Demo"> <head> <meta charset="utf-8" /> <title> Fun With Emoticons And Service Providers In AngularJS </title> <link rel="stylesheet" type="text/css" href="./demo.css"></link> </head> <body ng-controller="AppController as vm"> <h1> Fun With Emoticons And Service Providers In AngularJS </h1> <form> <input type="text" ng-model="vm.text" size="60" /> </form> <p ng-bind-html="vm.content"> <!-- Emoticon-embedded content will appear here. --> </p> <!-- Load scripts. --> <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.2.min.js"></script> <script type="text/javascript"> // Create an application module for our demo. angular.module( "Demo", [] ); // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // During the configuration phase, let's update the emoticon functionality. The // provider allows us to overwrite the tokens collection and / or define aliases. angular.module( "Demo" ).config( function configureEmoticons( emoticonsProvider ) { // Setup common alias values. // -- // CAUTION: The more alias values we setup, the more complex the pattern // matching will become, which may have an affect on performance. emoticonsProvider.addAlias( ":)", "smile" ); emoticonsProvider.addAlias( ":D", "smiley" ); emoticonsProvider.addAlias( ":((", "rage" ); emoticonsProvider.addAlias( ":(", "frowning" ); emoticonsProvider.addAlias( ":'(", "cry" ); emoticonsProvider.addAlias( ":*", "kissing" ); // Override the token collection with our more robust offering. emoticonsProvider.setTokens([ "smile", "laughing", "blush", "smiley", "relaxed", "smirk", "heart_eyes", "kissing_heart", "kissing_closed_eyes", "flushed", "relieved", "satisfied", "grin", "wink", "winky_face", "grinning", "kissing", "kissing_smiling_eyes", "stuck_out_tongue", "sleeping", "worried", "frowning", "anguished", "open_mouth", "wow", "grimacing", "confused", "hushed", "expressionless", "unamused", "sweat_smile", "sweat", "weary", "pensive", "disappointed", "confounded", "fearful", "cold_sweat", "persevere", "cry", "sob", "joy", "astonished", "scream", "angry", "rage", "triumph", "sleepy", "yum", "mask", "sunglasses", "dizzy_face", "lips", "kiss", "mouse", "poop" ]); } ); // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // I control the root of the application. angular.module( "Demo" ).controller( "AppController", function provideAppController( $scope, $sce, emoticons ) { var vm = this; // I am the text coming out of the ngModel value. vm.text = "Hello world :smile:"; // I am the HTML-version of the emoticon text. vm.content = null; // When the user updates the ngModel text, we want to update the emoticons // in realtime and inject them into the page. $scope.$watch( "vm.text", function textChanged( newValue, oldValue ) { // Since we are injecting HTML, we have to tell Angular to trust // that the HTML we are providing is safe and doesn't expose a // security risk. vm.content = $sce.trustAsHtml( emoticons.injectTags( newValue ) ); } ); } ); // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // I provide an emoticons service that can inject HTML tags into plain text // markup using a set of emoticon tokens and emoticon alias values. angular.module( "Demo" ).provider( "emoticons", function emoticonsProvider() { // I am the tag name that will be used to create the HTML tag that // represents the emoticons in the markup. For example, <i></i>. var tagName = "i"; // I am the collection of tokens that can be used to identify emoticons // in the plain-text content. These tokens will also be used to define // the CSS classes in the markup. For example, "emoticon emoticon-smile". var tokens = [ "smile", "smiley", "frowning" ]; // I define the RegExp patterns that are used to search for and validate // emoticon tokens. var tokenSearchPattern = /:([\w+-]+):/g; var tokenValidationPattern = /^([\w+-]+)$/i; // I am the base CSS class used when generating the markup. var baseCssClass = "emoticon"; // I am the collection of alias values that represent emoticons. So, // instead of having a user enter ":smile:", maybe you want to have the // value, ":)" automatically work. In that case, ":)" would be an alias // for ":smile:". var aliases = []; // Expose the public API for the provider. return({ addAlias: addAlias, setTagName: setTagName, setTokens: setTokens, $get: emoticons }); // --- // PUBLIC METHODS. // --- // I allow non-token patterns to be mapped to emoticons. For example, // you might want to allow the ":)" pattern to be automatically mapped // to the ":smile:" pattern. // -- // CAUTION: Alias values are use to pre-treat the plain-text input using // a more complex pattern. As such, they are more expensive from a // performance standpoint. function addAlias( alias, token ) { testAlias( alias ); testToken( token ); // NOTE: I am not validating the alias-token connection at this point // since tokens may be changed later in the configuration phase. This // connection will validated during service initialization. aliases.push({ text: alias, token: token }); } // I allow the tag name to be overridden during the configuration phase. function setTagName( newTagName ) { tagName = newTagName; } // I allow the tokens to be overridden during the configuration phase. function setTokens( newTokens ) { // Because the tokens are located in plain-text using a regular // expression pattern, we need to ensure that each token adheres to // a particular format. for ( var i = 0, length = newTokens.length ; i < length ; i++ ) { testToken( newTokens[ i ] ); } // If we made it this far, all the new tokens are valid. tokens = newTokens; } // --- // PRIVATE METHODS. // --- // I test to make sure that the given alias is valid. If the alias is not // valid, an error is thrown; otherwise, returns quietly. function testAlias( newAlias ) { if ( ! newAlias.length ) { throw( new Error( "Alias cannot be an empty string." ) ); } if ( newAlias.indexOf( " " ) !== -1 ) { throw( new Error( "Alias [" + newAlias + "] cannot contain white space." ) ); } } // I test the format of the given token to make sure that it conforms to // the pattern we will be searching for in the text. If the token is not // valid, an error is thrown; otherwise, returns quietly. function testToken( newToken ) { if ( newToken.search( tokenValidationPattern ) !== 0 ) { throw( new Error( "Token [" + newToken + "] is not a valid emoticon." ) ); } } // --- // FACTORY FUNCTION. // --- // I am the actual emoticons service. function emoticons() { // I am a hash that maps the token values to pre-composed HTML tag // that represents the emoticon markup. var tokenMap = createTokenMap( tokens, baseCssClass ); // I am a hash that maps alias values onto token values. var aliasMap = createAliasMap( aliases, tokenMap ); // I am the [more] complex pattern that is used to search for both // standard token values and non-standard alias tokens. var compoundSearchPattern = createCompoundSearchPattern( aliases ); // Return the public API. return({ injectTags: injectTags }); // --- // PUBLIC METHODS. // --- // I take plain-text content and replace the emoticon tokens with // actual HTML tags that represent the graphical emotions. function injectTags( text ) { // If the text is empty, or not text, just pass it through. if ( ! text || ! angular.isString( text ) ) { return( text ); } // Search for and replace emoticon markers with HTML tags. var emotionalText = text.replace( compoundSearchPattern, function replaceMatch( $0, token, alias ) { // The first part of this pattern is looking for // normal tokens. This is so that we don't accidentally // find alias values inside of other tokens; the tokens // take precedence, like a boss. if ( token ) { return( tokenMap.hasOwnProperty( token ) ? tokenMap[ token ] : $0 ); } // If we didn't find a token, then by factor of // elimination, we must have found an alias. Replace it // with the appropriate HTML tag. // -- // NOTE: Since we only allow alias values that map onto // known tokens, we don't have to check to see if the // alias maps onto a valid token. return( tokenMap[ aliasMap[ alias ] ] ); } ); return( emotionalText ); } // --- // PRIVATE METHODS. // --- // I create an alias map that maps alias values onto token values. // If the alias points to a token that is not defined, an error is // thrown - since alias values present a more complex pattern, I // will not suffer unnecessary alias values. function createAliasMap( aliases, tokenMap ) { var aliasMap = {}; for ( var i = 0, length = aliases.length ; i < length ; i++ ) { var alias = aliases[ i ]; if ( ! tokenMap.hasOwnProperty( alias.token ) ) { throw( new Error( "Alias [" + alias.text + "] does not map to a known emoticon token." ) ); } aliasMap[ alias.text ] = alias.token; } return( aliasMap ); } // I create a RegExp object that will search for the alias values // in a single pattern. // -- // CAUTION: The pattern will preferentially search for normal token // values. function createCompoundSearchPattern( aliases ) { // If there are no aliases, we can just use the core token search // pattern. if ( ! aliases.length ) { return( tokenSearchPattern ); } // Since we do have alias values, we need to aggregate the // collection so that we can collapse them all down into a single // regular expression pattern group. var aliasPatterns = []; for ( var i = 0, length = aliases.length ; i < length ; i++ ) { aliasPatterns.push( quotePatternText( aliases[ i ].text ) ); } // When we create our compound pattern, we want to give precedence // to the standard token search. Then, only a secondary fallback // do we want to allow the RegExp engine to start looking for the // alias values. In this case, we have the following groups: // -- // $0 - matched value. // $1 - standard token. // $2 - alias value. return( new RegExp( ( tokenSearchPattern.source + "|(" + aliasPatterns.join( "|" ) + ")" ), "g" ) ); } // Rather than having to convert tokens to tags over and over again, // we can pre-compose the HTML tags during service initialization. // This way, we only eat that cost once. Returns a hash of tokens // mapped to their corresponding HTML tag. function createTokenMap( tokens, baseCssClass ) { var tagMap = {}; for ( var i = 0, length = tokens.length ; i < length ; i++ ) { var token = tokens[ i ]; tagMap[ token ] = createTokenTag( token, baseCssClass ); } return( tagMap ); } // I create an HTML tag that represents the given token. function createTokenTag( token, baseCssClass ) { // Each emoticon tag has two classes - the base class plus // a token-specific extension. For example: "emoticon emoticon-smile". var className = ( baseCssClass + " " + baseCssClass + "-" + token ); return( "<" + tagName + " title=':" + token + ":' class='" + className + "'></" + tagName + ">" ); } // I return the escaped RegExp pattern text that can be used to // safely create a RegExp instance, regardless of whether or not the // text contains "special" embedded characters. function quotePatternText( alias ) { // Escape characters that are meaningful in a RegExp context. return( alias.replace( /([.()+*[\]{}?-])/g, "\\$1" ) ); } } } ); </script> </body> </html>