Upload
tranbao
View
251
Download
4
Embed Size (px)
Citation preview
www.allitebooks.com
www.allitebooks.com
www.allitebooks.com
LearningAngularJS
byKenWilliamson
Copyright©2015KenWilliamson.Allrightsreserved.
PrintedintheUnitedStatesofAmerica.
PublishedbyO’ReillyMedia,Inc.,1005GravensteinHighwayNorth,Sebastopol,CA95472.
O’Reillybooksmaybepurchasedforeducational,business,orsalespromotionaluse.Onlineeditionsarealsoavailableformosttitles(http://safaribooksonline.com).Formoreinformation,contactourcorporate/institutionalsalesdepartment:[email protected].
Editor:MegFoley
ProductionEditor:NicoleShelby
Copyeditor:RachelHead
Proofreader:RachelMonaghan
Indexer:WordCo.IndexingServices
InteriorDesigner:DavidFutato
CoverDesigner:EllieVolckhausen
Illustrator:RebeccaDemarest
March2015:FirstEdition
www.allitebooks.com
RevisionHistoryfortheFirstEdition2015-03-10:FirstRelease
Seehttp://oreilly.com/catalog/errata.csp?isbn=9781491916759forreleasedetails.
Whilethepublisherandtheauthorhaveusedgoodfaitheffortstoensurethattheinformationandinstructionscontainedinthisworkareaccurate,thepublisherandtheauthordisclaimallresponsibilityforerrorsoromissions,includingwithoutlimitationresponsibilityfordamagesresultingfromtheuseoforrelianceonthiswork.Useoftheinformationandinstructionscontainedinthisworkisatyourownrisk.Ifanycodesamplesorothertechnologythisworkcontainsordescribesissubjecttoopensourcelicensesortheintellectualpropertyrightsofothers,itisyourresponsibilitytoensurethatyourusethereofcomplieswithsuchlicensesand/orrights.
978-1-491-91675-9
[LSI]
www.allitebooks.com
IwouldliketothankmysonChrisforallhishelpandforbeingasoundingboard.Thanks,Chris.
www.allitebooks.com
www.allitebooks.com
Preface
Theworldofsoftwaredevelopmenthaschangeddrasticallyoverthelastfewdecades.Manysoftwaremethodologiesandconceptsthatwereconsidered“cuttingedge”20orsoyearsagoarenowcommonpracticeinthefieldofsoftwaredevelopment,andhavebeenforyears.OneexampleistheWorldWideWebandtheuseofwebbrowserstodeliversoftwaretousers.In1993,theconceptofdeliveringsoftwareovertheInternetthatcouldthenruninawebbrowseronanymachinerunningonanyoperatingsystemwasconsideredbleedingedge.Butasanycomputeruserknows,thatpracticehasbeencommonplaceforyearsnow.
WhenJavaScriptclient-sidewebapplicationframeworkslikeAngularJS,Backbone.js,andEmber.jsfirstappeared,theywereconsideredtoocuttingedgeformostserioussoftwareprojects.Astheymatured,however,softwarearchitectsanddeveloperssawgreatpotentialintheseframeworks.ApplicationsbuiltwithJavaScriptclient-sideframeworksexistandrunentirelyontheuser’shardware,muchlikeconventionalthick-clientapplications.Applicationswrittenusingtheseframeworksaremuchfasterthanconventionalwebapplicationsandprovideamuchbetteruserexperience.
Overthelastcoupleofyears,JavaScriptclient-sideframeworkshavemadegreatstridesinfunctionalityandreliability,andtheyarenowheavilyusedtobuildmobileHTML5applications.Butmobileapplicationsareonlythestartingpoint.Theseframeworksnowhavethepotentialtoradicallychangethewaywebuildmodernwebapplicationsoftware.OfalltheJavaScriptframeworksavailable,AngularJS,backedbyGoogle,istheonethatshinesthebrightest.
AngularJShasmanyadvantagesoverotherJavaScriptclient-sideframeworks.AngularJSusestheMVCdesignpatternandembracesthatpatterncompletely.Themodel,view,andcontrollerareallclearlydefinedinAngularJSandservetogreatlysimplifythedevelopmentprocess.WithAngularJS,developerscanbuildapplicationsthathaveaclearseparationbetweentheirfunctionallayers.
OneofthegreatestadvantagesofAngularJSoverotherJavaScriptclient-sideframeworksistheuniquewayinwhichitletsdevelopersinteractwithRESTfulwebservices.AngularJS’sresourceobjectletsdevelopersinteractwithRESTserviceslikestandardobjects.ThecomplexityofRESTservicescanbegreatlysimplifiedusingthisapproach:withonlyafewlinesofcode,youcancreateanAngularJSservicethatinteractswithmultiplebackendRESTservices.Thoseservicescanthenbeusedthroughoutyourapplication,reducingthetotalnumberoflinesofcode.
Infact,oneofthebiggestadvantagesofAngularJSoverotherclient-sideframeworksisitsconceptofservices.AngularJSserviceshelptogreatlysimplifyanapplicationbycompartmentalizingclient-sidelogicintosingleunitsofcode.Thosesingleunits,called
www.allitebooks.com
services,canthenbeusedrepeatedlythroughoutanapplication.AngularJSservicesproveespeciallypowerfulwhenyou’rebuildinglargeenterpriseapplicationswithmanylinesofcodeandmuchcomplexity.ComplexlogiccanbewrittenonlyonceinsideanAngularJSserviceandthenusedwhereverneeded.ThatalonemakesAngularJSthebestchoiceforyournextJavaScriptproject.
Thankstothisuseofservicesanditsall-inclusivedesign,AngularJShelpsdeveloperswritelesscode,therebygreatlyreducingapplicationcomplexity.ThesimplicityofAngularJSmakesiteasytolearnandeasytouse.AnytimespentlearningAngularJSistimewellspent.AnytimespentdevelopingAngularJSapplicationsistimespentturningacutting-edgetechnologyintoacommonplacetechnology.InthisbookIstrivetohelpyoudoboth,encouragingdesignconceptsandpracticesthatwillhelpyoubuildbetterAngularJSapplications.
www.allitebooks.com
WhyIWroteThisBookIconstantlyseedevelopmentteamsavoidusingAngularJSbecauseofitsperceivedsteeplearningcurve.ThosesameteamsoftenchooseotherJavaScriptframeworksbecausetheyinitiallyseemeasiertolearn.ButAngularJSisnothardtolearnatall.ItisactuallymucheasiertolearnthanotherJavaScriptframeworks,ifthelearningprocessisapproachedcorrectly.Likemanyothers,IstruggledtolearnAngularJSinthebeginning.ThisbookwaswrittentohelpdevelopersavoidtheearlystrugglesassociatedwithlearningAngularJSandgetstartedbuildingAngularJSapplicationsandwebsitesveryquickly.
WhatThisBookCoversThisbookcoverseverythingyouneedtoknowtobuildfullyfunctionalAngularJSapplications.ThebookstartsoffwiththebasicsofAngularJS.YouwilllearnaboutAngularJScomponentsinearlychapters.Aschaptersprogress,youwillgethands-onexperiencebuildingworkingAngularJSprojects.
Neartheendofthebook,youwillwritetheAngularJSpartofaworkingMEANstackblogapplicationanddeploytheapplicationtothecloud.MEANstandsforMongoDB,ExpressJS,AngularJS,andNode.js.ManyindustryexpertsbelievetheMEANstackwillbeadominantwebdevelopmentplatformincomingyears.
Afterreadingthisbook,youwillhavetheknowledgetostartbuildinghigh-qualityAngularJSapplicationsandwebsites.YouwillalsogainaclearunderstandingofthedesignconceptsassociatedwithAngularJSapplications,andofsecurityasitrelatestoAngularJSapplications.
WhoShouldReadThisBookThisbookisintendedforanyonewhohasaninterestinlearningtodevelopAngularJSapplicationsorwebsitesquickly.ItwillalsobehelpfultoanyoneinterestedinlearninghowAngularJSisusedinaMEANstackapplication.ThereaderwillgainnotonlyaconceptualunderstandingofAngularJS,buthands-onexperienceaswell.AnyonereadingthisbookshouldhavesomeknowledgeofJavaScript,softwaredesignconcepts,andsoftwaredesignpatterns.Apriorknowledgeofwebdevelopmentwillalsobehelpful.
TheChaptersinThisBookThisbookstartsoffwiththeverybasicsofAngularJSandassumesthereaderhasnopriorknowledgeoftheframework.Thechaptersarearrangedasfollows:
Chapter1,IntroductiontoAngularJS,startsoffwithabasicintroductiontoAngularJS.IthelpsthereaderunderstandhowAngularJSdiffersfromotherframeworks.
Chapter2,TheIDEandAngularJSProjects,coverssettingupadevelopmentenvironmentandbuildingAngularJSprojects.ItalsocovershowtosetupatestenvironmentforAngularJS.
Chapter3,MVCandAngularJS,comparesAngularJStotraditionalwebMVCframeworks.ItshowswhyAngularJSisabetterframeworkforbuildingmodernwebapplicationsandwebsites.
Chapter4,AngularJSControllers,isadiscussionofAngularJScontrollers.Thereaderwillbuildpartofaworkingapplicationandimplementcontrollertestingneartheendofthechapter.
Chapter5,AngularJSViewsandBootstrap,coversAngularJSviewsbuiltwithTwitterBootstrap.Thereaderwillcontinueworkingonafunctionalapplicationandimplementtesting.
Chapter6,AngularJSandRESTServices,coversAngularJSservicesastheyrelatetoRESTservices.ThereaderwillcontinueworkingontheapplicationandconnectittopublicRESTserviceswrittenforthisbook.
Chapter7,AngularJSModels,coversAngularJSmodelsandhowtheyrelatetocontrollersandviews.Thereaderwillcontinueworkingontheapplicationstartedearlier.
Chapter8,ServicesandBusinessLogic,coversnon-RESTAngularJSservices.Inthischapterthereaderwillbuildpartofthesecuritylayerusedlaterinthebook.
Chapter9,AngularJSDirectives,coversthebasicsofbuildingandtestingAngularJSdirectives.
Chapter10,AngularJSSecurity,showsthereaderhowtousethesecuritylayerintroducedinChapter8tosecuretheAngularJSapplicationstartedearlier.
Chapter11,MEANCloudandMobile,showsthereaderhowtousetheAngularJSapplicationdevelopedinpreviouschaptersinaMEANstackapplicationandinamobileapplication.
Chapter12,AngularJSandSEO,coverssearchengineoptimizationasitrelatestoAngularJSapplicationsandwebsites.
ConventionsUsedinThisBookThefollowingtypographicalconventionsareusedinthisbook:
Italic
Indicatesnewterms,URLs,emailaddresses,filenames,andfileextensions.Constantwidth
Usedforprogramlistings,aswellaswithinparagraphstorefertoprogramelementssuchasvariableorfunctionnames,databases,datatypes,environmentvariables,statements,andkeywords.
Constantwidthbold
Showscommandsorothertextthatshouldbetypedliterallybytheuser.Constantwidthitalic
Showstextthatshouldbereplacedwithuser-suppliedvaluesorbyvaluesdeterminedbycontext.
NOTEThiselementsignifiesageneralnote.
WARNINGThiselementsignifiesawarningorcaution.
UsingCodeExamplesSupplementalmaterial(codeexamples,exercises,etc.)isavailablefordownloadathttps://github.com/KenWilliamson.
Thisbookisheretohelpyougetyourjobdone.Ingeneral,ifexamplecodeisofferedwiththisbook,youmayuseitinyourprogramsanddocumentation.Youdonotneedtocontactusforpermissionunlessyou’rereproducingasignificantportionofthecode.Forexample,writingaprogramthatusesseveralchunksofcodefromthisbookdoesnotrequirepermission.SellingordistributingaCD-ROMofexamplesfromO’Reillybooksdoesrequirepermission.Answeringaquestionbycitingthisbookandquotingexamplecodedoesnotrequirepermission.Incorporatingasignificantamountofexamplecodefromthisbookintoyourproduct’sdocumentationdoesrequirepermission.
Weappreciate,butdonotrequire,attribution.Anattributionusuallyincludesthetitle,author,publisher,andISBN.Forexample:“LearningAngularJSbyKenWilliamson(O’Reilly).Copyright2015KenWilliamson,978-1-491-91675-9.”
Ifyoufeelyouruseofcodeexamplesfallsoutsidefairuseorthepermissiongivenabove,[email protected].
Safari®BooksOnlineSafariBooksOnlineisanon-demanddigitallibrarythatdeliversexpertcontentinbothbookandvideoformfromtheworld’sleadingauthorsintechnologyandbusiness.
Technologyprofessionals,softwaredevelopers,webdesigners,andbusinessandcreativeprofessionalsuseSafariBooksOnlineastheirprimaryresourceforresearch,problemsolving,learning,andcertificationtraining.
SafariBooksOnlineoffersarangeofplansandpricingforenterprise,government,education,andindividuals.
Membershaveaccesstothousandsofbooks,trainingvideos,andprepublicationmanuscriptsinonefullysearchabledatabasefrompublisherslikeO’ReillyMedia,PrenticeHallProfessional,Addison-WesleyProfessional,MicrosoftPress,Sams,Que,PeachpitPress,FocalPress,CiscoPress,JohnWiley&Sons,Syngress,MorganKaufmann,IBMRedbooks,Packt,AdobePress,FTPress,Apress,Manning,NewRiders,McGraw-Hill,Jones&Bartlett,CourseTechnology,andhundredsmore.FormoreinformationaboutSafariBooksOnline,pleasevisitusonline.
HowtoContactUsPleaseaddresscommentsandquestionsconcerningthisbooktothepublisher:
O’ReillyMedia,Inc.
1005GravensteinHighwayNorth
Sebastopol,CA95472
800-998-9938(intheUnitedStatesorCanada)
707-829-0515(internationalorlocal)
707-829-0104(fax)
Wehaveawebpageforthisbook,wherewelisterrata,examples,andanyadditionalinformation.Youcanaccessthispageathttp://bit.ly/learning-angularjs.
Tocommentorasktechnicalquestionsaboutthisbook,[email protected].
Formoreinformationaboutourbooks,courses,conferences,andnews,seeourwebsiteathttp://www.oreilly.com.
FindusonFacebook:http://facebook.com/oreilly
FollowusonTwitter:http://twitter.com/oreillymedia
WatchusonYouTube:http://www.youtube.com/oreillymedia
Chapter1.IntroductiontoAngularJS
Google’sAngularJSisanall-inclusiveJavaScriptmodel-view-controller(MVC)frameworkthatmakesitveryeasytoquicklybuildapplicationsthatrunwellonanydesktopormobileplatform.Inaveryshortperiodoftime,AngularJShasmovedfrombeinganunknownopensourceofferingtooneofthebestknownandmostwidelyusedJavaScriptclient-sideframeworksoffered.AngularJS1.3andgreatercombinedwithjQueryandTwitterBootstrapgiveyoueverythingyouneedtorapidlybuildHTML5JavaScriptapplicationfrontendsthatuseRESTwebservicesforthebackendprocesses.ThisbookwillshowyouhowtouseallthreefrontendcomponentstoharnessthepowerofRESTservicesonthebackendandquicklybuildpowerfulmobileanddesktopapplications.
www.allitebooks.com
JavaScriptClient-SideFrameworksJavaScriptclient-sideapplicationsrunontheuser’sdeviceorPC,andthereforeshifttheworkloadtotheuser’shardwareandawayfromtheserver.Untilfairlyrecently,server-sidewebMVCframeworkslikeStruts,SpringMVC,andASP.NETweretheframeworksofchoiceformostweb-basedsoftwaredevelopmentprojects.JavaScriptclient-sideframeworks,however,aresustainablemodelsthatoffermanyadvantagesoverconventionalwebframeworks,suchassimplicity,rapiddevelopment,speedofoperation,testability,andtheabilitytopackagetheentireapplicationanddeployittoallmobiledevicesandtheWebwithrelativeease.Youcanbuildyourapplicationonetimeanddeployandrunitanywhere,onanyplatform,withnomodifications.That’spowerful.
AngularJSmakesthatprocessevenfasterandeasier.Ithelpsyoubuildfrontendapplicationsindaysratherthanmonthsandhascompletesupportforunittestingtohelpreducequalityassurance(QA)time.AngularJShasarichsetofuserdocumentationandgreatcommunitysupporttohelpanswerquestionsduringyourdevelopmentprocess.ModelsandviewsinAngularJSaremuchsimplerthanwhatyoufindinmostJavaScriptclient-sideframeworks.Controllers,oftenmissinginotherJavaScriptclient-sideframeworks,arekeyfunctionalcomponentsinAngularJS.
Figure1-1showsadiagramofanAngularJSapplicationandallrelatedMVCcomponents.OncetheAngularJSapplicationislaunched,themodel,view,controller,andallHTMLdocumentsareloadedontheuser’smobileordesktopdeviceandrunentirelyontheuser’shardware.Asyoucansee,callsaremadetothebackendRESTservices,whereallbusinesslogicandbusinessprocessesarelocated.ThebackendRESTservicescanbelocatedonaprivatewebserverorinthecloud(whichismostoftenthecase).CloudRESTservicescanscalefromahandfulofuserstomillionsofuserswithrelativeease.
Figure1-1.DiagramofanAngularJSMVCapplication
Single-PageApplicationsAngularJSismostoftenusedtobuildapplicationsthatconformtothesingle-pageapplication(SPA)concept.SPAsareapplicationsthathaveoneentrypointHTMLpage;alltheapplicationcontentisdynamicallyaddedtoandremovedfromthatonepage.YoucanseetheentrypointofourSPAintheindex.htmlcodethatfollows.Thetag<divng-view></div>iswherealldynamiccontentisinsertedintoindex.html:
<!--chapter1/index.html-->
<!DOCTYPEhtml>
<htmllang="en"ng-app="helloWorldApp">
<head>
<title>AngularJSHelloWorld</title>
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<metahttp-equiv="Content-Type"content="text/html;charset=UTF-8">
<scriptsrc="js/libs/angular.min.js"></script>
<scriptsrc="js/libs/angular-route.min.js"></script>
<scriptsrc="js/libs/angular-resource.min.js"></script>
<scriptsrc="js/libs/angular-cookies.min.js"></script>
<scriptsrc="js/app.js"></script>
<scriptsrc="js/controllers.js"></script>
<scriptsrc="js/services.js"></script>
</head>
<body>
<divng-view></div>
</body>
</html>
Astheuserclicksonlinksintheapplication,existingcontentattachedtothetagisremovedandnewdynamiccontentisthenattachedtothesametag.Ratherthantheuserwaitingforanewpagetoload,newcontentisdynamicallydisplayedinafractionofthetimethatitwouldtaketoloadanewHTMLwebpage.
TIPYoucandownloadthesourcefortheChapter1“Hello,World”applicationfromGitHub.
BootstrappingtheApplicationBootstrappingAngularJSistheprocessofloadingAngularJSwhenanapplicationfirststarts.LoadingtheAngularJSlibrariesinapagewillstartthebootstrapprocess.Theindex.htmlfileisanalyzed,andtheparserlooksfortheng-apptag.Theline<htmllang="en"ng-app="helloWorldApp"></html>showshowng-appisdefined.ThefollowingcodeshowstheJavaScriptthatisfiredbythatlineintheindex.htmlfile.Asyoucansee,app.jsiswheretheAngularJSapplicationhelloWorldAppisdefinedasanAngularJSmodule,andthisistheentrypointintotheapplication.ThevariablehelloWorldAppinthisfilecouldbenamedanything.Iwill,however,callithelloWorldAppforthesakeofuniformity:
/*chapter1/app.jsexcerpt*/
'usestrict';
/*AppModule*/
varhelloWorldApp=angular.module('helloWorldApp',[
'ngRoute',
'helloWorldControllers'
]);
DependencyInjectionDependencyinjection(DI)isadesignpatternwheredependenciesaredefinedinanapplicationaspartoftheconfiguration.Dependencyinjectionhelpsyouavoidhavingtomanuallycreateapplicationdependencies.AngularJSusesdependencyinjectiontoloadmoduledependencieswhenanapplicationfirststarts.Theapp.jscodeintheprevioussectionshowshowAngularJSdependenciesaredefined.
Asyoucansee,twodependenciesaredefinedasneededbythehelloWorldAppapplicationatstartup.Thedependenciesaredefinedinanarrayinthemoduledefinition.ThefirstdependencyistheAngularJSngRoutemodule,whichprovidesroutingtotheapplication.Theseconddependencyisourcontrollermodule,helloWorldControllers.Wewillcovercontrollersindepthlater,butfornowjustunderstandthatcontrollersareneededbyourapplicationsatstartuptime.
Dependencyinjectionisnotanewconcept.Itwasintroducedover10yearsagoandhasbeenusedconsistentlyinvariousapplicationframeworks;DIwasatthecoreofthepopularSpringframeworkwritteninJava.Oneofitsmainadvantagesisthatitreducestheneedforboilerplatecode,writingofwhichwouldnormallybeatime-consumingprocessforadevelopmentteam.
Dependencyinjectionalsohelpstomakeanapplicationmoretestable.ThatisoneofthemainadvantagesofusingAngularJStobuildJavaScriptapplications.AngularJSapplicationsaremucheasiertotestthanapplicationswrittenwithmostJavaScriptframeworks.Infact,thereisatestframeworkthathasbeenspecificallywrittentomaketestingAngularJSapplicationseasy.Wewilltalkmoreabouttestingattheendofthischapter.
AngularJSRoutesAngularJSroutesaredefinedthroughthe$routeProviderAPI.RoutesaredependentonthengRoutemodule,andthat’swhyitisarequirementwhentheapplicationstarts.Thefollowingcodefromapp.jsshowshowwedefineroutesinanAngularJSapplication.Tworoutesaredefined—thefirstis/andthesecondis/show:
/*chapter1/app.jsexcerpt*/
helloWorldApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'MainCtrl'}).
when('/show',{
templateUrl:'partials/show.html',
controller:'ShowCtrl'
});
ThetwodefinedroutesmapdirectlytoURLsdefinedintheapplication.Ifauserclicksonalinkintheapplicationspecifiedaswww.someDomainName/show,the/showroutewillbefollowedandthecontentassociatedwiththatURLwillbedisplayed.Iftheuserclicksonalinkspecifiedaswww.someDomainName/,the/routewillbefollowedandthatcontentwillbedisplayed.
HTML5ModeThecompleteapp.jsfileisshownnext.Thelastlineinapp.js($locationProvider.html5Mode(false).hashPrefix('!');)usesthelocationProviderservice.ThislineofcodeturnsofftheHTML5modeandturnsonthehashbangmodeofAngularJS.IfyouweretoturnonHTML5modeinsteadbypassingtrue,theapplicationwouldusetheHTML5HistoryAPI.HTML5modealsogivestheapplicationprettyURLslike/someAppName/blogPost/5insteadofthestandardAngularJSURLslike/someAppName/#!/blogPost/5thatusethe#!,knownasthehashbang.
/*chapter1/app.jscompletefile*/
'usestrict';
/*AppModule*/
varhelloWorldApp=angular.module('helloWorldApp',[
'ngRoute',
'helloWorldControllers'
]);
helloWorldApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'MainCtrl'
}).when('/show',{
templateUrl:'partials/show.html',
controller:'ShowCtrl'
});
$locationProvider.html5Mode(false).hashPrefix('!');
}]);
HTML5modecanprovideprettyURLs,butitdoesrequireconfigurationchangesonthewebserverinmostcases.Thechangesaredifferentforeachindividualwebserver,andcandifferfordifferentserverinstallationsaswell.HTML5modealsohandlesURLchangesinadifferentway,byusingtheHTMLHistoryAPIfornavigation.
UsingHTML5modeisjustaconfigurationchangeinAngularJS,andwewon’tcovertheneededserverchangesinthisbookasourfocusisonAngularJS.TheAngularJSsitehasdocumentationonthechangesneededforallmodernwebserverswhenHTML5modeisenabled.Usingthismodehassomebenefits,butwewillstickwithhashbangmodeinourchapterexercises.
Hashbangmodeisusedtosupportconventionalsearchenginesthatdon’thavetheabilitytoexecuteJavaScriptonAjaxsiteslikethosebuiltwithAngularJS.WhenaconventionalsearchenginesearchesasitebuiltwithAngularJSthatuseshashbangs,thesearchenginereplacesthe#!with?_escaped_fragment_=.ConventionalsearchenginesexpecttheservertohaveHTMLsnapshotsatthelocationwhere_escaped_fragment_=isconfiguredtopoint.HTMLsnapshotsaremerelycopiesoftheHTMLrenderedversionofthewebsiteorapplication.
ModernSearchEnginesFortunately,modernsearchengineshavetheabilitytoexecuteJavaScript,asannouncedbyGoogleinanewsreleaseonMay23,2014.HashbangmodealsoallowsAngularJSapplicationstostoreAjaxrequestedpagesinthebrowser’shistory.Thatprocessoftensimplifiesbrowserbookmarks.
AngularJSTemplatesAngularJSpartials,alsocalledtemplates,arecodesectionsthatcontainHTMLcodethatareboundtothe<divng-view></div></div>tagshownintheindex.htmlfileearlierinthischapter.Ifyoulookbackatthecompleteapp.jsfile,youcanseethatdifferenttemplateUrlvaluesaredefinedforeachroute.
Themain.htmlandshow.htmlfileslistednextshowthetwodefinedpartials(templates).ThetemplatescontainjustHTMLcode,withnothingspecialatthistime.Later,wewilluseAngularJS’sbuilt-intemplatelanguagetodisplaydynamicdatainourtemplates:
<!--chapter1/main.html-->
<div>HelloWorld</div>
<!--chapter1/show.html-->
<div>ShowTheWorld</div>
Astheuserclicksonthedifferentlinks,thevalueassignedto<divng-view>isreplacedwiththecontentoftheassociatedtemplatefiles.Thevalueofcontrollerdefinedforeachroutereferencesthecontrollercomponent(oftheMVCpattern)thatisdefinedforeachparticularroute.
ThenextsectionsprovideabriefoverviewofeachAngularJSMVCcomponentandhowitisused,togiveyouabetterunderstandingofhowAngularJSworks.UnlikemostJavaScriptclient-sideframeworks,AngularJSprovidesthemodel,view,andcontrollercomponentsforuseinallapplications.ThatoftenhelpsdevelopersfamiliarwithdesignpatternstoquicklygraspAngularJSconcepts.
AngularJSViews(MVC)ManyJavaScriptclient-sideframeworksrequireyoutoactuallydefinetheviewclassesinJavaScript,andtheycancontainanywherefromafewtohundredsoflinesofcode.SuchisnotthecasewithAngularJS.AngularJSpullsinallthetemplatesdefinedforanapplicationandbuildstheviewsinthedocumentobjectmodel(DOM)foryou.Therefore,theonlyworkyouneedtodotobuildtheviewsistocreatethetemplates.
BuildingviewsinAngularJSisasimpleprocessthatusesmostlyHTMLandCSS.ThesimplicityofAngularJSviewsisahugetime-saverwhenyou’rebuildingAngularJSapplications.WewillcovercreatingtemplatesinmoredetailinChapter5.
AngularJSModels(MVC)ManyJavaScriptclient-sideframeworksalsorequireyoutocreateJavaScriptmodelclasses.ThatisalsonotthecasewithAngularJS.AngularJShasa$scopeobjectthatisusedtostoretheapplicationmodel.ScopesareattachedtotheDOM.Thewaytoaccessthemodelisbyusingdatapropertiesassignedtothe$scopeobject.
TheAngularJS$scopehelpstosimplifyJavaScriptapplicationsconsiderably.OtherJavaScriptframeworksoftenencourageplacinglargeamountsofbusinesslogicinsidethemodelclassesfortheparticularframework.Unfortunately,thatpracticeoftenleadstoduplicatedbusinesslogic.Inalargeproject,thatcanleadtothousandsoflinesofuselesscode.WewilltalkmoreaboutmodelsinChapter7.
www.allitebooks.com
AngularJSControllers(MVC)AngularJScontrollersarethetapethatholdsthemodelsandviewstogether.Thecontrolleriswhereyoushouldplaceallbusinesslogicspecifictoaparticularviewwhenit’snotpossibletoplacethelogicinsideaRESTservice.BusinesslogicshouldalmostalwaysbeplacedinbackendRESTserviceswheneverpossible;thishelpstosimplifyAngularJSapplications.
Whenbusinesslogicplacedinsideanapplicationisusedbymultiplecontrollers,itshouldbeplacedinAngularJSnon-RESTservicesinstead.Thoseservicescanthenbeinjectedintoanycontrollerthatneedsaccesstothelogic.Wewillcovernon-RESTservicesinChapter8ingreatdetail.
ControllerBusinessLogicThefollowingcodeshowsthecontentsofthecontrollers.jsfile.AtthestartofthefilewedefinethehelloWorldControllermodule.Wethendefinetwonewcontrollers,MainCtrlandShowCtrl,andattachthemtothehelloWorldControllermodule.BusinesslogicspecifictotheMainCtrlcontrollerisdefinedinsidethatcontroller.Likewise,businesslogicspecifictotheShowCtrlcontrollerisdefinedinsidetheShowCtrlcontroller.Noticethat$scopeisinjectedintobothcontrollers.The$scopethatisinjectedintoeachcontrollerisspecifictothatcontrollerandnotvisibletoothercontrollers:
/*chapter1/controllers.js*/
'usestrict';
/*Controllers*/
varhelloWorldControllers=
angular.module('helloWorldControllers',[]);
helloWorldControllers.controller('MainCtrl',['$scope',
functionMainCtrl($scope){
$scope.message="HelloWorld";
}]);
helloWorldControllers.controller('ShowCtrl',['$scope',
functionShowCtrl($scope){
$scope.message="ShowTheWorld";
}]);
Asyoucansee,wearenowusingthemodeltopopulatethemessagesthatgetdisplayedinthetemplates.Thefollowingcodeshowsthemodifiedtemplatesthatusethenewlycreatedmodelvalues.Theline$scope.message="HelloWorld"intheMainCtrlcontrollerisusedtocreateapropertynamedmessagethatisaddedtothescope(whichholdsthemodelattributes).Wethenusethedoublecurlybracesmarkup({{}})insidethemain.htmltemplatetogainaccesstoanddisplaythevalueassignedto$scope.message:
<!--chapter1/main.html-->
<div>{{message}}</div>
UsingdoublecurlybracesisAngularJS’swayofdisplayingscopepropertiesintheview.Thedoublecurlybracessyntaxisactuallypartofthebuilt-inAngularJStemplatelanguage.
Likewise,weusethevalueassignedtothemessagepropertywiththeline$scope.message="ShowTheWorld"intheShowCtrlcontrollertopopulatethemessagedisplayedintheshow.htmltemplate.Weusethedoublecurlybracesmarkupinsidetheshow.htmltemplateasbeforetogainaccesstoanddisplaythemodelproperty:
<!--chapter1/show.html-->
<div>{{message}}</div>
IntegratingAngularJSwithOtherFrameworksAngularJScanbeintegratedintoexistingapplicationsthatuseotherframeworks.ThosemaybeotherJavaScriptclient-sideframeworks,orwebframeworkslikeSpringMVCorCakePHP.YoucouldtakeanapplicationwritteninJavaandaddsomenewclient-sidefunctionalityveryeasilyusingAngularJS,cuttingdevelopmenttimeconsiderably.
AddinganewAngularJSshoppingcarttoanexistingJavaapplicationwouldbeagoodexampletoconsider.TheexistingJavaapplicationcouldbewrittenwiththeSpringframeworkanduseSpringMVCasthewebframework.AddingashoppingcartbuiltwithJavausingSpringMVCcouldbeatime-consumingprocess.That,however,wouldnotbethecasewithAngularJS.
YoucouldquicklybuildashoppingcartwithAngularJSandbeupandrunninginafewhours,easilyintegratingthecartintotheexistingJavaapplication.Notonlywouldyoubeabletobuildthecartfaster,butyoucouldquicklyaddunittestingtoincreasecoverageandreducetheapplication’sdefects.AngularJSwasdesignedtobetestablefromtheverybeginning;thatisoneofthekeyfeaturesofAngularJSandamajorreasonforselectingitoverotherJavaScriptclient-sideframeworks.WewilltalkabouttestingAngularJSapplicationsinthenextsection.
TestingAngularJSApplicationsInrecentyearscontinuousintegration(CI)buildtoolssuchasTravisCI,Jenkins,andothershaveriseninpopularityandusage.CItoolshavetheabilitytoruntestscriptsduringabuildprocessandgiveimmediatefeedbackbywayoftestresults.CItoolshelptoautomatetheprocessoftestingsoftwareandcanoftenalertdevelopersofsoftwaredefectsassoonastheyoccur.
TherearetwotypesofAngularJSteststhatintegratewellwithCItools.Thefirsttypeoftestingisunittesting.Mostdevelopersarefamiliarwithunittesting;theycanoftenidentifysoftwaredefectsearlyinthedevelopmentprocessbytestingsmallunitsofcode.Thesecondtypeoftestingisend-to-end(E2E)testing.E2Etestinghelpstoidentifysoftwaredefectsbytestinghowsoftwarecomponentsconnectandinteract.
TherearemanytestingtoolsusedforunittestingAngularJSapplications.TwoofthemostpopularareKarmaandJSTestDriver.Karma,however,isquicklybecomingthetopchoiceforAngularJSdevelopmentteams.ThemostpopularE2Etesttoolforend-to-endtestingofAngularJSapplicationsisanewtoolcalledProtractor.BothtoolsintegratewellwithCIbuildtools.
LargeAngularJSdevelopmentteamswillfindtestingAngularJSapplicationswithcontinuousintegrationtoolstobeahugetime-saver.OftenafailedCItestisthefirstindicationofadefectforlargeteams.SmallteamswillalsoseemanyadvantagestoCI-basedtesting.AngularJSdevelopersshouldalwaysdevelopbothunittestsandend-to-endtestswheneverpossible.
Throughoutthisbook,wewillcoverbothunittestingandend-to-endtesting.WewillusebothKarmaandJsTestDriveforunittesting,andwewilluseProtractorforE2Etesting.
ConclusionWewillcovermodels,views,andcontrollersingreatdetailinlaterchapters,usingthosecomponentstobuildworkingapplicationsthatshowthepowerofAngularJS.WewillshowhowallthreecomponentsworktogethertosimplifythejobofbuildingJavaScriptclient-sideapplications.Wewillalsocoverbuildingbothunittestsandend-to-endtestsforAngularJSapplications.
Chapter2willfocusonhelpingyousetupadevelopmentenvironmentforHTML5.WewillalsodownloadthelatestversionsofAngularJS,jQuery,andTwitterBootstrapandaddthosetooursampleproject.
Chapter2.TheIDEandAngularJSProjects
ManyJavaScripteditorsareusedbyAngularJSdevelopers.Usinganintegrateddevelopmentenvironment(IDE)withagoodJavaScripteditorisahugetime-saverandspeedsupthedevelopmentprocessconsiderably.IDEswithgoodJavaScripttoolsusuallyhavegoodHTML5andCSS3toolsaswell,whichhelpstoincreaseadeveloper’sproductivitysubstantially.WewillharnessthepowerofanIDEinthisbook.
TheIDEWewillbeusingNetBeansasourintegrateddevelopmentenvironment.Youcan,however,useanyIDEoreditorthatyouprefer.MostofthischapterwillbegenericandwillworkfinewithanymodernIDE.Togetstarted,dothefollowing:
1. DownloadandinstallthelatestversionofNetBeansfromtheNetBeanswebsite(ordownloadanotherIDEofyourchoice).
2. DownloadthelatestversionsofthefollowingAngularJSfiles:a. angular.min.js(mainlibs)
b. angular-route.min.js(routinglibs)
c. angular-cookies.min.js(cookielibs)
d. angular-resource.min.js(RESTservicelibs)
3. DownloadthelatestversionofjQuery.
4. DownloadthelatestversionofTwitterBootstrap.
StartNetBeansandcreateanewHTML5project,asshowninFigure2-1.NametheprojectAngularJsHelloWorld_chapter2.
Figure2-1.CreatingyournewHTML5project
Nowdothefollowing:
1. CreatethedirectorystructureshowninFigure2-2underSiteRoot.
2. CopytheAngularJS,jQuery,andBootstrapfilesintothelibsfolder.
3. Right-clickthejsfolderandcreatethefollowing.jsfiles:a. app.js(wheretheapplicationisdefined)
b. controllers.js(wherecontrollersaredefined)
c. services.js(whereservicesaredefined)
d. main.htmlunderthepartialsfolder
e. show.htmlunderthepartialsfolder
f. index.htmlundertheSiteRootfolder
EditingtheHTMLCodeNowwemustedittheindex.htmlfiletocreatebootstrappingfortheapplicationandtousethelibrariesand.jsfilesjustadded.Edityournewlycreatedindex.htmlfiletomatchthecodethatfollows.Theseareallthechangesthatweneedtomaketothisfilefornow.Next,wewilledittheapp.jsandcontrollers.jsfiles:
<!--chapter2/index.html-->
<!DOCTYPEhtml>
<htmllang="en"ng-app="helloWorldApp">
<head>
<title>AngularJSHelloWorld</title>
<metaname="viewport"content="width=device-width,
initial-scale=1.0">
<metahttp-equiv="Content-Type"content="text/html;charset=UTF-8">
<scriptsrc="js/libs/jquery-1.10.2.min.js"></script>
<scriptsrc="js/libs/angular.min.js"></script>
<scriptsrc="js/libs/angular-route.min.js"></script>
<scriptsrc="js/libs/angular-resource.min.js"></script>
<scriptsrc="js/libs/angular-cookies.min.js"></script>
<scriptsrc="js/app.js"></script>
<scriptsrc="js/controllers.js"></script>
<scriptsrc="js/services.js"></script>
</head>
<body>
<divng-view></div>
</body>
</html>
EditingtheJavaScriptCodeUpdateyournewlycreatedapp.jsfilewiththecodeshownhere.Asyoucansee,itisthesamecodewecoveredinChapter1:
/*chapter2/app.js*/
'usestrict';
/*AppModule*/
varhelloWorldApp=angular.module('helloWorldApp',[
'ngRoute',
'helloWorldControllers'
]);
helloWorldApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'MainCtrl'
}).when('/show',{
templateUrl:'partials/show.html',
controller:'ShowCtrl'
});
$locationProvider.html5Mode(false).hashPrefix('!');
}]);
Likewise,updateyournewlycreatedcontrollers.jsfilewiththecodeshownnext.ThisisalsothesamecodecoveredinChapter1forthecontroller:
/*chapter2/controllers.js*/
'usestrict';
/*Controllers*/
varhelloWorldControllers=
angular.module('helloWorldControllers',[]);
helloWorldControllers.controller('MainCtrl',
['$scope','$location','$http',
functionMainCtrl($scope,$location,$http){
$scope.message="HelloWorld";
}]);
helloWorldControllers.controller('ShowCtrl',
['$scope','$location','$http',
functionShowCtrl($scope,$location,$http){
$scope.message="ShowTheWorld";
}]);
CreatingtheTemplatesNowallthatisleftistocreatethetemplates(partials).Dothefollowing:
<!--chapter2/main.html-->
<div>{{message}}</div>
1. Editthenewmain.htmlandaddthecodeshownhere:
2. Editshow.htmlandaddthecodeshownhere:
<!--chapter2/show.html-->
<div>{{message}}</div>
RunningtheApplicationsThatconcludesthecodechangesneededintheChapter2projectfornow.Right-clickonthenewHTML5projectandselect“Run.”AttheURLhttp://localhost:8383/AngularJsHelloWorld_chapter2/index.html#!/,youshouldseethewords“HelloWorld”inthetop-leftcornerofthebrowser.
NowchangetheURLtohttp://localhost:8383/AngularJsHelloWorld_chapter2/index.html#!/show,andyoushouldseethewords“ShowTheWorld”inthetop-leftcornerofthebrowser.Ifyougetthecorrectresults,yourprojectisconfiguredcorrectly.Ifyougetadifferentresult,gobackthroughthischapterandverifythatyoucompletedallthesteps.
Ifyoucontinuetohaveproblems,downloadtheChapter2sourcefromGitHubandtrytorunthatcode.
TestingAngularJSApplicationsintheIDEAsImentionedinthepreviouschapter,therearetwotypesofteststhatareusedfortestingAngularJSapplications.Thefirsttypeoftestistheunittest.Unittestingisusuallythefirstplacewhereissueswiththecodearefound,throughtestingsmallunitsofcode.Thesecondtypeoftestisend-to-end(E2E)testing.E2Etestinghelpstoidentifysoftwaredefectsbytestinghowcomponentsconnectandinteracttogetherasawhole.
NetBeanscaneasilyworkwithbothJsTestDriverandKarmaforunittestingAngularJSapplications.KarmaisquicklybecomingthemostpopularchoiceforAngularJSdevelopmentteams,sowewillfocusmoreonKarmainlaterchapters.ProtractoristhemostpopulartestframeworkforE2EtestingofAngularJSapplications.Currently,mostdevelopmentenvironmentsdon’thavebuilt-insupportforProtractor.Protractorisanewtestingframework,anditmaytakeawhilebeforemostIDEsandeditorssupportit.NetBeanscurrentlyhasnosupportforProtractor.
BothKarmaandProtractorrunonNode.js.Node.jsisanopensourcecross-platformframeworkbuiltontheGoogleV8JavaScriptengine.WewilluseNode.jslaterinthisbook,whenwefocusonbuildingMEANstackapplications.InstallingKarmaandProtractorisarelativelyeasyprocessthatusestheNode.jspackagemanager(npm)fortheinstallationprocess.
Node.js-basedprojectsuseaJSONfilenamedpackage.jsonastheprojectconfigurationfile.Thefollowingisastandardpackage.jsonfileusedinaNetBeansproject.Ifyoulookatthedependenciessectionofthefile,youwillseethatweactuallydefineKarmaasadependencyoftheapplication.ThatisbecauseKarmaisusuallyinstalledlocallyattheprojectlevelforeachindividualproject:
{"chapter":2,"name":"package.json"},
{
"name":"UlboraCmsMean",
"version":"2.0.0",
"description":"UlboraCms",
"keywords":["UlboraCMS","Node.js","Ken",
"Williamson","micbutton.com"],
"author":{
"name":"KenWilliamson",
"email":"[email protected]",
"url":"http://www.drivensolutions.com/"
},
"homepage":"http://www.ulboracms.org",
"repository":{
"type":"git",
"url":"https://github.com/Ulbora/ulboracms"
},
"engines":{
"node":">=0.6.0",
"npm":">=1.0.0"
},
"dependencies":{
"express":"~3.4.4",
"mongoose":"*",
"atob":"*",
"btoa":"*",
"node-rest-client":"*",
"consolidate":"*",
"ejs":"*",
"handlebars":"*",
"nodemailer":"*",
"karma":"*"
},
"bundleDependencies":[],
"private":true,
"main":"./server.js",
"bugs":{
"url":"null"
}
}
AfilesimilartothisonewillbeusedlaterinthebookwhenwebuildtheMEANstackblogapplication.NetBeans,usingaNode.jsplugin,cangeneratethepackage.jsonfileforyou.Thegeneratedfilewillneedtobemodifiedtoincludethespecificsofyourparticularproject.
TIPYoucanalsousenpminittogeneratethepackage.jsonfile.Aftertypingnpminitatthecommandprompt,youwillbepresentedwithafewquestions.Yourresponseswillthenbeusedtocreateadefaultpackage.jsonfile.
JsTestRunnerNetBeanshasbuilt-insupportforJsTestRunner.TheJsTestRunnerconfigurationfilecanbegeneratedandrequiresfewchangestogetunittestingrunningonyourlocalenvironment.
UnlikeKarma,JsTestRunnerisnotbasedonNode.js.ThefollowingisastandardJsTestRunnerconfigurationfilecreatedbyNetBeansforanAngularJSproject.NoticeinthefirstlinethatthetestserverURLandportarespecified:
/*chapter2/jsTestDriver.conf*/
server:http://localhost:42442
load:
-test/lib/jasmine/jasmine.js
-test/lib/jasmine-jstd-adapter/JasmineAdapter.js
-public_html/js/libs/angular.min.js
-public_html/js/libs/angular-mocks.js
-public_html/js/libs/angular-cookies.min.js
-public_html/js/libs/angular-resource.min.js
-public_html/js/libs/angular-route.min.js
-public_html/js/*.js
-test/unit/*.js
exclude:
Thelocationsofthetestlibraryfilesarespecifiedunderload.WealsospecifythelocationsofeachunittestscriptthatshouldberunbyJsTestDriver.Testfilenamesusuallyendwith“Spec.”ThefollowingcodeshowsatestspecificationfileusedtotestAngularJScontrollers.Wewillcovertestspecificationinlaterchapters,whenwerunourfirstunittests:
/*chapter2/controllerSpec.js*/
/*Jasminespecsforcontrollersgohere*/
describe('HelloWorld',function(){
beforeEach(module('helloWorldApp'));
describe('MainCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('MainCtrl',{$scope:scope});
}));
it('shouldcreateinitialedmessage',function(){
expect(scope.message).toEqual("HelloWorld");
});
});
describe('ShowCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('ShowCtrl',{$scope:scope});
}));
it('shouldcreateinitialedmessage',function(){
expect(scope.message).toEqual("ShowTheWorld");
});
});
describe('CustomerCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('CustomerCtrl',{$scope:scope});
}));
it('shouldcreateinitialedmessage',function(){
expect(scope.customerName).toEqual("Bob'sBurgers");
});
});
});
CurrentlyoneofthebigdisadvantagesoftestingJavaScriptapplicationsisthelackoftoolsthatgeneratetestscriptsbasedontheactualsourcefilesthatneedtobetested.ThosetoolshaveexistedintheJavaworldforyears,buttheyarestillrelativelynonexistentintherealmofJavaScript.So,afilelikethisoneneedstobecreatedbyhandtounittesteachAngularJScontroller.
KarmaTestRunnerAsImentionedearlier,KarmaisatestrunnerbasedonNode.js.TheKarmateamrecommendsinstallingKarmalocallyattheprojectlevel.So,wewilladdKarmainthepackage.jsonfileofeachofourprojects,thenusethefollowingcommandtopulldownandinstallKarmaonaper-projectbasis:
npminstall
Whenyourunthiscommand,npmreadsthepackage.jsonfileandinstallsthepackagesdefinedinthedependenciessectionofthefile.Afteryourunthecommand,Karmawillbelocatedunderthenode_modulesfolderwithinyourprojectfolder.AnyotherNode.jsdependenciesdefinedinthepackage.jsonfilewillalsobelocatedunderthenode_modulesfolder.
Karmarequiresaconfigurationfilenamedkarma.conf.jsthatspecifieshowitshouldrununittests.YoucanuseNetBeanstogeneratethekarma.conf.jsfile.ThefollowingcodeshowsaKarmaconfigurationfilegeneratedbyNetBeans.Youcanseetherearesectionsofthefiletospecifythelocationsoflibraryfiles,testscripts,andbrowserplugins:
/*chapter2/karma.conf.js*/
/*
*Tochangethislicenseheader,chooseLicenseHeadersin
*ProjectProperties.
*Tochangethistemplatefile,chooseTools->Templates
*andopenthetemplateintheeditor.
*/
module.exports=function(config){
config.set({
basePath:'../',
files:[
],
exclude:[
],
autoWatch:true,
frameworks:[
],
browsers:[
],
plugins:[
]
});
};
WewillcoverKarmainmoredetailwhenwerunourfirstunittestusingKarma,inChapter4.
ProtractorMostdevelopmentenvironmentsdonotyethavebuilt-insupportforProtractor.ProtractorisaNode.js-basedframework,justlikeKarma.TheinstallationprocessismuchliketheprocessforKarma.ProtractorisbuiltontopofWebDriverJS.TheProtractorteamrecommendsinstallingProtractorgloballyonyoursystem.
ToinstallProtractoronyourdevelopmentmachine,issuethefollowingcommand.Noticethe-gflaginthecommandline—thattellsnpmtoinstallProtractorgloballyforallprojectsandapplicationstouse:
npminstall-gprotractor
SinceProtractorisbuiltonWebDriverJS,wemustalsoconfigureWebDriverJSforourtestenvironment.RunthiscommandtoupdateWebDriverJSwithallthelatestbinaries:
webdriver-managerupdate
Oncethatcommandexecutessuccessfully,runthefollowingcommandtostarttheSeleniumServerthatWebDriverJSusestorunProtractortestscripts:
webdriver-managerstart
Protractorneedsaconfigurationfilethattellsithowtoruntestscripts.Herearethecontentsoftheconf.jsfileusedtoconfigureProtractor:
/*chapter2/conf.jsProtractorconfigurationfile*/
exports.config={
seleniumAddress:'http://localhost:4444/wd/hub',
specs:['blog-spec.js']
};
OnceProtractorisinstalledandconfiguredonyoursystem,allthatisleftistocreatethetestscripts(testspecifications)andrunthescripts.Here’sasamplescriptforaProtractortest:
/*chapter2/blog-spec.js*/
describe('MEANBlog',function(){
it('testtheMEANBlog',function(){
browser.get('http://localhost:8080');
element(by.model('blogList')).
sendKeys('thisisablogpost');
element(by.css('[value="add"]')).click();
varblogList=element.all(by.repeater('bloginblogs'));
expect(blogList.count()).toEqual(3);
expect(blogList.get(2).getText()).
www.allitebooks.com
toEqual('thisisablogpost');
});
});
TorunProtractor,issuethefollowingcommand.Onceyourunthecommand,thebrowserwindowshouldopenanddisplaythetestresults:
protractorconf.js
BothKarmaandProtractorcanbeintegratedwithcontinuousintegration(CI)buildsystemslikeTravisCIandJenkins,asImentionedinChapter1.ManyopensourceprojectsandenterprisedevelopmentteamsaremovingtowardCIbuildsystems.BuildingKarmaandProtractortestingintoyourAngularJSprojectisavitalpartofthesoftwaredevelopmentprocess.Timespentwritingtestscriptswillultimatelybeworththeeffortinthelongrun.
WewillcoverbothKarmaandProtractortestingingreatdetailinlaterchapters.AtthattimewewillinstallandconfigurebothKarmaandProtractor.SincebothrunonNode.js,youwillalsoneedtoinstallthatandtheNode.jspackagemanager(npm)onyoursystemtopowerthetestplatforms.
ConclusionInthischapter,wecoveredhowtosetupadevelopmentenvironmentforAngularJSandbuiltandranaprojectwithAngularJS.WealsocoveredhowtoinstallatestenvironmentwithbothJsTestDriverandKarmaforunittestingourAngularJSprojects.Finally,welookedathowtoinstallandconfigureProtractorfordoingend-to-endtestingofAngularJSprojects.Withtheknowledgegainedfromthischapter,wearereadytostartworkingwithmorecomplexprojects.
WearenowreadytomoveontoChapter3,wherewewillcoverMVCasitappliestoAngularJSinmoredetail.
Chapter3.MVCandAngularJS
AngularJSpresentsanewandpowerfulwaytodevelopwebapplicationsandwebsites—ithasthepowerandfunctionalityofconventionalwebframeworks,butwithmanyadvantages.AngularJSprovidesawaytobuildwebappsandsiteswithouttheoverheadnormallyassociatedwithwebframeworks.
Conventionalwebframeworksoftentolerateserver-sidepagescriptingusingPHP,ActiveServerPages(ASP),andJavaServerPages(JSP).Whileserver-sidepagescriptingworkssufficientlywellontheserverside,itdoesposemanymaintenanceissuesfordevelopers.Butthatisnotthebiggestissuewithconventionalwebframeworks.Conventionalwebframeworkstendtorunslowerandbesluggishonmobiledevices.Andmobileusershaveamuchlowertoleranceforsystemdelaysandslowpageloadsthandesktopusers.
WemustcompareconventionalwebframeworkstoAngularJStounderstandtheadvantagesthatAngularJSpresents.ThenextsectionwillgiveyouaclearunderstandingoftheadvantagesofAngularJSoverframeworksthatyoumayhaveusedinthepast.Withthatunderstanding,wewillbesettostartbuildingmoremaintainableapplicationsinabetterway.
TheOldWayWebMVCframeworkssuchasApacheStruts,SpringMVC,andtheZendFrameworkdominatedthewebdevelopmentframeworkspaceformorethan15years.Thosesameframeworksstilldominatethespaceeventoday.Therearesomecaseswherewebframeworksdopresentabetterapplicationdesignthanmoremodernclient-sideframeworks,butthosecaseshavediminishedconsiderablyoverthelastcoupleofyears.
WebMVCframeworksresideentirelyontheserver.Allfunctionssuchasdatabaseaccess,businesslogic,displaylogic,andUIactivitieshappenontheserver,usingservermemoryandresources.WebMVCframeworksoftenusevariouspagescriptingtechniquessuchasASP,JSP,andPHPtocontrolpresentationlogic,andinsomecasesbusinesslogicisalsoplacedinsidethepages.
Figure3-1showsadiagramofaconventionalwebMVCframework.Fromthediagram,youcanseethattheapplicationorwebsiterunsonthebackendserver,andonlythewebbrowserrunsontheuser’shardware.AlthoughthedesigninFigure3-1isoldtechnology,itisstillinheavyusetoday.
Figure3-1.ConventionalwebMVCframework
WebapplicationsandsitesbuiltwithRubyonRails,theZendFramework,SpringMVC,CakePHP,andotherwebframeworksarebasedonthisdesign.Althoughthedesignworkswellinmanysituations,itdoeshaveseveralflaws.
Onesuchdesignflawisrelatedtomobileapplicationsandmobilewebsites.WhilewebpagesassociatedwithwebframeworkscanbedesignedwithHTML5andCSS3andbemaderesponsiveandlookgoodonmobiledevices,theapplicationorwebsiteisdependentonthewebservertomakethedifferentpagesavailabletothemobiledevice.Inaddition,thewebpagesmustruninthemobiledevice’swebbrowser.
Theapplicationorsitedeveloperhasverylittlecontroloverthemobiledevice’sweb
browser.AusermustfindthesiteorapplicationandenteritsURLintothebrowser’saddressbarinordertoviewthewebpageortoruntheapplication.Mobileusers,however,oftenfindthatprocesstootime-consuming.
Whilemobilesitesandapplicationsdistributedasweb-baseddesignshavetheadvantageofsavingdevelopmenthoursandmoney,theydoposeaprobleminmanysituations.Often,mobiledevelopersneedtobuildcustomdeviceapplicationsandhavethoseapplicationsdistributedviathevariousonlinestores.Notonlydoesacustomapplicationofferahigherlevelofcustomerservice,butitalsoservesasamarketingtool.Asthenumberofmobiledevicesinuseincreases,thedemandforcustommobileapplicationswillalsoincrease.
Consider,forexample,adoctor’sofficethatneedstoallowpatientstomakeappointmentsfromtheirmobiledevices.Suchanapplicationwouldneedtobefastandhavealmostnodelaywhenpatientsarenavigatingfrompagetopage.Theapplicationwouldalsoneedtolookgoodonanydevice.Auserwithasmallsmartphoneshouldhavethesameuserexperienceasauserwitha10-inchtablet.
AnapplicationdeveloperorarchitectattemptingamobiledesignbasedonthesystemdesignshowninFigure3-1reallyonlyhastwochoicestoconsider.
ChoiceOneThefirstoptionistobuildacustommobileapplicationasa“wrapper”aroundtheconventionalsiteshowninFigure3-1.Figure3-2showsanAndroidapplicationdesignedasawrapperapplication.Asyoucansee,theAndroidapplicationconsistssolelyofanAndroidWebViewcomponentthatisconfiguredtopointtothewebapplicationURL.
Figure3-2.AnAndroidwrapperaroundatraditionalwebapplication
TheWebViewcomponentservesasabrowsercontrolinsidetheAndroidapplication.Thedevelopercancustom-configuretheWebViewcomponentfortheneedsoftheparticularmobileapplication.Allapplicationoperationsstill,however,runonthebackendserver(thewebserver),andthespeedandresponsivenessoftheAndroidapplicationarestillhighlydependentonthatserverandthequalityoftheuser’sInternetconnection.
ThefollowingcodeshowsasegmentofanAndroidmainActivity.AnewAndroidWebViewobjectisfirstinstantiated.JavaScriptisthenenabledforthenewinstance.Finally,theURLofthewebsiteisloadedintothenewinstancewiththeloadUrlmethod:
/*chapter3excerptfromanAndroidWebViewshownloadinga
conventionalwebsite*/
WebViewwebview=newWebView(this);
webview.getSettings().setJavaScriptEnabled(true);
finalActivityactivity=this;
webview.setWebViewClient(newWebViewClient(){
webview.loadUrl("http://www.google.com");
TheWebViewinstanceshownhereisjustacontrolforthedevice’sinternalwebbrowser.TheAndroiddevice’sbrowseriscompletelydependentonthewebsiteforfunctionality.Ifthewebsitethatislinkedtogoesdownorthenetworkconnectionislost,theuser’sbrowserwillhangandcompletelystopworking.Thatfunctionalityisveryfrustratingformobiledeviceusers.Itis,however,acommonconfigurationformobileapplications.
ChoiceTwoThesecondoptionwouldrequirethedevelopertowriteanativeorHTML5mobileapplicationthatcalledwebservicesonthebackendforbusinessfunctions.ThisapproachwouldrequireaddingRESTwebservicestotheexistingwebapplicationtomakeuseofexistingbusinesslogic.Optiontwois,ineffect,acompleterewriteoftheapplication.AddingRESTservicestotheexistingwebapplicationwouldnotbeatrivialmatter.Optiontwowould,however,offerthebestapplicationdesignandwouldprovidethebestuserexperience.
ThedesignshowninFigure3-1isn’tdirectlytransferabletomobiledevices.Fifteenyearsago,whenmobiledeviceswerenotinheavyuse,thatdesignwasacommonchoiceforapplicationdevelopersandarchitects,andposedfewproblems.Mobiledevicesalesreachedanall-timehighin2014,however,andmostanalystspredictthattrendwillonlyincreaseinthecomingyears.
Mobileisthefutureofeverything.Aswirelesssystemsimproveandevolve,mobiledeviceswillevolvetooandplayamajorroleinallourdailyactivities.Amobiledevicewillalertyouwhenyourtableisreadyatyourfavoriterestaurant.Thatsamedevicewillreplaceyourdebitcardorcreditcardwhenit’stimetopaythebillandtiptheserver.
So,developersmustplanforthefuturenow.It’stimetostopbuildingsoftwarebasedonanoldandoutdatedtechnology.That’swhereJavaScriptclient-sideframeworkscomeintoplay,andthat’swhereAngularJSshinesthebrightestofalltheJavaScriptframeworksavailable.AngularJSisasolidfoundationforbuildingscalableapplicationsthatrunwellondesktopsandabroadarrayofmobiledevices,withfewifanymodificationsneededforeachplatform.
ANewandBetterWayAngularJSisaJavaScriptMVCframeworkthatcutsdevelopmenttimeforbothwebapplicationsandmobileapplicationsthatrunonmultipledeviceplatforms.Figure3-3showsadiagramofanAngularJSapplicationthatusesbusinesslogicthat’sexposedthroughRESTwebservices.TheRESTservicescanrunanywhereandbewritteninanyprogramminglanguage.TwopopularframeworksusedtobuildRESTservicesaretheSpringframework,writteninJava,andExpressJSforNode.js.
Figure3-3.AngularJSapplicationdesign
IfyoulookcloselyatFigure3-3,youcanseethattheentireAngularJSapplicationrunsontheuser’shardware,intheuser’swebbrowser.Thatmaybeadesktopbrowserorthebrowserofamobiledevice.Withthisdesignweshiftthedisplaylogicfromtheservertotheuser’shardware,resultinginamuchbetteruserexperience.Theapplicationrunsfasterandismuchmoreresponsive—morelikeathick-clientornativeapplicationthanabrowser-basedapplication.
AngularJSapplicationsharnessthepoweroftheuser’shardware.Theapproachthat’stakenfreestheserverorserverstohandlenothingbutbusinesslogicanddataaccess.UsingRESTservicesthatsendandreceiveJSONhelpstogreatlysimplifyAngularJSapplications:JSONisadata-interchangeformatforRESTservicesthatiseasytoreadandunderstand.
Figure3-4showsthesameAngularJSapplicationdeployedaspartofanAndroidapplication.TheJavaScript,CSS3,andHTML5codeisallthesameregardlessofwheretheapplicationisdeployed.Iftheapplicationwasdesignedfromamobile-firstperspective,itshouldlookgreatandrunwellonanyplatform.
www.allitebooks.com
Figure3-4.TheAngularJSapplicationdeployedasanAndroidapp
NotonlydoesthedesigninFigure3-4produceabetteruserexperience,butitalsocutsdevelopmenttimesignificantly.AndaswiththedesigninFigure3-3,theapplicationrunsentirelyontheuser’shardware,shiftingtheloadfromtheservertotheuser’sdevice.
TestingConsiderationsWecoveredsomeofthebasicsoftestingAngularJSapplicationsbackinChapter2.TheabilitytoeffectivelyandeasilytestAngularJSapplicationsisoneofthestrongestmotivatorsforusingtheframework.NotonlyareAngularJSapplicationsfastertowrite,buttheyarealsomuchfasterandeasiertotestthanconventionalwebframework–basedapplications.Hereiswhy.
TestscriptsforAngularJS,knownastestspecifications,arealwayswritteninJavaScript.Therearenocomplextestframeworkstoinstalllikeyoufindwithtraditionalwebframeworks.Onemorething:JavaScripttestsrunfasterthantestswrittenforconventionalwebframeworks.Thatisveryimportantwhenacontinuousintegrationsystemisused.
Testexecutionspeedsmaynotseemlikeaseriousconcernatfirst.ButconsiderthecontinuousintegrationplatformslikeTravisCIandJenkinsthatwediscussedbackinChapter2.Ifyouhadasmallshopwithfiveorsixdevelopers,testscriptexecutionspeedswouldn’tusuallybeaconcern.Ifyouhadalargeenterpriseshop,however,withafewhundreddevelopersallrunningCIbuildsatthesametime,thenconcernswouldchangequickly.
Thetwomostpopulartestframeworksusedforclient-sideJavaScriptandAngularJS,KarmaandProtractor,runontheNode.jsframework.ApplicationsandtestscriptsthatrunonNode.jsrunextremelyfast.ThatisoneofthemajoradvantagesofusingNode.js.ContinuousintegrationsystemsalsouseNode.jsforJavaScriptbuildsandtoruntestscripts.Itiseasytosee,then,whyJavaScripttestingisfasterinaCIenvironment.
ResponsiveDesignConsiderationsAnotherconsiderationwhenyouarecomparingtraditionalwebframeworkstoAngularJSishowwellresponsivedesignisaccomplished.Astrongresponsivedesignlooksgoodbothonadesktopandonallmobiledevicesthatusethesoftware.Whileyoucanbuildresponsiveapplicationswithtraditionalwebframeworks,it’snotoftendone.Unfortunately,manywebapplicationdevelopersoftentargetdesktopsandmaybetabletsandignorethevarioussmallerdevicesthatusetheirwebsites.
Take,forexample,theCSS3codeshownnext.Thecodeistakenfromaserver-sideapplicationwrittenwithCakePHP,awebMVCframework:
/*chapter3server-sidecss3*/
/*notbuiltformobile*/
.page-container{
float:left;
margin:3%000;
padding:0000;
width:100%;
}
img{
max-width:50%;
}
.partner-form{
float:left;
width:50%;
margin:00025%;
padding:1%5%1%5%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
border:#230fbasolid1px;
}
.new-article-upload-wrapper{
float:left;
width:30%;
margin:00035%;
padding:1%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
border:#230fbasolid1px;
}
.login-title{
float:left;
width:100%;
margin:6%01%0;
text-align:center;
font-size:18pt;
font-weight:bold;
}
.config-form-wrapper{
float:left;
width:60%;
padding:0000;
margin:00020%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
border:#230fbasolid1px;
}
.comment-form-wrapper{
float:left;
width:60%;
padding:0000;
margin:00020%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
border:#230fbasolid1px;
}
.summary-cell{
height:300px;
}
.summary-cell-data{
height:250px;
font-size:12pt;
}
Anapplicationstyledwiththiscodewouldlookfineonadesktop,andmaybeatablet.Therewouldbemajorstylingissueswithasmallmobiledevice,however.AmobilewrapperapplicationliketheoneImentionedearlierthatwrappedawebsitethatusedthiscodewouldbeatagreatdisadvantage.Youcouldnevermaketheapplicationlookgoodonasmallphone.
ThecodethatfollowsistakenfromamobileapplicationbuiltwithAngularJS.Noticethemediaquerylineslike@mediascreenand(min-width:1200px)thatwrappartsoftheCSS3.MediaqueriesletdevelopersstyleAngularJSapplicationstospecificscreensizes:
/*chapter3mobilecss3*/
/*builtformobile*/
@mediascreenand(min-width:1200px){
.page-container{
margin:3%000;
padding:0000;
width:100%;
}
img{
max-width:50%;
}
.partner-form{
width:50%;
margin:00025%;
padding:1%5%1%5%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.new-article-upload-wrapper{
width:30%;
margin:00035%;
padding:1%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.login-title{
width:100%;
margin:6%01%0;
font-size:18pt;
}
.config-form-wrapper{
width:60%;
padding:0000;
margin:00020%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.comment-form-wrapper{
width:60%;
padding:0000;
margin:00020%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.summary-cell{
height:300px;
}
.summary-cell-data{
height:250px;
font-size:12pt;
}
}
@mediascreenand(max-width:1200px){
.page-container{
margin:5%000;
padding:0000;
width:100%;
}
img{
max-width:60%;
}
.nav-ds{
margin:0000;
}
.nav-dsli{
width:11%;
}
.nav-dslia{
margin:0000;
padding:4%04%0;
}
.partner-form{
width:50%;
margin:00025%;
padding:1%5%1%5%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.new-article-upload-wrapper{
margin:00030%;
padding:1%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.login-title{
width:100%;
margin:6%01%0;
font-size:18pt;
}
.config-form-wrapper{
width:60%;
padding:0000;
margin:00020%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.comment-form-wrapper{
width:60%;
padding:0000;
margin:00020%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.summary-cell{
height:300px;
}
.summary-cell-data{
height:250px;
font-size:12pt;
}
}
@mediascreenand(max-width:800px){
.page-container{
margin:7%000;
padding:0000;
width:100%;
}
img{
max-width:70%;
}
.nav-ds{
margin:0000;
}
.nav-dsli{
width:11%;
}
.nav-dslia{
margin:0000;
padding:4%04%0;
}
.partner-form{
width:60%;
margin:00020%;
padding:1%5%1%5%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.new-article-upload-wrapper{
width:50%;
margin:00025%;
padding:1%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.login-title{
width:100%;
margin:6%01%0;
font-size:16pt;
}
.config-form-wrapper{
width:80%;
padding:0000;
margin:00010%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.comment-form-wrapper{
width:80%;
padding:0000;
margin:00010%;
border-radius:7px;
}
.summary-cell{
height:300px;
}
.summary-cell-data{
height:250px;
font-size:10pt;
}
}
@mediascreenand(max-width:450px){
.page-container{
margin:12%000;
padding:0000;
width:100%;
}
img{
max-width:100%;
}
.nav-ds{
margin:0000;
}
.nav-dsli{
width:15%;
}
.nav-dslia{
margin:0000;
padding:4%04%0;
}
.partner-form{
width:100%;
margin:0000%;
padding:1%5%1%5%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.new-article-upload-wrapper{
width:100%;
margin:0000%;
padding:1%;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.login-title{
width:100%;
margin:6%01%0;
font-size:14pt;
}
.config-form-wrapper{
width:100%;
padding:0000;
margin:0000;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.comment-form-wrapper{
float:left;
width:100%;
padding:0000;
margin:0000;
border-radius:7px;
-moz-border-radius:7px;/*Firefox3.6andearlier*/
}
.summary-cell{
height:150px;
}
.summary-cell-data{
height:120px;
font-size:6pt;
}
}
IfthewebapplicationshownpreviouslyhadbeenwrittenwithAngularJS,itwouldhavebeenasimpletasktoconverttheAngularJSapplicationintoamobileapplication.ThedevelopmentteamcouldthenhavefixedtheCSS3issuesandbeendone.TheapplicationwrittenwithCakePHPhadtobecompletelyrewritten,however.
ConclusionInthischapterwecomparedAngularJSapplicationstoapplicationsbuiltwithconventionalserver-sidewebframeworks.Weidentifiedmanyoftheshortcomingsofconventionalserver-sideframeworks,especiallyastheyrelatetomobileapplications,andgainedanunderstandingoftheseriouslimitationstheyposeonhowdevelopersbuildmobileapplications.
WealsolookedatthemanyadvantagesofbuildingapplicationswithAngularJS,suchasshorterdevelopmenttimesandincreasedapplicationspeedandtestability.WesawhowAngularJSgreatlysimplifiestheprocessofbuildingresponsivemobileapplications,thenlookedatareal-worldsituationwhereasimpleissuelikepoorlywrittenCSSposedaseriousproblemforamobiledevelopmentteamworkingwithanapplicationbuiltusingaconventionalserver-sidewebframework.
Theinformationpresentedinthischapterisagreatfoundationforthematerialcoveredinthefollowingchapters.WewillnowtakeourunderstandingoftheadvantagesofAngularJStothenextlevel,exploringhowAngularJShelpstosimplifytheprocessofinteractingwithbackendsystemsusingRESTservices.
AlthoughthisisnotabookonRESTservices,wewillcoverthebasicsofRESTservicesinChapter6,lookingindetailathowAngularJSconnectstotheseservicesandhowtointerfacewithJSONpayloads.Chapter7willprovideyouwithinformationonpublicRESTserviceendpointswrittenespeciallyforthisbookthatyoucanusetocompletethechapterexercises.
TheRESTservicesthatyouwilluseinChapter7arebuiltwithExpressJS,runonNode.js,anduseJSONasthedata-interchangeformat.Theservicesusedinthatandotherchaptersaredeployedtothecloudandopentoanyoneusingthisbookasalearningtool.Beforewegetintoallofthat,however,we’regoingtotakealookatAngularJScontrollers.
Chapter4.AngularJSControllers
AngularJScontrollersareatthecenterofAngularJSapplicationsandareprobablythemostimportantcomponenttounderstand.ControllersarenotalwaysclearlydefinedinsomeJavaScriptclient-sideframeworks,andthattendstoconfusedeveloperswhohaveexperiencewithMVCframeworks.ThatisnotthecasewithAngularJS.AngularJSclearlydefinescontrollers,andcontrollersareatthecenterofAngularJSapplications.
AlmosteverythingthathappensinanAngularJSapplicationpassesthroughacontrolleratsomepoint.Dependencyinjectionisusedtoaddtheneededdependencies,asshowninthefollowingexamplefile,whichillustrateshowtocreateanewcontroller:
/*chapter4/controllers.js-anewcontroller*/
varaddonsControllers=
angular.module('addonsControllers',[]);
addonsControllers.controller('AddonsCtrl',
['$scope','checkCreds','$location','AddonsList','$http','getToken',
functionAddonsCtrl($scope,checkCreds,$location,AddonsList,
$http,getToken){
if(checkCreds()!==true){
$location.path('/loginForm');
}
$http.defaults.headers.common['Authorization']=
'Basic'+getToken();
AddonsList.getList({},
functionsuccess(response){
console.log("Success:"+
JSON.stringify(response));
$scope.addonsList=response;
},
functionerror(errorResponse){
console.log("Error:"+
JSON.stringify(errorResponse));
}
);
$scope.addonsActiveClass="active";
}]);
Inthiscode,wefirstcreateanewmodulenamedaddonsControllerbymakingacalltothemodulemethodofangular.Onthesecondline,wecreateanewcontrollernamedAddonsCtrlbycallingthecontrollermethodoftheaddonsControllersmodule.Doingthatattachesthenewcontrollertothatmodule.Allcontrollerscreatedinthecontrollers.jsfilewillbeaddedtotheaddonsControllersmodule.
Alsonoticethelineconsole.log("Success:"+JSON.stringify(response)).MostmodernbrowsershaveaccompanyingdevelopertoolsthatgivedeveloperseasyaccesstotheJavaScriptconsole.ThislineusestheJSON.stringifymethodtologtheJSONthat’sreturnedfromthewebservicetotheJavaScriptconsole.DeveloperscaneasilyusetheJavaScriptconsoletotroubleshootRESTserviceissuesbyviewingtheJSONloggedinthesuccesscallbackfunction,orintheerrorcallbackfunctionifaservicecallfails.
www.allitebooks.com
MostdevelopertoolsandsomeIDEs,likeNetBeans,alsoincludeJavaScriptdebuggersthatallowdeveloperstoplacebreakpointsinboththesuccessanderrorcallbackfunctions.Doingsoallowsthedevelopertotakeafine-grainedapproachtotroubleshootingRESTservices.Quiteoften,thedevelopercanresolveotherwisecomplexRESTserviceissuesveryquicklybyusingaJavaScriptdebugger.
Thefollowingcodeisanexcerptofthepreviousfile.Itshowshowweusedependencyinjectiontoadddependenciestothenewcontroller.Thiscodeshows$scope,checkCreds,$location,AddonsList,$http,andgetTokensasdependenciesforthenewcontroller.Wehavealreadycoveredthe$scopebriefly.Fornowit’snotimportantwhattheotherdependenciesactuallyrepresent;youonlyneedtounderstandtheyarerequiredbythenewcontroller:
/*chapter4/controllers.jsexcerpt*/
/*usingdependencyinjection*/
['$scope','checkCreds','$location','AddonsList','$http','getToken',
functionAddonsCtrl($scope,checkCreds,$location,AddonsList,
$http,getToken){
}
Thiscontrollerplaysamajorroleintheapplicationinwhichitwasdefined.Controllersreallyhavetwoprimaryresponsibilitiesinanapplication.Wewilltakealookatthoseresponsibilitiesinmoredetailinthenextsection.
InitializingtheModelwithControllersAngularJScontrollershavetwoprimarydutiesinanapplication.First,controllersshouldbeusedtoinitializethemodelscopeproperties.WhenacontrolleriscreatedandattachedtotheDOM,achildscopeiscreated.Thechildscopeholdsamodelusedspecificallyforthecontrollertowhichitisattached.Youcanaccessthechildscopebyusingthe$scopeobject.
CreateacopyoftheChapter2projectandnameitAngularJsHelloWorld_chapter4.Wewillusethisnewprojectfortherestofthischapter.YoucanalsodownloadtheprojectfromtheGitHubprojectsite.
Modelpropertiescanbeaddedtothescope,andonceaddedtheyareavailableinsidetheviewtemplates.Thecontrollercodeshownhereillustrateshowtoaddtwopropertiestothescope.Afteraddingthecustomernameandcustomernumbertothescope,bothareavailabletotheviewandcanbeaccessedwithdoublecurlybraces:
/*chapter4/controllers.jsexcerpt*/
helloWorldControllers.controller('CustomerCtrl',['$scope',
functionCustomerCtrl($scope){
$scope.customerName="Bob'sBurgers";
$scope.customerNumber="44522";
}]);
Nowaddthenewcontroller,CustomerCtrl,toyourproject’scontrollers.jsfile.Wewillmakeseveraladditionstothecontrollers.jsfileinthischapter.
Thefollowingviewtemplatecodeshowshowtoaccessthenewlyaddedmodelpropertiesinsidetheviewtemplate.Allpropertiesthatneedtobeaccessedfromtheviewshouldbeaddedtothe$scopeobject:
<!--chapter4/partials/customer.html-->
<div><b>CustomerName:</b>{{customerName}}</div>
<div><b>CustomerNumber:</b>{{customerNumber}}</div>
NowaddanewHTMLfileunderthepartialsfolderandnameitcustomer.html.Replacethegeneratedcodewiththecodejustshown.
AddingBehaviorwithControllersThesecondprimaryuseforcontrollersisaddingbehaviortothe$scopeobject.Weaddbehaviorbyaddingmethodstothescope,asshowninthefollowingcontrollercode.Here,weattachachangeCustomermethodto$scopesothatitcanbeinvokedfrominsidetheview.Bydoingthis,weareaddingbehaviorthatallowsustochangethecustomernameandcustomernumber:
/*chapter4/controllers.jsexcerpt*/
helloWorldControllers.controller('CustomerCtrl',['$scope',
functionCustomerCtrl($scope){
$scope.customerName="Bob'sBurgers";
$scope.customerNumber=44522;
//addmethodtoscope
$scope.changeCustomer=function(){
$scope.customerName=$scope.cName;
$scope.customerNumber=$scope.cNumber;
};
}]);
AddthechangeCustomermethodshownheretotheCustomerCtrlcontrollerdefinedinyourcontrollers.jsfile.
Thefollowingcodeshowsthecustomer.htmlfileandthechangesneededintheviewtomakeuseofthenewbehaviorthatwasjustadded.Weaddtwonewpropertiestothemodelbyusingng-model="cName"andng-model="cNumber".Weuseng-click="changeCustomer();"toinvokethenewchangeCustomermethodthatisattachedtothescope:
<!--chapter4/partials/customer.html-->
<div><b>CustomerName:</b>{{customerName}}</div>
<div><b>CustomerNumber:</b>{{customerNumber}}</div>
<form>
<div>
<inputtype="text"ng-model="cName"required/>
</div>
<div>
<inputtype="number"ng-model="cNumber"required/>
</div>
<div>
<buttonng-click="changeCustomer();">ChangeCustomer</button>
</div>
</form>
Modifythecustomer.htmlfiletoincludethenewformdefinedhere.
OncethechangeCustomermethodisinvoked,thenewpropertiesareattachedto$scopeandavailabletothecontroller.Asyoucansee,wesimplyassignthetwonewproperties
boundtothemodelbacktotheoriginaltwoproperties,customerNameandcustomerNumber,insidethechangeCustomermethod.Bothng-modelandng-clickareAngularJSdirectives.WewillcoverdirectivesindetailinChapter9.
ControllerBusinessLogicControllersareusedasjustdemonstratedtoaddbusinesslogictoanapplication.Businesslogicaddedinthecontroller,however,shouldbespecifictotheviewassociatedwiththatonecontrollerandusedtosupportsomedisplaylogicfunctionalityofthatoneview.Anybusinesslogicthatcanbepushedofftheclient-sideapplicationshouldbeimplementedasaRESTserviceandnotactuallyinsidetheAngularJSapplication.
Thereisonecaveattothisconcept,however:RESTservicesmusthavearesponsetimeoftwo(2)secondsorless.Long-runningserviceswillonlycausedelaysintheUIandmakeforabaduserexperience.Meetingthetwo-seconds-or-lessrulerequireshavingRESTservicesthatareproperlydesignedandrunningonabackendsystemthatscaleswelltoloaddemandchanges.Thereareotherconcernsrelatedtomobileapplications,butwewillcoverthoseinChapter7andChapter8.
Businesslogicthatcan’tbeplacedinRESTservicesbutneedstobeavailabletomultiplecontrollersshouldnotbeplacedinthecontrollerbutshouldinsteadbeplacedinAngularJSnon-RESTservices.InChapter8,wewillcoverbusinesslogicservicesinmoredetail.Businesslogicthatisplacedinthecontrollershouldbesimplelogicthatrelatesonlytothecontrollerinwhichitisdefined.PlacingtoomuchbusinesslogicinsideanAngularJSapplicationwouldbeabaddesigndecision,however.
PresentationLogicandFormattingDataPresentationlogicshouldnotbeplacedinsidethecontrollerbutinsteadshouldbeplacedintheview.AngularJShasmanyfeaturesforDOMmanipulationthathelpyouavoidplacingpresentationlogicinthecontrollers.Thecontrollerisalsonottheplacewhereyoushouldformatdata.AngularJShasfeaturesespeciallydesignedforformattingdata,andthat’swheredataformattingshouldtakeplace.Someofthosefeatureswillbecoveredindetailinthenextchapter.
FormSubmissionNowwewilllookathowformsubmissionsarehandledinAngularJSusingcontrollers.ThefollowingcodeforthenewCustomer.htmlfileshowstheviewforanewform.CreateanewHTMLfileunderthepartialsfolderandreplacethegeneratedcodewiththecodelistedhere:
<!--chapter4/partials/newCustomer.html-->
<formng-submit="submit()"ng-controller="AddCustomerCtrl">
<div>
<inputtype="text"ng-model="cName"required/>
</div>
<div>
<inputtype="text"ng-model="cCity"required/>
</div>
<div>
<buttontype="submit">AddCustomer</button>
</div>
</form>
Asyoucansee,weusestandardHTMLfortheformwithnothingreallyspecialexceptthedirectives.Thedirectiveng-submitbindsthemethodnamedsubmit,definedintheAddCustomerCtrlcontroller,totheformforformsubmission.Theng-modeldirectivebindsthetwoinputelementstoscopeproperties.
Twoormorecontrollerscanbeappliedtothesameelement,andwecanusecontrollerastoidentifyeachindividualcontroller.Thefollowingcodeshowshowcontrollerasisused.YoucanseethataddCustidentifiestheAddCustomerCtrlcontroller.WeuseaddCusttoaccessthepropertiesandmethodsofthecontroller,asshown:
<!--chapter4/partials/newCustomer.html(withcontrolleras)-->
<formng-submit="addCust.submit()"
ng-controller="AddCustomerCtrlasaddCust">
<div>
<inputtype="text"ng-model="addCust.cName"required/>
</div>
<div>
<inputtype="text"ng-model="addCust.cCity"required/>
</div>
<div>
<buttonid="f1"type="submit">AddCustomer</button>
</div>
</form>
ThefollowingcodeshowstheAddCustomerCtrlcontrollerandhowweuseittohandlethesubmittedformdata.HereweusethepathmethodontheAngularJSservice$locationtochangethepathaftertheformissubmitted.Thenewpathishttp://localhost:8383/AngularJsHelloWorld_chapter4/index.html#!/addedCustomer/name/city
Addthiscodetothecontrollers.jsfile:
/*chapter4/controllers.js*/
helloWorldControllers.controller('AddCustomerCtrl',
['$scope','$location',
functionAddCustomerCtrl($scope,$location){
$scope.submit=function(){
$location.path('/addedCustomer/'+$scope.cName+"/"+$scope.cCity);
};
}]);
That’sallthatisneededtohandletheformsubstitutionprocess.Wewillnowlookathowwegetaccesstothesubmittedvaluesinsideanothercontroller.
UsingSubmittedFormDataTheapp.jsfileshownnextincludesthenewroutedefinitions.Modifytheapp.jsfileintheChapter3projectandaddthenewroutes.Makesureyourfilelookslikethefileshownhere:
/*chapter4/app.js*/
/*AppModule*/
varhelloWorldApp=angular.module('helloWorldApp',[
'ngRoute',
'helloWorldControllers'
]);
helloWorldApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'MainCtrl'
}).when('/show',{
templateUrl:'partials/show.html',
controller:'ShowCtrl'
}).when('/customer',{
templateUrl:'partials/customer.html',
controller:'CustomerCtrl'
}).when('/addCustomer',{
templateUrl:'partials/newCustomer.html',
controller:'AddCustomerCtrl'
}).when('/addedCustomer/:customer/:city',{
templateUrl:'partials/addedCustomer.html',
controller:'AddedCustomerCtrl'
});
$locationProvider.html5Mode(false).hashPrefix('!');
}]);
Youcanseetherearetwopathparameters,customerandcity,fortheaddedCustomerroute.Thevaluesarepassedasargumentstoanewcontroller,AddedCustomerCtrl,showninthefollowingexcerpt.Weusethe$routeParamsserviceinthenewcontrollertogetaccesstothevaluespassedaspathparameterargumentsintheURL.Byusing$routeParams.customerwegetaccesstothecustomername,and$routeParams.citygetsusaccesstothecity:
/*chapter4/controllers.jsexcerpt*/
helloWorldControllers.controller('AddedCustomerCtrl',
['$scope','$routeParams',
functionAddedCustomerCtrl($scope,$routeParams){
$scope.customerName=$routeParams.customer;
$scope.customerCity=$routeParams.city;
}]);
Addthenewcontroller,AddedCustomerCtrl,toyourcontrollers.jsfilenow.
ThecodeforournewaddedCustomertemplateisshownnext.Onceagain,weuseAngularJSdoublecurlybracestogetaccesstoanddisplayboththecustomerNameandcustomerCitypropertiesintheview:
<!--chapter4/addedCustomer.html-->
<div><b>CustomerName:</b>{{customerName}}</div>
<div><b>CustomerCity:</b>{{customerCity}}</div>
Toaddthetemplatetotheproject,createanewHTMLfileinthepartialsfolderandnameitaddedCustomer.html.Replacethegeneratedcodewiththecodejustshown.NotehowsimpleitistosubmitformswithAngularJS.SimplicityisoneofthefactorsthatmakesAngularJSagreatchoiceforanyJavaScriptclient-sideapplicationproject.
JSTestDriverTherestofthischapterwillcoversettingupatestenvironmentandtestingAngularJScontrollers.NetBeanshasagreattestingenvironmentforbothJSTestDriverandKarma.WewillfocusfirstonsettingupJSTestDriverforunittesting.WewillthentakealookatKarmaforunittesting.Tobegin,dothefollowing:
1. DownloadtheJSTestDriverJAR.
2. IntheServicestab,right-click“JSTestDriver”andclick“Configure”(seeFigure4-1).
3. SelectthelocationoftheJSTestDriverJARjustdownloadedandchoosethebrowserofyourchoice(seeFigure4-2).
4. Right-clicktheprojectnode,thenclick“New”→“Other”→“UnitTests.”
5. Select“jsTestDriverConfigurationFile”andclick“Next.”
6. Makesurethefileisplacedintheconfigsubfolder,asshowninFigure4-3.
7. Makesurethecheckboxfor“DownloadandsetupJasmine”ischecked.
8. Click“Finish.”
9. Right-clicktheprojectnode,clickProperties,andselect“JavaScriptTesting.”
10. Select“jsTestDriver”fromthedrop-downbox.
Figure4-1.Right-click“JSTestDriver”intheServicestab
Figure4-2.Selectyourbrowser(s)
Figure4-3.Makesurethefileiscreatedintheconfigsubfolder
ThefollowingcodeshowstheJSTestDriverconfigurationfile.Insidethefile,wespecifytheserverURLthatisusedbyJSTestDriver.Wealsospecifytheneededlibraryfilesintheloadsectionofthefile,alongwiththelocationsofourJavaScriptfilesandtestscripts:
/*chapter4/jsTestdriver.conf*/
server:http://localhost:42442
load:
-test/lib/jasmine/jasmine.js
-test/lib/jasmine-jstd-adapter/JasmineAdapter.js
-public_html/js/libs/angular.min.js
-public_html/js/libs/angular-mocks.js
-public_html/js/libs/angular-cookies.min.js
-public_html/js/libs/angular-resource.min.js
-public_html/js/libs/angular-route.min.js
-public_html/js/*.js
-test/unit/*.js
exclude:
Noticewe’veaddedangular-mocks.jstothelistofrequiredAngularJSlibraryfiles.ThatfileisneededforunittestingAngularJSapplications.So,beforecontinuing,addtheangular-mocks.jsfiletothejs/libsfolder.
CreatingTestScriptsNext,createanewJavaScriptfileintheunitsubfolderofthenewlycreatedUnitTestfolder,asshowninFigure4-4.NamethenewfilecontrollerSpec.js.
Figure4-4.CreatethecontrollerSpec.jsfileintheunitsubfolder
ThecontentsofthecontrollerSpec.jsfileareshownnext.OurtestscriptfilenamewillendwithSpec.ThefilespecifiesastandardsetofunittestscommonlyusedtotestAngularJScontrollers.Noticethatwehaveatestforeachofourcontrollersdefinedinthecontrollers.jsfile:
/*chapter4/controllerSpec.js*/
/*Jasminespecsforcontrollersgohere*/
describe('HelloWorld',function(){
beforeEach(module('helloWorldApp'));
describe('MainCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('MainCtrl',{$scope:scope});
}));
it('shouldcreateinitialedmessage',function(){
expect(scope.message).toEqual("HelloWorld");
});
});
describe('ShowCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('ShowCtrl',{$scope:scope});
}));
it('shouldcreateinitialedmessage',function(){
expect(scope.message).toEqual("ShowTheWorld");
});
});
describe('CustomerCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('CustomerCtrl',{$scope:scope});
}));
it('shouldcreateinitialedmessage',function(){
expect(scope.customerName).toEqual("Bob'sBurgers");
});
});
});
ThistestscriptusesJasmineasthebehavior-drivendevelopmentframeworkfortestingourcode.WewilluseJasmineforallourtestscriptsinthisbook.
Hereisthecompletecontrollers.jsfile:
/*chapter4/controllers.js*/
'usestrict';
/*Controllers*/
varhelloWorldControllers=
angular.module('helloWorldControllers',[]);
helloWorldControllers.controller('MainCtrl',['$scope',
functionMainCtrl($scope){
$scope.message="HelloWorld";
}]);
helloWorldControllers.controller('ShowCtrl',['$scope',
functionShowCtrl($scope){
$scope.message="ShowTheWorld";
}]);
helloWorldControllers.controller('CustomerCtrl',['$scope',
functionCustomerCtrl($scope){
$scope.customerName="Bob'sBurgers";
$scope.customerNumber=44522;
$scope.changeCustomer=function(){
$scope.customerName=$scope.cName;
$scope.customerNumber=$scope.cNumber;
};
}]);
helloWorldControllers.controller('AddCustomerCtrl',
['$scope','$location',
functionAddCustomerCtrl($scope,$location){
$scope.submit=function(){
$location.path('/addedCustomer/'+$scope.cName+"/"+$scope.cCity);
};
}]);
helloWorldControllers.controller('AddedCustomerCtrl',
['$scope','$routeParams',
functionAddedCustomerCtrl($scope,$routeParams){
$scope.customerName=$routeParams.customer;
$scope.customerCity=$routeParams.city;
}]);
TIPTosavetime,youcandownloadtheChapter4codefromGitHub.ForacompleteguidetoJavaScripttestinginNetBeans,seethedocumentationatontheNetBeanswebsite.
TestingwithJSTestDriverNowtoactuallytestthecontrollerswe’vedefined,justright-clicktheprojectnodeandselect“Test”fromthemenu.Ifyourprojectisconfiguredcorrectly,youshouldseeasuccessmessageforallthreecontrollersthatweretested.Ifyouhaveanyissueswiththetestresults,gobackovertheconfigurationfilesandvalidatethatallyourfilesmatchthoselistedinthischapter.Ifyoucontinuetohaveproblems,downloadandrunthesourcecodefromtheprojectsite.
TestingwithKarmaKarmaisanewandfunwaytounittestAngularJSapplications.WewilluseKarmaheretotestthecontrollersthatwetestedearlier.
InstallingKarmaKarmarunsonNode.js,asmentionedinChapter2,sofirstyoumustinstallNode.jsifit’snotalreadyinstalled.Refertonodejs.orgforinstallationdetailsforyourparticularoperatingsystem.You’llalsoneedtoinstalltheNode.jspackagemanager(npm)onyoursystem.npmisacommand-linetoolusedtoaddtheneededNode.jsmodulestoaproject.
Now,intherootoftheChapter4project,createaJSONfilenamedpackage.jsonandaddthefollowingcontent.Thepackage.jsonfileisusedasaconfigurationfileforNode.js:
{
"name":"package.json",
"devDependencies":{
"karma":"*",
"karma-chrome-launcher":"*",
"karma-firefox-launcher":"*",
"karma-jasmine":"*",
"karma-junit-reporter":"*",
"karma-coverage":"*"
}
}
Openacommand-linewindowonyoursystem,andnavigatetotherootoftheChapter4project.Youshouldseethepackage.jsonfilewhenyoulistoutthefilesinthefolder.
TypethiscommandtoactuallyinstalltheNode.jsdependenciesdefinedinthepackage.jsonfile:
npminstall
NowinstalltheKarmacommand-lineinterface(karma-cli)bytypingthefollowingcommand:
npminstall-gkarma-cli
WARNINGMakesuretorecordthelocationwherekarma-cliwasinstalled.Youwillneedthelocationlaterinthischapter.
Thiscommandinstallsthecommand-linetoolgloballyonyoursystem.
AlltheNode.jsdependenciesspecifiedinthepackage.jsonfilewillbeinstalledunderthenode_modulesfolderinsidetheprojectrootfolder.Ifyoulistoutthefilesandfolders,youshouldseethenewfolder.Youwon’tbeabletoseethenewfolderinsideNetBeans,however.
KarmaConfigurationNext,createanewKarmaconfigurationfilenamedkarma.conf.jsinsidetheprojecttestfolder.Dothefollowing:
1. Right-clicktheprojectinNetBeans.
2. Select“New”→“Other”→“UnitTests.”
3. CreateanewKarmaconfigurationfileinsidethetestfolder.
Editthenewkarma.conf.jsfileandaddthefollowingcode:
/*chapter4/karma.conf.js*/
module.exports=function(config){
config.set({
basePath:'../',
files:[
"public_html/js/libs/angular.min.js",
"public_html/js/libs/angular-mocks.js",
"public_html/js/libs/angular-route.min.js",
"public_html/js/*.js",
"test/**/*Spec.js"
],
exclude:[
],
autoWatch:true,
frameworks:[
"jasmine"
],
browsers:[
"Chrome",
"Firefox"
],
plugins:[
"karma-junit-reporter",
"karma-chrome-launcher",
"karma-firefox-launcher",
"karma-jasmine"
]
});
};
NowdothefollowingtosetKarmaasthetestframework:
1. Right-clicktheproject.
2. Select“Properties.”
3. Select“JavaScriptTesting”fromthelistofcategories.
4. Select“Karma”asthetestingprovider.
5. Selectthelocationofthekarma-clitoolinstalledearlier.
6. Selectthelocationofthekarma.conf.jsfilejustcreated.
7. Select“OK.”
RunningKarmaUnitTestsNowtoactuallyruntheunittests(usingthetestspecificationwrittenearlier)underKarma,right-clicktheprojectandselect“Test”fromthemenu.Karmawillstart.YoushouldseebothChromeandFirefoxbrowserwindowsopen.TheNetBeanstestresultswindowshouldopenanddisplaythreepassedtestsforChromeandthreepassedtestsforFirefox.
Ifyougetanyerrormessagesorfailedtests,gobackoverthissectionandverifythatyoucompletedalltheconfigurationsandinstallations.YoucanalsodownloadtheChapter4codefromtheGitHubprojectsite.
End-to-EndTestingwithProtractorProtractorisanewtestframeworkforrunningend-to-end(E2E)tests.Protractorletsyourunteststhatexercisetheapplicationasauserwould.WithProtractorE2Etesting,youcantestvariouspages,navigatethrougheachpagefromwithinthetestscript,andfindanypotentialdefects.Protractoralsointegrateswithmostcontinuousintegrationbuildsystems.
InstallingProtractorLikeKarma,ProtractorisaNode.js-basedtestframework.TheProtractorteamrecommendsinstallingProtractorglobally.Todoso,openacommand-linewindowandtypethecommand:
npminstall-gprotractor
ProtractorreliesonWebDriverJS,sowewillalsousethiscommandtoupdateWebDriverJSwiththelatestlibraries:
webdriver-managerupdate
ConfiguringProtractorNext,wewillcreateaProtractorconfigurationfileforourproject.CreateanewJavaScriptfilenamedconf.jsunderthetestfolderoftheChapter4project.Enterthecodeshownhereinthenewfile:
/*chapter4/conf.js*/
exports.config={
seleniumAddress:'http://localhost:4444/wd/hub',
specs:['e2e/Hw-spec.js']
};
CreatingProtractorTestSpecificationsNowweneedtocreateaProtractortestspecification.Dothefollowing:
1. Createanewfolderunderthetestfolderoftheprojectandnameite2e.
2. CreateanewJavaScriptfileinsidethenewe2efolderandnameitHw-spec.js.
NowcopythecodeshownhereintothenewHw-spec.jsfile:
/*chapter4/Hw-spec.jsProtractortestspecification*/
describe("HelloWorldTest",function(){
it("shouldtestthemainpage",function(){
browser.get(
"http://localhost:8383/AngularJsHelloWorld_chapter4/");
expect(browser.getTitle()).toEqual("AngularJSHelloWorld");
varmsg=element(by.binding("message")).getText();
expect(msg).toEqual("HelloWorld");
browser.get(
"http://localhost:8383/AngularJsHelloWorld_chapter4/#!/show");
expect(browser.getTitle()).toEqual("AngularJSHelloWorld");
varmsg=element(by.binding("message")).getText();
expect(msg).toEqual("ShowTheWorld");
browser.get(
"http://localhost:8383/AngularJsHelloWorld_chapter4/#!/
addCustomer");
element(by.model("cName")).sendKeys("tester");
element(by.model("cCity")).sendKeys("Atlanta");
element(by.id("f1")).click();
browser.get(
"http://localhost:8383/
AngularJsHelloWorld_chapter4/#!/addedCustomer/tester/Atlanta");
varmsg=element(by.binding("customerName")).getText();
expect(msg).toEqual("CustomerName:tester");
varmsg=element(by.binding("customerCity")).getText();
expect(msg).toEqual("CustomerCity:Atlanta");
});
});
StartingtheSeleniumServerWebDriverJSrunsontheSeleniumServer.TostarttheSeleniumServerthatrunsProtractortests(usingthewebdriver-managertool),openanewcommandwindowandenterthefollowingcommand:
webdriver-managerstart
RunningProtractorNowthattheSeleniumServerisrunning,wecanrunourProtractortests.Openanewcommandwindow,navigatetotherootoftheChapter4project,andtypethiscommand:
protractortest/conf.js
Youshouldseeabrowserwindowopen.YoushouldthenseethetestscriptnavigatethroughthepagesoftheChapter4application.Ifyouwatchthebrowserwindowclosely,youwillseethescriptentervaluesintheformthataddsanewcustomer.WhentheProtractorscripthasfinished,thebrowserwindowwillclose.
YoushouldseeresultslikethefollowinginthecommandwindowwhentheProtractorscriptcompletes.Thenumberofsecondsthatittakesthescripttofinishwillvarydependingonyourparticularsystem:
Finishedin3.368seconds
1test,6assertions,0failures
NOTEFormoreinformationontestingwithProtractor,seetheprojectsiteonGitHub.Protractorhasacompletesetofdocumentationtohelpyougetstarted.
ConclusionUnittestingAngularJScontrollersallowsustovalidatethebasicfunctionalityofeachcontroller.Fornow,ourtestsareverysimple.TestingacontrollerthatretrievesdatafromaRESTservice,forexample,wouldbeamorecomplextask.
End-to-endtestingisabitmoreinvolved,andcanbedesignedtocompletelyexercisetheentireapplication.Fornow,ourE2Etestsarealsosimple.E2EtestshelptoidentifysoftwaredefectsearlyinthedevelopmentprocesswhenusedwithCIbuildsystems.
We’llbedoingmoretestinginthenextchapter,wherewefocusonAngularJSviews.
Chapter5.AngularJSViewsandBootstrap
WewillnowstartanewAngularJSblogprojectthatusespublicRESTservicescreatedespeciallyforthisbook.Wewillworkontheblogprojectfortherestofthisbook.YoucanalsodownloadtheprojectcodefromGitHub.Wewillstartoffbybuildingtheviewsandthecontrollersforthoseviews.
TwitterBootstrapisafreecollectionofHTMLandCSStemplates.WewillbuildtheAngularJSviewswiththehelpofTwitterBootstraptohelpcutdevelopmenttime.Oncewehavetheviewsandcontrollersinplaceandunderstandtheiroperation,wewillfocusonthemodelandRESTservices(inthenexttwochapters).
AngularJSTemplatesAngularJSviewsaredefinedbybuildingtemplates(partials).ViewsinAngularJSarecomposedofHTMLcodewithdirectivesadded,suchastheng-modeldirectiveshownpreviously.AngularJSbuildstheviewsdynamicallyatruntimebymergingthetemplateswiththepropertiespassedtothetemplatesinthe$scopeobject.TheendresultispureHTMLcodeboundtotheng-viewdirective,asexplainedbackinChapter1.Wewillcovertheng-viewdirectiveagaininthischapterasareview.
CreatingtheBlogProjectStartanewHTML5projectinNetBeansandcallitAngularJsBlog.SetupthefolderstructureasshowninFigure5-1.MovethedownloadedAngularJS,jQuery,andBootstraplibraryfilestothejs/libsfolder,asshown.
Figure5-1.Blogprojectfolderstructure
We’llbeginwiththecodefortheindex.htmlfile.Asyoucansee,weloadtheneededlibraryfileswiththe<script>taginthe<head>sectionofthepage.Thetag<divng-view></div>iswherealldynamiccontentisinserted.Astheuserclicksonlinksintheapplication,existingcontentattachedtothetagisremovedandnewdynamiccontentisthenattachedtothatsametag:
<!--chapter5/index.html-->
<!DOCTYPEhtml>
<htmllang="en"ng-app="blogApp">
<head>
<title>AngularJSBlog</title>
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<metahttp-equiv="Content-Type"content="text/html;charset=UTF-8">
<scriptsrc="js/libs/jquery-1.10.2.min.js"></script>
<scriptsrc="js/libs/angular.min.js"></script>
<scriptsrc="js/libs/angular-route.min.js"></script>
<scriptsrc="js/libs/angular-resource.min.js"></script>
<scriptsrc="js/libs/angular-cookies.min.js"></script>
<scriptsrc="js/app.js"></script>
<scriptsrc="js/controllers.js"></script>
</head>
<body>
<divng-view></div>
</body>
</html>
AddingaNewBlogControllerNextwewillsetupthecontrollersforournewblogapplication.ThefollowingcodedefinestheblogControllersmoduleandtheBlogCtrlcontrollerforthatmodule.WewilldefinemorecontrollersontheblogControllersmoduleasweworkontheblogapplication.Fornow,thecontrollers.jsfileisrelativelysmall:
/*chapter5/controllers.js*/
'usestrict';
/*Controllers*/
varblogControllers=
angular.module('blogControllers',[]);
blogControllers.controller('BlogCtrl',['$scope',
functionBlogCtrl($scope){
$scope.blogArticle=
"ThisisablogpostaboutAngularJS.
Wewillcoverhowtobuildablogandhowtoadd
commentstotheblogpost.";
}]);
Nextisthecodefortheapp.jsfilethatstartsthebootingprocessfortheblogapplication.Thisiswherewedefinetherouteforthemainpageoftheblog.Asyoucansee,wedefinengRouteandblogControllersasdependenciesoftheapplicationatstartuptime,usinginlinearrayannotations.ThetwodependenciesareinjectedintotheapplicationusingDIandareavailablethroughouttheapplicationwhenweneedthem.AnycontrollersattachedtotheblogControllersmoduleareaccessibletotheblogAppmodule(theAngularJSapplication):
/*chapter5/app.js*/
'usestrict';
/*AppModule*/
varblogApp=angular.module('blogApp',[
'ngRoute',
'blogControllers'
]);
blogApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'BlogCtrl'
});
$locationProvider.html5Mode(false).hashPrefix('!');
}]);
Theroutesaredefinedintheapplicationconfigurationblock.Fornow,wewillonlydefinethemainpageoftheblog.WedefineBlogCtrlasthecontrollerand'partials/main.html'asthetemplateusedforthemainroute.Wewilladdmoreroutesasweneedthem.
AddingaNewBlogTemplateNowwewilladdasimpletemplatefileandtestruntheapplicationbeforeaddingcodetothetemplate.Right-clicktheNetBeansprojectfolderandaddanewHTMLpagenamedmain.htmlinthepartialsfolder.ReplacethegeneratedHTMLcodewiththecodeshownhere:
<!--chapter5/main.html-->
{{blogArticle}}
Right-clicktheprojectfolderandselect“Run”fromthemenu.Ifyousetuptheprojectcorrectly,thebrowsershouldopenwiththefollowingtextdisplayed:“ThisisablogpostaboutAngularJS.Wewillcoverhowtobuildablogandhowtoaddcommentstotheblogpost.”Thistellsusourapplicationisproperlyconfigured.NowwewilluseTwitterBootstrapandHTMLtobuildamenuandmainpageforourblog.
TwitterBootstrapYoushouldhavealreadyaddedbootstrap.min.jstotheproject.IfyourunintoJavaScripterrorsrelatedtoTwitterBootstrap,youcaneasilyreplacethebootstrap.min.jsfilewiththenonminifiedbootstrap.jsfiledistributedbyTwitter.UsingthenonminifiedversionofthefileallowsthedevelopertoplacebreakpointsintheBootstrapJavaScriptfileanddebuganyrelatedissues.WewillonlycoverthebasicsofTwitterBootstraphere.FormoredocumentationandtutorialsonBootstrap,seetheprojectsite.
First,weneedtoaddthreemorefoldersandsomeadditionalTwitterBootstrapfilestotheproject.WewilladdalltheBootstrapfileshere,althoughmuchofBootstrapisnotactuallyusedinthisproject.Dothefollowing:
1. AddasubfoldernamedcssundertheSiteRootfolder.
2. AddasubfoldernamedfontsundertheSiteRootfolder.
3. Addasubfoldernamedlib-cssundertheSiteRootfolder.
4. Copythebootstrap-theme.min.cssandbootstrap.min.cssfilesintothelib-cssfolder.
5. Copythefollowingfilestothefontsfolder:a. glyphicons-halflings-regular.eot
b. glyphicons-halflings-regular.svg
c. glyphicons-halflings-regular.ttf
d. glyphicons-halflings-regular.woff
6. Addthetwolinesofcodeshownnexttotheindex.htmlfile.ThesetwolinesareallthatweneedtomakeuseofTwitterBootstrap:
<!--chapter5/index.htmlexcerpt-->
<linkrel="stylesheet"href="lib-css/bootstrap.min.css"media="screen"/>
<scriptsrc="js/libs/bootstrap.min.js"></script>
Hereisthecompletedindex.htmlfile:
<!--chapter5/index.htmlcompletefile-->
<!DOCTYPEhtml>
<htmllang="en"ng-app="blogApp">
<head>
<title>Blog</title>
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<metahttp-equiv="Content-Type"content="text/html;charset=UTF-8">
<linkrel="stylesheet"href="lib-css/bootstrap.min.css"media="screen"/>
<scriptsrc="js/libs/bootstrap.min.js"></script>
<scriptsrc="js/libs/jquery-1.10.2.min.js"></script>
<scriptsrc="js/libs/angular.min.js"></script>
<scriptsrc="js/libs/angular-route.min.js"></script>
<scriptsrc="js/libs/angular-resource.min.js"></script>
<scriptsrc="js/libs/angular-cookies.min.js"></script>
<scriptsrc="js/app.js"></script>
<scriptsrc="js/controllers.js"></script>
</head>
<body>
<divng-view></div>
</body>
</html>
Figure5-2showstheprojectfilestructure.Makesureyourprojectissetupasshown.TheaddedCSSfilesandfontswillgiveusaccesstomanytime-savingfeaturesofTwitterBootstrap.WewillnowaddaBootstrapmenutoourproject.
Figure5-2.Thecompletedfilestructurefortheblogproject
AddingaBootstrapMenuThefollowingarethecontentsofthemenu.htmlfile.MostofthecodeshownisclearlyexplainedontheBootstrapprojectsite.Thestylesaddedtothemenuherearedefinedinthebootstrap.min.cssfileaddedintheprevioussection.IfyouhavequestionsonBootstrapmenus,pleaserefertotheBootstrapprojectdocumentationforafullerexplanation.Yourmenu.htmlfileshouldlooklikethis:
<!--chapter5/menu.html-->
<navclass="navbarnavbar-inversenavbar-fixed-top"role="navigation">
<!--Brandandtogglegetgroupedforbettermobiledisplay-->
<divclass="container">
<divclass="navbar-header">
<buttontype="button"class="navbar-toggle"data-toggle="collapse"
data-target=".navbar-collapse">
<spanclass="sr-only">Togglenavigation</span>
<spanclass="icon-bar"></span>
<spanclass="icon-bar"></span>
<spanclass="icon-bar"></span>
</button>
<aclass="navbar-brand"style="{{brandColor}}"href="#!/">AngularBlog</a>
</div>
<!--Collectthenavlinks,forms,andothercontentfortoggling-->
<divclass="collapsenavbar-collapse">
<ulclass="navnavbar-nav">
<liclass="{{aboutActiveClass}}"><ahref="#!about">About</a></li>
<liclass="">
<ahref="https://github.com/KenWilliamson">DownloadProjectCode</a></li>
</ul>
</div><!--/.navbar-collapse-->
</div>
</nav>
Here’showtoaddthemenu.htmlfileinsidethemain.htmlfile:
<!--chapter5/main.html-->
<divng-includesrc="'partials/menu.html'"></div>
{{blogArticle}}
Thefirstlineshowstheneededadditiontomain.html.Asyousee,weusetheng-includedirectivetoincludethemenutemplateinsidethemaintemplate.Thisapproachallowsustokeepthemenucompletelyseparatefromtheothertemplates.Usingthisapproachmakesthecodebaseeasytomaintainandunderstand.WewillnowfocusonusingotherBootstrapstylestoenhanceourblog.
AddingMockBlogDataWewillmodifytheBlogCtrlcontrollerandsetalistofblogpostsasascopepropertynamedblogList.Themodifiedcontrollers.jscodeisshownhere.TheJSONlistrepresentsthedatathatwilleventuallyberetrievedfromaRESTservice.Fornow,however,wewilljusthardcodetheJSONintothecontrollerasmockdata.TherearemoreadvancedwaystoaddmockdatatoanAngularJSapplication,butthatisbeyondthescopeofthisbook.Let’stakealookatthecontrollersfile:
/*chapter5/controllers.js*/
'usestrict';
/*Controllers*/
varblogControllers=
angular.module('blogControllers',[]);
blogControllers.controller('BlogCtrl',['$scope',
functionBlogCtrl($scope){
$scope.blogList=[
{
"_id":1,
"date":1400623623107,
"introText":"ThisisablogpostaboutAngularJS.
Wewillcoverhowtobuild",
"blogText":"ThisisablogpostaboutAngularJS.
Wewillcoverhowtobuildablogandhowtoadd
commentstotheblogpost."
},
{
"_id":2,
"date":1400267723107,
"introText":"Inthisblogpostwewilllearnhowto
buildapplicationsbasedonREST",
"blogText":"Inthisblogpostwewilllearnhowto
buildapplicationsbasedonRESTwebservicesthat
containmostofthebusinesslogicneededforthe
application."
}
];
}]);
Asyoucansee,thereisnopresentationlogicinthiscode,andnodataformattingisdoneinthecontroller.Thedate,forinstance,issenttotheviewasalongvaluethatisastandardrepresentationofadateinmostprogramminglanguages.Tryingtoformatthedateinthecontrollerwouldbeanincorrectdesignthatshouldn’tbeused.AngularJShasmanyfeaturesthatmakeformattingandpresentingdataeasy;we’lllookatsomeofthesenext.
UsingCSS3toStylethePageNowwewilladdsomeCSS3tostyleourpages.Dothefollowing:
1. Right-clicktheprojectnodeandcreateanewCSSfilenamedstyle.css.
2. PlacethefollowingcodeintothenewCSSfile:
/*chapter5/styles.css*/
body{
font-family:arial;
font-size:12pt;
color:#2a6496;
}
.post-wrapper{
float:left;
width:100%;
margin:5%000;
padding:0000;
}
.blog-post-label{
float:left;
width:100%;
margin:10%000;
padding:0000;
text-align:center;
font-weight:bold;
font-size:16pt;
}
.blog-post-outer{
float:left;
width:60%;
margin:2%02%20%;
padding:1%;
background:#e0e0e0;
border-radius:6px;
-moz-border-radius:6px;/*Firefox3.6andearlier*/
border:darkgreensolid1px;
}
.blog-intro-text{
float:left;
width:100%;
margin:0000;
padding:0000;
text-align:center;
}
.blog-read-more{
float:left;
width:100%;
margin:2%000;
padding:0000;
text-align:center;
}
Nowmodifytheindex.htmlfile,addingthelineshownheretoloadthenewlycreatedCSSfile:
<!--chapter5/index.htmlexcerpt-->
<linkrel="stylesheet"href="css/styles.css"media="screen"/>
Thecompleteindex.htmlfileisshownhere.Makesureyourversionofthefilematchesthisone:
<!--chapter5/index.htmlcompletefile-->
<!DOCTYPEhtml>
<htmllang="en"ng-app="blogApp">
<head>
<title>Blog</title>
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<metahttp-equiv="Content-Type"content="text/html;charset=UTF-8">
<linkrel="stylesheet"href="lib-css/bootstrap.min.css"media="screen"/>
<scriptsrc="js/libs/jquery-1.10.2.min.js"></script>
<scriptsrc="js/libs/bootstrap.min.js"></script>
<scriptsrc="js/libs/angular.min.js"></script>
<scriptsrc="js/libs/angular-route.min.js"></script>
<scriptsrc="js/libs/angular-resource.min.js"></script>
<scriptsrc="js/libs/angular-cookies.min.js"></script>
<linkrel="stylesheet"href="css/styles.css"media="screen"/>
<scriptsrc="js/app.js"></script>
<scriptsrc="js/controllers.js"></script>
</head>
<body>
<divng-view></div>
</body>
</html>
AddingStylesandPresentationLogicYoumustmodifythemain.htmltemplatetomakeuseofthenewstylesandtoaddproperpresentationlogicfordisplayingblogpostsandformattingdata.Modifyyourmain.htmltomatchthecodeshownhere.Thesecondline,<divid="container"class="container">,setsupaBootstrapcontainerandisstandardpracticewithTwitterBootstrap:
<!--chapter5/main.html-->
<divng-includesrc="'partials/menu.html'"></div>
<divid="container"class="container">
<divclass="blog-post-label">BlogPosts</div>
<divclass="post-wrapper">
<divng-repeat="blogPostinblogList">
<divclass="blog-post-outer">
<divclass="blog-intro-text">
Posted:{{blogPost.date|date:'MM/dd/yyyy@h:mma'}}
</div>
<divclass="blog-intro-text">
{{blogPost.introText}}
</div>
<divclass="blog-read-more">
<ahref="#!blogPost/{{blogPost._id}}">ReadMore</a>
</div>
</div>
</div>
</div>
</div>
TheBootstrapcontainerhandlesmuchofthepagestylingforvariousscreensizestomakethepageresponsiveforanyscreensizeonanydevice.InsidethecontainerweusetheCSSthatwasaddedinthestyles.cssfile.Wewon’tfocusmuchonthecustomCSS,becauseitisnotspecifictoAngularJSandiscoveredinmanyotherbooksonCascadingStyleSheets.
Wewill,however,takealookattheAngularJSdirectivesthatallowustobuildthepresentationlogicintheviewandhandleformatting.Theline<divng-repeat="blogPostinblogList">isveryimportanttounderstandingAngularJSviews.Thedirectiveng-repeatworkslikeaforloop,iteratingoverthelistofblogpostsinthescopepropertyblogList.
EachiterationthroughthelistgivesaccesstoeachiteminthelistthroughthevariableblogPost.Weusetheline{{blogPost.introText}}todisplaytheintrotext(thevalueoftheintroTextpropertyoftheblogPostvariable).
AnotherlinethatisveryimportantistheHTMLtemplatebinding{{blogPost.date|date:'MM/dd/yyyy@h:mma'}},whichallowsustoformatthedateintheview,whereitshouldbeformatted.AsIstatedpreviously,therearemanyfeaturesofAngularJSforformattingdata,andthisisjustone.Asyoucansee,thetemplatecodeissimpleandeasy
tounderstand.
Wewillnowaddacontroller,route,andviewtodisplaytheindividualblogpostwhenauserclicksonthe“ViewMore”link.Ifyoulookclosely,youcanseethatthelinkpassesblogPost.idasapathparameterargumenttoanewroute,/blogPost.Wewillnowaddtheneededcodetoviewablogpost.
ViewingtheBlogPostToaddtheextrafunctionality,firstappendthisCSScodetotheendofthestyles.cssfile:
/*chapter5/styles.cssexcerpt*/
.blog-entry-wrapper{
float:left;
width:100%;
margin:1%000;
padding:0000;
}
.blog-entry-outer{
float:left;
width:60%;
margin:2%02%20%;
padding:1%;
background:#e0e0e0;
border-radius:6px;
-moz-border-radius:6px;/*Firefox3.6andearlier*/
border:darkgreensolid1px;
}
.blog-comment-wrapper{
float:left;
width:50%;anHTML5project
margin:2%02%25%;
padding:1%;
border-radius:6px;
-moz-border-radius:6px;/*Firefox3.6andearlier*/
border:darkgreensolid1px;
}
.blog-entry-comments{
float:left;
width:96%;
margin:2%02%2%;
padding:1%;
background:#f5e79e;
border-radius:6px;
-moz-border-radius:6px;/*Firefox3.6andearlier*/
border:darkgreensolid1px;
}
.blog-comment-label{
float:left;
width:100%;
margin:1%000;
padding:0000;
text-align:center;
font-weight:bold;
font-size:16pt;
}
Thenaddthiscodetothebottomofthecontrollers.jsfile:
/*chapter5/controllers.jsexcerpt*/
blogControllers.controller('BlogViewCtrl',
['$scope','$routeParams',
functionBlogViewCtrl($scope,$routeParams){
varblogId=$routeParams.id;
varblog1={
"_id":1,
"date":1400623623107,
"introText":"ThisisablogpostaboutAngularJS.
Wewillcoverhowtobuild",
"blogText":"ThisisablogpostaboutAngularJS.
Wewillcoverhowtobuildablogandhowtoadd
commentstotheblogpost.",
"comments":[
{
"commentText":"Verygoodpost.Iloveit."
},
{
"commentText":"Whencanwelearnservices."
}
]
};
varblog2={
"_id":2,
"date":1400267723107,
"introText":"Inthisblogpostwewilllearnhowto
buildapplicationsbasedonREST",
"blogText":"Inthisblogpostwewilllearnhowto
buildapplicationsbasedonRESTwebservicesthat
containmostofthebusinesslogicneededfortheapplication.",
"comments":[
{
"commentText":"RESTisgreat.Iwanttoknowmore."
},
{
"commentText":"WillweuseNode.jsforRESTservices?."
}
]
};
if(blogId==='1'){
$scope.blogEntry=blog1;
}elseif(blogId==='2'){
$scope.blogEntry=blog2;
}
}]);
Next,addanewtemplatefilenamedblogPost.htmlinthepartialsfolderandreplacethegeneratedcodewiththecodeshownhere:
<!--chapter5/blogPost.html-->
<divng-includesrc="'partials/menu.html'"></div>
<divid="container"class="container">
<divclass="blog-post-label">BlogEntry</div>
<divclass="blog-entry-wrapper">
<divclass="blog-intro-text">
Posted:{{blogEntry.date|date:'MM/dd/yyyy@h:mma'}}
</div>
<divclass="blog-entry-outer">
{{blogEntry.blogText}}
</div>
<divclass="blog-comment-wrapper">
<divclass="blog-comment-label">BlogComments</div>
<divclass="blog-entry-comments"ng-repeat="commentin
blogEntry.comments">
{{comment.commentText}}
</div>
</div>
</div>
</div>
Andaddthiscodetotherouteprovidersectionofapp.js:
/*chapter5/app.jsexcerpt*/
.when('/blogPost/:id',{
templateUrl:'partials/blogPost.html',
controller:'BlogViewCtrl'
Thecompleteroutedefinitionisshownhere:
/*chapter5/app.jsexcerpt-completeroute*/
blogApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'BlogCtrl'
}).when('/blogPost/:id',{
templateUrl:'partials/blogPost.html',
controller:'BlogViewCtrl'
});
$locationProvider.html5Mode(false).hashPrefix('!');
}]);
Asyoucansee,theeffortrequiredtoaddanewpagewasminimal.Ifyoulookattheroutedefinition,you’llseetheidpassedasapathparameterargument.Lookatthenewcontrollerandyoucanseehowwehandletheidparameter.SincewedonotyethaveRESTservicesinplace,wehardcodedtheJSONforthetwoblogpostsintothecontroller.
Onceweretrievethepassedidfrom$routeParams,weusethattodeterminewhichblogentrytosetasascopeproperty.Noticethatweneveractuallysetascopepropertyuntilweknowwhichblogentrygetssenttotheview.Noticealsothatblog1andblog2aredefinedaslocalvariables.Onlythevariablesneededinthepagearesetasscopeproperties.
WARNINGYoushouldneveraddpropertiestothescopethatarenotneededintheview.
RunningtheBlogApplicationNowlet’sruntheprojecttotestourwork.Right-clicktheprojectnodeandselect“Run”fromthemenu.Ifyoumadeallthechangescorrectly,youshouldseethescreenshowninFigure5-3.Ifyougetadifferentresult,gobackoverthechangesinthischapterandverifythatyoumadealltheneededmodifications.
Figure5-3.Successfulresultfromrunningtheproject
Ifyouhaveproblemsthatyoucan’tresolve,downloadtheprojectcodefromGitHubandrunthatcode.Oncetheprojectisrunning,clickthe“ReadMore”linkonthefirstblogpost.YoushouldthenseethescreenshowninFigure5-4.Clickthe“ReadMore”linkonthesecondblogpost,andyoushouldseeasimilarpage.
Figure5-4.Viewingthecommentsonthefirstblogpost
TestingwithKarmaWewilluseKarmanowtotestourview.FromtherootoftheChapter5project,createaJSONfilenamedpackage.jsonandaddthefollowingcontents.Thepackage.jsonfileisusedasaconfigurationfileforNode.js,asmentionedinChapter4:
{
"name":"package.json",
"devDependencies":{
"karma":"*",
"karma-chrome-launcher":"*",
"karma-firefox-launcher":"*",
"karma-jasmine":"*",
"karma-junit-reporter":"*",
"karma-coverage":"*"
}
}
Openacommand-linewindowonyoursystem,andnavigatetotherootoftheChapter5project.Youshouldseethepackage.jsonfilewhenyoulistoutthefilesinthefolder.NowtypethefollowingcommandtoinstalltheNode.jsdependenciesdefinedinthepackage.jsonfile.ThisisthesameprocessdescribedinChapter4:
npminstall
KarmaConfigurationNowwewillcreateanewKarmaconfigurationfilenamedkarma.conf.jsinsidetheproject’stestfolder,aswedidinChapter4.Dothefollowing:
1. Right-clicktheprojectinNetBeans.
2. Select“New”→“Other”→“UnitTests.”
3. CreateanewKarmaconfigurationfileinsidethetestfolder.
Editthenewkarma.conf.jsfileandaddthecodeshownhere:
/*chapter5/karma.conf.js*/
module.exports=function(config){
config.set({
basePath:'../',
files:[
"public_html/js/libs/angular.min.js",
"public_html/js/libs/angular-mocks.js",
"public_html/js/libs/angular-route.min.js",
"public_html/js/*.js",
"test/**/*Spec.js"
],
exclude:[
],
autoWatch:true,
frameworks:[
"jasmine"
],
browsers:[
"Chrome",
"Firefox"
],
plugins:[
"karma-junit-reporter",
"karma-chrome-launcher",
"karma-firefox-launcher",
"karma-jasmine"
]
});
};
NowdothefollowingtoconfigureKarmaasthetestframework:
1. Right-clicktheproject.
2. Select“Properties.”
3. Select“JavaScriptTesting”fromthelistofcategories.
4. Select“Karma”asthetestingprovider.
5. Selectthelocationofthekarma-clitoolinstalledinChapter4.
6. Selectthelocationofthekarma.conf.jsfilejustcreated,andselect“OK.”
KarmaTestSpecificationsNowweneedtoaddnewtestspecificationsfortheChapter5project.Dothefollowing:
1. Createanewfoldernamedunitunderthetestfolderoftheproject.
2. CreateanewJavaScriptfilenamedcontrollerSpec.jsundertheunitfolder.
3. Enterthecodeshownhereinthenewfile:
/*chapter5/controllerSpec.js*/
describe('AngularJSBlogApplication',function(){
beforeEach(module('blogApp'));
describe('BlogCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('BlogCtrl',{$scope:scope});
}));
it('shouldcreateshowblogentrycount',function(){
console.log("blogList:"+scope.blogList.length);
expect(scope.blogList.length).toEqual(2);
});
});
describe('BlogViewCtrl',function(){
varscope,ctrl,$httpBackend;
beforeEach(inject(function(_$httpBackend_,
$routeParams,$rootScope,$controller){
$httpBackend=_$httpBackend_;
$httpBackend.expectGET('blogPost').respond({_id:'1'});
$routeParams.id='1';
scope=$rootScope.$new();
ctrl=$controller('BlogViewCtrl',{$scope:scope});
}));
it('shouldshowblogentryid',function(){
expect(scope.blogEntry._id).toEqual(1);
});
});
});
KarmaTestingThenewtestspecificationwillunittestbothcontrollers.Right-clicktheprojectandselect“Test”fromthemenu.Karmawillstart.YoushouldseebothChromeandFirefoxbrowserwindowsopen.TheNetBeanstestresultswindowshouldopenanddisplaytwopassedtestsforChromeandtwopassedtestsforFirefox.
Ifyougetanyerrormessagesorfailedtests,gobackoverthissectionandverifythatyoucompletedalltheconfigurationsandinstallations.YoucanalsodownloadtheChapter5codefromtheGitHubprojectsite.
End-to-EndTestingNext,weneedtocreateaProtractorconfigurationfilefortheproject.CreateanewJavaScriptfilenamedconf.jsunderthetestfolderoftheChapter5project.Enterthecodeshownhereinthenewfile:
/*chapter5/conf.jsProtractorconfigurationfile*/
exports.config={
seleniumAddress:'http://localhost:4444/wd/hub',
specs:['e2e/blog-spec.js']
};
ProtractorTestSpecificationNowweneedtocreateaProtractortestspecification.Dothefollowing:
1. Createanewfolderunderthetestfolderoftheprojectandnameite2e.
2. CreateanewJavaScriptfileinsidethenewe2efolderandnameitblog-spec.js.
Thencopythecodeshownnextintothenewblog-spec.jsfile.
WARNINGMakesurethelinesbrowser.get("http://localhost:8383/AngularJsBlog/");matchtheURLthatyouuseonyoursystemtocalltheblogapplication.TheURLcanbedifferentfordifferentdevelopmentenvironmentsandcandependonhowyounamedyourproject.
/*chapter5/blog-spec.js*/
describe("BlogApplicationTest",function(){
it("shouldtestthemainblogpage",function(){
browser.get("http://localhost:8383/AngularJsBlog/");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthebloglist
varblogList=element.all(by.repeater('blogPostinblogList'));
//teststhesizeoftheblogList
expect(blogList.count()).toEqual(2);
browser.get(
"http://localhost:8383/AngularJsBlog/#!/blogPost/1");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthecommentlist
varcommentList=element.all(
by.repeater('commentinblogEntry.comments'));
//checksthesizeofthecommentList
expect(commentList.count()).toEqual(2);
});
});
ProtractorTestingStartanewcommandwindowandenterthefollowingcommandtostartthetestserver:
webdriver-managerstart
OpenanewcommandwindowandnavigatetotherootoftheChapter5project.Typethecommand:
protractortest/conf.js
Youshouldseeabrowserwindowopen.Youshouldthenseethetestscriptnavigatethroughthepagesoftheblogapplication.WhentheProtractorscripthasfinished,thebrowserwindowwillclose.
YoushouldseeresultslikethefollowinginthecommandwindowwhentheProtractorscriptcompletes.Thenumberofsecondsthatittakesthescripttofinishwillvarydependingonyourparticularsystem:
Finishedin1.377seconds
1test,4assertions,0failures
ConclusionInthischapterwebuiltourviewusingTwitterBootstrap.WealsomadeourapplicationresponsivetodifferentscreensizesusingCSS3.WeconfiguredbothKarmaandProtractorforourblogproject,andranbothunitandend-to-endtests.
WewillnowcoverRESTservicesandhowtheyareusedinAngularJS.Thenwewillmoveontothemodel.
Chapter6.AngularJSandRESTServices
Intheneweraofmobileeverywhere,thebusinesslogicforAngularJSapplicationsshouldalwaysbeplacedinRESTserviceswheneverpossible.AngularJSapplicationsshouldbekeptcleanandsimple.Why?AsAngularJSevolvesoverthenextfewyears,itisverypossiblethatmostAngularJSapplicationswillberewritten.
ThismeansthatanybusinesslogicplacedinsideanAngularJSapplicationwillneedtoberewrittenaswell—aseriousconsiderationforapplicationscontaininglargeamountsofbusinesslogic.RESTservices,ontheotherhand,maybearoundforyearstocome.Aswebservicestechnologiesevolve,manyRESTservicesmayundergoupgradesandmodifications,butacompleteservicerewriteisunlikelyinmostcases.Thebestplaceforbusinesslogicistheplacethatwillundergotheleastamountofchangeandbeavailabletoalltypesofapplications,nowandinthefuture.
RESTServicesREST(REpresentationalStateTransfer)servicesallowfora“separationofconcerns.”RESTservicesarenotconcernedwiththeuserinterfaceoruserstate,andclientsthatuseRESTservicesarenotconcernedwithdatastorageorbusinesslogic.ClientscanbedevelopedindependentlyoftheRESTservices,aswehaveshowninpreviouschapters,usingmockdata.RESTservicescanlikewisebedevelopedindependentlyoftheclient,withnoconcernforclientspecificsoreventhetypesofclientsusingtheservices.RESTservicesshouldperforminthesamewayforallclients.
RESTservicesshouldbestateless.ARESTserviceshouldneverholddatainasessionvariable.AllinformationneededforaRESTservicecallshouldbecontainedintherequestandheaderpassedfromtheclienttotheservice.Anystateshouldbeheldintheclientandnotintheservice.TherearemanywaystoholdstateinanAngularJSapplication,includinglocalstorage,cookies,orcachestorage.
ARESTwebserviceissaidtobeRESTfulwhenitadherestothefollowingconstrants:
It’sURL-based(e.g.,http://www.micbutton.com/rs/blogPost).
ItusesanInternetmediatypesuchasJSONfordatainterchange.
ItusesstandardHTTPmethods(GET,PUT,POST,DELETE).
HTTPmethodshaveaparticularpurposewhenusedwithRESTservices.ThefollowingisthestandardwaythatHTTPmethodsshouldbeusedwithRESTservices:
1. POSTshouldbeusedto:a. Createanewresources.
b. Retrievealistofresourceswhenalargeamountofrequestdataisrequiredtobepassedtotheservice.
2. PUTshouldbeusedtoupdatearesource.
3. GETshouldbeusedtoretrievearesourceoralistofresources.
4. DELETEshouldbeusedtodeletearesource.
Forexample,thefollowingwouldbetheproperuseofHTTPmethods:
1. POST:http://www.micbutton.com/rs/blogPosttocreateanewblogpost
2. PUT:http://www.micbutton.com/rs/blogPosttoupdateablogpost
3. GET:http://www.micbutton.com/rs/blogPost/50togettheblogpostwithidequalto50
4. DELETE:http://www.micbutton.com/rs/blogPost/50todeletetheblogpostwith
idequalto50
AngularJSandRESTServicesAngularJSRESTservicecallsareasynchronousAjaxcallsbasedonthe$qservice’spromiseanddeferredAPIs.Wewillnotcoverpromises,deferredobjects,orAjaxinthisbook.IfyoudonotunderstandhowAjaxisusedtomakeasynchronouscalls,nowwouldbeagoodtimetoresearchthesetopics.MakingasynchronousAjaxRESTservicecallsisnotspecifictoAngularJSoranyotherclient-sideJavaScriptframework.ManylibrariesprovideAjaxfunctionality,includingjQuery,Dojo,andothers.
WaystoCreateAngularJSServicesTherearethreewaystocreateandregisterservicesinAngularJS.Theyareasfollows:
Usingtheservicefunction
Usingtheproviderfunction
Usingthefactoryfunction
Here’showtocreateaservicewiththeservicefunction(wewillnotusethismethodtocreateservicesinthisbook):
/*chapter6/servicefunction*/
varblogServices=angular.module('blogServices',['ngResource']);
blogServices.service('BlogPost',[…]
Youcanalsocreateserviceswiththeproviderfunction,asshownhere:
/*chapter6/providerfunction*/
varblogServices=angular.module('blogServices',['ngResource']);
blogServices.provider('BlogPost',[…]
ThethirdwaytocreateservicesinAngularJSiswiththefactoryfunction.Thisisthemostcommonlyusedmethod,andthemethodwewillusetocreateAngularJSservicesthroughoutthisbook:
/*chapter6/factoryfunction*/
varblogServices=angular.module('blogServices',['ngResource']);
blogServices.factory('BlogPost',[…]
WewillnowlookathowtoconnecttoRESTservicesinAngularJS,althoughwewillnotactuallyimplementtheservicecodeinourblogapplicationuntilChapter7.WeneedtogetagoodtheoreticalunderstandingofAngularJSservicesbeforewestartcoding.Oncewehavethatunderstanding,wewillbesetforChapter7.
WaystoCommunicatewithRESTServicesTherearecurrentlytwowaystocommunicatewithRESTservicesusingAngularJS:
The$httpservice
Thisserviceprovideslow-levelinteractionwithRESTservicesusingthebrowser’sXMLHttpRequestobject.
The$resourceobject
Thisobjectprovidesahigh-levelapproachtointeractingwithRESTservices,simplifyingtheprocessconsiderably.
Wewillfocusmostlyonusingthe$resourceobjectforcommunicatingwithRESTservicesandleavethe$httpservicediscussiontootherbooks(althoughwewillusethe$httpserviceinlaterchaptersforhandlingBasicAuthenticationheaders).Allourprojectcodeusesthe$resourceobject.
ThefollowingcodeshowshowtodefineanAngularJSservicethatcanbeusedtointeractwiththeBlogPostRESTservice.NoticethatwepasstheRESTserviceURLtothe$resourceobject.ThemethodsdefinedmatchtheRESTservicesthataredefinedonthatparticularURL.OncetheBlogPostserviceisdefined,itcanbeusedlikeastandardJavaScriptobjecttoaccessthedifferentRESTservicesdefinedonthisURL:
/*chapter6/services.js*/
'usestrict';
/*Services*/
varblogServices=
angular.module('blogServices',['ngResource']);
blogServices.factory('BlogPost',['$resource',
function($resource){
return$resource("http://www.micbutton.com/rs/blogPost",{},{
get:{method:'GET',cache:false,isArray:false},
save:{method:'POST',cache:false,isArray:false},
update:{method:'PUT',cache:false,isArray:false},
delete:{method:'DELETE',cache:false,isArray:false}
});
}]);
Usingthe$resourceobjectisbyfartheeasiestwaytocallRESTservices.Asyoucanseefromthisexample,theAngularJSservicecodeisstraightforwardandreallyfairlyuncomplicated.Evenwhenmanyservicesaredefined,theservices.jsfileisrelativelysimple.
TheAngularJS$httpservicementionedearlierisanotherwaytocallRESTservices.However,usingthe$httpservicewouldrequiremanymorelinesofcoderelatedtoRESTservicecallsthanweneedusingthe$resourceobject.Wedousethe$httpserviceinseveralplacesintheblogapplication,though,suchastosendaBasicAuthentication
headertoRESTservices.Wewillcoverthatinlaterchapters.
UpdatingtheProjectforRESTBeforewecanuseourservice,thenewservices.jsfilemustbeloadedatruntimeandthenewservicesmodule,blogServices,mustbespecifiedasadependencyoftheapplicationatstartuptime.Hereisthelinethatshouldbeaddedtotheindex.htmlfiletoloadtheservices.jsfile:
/*chapter6/index.htmlexcerpt*/
<scriptsrc="js/services.js"></script>
Andhereisthecompleteindex.htmlfile,withthisaddition:
<!--chapter6/index.htmlcompletefile-->
<!DOCTYPEhtml>
<htmllang="en"ng-app="blogApp">
<head>
<title>AngularJSBlog</title>
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<metahttp-equiv="Content-Type"content="text/html;charset=UTF-8">
<linkrel="stylesheet"href="lib-css/bootstrap.min.css"media="screen"/>
<linkrel="stylesheet"href="css/styles.css"media="screen"/>
<scriptsrc="js/libs/jquery-1.10.2.min.js"></script>
<scriptsrc="js/libs/bootstrap.min.js"></script>
<scriptsrc="js/libs/angular.min.js"></script>
<scriptsrc="js/libs/angular-route.min.js"></script>
<scriptsrc="js/libs/angular-resource.min.js"></script>
<scriptsrc="js/libs/angular-cookies.min.js"></script>
<scriptsrc="js/app.js"></script>
<scriptsrc="js/controllers.js"></script>
<scriptsrc="js/services.js"></script>
</head>
<body>
<divng-view></div>
</body>
</html>
ThefollowingcodeshowshowweuseinlineannotationstoaddthenewBlogServicesmoduleasadependencyoftheapplicationatstartuptime.Oncethenewmoduleisaddedhere,theservicesdefinedonthemodulecanbeusedbyanycontrollerintheapplication:
/*chapter6/app.js*/
'usestrict';
/*AppModule*/
varblogApp=angular.module('blogApp',[
'ngRoute',
'blogControllers',
'blogServices'
]);
blogApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'BlogCtrl'
}).when('/blogPost/:id',{
templateUrl:'partials/blogPost.html',
controller:'BlogViewCtrl'
});
$locationProvider.html5Mode(false).hashPrefix('!');
}]);
RESTServicesandControllersNowlet’slookathowtousetheBlogPostserviceinsidetheBlogViewCtrlcontroller.Firstwemustdefinetheserviceasarequirementofthecontroller,asshownhere.Wethenmakeacalltothegetmethodandpasstheidasanargument.Wealsodefinetwocallbackfunctions,successanderror(ifyoudonotunderstandJavaScriptcallbackfunctions,nowwouldbeagoodtimetostopandresearchhowtheywork):
/*chapter6/controllers.jsexcerpt*/
blogControllers.controller('BlogViewCtrl',
['$scope','$routeParams','BlogPost',
functionBlogViewCtrl($scope,$routeParams,BlogPost){
varblogId=$routeParams.id;
BlogPost.get({id:blogId},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.blogEntry=response;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
}
);
}]);
WhenacallismadetotheBlogViewCtrlcontroller,theidisretrievedfrom$routeParams.AcallisthenmadetothegetmethodoftheBlogPostservice,passingtheidasanargument.Atthatpoint,thecalltothecontrollercompletes.
Theoreticallywedon’tknowwhentheRESTservicecallwillreturnresults,butwhenitdoes,eitherthesuccesscallbackfunctionortheerrorcallbackfunctionwillbecalled.IftheRESTservicecallfails,thecodeinsidetheerrorcallbackfunctionshouldhandletheerrorcondition.IftheRESTservicecallissuccessful,thecodeinsidethesuccesscallbackfunctionhandlesthesuccessfunctionality.
TheJSONResponseNowlet’stakealookattheJSONresponseobjectreturneduponsuccess.IftheRESTservicecallissuccessful,wesettheJSONreturnedasthevalueofascopepropertynamedblogEntry.Thepropertyisatthatpointboundtotheview,andAngularJSupdatestheviewwiththenewvaluesthatwereretrievedfromtheRESTservicecall.IftheRESTservicecallfails,thescreenisnotupdated,butwelogtheerrortotheconsoletohelpdiagnosethefailure.TheJSONresponseobjectreturnedfromasuccessfulcalllookslikethis:
{"chapter:6,"JSON":"response"}
{
"_id":1,
"date":1400623623107,
"introText":"ThisisablogpostaboutAngularJS.
Wewillcoverhowtobuild",
"blogText":"ThisisablogpostaboutAngularJS.
Wewillcoverhowtobuildablogandhowtoadd
commentstotheblogpost.",
"comments":[
{
"commentText":"Verygoodpost.Iloveit."
},
{
"commentText":"Whencanwelearnservices."
}
]
}
ListServicesIfwewantedalistofblogposts,wecoulddefinethefollowingRESTservice:GET:http://www.micbutton.com/rs/blogList.Let’stakealookathowwewoulddefinethatserviceintheservices.jsfile.NoticethatwespecifyisArray:true.Thisdefinestheserviceasreturningalistandnotanindividualresource:
/*chapter6/services.jsexcerpt*/
blogServices.factory('BlogList',['$resource',
function($resource){
return
$resource
("http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blogList",
{},{
get:{method:'GET',cache:false,isArray:true}
});
}]);
FollowingisthecontrollercodeusedtoaccesstheBlogListservice.Weinjecttheserviceintothecontrolleraswedidearlier,andlikebefore,wepasssuccessanderrorcallbackfunctionstotheservicecall.TheresponsefromasuccessfulservicecallisassignedtotheblogListpropertyofthescopeandpassedtotheview:
/*chapter6/controllers.jsexcerpt*/
blogControllers.controller('BlogCtrl',['$scope','BlogList',
functionBlogCtrl($scope,BlogList){
BlogList.get({},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.blogList=response;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
}
);
}]);
WeaccesstheJSONinsidetheviewbyusingtheblogListscopeproperty,asshownhere.ThisisthesametechniqueweusedinChapter5.Weusetheng-repeatdirectivetoiterateoverthelistasbefore:
<!--chapter6/main.htmlexcerpt-->
<divng-repeat="blogPostinblogList">
<divclass="blog-post-outer">
<divclass="blog-intro-text">
Posted:{{blogPost.date|date:'MM/dd/yyyy@h:mma'}}</div>
<divclass="blog-intro-text">{{blogPost.introText}}</div>
<divclass="blog-read-more">
<ahref="#!blogPost/{{blogPost._id}}">ReadMore</a>
</div>
TestingServiceswithKarmaThebestwaytotestAngularJSservicesiswithKarma.WeusedKarmaasoneofourtestframeworksinpreviouschapters.Unittestingaserviceletsusvalidatethattheunitofcodethatisusedtobuildtheserviceisworkingcorrectly.UnittestinganAngularJSservicethatconnectstoaRESTserviceisapotentialcauseoferrors,however.
RESTservicecallsareasynchronous,sotherecanbeadelaybeforetheservicecallresultsareavailabletothepartoftheapplicationthatinitiatedtheRESTcall.ConsideringthataRESTserviceisnotactuallypartoftheunitofcodethatwewouldbetestingwithaunittest,weshouldn’tbetooconcernedaboutRESTcallswhenunittesting.
Karma,asImentionedbefore,shouldbetheunittestframeworkforourblogapplication.ThefollowingcodeshowshowwemodifyanormalKarmaconfigurationfiletoallowustotestcodewheretheAngularJS$resourceobjectisused.Noticetheline"public_html/js/libs/angular-resource.min.js".Withthatline,wetellKarmatousetheAngularJSangular-resource.min.jsfile.Thatfileisneededonlywhenwe’reworkingwithcodethatcallsRESTservices:
/*chapter6/karma.conf.js*/
module.exports=function(config){
config.set({
basePath:'../',
files:[
"public_html/js/libs/angular.min.js",
"public_html/js/libs/angular-mocks.js",
"public_html/js/libs/angular-route.min.js",
"public_html/js/libs/angular-resource.min.js",
"public_html/js/*.js",
"test/**/*Spec.js"
],
exclude:[
],
autoWatch:true,
frameworks:[
"jasmine"
],
browsers:[
"Chrome",
"Firefox"
],
plugins:[
"karma-junit-reporter",
"karma-chrome-launcher",
"karma-firefox-launcher",
"karma-jasmine"
]
});
};
KarmaServiceSpecificationsInordertotestAngularJSservices,weneedtoaddatestspecificationspecificallyfortheblogapplicationservices.ThefollowingcodeshowsaservicesSpec.jsfile.Thetestspecificationhasunittestingfortwoservices.ThefirstunittestisfortheBlogListservice,andthesecondtestisfortheBlogPostservice:
/*chapter6/servicesSpec.js*/
describe('AngularJSBlogServiceTesting',function(){
describe('testBlogList',function(){
var$rootScope;
varblogList;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
blogList=$injector.get('BlogList');
}));
it('shouldtestBlogListservice',function(){
expect(blogList).toBeDefined();
});
});
describe('testBlogPost',function(){
var$rootScope;
varblogPost;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
blogPost=$injector.get('BlogPost');
}));
it('shouldtestBlogPostservice',function(){
expect(blogPost).toBeDefined();
});
});
});
Noticeinthiscodethatweuse$injectortoinjectthetwoservicesdirectlyintothetestscripts.AsImentionedearlier,wearenottestingtheRESTservicesthemselves;weareonlytestingtheAngularJSservicesthatconnecttoRESTservices.ThetestsshouldsucceedeveniftheRESTservicesaredownforsomereason.
End-to-EndTestingEnd-to-endtestingdonewithProtractorisamuchbetterwaytotestthefunctionalityofRESTservicesandtheapplicationsassociatedwiththem.Mostmodernsoftwaredevelopmentteamsusesometypeofcontinuousintegration(CI)buildsystem.MostCIsystemscanbeconfiguredtorunend-to-endtestsusingProtractor.
ProtractorE2Etestingcanevenbeconfiguredtoruntestsagainstproductionenvironments.Moreoften,however,E2EtestingiswrittentorunagainstservicesrunningonQAservers.E2Etestingisagoodwaytotestanapplicationthesamewayauserwouldusetheapplication.
ProtractorConfigurationThefollowingisaconfigurationfileforProtractor.Aspecificationfilenamedblog-spec.jsisreferencedfromtheconfigurationfile:
/*chapter6/conf.jsProtractorconfigurationfile*/
exports.config={
seleniumAddress:'http://localhost:4444/wd/hub',
specs:['e2e/blog-spec.js']
};
ProtractorTestSpecificationLet’stakealookatthecontentsoftheblog-spec.jsfile.Youcanseethatthebrowser.get(URL)callcanbemadeagainstanyaccessibleURL.TheURLcouldpointtoalocaldevelopmentbox,aQAserver,oraproductionserver.RESTservicescanbethoroughlytestedwithaProtractortestscript:
/*chapter6/blog-spec.jsProtractortestspecification*/
describe("BlogApplicationTest",function(){
it("shouldtestthemainblogpage",function(){
browser.get(
"http://localhost:8383/AngularJsBlogChapter6/");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthebloglist
varblogList=
element.all(by.repeater('blogPostinblogList'));
//teststhesizeoftheblogList
expect(blogList.count()).toEqual(1);
browser.get(
"http://localhost:8383/AngularJsBlogChapter6
/#!/blogPost/5394e59c4f50850000e6b7ea");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthecommentlist
varcommentList=
element.all(by.repeater('commentinblogEntry.comments'));
//checksthesizeofthecommentList
expect(commentList.count()).toEqual(2);
});
})
ConclusionThisconcludesourdiscussionofRESTservicebasics.Throughouttherestofthisbookwe’llbeworkingwithliveRESTservices.Asweproceed,youwillgainabetterunderstandingofRESTserviceconcepts.WewillnowstartworkingwithactualRESTservicescreatedespeciallyforthisbook.
Chapter7.AngularJSModels
AngularJSmodelsareheldinthe$scopeobject.InAngularJS,$scopeisusedtogainaccesstothemodelrelatedtoaparticularcontroller.$rootScopeisaparentscopethatcanbeusedtosaveandaccessmodelpropertiesthatspanmultiplecontrollers.Theuseof$rootScopeishighlydiscouragedinmostdesigns,however.Thereisonlyone$rootScopeinanapplication.$scopeisachildscopeof$rootScope.
AproperlydesignedAngularJSapplicationwillhavelittleornousefor$rootScopetostoremodelproperties.Inthischapterwewillfocusonlyon$scope,usedtostorethemodelretrievedfromRESTservices.
PublicRESTServicesTheRESTservicesusedforthischapterareavailableathttp://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog.TheservicesareopentothepublicandwritteninJavaScriptusingNode.js,ExpressJS,andMongoDB.InChapter11,youwilldeploythesameRESTserviceswithyourAngularJSblogapplicationasaMEANstack(MongoDB,ExpressJS,AngularJS,andNode.js)application.YouwillthendeploytheMEANstacktothecloudusingafreeRedHatOpenShiftaccount.
ThefollowingexcerptshowshowAngularJSservicesaccesstheRESTservicesusedforthischapter.TheRESTservicesreturnthesameJSONthatwaspreviouslyhardcodedinthecontrollers:
/*chapter7/services.jsexcerpt*/
$resource(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blog/:id"
...
$resource(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blogList"
...
Thecompletemodifiedservices.jsfileisshownhere:
/*chapter7/services.jscompletefile*/
'usestrict';
/*Services*/
varblogServices=
angular.module('blogServices',['ngResource']);
blogServices.factory('BlogPost',['$resource',
function($resource){
return$resource(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blog/:id",
{},{
get:{method:'GET',cache:false,isArray:false},
save:{method:'POST',cache:false,isArray:false},
update:{method:'PUT',cache:false,isArray:false},
delete:{method:'DELETE',cache:false,isArray:false}
});
}]);
blogServices.factory('BlogList',['$resource',
function($resource){
return$resource(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blogList",
{},{
get:{method:'GET',cache:false,isArray:true}
});
}]);
ChangestotheControllersShownnextisthecontrollers.jsfile.Thechangesmadeheregreatlysimplifythecontrollers.Theservicesneededforeachindividualcontrollerareinjectedandmadeaccessibleforthatparticularcontrollertouse.TheblogIDispassedasapathparameterargumenttotheBlogPostservice.Apathparameterisusedbecausewedefined/id:attheendoftheBlogPostserviceURLintheservices.jsfile.Ifweremovedthe/:idfromtheendoftheserviceURL,AngularJSwouldpassthevalueasaqueryparameterargumentinstead.Theupdatedfilelookslikethis:
/*chapter7/controllers.js*/
'usestrict';
/*Controllers*/
varblogControllers=
angular.module('blogControllers',[]);
blogControllers.controller('BlogCtrl',
['$scope','BlogList',
functionBlogCtrl($scope,BlogList){
$scope.blogList=[];
BlogList.get({},
functionsuccess(response){
console.log("Success:"+
JSON.stringify(response));
$scope.blogList=response;
},
functionerror(errorResponse){
console.log("Error:"+
JSON.stringify(errorResponse));
}
);
}]);
blogControllers.controller('BlogViewCtrl',['$scope',
'$routeParams','BlogPost',
functionBlogViewCtrl($scope,$routeParams,BlogPost){
varblogId=$routeParams.id;
$scope.blg=1;
BlogPost.get({id:blogId},
functionsuccess(response){
console.log("Success:"+
JSON.stringify(response));
$scope.blogEntry=response;
},
functionerror(errorResponse){
console.log("Error:"+
JSON.stringify(errorResponse));
}
);
}]);
ModelPropertiesOnceyou’veaddedtheJSONreturnedfromtheRESTservicetothemodelbyassigningittoascopeproperty,thatJSONismadeavailabletotheview.Allscopepropertiesareaccessedfrominsidetheview,asdescribedinpreviouschapters.Therearenochangesthatneedtobemadeintheview.
IfyouhaveusedotherJavaScriptclient-sideframeworks,bynowyoushouldseethesimplicityofAngularJSmodels.WithAngularJS,therearenomodelclassesthatneedtobedefined;youdon’tneedtowritemodelAjaxcodeorcreatemodelobjectsthathavetobeboundtotheviews.Allyouhavetodoisassignmodelpropertiestothescope.TheAngularJSframeworkhandlestherest.
AngularJSmodelsgreatlysimplifythecreationofJavaScriptapplications.Youcancutwhatpotentiallycouldbethousandsoflinesofmodel-relatedcodedowntoonlyafewlines.Bycuttinglinesofcodeyoualsocutvaluabledevelopmenttime,andpotentiallythenumberofdevelopersneededonaproject.Thesimplicityofthemodelcodealsomakesapplicationseasiertomaintainorenhance,onceagaincuttingcostsbycuttingdevelopmenttime.
BlogApplicationPublicServicesNowwewillmaketheneededchangestoenableourblogapplicationtousethepublicRESTservicesdiscussedinthepreviouschapter.First,wemustaddtheservices.jsfiletoourproject.
Right-clicktheprojectandaddanewJavaScriptfilenamedservices.jsunderthejsfolder,asshowninFigure7-1.
Figure7-1.Addingtheservices.jsfile
Addthiscodetothenewlycreatedfile:
/*chapter7/services.js*/
'usestrict';
/*Services*/
varblogServices=
angular.module('blogServices',['ngResource']);
blogServices.factory('BlogPost',['$resource',
function($resource){
return$resource(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blog/:id",
{},{
get:{method:'GET',cache:false,isArray:false},
save:{method:'POST',cache:false,isArray:false},
update:{method:'PUT',cache:false,isArray:false},
delete:{method:'DELETE',cache:false,isArray:false}
});
}]);
blogServices.factory('BlogList',['$resource',
function($resource){
return$resource(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blogList",
{},{
get:{method:'GET',cache:false,isArray:true}
});
}]);
Nowaddthenewservices.jsfiletotheindex.htmlfile’s<head>section,asshownhere,sothefilecanbeloadedbyourAngularJSapplication:
<!--chapter7/index.htmlexcerpt-->
<scriptsrc="js/services.js"></script>
ModifyingtheHTMLThecompleteindex.htmlfileisshownhereforconvenience:
<!--chapter7/index.html-->
<!DOCTYPEhtml>
<htmllang="en"ng-app="blogApp">
<head>
<title>AngularJSBlog</title>
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<metahttp-equiv="Content-Type"content="text/html;charset=UTF-8">
<linkrel="stylesheet"href="lib-css/bootstrap.min.css"media="screen"/>
<linkrel="stylesheet"href="css/styles.css"media="screen"/>
<scriptsrc="js/libs/jquery-1.10.2.min.js"></script>
<scriptsrc="js/libs/bootstrap.min.js"></script>
<scriptsrc="js/libs/angular.min.js"></script>
<scriptsrc="js/libs/angular-route.min.js"></script>
<scriptsrc="js/libs/angular-resource.min.js"></script>
<scriptsrc="js/libs/angular-cookies.min.js"></script>
<scriptsrc="js/app.js"></script>
<scriptsrc="js/controllers.js"></script>
<scriptsrc="js/services.js"></script>
</head>
<body>
<divng-view></div>
</body>
</html>
ModifyingApp.jsThenewlycreatedservicesmodulemustbeaddedtotheapplicationbeforeitcanbeused.WeaddthenewblogServicesmoduleasadependencyoftheapplicationatstartuptimeusinginlinearrayannotations,asshownhere.Nowthenewservicescanbeinjectedandusedincontrollerswheneverneeded.WecannowreplacethehardcodedJSONusedasmockdatainpreviouschapters:
/*chapter7/app.js*/
'usestrict';
/*AppModule*/
varblogApp=angular.module('blogApp',[
'ngRoute',
'blogControllers',
'blogServices'
]);
blogApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'BlogCtrl'
}).when('/blogPost/:id',{
templateUrl:'partials/blogPost.html',
controller:'BlogViewCtrl'
});
$locationProvider.html5Mode(false).hashPrefix('!');
}]);
ModifyingtheControllersNowlet’sseehowtousethenewservicesinourcontrollers.Replacethepreviouscodeincontrollers.jswiththecodeshownnext.Thecodeshowshowweinjecttheservicesintoeachcontroller.Wepopulatethescopepropertiesinsidethesuccesscallbackfunction,asexplainedinpreviouschapters.
Asexplainedearlier,thesuccesscallbackfunctionisonlycalledwhentheRESTservicecallreturnssuccessfully.Atthatpoint,wecansafelypopulatethescopeproperties.ThescopepropertiesarethenboundtotheviewbytheAngularJSframework:
/*chapter7/controllers.js*/
'usestrict';
/*Controllers*/
varblogControllers=
angular.module('blogControllers',[]);
blogControllers.controller('BlogCtrl',
['$scope','BlogList',
functionBlogCtrl($scope,BlogList){
BlogList.get({},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.blogList=response;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
});
}]);
blogControllers.controller('BlogViewCtrl',
['$scope','$routeParams','BlogPost',
functionBlogViewCtrl($scope,$routeParams,BlogPost){
varblogId=$routeParams.id;
BlogPost.get({id:blogId},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.blogEntry=response;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
});
}]);
Wealsomadesomechangestothecontrollers.jsfiletomaketestingeasier.TestingAngularJScontrollerscanbemorecomplexwhenRESTservicesareinvolved.Asmentionedpreviously,wedon’tknowwhenRESTserviceswillreturnresults,becausetheyareasynchronouscalls.
AsynchronousRESTservicecallswillalwayscausecontrollerunitteststofail.UnittestsofcontrollersthatdependonRESTserviceswillfinishexecutionbeforetheRESTserviceseverreturnresults,soanyscopepropertiesusedbycontrollerunittestswillbemissingwhenthetestscriptexecutesifthosepropertiesarereturnedfromaRESTservicecall.
TherearewaystoaddadelayandmakeunittestscriptswaitontheRESTserviceresults,buttheyaddanunneededlevelofcomplexitytothetestscripts.Unittesting,afterall,
shouldbeatestofaunitofcodeandnotanend-to-endtest.ProtractorE2EtestsareabetterwaytotestRESTservices.
Lookatthecodethatfollows.TheBlogListserviceisinjectedintotheBlogCtrlcontroller.WemakeanasynchronouscalltothegetmethodoftheBlogListservicebypassingtwocallbackfunctionstothecall.Thesuccesscallbackfunctionreturnsasuccessfulserviceresponseobject,andtheerrorcallbackfunctionreturnsanyerrorsiftheservicecallfails:
/*chapter7/controllers.jsexcerpt*/
blogControllers.controller('BlogCtrl',['$scope','BlogList',
functionBlogCtrl($scope,BlogList){
$scope.blogList=[];
BlogList.get({},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.blogList=response;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
}
);
}]);
ItmaytakeasecondormorefortheRESTservicetoreturnresults.OncetheRESTservicedoesreturnresults,thesuccesscallbackfunctionwillbecalled.Unfortunately,theunittestscriptwillhavefinishedexecutionlongbefore.Weremedythisissuebymakingachangetothecontroller.
Noticetheassignment$scope.blogList=[];intheprecedingcode.Theassignmenthasnoimpactonthefunctionalityofthecontroller,butithasamajorimpactontheunittestscriptassociatedwiththeBlogCtrlcontroller.TheassignmentinitializesthescopeblogListpropertywithanemptyarray.
ThefollowingcodeshowshowtheemptyarrayisusedtotesttheblogCtrlcontroller.Noticethelineofcodechecksthatthearraylengthisequalto0:
/*chapter7/controllerSpec.jsexcerpt*/
expect(scope.blogList.length).toEqual(0);
Wecanthenrestassuredthatthecontrollerisworkingsuccessfullyfroma“unitofcode”perspective.YouwillseelaterhowtomakesuretheRESTserviceworkedasexpected.
RunningtheApplicationInNetBeans,right-clickandrunyourAngularJSblogapplication.Youshouldseethesamedatadisplayedonthescreenthatwastherewhenthedatawashardcoded.IfyouareusingChromeasyourbrowser,youcanturnon“DeveloperTools”andclickthe“Network”menubuttontoseetheRESTservicecallsthataremadeasyouclickvariouslinksintheapplication.YoucanalsoclicktheHeaders,Preview,Response,andTimingtabsinDeveloperToolstoseespecificinformationabouteachservicecall.
UsingChromeDeveloperToolsisalsoagreatwaytotroubleshootissueswithAngularJSRESTservicecallsifyouhaveproblems.There’sagreatJavaScriptdebuggerthatcanbeusedtodebugRESTservicecallsandotherJavaScriptissues.
IfyouarenotfamiliarwithChromeDeveloperTools,seetheGoogleChromesiteformoreinformation.InadditiontotheChromedebugger,NetBeansalsohasadebuggerbuiltinfordebuggingJavaScriptapplications.FormoreinformationondebuggingJavaScriptinNetBeans,takealookattheNetBeanswebsite.
TestingServiceswithKarmaThebestwaytotestAngularJSservicesiswithKarma.WeusedKarmaasoneofourtestframeworksinpreviouschapters.Youshouldhavealreadycreatedthepackage.jsonfilefortheblogprojectbackinChapter5.Thefileisshownagainhereforreference:
/*chapter7/package.json*/
{
"name":"package.json",
"devDependencies":{
"karma":"*",
"karma-chrome-launcher":"*",
"karma-firefox-launcher":"*",
"karma-jasmine":"*",
"karma-junit-reporter":"*",
"karma-coverage":"*"
}
}
WealsocreatedtheKarmaconfigurationfilefortheblogprojectbackinChapter5,butweneedtomakeasmallchangetothat:weneedtoaddtheAngularJSangular-resource.min.jsfiletothekarma.conf.jsfiletotestourservices.Theangular-resource.min.jsfileisusedbyboththeBlogListandBlogPostservices.Themodifiedkarma.conf.jsfilelookslikethis:
/*chapter7/karma.conf.js*/
module.exports=function(config){
config.set({
basePath:'../',
files:[
"public_html/js/libs/angular.min.js",
"public_html/js/libs/angular-mocks.js",
"public_html/js/libs/angular-route.min.js",
"public_html/js/libs/angular-resource.min.js",
"public_html/js/*.js",
"test/**/*Spec.js"
],
exclude:[
],
autoWatch:true,
frameworks:[
"jasmine"
],
browsers:[
"Chrome",
"Firefox"
],
plugins:[
"karma-junit-reporter",
"karma-chrome-launcher",
"karma-firefox-launcher",
"karma-jasmine"
]
});
};
KarmaServiceSpecificationsNowweneedtoaddnewservicetestspecificationsfortheblogproject.Dothefollowing:
1. CreateanewJavaScriptfilenamedservicesSpec.jsundertheunitfolder.
2. Enterthefollowingcodeinthenewfile:
/*chapter7/servicesSpec.js*/
/*Jasminespecsforcontrollers*/
describe('AngularJSBlogServiceTesting',function(){
describe('testBlogList',function(){
var$rootScope;
varblogList;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
blogList=$injector.get('BlogList');
}));
it('shouldtestBlogListservice',function(){
expect(blogList).toBeDefined();
});
});
describe('testBlogPost',function(){
var$rootScope;
varblogPost;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
blogPost=$injector.get('BlogPost');
}));
it('shouldtestBlogPostservice',function(){
expect(blogPost).toBeDefined();
});
});
});
ItisimportanttopointoutherethatourtestspecificationsfortheblogservicesdonotdependonthepresenceandfunctionalityoftheassociatedRESTservicesthatgetcalledbythoseservices.KarmaunittestsshouldtestthattheAngularJSservicescanbeinjected.Ifthetestsaresuccessful,thatprovesthattheservicesareconstructedproperly.Ourunittestingofservicesdoesnot,however,provethattheRESTservicesareworking.
AsImentionedbefore,Karmaunittestsoftenruninsidesomecontinuousintegration(CI)framework.CIsystemsareoftenconfiguredtotriggertherunningofunittestseverytimeachangeispushedtothesourcerepository.TheexistenceandaccessibilityofRESTservicescan’talwaysbeguaranteedwhenyou’reunittestinginsideaCI.
Unittestsshouldn’tdependontheexistenceofRESTservicesorothernetwork-relateddevices.Unittestingshouldtesttheindividualunitsofcodeandnottrytodoend-to-endtesting.WewilltestthefunctionalityofourRESTserviceswhenwedoE2EtestingwithProtractor.AnyproblemsrelatedtothecallingofRESTserviceswillshowasfailuresinProtractor.
KarmaTestingThenewtestspecificationswillunittestthenewservices.ThecontrollerswillalsobetestedbecausewestillhavethecontrollerSpec.jsfileinoursystem.OurKarmaconfigurationfilelooksforalltestfilesthatendinSpec.js.
Right-clicktheprojectandselect“Test”fromthemenu.Karmawillstart.YoushouldseebothChromeandFirefoxbrowserwindowsopen.TheNetBeanstestresultswindowshouldopenanddisplayfourpassedtestsforChromeandfourpassedtestsforFirefox.
Ifyougetanyerrormessagesorfailedtests,gobackoverthissectionandverifythatyoucompletedalltheconfigurationsandinstallations.YoucanalsodownloadtheChapter7codefromtheGitHubprojectsite.
End-to-EndTestingWealreadycreatedaProtractorconfigurationfilefortheblogapplicationinChapter5.TheProtractorconfigurationfileisshownhereforreference:
/*chapter7/conf.jsProtractorconfigurationfile*/
exports.config={
seleniumAddress:'http://localhost:4444/wd/hub',
specs:['e2e/blog-spec.js']
};
ProtractorTestSpecificationNowweneedtochangetheProtractortestspecificationscreatedearlier.ThenewProtractortestsneedtointeractwiththeRESTservicesthatweuseinthischapter.
Copythecodeshownhereintotheblog-spec.jsfile.Makesurethelineslikebrowser.get("http://localhost:8383/AngularJsBlog/");matchtheURLthatyouuseonyoursystemtocalltheblogapplication.TheURLcanbedifferentfordifferentdevelopmentenvironmentsandcandependonhowyounamedyourproject:
/*chapter7/blog-spec.jsProtractortestspecification*/
describe("BlogApplicationTest",function(){
it("shouldtestthemainblogpage",function(){
browser.get(
"http://localhost:8383/AngularJsBlogChapter7/");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthebloglist
varblogList=
element.all(by.repeater('blogPostinblogList'));
//teststhesizeoftheblogList
expect(blogList.count()).toEqual(1);
browser.get(
"http://localhost:8383/AngularJsBlogChapter7
/#!/blogPost/5394e59c4f50850000e6b7ea");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthecommentlist
varcommentList=
element.all(by.repeater('commentinblogEntry.comments'));
//checksthesizeofthecommentList
expect(commentList.count()).toEqual(2);
});
});
ProtractorTestingStartanewcommandwindowandenterthefollowingcommandtostartthetestserver:
webdriver-managerstart
OpenanewcommandwindowandnavigatetotherootoftheChapter5project.Typethecommand:
protractortest/conf.js
Youshouldseeabrowserwindowopen.Youshouldthenseethetestscriptnavigatethroughthepagesoftheblogapplication.WhentheProtractorscripthasfinished,thebrowserwindowwillclose.
YoushouldseeresultslikethefollowinginthecommandwindowwhentheProtractorscriptcompletes.Thenumberofsecondsthatittakesthescripttofinishwillvarydependingonyourparticularsystem:
Finishedin1.52seconds
1test,4assertions,0failures
ConclusionThisconcludesourdiscussionofAngularJSmodels.WeaddedcodetomakeourblogapplicationworkwithRESTservicesrunninginthecloud,andwewroteunitteststotestthenewservicesthatweadded.WethenusedProtractortodoend-to-endtestingthatvalidatedthefunctionalityofourRESTservicesandtheAngularJSservicesassociatedwiththoseRESTservices.
WewilltalkaboutmodelsagaininChapter11,whenwedeployourapplicationtothecloudasaMEANstackapplication.Next,wewilladdsomenon-RESTservicestohandlebusinesslogicandseethepowerofAngularJSinaction.
Chapter8.ServicesandBusinessLogic
NotallAngularJSservicesconnecttoRESTservices.Servicescanalsocontainbusinesslogicthatisusedbymultiplecontrollers.AsImentionedbefore,ifthebusinesslogiccanbemovedtoaRESTservice,thatiswhereitshouldbedefined.DefiningbusinesslogicinRESTservicesassuresthatthesamelogicwillbereadilyavailabletoallclient-sideapplications.
Often,however,itisnotpossibletomoveallbusinesslogictoRESTservices.Oftenthatsamebusinesslogicisneededacrossmultiplecontrollers.ThatiswhereAngularJSnon-RESTservicescomeinhandyonceagain.InthischapterwewilllookatseveralexamplesofwhereAngularJSnon-RESTservicesareuseful.
Take,forexample,asituationwhereauserneedstoauthenticateacrossmultipleRESTservices.OnewaytodothatisbyusingBasicAuthentication,wheretheuser’susernameandpasswordarepassedtoaserviceasatokenintheHTTPSheaderduringaservicecall.Thetokenisintheformof“username:password”andencodedwithbase64.
Asweknow,aRESTserviceshouldn’tholdstate,andholdingauser’scredentialsinasessionvariableontheserverisaserioussecurityconcern.UsingasessionvariabletoholdauthenticationstateontheserversideisusuallynotacceptableinmostRESTservicedesigns.AngularJSservicesaregreatforhandlingsuchsituations.
HandlingUserAuthenticationFirst,weneedawaytovalidateauser’scredentialsoverHTTPS.ThefollowingcodeshowsaRESTserviceusedtoauthenticateauser:
/*chapter8/loginRESTserviceURLfromservices.js*/
POST:https://www.micbutton.com/user/login
HereistheJSONrequestfortheRESTservice:
{
"username":"ken",
"password":"password"
}
AndhereistheJSONresponsefortheRESTservice:
{
"authenticated":true
}
Thisparticularservicecallwouldnormallybeopentoanyuserandthereforewouldnotrequireauthentication.Allowingalluserstoaccessthisserviceuninhibitedmeansanyusercantrytovalidateagainsttheservice.Ifthereisapossibilityofabuse,theservicecouldbesecuredatthenetworklevel,orachallengeandresponsesystemcouldbeusedtodiscourageunwantedusers.
Onceausermakesacalltotheloginserviceandtheuser’scredentialsarevalidated,itisthejoboftheAngularJSapplicationtotemporarilystorethosecredentials.ItisalsothejoboftheAngularJSapplicationtodirecttheusertoaloginpagewhentheuserhasnotauthenticated.AngularJSnon-RESTservicesplayamajorroleinthisprocess.
UsingBasicAuthenticationIftheRESTservicesaredesignedproperlytorequireauthenticationonallservicesthatcontainprivatedata,theAngularJSapplicationuserwillneverhaveaccesstoprivatedatawithoutprovidingthepropercredentials.Oncetheuserprovidesvalidusercredentials,theAngularJSapplicationcanstorethosecredentialsinacookieorsomeothertemporarystorage.Cookiesareagoodplacetostoreusercredentialsbecauseallmodernbrowsersstorecookiesmappedtoaparticularwebdomain.Cookieaccessisthengrantedonlytotheapplicationthatactuallycreatedthecookieonthatparticulardomain.OtherJavaScriptapplicationsrunninginthebrowseronlyhaveaccesstocookiestheycreate,whichareassociatedwiththeirrespectivedomains.
CreatingAngularJSServicesAsImentionedinChapter6,therearethreewaystocreateservicesinAngularJS.Aservicecanbecreatedwiththeservicefunction,asshownhere:
/*chapter8/servicefunction*/
varblogServices=angular.module('blogServices',
['ngResource']);blogServices.service('BlogPost',[…]
orwiththeproviderfunction:
/*chapter8/providerfunction*/
varblogServices=angular.module('blogServices',
['ngResource']);blogServices.provider('BlogPost',[…]
ThethirdwaytocreateservicesinAngularJSiswiththefactoryfunction.ThisisthemethodwewillusetocreateAngularJSservicesinthischapterandthroughoutthisbook,becauseitisthemostcommonlyusedmethod.Thefollowingcodeshowshowtocreateaservicewiththefactoryfunction:
/*chapter8/factoryfunction*/
varblogServices=angular.module('blogServices',
['ngResource']);blogServices.factory('BlogPost',[…]
HoldingUserCredentialsNowlet’stakealookatanAngularJSbusinesslogicservicedesignedtosavetheuser’scredentialsoncetheuserhasauthenticated.TheservicemakesuseofAngularJScookies,whichwecanincludeinanapplicationbyincludingtheangular-cookies.min.jslibraryfile.Theservicehastwoparametersdefined:theusername(un)andpassword(pw).
ThetwovaluesassignedtotheserviceareusedtobuildthetokenthatissentintheHTTPSheaderofeachRESTservicecall.TheAngularJSservicethenstoresthetokenandtheusernameascookiesforuselater:
/*chapter8/non-RESTbusinessservicetosetusercredentials*/
blogBusinessServices.factory('setCreds',
['$cookies',function($cookies){
returnfunction(un,pw){
vartoken=un.concat(":",pw);
$cookies.blogCreds=token;
$cookies.blogUsername=un;
};
}]);
Here’swhatacalltothesetCredsbusinesslogicservicetosaveanauthenticateduser’scredentialslookslike:
/*chapter8/controllers.jsexcerpt*/
setCreds($scope.username,$scope.password);
CheckingUserCredentialsNowlet’slookatabusinesslogicservicethatchecksthestatusofauser’scredentials.Iftheservicereturnsfalse,theAngularJSapplicationshouldredirecttheusertotheloginpage.Itisalsoimportanttoremembertosavetheuser’scredentialsbymakingacalltosetCredsanytimetheuser’spasswordischanged:
/*chapter8/non-RESTbusinesslogicservicetocheckcredentials*/
blogBusinessServices.factory('checkCreds',
['$cookies',function($cookies){
returnfunction(){
varreturnVal=false;
varblogCreds=$cookies.blogCreds;
if(blogCreds!==undefined&&blogCreds!==""){
returnVal=true;
}
returnreturnVal;
};
}]);
TheservicesimplylooksfortheexistenceoftheblogCredscookieandreturnstrueifthecookieexists.IfasubsequentservicecallfailswiththesavedcredentialsandreturnsanHTTP401errorcode,itisthejoboftheAngularJSapplicationtodeletethesavedcookiesandredirecttheusertotheloginpage.ThefollowingcodeshowsacalltothecheckCredsservice:
/*chapter8/controllers.jsexcerpt*/
if(checkCreds()){
//dosomethingtocontinue
}
DeletingUserCredentialsOurnextservicedeletestheuser’scredentialsoncetheuser’ssessionhasended,orwhentheuser’scredentialsfailedtoauthenticateduringaRESTservicecall.OncetheblogCredscookieisremoved,theAngularJSapplicationshouldredirecttheusertotheloginpage:
/*chapter8/non-RESTbusinesslogicservicetodeletecredentials*/
blogBusinessServices.factory('deleteCreds',
['$cookies',function($cookies){
returnfunction(){
$cookies.blogCreds="";
$cookies.blogUsername="";
};
}]);
Here’swhatacalltothedeleteCredsservicelookslike:
/*chapter8/controllers.jsexcerpt*/
deleteCreds();
RetrievingUserCredentialsThefollowingcodeshowsabusinesslogicservicethatretrievestheuser’stokenfromtheblogCredscookie.AtokenpassedtoaRESTserviceintheHTTPSheadermustbeencodedwithbase64.Thebusinessserviceencodesthetokeninbase64andthenreturnsthatencodedtoken:
/*chapter8/non-RESTbusinesslogicservicetoretrievecredentials*/
blogBusinessServices.factory('getToken',
['$cookies',function($cookies){
returnfunction(){
varreturnVal="";
varblogCreds=$cookies.blogCreds;
if(blogCreds!==undefined&&blogCreds!==""){
returnVal=btoa(blogCreds);
}
returnreturnVal;
};
}]);
ThefollowingcodeshowshowthetokenreturnedfromtheserviceisusedtobuildtheBasicAuthenticationheaderwhenwe’recallingaRESTservice.ThislineshouldbedefinedbeforeeveryRESTservicecallthatrequiresauthentication.ThecallmakesuseoftheAngularJS$httpservice:
/*chapter8/controllers.jsexcerpt*/
$http.defaults.headers.common['Authorization']='Basic'+getToken();
ThefollowingcodeshowshowtousethegetTokenservicetoauthenticatetotheBlogservicewhenwearesavingablogpost:
/*chapter8/controllers.jsexcerpt*/
blogControllers.controller('NewBlogCtrl',
['$scope','checkCreds','$location','$http','getToken',
functionNewBlogCtrl($scope,checkCreds,$location,$http,getToken){
$http.defaults.headers.common['Authorization']='Basic'+getToken();
Blog.save({},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.status=response;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
}
);
}]);
Onefinalbusinesslogicservicethatwouldbeusefulisshownnext.Theserviceretrievestheuser’susernamefromtheblogUsernamecookie.Theusernameisthenreturnedforuseinmultipleplacesthroughouttheapplication.UsingthegetUsernameservicesimplifiesstoringandaccessingtheuser’susername:
/*chapter8/non-RESTbusinesslogicservicetoretrieveusername*/
blogBusinessServices.factory('getUsername',
['$cookies',function($cookies){
returnfunction(){
varreturnVal="";
varblogUsername=$cookies.blogUsername;
if(blogUsername!==undefined&&blogUsername!==""){
returnVal=blogUsername;
}
returnreturnVal;
};
}]);
ItshouldbeobviousbynowthatAngularJSservicesareveryvaluabletohaveinanapplication.AnytimeAngularJSbusinesslogicneedstobeusedbymultiplecontrollers,thatlogicshouldbedefinedinservices.
Wewillnowaddeverythingthatwehavecoveredinthischapterintoonefile,calledbusinessServices.js,andaddtheservicesinthatfiletoourblogproject.InChapter10wewilladdaloginscreenandsecuritytoourblogapplication.Withsecurityinplace,wewillthendeployourapplicationtothecloudinChapter11.Beforewedeployourblogapplicationtothecloud,however,wewilladdnewscreensinChapter11toallowausertosubmitnewblogpostsandcomments.
BlogApplicationBusinessLogicNow,toaddthenewbusinessservices,right-clicktheprojectnodeandaddanewJavaScriptfilenamedbusinessServices.jsunderthejsfolder.Hereisthecodethatshouldbeplacedinthenewlycreatedservicesfile.NoticethatwehavemadeAngularJScookiesavailablebyinjectingngCookies.AngularJScookiesareprovidedbyangular-cookies.min.js,whichwealreadyaddedtotheprojectearlier:
/*chapter8/businessServices.js*/
'usestrict';
/*businesslogicservicesonly*/
varblogBusinessServices=
angular.module('blogBusinessServices',['ngCookies']);
blogBusinessServices.factory('checkCreds',
['$cookies',function($cookies){
returnfunction(){
varreturnVal=false;
varblogCreds=$cookies.blogCreds;
if(blogCreds!==undefined&&blogCreds!==""){
returnVal=true;
}
returnreturnVal;
};
}]);
blogBusinessServices.factory('getToken',
['$cookies',function($cookies){
returnfunction(){
varreturnVal="";
varblogCreds=$cookies.blogCreds;
if(blogCreds!==undefined&&blogCreds!==""){
returnVal=btoa(blogCreds);
}
returnreturnVal;
};
}]);
blogBusinessServices.factory('getUsername',
['$cookies',function($cookies){
returnfunction(){
varreturnVal="";
varblogUsername=$cookies.blogUsername;
if(blogUsername!==undefined&&blogUsername!==""){
returnVal=blogUsername;
}
returnreturnVal;
};
}]);
blogBusinessServices.factory('setCreds',
['$cookies',function($cookies){
returnfunction(un,pw){
vartoken=un.concat(":",pw);
$cookies.blogCreds=token;
$cookies.blogUsername=un;
};
}]);
blogBusinessServices.factory('deleteCreds',
['$cookies',function($cookies){
returnfunction(){
$cookies.blogCreds="";
$cookies.blogUsername="";
};
}]);
UsingtheBusinessLogicNowtoloadthenewbusinesslogicservices,wemustaddthebusinessServices.jsfiletothe<head>sectionofindex.html,asshownhere:
<!--chapter8/index.htmlexcerpt-->
<scriptsrc="js/businessServices.js"></script>
Thecompleteindex.htmlfileisshownhereforconvenience:
<!--chapter8/index.html-->
<!DOCTYPEhtml>
<htmllang="en"ng-app="blogApp">
<head>
<title>AngularJSBlog</title>
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<metahttp-equiv="Content-Type"content="text/html;charset=UTF-8">
<linkrel="stylesheet"href="lib-css/bootstrap.min.css"media="screen"/>
<linkrel="stylesheet"href="css/styles.css"media="screen"/>
<scriptsrc="js/libs/jquery-1.10.2.min.js"></script>
<scriptsrc="js/libs/bootstrap.min.js"></script>
<scriptsrc="js/libs/angular.min.js"></script>
<scriptsrc="js/libs/angular-route.min.js"></script>
<scriptsrc="js/libs/angular-resource.min.js"></script>
<scriptsrc="js/libs/angular-cookies.min.js"></script>
<scriptsrc="js/app.js"></script>
<scriptsrc="js/controllers.js"></script>
<scriptsrc="js/services.js"></script>
<scriptsrc="js/businessServices.js"></script>
</head>
<body>
<divng-view></div>
</body>
</html>
WemustalsoaddthenewblogBusinessServicesmoduleasadependencyoftheapplicationatstartuptime.Wedothisusinginlinearrayannotations:
/*chapter8/app.js*/
'usestrict';
/*AppModule*/
varblogApp=angular.module('blogApp',[
'ngRoute',
'blogControllers',
'blogServices',
'blogBusinessServices'
]);
blogApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'BlogCtrl'
}).when('/blogPost/:id',{
templateUrl:'partials/blogPost.html',
controller:'BlogViewCtrl'
});
$locationProvider.html5Mode(false).hashPrefix('!');
}]);
TestingServiceswithKarmaUnittestingservicesishowwefinddefectsearlyinthedevelopmentprocess.Infact,unittestsforeachindividualserviceshouldbewrittenwhentheserviceiswritten.Althoughourservicesinthischapterarenotoverlycomplicated,unittestingisstillveryimportant.WewillcontinuetouseKarmaforunittestinginthischapter.
KarmaConfigurationWealreadyhaveaKarmaconfigurationfileforourblogproject,butweneedtomakeamodificationtothefiletoaccommodateAngularJScookiesinourKarmaunittestscripts.SincetheservicesinthischapterrelyonAngularJScookies,weneedtomakethekarma.conf.jsfileawareoftheangular-cookies.min.jsfileinourproject.
Thelineinthekarma.conf.jsfilethatmakesKarmaawareofAngularJScookiesisshownhere:
/*chapter8/karma.conf.jsexcerpt*/
files:[
...
"public_html/js/libs/angular-cookies.min.js",
...
],
Thecompletekarma.conf.jsfileisshownhere.Maketheneededchangetothekarma.conf.jsfileinyourblogproject,andthenwewilllookathowwetestournewbusinessservices:
/*chapter8/karma.conf.jscompletefile*/
module.exports=function(config){
config.set({
basePath:'../',
files:[
"public_html/js/libs/angular.min.js",
"public_html/js/libs/angular-mocks.js",
"public_html/js/libs/angular-route.min.js",
"public_html/js/libs/angular-resource.min.js",
"public_html/js/libs/angular-cookies.min.js",
"public_html/js/*.js",
"test/**/*Spec.js"
],
exclude:[
],
autoWatch:true,
frameworks:[
"jasmine"
],
browsers:[
"Chrome",
"Firefox"
],
plugins:[
"karma-junit-reporter",
"karma-chrome-launcher",
"karma-firefox-launcher",
"karma-jasmine"
]
});
};
KarmaTestSpecificationsNowweneedtoaddunittestspecificationsforeachofthefivebusinesslogicservicesthatweaddedearlierinthechapter.Wewilltalkbrieflyabouteachindividualunittesttogainafullunderstandingofthetestspecifications.
FirstwewilltakealookattheunittestforthesetCredsservice.Ifyouremember,thesetCredsservicetakestwoparameters,theusernameandpassword.Wewilltesttheoperationoftheservicethoroughlyintheunitteststhatfollow,butfornowourunittestwillonlycheckthatthesetCredsservicecanbeinjected:
/*chapter8/businessServicesSpec.jsexcerpt-setCredsservice*/
describe('testsetCreds',function(){
var$rootScope;
varsetCreds;
beforeEach(module('blogBusinessServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
setCreds=$injector.get('setCreds');
setCreds("test","test");
}));
it('shouldtestsetCredsserviceexist',function(){
expect(setCreds).toBeDefined();
});
});
NextwewilllookattheunittestforthecheckCredsservice.TheunittestscriptusesboththesetCredsserviceandthecheckCredsservice.RecallthatthecheckCredsserviceusesAngularJScookies.Whencookiesarecreatedfromaunittestscript,thecookiescreatedexistonlyforthedurationofthetestscript.Whentheunittestscriptends,sodothecookies.OurcheckCredsunittestlookslikethis:
/*chapter8/businessServicesSpec.jsexcerpt-checkCredsservice*/
describe('testcheckCreds',function(){
var$rootScope;
varcheckCreds;
varsetCreds;
beforeEach(module('blogBusinessServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
checkCreds=$injector.get('checkCreds');
setCreds=$injector.get('setCreds');
setCreds("test","test");
}));
it('shouldtestsetCredsserviceexist',function(){
expect(checkCreds()).toEqual(true);
});
});
ThetestscriptfirstmakesacalltothesetCredsservice,passingausernameof“test”andapasswordof“test”asparameters.Thosevaluesarestoredinacookievalidonlyforthistestscriptrun.WethenvalidatethatthecheckCredsservicereturnstrue,indicatingthatboththesetCredsandcheckCredsservicecallsweresuccessful.Wecannowrestassured
thatbothservicesareworkingasexpected.
NowwewilltakealookattheunittestforthegetTokenservice.Justasbefore,wemakeacalltothesetCredsserviceandpassausernameof“test”andapasswordof“test”totheservice.WethenmakeacalltothegetTokenservice.Thereturnedvaluefromtheserviceisabase64-encodedstringthatiscomposedoftheusernameandthepassword.Wewillonlyvalidatethatavalueisreturned,withthetoBeDefinedmethod:
/*chapter8/businessServicesSpec.jsexcerpt-getTokenservice*/
describe('testgetToken',function(){
var$rootScope;
vargetToken;
varsetCreds;
beforeEach(module('blogBusinessServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
getToken=$injector.get('getToken');
setCreds=$injector.get('setCreds');
setCreds("test","test");
}));
it('shouldtestsetCredsserviceexist',function(){
expect(getToken()).toBeDefined();
});
});
WhenwetestthegetUsernameservice,wecanactuallyvalidatethevaluesetfortheusername.ThefollowingcodeshowstheunittestforthegetUsernameservice.Justasbefore,wemakeacalltothesetCredsserviceandpassausernameof“test”andapasswordof“test.”WethenmakeacalltothegetUsernameserviceandvalidatethatitreturns“test”astheusername:
/*chapter8/businessServicesSpec.jsexcerpt-getUsernameservice*/
describe('testgetUsername',function(){
var$rootScope;
vargetUsername;
varsetCreds;
beforeEach(module('blogBusinessServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
getUsername=$injector.get('getUsername');
setCreds=$injector.get('setCreds');
setCreds("test","test");
}));
it('shouldtestsetCredsserviceexist',function(){
expect(getUsername()).toEqual("test");
});
});
Thelastunittestisshownnext.ItisatestofthedeleteCredsservice.InthistestscriptwemakeacalltothesetCredsservice,thenwecallthedeleteCredsservicetoremovethecredentialsthatwejustadded.WethencallthecheckCredsservicetovalidatethatnocredentialsarestoredbycheckingforareturnedvalueoffalse:
/*chapter8/businessServicesSpec.jsexcerpt-deleteCredsservice*/
describe('testdeleteCreds',function(){
var$rootScope;
vardeleteCreds;
varsetCreds;
varcheckCreds;
beforeEach(module('blogBusinessServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
deleteCreds=$injector.get('deleteCreds');
setCreds=$injector.get('setCreds');
checkCreds=$injector.get('checkCreds');
setCreds("test","test");
deleteCreds();
}));
it('shouldtestsetCredsserviceexist',function(){
expect(checkCreds()).toEqual(false);
});
});
FollowingisthecompletebusinessServicesSpec.jsfile.Right-clicktheunitfolderunderthetestfolder,createanewJavaScriptfilenamedbusinessServicesSpec.js,andenterthecodeshownhere:
/*chapter8/businessServicesSpec.jscompletefile*/
describe('AngularJSBlogBusinessServiceTesting',function(){
describe('testsetCreds',function(){
var$rootScope;
varsetCreds;
beforeEach(module('blogBusinessServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
setCreds=$injector.get('setCreds');
setCreds("test","test");
}));
it('shouldtestsetCredsserviceexist',function(){
expect(setCreds).toBeDefined();
});
});
describe('testcheckCreds',function(){
var$rootScope;
varcheckCreds;
varsetCreds;
beforeEach(module('blogBusinessServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
checkCreds=$injector.get('checkCreds');
setCreds=$injector.get('setCreds');
setCreds("test","test");
}));
it('shouldtestsetCredsserviceexist',function()
expect(checkCreds()).toEqual(true);
});
});
describe('testgetToken',function(){
var$rootScope;
vargetToken;
varsetCreds;
beforeEach(module('blogBusinessServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
getToken=$injector.get('getToken');
setCreds=$injector.get('setCreds');
setCreds("test","test");
}));
it('shouldtestsetCredsserviceexist',function()
expect(getToken()).toBeDefined();
});
});
describe('testgetUsername',function(){
var$rootScope;
vargetUsername;
varsetCreds;
beforeEach(module('blogBusinessServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
getUsername=$injector.get('getUsername');
setCreds=$injector.get('setCreds');
setCreds("test","test");
}));
it('shouldtestsetCredsserviceexist',function(){
expect(getUsername()).toEqual("test");
});
});
describe('testdeleteCreds',function(){
var$rootScope;
vardeleteCreds;
varsetCreds;
varcheckCreds;
beforeEach(module('blogBusinessServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
deleteCreds=$injector.get('deleteCreds');
setCreds=$injector.get('setCreds');
checkCreds=$injector.get('checkCreds');
setCreds("test","test");
deleteCreds();
}));
it('shouldtestsetCredsserviceexist',function(){
expect(checkCreds()).toEqual(false);
});
});
});
KarmaTestingTheprecedingtestspecificationswilltestallthenewbusinesslogicservicesaddedinthischapter.ThecontrollertestspecificationandtheRESTservicetestspecificationunittestswillalsorunwhenKarmastarts.
Right-clicktheprojectandselect“Test”fromthemenu.Karmawillstart.YoushouldseebothChromeandFirefoxbrowserwindowsopen.TheNetBeanstestresultswindowshouldopenanddisplayninepassedtestsforChromeandninepassedtestsforFirefox.
Ifyougetanyerrormessagesorfailedtests,gobackoverthissectionandverifythatyoucompletedalltheconfigurationsandinstallations.YoucanalsodownloadtheChapter8codefromtheGitHubprojectsite.
End-to-EndTestingWehaven’tyetaddedthebusinesslogicservicescreatedinthischaptertoourcontrollers,soweshouldseenochangeintheend-to-endtesting.WewillvalidatethatnoadverseissueswereintroducedinthischapterwithProtractor.
ProtractorConfiguration
WealreadycreatedaProtractorconfigurationfilefortheblogapplicationinChapter5.TheProtractorconfigurationfileisshownhereforreference:
/*chapter8/conf.jsProtractorconfigurationfile*/
exports.config={
seleniumAddress:'http://localhost:4444/wd/hub',
specs:['e2e/blog-spec.js']
};
ProtractorTestSpecification
NochangesarerequiredtotheProtractortestspecification,shownhereforreference:
/*chapter8/blog-spec.jsProtractortestspecification*/
describe("BlogApplicationTest",function(){
it("shouldtestthemainblogpage",function(){
browser.get(
"http://localhost:8383/AngularJsBlog/");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthebloglist
varblogList=
element.all(by.repeater('blogPostinblogList'));
//teststhesizeoftheblogList
expect(blogList.count()).toEqual(1);
browser.get(
"http://localhost:8383/AngularJsBlog/
#!/blogPost/5394e59c4f50850000e6b7ea");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthecommentlist
varcommentList=
element.all(by.repeater('commentinblogEntry.comments'));
//checksthesizeofthecommentList
expect(commentList.count()).toEqual(2);
});
});
ProtractorTesting
Startanewcommandwindowandenterthiscommandtostartthetestserver:
webdriver-managerstart
OpenanewcommandwindowandnavigatetotherootoftheChapter5project.Typethecommand:
protractortest/conf.js
Youshouldseeabrowserwindowopen.Youshouldthenseethetestscriptnavigatethroughthepagesoftheblogapplication.WhentheProtractorscripthasfinished,thebrowserwindowwillclose.
YoushouldseeresultslikethefollowinginthecommandwindowwhentheProtractorscriptcompletes.Thenumberofsecondsthatittakesthescripttofinishwillvarydependingonyourparticularsystem:
Finishedin1.768seconds
1test,4assertions,0failures
ConclusionThechangestoourblogapplicationmadeinthischaptergiveuseverythingweneedtoenableustoworkwithRESTserviceauthentication.Asmentionedbefore,ourAngularJSapplicationdoesn’tactuallyhandleauthentication,butinsteadholdsthestatusofauthentication.
Thebusinesslogicservicesthatweaddedinthischaptergreatlysimplifytheprocessoftrackingauthenticationacrossmultiplecontrollers.WewilltalkmoreaboutsecurityinChapter10.WewillnowmoveontoAngularJSdirectives.
Chapter9.AngularJSDirectives
Fromauser’sperspective,directivesarenothingmorethancustomHTMLtagsthatareaddedtoapplicationtemplates.Directivescanbesimple,ortheycanbeverycomplex.DirectivesareusedbytheAngularJSHTMLcompilertoenhancethefunctionalityoftheassociatedtemplate.SomeexamplesofAngularJSdirectivesarengModel,ngView,andngRepeat.
TheHTMLCompilerLet’stalkbrieflyabouttheAngularJSHTMLcompiler.TheuseofthewordcompilerinrelationtoAngularJSisoftenconfusingforexperienceddevelopersnewtotheframework.Experienceddevelopersdon’tnormallyassociatecompilerswithHTML.Thewordcompiler,however,takesonawholenewmeaninginthecontextofAngularJS.
CompilingHTMLinAngularJSissimplytheprocessofsearchingthroughtheDOMtreetoidentifyHTMLelementsassociatedwithdirectives.Thecompilerthenbuildsthetemplateandassignseventstotheassociatedelementsinthetemplate.This,however,isagreatlysimplifieddescriptionoftheAngularJSHTMLcompilerandthecompilerprocesses.Ifyouwouldliketoknowmoreaboutthecompiler,takealookattheAngularJSwebsitedocumentation,whichcoverstheHTMLcompileringreatdetail.
WhatAreDirectives?DirectivesareveryvaluableinAngularJSandarewhatsetsAngularJSapartfrommostJavaScriptclient-sideframeworks.Thankstodirectives,wecanavoidcreatingmodelclasseswithhundredsoflinesofcode.Thankstodirectives,wehaveasimplifiedmodelandviewinAngularJSthatallowsdeveloperstoquicklycreatepowerfulJavaScriptapplications.
AlthoughbuildingcustomdirectivesinAngularJSisabitmorecomplextolearnthanotherareasoftheframework,Iwilltrytosimplifythelearningprocessbyshowingyouhowtocreateafairlysimpledirective.TherearecompletebooksthatcovertheAngularJSdirectivedesignprocess,soifyouhaveadesiretolearnaboutAngularJSdirectivesingreatdetail,abookthatcoversonlydirectiveswouldbeagoodstartingpointafteryoufinishthischapter.
BuildingCustomDirectivesIfyourememberbackinChapter5,webuiltamenuforourblogapplicationandused<divng-includesrc="'partials/menu.html'"></div>toincludethatmenuintoeachtemplate.Themenuwasdefinedinthemenu.htmlfileasHTML.Whilethatapproachworkswellandisacommonwaytoaddanapplicationmenu,thereisanotherwaytoaddamenuthatisabitmoreelegant.
Ournewmenuapproachwillinvolvebuildingacustomdirectivetohandletheinclusionofamenuintoourtemplates.Firstwemustaddanewdirectivesfiletoourblogproject.Wethendefinethenewdirectiveandinjectthedirectiveintoourapplication.Oncethatisdone,wecanreplace<divng-includesrc="'partials/menu.html'"></div>withatagthatusesourcustomdirective.
Openyoureditor,right-clicktheapplicationnode,andcreateanewJavaScriptfilenameddirectives.jsunderthejsfolder.Thecodetoplaceinthefileisshownnext.Wewillwalkthroughthecode,andI’llexplainhowthedirectiveactuallyworks.Wewillthenconfigureourblogapplicationtousethenewdirectiveandseeitinaction:
/*chapter9/directives.js*/
'usestrict';
/*Directives*/
varblogDirectives=
angular.module('blogDirectives',[]);
blogDirectives.directive('blgMenu',function(){
return{
restrict:'A',
templateUrl:'partials/menu.html',
link:function(scope,el,attrs){
scope.label=attrs.menuTitle;
}
};
});
FirstwemustcreateanewmodulenamedblogDirectives.Wewillthencreateanewdirectiveonthatmodule.WepassboththedirectivenameandacallbackfunctiontothedirectivescallontheblogDirectivesmodule.
NamingConventionsforDirectivesTakenoticeofthecamelcasedirectivenameblgMenu.SinceHTMLiscase-insensitive,werefertothenewdirectiveinsideanHTMLtemplatefileasblg-menu.TheAngularJSHTMLcompilerthennormalizesthedirectivenameintoitscamelcaseequivalent,blgMenu.
Alsotakenoticeoftheblgprefixonthenewdirectivename.Alldirectivenamesusedintemplatesmustbeunique.DirectivenamescannotmatchanyexistingHTMLtagname,oranyfutureHTMLtagname.CustomdirectivesalsocannotusethengprefixalreadyusedbyAngularJSdirectives.
So,wemustuseauniquedirectivenamethatwon’tconflictwithcurrentorfutureHTMLnamesorwithAngularJSdirectivenames.Thebestwaytodothatistouseauniquenameprefixforcustomdirectives.Wewilluseblgforourprefixbecauseitisunlikelytocauseaproblemnoworinthefuture.
TheRestrictOptionAlsotakenoticeofthelinerestrict:'A'inourdirective.Thatisknownastherestrictoption.TherestrictoptionishowAngularJStriggersthedirectiveinsideatemplate.Thevalueof"A"causesthedirectivetobetriggeredontheattributename.Thefollowingtableshowsallthepossiblevaluesfortherestrictoption.Thedefaultvaluefortherestrictoptionis'A'.
Table9-1.Restrictoption
Value UsageinAngularJS
'A' Onlymatchtheattributename(<divblg-menu></div>)(default)
'E' Onlymatchtheelementname(<blg-menu></blg-menu>)
'C' Onlymatchtheclassname(<divclass="blg-menu"></div>)
'M' Onlymatchthecommentname(<!--directive:blg-menu-->)
TheTemplateURLAlsonoticetheattributeassignmenttemplateUrl:'partials/menu.html'.ThetemplateUrlattributetellstheAngularJSHTMLcompilertoreplacethedirectiveblg-menuinsideatemplatewithHTMLcontentlocatedinsideaseparatefile.Theblg-menuattributewillbereplacedwiththecontentofouroriginalmenutemplatefile(partials/menu.html).
Thereisonesmallchangethatneedstobemadeinthemenutemplatefiletoallowustopassthesitetitletothedirectiveasanargument.Iwillshowthatchangeshortly.Passingthetitleasanargumentisnotrequiredorevenneeded,butIshowitheretohelpexplainhowdirectiveswork.
TemplateAttributesThefollowingcodeshowshowwepassmenu-titleasanargumenttoournewdirective.Allvaluesarepassedtothemethodnamedlinkasaparameternamedattrs.Wegainaccesstothetitlevaluebyassigningthevalueofattrs.menuTitletoascopeproperty:
/*chapter9/directives.jsexcerpt*/
link:function(scope,el,attrs){
scope.label=attrs.menuTitle;
}
Thescopeispassedasanargumenttothelinkmethodandisaccessibleinsidethemethod,asseenbytheassignmentofthemenuTitleattribute.Directivesareusedinsideatemplateasshownnext,inthemain.htmltemplate.blg-menuisthenameofthedirective,andmenu-titleisthenamepassedtothedirectiveasthetitleattributeofthenewdirective.TheAngularJSHTMLcompileralsonormalizestheattributenameintoitscamelcaseform,soitbecomesmenuTitleinsidethetemplate(asshownbeforeinthetemplatecodefromdirectives.js):
<!--chapter9/main.htmlexcerpt-->
<divblg-menumenu-title="AngularJSBlog"></div>
AddingtheCustomDirectiveNowwemustconfigureourblogapplicationtousethenewlycreatedcustomdirective.Toloadthenewdirectivesfile,weneedtoaddonelineintheindex.htmlfile:
<!--chapter9/index.htmlexcerpt-->
<scriptsrc="js/directives.js"></script>
Thecompleteindex.htmlfileisshownhereforconvenience:
<!--chapter9/index.html-->
<!DOCTYPEhtml>
<htmllang="en"ng-app="blogApp">
<head>
<title>AngularJSBlog</title>
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<metahttp-equiv="Content-Type"content="text/html;charset=UTF-8">
<linkrel="stylesheet"href="lib-css/bootstrap.min.css"media="screen"/>
<linkrel="stylesheet"href="css/styles.css"media="screen"/>
<scriptsrc="js/libs/jquery-1.10.2.min.js"></script>
<scriptsrc="js/libs/bootstrap.min.js"></script>
<scriptsrc="js/libs/angular.min.js"></script>
<scriptsrc="js/libs/angular-route.min.js"></script>
<scriptsrc="js/libs/angular-resource.min.js"></script>
<scriptsrc="js/libs/angular-cookies.min.js"></script>
<scriptsrc="js/app.js"></script>
<scriptsrc="js/controllers.js"></script>
<scriptsrc="js/services.js"></script>
<scriptsrc="js/businessServices.js"></script>
<scriptsrc="js/directives.js"></script>
</head>
<body>
<divng-view></div>
</body>
</html>
Wealsoneedtomakeachangetotheapp.jsfile.WeaddthenewblogDirectivesmoduleasadependencyoftheapplicationatstartuptime,usinginlinearrayannotations:
/*chapter9/app.js*/
'usestrict';
/*AppModule*/
varblogApp=angular.module('blogApp',[
'ngRoute',
'blogControllers',
'blogServices',
'blogBusinessServices',
'blogDirectives'
]);
blogApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'BlogCtrl'
}).when('/blogPost/:id',{
templateUrl:'partials/blogPost.html',
controller:'BlogViewCtrl'
});
$locationProvider.html5Mode(false).hashPrefix('!');
}]);
Nowwemustmodifyourtemplatefilestousethenewlycreatedcustomdirective.Inthemain.htmltemplatefile,wereplacetheline<divng-includesrc="'partials/menu.html'"></div>withthelineshownhere:
<!--chapter9/main.htmlexcerpt-->
<divblg-menumenu-title="AngularJSBlog"></div>
Thecompletemain.htmlfileisshownhereforconvenience:
<!--chapter9/main.html-->
<divblg-menumenu-title="AngularJSBlog"></div>
<divid="container"class="container">
<divclass="blog-post-label">BlogPosts</div>
<divclass="post-wrapper">
<divng-repeat="blogPostinblogList">
<divclass="blog-post-outer">
<divclass="blog-intro-text">
Posted:{{blogPost.date|date:'MM/dd/yyyy@h:mma'}}
</div>
<divclass="blog-intro-text">
{{blogPost.introText}}
</div>
<divclass="blog-read-more">
<ahref="#!blogPost/{{blogPost._id}}">ReadMore</a>
</div>
</div>
</div>
</div>
</div>
WemakethesamechangetotheblogPost.htmltemplate,asshownhere:
<!--chapter9/blogPost.html-->
<divblg-menumenu-title="AngularJSBlog"></div>
<divid="container"class="container">
<divclass="blog-post-label">BlogEntry</div>
<divclass="blog-entry-wrapper">
<divclass="blog-intro-text">
Posted:{{blogEntry.date|date:'MM/dd/yyyy@h:mma'}}
</div>
<divclass="blog-entry-outer">
{{blogEntry.blogText}}
</div>
<divclass="blog-comment-wrapper">
<divclass="blog-comment-label">BlogComments</div>
<divclass="blog-entry-comments"ng-repeat="commentinblogEntry.comments">
{{comment.commentText}}
</div>
</div>
</div>
</div>
PassingtheTitleAttributeFinally,wemustmakeonelastchangetothemenu.htmltemplatefiletomakeuseofthetitlevaluepassedtothedirectiveinthemenu-titleattribute.Replacethehardcodedtitlewith{{label}},asshownhere:
<!--chapter9/menu.html-->
<navclass="navbarnavbar-inversenavbar-fixed-top"role="navigation">
<!--Brandandtogglegetgroupedforbettermobiledisplay-->
<divclass="container">
<divclass="navbar-header">
<buttontype="button"
class="navbar-toggle"data-
toggle="collapse"
data-target=".navbar-collapse">
<spanclass="sr-only">Togglenavigation</span>
<spanclass="icon-bar"></span>
<spanclass="icon-bar"></span>
<spanclass="icon-bar"></span>
</button>
<aclass="navbar-brand"style="{{brandColor}}"href="#!/">{{label}}</a>
</div>
<!--Collectthenavlinks,forms,andothercontentfortoggling-->
<divclass="collapsenavbar-collapse">
<ulclass="navnavbar-nav">
<liclass="{{aboutActiveClass}}"><ahref="#!about">About</a></li>
<liclass="">
<ahref="https://github.com/KenWilliamson">DownloadProjectCode</a>
</li>
</ul>
</div><!--/.navbar-collapse-->
</div>
</nav>
Withthischangemade,wecanruntheapplicationandtestournewmenu.
RunningtheBlogApplicationNowwewillrunourblogprojecttocheckthatallchangesweremadesuccessfully.Saveallyourchangesandright-clicktheprojectnode.Select“Run”fromthemenu,andtheapplicationshouldlaunch.Ifallchangesweremadecorrectly,youshouldseethemenubaracrossthetopofthepagejustasbefore.
Turnondevelopertoolsforyourbrowserandcheckforanyerrors.Ifyouhaveanyproblems,gooverwhatwecoveredandvalidatethatallthechangesweremadecorrectly.Ifyouhaveissuesthatcan’tberesolved,downloadthecodeforChapter9fromtheprojectsite.Runthedownloadedprojecttoseethechangesmadeinthischapter,andcompareittoyourcodetofindandfixanyissues.
TestingDirectiveswithKarmaWritingatestspecificationforadirectivethatusesanexternalHTMLtemplatefileisabitmorecomplicatedthanwritingmosttestspecifications.ThetestscriptwillfailwhenittriestoloadthetemplatefileusingHTTPfromtheserver.IfyouweretousehardcodedHTMLforthemenuinsidethedirective,everythingwouldworkfine.NotsowithexternalHTMLtemplates,however.
OnewayaroundtheproblemistouseapreprocessorthatconvertsourHTMLtemplatefileintoaJavaScriptstringandthengeneratesanAngularJSmodulefromthatstring.Thepreprocessedmoduleisthenloadedintothe$templateCacheandmadeavailabletoKarma.Thatwaywecanusethecachedversionofourtemplatefileandourdirectiveworksasexpected.
Onewaytohandlethepreprocessingistousethekarma-ng-html2js-preprocessorKarmaplugin.Althoughthepluginisabittrickytoconfigureproperly,itquicklysolvestheexternaltemplateproblem.Payparticularattentiontothewaythepluginisconfigured.IfyouareusinganIDEotherthanNetBeans,youmayneedtolookfordocumentationspecifictoyourIDE.
KarmaConfigurationFirst,weneedtoeditthepackage.jsonfileusedtoconfigureNode.jsdependencies.Hereistheneededchange:
/*chapter9/package.jsonexcerpt*/
"karma-ng-html2js-preprocessor":"~0.1"
Thecompletepackage.jsonfileisshownnext.Theaddedlinemakesthekarma-ng-html2js-preprocessorpluginaNode.jsdependency.ThemoduleisthenaccessibletoKarma.Edittheexistingblogprojectpackage.jsonfileandaddtherequiredlineasshown:
{
"name":"package.json",
"devDependencies":{
"karma":"*",
"karma-chrome-launcher":"*",
"karma-firefox-launcher":"*",
"karma-jasmine":"*",
"karma-junit-reporter":"*",
"karma-coverage":"*",
"karma-ng-html2js-preprocessor":"~0.1"
}
}
Afterwechangethepackage.jsonfile,weneedtousenpmtoinstalltheplugin.
OpenanewcommandwindowandnavigatetotherootoftheChapter9project.Youshouldseethepackage.jsonfilewhenyoulistoutthefilesinthefolder.
Nowtypethefollowingcommandtoinstallthekarma-ng-html2js-preprocessorplugindefinedinthepackage.jsonfile:
npminstall
Weneedtomakeseveralchangestothekarma.conf.jsfilethatwecreatedearlier.Thechangesareconfigurationchangesforthenewpluginjustinstalled;theyaresubtlebutimportant.
First,noticeinthefollowingcodethatwe’veaddedanewlineinthefilessection.Thenewline,'public_html/partials/*.html',tellsthepluginwheretofindthetemplatefileusedinourdirective:
/*chapter9/karma.conf.jsexcerpt*/
files:[
"public_html/js/libs/angular.min.js",
"public_html/js/libs/angular-mocks.js",
"public_html/js/libs/angular-route.min.js",
"public_html/js/libs/angular-resource.min.js",
"public_html/js/libs/angular-cookies.min.js",
"public_html/js/*.js",
"public_html/partials/*.html",
"test/**/*Spec.js"
]
Wemustalsoaddapreprocessorssectiontothefile.TheentryinthissectionmapsthelocationofthetemplatefilestothenewKarmaplugin:
/*chapter9/karma.conf.jsexcerpt*/
preprocessors:{
'public_html/partials/*.html':['ng-html2js']
}
Next,weneedtoaddthenewplugintothelistofKarmaplugins,asshownhere—thelastlinetellsKarmathatthispluginwillbeused:
/*chapter9/karma.conf.jsexcerpt*/
plugins:[
"karma-junit-reporter",
"karma-chrome-launcher",
"karma-firefox-launcher",
"karma-jasmine",
"karma-ng-html2js-preprocessor"
]
Thereisonemorechangethatweneedtomaketothekarma.conf.jsfile.Weneedtotellthenewplugintostrip"public_html/"fromthepathtothetemplatefiles:
/*chapter9/karma.conf.jsexcerpt*/
ngHtml2JsPreprocessor:{
stripPrefix:'public_html/'
}
Followingisthecompletemodifiedkarma.conf.jsfile.Openthekarma.conf.jsfileintheblogprojectandmaketheneededchanges:
/*chapter9/karma.conf.jscompletefile*/
module.exports=function(config){
config.set({
basePath:'../',
files:[
"public_html/js/libs/angular.min.js",
"public_html/js/libs/angular-mocks.js",
"public_html/js/libs/angular-route.min.js",
"public_html/js/libs/angular-resource.min.js",
"public_html/js/libs/angular-cookies.min.js",
"public_html/js/*.js",
"public_html/partials/*.html",
"test/**/*Spec.js"
],
preprocessors:{
'public_html/partials/*.html':['ng-html2js']
},
exclude:[
],
autoWatch:true,
frameworks:[
"jasmine"
],
browsers:[
"Chrome",
"Firefox"
],
plugins:[
"karma-junit-reporter",
"karma-chrome-launcher",
"karma-firefox-launcher",
"karma-jasmine",
"karma-ng-html2js-preprocessor"
],
ngHtml2JsPreprocessor:{
stripPrefix:'public_html/'
}
});
};
KarmaTestSpecificationNowweneedtoaddanewtestspecificationtotheblogproject.Dothefollowing:
1. Right-clicktheunitfolderunderthetestfolderandaddanewJavaScriptfilenameddirectivesSpec.jstotheproject.
2. CopythiscodeintothenewdirectivesSpec.jsfile:
/*chapter9/directivesSpec.js*/
describe('AngularJSBlogApplication',function(){
beforeEach(module('blogDirectives'));
describe('UnittestofMenuDirective',function(){
varrootScope,compile;
//TheexternaltemplatefilereferencedbytemplateUrl
beforeEach(module('partials/menu.html'));
beforeEach(inject(function(_$compile_,_$rootScope_){
compile=_$compile_;
rootScope=_$rootScope_;
}));
it('Replacesthemenuattributewiththemenu',function(){
varelm=angular.element(
"<divblg-menumenu-title=\"AngularJSBlog\"></div>");
varmenu=compile(elm)(rootScope);
rootScope.$digest();
expect(menu.html()).toContain("AngularJSBlog");
});
});
});
Thiscodediffersabitfromthetestspecificationsthatwehaveseensofar.RememberthatdirectivesneedtobecompiledbytheHTMLcompiler.Thetestspecificationaccountsforthatneed.
First,noticeinthelineshownherethatweloadtheAngularJSmodulethatrepresentsthetemplateHTMLfilethatisneededbythedirective.RememberthatthetemplateHTMLfilewasconvertedtoaJavaScriptstring,andthenthatstringwasusedbytheKarmapreprocessorplugintogenerateanAngularJSmodule:
/*chapter9/directivesSpec.jsexcerpt*/
//TheexternaltemplatefilereferencedbytemplateUrl
beforeEach(module('partials/menu.html'));
AlsonoticethatwenowinjecttheHTMLcompilerwith_$compile_.WealsoinjecttherootScopewith_$rootScope_:
/*chapter9/directivesSpec.jsexcerpt*/
beforeEach(inject(function(_$compile_,_$rootScope_){
compile=_$compile_;
rootScope=_$rootScope_;
}));
Recallthatwhenweincludedournewdirectiveinsidethemain.htmltemplate,weusedtheline<divblg-menumenu-title="AngularJSBlog"\></div>toincludethenewdirective-basedmenuintothepage.Thefollowingcodeshowsthatsamelinegettingpassedtotheangular.elementmethod:
/*chapter9/directivesSpec.jsexcerpt*/
varelm=angular.
element("<divblg-menumenu-title=\"AngularJSBlog\"></div>");
varmenu=compile(elm)(rootScope);
rootScope.$digest();
Theresultingelmvariableisthenpassedtothecompileralongwiththerootscopereference,asshownhere.Thenwecall$digest,andthattellsAngularJStoupdatebindingsandfireanywatches.
Finally,weevaluatetheHTMLbycallingthemenu.htmlmethodandlookingforthetitlethatwepassedtothedirectivewithmenu-title="AngularJSBlog".
KarmaTestingNow,withallthechangesmadetotheblogproject,wearereadytotestournewdirective.
Right-clicktheprojectandselect“Test”fromthemenu.Karmawillstart.YoushouldseebothChromeandFirefoxbrowserwindowsopen.TheNetBeanstestresultswindowshouldopenanddisplay10passedtestsforChromeand10passedtestsforFirefox.
Ifyougetanyerrormessagesorfailedtests,gobackoverthissectionandverifythatyoucompletedalltheconfigurationsandinstallations.YoucanalsodownloadtheChapter9codefromtheGitHubprojectsite.
End-to-EndTestingWewillmakeonesmallchangetoallowustotestthenewdirective-basedmenuduringend-to-endtesting.ThemodificationwillinvolveourProtractortestscriptclickingthemainmenulinkafternavigatingtoablogentry.
ProtractorConfigurationWealreadycreatedaProtractorconfigurationfilefortheblogapplicationinChapter5.TheProtractorconfigurationfileisshownhereforreference:
/*chapter9/conf.jsProtractorconfigurationfile*/
exports.config={
seleniumAddress:'http://localhost:4444/wd/hub',
specs:['e2e/blog-spec.js']
};
ProtractorTestSpecificationWewillmakeasmallchangetothetestspecification,shownnext.Noticethelastlineinthefile.Thelineusesthenavbar-brandCSSclasstolookupthelinktothemainpage.Thescriptthenclicksthelinkandnavigatesbacktothemainpage.Thetestvalidatesthatthenewmenuisworkingcorrectly:
/*chapter9/blog-spec.js*/
describe("BlogApplicationTest",function(){
it("shouldtestthemainblogpage",function(){
browser.get("http://localhost:8383/AngularJsBlogChapter9/");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthebloglist
varblogList=element.all(by.repeater('blogPostinblogList'));
//teststhesizeoftheblogList
expect(blogList.count()).toEqual(1);
browser.get(
"http://localhost:8383/AngularJsBlogChapter9/
#!/blogPost/5394e59c4f50850000e6b7ea");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthecommentlist
varcommentList=
element.all(by.repeater('commentinblogEntry.comments'));
//checksthesizeofthecommentList
expect(commentList.count()).toEqual(2);
element(by.css('.navbar-brand')).click();
});
});
ProtractorTestingWiththosechangesmade,wearereadytostarttheend-to-endtesting.
Startanewcommandwindowandenterthiscommandtostartthetestserver:
webdriver-managerstart
OpenanewcommandwindowandnavigatetotherootoftheChapter9project.Typethecommand:
protractortest/conf.js
Youshouldseeabrowserwindowopen.Youshouldthenseethetestscriptnavigatethroughthepagesoftheblogapplication.WhentheProtractorscripthasfinished,thebrowserwindowwillclose.
YoushouldseeresultslikethefollowinginthecommandwindowwhentheProtractorscriptcompletes.Thenumberofsecondsthatittakesthescripttofinishwillvarydependingonyourparticularsystem:
Finishedin1.91seconds
1test,4assertions,0failures
ConclusionInthischapteryoulearnedhowtocreateacustomAngularJSdirective.YoualsolearnedhowtowritetestspecificationsforAngularJSdirectives.Wemadealltheneededchangestoourblogapplicationtoaddanewdirective-basedmenutoourblog.
Onceyourblogapplicationisrunningcorrectly,wecanmoveon.Thisconcludesourdiscussionofdirectives;we’llstartaddingsecurityfeaturestoourblogapplicationinthenextchapter.
Chapter10.AngularJSSecurity
YoumightwonderwhywearecoveringsecurityinabookonAngularJS.Well,quitesimply,securityisoneofthemostimportantandmostchallengingtasksfacedbyanAngularJSdeveloper.It’snotthatthedeveloperisactuallyresponsibleforimplementingthesecuritylayer—thatisnotthecaseatall—butitisveryimportantforanAngularJSdevelopertounderstandtherolethatAngularJSplaysintheoverallsecuritymodelofanapplicationorwebsite.
Youshouldneverattempttoimplementanindependentclient-sidesecuritylayerinanAngularJSapplication,oranyotherJavaScriptapplicationforthatmatter.Securityshouldalwaysbeimplementedonthebackendserviceswherethedataresides.Thatistheonlysafeplacetoimplementasecuritylayer.
RemembertheuserhasfullaccesstotheJavaScriptrunninginthebrowser.AsIsaidbefore,ourAngularJSapplicationrunsintheuser’sbrowserontheuser’shardware.TheusercansavetheJavaScriptlocallyandeasilymakemodificationscircumventinganysecuritylayerimplementedbyanunsuspectingJavaScriptdeveloper.
Withthatinmind,thereareseveralrulesthatAngularJSdevelopersandbackenddevelopersneedtoremember.AlthoughactuallyimplementingthesecuritylayerisnotusuallythejobofanAngularJSdeveloper,itisoftenacollaborativeeffortforalldevelopersinvolvedinaproject.Thefollowingrulesshouldalwaysbeconsidered:
1. AlwaysuseSSLtocommunicatewithRESTservicesthatcontainprivatedata(HTTPS).
2. AlwaysusesometypeofauthenticationoneachRESTservicecallthatcontainsprivatedata(BasicAuthentication,forexample).
3. NeverholdRESTserviceauthenticationstatusinasessionvariableontheserver.Doingthatopensyourserver-sideapplicationuptocross-originattacksandotherserioussecurityconcerns.
4. NeverimplementaCross-OriginResourceSharing(CORS)layerthatreturns*asthelistofalloweddomains.Forexample,(Access-Control-Allow-Origin:*)wouldallowalldomainstomakecross-origincallstotheRESTservicesonthesite.Doingthatcircumventsthebrowser’sCORSsecurityimplementationcompletely.
5. AlwaysmakesurethatanyJavaScriptthatmaygetinjectedinsideaJSONpropertydoesnotgetexecutedontheserverside.ThisdesignflawisatthecoreoftheNoSQLinjectionattack,whereJavaScriptfunctionsareinjectedintheJSONrequestofaserviceandunknowinglyexecutedbytheserver,inordertobreachthesecurityofaNoSQLdatabase.
Alwaysrememberthatanysecurity-relatedJavaScriptcodecanbeviewedandmodifiedbytheuser.Whilemostmodernbrowsersdoofferbuilt-insecurity,JavaScriptdevelopersshouldneverrelyonthebrowserforsecurity.Theresponsibilityforsecurityrestsentirelyontheshouldersofthebackendservicedevelopers.Withthatsaid,IwillshowsometechniquesfordevelopingAngularJSapplicationsthatworkwellwithasecuritylayerimplementedproperlyinthebackendservices.
AuthenticationWewillstartourdiscussionofsecuritybybuildingaloginscreenandtheassociatedcontrollerandserviceforourblogapplication.Wewillsendtheuser’scredentialstoaloginRESTserviceforvalidation.WewillalsomakeuseofthebusinesslogicservicesthatwedevelopedbackinChapter8.
Wedon’tactuallyuseHTTPSforourblogapplicationbecauseit’snotaproductionapplication.Butinaproductionenvironment,SSLshouldalwaysbeusedtoprotectprivatedataandtheuser’scredentialswhencallingaloginRESTservice.AdditionalsecuritystepscouldevenbetakenintheRESTservicestolimitaccesstoaparticularmachineoraparticularIPaddress.Wewillnot,however,beconcernedwiththatlevelofsecurityinthisbook.
AddingaLoginServiceWewillstartoffbyaddinganAngularJSloginservice.Openyoureditorandaddthefollowingcodetothebottomofyourproject’sservices.jsfile.ThenewAngularJSloginservicemapstoaloginRESTserviceonourbackendserver.ThecodeismuchlikethatoftheotherAngularJSserviceswe’vesetupsofar.Ithasonemethod,login,thatmapstoaPOSTmethodontheRESTservice:
/*chapter10/services.jsexcerpt*/
blogServices.factory('Login',['$resource',
function($resource){
return
$resource(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/login",
{},{
login:{method:'POST',cache:false,isArray:false}
});
}]);
AddingaLoginControllerNowweneedtoaddalogincontroller.Openyoureditorandaddthecodeshownnexttothebottomofthecontrollers.jsfile.NoticethatweinjectthenewLoginserviceandthesetCredsbusinesslogicservicethatwedevelopedbackinChapter8.Wealsoinjectthe$locationservicetoallowustoredirecttheuseronceauthenticated.Thenewcontrollerhasasubmitmethodthatisattachedtothescope.Attachingthemethodtothescopeallowsustocallthemethodfrominsidethelogintemplate.WebuildtheJSONrequestthatgetspassedtotheserviceinthevariablenamedpostData,usingthescopepropertiessubmittedbytheform:
/*chapter10/controllers.jsexcerpt*/
blogControllers.controller('LoginCtrl',
['$scope','$location','Login','setCreds',
functionLoginCtrl($scope,$location,Login,setCreds){
$scope.submit=function(){
$scope.sub=true;
varpostData={
"username":$scope.username,
"password":$scope.password
};
Login.login({},postData,
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
if(response.authenticated){
setCreds($scope.username,$scope.password)
$location.path('/');
}else{
$scope.error="LoginFailed"
}
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
}
);
};
}]);
Wealsoaddascopepropertynamederror.Thispropertyispopulatedanytimetheuserfailstoauthenticate,displayinga“LoginFailed”message.Wewillseehowtheerrorispresentedlaterinthechapter.Oncetheuserauthenticates,wemakeacalltotheAngularJSbusinesslogicservicesetCredsandpasstheuser’susernameandpasswordtobesavedinacookie.Wethenredirecttheusertothemainapplicationlink.
SecurityModificationstoOtherControllersWemustalsomakeminormodificationstotheothertwocontrollersinourblogproject.Openyoureditorandreplacethetwocontrollersaddedearlierwiththecodeshownnext.Noticewenowinjectthe$locationserviceandthecheckCredsbusinessservicethatweaddedbackinChapter8.ThecheckCredsserviceworksbycheckingtheuser’scredentialsatthetopofthecontroller.Iftheuserhasnotauthenticated,acallismadetothepathmethodonthe$locationservicetoredirecttheusertotheloginpage(wewillcoverthenewloginpathshortly):
/*chapter10/controllers.jsexcerpt*/
blogControllers.controller('BlogCtrl',
['$scope','BlogList','$location','checkCreds',
functionBlogCtrl($scope,BlogList,$location,checkCreds){
if(!checkCreds()){
$location.path('/login');
}
BlogList.get({},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.blogList=response;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
}
);
}]);
blogControllers.controller('BlogViewCtrl',
['$scope','$routeParams','BlogPost','$location','checkCreds',
functionBlogViewCtrl($scope,$routeParams,BlogPost,
$location,checkCreds){
if(!checkCreds()){
$location.path('/login');
}
varblogId=$routeParams.id;
BlogPost.get({id:blogId},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.blogEntry=response;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
}
);
}]);
AddingaLogoutControllerWehaveonemorechangetomaketothecontrollers.jsfile:weneedtoaddanewcontrollertologtheuseroutofthesystemandresethiscredentials.Addthecodeshownheretothebottomofthecontrollers.jsfile.Onceagain,wemakeuseoftheAngularJSbusinesslogicserviceswrittenbackinChapter8byaddingacalltothedeleteCredsservice.Theservicecallremovestheuser’scredentials,andthenweredirecttheusertotheloginpage:
/*chapter10/controllers.jsexcerpt*/
blogControllers.controller('LogoutCtrl',
['$location','deleteCreds',
functionLogoutCtrl($location,deleteCreds){
deleteCreds();
$location.path('/login');
}]);
Theentirecontrollers.jsfileisshownheretohelpmakethechangesclearer:
/*chapter10/controllers.js*/
'usestrict';
/*Controllers*/
varblogControllers=
angular.module('blogControllers',[]);
blogControllers.controller('BlogCtrl',
['$scope','BlogList','$location','checkCreds',
functionBlogCtrl($scope,BlogList,$location,checkCreds){
if(!checkCreds()){
$location.path('/login');
}
BlogList.get({},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.blogList=response;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
});
}]);
blogControllers.controller('BlogViewCtrl',
['$scope','$routeParams','BlogPost','$location','checkCreds',
functionBlogViewCtrl($scope,$routeParams,BlogPost,
$location,checkCreds){
if(!checkCreds()){
$location.path('/login');
}
varblogId=$routeParams.id;
BlogPost.get({id:blogId},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.blogEntry=response;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
});
}]);
blogControllers.controller('LoginCtrl',
['$scope','$location','Login','setCreds',
functionLoginCtrl($scope,$location,Login,setCreds){
$scope.submit=function(){
$scope.sub=true;
varpostData={
"username":$scope.username,
"password":$scope.password
};
Login.login({},postData,
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
if(response.authenticated){
setCreds($scope.username,$scope.password)
$location.path('/');
}else{
$scope.error="LoginFailed"
}
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
});
};
}]);
blogControllers.controller('LogoutCtrl',
['$location','deleteCreds',
functionLogoutCtrl($location,deleteCreds){
deleteCreds();
$location.path('/login');
}]);
Next,wewilladdedanewlogintemplateandtheassociatedCSS.Wewillthenaddtwonewpathstothe$routeProvidersectionoftheapp.jsfile.
AddingaLoginTemplateRight-clicktheprojectnodeandaddanewHTMLfiletothepartialsfolder.Namethenewfilelogin.html.Replacethecontentofthenewlycreatedfilewiththecodeshownhere.Noticethatweusetheng-submitdirectivetoconnectthesubmitmethodinourLoginCtrltotheformforformsubmission:
<!--chapter10/login.html-->
<divclass="blog-login-wrapper">
<formclass=""ng-submit="submit()"ng-controller="LoginCtrl">
<divclass="blog-login-error">{{error}}</div>
<divclass="blog-login-label">
<labelfor="username">Username:</label></div>
<divclass="blog-login-element">
<inputtype="text"ng-model="username"name="username"
placeholder="username"required/></div>
<divclass="blog-login-label">
<labelfor="password">Password:</label></div>
<divclass="blog-login-element">
<inputtype="password"ng-model="password"name="password"
placeholder="password"required/></div>
<divclass="blog-login-button">
<buttontype="submit"class="form-button">Signin</button></div>
</form>
</div>
NowopentheCSSfilestyles.cssinyoureditorandaddthefollowingcodetothebottomofthefile.NoticethatweuseCSS3mediaquerieslike@mediascreenand(min-width:1200px)tomakeourlogintemplateberesponsiveandlookgoodonanymobileordesktopplatform:
/*chapter10/styles.css*/
.blog-login-wrapper{
float:left;
background:#e0e0e0;
border-radius:6px;
-moz-border-radius:6px;/*Firefox3.6andearlier*/
border:darkgreensolid1px;
}
@mediascreenand(min-width:1200px){
.blog-login-wrapper{
width:40%;
margin:10%0030%;
padding:1%;
background:#e0e0e0;
border-radius:6px;
-moz-border-radius:6px;/*Firefox3.6andearlier*/
border:darkgreensolid1px;
}
}
@mediascreenand(max-width:1200px){
.blog-login-wrapper{
width:40%;
margin:10%0030%;
padding:1%;
background:#e0e0e0;
border-radius:6px;
-moz-border-radius:6px;/*Firefox3.6andearlier*/
border:darkgreensolid1px;
}
}
@mediascreenand(max-width:600px){
.blog-login-wrapper{
width:80%;
margin:10%0010%;
padding:1%;
background:#e0e0e0;
border-radius:6px;
-moz-border-radius:6px;/*Firefox3.6andearlier*/
border:darkgreensolid1px;
}
}
.blog-login-label{
float:left;
width:70%;
margin:00015%;
padding:1%000;
text-align:center;
}
.blog-login-element{
float:left;
width:70%;
margin:00015%;
padding:1%000;
text-align:center;
}
.blog-login-button{
float:left;
width:100%;
margin:0000;
padding:5%000;
text-align:center;
}
.blog-login-error{
float:left;
width:100%;
margin:0000;
padding:0000;
text-align:center;
color:red;
}
AddingNewRoutesNowweneedtoaddthetwonewroutestoourrouteproviderintheapp.jsfile.Thefollowingcodeshowsthechangesneededforthisfile.Asyoucansee,thetwonewroutesmakeuseofthetwonewcontrollersandthenewtemplatefile:
/*chapter10/app.js*/
'usestrict';
/*AppModule*/
varblogApp=angular.module('blogApp',[
'ngRoute',
'blogControllers',
'blogServices',
'blogBusinessServices',
'blogDirectives'
]);
blogApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'BlogCtrl'
}).when('/blogPost/:id',{
templateUrl:'partials/blogPost.html',
controller:'BlogViewCtrl'
}).when('/login',{
templateUrl:'partials/login.html',
controller:'LoginCtrl'
}).when('/logOut',{
templateUrl:'partials/login.html',
controller:'LogoutCtrl'
});
$locationProvider.html5Mode(false).hashPrefix('!');
}]);
AddingaLogoutLinkFinally,weneedtomakeonemorechangetoourblogapplication:weneedtomodifythemenu.htmlfileandaddthenew“Logout”menulink.Hereisthelineyou’llneedtoaddtothemenu.htmlfile.Thenewlogoutlinkmapstothelogoutroutethatwejustadded:
<!--chapter10/menu.htmlexcerpt-->
<li><aid="lo"href="#!logOut">Logout</a></li>
Thecompletemenu.htmlfileisshownhereforconvenience:
<!--chapter10/menu.htmlcompletefile-->
<navclass="navbarnavbar-inversenavbar-fixed-top"role="navigation">
<!--Brandandtogglegetgroupedforbettermobiledisplay-->
<divclass="container">
<divclass="navbar-header">
<buttontype="button"class="navbar-toggle"data-toggle="collapse"
data-target=".navbar-collapse">
<spanclass="sr-only">Togglenavigation</span>
<spanclass="icon-bar"></span>
<spanclass="icon-bar"></span>
<spanclass="icon-bar"></span>
</button>
<aclass="navbar-brand"style="{{brandColor}}"href="#!/">{{label}}</a>
</div>
<!--Collectthenavlinks,forms,andothercontentfortoggling-->
<divclass="collapsenavbar-collapse">
<ulclass="navnavbar-nav">
<liclass="{{aboutActiveClass}}"><ahref="#!about">About</a></li>
<liclass="">
<ahref="https://github.com/KenWilliamson">DownloadProjectCode</a>
</li>
<li><aid="lo"href="#!logOut">Logout</a></li>
</ul>
</div><!--/.navbar-collapse-->
</div>
</nav>
Onceyouhavemadeallthechangesoutlinedinthischapter,yourblogapplicationshouldhavealltheneededsecurityadditionsthatwerespecified.Totestthechangesthatweremade,wewillruntheprojectandcheckforerrors.
RunningtheBlogApplicationRight-clicktheprojectnodeandselect“Run”fromthemenu.YourprojectshouldrunandyoushouldseethescreeninFigure10-1.Ifyoudonotseetheloginscreen,checkthatallthechangesoutlinedinthischapterwereperformedcorrectly.Turnondevelopertoolsforyourbrowserandlookforerrors,asdescribedinpreviouschapters.
Figure10-1.Theloginscreen
LoggingInOnceyourprojectisrunning,dothefollowing:
1. Enter“node”astheusername.
2. Enter“password”asthepassword.
3. Clickthe“Signin”button.
Youshouldnowseethesameblogscreensthatyoubuiltinthepreviouschapters.Theapplicationshouldfunctionjustasbeforewithnochanges.Navigatethroughtheapplicationtovalidatethateverythingworkscorrectly.
Ifyouweretoenterincorrectusercredentials,youwouldseetheerrormessagedescribedearlier(“LoginFailed”)displayedinred.Noticethenewmenuitem“Logout”attherightendofthemenubar.Click“Logout”andyoursessionshouldend.Youshouldthenbetakenbacktotheloginscreen.Iftheloginandlogoutprocessworkcorrectly,yoursecuritychangeswereimplementedsuccessfully.
TestingwithKarmaWe’veaddedanewAngularJSserviceandtwonewcontrollerstoourblogapplication.Wenowneedtotesttheapplicationtomakecertaintherearenodefectsinourcode.Wealsoneedtovalidatethatallpreviousunittestsarestillpassing.
Wewillstartoffbywritingatestspecificationforthenewservice.Wewillthenwritetwonewtestspecificationsforthetwonewcontrollers.Onceourunittestingiscomplete,wewillmakechangestoourend-to-endtesting.
KarmaConfigurationWealreadyhaveanup-to-dateKarmaconfigurationfileforourblogproject.Thereshouldbenochangestothefileatthispoint.Thecompletekarma.conf.jsfileisshownhereforreference:
/*chapter10/karma.conf.js*/
module.exports=function(config){
config.set({
basePath:'../',
files:[
"public_html/js/libs/angular.min.js",
"public_html/js/libs/angular-mocks.js",
"public_html/js/libs/angular-route.min.js",
"public_html/js/libs/angular-resource.min.js",
"public_html/js/libs/angular-cookies.min.js",
"public_html/js/*.js",
"public_html/partials/*.html",
"test/**/*Spec.js"
],
preprocessors:{
'public_html/partials/*.html':['ng-html2js']
},
exclude:[
],
autoWatch:true,
frameworks:[
"jasmine"
],
browsers:[
"Chrome",
"Firefox"
],
plugins:[
"karma-junit-reporter",
"karma-chrome-launcher",
"karma-firefox-launcher",
"karma-jasmine",
"karma-ng-html2js-preprocessor"
],
ngHtml2JsPreprocessor:{
stripPrefix:'public_html/'
});
};
KarmaTestSpecificationsWeneedtoaddunittestspecificationsforthenewLoginserviceandthetwonewcontrollers.ThefollowingcodeshowsthenewtestspecificationfortheLoginservice.TheservicereliesonaRESTservice,sowewillonlytesttomakesurewecaninjecttheservice.WewillactuallytesttheserviceinteractionwiththeRESTserviceduringend-to-endtesting.Ifthereareanyissues,wewillfindthemthere.Addthistestspecificationtotheproject’sservicesSpec.jsfile:
/*chapter10/servicesSpec.jsexcerpt*/
describe('testLogin',function(){
var$rootScope;
varlogin;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
login=$injector.get('Login');
}));
it('shouldtestLoginservice',function(){
expect(login).toBeDefined();
});
});
ThecompleteservicesSpec.jsfileisshownhere:
/*chapter10/servicesSpec.jscompletefile*/
describe('AngularJSBlogServiceTesting',function(){
describe('testBlogList',function(){
var$rootScope;
varblogList;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
blogList=$injector.get('BlogList');
}));
it('shouldtestBlogListservice',function(){
expect(blogList).toBeDefined();
});
});
describe('testBlogPost',function(){
var$rootScope;
varblogPost;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
blogPost=$injector.get('BlogPost');
}));
it('shouldtestBlogPostservice',function(){
expect(blogPost).toBeDefined();
});
});
describe('testLogin',function(){
var$rootScope;
varlogin;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
login=$injector.get('Login');
}));
it('shouldtestLoginservice',function(){
expect(login).toBeDefined();
});
});
});
Nowweneedtestspecificationsforthetwonewcontrollers.FirstweshowthetestspecificationfortheLoginCtrlcontroller.Wefirstgetareferencetothecontrollerandthencallthesubmitmethodattachedtothescope.Weuseascopepropertytovalidatethatthemethodcallwassuccessful:
/*chapter10/controllerSpec.jsexcerpt*/
describe('LoginCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('LoginCtrl',{$scope:scope});
scope.submit();
}));
it('shouldshowsubmitsuccess',function(){
console.log("LoginCtrl:"+scope.sub);
expect(scope.sub).toEqual(true);
});
});
NextisthetestspecificationfortheLogoutCtrlcontroller.Inthiscase,wejustvalidatethatwecangetareferencetothecontroller.Wewillvalidatethatthecontrolleractuallyhandleslogoutcorrectlywhenwedoend-to-endtesting:
/*chapter10/controllerSpec.jsexcerpt*/
describe('LogoutCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('LogoutCtrl',{$scope:scope});
}));
it('shouldcreateLogoutCtrlcontroller',function(){
console.log("LogoutCtrl:"+ctrl);
expect(ctrl).toBeDefined();
//expect(scope.blogList).toBeUndefined();
});
});
ThecompletecontrollerSpec.jsfileisshownnext.Makethechangestoyourfileintheblogapplicationandvalidatethatitmatchestheversionshownhere:
/*chapter10/controllerSpec.jscompletefile*/
describe('AngularJSBlogApplication',function(){
beforeEach(module('blogApp'));
//beforeEach(module('blogServices'));
describe('BlogCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('BlogCtrl',{$scope:scope});
}));
it('shouldcreateshowblogentrycount',function(){
console.log("blogList:"+scope.blogList);
expect(scope.blogList.length).toEqual(0);
//expect(scope.blogList).toBeUndefined();
});
});
describe('BlogViewCtrl',function(){
varscope,ctrl,$httpBackend;
beforeEach(inject(function(_$httpBackend_,$routeParams,
$rootScope,$controller){
$httpBackend=_$httpBackend_;
$httpBackend.expectGET('blogPost').respond({_id:'1'});
$routeParams.id='1';
scope=$rootScope.$new();
ctrl=$controller('BlogViewCtrl',{$scope:scope});
}));
it('shouldshowblogentryid',function(){
//expect(scope.blogEntry._id).toEqual(1);
//expect(scope.blogList).toBeUndefined();
expect(scope.blg).toEqual(1);
});
});
describe('LoginCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('LoginCtrl',{$scope:scope});
scope.submit();
}));
it('shouldshowsubmitsuccess',function(){
console.log("LoginCtrl:"+scope.sub);
expect(scope.sub).toEqual(true);
//expect(scope.blogList).toBeUndefined();
});
});
describe('LogoutCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('LogoutCtrl',{$scope:scope});
}));
it('shouldcreateLogoutCtrlcontroller',function(){
console.log("LogoutCtrl:"+ctrl);
expect(ctrl).toBeDefined();
//expect(scope.blogList).toBeUndefined();
});
});
});
KarmaTestingThetestspecificationsjustaddedwilltestthenewserviceandthetwonewcontrollers.Wewillalsotestalltheexistingcontrollers,theexistingservices,andtheexistingdirectivewhenKarmaruns.
Right-clicktheprojectandselect“Test”fromthemenu.Karmawillstart.YoushouldseebothChromeandFirefoxbrowserwindowsopen.TheNetBeanstestresultswindowshouldopenanddisplayatotalof26passedtestcases.
Ifyougetanyerrormessagesorfailedtests,gobackoverthissectionandverifythatyoucompletedalltheconfigurationsandinstallations.YoucanalsodownloadtheChapter10codefromtheGitHubprojectsite.
End-to-EndTestingWewillmakeseveralchangestotheend-to-endtestspecificationsforourblogapplicationhere.Wewillneedtologintotheblogapplicationwiththescript.Then,onceloggedin,wewillnavigatethroughtheblogasbeforetoverifythatallpreviousE2Efunctionalitystillworks.Wewillthenneedtologoutwiththetestscripttotestthelogoutfunctionality.
ProtractorConfigurationWealreadycreatedaProtractorconfigurationfilefortheblogapplicationinChapter5.TheProtractorconfigurationfileisshownhereforreference:
/*chapter10/conf.jsProtractorconfigurationfile*/
exports.config={
seleniumAddress:'http://localhost:4444/wd/hub',
specs:['e2e/blog-spec.js']
};
ProtractorTestSpecificationTheblog-spec.jsfileshownherecontainsseveralchanges.Firstnoticethatthescriptneedstocompletetheloginformbypopulatingtheusernameandpasswordfields.ThenitlooksuptheloginformbuttonbytheCSSclassname,andclicksthebutton:
/*chapter10/blog-spec.jsProtractortestspecification*/
describe("BlogApplicationTest",function(){
it("shouldtestthemainblogpage",function(){
browser.get("http://localhost:8383/AngularJsBlog/");
//logsintotheblogapplication
element(by.model("username")).sendKeys("node");
element(by.model("password")).sendKeys("password");
element(by.css('.form-button')).click();
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthebloglist
varblogList=
element.all(by.repeater('blogPostinblogList'));
//teststhesizeoftheblogList
expect(blogList.count()).toEqual(1);
browser.get(
"http://localhost:8383/AngularJsBlog/#!/
blogPost/5394e59c4f50850000e6b7ea");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthecommentlist
varcommentList=
element.all(by.repeater('commentinblogEntry.comments'));
//checksthesizeofthecommentList
expect(commentList.count()).toEqual(2);
element(by.css('.navbar-brand')).click();
//logsoutoftheblogapplication
element(by.id('lo')).click();
expect(browser.getTitle()).toEqual("AngularJSBlog");
});
});
Oncethescripthassuccessfullyloggedintotheapplication,itnavigatesthroughtheapplicationasbefore.Then,attheendofthetestscript,itlooksupthelogoutlinkbyid.Itthenclicksthelink,loggingoutoftheapplication.
Theend-to-endtestspecificationvalidatesthattheloginprocessworks.ItalsovalidatesallthepreviousfunctionalitytestedinChapter9.Thenitvalidatesthatthelogoutprocessworkscorrectly.
ProtractorTestingNow,withthosechangesadded,wearereadytostarttheend-to-endtesting.
Startanewcommandwindowandenterthefollowingcommandtostartthetestserver:
webdriver-managerstart
OpenanewcommandwindowandnavigatetotherootoftheChapter10project.Typethecommand:
protractortest/conf.js
Youshouldseeabrowserwindowopen.Youshouldthenseethetestscriptlogintotheblogapplicationandnavigatethroughthepagesoftheapplication,andfinallylogoutoftheapplication.WhentheProtractorscripthasfinished,thebrowserwindowwillclose.
YoushouldseeresultslikethefollowinginthecommandwindowwhentheProtractorscriptcompletes.Thenumberofsecondsthatittakesthescripttofinishwillvarydependingonyourparticularsystem:
Finishedin3.285seconds
1test,5assertions,0failures
OneLastPointonSecurityIwanttoemphasizeonelastthingaboutimplementingsecurityinaJavaScriptapplication.AnysecuritythatyouimplementinJavaScriptcanbecircumventedbytheuser,asIexplainedatthestartofthechapter.TheloginscreenandsecuritythatweimplementedinthischapterarecompletelydependentontheloginRESTservice.
Theloginscreenisusedjustasawaytogatherandstoretheuser’scredentialsinasafeplacetemporarilyandtocontroltheauthenticationprocessforeachRESTservicethatcontainsprivatedata.Theuser’scredentialsareremovedaftereachsessionandhavetobeenteredagainateachlogin,unlesstheuserchoosestosavetheircredentials.
ConclusionInthenextchapteryouwillseehowtheuser’scredentialsareusedtogainaccesstoprivateRESTservicesthataddnewblogpostsandcomments.YouwillfirstdeploytheRESTservicesandtheAngularJSapplicationtogetherinaMEANstackdeploymenttoyourlocalmachinetoseethewholeprocessinaction.Oncetheapplicationisupandrunningonyourlocalmachine,youwillbeabletousethedevelopertoolsinChrometoviewtheRESTservicelogsatruntime:you’llbeabletoviewtheURL,request,andresponseofeachservicecall.
Youwillalsoseeanyerrorsthatoccur.OnceyouhavetestedtheMEANstackonyourlocalmachine,youwilldeploytheprojecttothecloudusingGit,whichisadistributedversioncontrolandsourcecodemanagement(SCM)systeminitiallydevelopedbyLinusTorvalds.
Chapter11.MEANCloudandMobile
ThischapterwillcoverboththeclouddeploymentofourblogapplicationandashortdiscussiononbuildingamobileHTML5versionofourapplication.TheclouddeploymentwillbetoafreeaccountonRedHat’sOpenShiftplatform.Themobilediscussionwillcoverthestepsneededtobuildamobileversionoftheblogapplicationthatwillrunonanymobiledeviceandcanbedistributedthroughtherespectivemobileapplicationstores.ThemobileversionwillusethesameRESTservicesthatweuseforthecloudversionofourblogapplication.
LocalDeploymentBeforewedeployourblogapplicationtothecloud,wewillsetupalocalprojectinNetBeansthatwewilllaterusetodeployourblogtoOpenShift.Wecanalsorunandtestourblogapplicationlocallybeforepushingittothecloud.AllthecodeforthischapterhasalreadybeenwrittenandcanbedownloadedfromGitHub.WewillwalkthroughthecodeanddiscussthechangesthathavebeenmadetoourAngularJSapplicationtoallowforadeploymenttothecloud.
OurclouddeploymentusesNode.jsastheserverplatform,ExpressJSasthewebapplicationframework,andMongoDBasthedatabase.WewilldiscusshowAngularJSintegrateswithallthreeofthesetoformaMEAN(MongoDB,ExpressJS,AngularJS,andNode.js)stackdeployment.WewillprimarilyfocusontherolethatAngularJSplaysinaMEANstackapplication.
WewillnotcovertheNode.jscodeingreatdetail.AlthoughtheNode.jsserver-sidecodeisJavaScript,itcanoftenbequitecomplex.Ifyouhaveserver-sideexperience,feelfreetoexperimentwiththeservercode.BookswrittenspecificallyontheMEANstackwillcovertheNode.jsandExpressJScodeofMEANstackapplicationsinmuchgreaterdepththanwewillhere.
InstallingNode.js,npm,andMongoDBBeforeyoucanrunthenewMEANblogapplicationlocally,youmustinstallNode.js,MongoDB,andnpm(theNode.jspackagemanager)onyourlocalsystem.Theinstallationsaredifferentforeachoperatingsystem,butyoucanfindmoreinformationaboutNode.jsatnodejs.organdyoucanfindinformationaboutMongoDBathttp://www.mongodb.org.IfyouareusingoneoftheLinuxdistributions,youcanusuallyinstallandconfigurebothNode.jsandMongoDBthroughtheOSpackagemanagementsystem.Beforewecontinue,installandconfigureNode.js,npm,andMongoDBifyouhaven’tdonesoalready.
InstallingtheNetBeansNode.jsPluginNowwewillinstallaNode.jspluginforNetBeanstosimplifyourinteractionwithNode.js.Dothefollowing:
1. FollowthedirectionsonTimBoudreau’sblog.
2. Downloadandinstalltheplugin.
3. Configurethepluginasspecified.
OnceyouhavetheNode.jspluginforNetBeansinstalledandconfigured,downloadthesourceforthischapterfromGitHub.Unzipthefilesomewhereonyourlocaldrive.InNetBeans,click“File”andselect“OpenProject”fromthemenu,thennavigatetotheprojectsourcethatyoujustdownloadedandopentheNode.jsproject.YoushouldseetheNodeBlogproject,asshowninFigure11-1.
Figure11-1.TheNodeBlogprojectinNetBeans
TheMEANApplicationWe’lluseMongoDBasourserver-sidedatabase.MongoDBisaNoSQLdatabasethatisfastandeasytouse.WithMongoDB,thereisnoconcernaboutwritingSQLqueries;wejustusetheMongoDBAPItointeractwiththedatabase.We’llactuallysimplifyourinteractionwithMongoDBevenmorebyusingMongoose.js,anobjectdatamodeling(ODM)librarythatallowsustointeractwithMongoDBusingJSONviaagreatlysimplifiedAPIinterface.
OurMEANstackusesRESTservicesbuiltwithExpressJS.ExpressJSisawebframeworkthatislightweightandeasytouse.RESTservicesbuiltonExpressJScanbeusedexclusivelyinourapplicationorexposedtotheoutsideworldforusebyexternalapplications.
MEANstackapplicationsrunonNode.js,whichrunsonGoogle’sV8JavaScriptengine.Node.jsisaverypowerfulplatformfordevelopingserver-sidesoftwareapplicationsinJavaScript.AngularJSsitsontopoftheotherthreepiecesoftheMEANstackandisusedtobuildJavaScriptapplicationsthatinteractdirectlywiththeRESTservicesbuiltwithExpressJS.
Node.jsPublicFolderYouwillnoticeourAngularJSblogcodeisnowlocatedunderthepublicfolderintheMEANproject.PlacingtheAngularJScodeinthepublicfolderiscommonpracticewhenyou’rebuildingMEANapplications.Openthepublicfolderandyoushouldseethesamecodethatwedevelopedinthepreviouschapters.
MEANServicesSeveralchangeswereneededtoourservices.jsfile,asshowninthefollowingcode.NoticethatwechangedtheURLforeachservicefromhttp://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/to./NodeBlog/.Thatsmallchangemakesourapplicationtransportabletoanycloudplatform.Withoutmakingthatchange,wewouldneedtoconfiguretheserviceURLseverytimewemovedtheapplicationtoanewcloudplatform:
/*chapter11/services.js*/
'usestrict';
/*Services*/
varblogServices=
angular.module('blogServices',['ngResource']);
blogServices.factory('BlogPost',['$resource',
function($resource){
return$resource("./NodeBlog/blog/:id",{},{
get:{method:'GET',cache:false,isArray:false},
save:{method:'POST',cache:false,isArray:false},
update:{method:'PUT',cache:false,isArray:false},
delete:{method:'DELETE',cache:false,isArray:false}
});
}]);
blogServices.factory('BlogList',['$resource',
function($resource){
return$resource("./NodeBlog/blogList",{},{
get:{method:'GET',cache:false,isArray:true}
});
}]);
blogServices.factory('Login',['$resource',
function($resource){
return$resource("./NodeBlog/login",{},{
login:{method:'POST',cache:false,isArray:false}
});
}]);
blogServices.factory('BlogPostComments',['$resource',
function($resource){
return$resource("./NodeBlog/comment/:id",{},{
save:{method:'POST',cache:false,isArray:false}
});
}]);
Wealsomadechangestotheapplicationtoallowtheusertocreatenewblogpostsandtoaddcommentstoposts.Oneofthosechangeswastothisfileaswell:noticethatweaddedanewBlogPostCommentsserviceatthebottomofthefile.Therewerealsochangesmadetootherfilesintheapplication.Wewillfirstdiscussthechangestocontrollers.js.
MEANBlogControllersFollowingisthenewcontrollers.jsfile,whichwe’vemodifiedtogiveustheabilitytoaddnewblogpostsandcomments.NoticefirstthechangesthatweremadetotheBlogViewCtrlcontroller.We’veinjectedseveralnewservicesintothecontroller,includingtheBlogPostCommentsservicejustshown.We’vealsoaddedanewsubmitmethodtothecontrollerthathandlestheprocessofaddinganewcommenttoablogpost.ThenewsubmitmethodmakesacalltothesavemethodontheBlogPostCommentsservice:
/*chapter11/controllers.js*/
'usestrict';
/*Controllers*/
varblogControllers=
angular.module('blogControllers',[]);
blogControllers.controller('BlogCtrl',
['$scope','BlogList','$location','checkCreds',
functionBlogCtrl($scope,BlogList,$location,checkCreds){
if(!checkCreds()){
$location.path('/login');
}
$scope.brandColor="color:white;";
$scope.blogList=[];
BlogList.get({},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.blogList=response;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
});
}]);
blogControllers.controller('BlogViewCtrl',
['$scope','$routeParams','BlogPost','BlogPostComments',
'$location','checkCreds','$http','getToken','$route',
functionBlogViewCtrl($scope,$routeParams,BlogPost,
BlogPostComments,$location,checkCreds,$http,getToken,
$route){
if(!checkCreds()){
$location.path('/login');
}
varblogId=$routeParams.id;
$scope.blg=1;
BlogPost.get({id:blogId},
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$scope.blogEntry=response;
$scope.blogId=response._id;
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
}
);
$scope.submit=function(){
$scope.sub=true;
$http.defaults.headers.common['Authorization']='Basic'+
getToken();
varpostData={
"commentText":$scope.commentText,
"blog":$scope.blogId
};
BlogPostComments.save({},postData,
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$location.path('/blogPost/'+$scope.blogId);
$route.reload();
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
});
};
}]);
blogControllers.controller('LoginCtrl',['$scope',
'$location','Login','setCreds',
functionLoginCtrl($scope,$location,Login,setCreds){
$scope.submit=function(){
$scope.sub=true;
varpostData={
"username":$scope.username,
"password":$scope.password
};
Login.login({},postData,
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
if(response.authenticated){
setCreds($scope.username,$scope.password)
$location.path('/');
}else{
$scope.error="LoginFailed"
}
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
});
};
}]);
blogControllers.controller('LogoutCtrl',['$location','deleteCreds',
functionLogoutCtrl($location,deleteCreds){
deleteCreds();
$location.path('/login');
}]);
blogControllers.controller('NewBlogPostCtrl',
['$scope','BlogPost','$location','checkCreds','$http','getToken',
functionNewBlogPostCtrl($scope,BlogPost,$location,checkCreds,
$http,getToken){
if(!checkCreds()){
$location.path('/login');
}
$scope.languageList=[
{
"id":1,
"name":"English"
},
{
"id":2,
"name":"Spanish"
}
];
$scope.languageId=1;
$scope.newActiveClass="active";
$scope.submit=function(){
$scope.sub=true;
$http.defaults.headers.common['Authorization']='Basic'+
getToken();
varpostData={
"introText":$scope.introText,
"blogText":$scope.blogText,
"languageId":$scope.languageId
};
BlogPost.save({},postData,
functionsuccess(response){
console.log("Success:"+JSON.stringify(response));
$location.path('/');
},
functionerror(errorResponse){
console.log("Error:"+JSON.stringify(errorResponse));
});
};
}]);
blogControllers.controller('AboutBlogCtrl',['$scope',
'$location','checkCreds',
functionAboutBlogCtrl($scope,$location,checkCreds){
if(!checkCreds()){
$location.path('/login');
}
$scope.aboutActiveClass="active";
}]);
TheRESTservicelinkedtotheBlogPostCommentsservicerequiresBasicAuthentication.IfyoulookatthefirstlineofthenewsubmitmethodaddedtotheBlogViewCtrlcontroller($http.defaults.headers.common['Authorization']='Basic'+getToken();),youwillseehowRESTserviceBasicAuthenticationishandledinAngularJS.Thecodeonthatlinemakesuseofthe$httpservicetoaddaBasicAuthenticationheadertotheRESTservicecall.
WeusethegetTokenAngularJSbusinesslogicservicedevelopedinChapter8toaddthebase64tokentotheheader,asdescribedinthatchapter.Onceanewcommentisaddedsuccessfully,wethenmakeacalltothepathmethodonthe$locationservice($location.path('/blogPost/'+$scope.blogId);)andacalltothereloadmethodonthe$routeservice($route.reload();).Makingthosetwocallsrefreshestheblogpostpagetoshowthenewlyaddedcomment.
WealsoaddedanewcontrollernamedNewBlogPostCtrl.ThenewcontrollerhasasubmitmethodthatmakesacalltotheBlogPostserviceusedpreviously.ThesavemethodiscalledontheBlogPostservice,andtheRESTservicemappedtothesavemethodrequiresBasicAuthentication,asdescribedpreviously.Theimplementationforauthenticationisthesame.
MEANBlogTemplatesThenewcontrolleralsohasanewlanguageListJSONarraythatisusedtopopulateanewHTML<select>elementinthetemplateusedfornewblogposts.Thelanguagefieldisnotactuallyusedbyourblogapplicationbutisincludedtoshowhowtopopulatea<select>elementinanAngularJSview.Wepreselectthe<select>elementwith“English”bysettingthelanguageIdscopeproperty($scope.languageId=1;).
Therewerenoothersignificantchangesmadetothecontrollers.jsfile.Wewillnowtalkaboutthenewtemplateaddedtoallowuserstoaddnewblogposts.Wewillalsocoverchangesmadetotheblogposttemplateneededforaddingcommentstoblogentries.
AddingCommentsThefollowingcodeshowsthemodificationsneededtotheexistingblogposttemplate.Youwillnoticethatwe’veaddedanewformforsubmittingnewcomments.ThenewformismappedtothenewsubmitmethodoftheBlogEntryCtrlcontroller.AlsonoticethatweholdtheblogIDinahiddenelementandpassthatIDbacktothecontrollerwhentheusersubmitstheform.TheblogIDispassedtotheRESTservicethataddsnewcomments:
<!--chapter11/blogPost.html-->
<divblg-menumenu-title="AngularJSMEANBlog"></div>
<divid="container"class="container">
<divclass="blog-post-label">BlogEntry</div>
<divclass="blog-entry-wrapper">
<divclass="blog-intro-text">
Posted:{{blogEntry.date|date:'MM/dd/yyyy@h:mma'}}
</div>
<divclass="blog-entry-outer">
{{blogEntry.blogText}}
</div>
<divclass="blog-comment-wrapper">
<divclass="blog-comment-label">BlogComments</div>
<divclass="blog-entry-comments"ng-repeat="commentinblogEntry.comments">
{{comment.commentText}}
</div>
</div>
<divclass="blog-comment-entry-wrapper">
<formclass=""ng-submit="submit()"ng-controller="BlogViewCtrl">
<inputtype="hidden"ng-model="blogId"/>
<divclass="blog-post-entry-label">
<labelfor="commentText">NewComment:</label>
</div>
<divclass="blog-post-entry-element">
<textareaclass="blog-post-textarea"type="text"
ng-model="commentText"name="commentText"placeholder="Comment"required/>
</div>
<divclass="blog-post-button">
<buttontype="submit"class="form-button">Submit</button>
</div>
</form>
</div>
</div>
</div>
AddingBlogEntriesThefollowingcodeshowsthenewtemplateusedtoaddnewblogposts.ThetemplatemapsformsubmissiontothesubmitmethodoftheNewBlogPostCtrlcontrollerusingtheng-submitdirective,asbefore:
<!--chapter11/newPost.html-->
<divblg-menumenu-title="AngularJSMEANBlog"></div>
<divid="container"class="container">
<divclass="blog-post-label">NewBlogPosts</div>
<divclass="blog-post-wrapper">
<formclass=""ng-submit="submit()"ng-controller="NewBlogPostCtrl">
<divclass="blog-post-entry-label">
<labelfor="introText">IntroText:</label></div>
<divclass="blog-post-entry-element">
<textareaclass="blog-post-textarea"type="text"
ng-model="introText"name="introText"placeholder="IntroText"required/></div>
<divclass="blog-post-entry-label">
<labelfor="blogText">BlogText:</label></div>
<divclass="blog-post-entry-element">
<textareaclass="blog-post-textarea"type="text"
ng-model="blogText"name="blogText"placeholder="BlogText"required/></div>
<divclass="blog-post-entry-label">
<labelfor="blogText">Language:</label></div>
<divclass="blog-post-entry-element">
<selectclass="form-select-element-left"ng-model="languageId"
ng-options="lan.idaslan.nameforlaninlanguageList"
name="languageId"required>
</select>
</div>
<divclass="blog-post-button"><buttontype="submit"
class="form-button">Submit</button></div>
</form>
</div>
</div>
Thefollowingcodeshowsthechangeneededtothemenu.htmlfile:we’veaddedalinkinthemenutothenewblogpostcreationview.Thenewpathconfigurationisalsoshown:
<!--chapter11/menu.html-->
<navclass="navbarnavbar-inversenavbar-fixed-top"role="navigation">
<!--Brandandtogglegetgroupedforbettermobiledisplay-->
<divclass="container">
<divclass="navbar-header">
<buttontype="button"class="navbar-toggle"data-toggle="collapse"
data-target=".navbar-collapse">
<spanclass="sr-only">Togglenavigation</span>
<spanclass="icon-bar"></span>
<spanclass="icon-bar"></span>
<spanclass="icon-bar"></span>
</button>
<aclass="navbar-brand"style="{{brandColor}}"href="#!/">{{label}}</a>
</div>
<!--Collectthenavlinks,forms,andothercontentfortoggling-->
<divclass="collapsenavbar-collapse">
<ulclass="navnavbar-nav">
<liclass="{{aboutActiveClass}}"><ahref="#!about">About</a></li>
<liclass="{{newActiveClass}}"><ahref="#!newBlogPost">New</a></li>
<liclass="">
<ahref="https://github.com/KenWilliamson">DownloadProjectCode</a>
</li>
<li><ahref="#!logOut">Logout</a></li>
</ul>
</div><!--/.navbar-collapse-->
</div>
</nav>
AddingNewRoutesThefollowingcodeshowsthechangesneededfortheapp.jsfile.Thenewrouteusedtoaddanewblogpostisshown.Theroutewasaddedtothe$routeProviderasbefore:
/*chapter11/app.jsexcerpt*/
.when('/newBlogPost',{
templateUrl:'partials/newPost.html',
controller:'NewBlogPostCtrl'
})
Thecompleteapp.jsfileisshownhereforconvenience:
/*chapter11/app.jscompletefile*/
'usestrict';
/*AppModule*/
varblogApp=angular.module('blogApp',[
'ngRoute',
'blogControllers',
'blogServices',
'blogBusinessServices',
'blogDirectives'
]);
blogApp.config(['$routeProvider','$locationProvider',
function($routeProvider,$locationProvider){
$routeProvider.
when('/',{
templateUrl:'partials/main.html',
controller:'BlogCtrl'
}).when('/blogPost/:id',{
templateUrl:'partials/blogPost.html',
controller:'BlogViewCtrl'
}).when('/newBlogPost',{
templateUrl:'partials/newPost.html',
controller:'NewBlogPostCtrl'
}).when('/about',{
templateUrl:'partials/about.html',
controller:'AboutBlogCtrl'
}).when('/login',{
templateUrl:'partials/login.html',
controller:'LoginCtrl'
}).when('/logOut',{
templateUrl:'partials/login.html',
controller:'LogoutCtrl'
});
$locationProvider.html5Mode(false).hashPrefix('!');
}]);
AddingNode.jsDependenciesNoothersignificantchangesweremadetotheblogapplication.Wewillnowruntheapplicationlocallybeforedeployingtothecloud.
Thereisonesmallcommand-linetaskthatneedstobeperformedbeforeyoucanruntheblogapplicationlocally.ThisisstandardpracticewhenworkingwithNode.js.Dothefollowing:
1. OpenacommandwindowandnavigatetothelocationonyourdrivewhereyouunzippedtheNodeBlogproject.
2. Youshouldseethepackage.jsonfileatthatlocation.
3. Inthecommandwindow,dothefollowing:a. Typenpminstall.
b. PressEnter.
Thiscommandusesnpmtoinstallalltheblogapplicationdependencies.Iftheinstallationwassuccessful,youshouldseealltherequiredNode.jspackagesinstalledinthecurrentdirectoryunderanewfoldernamednode_modules.
Whenyourunthenpminstallcommand,npmreadsthepackage.jsonfileandinstallsalltherequiredpackagesthataredefinedinthatfile.Iftherewereerrorsandthenewfolderdidn’tgetcreated,thereisaproblemwiththeNode.jsinstallationonyourmachine.OnceyouhavetherequiredNode.jspackagesinstalledinyourproject,youarereadytoruntheproject.
RunningtheBlogApplicationLocallyRight-clicktheNodeBlogprojectandselect“Run”fromthemenu.YoushouldseeasmallindicatoratthebottomrightofNetBeans,asshowninFigure11-2.Ifyousee“Running,”yourprojectandNode.jsareinstalledcorrectly.Openabrowserandnavigatetohttp://localhost:8080,andyoushouldseetheloginscreenasbefore.
Figure11-2.RunningtheNodeBlogproject
Loginwiththefollowingcredentials:
username=“node”
password=“password”
Theapplicationshouldperformjustasitdidbefore.Ifyouhaveanyissuesrunningtheapplicationlocally,resolvethoseissuesbeforeyoucontinue.Oncetheapplicationrunslocallyonyourmachine,continueontothenextsection.
TestingwithKarmaWe’veaddedanewBlogPostCommentsservicetotheservices.jsfile,andmadechangestothecontrollers.jsfile.Inordertovalidatethateverythingisworkingcorrectly,weneedtoupdatethetestspecificationsaswell.Ifyoulookatthetestspecificationsforcontrollersandservicesinthedownloadedcodeforthischapter,youwillseetheneededchangesandadditions.
FirstIwillshowhowtoconfigureKarmainaMEANstackenvironment.ThenwewilllookatthetestspecificationforthenewBlogPostCommentsserviceandthechangestothetestspecificationsforcontrollers.
KarmaConfigurationTheKarmaconfigurationfilewasmodifiedfromthefileweusedinChapter10.NowtheAngularJSapplicationislocatedunderthepublicfolderoftheMEANblogapplication.InChapter10,thepublic_htmlfolderwasusedinstead.TheKarmaconfigurationfilewasmodifiedtoaccountforthatchange.ThefullKarmaconfigurationfileisshownhere:
/*chapter11/karma.conf.jsKarmaconfigurationfile*/
module.exports=function(config){
config.set({
basePath:'../',
files:[
"public/js/libs/angular.min.js",
"public/js/libs/angular-mocks.js",
"public/js/libs/angular-route.min.js",
"public/js/libs/angular-resource.min.js",
"public/js/libs/angular-cookies.min.js",
"public/js/*.js",
"public/partials/*.html",
"test/**/*Spec.js"
],
preprocessors:{
'public/partials/*.html':['ng-html2js']
},
exclude:[
],
autoWatch:true,
frameworks:[
"jasmine"
],
browsers:[
"Chrome",
"Firefox"
],
plugins:[
"karma-junit-reporter",
"karma-chrome-launcher",
"karma-firefox-launcher",
"karma-jasmine",
"karma-ng-html2js-preprocessor"
],
ngHtml2JsPreprocessor:{
stripPrefix:'public/'
}
});
};
ThereisoneotherthingtonoteifyouareusingNetBeans:aNode.jsprojectinNetBeansdoesnothavebuilt-insupportforKarma.Thatisnotreallyaproblem;wejustneedtolaunchKarmafromthecommandlineinstead.Wewillcoverthatinthenextsection.
Now,beforewestartunittesting,weneedtoinstallalltheNode.jsdependenciesdefinedintheproject’spackage.jsonfile.Dothefollowing:
1. NavigatetothelocationwhereyouunzippedtheMEANblogproject.
2. Navigatetothelocationofthepackage.jsonfile.
3. Typethefollowingcommandtoinstallalldependencies:
npminstall
Theinstallprocesswillrunforseveralminutes.Whenallpackagesareinstalled,youwillbereadytomoveontothenextsection.
KarmaTestSpecificationsThetestspecificationforthenewBlogPostCommentsserviceisshownnext.Wewillonlyverifythatwecaninjecttheserviceatthispoint.WewillcompletelychecktheservicewhenwedoE2Etesting:
/*chapter11/servicesSpec.jsexcerpt*/
describe('testBlogPostComments',function(){
var$rootScope;
varcomment;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
comment=$injector.get('BlogPostComments');
}));
it('shouldtestBlogPostCommentsservice',function(){
expect(comment).toBeDefined();
});
});
ThenewtestspecificationfortheNewBlogPostCtrlcontrollerisshownnext.Noticethatwemakeacalltothesubmitmethodthatisattachedtothecontroller’sscope.Wethenvalidatethatthecalltothesubmitmethodwassuccessful:
/*chapter11/controllerSpec.jsexcerpt*/
describe('NewBlogPostCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('NewBlogPostCtrl',{$scope:scope});
scope.submit();
}));
it('shouldshowsubmitsuccessofNewBlogPostCtrl',
function(){
console.log("NewBlogPostCtrl:"+scope.sub);
expect(scope.sub).toEqual(true);
});
});
NextupisthetestspecificationfortheAboutBlogCtrlcontroller.WevalidatethefunctionalityofthecontrollerbycheckingthevalueassignedtotheaboutActiveClassvariable:
/*chapter11/controllerSpec.jsexcerpt*/
describe('AboutBlogCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('AboutBlogCtrl',{$scope:scope});
}));
it('shouldcreateAboutBlogCtrlcontroller',function(){
console.log("AboutBlogCtrl:"+ctrl);
expect(scope.aboutActiveClass).toEqual("active");
});
});
WealsomadeachangetothetestspecificationfortheBlogViewCtrlcontroller,asshownhere.Wenowneedtovalidateacalltothenewsubmitmethodattachedtothescopeof
thatcontroller:
/*chapter11/controllerSpec.jsexcerpt*/
describe('BlogViewCtrl',function(){
varscope,ctrl,$httpBackend;
beforeEach(inject(function(_$httpBackend_,
$routeParams,$rootScope,$controller){
$httpBackend=_$httpBackend_;
$httpBackend.expectGET('blogPost').respond({_id:'1'});
$routeParams.id='1';
scope=$rootScope.$new();
ctrl=$controller('BlogViewCtrl',{$scope:scope});
scope.submit();
}));
it('shouldshowblogentryid',function(){
expect(scope.blg).toEqual(1);
expect(scope.sub).toEqual(true);
});
});
ThecompleteservicesSpec.jsandcontrollerSpec.jsfilesareshownnextforreference:
/*chapter11/servicesSpec.jscompletefile*/
describe('AngularJSBlogServiceTesting',function(){
describe('testBlogList',function(){
var$rootScope;
varblogList;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
blogList=$injector.get('BlogList');
}));
it('shouldtestBlogListservice',function(){
expect(blogList).toBeDefined();
});
});
describe('testBlogPost',function(){
var$rootScope;
varblogPost;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
blogPost=$injector.get('BlogPost');
}));
it('shouldtestBlogPostservice',function(){
expect(blogPost).toBeDefined();
});
});
describe('testLogin',function(){
var$rootScope;
varlogin;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
login=$injector.get('Login');
}));
it('shouldtestLoginservice',function(){
expect(login).toBeDefined();
});
});
describe('testBlogPostComments',function(){
var$rootScope;
varcomment;
beforeEach(module('blogServices'));
beforeEach(inject(function($injector){
$rootScope=$injector.get('$rootScope');
comment=$injector.get('BlogPostComments');
}));
it('shouldtestBlogPostCommentsservice',function(){
expect(comment).toBeDefined();
});
});
});
/*chapter11/controllerSpec.jscompletefile*/
describe('AngularJSBlogApplication',function(){
beforeEach(module('blogApp'));
//beforeEach(module('blogServices'));
describe('BlogCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('BlogCtrl',{$scope:scope});
}));
it('shouldcreateshowblogentrycount',function(){
console.log("blogList:"+scope.blogList);
expect(scope.blogList.length).toEqual(0);
//expect(scope.blogList).toBeUndefined();
});
});
describe('BlogViewCtrl',function(){
varscope,ctrl,$httpBackend;
beforeEach(inject(function(_$httpBackend_,$routeParams,
$rootScope,$controller){
$httpBackend=_$httpBackend_;
$httpBackend.expectGET('blogPost').respond({_id:'1'});
$routeParams.id='1';
scope=$rootScope.$new();
ctrl=$controller('BlogViewCtrl',{$scope:scope});
scope.submit();
}));
it('shouldshowblogentryid',function(){
//expect(scope.blogEntry._id).toEqual(1);
//expect(scope.blogList).toBeUndefined();
expect(scope.blg).toEqual(1);
expect(scope.sub).toEqual(true);
});
});
describe('LoginCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('LoginCtrl',{$scope:scope});
scope.submit();
}));
it('shouldshowsubmitsuccess',function(){
console.log("LoginCtrl:"+scope.sub);
expect(scope.sub).toEqual(true);
//expect(scope.blogList).toBeUndefined();
});
});
describe('LogoutCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('LogoutCtrl',{$scope:scope});
}));
it('shouldcreateLogoutCtrlcontroller',function(){
console.log("LogoutCtrl:"+ctrl);
expect(ctrl).toBeDefined();
//expect(scope.blogList).toBeUndefined();
});
});
describe('NewBlogPostCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('NewBlogPostCtrl',{$scope:scope});
scope.submit();
}));
it('shouldshowsubmitsuccessofNewBlogPostCtrl',
function(){
console.log("NewBlogPostCtrl:"+scope.sub);
expect(scope.sub).toEqual(true);
//expect(scope.blogList).toBeUndefined();
});
});
describe('AboutBlogCtrl',function(){
varscope,ctrl;
beforeEach(inject(function($rootScope,$controller){
scope=$rootScope.$new();
ctrl=$controller('AboutBlogCtrl',{$scope:scope});
}));
it('shouldcreateAboutBlogCtrlcontroller',function(){
console.log("AboutBlogCtrl:"+ctrl);
expect(scope.aboutActiveClass).toEqual("active");
//expect(scope.blogList).toBeUndefined();
});
});
});
KarmaTestingNowweneedtolaunchKarmaandverifythatalltestsweresuccessful.WeneedtousethecommandlinetolaunchKarma,asmentionedearlier.Dothefollowing:
1. Openacommandwindow.
2. NavigatetothelocationoftheMEANblogproject.
3. Navigateinsidetheprojecttowherethetestfolderandthepackage.jsonfilearelocated.
4. TypethiscommandtolaunchKarma:
karmastarttest/karma.conf.js
YoushouldseeaChromeandaFirefoxbrowserwindowopen.Youshouldthenseetextlikethefollowingdisplayedinthecommandwindow,indicatingsuccess:
Chrome38.0.2125(Linux):Executed16of16SUCCESS(0.17secs)
Firefox33.0.0(Ubuntu):Executed16of16SUCCESS(0.157secs)
TOTAL:32SUCCESS
End-to-EndTestingTheMEANblogapplicationrequiresachangetotheURLintheE2Etestspecifications.AsinChapter10,thescriptwillneedtologintotheblogapplication.Then,onceloggedin,itwillnavigatethroughtheblogasbeforetoverifythatallpreviousE2Efunctionalitystillworks.Itwillthenneedtologouttotestthelogoutfunctionalityaswell.
ProtractorConfigurationWealreadycreatedaProtractorconfigurationfilefortheblogapplicationinChapter5,andwe’vejustmovedthatfileintotheMEANapplication.TheProtractorconfigurationfileisshownhereforreference:
/*chapter11/conf.jsProtractorconfigurationfile*/
exports.config={
seleniumAddress:'http://localhost:4444/wd/hub',
specs:['e2e/blog-spec.js']
};
ProtractorTestSpecificationThemodifiedProtractortestspecificationisshownnext.NoticethenewURL,asmentionedpreviously:
/*chapter11/blog-spec.jsProtractortestspecification*/
describe("BlogApplicationTest",function(){
it("shouldtestthemainblogpage",function(){
browser.get("http://localhost:8080/#!/");
//logsintotheblogapplication
element(by.model("username")).sendKeys("node");
element(by.model("password")).sendKeys("password");
element(by.css('.form-button')).click();
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthebloglist
varblogList=element.all(by.repeater('blogPostinblogList'));
//testthesizeoftheblogList
expect(blogList.count()).toEqual(3);
browser.get("http://localhost:8080/#!/blogPost/5387bafe185e4e972996adff");
expect(browser.getTitle()).toEqual("AngularJSBlog");
//getsthecommentlist
varcommentList=element.all(by.repeater('commentinblogEntry.comments'));
//checksthesizeofthecommentList
expect(commentList.count()).toEqual(2);
element(by.css('.navbar-brand')).click();
//logoutoftheblogapplication
element(by.id('lo')).click();
expect(browser.getTitle()).toEqual("AngularJSBlog");
});
});
ProtractorTestingWearenowreadytostarttheend-to-endtesting.Startanewcommandwindowandenterthefollowingcommandtostartthetestserver:
webdriver-managerstart
OpenanewcommandwindowandnavigatetotherootoftheChapter11project.Typethecommand:
protractortest/conf.js
Youshouldseeabrowserwindowopen.Youshouldthenseethetestscriptlogintotheblogapplicationandnavigatethroughthepagesoftheapplication,asinChapter10.Thescriptshouldthenlogoutoftheapplication.WhentheProtractorscripthasfinished,thebrowserwindowwillclose.
YoushouldseeresultslikethefollowinginthecommandwindowwhentheProtractorscriptcompletes.Thenumberofsecondsthatittakesthescripttofinishwillvarydependingonyourparticularsystem:
Finishedin2.644seconds
1test,5assertions,0failures
Wearenowreadytocontinuewithourdeploymenttothecloud.
MEANDeploymenttotheCloudNowwewilldeployourblogapplicationtoOpenShiftusingGit.NetBeanscomeswithabuilt-inversionofGitthatisveryeasytoconfigureandusewhenyou’redeployingtoOpenShift.FirstyoumustopenafreeOpenShiftaccount,whichgivesyouthreefreegears(cloudserverinstances)thatcanrunNode.js.Dothefollowing:
1. Gotohttps://www.openshift.com/app/account/newandcreateanewaccount.
2. Clickthe“AddApplication”buttonandcreateanewNode.js0.10application(saveacopyofthepageforreferencelater).
3. AddaMongoDBcartridgetotheapplication(saveacopyofthepageforreferencelater).
4. FollowtheOpenShiftdocumentationandsetupGitonyourdevelopmentenvironment.You’llneedapublicSSHkeytouseGitontheOpenShiftsystem.
5. OnceGitisconfigured,clonetheapplicationwithGittoalocationonyourdriveseparatefromthelocationwhereyouunzippedtheNodeBlogdownload.
6. OpenthenewOpenShiftproject,andcopythefollowingfilesfromtheNodeBlogprojecttothenewOpenShiftproject,replacingtheexistingversions:a. package.json
b. server.js
7. CopythepublicfolderfromtheNodeBlogprojecttothenewOpenShiftproject.
8. CopythedbfolderfromtheNodeBlogprojecttothenewOpenShiftproject.
Nowweneedtotestthecloudversionoftheapplicationlocally.OpenacommandwindowandnavigatetothefolderwhereyouplacedthenewOpenShiftproject.Makesureyouseethepackage.jsonfile,andenternpminstallinthecommandwindowasyoudidearlier.Nowright-clicktheOpenShiftprojectandselect“Run”fromthemenu.Ifyouseetherunningindicatorasshownbefore,theapplicationisworkingproperly.
Now,usingtheGitcredentialsthatyousetupearlierforyourOpenShiftapplication,doaGitremotepushinNetBeansandtheapplicationwillbedeployedtoOpenShift.Ifyouseeanyerrors,usetheOpenShiftdocumentationtoresolvetheerrorcondition.Mostproblemsareusuallyrelatedtocredentialsandcanberesolvedeasily.
TestingtheCloudBlogOncetheapplicationisdeployedtothecloud,openabrowserandnavigatetotheOpenShift-suppliedlinkforyourapplication.Ifyoudidn’tkeepacopyoftheapplicationpage,logintoyourOpenShiftaccountandclicktheapplicationthatyoujustcreated.Thelinktotheapplicationwillbeshownonthedetailspages.
Onceyounavigatetothelinkforyourapplication,youshouldseetheloginscreenasbefore.Ifyouseetheloginscreen,yourapplicationwassuccessfullydeployedtothecloud.Logintoyourblogapplicationandaddanewblogpost.Addacommenttothepost.Yourblogapplicationshoulddisplaythenewpostandthecomment.Ifyouwouldliketoviewtheapplicationlogfiles,followtheOpenShiftdocumentationrelatedtoviewinglogfilesformorehelp.
Thisconcludesourdiscussiononclouddeployment.Next,we’lltakeabrieflookathowtoturnyourblogapplicationintoamobileHTML5application.
MobileVersionTheAngularJSblogapplicationhasallthatweneedtobuildamobileversionforanymobileplatform.OurbusinesslogicisintheRESTservices,andallmodernmobiledevicescanaccessRESTservices.Weusedaresponsivedesign,sotheapplicationshouldlookgoodonanymobiledevice.AllmodernmobiledevicesalsohavewebbrowsersandnativebrowsercontrolssuchastheAndroidWebViewthatcanlaunchinternalHTMLpages.
Theprocessforbuildingamobileblogapplicationisstraightforwardforanymobiledevice.Theprocessinvolvesthefollowingsteps:
1. Createanewmobileprojectfortheparticularmobiledeviceofchoice.
2. FollowtheCordovadocumentationandaddCordovatoyourmobileproject.
3. CopytheentirecontentsoftheChapter10project(AngularJsBlogChapter10)tothefolderinthemobileprojectspecifiedbyCordovaasadestinationforHTMLfilesforyourparticularmobileplatform.
4. FollowtheCordovadocumentationandconfigureyourmobileapplicationtolaunchtheindex.htmlfilecopiedfromtheChapter10project.
5. OncethemobileprojectisconfiguredaccordingtotheCordovaspecifications,runtheprojectonanemulatororamobiledevice.
Theapplicationshouldrunandlookthesameasthewebversion.TherearenoAngularJS-specificchangesthatweneedtomaketotheprojectcode.IfyouareinterestedinbuildingAngularJS-basedmobileapplications,feelfreetotakethecodefromChapter10andbuildaCordova-basedHTML5mobileapplicationforyourplatformofchoice.TheCordovawebsitehasdocumentationforallmodernplatformstohelpyougetstartedwithyourproject.
ConclusionInthischapterwemadeafewmodificationsanddeployedourblogapplicationtothecloud.Werantheapplicationlocally,andalsoranthecloud-deployedapplication.WealsotookaquicklookathoweasyitistobuildmobileapplicationswithAngularJS.Wewillnowfocusonhowtogetyourapplicationfoundbysearchengines.
Chapter12.AngularJSandSEO
Youmightwonderwhywearecoveringsearchengineoptimization(SEO)inanAngularJSbook.Theanswerissimple.
Currently,AngularJSandmostJavaScriptclient-sideframeworksareusedmostlyforwebapplications.Often,SEOisreallynotthatimportantwherewebapplicationsareconcerned.AsAngularJSgainsinpopularity,however,itcouldverywellbecomeamajorplayerintheworldofwebsitedesign.AngularJScouldpotentiallyreplaceclient-sidecodethatiscurrentlywritteninJava,PHP,Ruby,andPython.
Thatisnottosaythatthoselanguageswillbecompletelyreplaced—theywon’t.Java,PHP,Ruby,andPythonwillcontinuetobeasimportantaseverintheworldofsoftwaredevelopment,butinadifferentway.ThoselanguagesandtheirassociatedframeworkswilltakeontheroleofprovidingthebackendRESTservicesneededforAngularJSandotherJavaScriptclient-sideframeworks.WhenyouconsiderthatcompletewebsitescouldsoonbewrittenwithAngularJS,it’sclearthatSEOshouldthenbecomeamajorconcernforAngularJSdevelopers.ThischapterwillhelpyoutobetterunderstandAngularJSandSEO.
Itisalwaysbesttofocusmoreonbuildingagreatwebapplicationorwebsite,andlessonthespecificsofsearchengines.Gooddesignandperformancearealwaysbyfarthemostimportantconsiderationsforanewsoftwareproject.Althoughsearchengineoptimizationisimportant,focusingtoomuchonSEOduringthedesignandimplementationphaseofaprojectcanultimatelycostyouvaluabledevelopmenthours.
Eventually,however,youdohavetofocusongettingyourapplicationorsitefoundbyallthemajorsearchengines.InthisfinalchapterwewilllookatsomeofthewaystogetyournewAngularJSsoftwarefound.ManyofthepracticespresentedherearerecommendedbyGoogle.
OldVersusNewAngularJSSEOInthepast,usersofwebsitesbuiltwithAngularJShadtofollowaratherarchaicprocessinwhichpagesnapshotsweremadeforanentiresite,andthewebsitecouldthenforwardsearchenginestothesnapshotssothattheywouldseetheprerenderedversionofthesiteratherthantheactualJavaScriptversionofthesite.Sinceconventionalsearchenginesdidn’thavetheabilitytoexecuteJavaScript,pagesbuiltwithAngularJSwererenderedtooldersearchenginesasablankwhitepagewithnocontent.
However,inanewsreleaseonMay23,2014,GoogleconfirmedthatitnowhasthecapabilitytoindexJavaScriptwebsitesandapplications.Thatis,theGooglebothasundergoneupgradestomakeitpossibletoindexsitesandapplicationsthatuseGoogle’sAngularJSandotherJavaScriptframeworks.ForGoogle,thattime-consumingandoftenexpensiveprocessofSEOforAngularJSisnolongernecessary.AlthoughthestateofothersearchenginesandtheirabilitytoexecuteJavaScriptisunknownatthistime,theywillundoubtedlyfollowGoogle’sleadveryquickly,beingforcedtofollowsuitorgetleftbehind.
Therearealsoseveralcompaniesthatspecializeinhelpingclientswiththewebsiteprerenderingprocess.Eventhoughsearchenginesarechanging,manyofthesecompanieswilldoubtlesscontinueofferingprerenderingservicesforseveralyears,ifyoufeeltheneedforthoseservices.
GettingFoundbySearchEnginesWithallthatsaid,therearestillsomewaystoincreaseyourchancesofgettingabetterrankingwithGoogleandothersearchengines.WewillcovertheSEOtasksthatareabsolutelynecessary:
1. SignupforaGoogleWebmasterToolsaccount,addyoursitetotheaccount,andfollowGoogle’sadvice.
2. Buildasitemap.xmlfileforyoursite.
3. Addmicroformattagstoyoursite.
4. MakesureyourJavaScriptiscleanandeasyforsearchenginestoexecute.
5. AvoidcallingRESTservicesthattakelongerthantwosecondstoreturnresults.
GoogleWebmasterToolsOneofthefirstthingsthatyoushoulddoforSEOistogetaGoogleWebmasterToolsaccount.OnceyouaddyoursiteandstarttofollowGoogle’sadvice,youwillseeimmediateimprovementsinyourrankingandthenumberofpagesofyoursitethatareindexedbyGoogle.TheadvicegivenbyGoogleappliestoothersearchenginesaswell.Don’texpecttoseeSEOimprovementsdrasticallyincreaseyourranking,however;SEOisanongoingandtime-consumingprocessthatcantakemonthsorevenyearstorendersignificantresults.
AddingaSitemapAccordingtoGoogle,asitemapfileisveryimportanttoSEO.Google’sWebmasterToolswillhelpyouwiththeprocessofbuildingasitemapanduploadingittoGoogle.Usingasitemapspeedsuptheprocessofgettingyoursiteindexedbymakingsearchenginesawareofthepagesandlinksonyoursite.Youshouldkeepthesitemapup-to-date,withanynewpagesadded.Makesuretoremoveanypagesfromthesitemapthatnolongerexistonthesite.
MicroformatTagsAnotherthingthatimprovesSEOistheuseofmicroformattags(tag-basednavigation).Theuseoftag-basednavigationstartedonblogsitesbuthasspreadconsiderablyoverthelastfewyears;itisnowusedonbusinesswebsitesaswell.
Tag-basednavigationusestheformatshownheretoindicatetosearchenginesthatthepagecontentcontainstherelatedkeywords.Asyoucansee,thehrefattributecontainsalinktoapageonthesite,andtherelattributetellssearchenginesthatthepagecontainsthereferencedkeywords:
<!--chapter12/tag-basednavigation-->
<p>Tags:<ahref="http://www.ulboracms.org/#!/"rel="tag">UlboraCMS</a>,
<ahref="http://www.ulboracms.org/#!/"rel="tag">JavaCMS</a>,
<ahref="http://www.ulboracms.org/#!/"rel="tag">RESTservice</a>,
<ahref="http://www.ulboracms.org/#!/"rel="tag">JSONREST</a>,
<ahref="http://
www.ulboracms.org/#!/article/26"rel="tag">RESTwebservices</a></p>
Tag-basednavigationissupportedbyallmajorsearchengines.
BuildingCleanClientCodeOneofthebestwaystoimproveSEOistocreateacleanandefficientAngularJSapplication.UnnecessaryJavaScriptshouldalwaysbeavoided.JavaScriptmethodsshouldexecutequickly,withnounnecessaryprocessesrunninginthebackground.
Searchenginestakepagespeedintoconsiderationwhenrankingsites.Pagesthatcontainlong-runningJavaScriptfunctionsmaygetdroppedbyGoogleandothersearchenginesandnotgetindexed.Onceapagegetsdroppedbyasearchengine,itcantakealongtimetogetthatpageindexedagain.
BuildingFastRESTServicesOnelastthingthatcandirectlyaffectpagespeedandSEOisthespeedoftheRESTwebservicesusedtopopulatepagecontent.PagesthatrelyonslowRESTservicescansufferasaresult.RESTservicesshouldreturnresultsintwosecondsorless.
ServicesthatreturnresultsinunderasecondarebestforSEOandsiteperformance.AlthoughRESTservicedesignisbeyondthescopeofthisparticularbook,IwanttoemphasizehowimportantwebservicedesignistoSEOwhenwebpagesrelyonthoseservicesforcontent.WhenyoursitedependsonRESTservices,alwaysmakesurethoseservicesperformwellandaddnounnecessarydelaytoyoursiteorapplication.Alwaysinsistonpeak-performingservices.
ConclusionThatbringsustotheendofthischapterandtheendofthebook.I’vedonemybesttopresentAngularJSinawaythatwillmakeiteasytounderstandforbothbeginnersandexperienceddevelopersalike.TheconceptofusingJavaScriptclient-sideframeworkstobuildcompletefrontendapplicationsandwebsitesisrelativelynew,andoftenreferredtoas“cuttingedge”bymany.TherecentGoogleannouncementrelatedtoJavaScriptandSEOmentionedearlieratteststothat.
Butthingsthatareconsideredcuttingedgetodaywillbecommonplaceinafewyears.IbelieveAngularJSwillbeattheforefrontofapplicationdevelopmentincomingyears,andiswellworththetimespentlearningtheframework.Thisbookisonlyastartingpoint,however.NowyoumustgooutanddevelopgreatapplicationswithAngularJS,andhavefunbuildingthoseapplicationstoo!Remember,thebestAngularJSapplicationisawell-designedAngularJSapplication.Alwaysbuildthebestapplicationsthatyoupossiblycan.It’sworththeeffortintheend.
References
AngularJS
Bootstrap
jQuery
WikipediaentryforMVC
WikipediaentryforREST
WikipediaentryforWebservice
UlboraCMS
UlboraCMSatSourceForge
WikipediaentryforSPA
WikipediaentryforRWD
Index
Symbols
$locationservice,AddingaLoginController
$rootScopeobject,AngularJSModels
$scopeobject,AngularJSModels(MVC)
addingbehaviorto,InitializingtheModelwithControllers
attachingmethodsto,AddingaLoginController
modelsin,AngularJSModels
<select>element(HTML),MEANBlogTemplates
{{}}(doublecurlybraces),ControllerBusinessLogic
A
ActiveServerPages(ASP),MVCandAngularJS
Ajax
RESTservicesand,AngularJSandRESTServices
sites,HTML5Mode
AngularJS
asclient-sideframework,JavaScriptClient-SideFrameworks
asMVCframework,ANewandBetterWay
bootstrappingwith,BootstrappingtheApplication
businesslogicin,ControllerBusinessLogic
controllers,AngularJSControllers(MVC)
dependencyinjection,DependencyInjection
directives,AngularJSDirectives-Conclusion
downloadingfilesfor,TheIDE
HTMLcompiler,TheHTMLCompiler
HTML5and,HTML5Mode
integratingwithotherframeworks,IntegratingAngularJSwithOtherFrameworks
modelclasses,AngularJSModels(MVC)
routes,AngularJSRoutes
searchenginesand,ModernSearchEngines
SEOfor,AngularJSandSEO-Conclusion
services,non-REST,CreatingAngularJSServices
single-pageapplicationsin,Single-PageApplications
templates,AngularJSTemplates
testing,TestingAngularJSApplications
viewclassesin,AngularJSViews(MVC)
ApacheCordova,MobileVersion
applications
addingservicemodulesto,ModifyingApp.js
runninginIDEs,RunningtheApplications,RunningtheBlogApplication
runningwithmodels,RunningtheApplication
testinginIDEs,TestingAngularJSApplicationsintheIDE-Protractor
transportable,MEANServices
usingRESTservicesin,BlogApplicationPublicServices
ASP.NETframework,JavaScriptClient-SideFrameworks
authentication,HandlingUserAuthentication-RetrievingUserCredentials
B
BasicAuthentication,UsingBasicAuthentication,MEANBlogControllers
bootstrapping,BootstrappingtheApplication
HTMLcodeand,EditingtheHTMLCode
businesslogic,ServicesandBusinessLogic-Conclusion
addingtoprojects,BlogApplicationBusinessLogic-TestingServiceswithKarma
controller,ControllerBusinessLogic
incontrollers,ControllerBusinessLogic
RESTservicesand,RESTServices
userauthentication,HandlingUserAuthentication-RetrievingUserCredentials
using,UsingtheBusinessLogic
C
CakePHPframework,TheOldWay
integratingwithAngularJS,IntegratingAngularJSwithOtherFrameworks
callbackfunctions,RESTServicesandControllers
cascadingstylesheets,AddingStylesandPresentationLogic
ChromeDeveloperTools,RunningtheApplication
clientcode,BuildingCleanClientCode
client-sideframeworks,JavaScriptClient-SideFrameworks
integratingAngularJSwith,IntegratingAngularJSwithOtherFrameworks
modelclasses,AngularJSModels(MVC)
viewclassesin,AngularJSViews(MVC)
client-sidesecurity,AngularJSSecurity
clouddeployment,MEANDeploymenttotheCloud
makingappstransportable,MEANServices
continuousintegration(CI),TestingAngularJSApplications
end-to-endtestingand,End-to-EndTesting
controlleras,FormSubmission
controllers,AngularJSControllers(MVC),AngularJSControllers-Conclusion
behavior,addingwith,AddingBehaviorwithControllers-AddingBehaviorwithControllers
businesslogicin,ControllerBusinessLogic,ControllerBusinessLogic
editingJavaScriptcodefor,EditingtheJavaScriptCode
end-to-endtestingof,End-to-EndTestingwithProtractor-RunningProtractor
formdata,using,UsingSubmittedFormData
formsubmissionsand,FormSubmission-FormSubmission
initializingmodelswith,InitializingtheModelwithControllers
JSTestDriverand,JSTestDriver-TestingwithJSTestDriver
Karma,testingwith,TestingwithKarma-RunningKarmaUnitTests
login,adding,AddingaLoginService
logout,AddingaLogoutController-AddingaLogoutController
MEAN,MEANBlogControllers-MEANBlogControllers
modelsand,ChangestotheControllers,ModifyingtheControllers-ModifyingtheControllers
multiple,forsingleelements,FormSubmission
presentationlogicand,PresentationLogicandFormattingData
projects,addingto,AddingaNewBlogController
Protractor,testingwith,End-to-EndTestingwithProtractor-RunningProtractor
RESTservicesand,RESTServicesandControllers
securitymodificationsfor,SecurityModificationstoOtherControllers
templatesand,AngularJSTemplates
cookies,UsingBasicAuthentication
checking,CheckingUserCredentials
deleting,DeletingUserCredentials
holdingusercredentialsin,HoldingUserCredentials
retrievinginformationfrom,RetrievingUserCredentials
Cross-OriginResourceSharing(CORS)layer,AngularJSSecurity
CSS3
mediaqueriesin,AddingaLoginTemplate
stylingpageswith,UsingCSS3toStylethePage
D
data
formattingwithcontrollers,PresentationLogicandFormattingData,AddingMockBlogData
mock,addingtoprojects,AddingMockBlogData
storage,RESTservicesand,RESTServices
dates,formatting,AddingMockBlogData,AddingStylesandPresentationLogic
dependencyinjection(DI),DependencyInjection
npminstallcommandand,AddingNode.jsDependencies
OpenShiftand,MEANDeploymenttotheCloud
servicesmoduleand,UpdatingtheProjectforREST
deployment,MEANCloudandMobile-Conclusion
cloud,MEANDeploymenttotheCloud
local,LocalDeployment-ProtractorTesting
directives,AngularJSDirectives-Conclusion
addingtoprojects,AddingtheCustomDirective-PassingtheTitleAttribute
buildingpresentationlogicwith,AddingStylesandPresentationLogic
custom,building,BuildingCustomDirectives
defined,WhatAreDirectives?
end-to-endtestingof,End-to-EndTesting
Karmaand,TestingDirectiveswithKarma-KarmaTesting
namingconventionsfor,NamingConventionsforDirectives
ng-click,AddingBehaviorwithControllers,AddingBehaviorwithControllers
ng-include,BuildingCustomDirectives
ng-model,AddingBehaviorwithControllers,AddingBehaviorwithControllers,AngularJSTemplates
ng-repeat,AddingStylesandPresentationLogic,ListServices
ng-submit,FormSubmission,AddingaLoginTemplate,AddingBlogEntries
ng-view,AngularJSTemplates
passingtitleattribute,PassingtheTitleAttribute
Protractorand,End-to-EndTesting
restrictoption,TheRestrictOption
templateattributesfor,TemplateAttributes
templateUrlattribute,TheTemplateURL
unittestingfor,TestingDirectiveswithKarma-KarmaTesting
viewsand,AddingStylesandPresentationLogic
E
end-to-endtesting(E2E),TestingAngularJSApplications,Protractor
businesslogic,End-to-EndTesting
MEANstackdeployment,End-to-EndTesting-ProtractorTesting
models,End-to-EndTesting
non-RESTservices,End-to-EndTesting
ofdirectives,End-to-EndTesting
ofsecurity,End-to-EndTesting
RESTservices,End-to-EndTesting
ExpressJS,TheMEANApplication
buildingRESTserviceswith,ANewandBetterWay
F
factoryfunction,WaystoCreateAngularJSServices,CreatingAngularJSServices
failedRESTservicecalls,TheJSONResponse
Firefox,RunningKarmaUnitTests
forms,FormSubmission-UsingSubmittedFormData
data,using,UsingSubmittedFormData
submissionsfrom,FormSubmission-FormSubmission
frameworks
ASP.NET,JavaScriptClient-SideFrameworks
CakePHP,IntegratingAngularJSwithOtherFrameworks,TheOldWay
client-side,JavaScriptClient-SideFrameworks,AngularJSViews(MVC),AngularJSModels(MVC)
Jasmine,CreatingTestScripts
MVC,AngularJSViews(MVC),MVCandAngularJS-Conclusion
server-sidewebMVC,JavaScriptClient-SideFrameworks
SpringMVC,JavaScriptClient-SideFrameworks,DependencyInjection,IntegratingAngularJSwithOtherFrameworks,TheOldWay,ANewandBetterWay
Struts,JavaScriptClient-SideFrameworks,TheOldWay
web,TheOldWay-ChoiceTwo
webMVC,JavaScriptClient-SideFrameworks
Zend,TheOldWay
G
Git,Conclusion,MEANDeploymenttotheCloud
Google,OldVersusNewAngularJSSEO
GoogleChrome,RunningKarmaUnitTests
GoogleWebmasterTools,GoogleWebmasterTools
H
hashbangmode,HTML5Mode,HTML5Mode
HTMLcompiler,TheHTMLCompiler
HTML5,HTML5Mode
editing,EditingtheHTMLCode
HistoryAPI,HTML5Mode
mobileapplicationsfor,MobileVersion
mode,turningoff,HTML5Mode
modifyingtousemodels,ModifyingtheHTML
HTTPmethods,RESTServices
HTTPS,Authentication
I
IDE,TheIDEandAngularJSProjects-Conclusion
HTML,editing,EditingtheHTMLCode
JavaScript,editing,EditingtheJavaScriptCode
NetBeans,TheIDE
runningapplicationsin,RunningtheApplications
templates,creating,CreatingtheTemplates
testingapplicationsin,TestingAngularJSApplicationsintheIDE-TestingAngularJSApplicationsintheIDE
inputelements(fromforms),UsingSubmittedFormData
J
Jasmineframework,CreatingTestScripts
Java,AngularJSandSEO
JavaServerPages(JSP),MVCandAngularJS
JavaScript
console,accessing,AngularJSControllers
editing,EditingtheJavaScriptCode
JenkinsCIsystem,Protractor
testingand,TestingConsiderations
JQuery,IntroductiontoAngularJS
downloading,TheIDE
JSTestDriver,TestingAngularJSApplications,JSTestDriver-TestingwithJSTestDriver
testscripts,creating,CreatingTestScripts
testingwith,TestingwithJSTestDriver
JSON,RESTresponseobjectsas,TheJSONResponse
JsTestRunner,JsTestRunner
K
Karma,TestingAngularJSApplications,KarmaTestRunner
businesslogicand,KarmaConfiguration-KarmaTesting
configuring,KarmaConfiguration,KarmaConfiguration
configuringforMEANstackdeployment,KarmaConfiguration
directivesand,TestingDirectiveswithKarma-KarmaTesting
installing,InstallingKarma
MEANstackdeployment,testing,TestingwithKarma-KarmaTesting
models,testing,TestingServiceswithKarma-KarmaTesting
non-RESTservicesand,KarmaConfiguration-KarmaTesting
RESTservices,testingwith,TestingServiceswithKarma-KarmaServiceSpecifications
security,testingwith,TestingwithKarma-KarmaTesting
servicespecificationsfor,KarmaServiceSpecifications
testingconsiderationsfor,TestingConsiderations
unittests,running,RunningKarmaUnitTests
karma-ng-html2js-preprocessorKarmaplugin,TestingDirectiveswithKarma
L
listservices,ListServices
lists,returning,ListServices
locationProviderservice,HTML5Mode
loginservices,AddingaLoginService
controllersand,AddingaLoginService
logincontrollersand,AddingaLoginController
non-REST,AddingaLoginService
security,AddingaLoginController,AddingaLoginTemplate
templates,AddingaLoginTemplate
userauthentication,AddingaLoginService,AddingaLoginTemplate
M
MEAN(MongoDB,ExpressJS,AngularJS,andNode.js)stackdeployment,MEANCloudandMobile-Conclusion
changingserviceURLfor,MEANServices
clouddeployment,MEANDeploymenttotheCloud
controllers,MEANBlogControllers-MEANBlogControllers
end-to-endtestingof,End-to-EndTesting-ProtractorTesting
Karmaand,TestingwithKarma-KarmaTesting
mobileapps,MobileVersion
Node.jsdependencies,adding,AddingNode.jsDependencies
Protractorand,End-to-EndTesting-ProtractorTesting
runninglocally,RunningtheBlogApplicationLocally
services,MEANServices
templates,MEANBlogTemplates
unittesting,TestingwithKarma-KarmaTesting
microformattags,MicroformatTags
mobileapps
aswrapperforserver-sideapplication,ChoiceOne
convertingwebapplicationsto,ChoiceTwo
devices,designingfor,ResponsiveDesignConsiderations-ResponsiveDesignConsiderations
MEANstackdeploymentsfor,MobileVersion
responsivedesignand,ResponsiveDesignConsiderations-ResponsiveDesignConsiderations
webMVCframeworksand,TheOldWay
models,AngularJSModels(MVC),AngularJSModels-Conclusion
addingtoapp,ModifyingApp.js
controllersand,AngularJSControllers(MVC),ChangestotheControllers,ModifyingtheControllers-ModifyingtheControllers
end-to-endtesting,End-to-EndTesting
initializingwithcontrollers,InitializingtheModelwithControllers
Karma,testingwith,TestingServiceswithKarma-KarmaTesting
propertiesof,ModelProperties
Protractor,testingwith,End-to-EndTesting
RESTservicesassourceof,PublicRESTServices
scopepropertiesand,ModelProperties
unittesting,TestingServiceswithKarma-KarmaTesting
MongoDB,InstallingNode.js,npm,andMongoDB
interactingwith,TheMEANApplication
Mongoose.jsODMlibrary,TheMEANApplication
MVCframeworks,MVCandAngularJS-Conclusion
AngularJSas,ANewandBetterWay
controllers,AngularJSControllers(MVC)
modelclasses,AngularJSModels(MVC)
responsivedesignand,ResponsiveDesignConsiderations-ResponsiveDesignConsiderations
testingconsiderationsfor,TestingConsiderations
viewclasses,AngularJSViews(MVC)
web,TheOldWay-ChoiceTwo
N
NetBeans
configuring,TheIDE
installing,TheIDE
JSTestDriver,JSTestDriver-TestingwithJSTestDriver
JsTestRunnersupportin,JsTestRunner
Karmaand,KarmaConfiguration
Node.jsplugin,installing,InstallingtheNetBeansNode.jsPlugin
Protractorand,TestingAngularJSApplicationsintheIDE
ng-apptag,BootstrappingtheApplication
ng-clickdirective,AddingBehaviorwithControllers,AddingBehaviorwithControllers
ng-includedirective,BuildingCustomDirectives
ng-modeldirective,AddingBehaviorwithControllers,AddingBehaviorwithControllers,AngularJSTemplates
ng-repeatdirective,ListServices
ng-submitdirective,FormSubmission,AddingBlogEntries
userauthenticationand,AddingaLoginTemplate
ng-viewdirective,AngularJSTemplates
dynamiccontentand,CreatingtheBlogProject
single-pageapplicationsin,Single-PageApplications,AngularJSTemplates
ngRoutemodule,DependencyInjection
Node.js,InstallingNode.js,npm,andMongoDB
dependencies,adding,AddingNode.jsDependencies
Karmaand,TestingAngularJSApplicationsintheIDE
Protractorand,TestingAngularJSApplicationsintheIDE
NoSQL,TheMEANApplication
injectionattack,AngularJSSecurity
npm(Node.jspackagemanager),InstallingNode.js,npm,andMongoDB
npminstallcommand,AddingNode.jsDependencies
O
OpenShift,MEANDeploymenttotheCloud
P
package.jsonfile,TestingAngularJSApplicationsintheIDE
performanceandSEO,BuildingCleanClientCode
PHP,MVCandAngularJS,AngularJSandSEO
presentationlogic
addingtoprojects,AddingStylesandPresentationLogic
controllersand,PresentationLogicandFormattingData,AddingMockBlogData
directivesand,AddingStylesandPresentationLogic
projects,AngularJSViewsandBootstrap-Conclusion
application,running,RunningtheBlogApplication
controllers,adding,AddingaNewBlogController
creating,CreatingtheBlogProject
directives,adding,AddingtheCustomDirective-PassingtheTitleAttribute
end-to-endtesting,End-to-EndTesting
functionality,adding,ViewingtheBlogPost-ViewingtheBlogPost
Karma,testingwith,TestingwithKarma-ProtractorTesting
menus,adding,AddingaBootstrapMenu
mockdata,adding,AddingMockBlogData
non-RESTservices,adding,BlogApplicationBusinessLogic-TestingServiceswithKarma
presentationlogic,adding,AddingStylesandPresentationLogic
Protractor,testingwith,End-to-EndTesting
RESTservices,updatingfor,UpdatingtheProjectforREST
servicemodules,adding,ModifyingApp.js
styles,addingto,AddingStylesandPresentationLogic
stylingpagesin,UsingCSS3toStylethePage
templates,adding,AddingaNewBlogTemplate
testing,TestingwithKarma-KarmaTesting
TwitterBootstrap,adding,TwitterBootstrap
unittesting,TestingwithKarma-KarmaTesting
Protractor,TestingAngularJSApplications,Protractor
businesslogicand,End-to-EndTesting
configuring,ConfiguringProtractor
directivesand,End-to-EndTesting
installing,InstallingProtractor
MEANstackdeploymentand,TestingwithKarma-KarmaTesting
models,testingwith,End-to-EndTesting
non-RESTservicesand,End-to-EndTesting
RESTservices,testingwith,End-to-EndTesting
running,RunningProtractor
securityand,End-to-EndTesting
SeleniumServerand,StartingtheSeleniumServer
testserver,starting,ProtractorTesting
testspecifications,creating,CreatingProtractorTestSpecifications,ProtractorTestSpecification
testingconsiderationsfor,TestingConsiderations
testingwith,ProtractorTesting
providerfunction,WaystoCreateAngularJSServices,CreatingAngularJSServices
publicSSHkeys,MEANDeploymenttotheCloud
Python,AngularJSandSEO
R
responsivedesign,ResponsiveDesignConsiderations-ResponsiveDesignConsiderations
HTML5mobileapplicationsand,MobileVersion
RESTservices,AngularJSandRESTServices-Conclusion
AngularJSand,AngularJSandRESTServices
asobjects,WaystoCommunicatewithRESTServices
authenticatingacrossmultiple,ServicesandBusinessLogic
ChromeDeveloperToolsand,RunningtheApplication
communicatingwith,WaystoCommunicatewithRESTServices
controllersand,RESTServicesandControllers
creatingAngularJSservices,WaystoCreateAngularJSServices
end-to-endtestingof,End-to-EndTesting
failedcalls,TheJSONResponse
JavaScriptdebuggersand,AngularJSControllers
Karma,testingwith,TestingServiceswithKarma-KarmaServiceSpecifications,KarmaServiceSpecifications
lists,displayingwith,ListServices
lists,returning,ListServices
Protractor,testingwith,End-to-EndTesting
responseobjectsfrom,TheJSONResponse
responsetimesfor,ControllerBusinessLogic,BuildingFastRESTServices
SEOand,BuildingFastRESTServices
testingspecificationsfor,KarmaServiceSpecifications
troubleshooting,RunningtheApplication
unittestsand,ModifyingtheControllers
restrictoption(directives),TheRestrictOption
routes,AngularJSRoutes
adding,forsecurity,AddingNewRoutes
templatesand,AngularJSTemplates
Ruby,AngularJSandSEO
RubyonRails,TheOldWay
S
scopeproperties,ModelProperties
displaying,ControllerBusinessLogic
errorhandlingwith,AddingaLoginController
passingvaluesto,TemplateAttributes
searchengineoptimization(SEO),AngularJSandSEO-Conclusion
clientcodeand,BuildingCleanClientCode
GoogleWebmasterTools,GoogleWebmasterTools
microformattags,MicroformatTags
performanceand,BuildingCleanClientCode
RESTservicesand,BuildingFastRESTServices
sitemaps,AddingaSitemap
tag-basednavigation,MicroformatTags
searchengines,ModernSearchEngines
security,AngularJSSecurity-Conclusion
end-to-endtestingfor,End-to-EndTesting
Karma,testingwith,TestingwithKarma-KarmaTesting
logincontrollers,AddingaLoginController
logintemplate,AddingaLoginTemplate
logoutcontrollersand,AddingaLogoutController-AddingaLogoutController
logoutlinkand,AddingaLogoutLink
modificationsofcontrollers,SecurityModificationstoOtherControllers
non-RESTservicesand,ServicesandBusinessLogic
Protractorand,End-to-EndTesting
routes,adding,AddingNewRoutes
unittesting,TestingwithKarma-KarmaTesting
userauthenticationand,Authentication
SeleniumServer,StartingtheSeleniumServer
running,Protractor
SEOcompanies,OldVersusNewAngularJSSEO
server-sidewebMVCframeworks,JavaScriptClient-SideFrameworks
servicefunction,WaystoCreateAngularJSServices,CreatingAngularJSServices
servicesmodule,UpdatingtheProjectforREST
modifyingtomakeappstransportable,MEANServices
services,non-REST,ServicesandBusinessLogic-Conclusion
checkingdatawith,CheckingUserCredentials
cookies,readingwith,RetrievingUserCredentials
creating,CreatingAngularJSServices
deletingdatawith,DeletingUserCredentials
holdingdatawith,HoldingUserCredentials
login,AddingaLoginService
MEAN,MEANServices
userauthentication,HandlingUserAuthentication-RetrievingUserCredentials
single-pageapplications,Single-PageApplications
sitemaps,AddingaSitemap
SpringMVCframework,JavaScriptClient-SideFrameworks,DependencyInjection,TheOldWay
buildingRESTserviceswith,ANewandBetterWay
integratingwithAngularJS,IntegratingAngularJSwithOtherFrameworks
SSL,AngularJSSecurity
Strutsframework,JavaScriptClient-SideFrameworks,TheOldWay
styles
addingtoprojects,AddingStylesandPresentationLogic
Bootstrapand,AddingStylesandPresentationLogic
successcallbackfunction,RESTServicesandControllers,ModifyingtheControllers
T
tag-basednavigation,MicroformatTags
templates,AngularJSTemplates
creating,CreatingtheTemplates
login,AddingaLoginTemplate
MEAN,MEANBlogTemplates
projects,addingto,AddingaNewBlogTemplate
viewsas,AngularJSTemplates
templateUrlattribute(directives),TheTemplateURL
testscripts,creating,CreatingTestScripts
testspecifications,TestingConsiderations
testing,TestingAngularJSApplications
clouddeployments,TestingtheCloudBlog
considerationsforMVC,TestingConsiderations
end-to-end,Protractor
inIDE,TestingAngularJSApplicationsintheIDE-Protractor
Karma,KarmaTestRunner
libraryfiles,locationof,JsTestRunner
Protractor,Protractor
unit,KarmaTestRunner
withJSTestDriver,TestingwithJSTestDriver
withJsTestRunner,JsTestRunner
Torvalds,Linus,Conclusion
TravisCIsystem,Protractor
testingand,TestingConsiderations
TwitterBootstrap,IntroductiontoAngularJS,AngularJSViewsandBootstrap,TwitterBootstrap
downloading,TheIDE
menus,adding,AddingaBootstrapMenu
U
unittesting,TestingAngularJSApplications,KarmaTestRunner
asynchronouscalls,ModifyingtheControllers
businesslogic,KarmaConfiguration-KarmaTesting
directives,TestingDirectiveswithKarma-KarmaTesting
MEANstackdeployment,TestingwithKarma-KarmaTesting
models,TestingServiceswithKarma-KarmaTesting
non-RESTservices,KarmaConfiguration-KarmaTesting
RESTservices,ModifyingtheControllers
security,testing,TestingwithKarma-KarmaTesting
withJSTestDriver,JSTestDriver-TestingwithJSTestDriver
userauthentication,HandlingUserAuthentication-RetrievingUserCredentials
basic,UsingBasicAuthentication
logincontrollers,AddingaLoginController
loginservices,AddingaLoginService
logintemplate,AddingaLoginTemplate
logoutcontrollersand,AddingaLogoutController-AddingaLogoutController
logoutlink,AddingaLogoutLink
securityand,AngularJSSecurity
testingwithKarma,KarmaTestSpecifications-KarmaTesting
unittesting,KarmaTestSpecifications-KarmaTesting
usercredentials
checking,CheckingUserCredentials
deleting,DeletingUserCredentials
holding,HoldingUserCredentials
retrieving,RetrievingUserCredentials
V
V8JavaScriptengine(Google),TheMEANApplication
views
astemplates,AngularJSTemplates
controllersand,AngularJSControllers(MVC)
directivesand,AddingStylesandPresentationLogic
testingwithKarma,TestingwithKarma-KarmaTesting
W
webapplications
convertingtomobile,ChoiceTwo
wrappersfor,ChoiceOne
webbrowsers,securityand,AngularJSSecurity
webframeworks,TheOldWay-ChoiceTwo
webMVCframeworks,JavaScriptClient-SideFrameworks
webdriver-managertool,StartingtheSeleniumServer
WebDriverJS,Protractor
WebViewcomponent(Android),ChoiceOne
Z
ZendFramework,TheOldWay
AbouttheAuthor
KenWilliamsonisasoftwareengineerandarchitectwithovertwentyyearsofexperienceinthetechnologyindustry.Ken’sfirstprogramminglanguagewasAssemblyusingthe6502chip.HemovedontoCandC++andeventuallytoJavaandJavaScript.Kenhasdesignedandwrittenmobile,desktop,andserversoftwareforsomeofthebiggestcompaniesintheworld.
KenholdsaBSinComputerSciencefromKennesawStateUniversity.HeisthefounderofseveralopensourceprojectsincludingUlboraCMS;hehasalsocontributedtomanyotheropensourceprojectsovertheyears.KenmakeshishomeinAtlanta,Georgiawithhiswife,Sherry.YoucanfindKenatwww.ken-williamson.com.
Colophon
TheanimalsonthecoverofLearningAngularJSareFloridacricketfrogs(Acrisgryllusdorsalis),whicharesubspeciesoftheSoutherncricketfrog.TheycanbefoundallthroughoutFlorida,withtheexceptionoftheextremenorthwesternpanhandle.
Cricketfrogspreferafreshwaterenvironment,suchaspuddles,lakes,marshes,andstreams.Theyareeasilyrecognizedbythetriangularmarkontheirheadsandthetwodarkstripesontheirrear.
BreedingoccursfromAprilintothefall,withsmallclustersofeggsattachedtosubmergedplants.Malesadvertisetheirreadinesswithaloud,rapidcallofgick,gick,gick,whichhasbeendescribedbysomeasthesoundofmarblesclickingtogether.
AdultFloridacricketfrogsgrowtobeabout1.25incheslong,andvaryincolorfromdarkbrowntotanorgreen.Theyenjoyhealthypopulationgrowthandarenotconsideredthreatenedinanyway.
ManyoftheanimalsonO’Reillycoversareendangered;allofthemareimportanttotheworld.Tolearnmoreabouthowyoucanhelp,gotoanimals.oreilly.com.
ThecoverimageisfromLydekker’sRoyalNaturalHistory.ThecoverfontsareURWTypewriterandGuardianSans.ThetextfontisAdobeMinionPro;theheadingfontisAdobeMyriadCondensed;andthecodefontisDaltonMaag’sUbuntuMono.
PrefaceWhyIWroteThisBook
WhatThisBookCovers
WhoShouldReadThisBook
TheChaptersinThisBook
ConventionsUsedinThisBook
UsingCodeExamples
Safari®BooksOnline
HowtoContactUs
1.IntroductiontoAngularJSJavaScriptClient-SideFrameworks
Single-PageApplications
BootstrappingtheApplication
DependencyInjection
AngularJSRoutes
HTML5Mode
ModernSearchEngines
AngularJSTemplates
AngularJSViews(MVC)
AngularJSModels(MVC)
AngularJSControllers(MVC)
ControllerBusinessLogic
IntegratingAngularJSwithOtherFrameworks
TestingAngularJSApplications
Conclusion
2.TheIDEandAngularJSProjectsTheIDE
EditingtheHTMLCode
EditingtheJavaScriptCode
CreatingtheTemplates
RunningtheApplications
TestingAngularJSApplicationsintheIDE
JsTestRunner
KarmaTestRunner
Protractor
Conclusion
3.MVCandAngularJSTheOldWay
ChoiceOne
ChoiceTwo
ANewandBetterWay
TestingConsiderations
ResponsiveDesignConsiderations
Conclusion
4.AngularJSControllersInitializingtheModelwithControllers
AddingBehaviorwithControllers
ControllerBusinessLogic
PresentationLogicandFormattingData
FormSubmission
UsingSubmittedFormData
JSTestDriverCreatingTestScripts
TestingwithJSTestDriver
TestingwithKarmaInstallingKarma
KarmaConfiguration
RunningKarmaUnitTests
End-to-EndTestingwithProtractorInstallingProtractor
ConfiguringProtractor
CreatingProtractorTestSpecifications
StartingtheSeleniumServer
RunningProtractor
Conclusion
5.AngularJSViewsandBootstrapAngularJSTemplates
CreatingtheBlogProject
AddingaNewBlogController
AddingaNewBlogTemplate
TwitterBootstrap
AddingaBootstrapMenu
AddingMockBlogData
UsingCSS3toStylethePage
AddingStylesandPresentationLogic
ViewingtheBlogPost
RunningtheBlogApplication
TestingwithKarmaKarmaConfiguration
KarmaTestSpecifications
KarmaTesting
End-to-EndTestingProtractorTestSpecification
ProtractorTesting
Conclusion
6.AngularJSandRESTServicesRESTServices
AngularJSandRESTServices
WaystoCreateAngularJSServices
WaystoCommunicatewithRESTServices
UpdatingtheProjectforREST
RESTServicesandControllers
TheJSONResponse
ListServices
TestingServiceswithKarmaKarmaServiceSpecifications
End-to-EndTestingProtractorConfiguration
ProtractorTestSpecification
Conclusion
7.AngularJSModelsPublicRESTServices
ChangestotheControllers
ModelProperties
BlogApplicationPublicServices
ModifyingtheHTML
ModifyingApp.js
ModifyingtheControllers
RunningtheApplication
TestingServiceswithKarmaKarmaServiceSpecifications
KarmaTesting
End-to-EndTestingProtractorTestSpecification
ProtractorTesting
Conclusion
8.ServicesandBusinessLogicHandlingUserAuthentication
UsingBasicAuthentication
CreatingAngularJSServices
HoldingUserCredentials
CheckingUserCredentials
DeletingUserCredentials
RetrievingUserCredentials
BlogApplicationBusinessLogic
UsingtheBusinessLogic
TestingServiceswithKarmaKarmaConfiguration
KarmaTestSpecifications
KarmaTesting
End-to-EndTesting
ProtractorConfiguration
ProtractorTestSpecification
ProtractorTesting
Conclusion
9.AngularJSDirectivesTheHTMLCompiler
WhatAreDirectives?
BuildingCustomDirectives
NamingConventionsforDirectives
TheRestrictOption
TheTemplateURL
TemplateAttributes
AddingtheCustomDirective
PassingtheTitleAttribute
RunningtheBlogApplication
TestingDirectiveswithKarmaKarmaConfiguration
KarmaTestSpecification
KarmaTesting
End-to-EndTestingProtractorConfiguration
ProtractorTestSpecification
ProtractorTesting
Conclusion
10.AngularJSSecurity
Authentication
AddingaLoginService
AddingaLoginController
SecurityModificationstoOtherControllers
AddingaLogoutController
AddingaLoginTemplate
AddingNewRoutes
AddingaLogoutLink
RunningtheBlogApplicationLoggingIn
TestingwithKarmaKarmaConfiguration
KarmaTestSpecifications
KarmaTesting
End-to-EndTestingProtractorConfiguration
ProtractorTestSpecification
ProtractorTesting
OneLastPointonSecurity
Conclusion
11.MEANCloudandMobileLocalDeployment
InstallingNode.js,npm,andMongoDB
InstallingtheNetBeansNode.jsPlugin
TheMEANApplication
Node.jsPublicFolder
MEANServices
MEANBlogControllers
MEANBlogTemplates
AddingComments
AddingBlogEntries
AddingNewRoutes
AddingNode.jsDependencies
RunningtheBlogApplicationLocally
TestingwithKarmaKarmaConfiguration
KarmaTestSpecifications
KarmaTesting
End-to-EndTestingProtractorConfiguration
ProtractorTestSpecification
ProtractorTesting
MEANDeploymenttotheCloud
TestingtheCloudBlog
MobileVersion
Conclusion
12.AngularJSandSEOOldVersusNewAngularJSSEO
GettingFoundbySearchEngines
GoogleWebmasterTools
AddingaSitemap
MicroformatTags
BuildingCleanClientCode
BuildingFastRESTServices
Conclusion
References
Index