angular 2 cookbook - programmer booksdowngrading angular 2 components to angular 1 directives with...
TRANSCRIPT
Angular2Cookbook
TableofContents
Angular2CookbookCreditsAbouttheAuthorAbouttheReviewerwww.PacktPub.com
Whysubscribe?CustomerFeedbackDedicationPreface
WhatthisbookcoversWhatyouneedforthisbookWhothisbookisforConventionsReaderfeedbackCustomersupport
DownloadingtheexamplecodeErrataPiracyQuestions
1.StrategiesforUpgradingtoAngular2IntroductionComponentizingdirectivesusingcontrollerAsencapsulation
GettingreadyHowtodoit...Howitworks...There'smore...Seealso
MigratinganapplicationtocomponentdirectivesGettingreadyHowtodoit...Howitworks...There'smore...Seealso
ImplementingabasiccomponentinAngularJS1.5GettingreadyHowtodoit...Howitworks...There'smore...Seealso
NormalizingservicetypesGettingreadyHowtodoit...Howitworks...There'smore...Seealso
ConnectingAngular1andAngular2withUpgradeModuleGettingreadyHowtodoit...
ConnectingAngular1toAngular2Howitworks...There'smore...Seealso
DowngradingAngular2componentstoAngular1directiveswithdowngradeComponentGettingreadyHowtodoit...Howitworks...Seealso
DowngradeAngular2providerstoAngular1serviceswithdowngradeInjectableGettingreadyHowtodoit...Seealso
2.ConqueringComponentsandDirectivesIntroductionUsingdecoratorstobuildandstyleasimplecomponent
GettingreadyHowtodoit...
WritingtheclassdefinitionWritingthecomponentclassdecorator
Howitworks...There'smore...Seealso
PassingmembersfromaparentcomponentintoachildcomponentGettingreadyHowtodoit...
ConnectingthecomponentsDeclaringinputs
Howitworks...There'smore...
AngularexpressionsUnidirectionaldatabindingMembermethods
SeealsoBindingtonativeelementattributes
Howtodoit...Howitworks...Seealso
RegisteringhandlersonnativebrowsereventsGettingreadyHowtodoit...Howitworks...There'smore...Seealso
GeneratingandcapturingcustomeventsusingEventEmitterGettingreadyHowtodoit...
CapturingtheeventdataEmittingacustomeventListeningforcustomevents
Howitworks...There'smore...Seealso
AttachingbehaviortoDOMelementswithdirectivesGettingreadyHowtodoit...
AttachingtoeventswithHostListenersHowitworks...There'smore...Seealso
ProjectingnestedcontentusingngContentGettingreadyHowtodoit...Howitworks...There'smore...Seealso
UsingngForandngIfstructuraldirectivesformodel-basedDOMcontrolGettingreadyHowtodoit...Howitworks...There'smore...Seealso
ReferencingelementsusingtemplatevariablesGettingreadyHowtodoit...
There'smore...Seealso
AttributepropertybindingGettingreadyHowtodoit...Howitworks...There'smore...Seealso
UtilizingcomponentlifecyclehooksGettingreadyHowtodoit...Howitworks...There'smore...Seealso
ReferencingaparentcomponentfromachildcomponentGettingreadyHowtodoit...Howitworks...There'smore...Seealso
Configuringmutualparent-childawarenesswithViewChildandforwardRefGettingreadyHowtodoit...
ConfiguringaViewChildreferenceCorrectingthedependencycyclewithforwardRefAddingthedisablebehavior
Howitworks...There'smore...
ViewChildrenSeealso
Configuringmutualparent-childawarenesswithContentChildandforwardRefGettingreadyHowtodoit...
ConvertingtoContentChildCorrectingdatabinding
Howitworks...There'smore...
ContentChildrenSeealso
3.BuildingTemplate-DrivenandReactiveFormsIntroductionImplementingsimpletwo-waydatabindingwithngModel
Howtodoit...Howitworks...There'smore...Seealso
ImplementingbasicfieldvalidationwithaFormControlGettingreadyHowtodoit...Howitworks...There'smore...
ValidatorsandattributedualityTaglesscontrols
SeealsoBundlingcontrolswithaFormGroup
GettingreadyHowtodoit...Howitworks...There'smore...
FormGroupvalidatorsErrorpropagation
SeealsoBundlingFormControlswithaFormArray
GettingreadyHowtodoit...Howitworks...There'smore...Seealso
ImplementingbasicformswithNgFormGettingreadyHowtodoit...
DeclaringformfieldswithngModelHowitworks...There'smore...Seealso
ImplementingbasicformswithFormBuilderandformControlNameGettingreadyHowtodoit...Howitworks...There'smore...Seealso
CreatingandusingacustomvalidatorGettingreadyHowtodoit...
Howitworks...There'smore...
RefactoringintovalidatorattributesSeealso
CreatingandusingacustomasynchronousvalidatorwithPromisesGettingreadyHowtodoit...Howitworks...There'smore...
ValidatorexecutionSeealso
4.MasteringPromisesIntroductionUnderstandingandimplementingbasicPromises
GettingreadyHowtodoit...Howitworks...There'smore...
DecoupledandduplicatedPromisecontrolResolvingaPromisetoavalueDelayedhandlerdefinitionMultiplehandlerdefinitionPrivatePromisemembers
SeealsoChainingPromisesandPromisehandlers
Howtodoit...Chainedhandlers'datahandoffRejectingachainedhandler
Howitworks...There'smore...
Promisehandlertreescatch()
SeealsoCreatingPromisewrapperswithPromise.resolve()andPromise.reject()
Howtodoit...Promisenormalization
Howitworks...There'smore...Seealso
ImplementingPromisebarrierswithPromise.all()Howtodoit...Howitworks...
There'smore...Seealso
CancelingasynchronousactionswithPromise.race()GettingreadyHowtodoit...Howitworks...Seealso
ConvertingaPromiseintoanObservableHowtodoit...Howitworks...There'smore...Seealso
ConvertinganHTTPserviceObservableintoaZoneAwarePromiseGettingreadyHowtodoit...Howitworks...Seealso
5.ReactiveXObservablesIntroduction
TheObserverPatternReactiveXandRxJSObservablesinAngular2ObservablesandPromises
BasicutilizationofObservableswithHTTPGettingreadyHowtodoit...Howitworks...
Observable<Response>TheRxJSmap()operatorSubscribe
There'smore...HotandcoldObservables
SeealsoImplementingaPublish-SubscribemodelusingSubjects
GettingreadyHowtodoit...Howitworks...There'smore...
NativeRxJSimplementationSeealso
CreatinganObservableauthenticationserviceusingBehaviorSubjectsGettingready
Howtodoit...InjectingtheauthenticationserviceAddingBehaviorSubjecttotheauthenticationserviceAddingAPImethodstotheauthenticationserviceWiringtheservicemethodsintothecomponent
Howitworks...There'smore...Seealso
BuildingageneralizedPublish-Subscribeservicetoreplace$broadcast,$emit,and$onGettingreadyHowtodoit...
IntroducingchannelabstractionHookingcomponentsintotheserviceUnsubscribingfromchannels
Howitworks...There'smore...
ConsiderationsofanObservable'scompositionandmanipulationSeealso
UsingQueryListsandObservablestofollowchangesinViewChildrenGettingreadyHowtodoit...
DealingwithQueryListsCorrectingtheexpressionchangederror
Howitworks...Hatetheplayer,notthegame
SeealsoBuildingafullyfeaturedAutoCompletewithObservables
GettingreadyHowtodoit...
UsingtheFormControlvalueChangesObservableDebouncingtheinputIgnoringserialduplicatesFlatteningObservablesHandlingunorderedresponses
Howitworks...Seealso
6.TheComponentRouterIntroductionSettingupanapplicationtosupportsimpleroutes
GettingreadyHowtodoit...
SettingthebaseURL
DefiningroutesProvidingroutestotheapplicationRenderingroutecomponentswithRouterOutlet
Howitworks...There'smore...
InitialpageloadSeealso
NavigatingwithrouterLinksGettingreadyHowtodoit...Howitworks...There'smore...
RouteorderconsiderationsSeealso
NavigatingwiththeRouterserviceGettingreadyHowtodoit...Howitworks...There'smore...Seealso
SelectingaLocationStrategyforpathconstructionHowtodoit...There'smore...
ConfiguringyourapplicationserverforPathLocationStrategyBuildingstatefulroutebehaviorwithRouterLinkActive
GettingreadyHowtodoit...Howitworks...There'smore...Seealso
ImplementingnestedviewswithrouteparametersandchildroutesGettingreadyHowtodoit...
AddingaroutingtargettotheparentcomponentDefiningnestedchildviewsDefiningthechildroutesDefiningchildviewlinksExtractingrouteparameters
Howitworks...There'smore...
RefactoringwithasyncpipesSeealso
WorkingwithmatrixURLparametersandroutingarraysGettingreadyHowtodoit...Howitworks...There'smore...Seealso
AddingrouteauthenticationcontrolswithrouteguardsGettingreadyHowtodoit...
ImplementingtheAuthserviceWiringuptheprofileviewRestrictingrouteaccesswithrouteguardsAddingloginbehaviorAddingthelogoutbehavior
Howitworks...There'smore...
TheactualauthenticationSecuredataandviews
Seealso7.Services,DependencyInjection,andNgModule
IntroductionInjectingasimpleserviceintoacomponent
GettingreadyHowtodoit...Howitworks...There'smore...Seealso
ControllingserviceinstancecreationandinjectionwithNgModuleGettingreadyHowtodoit...
SplittinguptherootmoduleHowitworks...There'smore...
InjectingdifferentserviceinstancesintodifferentcomponentsServiceinstantiation
SeealsoServiceinjectionaliasingwithuseClassanduseExisting
GettingreadyDualservicesAunifiedcomponent
Howtodoit...Howitworks...
There'smore...Refactoringwithdirectiveproviders
SeealsoInjectingavalueasaservicewithuseValueandOpaqueTokens
GettingreadyHowtodoit...Howitworks...There'smore...Seealso
Buildingaprovider-configuredservicewithuseFactoryGettingreadyHowtodoit...
DefiningthefactoryInjectingOpaqueTokenCreatingproviderdirectiveswithuseFactory
Howitworks...There'smore...Seealso
8.ApplicationOrganizationandManagementIntroductionComposingpackage.jsonforaminimumviableAngular2application
GettingreadyHowtodoit...
package.jsondependenciespackage.jsondevDependenciespackage.jsonscripts
SeealsoConfiguringTypeScriptforaminimumviableAngular2application
GettingreadyHowtodoit...
Declarationfilestsconfig.json
Howitworks...Compilation
There'smore...SourcemapgenerationSinglefilecompilation
SeealsoPerformingin-browsertranspilationwithSystemJS
GettingreadyHowtodoit...Howitworks...
There'smore...Seealso
ComposingapplicationfilesforaminimumviableAngular2applicationGettingreadyHowtodoit...
app.component.tsapp.module.tsmain.tsindex.htmlConfiguringSystemJS
SeealsoMigratingtheminimumviableapplicationtoWebpackbundling
GettingreadyHowtodoit...
webpack.config.jsSeealso
IncorporatingshimsandpolyfillsintoWebpackGettingreadyHowtodoit...Howitworks...Seealso
HTMLgenerationwithhtml-webpack-pluginGettingreadyHowtodoit...Howitworks...Seealso
SettingupanapplicationwithAngularCLIGettingreadyHowtodoit...
RunningtheapplicationlocallyTestingtheapplication
Howitworks...ProjectconfigurationfilesTypeScriptconfigurationfilesTestconfigurationfilesCoreapplicationfilesEnvironmentfilesAppComponentfilesAppComponenttestfiles
There'smore...Seealso
9.Angular2Testing
IntroductionCreatingaminimumviableunittestsuitewithKarma,Jasmine,andTypeScript
GettingreadyHowtodoit...
WritingaunittestConfiguringKarmaandJasmineConfiguringPhantomJSCompilingfilesandtestswithTypeScriptIncorporatingWebpackintoKarmaWritingthetestscript
Howitworks...There'smore...Seealso
WritingaminimumviableunittestsuiteforasimplecomponentGettingreadyHowtodoit...
UsingTestBedandasyncCreatingaComponentFixture
Howitworks...Seealso
Writingaminimumviableend-to-endtestsuiteforasimpleapplicationGettingreadyHowtodoit...
GettingProtractorupandrunningMakingProtractorcompatiblewithJasmineandTypeScriptBuildingapageobjectWritingthee2etestScriptingthee2etests
Howitworks...There'smore...Seealso
UnittestingasynchronousserviceGettingreadyHowtodoit...Howitworks...There'smore...
TestingwithoutinjectionSeealso
UnittestingacomponentwithaservicedependencyusingstubsGettingreadyHowtodoit...
Stubbingaservicedependency
TriggeringeventsinsidethecomponentfixtureHowitworks...Seealso
UnittestingacomponentwithaservicedependencyusingspiesGettingreadyHowtodoit...
SettingaspyontheinjectedserviceHowitworks...There'smore...Seealso
10.PerformanceandAdvancedConceptsIntroductionUnderstandingandproperlyutilizingenableProdModewithpureandimpurepipes
GettingreadyHowtodoit...
GeneratingaconsistencyerrorIntroducingchangedetectioncomplianceSwitchingonenableProdMode
Howitworks...There'smore...Seealso
WorkingwithzonesoutsideAngularGettingreadyHowtodoit...
ForkingazoneOverridingzoneeventswithZoneSpec
Howitworks...There'smore...
Understandingzone.run()Microtasksandmacrotasks
SeealsoListeningforNgZoneevents
zone.jsNgZoneGettingreadyHowtodoit...
DemonstratingthezonelifecycleHowitworks...
Theutilityofzone.jsSeealso
ExecutionoutsidetheAngularzoneHowtodoit...
Howitworks...There'smore...Seealso
ConfiguringcomponentstouseexplicitchangedetectionwithOnPushGettingreadyHowtodoit...
ConfiguringtheChangeDetectionStrategyRequestingexplicitchangedetection
Howitworks...There'smore...Seealso
ConfiguringViewEncapsulationformaximumefficiencyGettingreadyHowtodoit...
EmulatedstylingencapsulationNostylingencapsulationNativestylingencapsulation
Howitworks...There'smore...Seealso
ConfiguringtheAngular2RenderertousewebworkersGettingreadyHowtodoit...Howitworks...There'smore...
OptimizingforperformancegainsCompatibilityconsiderations
SeealsoConfiguringapplicationstouseahead-of-timecompilation
GettingreadyHowtodoit...
InstallingAOTdependenciesConfiguringngcAligningcomponentdefinitionswithAOTrequirementsCompilingwithngcBootstrappingwithAOT
Howitworks...There'smore...
GoingfurtherwithTreeShakingSeealso
ConfiguringanapplicationtouselazyloadingGettingready
Howtodoit...Howitworks...There'smore...
AccountingforsharedmodulesSeealso
Angular2Cookbook
Angular2CookbookCopyright©2017PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:January2017
Productionreference:1160117
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
Birmingham
B32PB,UK.
ISBN978-1-78588-192-3
www.packtpub.com
CreditsAuthor
MattFrisbie
ProjectCoordinator
RitikaManoj
Reviewer
PatrickGillespie
Proofreader
SafisEditing
AcquisitionEditor
VinayArgekar
Indexer
FrancyPuthiry
ContentDevelopmentEditor
ArunNadar
Graphics
KirkD'Penha
TechnicalEditor
VivekArora
ProductionCoordinator
DeepikaNaik
CopyEditor
GladsonMonteiro
CoverWork
DeepikaNaik
AbouttheAuthorMattFrisbieiscurrentlyasoftwareengineeratGoogle.HewastheauthorofthePacktPublishingbestsellerAngularJSWebApplicationDevelopmentCookbookandalsohaspublishedseveralvideoseriesthroughO'Reilly.HeisactiveintheAngularcommunity,givingpresentationsatmeetupsanddoingwebcasts.
WritingabookonAngular2whiletheframeworkitselfwasunfinishedwasanimmenselychallengingendeavor.Fragmentedexamples,incompletedocumentation,andanascentdevelopercommunitywerejustahandfulofthemanyroadblocksIencounteredonthejourneytofinishingthistitle,anditwasonlybecauseofalegionofsupportersthatthisbookwasfinishedandwasabletodojusticetotheframework.
ThisbookwouldnothavebeenpossiblewithoutthetirelessworkofallthePacktstaffinvolved.I'dspecificallyliketothankArunNadar,VivekArora,MerwynD'Souza,andVinayArgekarfortheireditorialoversightandexpertise,aswellasPatrickGillespieforhisworkascontentreviewer.I'dalsoliketothankJordan,Zoey,Scott,andmyfamilyandfriendsforcheeringmeon.
AbouttheReviewerPatrickGillespiehasbeenintosoftwaredevelopmentsince1996.Hehasbothabachelor'sandamaster'sdegreeincomputerscience.Inhissparetime,heenjoysphotography,spendingtimewithhisfamily,andworkingonvarioussideprojectsforhiswebsite(http://patorjk.com/).
www.PacktPub.comForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusatservice@packtpub.comformoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www.packtpub.com/mapt
Getthemostin-demandsoftwareskillswithMapt.MaptgivesyoufullaccesstoallPacktbooksandvideocourses,aswellasindustry-leadingtoolstohelpyouplanyourpersonaldevelopmentandadvanceyourcareer.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
CustomerFeedbackThankyouforpurchasingthisPacktbook.Wetakeourcommitmenttoimprovingourcontentandproductstomeetyourneedsseriously—that'swhyyourfeedbackissovaluable.Whateveryourfeelingsaboutyourpurchase,pleaseconsiderleavingareviewonthisbook'sAmazonpage.Notonlywillthishelpus,moreimportantlyitwillalsohelpothersinthecommunitytomakeaninformeddecisionabouttheresourcesthattheyinvestintolearn.
Youcanalsoreviewforusonaregularbasisbyjoiningourreviewers'club.Ifyou'reinterestedinjoining,orwouldliketolearnmoreaboutthebenefitsweoffer,pleasecontactus:[email protected].
DedicationTomygrandparents,RichardandMargery.Here'stoupholdingthefamilyhonor.
Preface"Everybodyhasaplanuntiltheygetpunchedinthemouth."-MikeTyson,undisputedheavyweightchampionboxer
Soonafteritscreationin2009,AngularJSgrewintoawidelypopularfoundationaltoolforbuildingfrontendapplications.Asyearsandreleaseswentby,andtheJavaScriptcommunitymatured,theworldofclient-sideprogrammingbroadenedbeyondwhatAngularwasoriginallydesignedfor.Itscaretakerstookstockanddecidedthatasweepingoverhauloftheframeworkwasinorder.
AngularJS,nowAngular1,stillexistsandwillbesupportedfortheyearstocome,butinitswakeliesAngular2—awhollydifferentanimalbuiltforthefutureofclient-sidecomputing.Angular2abandonsantipatternsbythefistfuland,instead,isreshapedintoapreciseandelegantsoftwareinstrument.Itembracestheimpendingrenaissanceofwebtechnologies,buildingatopES6,webcomponents,webworkers,TypeScript,andreactiveprogramming,tonameafew.Itbringsframeworkmodularitytonewheights,buildingitselfaroundtheconceptthatanymodularpieceofAngular2shouldbeeasilydiscardedorreplaced.Bestofall,Angular2offersabountifulcollectionofconfigurationandtoolingthatwillmakeyourapplicationsrunatbreakneckspeed.
Tomanydevelopers,Angular2isfrighteningbecausesomuchofitisnewandunfamiliar.ThisbookexiststoofferyouanapproachablepathtoafullunderstandingofAngular2,whatitoffers,andhowbesttouseit.Youwillfindbothsimpleexamplestosetafoundationalunderstanding,andcomplexdemonstrationstohintattheframework'spower.Thebookisorganizedintorecipesthatareindependentofeachother,soyouareabletojumpinatanypointandimmediatelybeginlearning.
WhatthisbookcoversThisbookisuptodateforthe2.4releaseandiscompatiblethroughthe4.0releaseaswell,anditdoesnothaveanycodebasedonthebetaorreleasecandidates.
Chapter1,StrategiesforUpgradingtoAngular2,isanoverviewofanumberofwaystomigrateanAngular1applicationtoAngular2.Althoughthereisnoone-size-fits-allupgradestrategy,youwillfindthattheserecipesdemonstratesomewaysthatwillallowyoutopreservealargeamountofyourexistingAngular1codebase.
Chapter2,ConqueringComponentsandDirectives,givesabroadanddeepsetofexamplesinvolvingwhatAngular2componentsareandhowtousethem.Angular2applicationsarebuiltentirelyofcomponents,andthischapteroffersyouatotalrundownoftheirrole.
Chapter3,BuildingTemplate-DrivenandReactiveForms,coversthereworkedAngular2formmodules.Angular2offersyoutwoprimarystylesoferectingformfeatures,andthischaptercoversbothofthemindepth.
Chapter4,MasteringPromises,showshowthePromiseobjecthasaroleinAngular2.AlthoughRxJShassubsumedsomeoftheusefulnessofPromises,theyarestillfirst-classcitizensinES6andstillplayacrucialrole.
Chapter5,ReactiveXObservables,givesyouacrashcourseinhowAngular2hasembracedreactiveprogramming.ItincludesrecipesthatdemonstratethebasicsofObservablesandSubjects,aswellasadvancedimplementationsthattakeRxJStoitslimits.
Chapter6,TheComponentRouter,takesyouthroughthetotallyreworkedroutingmoduleinAngular2.ItcoversbothroutingbasicsaswellasanarrayofadvancedroutingconceptsuniquetoAngular2.
Chapter7,Services,DependencyInjection,andNgModule,describesthenewandimproveddependencyinjectionandmodulestrategiesofAngular2.Itgivesyouallthepiecesyouneedtobreakyourapplicationintoindependentservicesandmodules,aswellasidealstrategiesforconnectingthosepiecestogether.
Chapter8,ApplicationOrganizationandManagement,isabroadoverviewofhowyoucanmanageyourAngular2applicationinsideandoutsidetheclient.Angular2introducesanumberoflayersofcomplexitythatrequireadvancedtooling,andthischapterwillguideyouthroughhowtoapproachthem.
Chapter9,Angular2Testing,willguideyouthroughbothhowtosetuptestsuitesforAngular2aswellashowtowritevarioustypesoftestsforthesesuites.Manydevelopersavoidtestingwhenlearningaframeworkanew,andthischaptergentlyguidesyouthroughAngular2'sexcellent
testinfrastructure.
Chapter10,PerformanceandAdvancedConcepts,isacrashcourseonthedizzyingarrayofcomplexconceptsthatAngular2comeswithoutofthebox.Thischaptercoversprogramorganizationandarchitecture,frameworkfeaturesandtooling,aswellascompile-timeoptimizations.
WhatyouneedforthisbookEveryrecipeinthisbookisaccompaniedbyalinktothebook'scompanionsite,http://ngcookbook.herokuapp.com/.RecipesthatinvolvecodeexampleswillincludealinktoaliveexampleonPlunker.Thiswillallowyoutoinspectandtestcodeinrealtimewithouthavingtoworryaboutcompilation,localservers,oranythingofthatilk.Itmustbenoted,however,thatthissetupisonlyappropriateforexperimentationandshouldnotbeusedforuser-facingorproductionapplications.
Angular2comesinbothJavaScriptandTypeScriptflavors,butthisbookaimsdirectlyattheTypeScriptedition,sinceitissyntacticallysuperior(asyouwillsoonrealize).Forproperproductionapplications,TypeScriptwillbecompiledintoJavaScriptbeforeitisservedtothebrowser.Thewaythisbookaccomplishesthis(andmanyothercodepreparationtasks)isinsideaNode.jsinstallonyourlocalmachine.Node.jsincludestheNodePackageManager(npm),whichletsyouinstallandrunopensourceJavaScriptsoftwarefromthecommandline.
SomechaptersinthisbookwillrequirethatyouhaveNode.jsinstalledbeforerunningcommandsandlaunchingalocalserverortestsuite.Furthermore,itisrecommended(butnotrequired)thatyouinstalltheNodeVersionManagerontopofNode.js,whichwillmakemanagingyourinstalledpackagesmucheasier.
WhothisbookisforTheuniverseofAngular2learningmaterialsiscurrentlyfragmentedandgross.Thisbookisforbothbeginnerdeveloperslookingtosinktheirteethintoanewframework,aswellasadvanceddevelopersinterestedinroundingouttheirknowledgeofaframeworkthatembracesthecomingworldofwebtech.
Fornewerdevelopers,ingestingallthesenewtechnologiesatoncemayseemoverwhelming.Theorganizationandpaceofthisbookisdesignedsothattopicsaregraduallyintroduced,anddesigndecisionsandrationalesareexplained.Don'tworry,thisbookisstillforyou.
ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:"Karmareadsitsconfigurationoutofakarma.conf.jsfile."
Ablockofcodeissetasfollows:
<p>{{date}}</p>
<h1>{{title}}</h1>
<h3>Writtenby:{{author}}</h3>
Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:
@Component({
selector:'article',
template:`
<p>{{currentDate|date}}</p>
<h1>{{title}}</h1>
<h3>Writtenby:{{author}}</h3>
`
})
Anycommand-lineinputoroutputiswrittenasfollows:
npminstallkarmajasmine-corekarma-jasmine--save-dev
npminstallkarma-cli-g
Newtermsandimportantwordsareshowninbold.
Note
Warningsorimportantnotesappearinaboxlikethis.
Tip
Tipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook-whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.Tosendusgeneralfeedback,[email protected],andmentionthebook'stitleinthesubjectofyourmessage.Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforthisbookfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Youcandownloadthecodefilesbyfollowingthesesteps:
1. Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.2. HoverthemousepointerontheSUPPORTtabatthetop.3. ClickonCodeDownloads&Errata.4. EnterthenameofthebookintheSearchbox.5. Selectthebookforwhichyou'relookingtodownloadthecodefiles.6. Choosefromthedrop-downmenuwhereyoupurchasedthisbookfrom.7. ClickonCodeDownload.
Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux
ThecodebundleforthebookisalsohostedonGitHubathttps://github.com/PacktPublishing/Angular-2-Cookbook.Wealsohaveothercodebundlesfromourrichcatalogofbooksandvideosavailableathttps://github.com/PacktPublishing/.Checkthemout!
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks-maybeamistakeinthetextorthecode-wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.
Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusatcopyright@packtpub.comwithalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
QuestionsIfyouhaveaproblemwithanyaspectofthisbook,[email protected],andwewilldoourbesttoaddresstheproblem.
Chapter1.StrategiesforUpgradingtoAngular2Thischapterwillcoverthefollowingrecipes:
ComponentizingdirectivesusingthecontrollerAsencapsulationMigratinganapplicationtocomponentdirectivesImplementingabasiccomponentinAngularJS1.5NormalizingservicetypesConnectingAngular1andAngular2withUpgradeModuleDowngradingAngular2componentstoAngular1directiveswithdowngradeComponentDowngradingAngular2providerstoAngular1serviceswithdowngradeInjectable
IntroductionTheintroductionofAngular2intotheAngularecosystemwillsurelybeinterpretedandhandleddifferentlyforalldevelopers.SomewillsticktotheirexistingAngular1codebases,somewillstartbrandnewAngular2codebases,andsomewilldoagradualorpartialtransition.
ItisrecommendedthatyoubecomefamiliarwiththebehaviorofAngular2componentsbeforeyoudiveintotheserecipes.ThiswillhelpyouframementalmodelsasyouadaptyourexistingapplicationstobemorecompliantwiththeAngular2style.
ComponentizingdirectivesusingcontrollerAsencapsulationOneoftheunusualconventionsintroducedinAngular1wastherelationshipbetweendirectivesandthedatatheyconsumed.Bydefault,directivesusedaninheritedscope,whichsuitedtheneedsofmostdevelopersjustfine.Whilethiswaseasytouse,ithadtheeffectofintroducingextradependenciesinthedirectives,andalsotheconventionthatdirectivesoftendidnotownthedatatheywereconsuming.Additionally,thedatainterpolatedinthetemplatewasunclearinrelationtowhereitwasbeingassignedorowned.
Angular2utilizescomponentsasthebuildingblocksoftheentireapplication.Thesecomponentsareclass-basedandarethereforeinsomewaysatoddswiththescopemechanismsofAngular1.Transitioningtoacontroller-centricdirectivemodelisalargesteptowardscompliancewiththeAngular2standards.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/8194.
GettingreadySupposeyourapplicationcontainsthefollowingsetupthatinvolvesthenesteddirectivesthatsharedatausinganisolatescope:
[index.html]
<divng-app="articleApp">
<article></article>
</div>
[app.js]
angular.module('articleApp',[])
.directive('article',function(){
return{
controller:function($scope){
$scope.articleData={
person:{firstName:'Jake'},
title:'LesothoYachtClubMembershipBooms'
};
},
template:`
<h1>{{articleData.title}}</h1>
<attributionauthor="articleData.person.firstName">
</attribution>
`
};
})
.directive('attribution',function(){
return{
scope:{author:'='},
template:`<p>Writtenby:{{author}}</p>`
};
});
Howtodoit...Thegoalistorefactorthissetupsothattemplatescanbeexplicitaboutwherethedataiscomingfromandsothatthedirectiveshaveownershipofthisdata:
[app.js]
angular.module('articleApp',[])
.directive('article',function(){
return{
controller:function(){
this.person={firstName:'Jake'};
this.title='LesothoYachtClubMembershipBooms';
},
controllerAs:'articleCtrl',
template:`
<h1>{{articleCtrl.title}}</h1>
<attribution></attribution>
`
};
})
.directive('attribution',function(){
return{
template:`<p>Writtenby:{{articleCtrl.author}}</p>`
};
});
Inthissecondimplementation,anywhereyouusethearticledata,youarecertainofitsorigin.Thisisbetter,butthechilddirectiveisstillreferencingtheparentcontroller,whichisn'tidealsinceitisintroducinganunneededdependency.Theattributiondirectiveinstanceshouldbeprovidedwiththedata,anditshouldinsteadinterpolatefromitsowncontrollerinstance:
[app.js]
angular.module('articleApp',[])
.directive('article',function(){
return{
controller:function(){
this.person={firstName:'Jake'};
this.title='LesothoYachtClubMembershipBooms';
},
controllerAs:'articleCtrl',
template:`
<h1>{{articleCtrl.title}}</h1>
<attributionauthor="articleCtrl.person.firstName">
</attribution>
`
};
})
.directive('attribution',function(){
return{
controller:function(){},
controllerAs:'attributionCtrl',
bindToController:{author:'='},
template:`<p>Writtenby:{{attributionCtrl.author}}</p>`
};
});
Muchbetter!Youprovidethechilddirectivewithastand-incontrollerandgiveitanaliasintheattributionCtrltemplate.ItisimplicitlyboundtothecontrollerinstanceviabindToControllerinthesamewayyouwouldaccomplisharegularisolatescope;however,thebindingisdirectlyattributedtothecontrollerobjectinsteadofthescope.
Nowthatyouhaveintroducedthenotionofdataownership,supposeyouwanttomodifyyourapplicationdata.What'smore,youwantdifferentpartsofyourapplicationtobeabletomodifyit.Anaïveimplementationofthiswouldbesomethingasfollows:
[app.js]
angular.module('articleApp',[])
.directive('attribution',function(){
return{
controller:function(){
this.capitalize=function(){
this.author=this.author.toUpperCase();
}
},
controllerAs:'attributionCtrl',
bindToController:{author:'='},
template:`
<png-click="attributionCtrl.capitalize()">
Writtenby:{{attributionCtrl.author}}
</p>`
};
});
Thedesiredbehaviorisforyoutoclickontheauthor,anditwillbecomecapitalized.However,inthisimplementation,thearticlecontroller'sdataismodifiedintheattributioncontroller,whichdoesnotownit.Itispreferableforthecontrollerthatownsthedatatoperformtheactualmodificationandinsteadsupplyaninterfacethatanoutsideentity—here,theattributiondirective—coulduse:
[app.js]
angular.module('articleApp',[])
.directive('article',function(){
return{
controller:function(){
this.person={firstName:'Jake'};
this.title='LesothoYachtClubMembershipBooms';
this.capitalize=function(){
this.person.firstName=
this.person.firstName.toUpperCase();
};
},
controllerAs:'articleCtrl',
template:`
<h1>{{articleCtrl.title}}</h1>
<attributionauthor="articleCtrl.person.firstName"
upper-case-author="articleCtrl.capitalize()">
</attribution>
`
};
})
.directive('attribution',function(){
return{
controller:function(){},
controllerAs:'attributionCtrl',
bindToController:{
author:'=',
upperCaseAuthor:'&'
},
template:`
<png-click="attributionCtrl.upperCaseAuthor()">
Writtenby:{{attributionCtrl.author}}
</p>`
};
});
Vastlysuperior!Youarestillabletonamespacewithintheclickbinding,buttheowningdirectivecontrollerisprovidingamethodtooutsideentitiesinsteadofjustgivingthemdirectdataaccess.
Howitworks...Whenacontrollerisspecifiedinthedirectivedefinitionobject,onewillbeexplicitlyinstantiatedforeachdirectiveinstancethatiscreated.Thus,itisnaturalforthiscontrollerobjecttoencapsulatethedatathatitownsandforittobedelegatedtheresponsibilityofpassingitsdatatothemembersthatrequireit.
Thefinalimplementationaccomplishesseveralthings:
Improvedtemplatenamespacing:Whenyouusethe$scopepropertiesthatspanmultipledirectivesornestings,youarecreatingascenariowheremultipleentitiescanmanipulateandreaddatawithoutbeingabletoconcretelyreasonaboutwhereitiscomingfromorwhatiscontrollingit.Improvedtestability:Ifyoulookateachofthedirectivesinthefinalimplementation,you'llfindtheyarenottoodifficulttotest.Theattributiondirectivehasnodependenciesotherthanwhatareexplicitlypassedtoit.Encapsulation:Introducingthenotionofdataownershipinyourapplicationaffordsyouamuchmorerobuststructure,betterreusability,andadditionalinsightandcontrolinvolvingpiecesofyourapplicationinteracting.Angular2style:Angular2usesthe@Inputand@Outputannotationsoncomponentdefinitions.Mirroringthisstylewillmaketheprocessoftransitioningtoanapplicationeasier.
There'smore...Youwillnoticethat$scopehasbeenmadetotallyirrelevantintheseexamples.Thisisgoodasthereisnonotionof$scopeinAngular2,whichmeansyouareheadingtowardshavinganupgradeableapplication.Thisisnottosaythat$scopedoesnotstillhaveutilityinanAngular1application,andsurely,therearescenarioswherethisisunavoidable,likewith$scope.$apply().
However,thinkingabouttheapplicationpiecesinthiscomponentstylewillallowyoutobemoreadequatelypreparedtoadoptAngular2conventions.
SeealsoMigratinganapplicationtocomponentdirectivesdemonstrateshowtorefactorAngular1toacomponentstyleImplementingabasiccomponentinAngularJS1.5detailshowtowriteanAngular1componentNormalizingservicetypesgivesinstructiononhowtoalignyourAngular1servicetypesforAngular2compatibility
MigratinganapplicationtocomponentdirectivesInAngular1,thereareseveralbuilt-indirectives,includingngControllerandngInclude,thatdeveloperstendtoleanonwhenbuildingapplications.Whilenotanti-patterns,usingthesefeaturesmovesawayfromhavingacomponent-centricapplication.
Allthesedirectivesareactuallysubsetsofcomponentfunctionality,andtheycanbeentirelyrefactoredout.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/1008/.
GettingreadySupposeyourinitialapplicationisasfollows:
[index.html]
<divng-app="articleApp">
<ng-includesrc="'/press_header.html'"></ng-include>
<divng-controller="articleCtrlasarticle">
<h1>{{article.title}}</h1>
<p>Writtenby:{{article.author}}</p>
</div>
<scripttype="text/ng-template"
id="/press_header.html">
<divng-controller="headerCtrlasheader">
<strong>
AngularChronicle-{{header.currentDate|date}}
</strong>
<hr/>
</div>
</script>
</div>
[app.js]
angular.module('articleApp',[])
.controller('articleCtrl',function(){
this.title='FoodFightEruptsDuringDiplomaticLuncheon';
this.author='Jake';
})
.controller('headerCtrl',function(){
this.currentDate=newDate();
});
Note
NotethatthisexampleapplicationcontainsalargenumberofverycommonAngular1patterns;youcanseethengControllerdirectivessprinkledthroughout.Also,itusesanngIncludedirectivetoincorporateaheader.Keepinmindthatthesedirectivesarenotinappropriateforawell-formedAngular1application.However,youcandobetter,andthisinvolvesrefactoringtoacomponent-drivendesign.
Howtodoit...Component-drivenpatternsdon'tneedtobefrighteninginappearance.Inthisexample(andforessentiallyallAngular1applications),youcandoacomponentrefactorwhileleavingtheexistingtemplatelargelyintact.
BeginwiththengIncludedirective.Movingthistoacomponentdirectiveissimple—itbecomesadirectivewithtemplateUrlsettothetemplatepath:
[index.html]
<divng-app="articleApp">
<header></header>
<divng-controller="articleCtrlasarticle">
<h1>{{article.title}}</h1>
<p>Writtenby:{{article.author}}</p>
</div>
<scripttype="text/ng-template"
id="/press_header.html">
<divng-controller="headerCtrlasheader">
<strong>
AngularChronicle-{{header.currentDate|date}}
</strong>
<hr/>
</div>
</script>
</div>
[app.js]
angular.module('articleApp',[])
.controller('articleCtrl',function(){
this.title='FoodFightEruptsDuringDiplomaticLuncheon';
this.author='Jake';
})
.controller('headerCtrl',function(){
this.currentDate=newDate();
})
.directive('header',function(){
return{
templateUrl:'/press_header.html'
};
});
Next,youcanalsorefactorngControllereverywhereitappears.Inthisexample,youfindtwoextremelycommonappearancesofngController.Thefirstisattheheadofthepress_header.htmltemplate,actingasthetop-levelcontrollerforthattemplate.Often,thisresultsinneedingasuperfluouswrapperelementjusttohousetheng-controllerattribute.ThesecondisngControllernestedinsideyourprimaryapplicationtemplate,controllingsomearbitraryportionoftheDOM.Bothofthesecanberefactoredtocomponentdirectivesby
reassigningngControllertoadirectivecontroller:
[index.html]
<divng-app="articleApp">
<header></header>
<article></article>
</div>
[app.js]
angular.module('articleApp',[])
.directive('header',function(){
return{
controller:function(){
this.currentDate=newDate();
},
controllerAs:'header',
template:`
<strong>
AngularChronicle-{{header.currentDate|date}}
</strong>
<hr/>
`
};
})
.directive('article',function(){
return{
controller:function(){
this.title='FoodFightEruptsDuringDiplomaticLuncheon';
this.author='Jake';
},
controllerAs:'article',
template:`
<h1>{{article.title}}</h1>
<p>Writtenby:{{article.author}}</p>
`
};
});
Tip
Notethattemplateshereareincludedinthedirectiveforvisualcongruity.Forlargeapplications,itispreferredthatyouusetemplateUrlandlocatethetemplatemarkupinitsownfile.
Howitworks...Generallyspeaking,anapplicationcanberepresentedbyahierarchyofnestedMVCcomponents.ngIncludeandngControlleractassubsetsofacomponentfunctionality,andsoitmakessensethatyouareabletoexpandthemintofullcomponentdirectives.
Intheprecedingexample,theultimateapplicationstructureiscomprisedofonlycomponents.Eachcomponentisdelegateditsowntemplate,controller,andmodel(byvirtueofthecontrollerobjectitself).SticklerswilldisputewhetherornotAngularbelongstotrueMVCstyle,butinthecontextofcomponentrefactoring,thisisirrelevant.Here,youhavedefinedastructurethatiscompletelymodular,reusable,testable,abstractable,andeasilymaintainable.ThisisthestyleofAngular2,andthevalueofthisshouldbeimmediatelyapparent.
There'smore...Analertdeveloperwillnoticethatnoattentionispaidtoscopeinheritance.Thisisadifficultproblemtoapproach,mostlybecausemanyofthepatternsinAngular1aredesignedforamishmashbetweenascopeandcontrollerAs.Angular2isbuiltaroundstrictinputandoutputbetweennestedcomponents;however,inAngular1,scopeisinheritedbydefault,andnesteddirectives,bydefault,haveaccesstotheirencompassingcontrollerobjects.
Thus,totrulyemulateanAngular2style,onemustconfiguretheirapplicationtoexplicitlypassdataandmethodstochildren,similartothecontrollerAsencapsulationrecipe.However,thisdoesnotprecludedirectdataaccesstoancestralcomponentdirectivecontrollers;itmerelywagsafingeratitsinceitaddsadditionaldependencies.
SeealsoComponentizingdirectivesusingcontrollerAsencapsulationshowsyouasuperiormethodoforganizingAngular1directivesImplementingabasiccomponentinAngularJS1.5detailshowtowriteanAngular1componentNormalizingservicetypesgivesinstructiononhowtoalignyourAngular1servicetypesforAngular2compatibility
ImplementingabasiccomponentinAngularJS1.5The1.5releaseofAngularJSintroducedanewtool:thecomponent.Whileitisn'texactlysimilartotheconceptoftheAngular2component,itdoesallowyoutobuilddirective-stylepiecesinanexplicitlycomponentizedfashion.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/7756/.
GettingreadySupposeyourapplicationhadadirectivedefinedasfollows:
[index.html]
<divng-app="articleApp">
<article></article>
</div>
[app.js]
angular.module('articleApp',[])
.directive('article',function(){
return{
controller:function(){
this.person={firstName:'Jake'};
this.title='PoliceBustIllegalSnailRacingRing';
this.capitalize=function(){
this.person.firstName=
this.person.firstName.toUpperCase();
};
},
controllerAs:'articleCtrl',
template:`
<h1>{{articleCtrl.title}}</h1>
<attributionauthor="articleCtrl.person.firstName"
upper-case-author="articleCtrl.capitalize()">
</attribution>
`
};
})
.directive('attribution',function(){
return{
controller:function(){},
controllerAs:'attributionCtrl',
bindToController:{
author:'=',
upperCaseAuthor:'&'
},
template:`
<png-click="attributionCtrl.upperCaseAuthor()">
Writtenby:{{attributionCtrl.author}}
</p>`
};
});
Howtodoit...SincethisapplicationisalreadyorganizedaroundthecontrollerAsencapsulation,youcanmigrateittousethecomponent()definitionintroducedintheAngular1.5release.
Componentsacceptanobjectdefinitionsimilartoadirective,buttheobjectdoesnotdemandtobereturnedbyafunction—anobjectliteralisallthatisneeded.ComponentsutilizethebindingspropertyinthisobjectdefinitionobjectinthesamewaythatbindToControllerworksfordirectives.Withthis,youcaneasilyintroducecomponentsinthisapplicationinsteadofdirectives:
[index.html]
<divng-app="articleApp">
<article></article>
</div>
[app.js]
angular.module('articleApp',[])
.component('article',{
controller:function(){
this.person={firstName:'Jake'};
this.title='PoliceBustIllegalSnailRacingRing';
this.capitalize=function(){
this.person.firstName=
this.person.firstName.toUpperCase();
};
},
controllerAs:'articleCtrl',
template:`
<h1>{{articleCtrl.title}}</h1>
<attributionauthor="articleCtrl.person.firstName"
upper-case-author="articleCtrl.capitalize()">
</attribution>`
})
.component('attribution',{
controller:function(){},
controllerAs:'attributionCtrl',
bindings:{
author:'=',
upperCaseAuthor:'&'
},
template:`
<png-click="attributionCtrl.upperCaseAuthor()">
Writtenby:{{attributionCtrl.author}}
</p>
`
});
Howitworks...Noticethatsinceyourcontroller-centricdataorganizationmatcheswhatacomponentdefinitionexpects,notemplatemodificationsarenecessary.Components,bydefault,willutilizeanisolatescope.What'smore,theywillnothaveaccesstothealiasofthesurroundingcontrollerobjects,somethingthatcannotbesaidforcomponent-styledirectives.Thisencapsulationisanimportantofferingofthenewcomponentfeature,asithasdirectparitytohowcomponentsoperateinAngular2.
There'smore...Sinceyouhavenowentirelyisolatedeachindividualcomponent,thereisonlyasinglecontrollerobjecttodealwithineachtemplate.Thus,Angular1.5automaticallyprovidesaconvenientaliasforthecomponent'scontrollerobject,namely—$ctrl.ThisisprovidedwhetherornotacontrollerAsaliasisspecified.Therefore,afurtherrefactoringyieldsthefollowing:
[index.html]
<divng-app="articleApp">
<article></article>
</div>
[app.js]
angular.module('articleApp',[])
.component('article',{
controller:function(){
this.person={firstName:'Jake'};
this.title='PoliceBustIllegalSnailRacingRing';
this.capitalize=function(){
this.person.firstName=
this.person.firstName.toUpperCase();
};
},
template:`
<h1>{{$ctrl.title}}</h1>
<attributionauthor="$ctrl.person.firstName"
upper-case-author="$ctrl.capitalize()">
</attribution>
`
})
.component('attribution',{
controller:function(){},
bindings:{
author:'=',
upperCaseAuthor:'&'
},
template:`
<png-click="$ctrl.upperCaseAuthor()">
Writtenby:{{$ctrl.author}}
</p>
`
});
SeealsoComponentizingdirectivesusingcontrollerAsencapsulationshowsyouasuperiormethodoforganizingAngular1directivesMigratinganapplicationtocomponentdirectivesdemonstrateshowtorefactorAngular1toacomponentstyleNormalizingservicetypesgivesinstructiononhowtoalignyourAngular1servicetypesforAngular2compatibility
NormalizingservicetypesAngular1developerswillbequitefamiliarwiththefactory/service/providertrifecta.Inmanyways,thishasgonelargelyunalteredinAngular2conceptually.However,intheinterestofupgradinganexistingapplication,thereisonethingthatshouldbedonetomakethemigrationaseasyaspossible:eliminatefactoriesandreplacethemwithservices.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/5637/.
GettingreadySupposeyouhadasimpleapplicationasfollows:
[index.html]
<divng-app="articleApp">
<article></article>
</div>
[app.js]
angular.module('articleApp',[])
.factory('ArticleData',function(){
vartitle='IncumbentSenatorOustedbyStalkofBroccoli';
return{
getTitle:function(){
returntitle;
},
author:'Jake'
};
})
.component('article',{
controller:function(ArticleData){
this.title=ArticleData.getTitle();
this.author=ArticleData.author;
},
template:`
<h1>{{$ctrl.title}}</h1>
<p>Writtenby:{{$ctrl.author}}</p>
`
});
Howtodoit...Angular2isclass-based,anditincludesitsservicetypesaswell.Theexampleheredoesnothaveaservicetypethatiscompatiblewithaclassstructure.Soitmustbeconverted.Thankfully,thisisquiteeasytodo:
[index.html]
<divng-app="articleApp">
<article></article>
</div>
[app.js]
angular.module('articleApp',[])
.service('ArticleData',function(){
vartitle='IncumbentSenatorOustedbyStalkofBroccoli';
this.getTitle=function(){
returntitle;
};
this.author='Jake';
})
.component('article',{
controller:function(ArticleData){
this.title=ArticleData.getTitle();
this.author=ArticleData.author;
},
template:`
<h1>{{$ctrl.title}}</h1>
<p>Writtenby:{{$ctrl.author}}</p>
`
});
Howitworks...Youstillwanttokeepthenotionoftitleprivate,butyoualsowanttomaintaintheAPIthattheinjectedservicetypeisproviding.Servicesaredefinedbyafunctionthatactsasaconstructor,andaninstancecreatedfromthisconstructoriswhatisultimatelyinjected.Here,youaresimplymovinggetTitle()andauthortobedefinedonthethiskeyword,whichtherebymakesitapropertyonallinstances.Notethattheuseinthecomponentandtemplatedoesnotchangeinanyway,anditshouldn't.
There'smore...Thesimplesttoimplementservicetypes,Angular1factorieswereoftenusedfirstbymanydevelopers,includingmyself.Somedevelopersmighttakeoffenseatthefollowingclaim,butIdon'tthinktherewaseveragoodreasonforbothfactoriesandservicestoexist.Bothhaveahighdegreeofredundancy,andifyoudigdownintotheAngularsourcecode,youwillseethattheyessentiallyconvergetothesamemethods.
Whyservicesoverfactoriesthen?ThenewworldofJavaScript,ES6,andTypeScriptisbeingbuiltaroundclasses.Theyareafarmoreelegantwayofexpressingandorganizinglogic.Angular1servicesareanimplementationofprototype-basedclasses,whichwhenusedcorrectlyfunctioninessentiallythesamewayasformalES6/TypeScriptclasses.Ifyoustophere,youwillhavemodifiedyourservicestobemoreextensibleandcomprehensible.Ifyouintendtoupgrade,youwillfindthatAngular1serviceswillcleanlyupgradetoAngular2services.
SeealsoComponentizingdirectivesusingcontrollerAsencapsulationshowsyouasuperiormethodfororganizingAngular1directivesMigratinganapplicationtocomponentdirectivesdemonstrateshowtorefactorAngular1toacomponentstyleImplementingabasiccomponentinAngularJS1.5detailshowtowriteanAngular1component
ConnectingAngular1andAngular2withUpgradeModuleAngular2comeswiththeabilitytoconnectittoanexistingAngular1application.ThisisobviouslyadvantageoussincethiswillallowyoutoutilizeexistingcomponentsandservicesinAngular1intandemwithAngular2'scomponentsandservices.UpgradeModuleisthetoolthatissupportedbyAngularteamstoaccomplishsuchafeat.
Note
Thecode,links,andaliveexampleinrelationtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/4137/.
GettingreadySupposeyouhadaverysimpleAngular1applicationasfollows:
[index.html]
<!DOCTYPEhtml>
<html>
<head>
<!--Angular1scripts-->
<scriptsrc="angular.js"></script>
</head>
<body>
<divng-app="hybridApp"
ng-init="val='Angular1bootstrappedsuccessfully!'">
{{val}}
</div>
</body>
</html>
ThisapplicationinterpolatesavaluesetinanAngularexpressionsoyoucanvisuallyconfirmthattheapplicationhasbootstrappedandisworking.
Howtodoit...Beginbydeclaringthetop-levelangularmoduleinsideitsownfile.Insteadofusingascripttagtofetchtheangularmodule,requireAngular1,importit,andcreatetherootAngular1module:
[ng1.module.ts]
import'angular'
exportconstNg1AppModule=angular.module('Ng1AppModule',[]);
Angular2shipswithanupgrademoduleoutofthebox,whichisprovidedinsideupgrade.js.ThetwoframeworkscanbeconnectedwithUpgradeModule.
Note
ThisrecipeutilizesSystemJSandTypeScript,thespecificationsforwhichlieinsideaverycomplicatedconfigfile.Thisisdiscussedinalaterchapter,sodon'tworryaboutthespecifics.Fornow,youarefreetoassumethefollowing:
SystemJSisconfiguredtocompileTypeScript(.ts)filesontheflySystemJSisabletoresolvetheimportandexportstatementsinTypeScriptfilesSystemJSisabletoresolveAngular1and2libraryimports
Angular2requiresatop-levelmoduledefinitionaspartofitsbaseconfiguration:
[app/ng2.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{UpgradeModule}from'@angular/upgrade/static';
import{RootComponent}from'./root.component';
@NgModule({
imports:[
BrowserModule,
UpgradeModule,
],
bootstrap:[
RootComponent
],
declarations:[
RootComponent
]
})
exportclassNg2AppModule{
constructor(publicupgrade:UpgradeModule){}
}
exportclassAppModule{}
Tip
Thereasonwhythismoduledefinitionexiststhiswayisn'tcriticalforunderstandingthisrecipe.Angular2modulesarecoveredinChapter7,Services,DependencyInjection,andNgModule.
CreatetherootcomponentoftheAngular2application:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<p>Angular2bootstrappedsuccessfully!</p>
`
})
exportclassRootComponent{}
SinceAngular2willoftenbootstrapfromatop-levelfile,createthisfileasmain.tsandbootstraptheAngular2module:
[main.ts]
import{platformBrowserDynamic}
from'@angular/platform-browser-dynamic';
import{Ng1AppModule}from'./app/ng1.module';
import{Ng2AppModule}from'./app/ng2.module';
platformBrowserDynamic()
.bootstrapModule(Ng2AppModule);
ConnectingAngular1toAngular2
Don'tuseanng-apptobootstraptheAngular1application;instead,dothisafteryoubootstrapAngular2:
[main.ts]
import{platformBrowserDynamic}
from'@angular/platform-browser-dynamic';
import{Ng1AppModule}from'./app/ng1.module';
import{Ng2AppModule}from'./app/ng2.module';
platformBrowserDynamic()
.bootstrapModule(Ng2AppModule)
.then(ref=>{
ref.instance.upgrade
.bootstrap(document.body,[Ng1AppModule.name]);
});
Withthis,you'llbeabletoremoveAngular1'sJSscript,theng-appdirective,andaddintherootelementoftheAngular2app:
[index.html]
<!DOCTYPEhtml>
<html>
<head>
<!--Angular2scripts-->
<scriptsrc="zone.js"></script>
<scriptsrc="reflect-metadata.js"></script>
<scriptsrc="system.js"></script>
<scriptsrc="system-config.js"></script>
</head>
<body>
<divng-init="val='Angular1bootstrappedsuccessfully!'">
{{val}}
</div>
<root></root>
</body>
</html>
Note
ThenewscriptslistedherearedependenciesofanAngular2application,butunderstandingwhatthey'redoingisn'tcriticalforunderstandingthisrecipe.Thisisexplainedlaterinthebook.
Withallthis,youshouldseeyourAngular1applicationtemplatecompileandtheAngular2componentrenderproperlyagain.ThismeansthatyouaresuccessfullyrunningAngular1andAngular2frameworkssidebyside.
Howitworks...Makenomistake,whenyouuseUpgradeModule,youcreateanAngular1andAngular2apponthesamepageandconnectthemtogether.Thisadapterinstancewillallowyoutoconnectpiecesfromeachframeworkandusetheminharmony.
Morespecifically,thiscreatesanAngular1applicationatthetoplevelandallowsyoutousespiecesofanAngular2applicationinsideit.
There'smore...Whileusefulforexperimentationandupgradingpurposes,thisshouldnotbeasolutionthatanyapplicationshouldrelyoninaproductioncontext.Youhaveeffectivelydoubledtheframeworkpayloadsizeandintroducedadditionalcomplexityinanexistingapplication.AlthoughAngular2isafarmoreperformantframework,donotexpecttohavethesamepristineresultswiththeUpgradeModulecross-pollination.
Thatsaid,asyouwillseeinsubsequentrecipes,youcannowuseAngular2componentsinanAngular1applicationusingtheadaptertranslationmethods.
SeealsoDowngradingAngular2componentstoAngular1directiveswithdowngradeComponentdemonstrateshowtouseanAngular2componentinsideanAngular1applicationDowngradeAngular2providerstoAngular1serviceswithdowngradeInjectable,whichdemonstrateshowtouseanAngular2serviceinsideanAngular1application
DowngradingAngular2componentstoAngular1directiveswithdowngradeComponentIfyouhavefollowedthestepsinConnectingAngular1andAngular2withUpgradeModule,youshouldnowhaveahybridapplicationthatiscapableofsharingdifferentelementswiththeopposingframework.
Tip
IfyouareunfamiliarwithAngular2components,itisrecommendedthatyougothroughthecomponentschapterbeforeyouproceed.
ThisrecipewillallowyoutofullyutilizeAngular2componentsinsideanAngular1template.
Note
Thecode,links,andaliveexampleinrelationtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/1499/.
GettingreadySupposeyouhadthefollowingAngular2componentthatyouwantedtouseinanAngular1application:
[app/article.component.ts]
import{Component,Input}from'@angular/core';
@Component({
selector:'ng2-article',
template:`
<h1>{{title}}</h1>
<p>Writtenby:{{author}}</p>
`
})
exportclassArticleComponent{
@Input()author:string
title:string='UnicycleJoustingRecognizedasOlympicSport';
}
BeginbycompletingtheConnectingAngular1andAngular2withUpgradeModulerecipe.
Howtodoit...Angular1hasnocomprehensionofhowtoutilizeAngular2components.TheexistingAngular2frameworkwilldutifullyrenderitifgiventheopportunity,butthedefinitionitselfmustbeconnectedtotheAngular1frameworksothatitmayberequestedwhenneeded.
Beginbyaddingthecomponentdeclarationstothemoduledefinition;thisisusedtolinkthetwoframeworks:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{UpgradeModule}from'@angular/upgrade/static';
import{RootComponent}from'./root.component';
import{ArticleComponent}from'./article.component';
@NgModule({
imports:[
BrowserModule,
UpgradeModule,
],
declarations:[
RootComponent,
ArticleComponent
],
bootstrap:[
RootComponent
]
})
exportclassNg2AppModule{
constructor(publicupgrade:UpgradeModule){}
}
ThisconnectsthecomponentdeclarationtotheAngular2context,butAngular1stillhasnoconceptofhowtointerfacewithit.Forthis,you'llneedtousedowngradeComponent()todefinetheAngular2componentasanAngular1directive.GivetheAngular1directiveadifferentHTMLtagtorenderinsidesoyoucanbecertainthatit'sAngular1doingtherenderingandnotAngular2:
[main.ts]
import{Component,Input}from'@angular/core';
import{downgradeComponent}from'@angular/upgrade/static';
import{Ng1AppModule}from'./ng1.module';
@Component({
selector:'ng2-article',
template:`
<h1>{{title}}</h1>
<p>Writtenby:{{author}}</p>
`
})
exportclassArticleComponent{
@Input()author:string
title:string='UnicycleJoustingRecognizedasOlympicSport';
}
Ng1AppModule.directive(
'ng1Article',
downgradeComponent({component:ArticleComponent}));
Finally,sincethiscomponenthasaninput,you'llneedtopassthisvalueviaabindingattribute.EventhoughthecomponentisstillbeingdeclaredasanAngular1directive,you'llusetheAngular2bindingsyntax:
[index.html]
<!DOCTYPEhtml>
<html>
<head>
<!--Angular2scripts-->
<scriptsrc="zone.js"></script>
<scriptsrc="reflect-metadata.js"></script>
<scriptsrc="system.js"></script>
<scriptsrc="system-config.js"></script>
</head>
<body>
<divng-init="authorName='JakeHsu'">
<ng1-article[author]="authorName"></ng1-article>
</div>
<root></root>
</body>
</html>
Theinputandoutputmustbeexplicitlydeclaredatthetimeofconversion:
[app/article.component.ts]
import{Component,Input}from'@angular/core';
import{downgradeComponent}from'@angular/upgrade/static';
import{Ng1AppModule}from'./ng1.module';
@Component({
selector:'ng2-article',
template:`
<h1>{{title}}</h1>
<p>Writtenby:{{author}}</p>
`
})
exportclassArticleComponent{
@Input()author:string
title:string='UnicycleJoustingRecognizedasOlympicSport';
}
Ng1AppModule.directive(
'ng1Article',
downgradeComponent({
component:ArticleComponent,
inputs:['author']
}));
Theseareallthestepsrequired.Ifdoneproperly,youshouldseethecomponentrenderalongwiththeauthor'snamebeinginterpolatedinsidetheAngular2componentthroughAngular1'sng-initdefinition.
Howitworks...YouaregivingAngular1theabilitytodirectAngular2toacertainelementintheDOMandsay,"Ineedyoutorenderhere."Angular2stillcontrolsthecomponentviewandoperation,andineverysense,themainthingwereallycareaboutisafullAngular2componentadaptedforuseinanAngular1template.
Tip
downgradeComponent()takesanobjectspecifyingthecomponentasanargumentandreturnsthefunctionthatAngular1isexpectingforthedirectivedefinition.
SeealsoConnectingAngular1andAngular2withUpgradeModuleshowsyouhowtorunAngular1and2frameworkstogetherDowngradeAngular2providerstoAngular1serviceswithdowngradeInjectabledemonstrateshowtouseanAngular2serviceinsideanAngular1application
DowngradeAngular2providerstoAngular1serviceswithdowngradeInjectableIfyouhavefollowedthestepsinConnectingAngular1andAngular2withUpgradeModule,youshouldnowhaveahybridapplicationthatiscapableofsharingdifferentelementswiththeopposingframework.IfyouareunfamiliarwithAngular2providers,itisrecommendedthatyougothroughthedependencyinjectionchapterbeforeyouproceed.
Likewithtemplatedcomponents,interchangeabilityisalsoofferedtoservicetypes.ItispossibletodefineaservicetypeinAngular2andtheninjectitintoanAngular1context.
Note
Thecode,links,andaliveexampleinrelationtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/2824/.
GettingreadyBeginwiththecodewritteninConnectingAngular1andAngular2withUpgradeModule.
Howtodoit...First,definetheserviceyouwouldliketoinjectintoanAngular1component:
[app/article.service.ts]
import{Injectable}from'@angular/core';
@Injectable()
exportclassArticleService{
article:Object={
title:'ResearchShowsMoonNotActuallyMadeofCheese',
author:'JakeHsu'
};
}
Next,definetheAngular1componentthatshouldinjectit:
[app/article.component.ts]
exportconstng1Article={
template:`
<h1>{{article.title}}</h1>
<p>{{article.author}}</p>
`,
controller:(ArticleService,$scope)=>{
$scope.article=ArticleService.article;
}
};
ArticleServicewon'tbeinjectedyetthough,sinceAngular1hasnoideathatthisserviceexists.Doingthisisverysimple,however.First,you'lllisttheserviceproviderintheAngular2moduledefinitionasyounormallywould:
[app/ng2.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{UpgradeModule}from'@angular/upgrade/static';
import{RootComponent}from'./root.component';
import{ArticleService}from'./article.service';
@NgModule({
imports:[
BrowserModule,
UpgradeModule,
],
declarations:[
RootComponent
],
providers:[
ArticleService
],
bootstrap:[
RootComponent
]
})
exportclassNg2AppModule{
constructor(publicupgrade:UpgradeModule){}
}
Still,Angular1doesnotunderstandhowtousetheservice.
InthesamewayyouconvertanAngular2componentdefinitionintoanAngular1directive,convertanAngular2serviceintoanAngular1factory.UsedowngradeInjectableandaddtheAngular1componentandtheconvertedservicetotheAngular1moduledefinition:
[app/ng1.module.ts]
import'angular';
import{ng1Article}from'./article.component';
import{ArticleService}from'./article.service';
import{downgradeInjectable}from'@angular/upgrade/static';
exportconstNg1AppModule=angular.module('Ng1AppModule',[])
.component('ng1Article',ng1Article)
.factory('ArticleService',downgradeInjectable(ArticleService));
That'sall!YoushouldbeabletoseetheAngular1componentrenderwiththedatapassedfromtheAngular2service.
SeealsoConnectingAngular1andAngular2withUpgradeModuleshowsyouhowtorunAngular1and2frameworkstogetherDowngradingAngular2componentstoAngular1directiveswithdowngradeComponentdemonstrateshowtouseanAngular2componentinsideanAngular1application
Chapter2.ConqueringComponentsandDirectivesYourobjectiveistoiteratethroughthisanddisplaythearticletitleonlyifitissetasactive.Thischapterwillcoverthefollowingrecipes:
UsingdecoratorstobuildandstyleasimplecomponentPassingmembersfromaparentcomponenttoachildcomponentBindingtonativeelementattributesRegisteringhandlersonnativebrowsereventsGeneratingandcapturingcustomeventsusingEventEmitterAttachingbehaviortoDOMelementswithDirectivesProjectingnestedcontentusingngContentUsingngForandngIfstructuraldirectivesformodel-basedDOMcontrolReferencingelementsusingtemplatevariablesAttributepropertybindingUtilizingcomponentlifecyclehooksReferencingaparentcomponentfromachildcomponentConfiguringmutualparent-childawarenesswithViewChildandforwardRefConfiguringmutualparent-childawarenesswithContentChildandforwardRef
IntroductionDirectivesasyoucametoknowtheminAngular1havebeendoneawaywith.Intheirplace,wehavetwonewentities:componentsandthenewversionofdirectives.Angular2applicationsarenowcomponent-driven;withfurtherexposure,youwilldiscoverwhythisstyleissuperior.
Muchofthesyntaxisentirelynewandmayseemstrangeatfirst.Fearnot!TheunderpinningsoftheAngular2styleareelegantandmarvelousoncecompletelyunderstood.
UsingdecoratorstobuildandstyleasimplecomponentWhenwritinganapplicationcomponentinTypeScript,thereareseveralnewparadigmsthatyoumustbecomefamiliarandcomfortablewith.Thoughpossiblyintimidatinginitially,youwillfindthatyou'llbeabletocarryovermuchofyourcomprehensionofAngular1directives.
Note
Thecodeandaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/6577/.
GettingreadyOneofthesimplestimaginablecomponentswecanbuildisatemplateelementthatinterpolatessomevaluesintoitstemplate.InAngular1,onewaythiscouldbeachievedwasbycreatinganelementdirective,attachingsomedatatothescopeinsidethelinkfunction,andatemplatethatwouldreferencethedata.Iselectedthisdescriptionspecificallybecausenearlyallthoseconceptshavebeenbinned.
Supposeyouwanttocreateasimplearticlecomponentwithapseudotemplateasfollows:
<p>{{date}}</p>
<h1>{{title}}</h1>
<h3>Writtenby:{{author}}</h3>
YouwanttocreateacomponentthatwillliveinsideitsownHTMLtag,renderthetemplate,andinterpolatethevalues.
Howtodoit...TheelementalbuildingblockofAngular2applicationsisthecomponent.Thiscomponentcouldgenerallybedefinedwithtwopieces:thecoreclassdefinitionandtheclassdecoration.
Writingtheclassdefinition
AllAngular2componentsbeginasaclass.Thisclassisusedtoinstantiatethecomponent,andanydatarequiredinsidethecomponenttemplatewillbeaccessiblefromtheclass'sproperties.Thus,thefoundationalclassforthecomponentwouldappearasfollows:
[app/article.component.ts]
exportclassArticleComponent{
currentDate:date;
title:string;
author:string;
constructor(){
this.currentDate=newDate();
this.title=`
FlightSecurityRestrictionstoInclude
Insults,MeanFaces
`;
this.author='Jake';
}
};
HereareafewthingstonoteforthosewhoarenewtoTypeScriptorES6ingeneral:
Youwillnotetheclassdefinitionisprefixedwithanexportkeyword.ThisisadherencetothenewES6moduleconvention,whichnaturallyisalsopartofTypeScript.AssumingtheArticleclassisdefinedinthefoo.tsfile,itcanbeimportedtoadifferentmoduleusingtheimportkeyword,andthepathtothatmodulewouldbeimport{Article}from'./foo';(thisassumesthattheimportingfileisinthesamedirectoryasfoo.ts).ThetitledefinitionusesthenewES6templatestringsyntax,apairofbackticks(``)insteadofthetraditionalsetofquotes('').Youwillfindyoubecomequitefondofthis,asitmeansthe''+''+''messinessformerlyusedtodefinemultilinetemplateswouldnolongerbenecessary.Allthepropertiesofthisclassaretyped.TypeScript'stypingsyntaxtakestheformofpropertyName:propertyType=optionalInitValue.JavaScriptis,ofcourse,alooselytypedlanguage,andJavaScriptiswhatthebrowserisinterpretinginthiscase.However,writingyourapplicationinTypeScriptallowsyoutoutilizetypesafetyatcompiletime,whichwillallowyoutoavoidundesirableandunanticipatedbehavior.AllES6classescomewithapredefinedconstructor()method,whichisinvokeduponinstantiation.Here,youareusingtheconstructortoinstantiatethepropertiesoftheclass,whichisaperfectlyfinestrategyformemberinitialization.Havingthememberproperty
definitionoutsidetheconstructorisallowed,sinceitisusefulforaddingtypestoproperties;thus,hereyouaresimplyobviatingtheuseoftheconstructorsinceyouareabletoaddatypeandassignthevalueinthesameline.Amoresuccinctstylecouldbeasfollows:
[app/article.component.ts]
exportclassArticleComponent{
currentDate:date=newDate();
title:string=`
FlightSecurityRestrictionstoInclude
Insults,MeanFaces
`;
author:string='Jake';
}
TheTypeScriptcompilerwillautomaticallymovethememberinitializationprocessinsidetheclassconstructor,sothisversionandthepreviousonearebehaviorallyidentical.
Writingthecomponentclassdecorator
Althoughyouhavecreatedaclassthathasinformationassociatedwithit,itdoesnotyethaveanywaytointerfacewiththeDOM.Furthermore,thisclassisyettobeassignedwithanymeaninginthecontextofAngular2.Youcanaccomplishthiswithdecorators.
Note
DecoratorsareafeatureofTypeScriptbutnotbyanymeansuniquetothelanguage.Pythondevelopers,amongmanyothers,shouldbequitefamiliarwiththeconceptofclassmodulationviaexplicitdecoration.Generallyspeaking,itallowsyoutohavearegularizedmodificationofthedefinedclassesusingseparatelydefineddecorators.However,inAngular2,youwilllargelybeutilizingthedecoratorsprovidedtoyoubytheframeworktodeclarethevariousframeworkelements.Decoratorssuchas@ComponentaredefinedintheAngularsourcecodeasaComponentfunction,andthefunctionisappliedasadecoratorusingthe@symbol.
An@prefixsignalsthattheimportedfunctionshouldbeappliedasadecorator.Thesedecoratorsarevisuallyobviousbutwillusuallynotexistbythemselvesorwithanemptyobjectliteral.ThisisbecauseAngular2decoratorsaregenerallymadeusefulbytheirdecoratormetadata.ThisconceptcanbemademoreconcreteherebyusingthepredefinedComponentdecorator.
NothingisavailableforfreeinAngular2.Inordertousethecomponentdecorator,itmustbeimportedfromtheAngular2coremoduleintothemodulethatwishestouseit.YoucanthenprependthisComponenttotheclassdefinition:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({})
exportclassArticleComponent{
currentDate:date=newDate();
title:string=`
FlightSecurityRestrictionstoInclude
Insults,MeanFaces
`;
author:string='Jake';
}
Asmentionedbefore,the@ComponentdecoratoracceptsaComponentMetadataobject,whichintheprecedingcodeisjustanemptyobjectliteral(notethattheprecedingcodewillnotcompile).Conceptually,thismetadataobjectisverysimilartothedirectivedefinitionobjectinAngular1.Here,youwanttoprovidethedecoratormetadataobjectwithtwoproperties,namelyselectorandtemplate:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<p>{{currentDate|date}}</p>
<h1>{{title}}</h1>
<h3>Writtenby:{{author}}</h3>
`
})
exportclassArticleComponent{
currentDate:date=newDate();
title:string=`
FlightSecurityRestrictionstoInclude
Insults,MeanFaces
`;
author:string='Jake';
}
NotethatselectoristhestringthatwillbeusedtofindwherethecomponentshouldbeinsertedintheDOM,andtemplateisobviouslythestringifiedHTMLtemplate.
Withallthis,youwillbeabletoseeyourarticlecomponentinactionwiththe<article></article>tag.
Howitworks...TheclassdefinitionhassupplantedtheAngular1conceptofhavingacontroller.Thecomponentinstancesofthisclasshavememberpropertiesthatcanbeinterpolatedandboundintothetemplate,similarto$scopeinAngular1.
Inthetemplate,theinterpolationanddatabindingprocessesseemtooccurmuchinthesameway,theydidinAngular1.Thisisnotactuallythecase,whichisvisitedingreaterdetaillaterinthischapter.Thebuilt-indatemodifier,whichresemblesanAngular1filter,isnowdubbedwithapipealthoughitworksinaverysimilarfashion.
TheselectormetadatapropertyisastringrepresentingaCSSselector.Inthisdefinition,youtargetalltheoccurrencesofanarticletag,buttheselectorspecificityanddetailisofcourseabletohandleagreatdealofadditionalcomplexity.Usethistoyouradvantage.
There'smore...TheconceptthatmustbeinternalizedforAngular2neophytesisthetotalencapsulationofacomponent.ThisbookwillgointofurtherdetailaboutthedifferentabilitiesoftheComponentMetadataobject,buttheparadigmtheyandallclassdecoratorsintroduceistheconceptthatAngular2componentsareself-describing.Byexaminingtheclassdefinition,youcanwhollyreasonthedata,service,class,andinjectabledependencies.InAngular1,thiswasnotpossiblebecauseofthe"scopesoup"pattern.
OnecouldarguethatinAngular1,CSSstylingwasasecond-classcitizen.Thisisnolongerthecasewithcomponents,asthemetadataobjectoffersrobustsupportforcomplexstyling.Forexample,toitalicizetheauthorinyourArticlecomponent,usethiscode:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<p>{{currentDate|date}}</p>
<h1>{{title}}</h1>
<h3>Writtenby:{{author}}</h3>
`,
styles:[`
h3{
font-style:italic;
}
`]
})
exportclassArticleComponent{
currentDate:date=newDate();
title:string=`
FlightSecurityRestrictionstoInclude
Insults,MeanFaces
`;
author:string='Jake';
}
Angular2willusethisstylespropertyandcompileitintoageneratedstylesheet,whichitwillthenapplytoonlythiscomponent.YoudonothavetoworryabouttherestoftheHTMLh3tagsbeinginadvertentlystyled.ThisisbecauseAngular2'sgeneratedstylesheetwillensurethatonlythiscomponent—anditschildren—aresubjecttotheCSSruleslistedinthemetadataobject.
Note
Thisisintendedtoemulatethetotalmodularityofwebcomponents.However,sincewebcomponentsdonotyethaveuniversalsupport,Angular2'sdesignessentiallyperformsapolyfill
forthisbehavior.
SeealsoPassingmembersfromaparentcomponentintoachildcomponentgoesthroughthebasicsofdownwarddataflowbetweencomponentsUsingngForandngIfstructuraldirectivesformodel-basedDOMcontrolinstructsyouonhowtoutilizesomeofAngular2'scorebuilt-indirectivesUtilizingcomponentlifecyclehooksgivesanexampleofhowyoucanintegratewithAngular2'scomponentrenderingflow
PassingmembersfromaparentcomponentintoachildcomponentWiththedepartureoftheAngular1.xconceptof$scopeinheritance,mentally(partiallyorentirely)remodelinghowinformationwouldbepassedaroundyourapplicationisamust.Initsplace,youhaveanentirelynewsystemofpropagatinginformationthroughouttheapplication'shierarchy.
Gonealsoistheconceptofdefaultingtobidirectionaldatabinding.Althoughitmadeforanapplicationthatwassimplertoreasonabout,bidirectionaldatabindingincursanunforgivablyexpensivedragonperformance.Thisnewsystemoperatesinanasymmetricfashion:membersarepropagateddownwardsthroughthecomponenttree,butnotupwardsunlessexplicitlyperformed.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/6543/.
GettingreadySupposeyouhadasimpleapplicationthatintendedto(butcurrentlycannot)passdatafromaparentArticleComponenttoachildAttributionComponent:
[app/components.ts]
import{Component}from'@angular/core';
@Component({
selector:'attribution',
template:`
<h3>Writtenby:{{author}}</h3>
`
})
exportclassAttributionComponent{
author:string;
}
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<attribution></attribution>
`
})
exportclassArticleComponent{
title:string='BelchingChoirBeginsWorldTour';
name:string='Jake';
}
Howtodoit...Inthisinitialimplementation,thecomponentsdefinedherearenotyetawareofeachother,andthe<attribution></attribution>tagwillremaininertintheDOM.Thisisagoodthing!Itmeansthesetwocomponentsarecompletelydecoupled,andyouareabletoonlyintroduceconnectionlogicasnecessary.
Connectingthecomponents
First,sincethe<attribution></attribution>tagappearsinsidetheArticlecomponent,youmustmakethecomponentawareoftheexistenceofAttributionComponent.ThisisaccomplishedbyintroducingthecomponentinthemoduleinwhichArticleComponentisalsodeclared:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{ArticleComponent,AttributionComponent}
from'./components';
@NgModule({
imports:[
BrowserModule
],
declarations:[
ArticleComponent,
AttributionComponent
],
bootstrap:[
ArticleComponent
]
})
exportclassAppModule{}
Note
Forthepurposeofthisrecipe,don'tconcernyourselfjustyetwiththedetailsofwhatNgModuleisdoing.Intheexampleinthisrecipe,theentireapplicationisjustaninstanceofArticleComponentwithAttributionComponentinsideit.So,allthecomponentdeclarationscanbedoneinsidethesamemodule.
Withthis,youwillseethatArticleComponentisabletomatchthe<attribution></attribution>tagwiththeAttributionComponentdefinition.
Note
Insideasinglemodule,theorderofthedefinitioncouldmatteralot.ES6andTypeScriptclass
declarationsarenothoisted,soyoucannotreferencethematallbeforethedeclarationwithoutgeneratingerrors.Inthisrecipe,sinceArticleComponentisdefinedbeforeAttributionComponent,theformercannotdirectlyreferencethelatterinsideitsdefinition.
IfyouweretoinsteaddefineAttributionComponentinsideaseparatemoduleandimportitwiththemoduleloader,theorderissuebecomesirrelevant.Asyouwillnotice,thisisoneoftheexcellentbenefitsofhavingahighlymodularapplicationstructure.
OnecaveattothisisthatAngulardoesmakeitpossibletodoout-of-orderclassreferencesusingaforwardRef.However,ifsolvingtheorderproblemispossiblebysplittingitintoseparatemodules,thatispreferredoverforwardRef.
Thisbeingthecase,goaheadandsplityourcomponentfileintotwoseparatemodulesandimportthemaccordingly:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{ArticleComponent}from'./article.component';
import{AttributionComponent}from'./attribution.component';
@NgModule({
imports:[
BrowserModule
],
declarations:[
ArticleComponent,
AttributionComponent
],
bootstrap:[
ArticleComponent
]
})
exportclassAppModule{}
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<attribution></attribution>
`
})
exportclassArticleComponent{
title:string='BelchingChoirBeginsWorldTour';
name:string='Jake';
}
[app/attribution.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'attribution',
template:`
<h3>Writtenby:{{author}}</h3>
`
})
exportclassAttributionComponent{
author:string;
}
Declaringinputs
SimilartotheAngular1directive'sscopepropertyobject,inAngular2,youmustdeclarethemembersoftheparentcomponenttobringthemdowntothechildcomponent.InAngular1,thiscouldbedoneimplicitlywithaninherited$scope,butthisisnolongerthecase.Angular2componentinputsmustbeexplicitlydefined.
Tip
AnotherimportantdifferencebetweenAngular1andAngular2isthat@InputinAngular2isaunidirectionaldatabindingfeature.Dataupdateswillflowdownwards,andtheparentwillnotbeupdatedunlessexplicitlynotified.
TheprocessofdeclaringinputsinachildcomponentisdonethroughtheInputdecorator,butthedecoratorisinvokedinsidetheclassdefinitioninsteadofdoingsoinfrontofit.Inputisimportedfromthecoremoduleandinvokedinsidetheclassdefinitionthatispairedwithamember.
Note
Don'tletthisconfuseyou.Theimplementationoftheactualdecoratingfunctionishiddenfromyousinceitisimportedasasingletarget,sodon'tthinkmuchaboutwhatthe@Input()syntaxisdoing.ThereisadefinedInputfunctionintheAngularsource,andyouarecertainlyinvokingthismethodhere.However,foryourpurposes,itismerelydeclaringthememberthatfollowsitastheonethatwillbepassedinexplicitlyfromtheparentcomponent.YouuseitinthesamewayastheComponentdecorator,justinadifferentplace.
[app/attribution.component.ts]
import{Component,Input}from'@angular/core';
@Component({
selector:'attribution',
template:`
<h3>Writtenby:{{author}}</h3>
`
})
exportclassAttributionComponent{
@Input()author:string;
}
Next,youmustpassthevalueboundtothechildcomponenttagtotheparentcomponent.Inthecontextofthisrecipe,youwanttopassthenamepropertyoftheArticlecomponentobjecttotheauthorpropertyoftheAttributioncomponentobject.Onewayofaccomplishingthisisbyusingthesquarebracketnotationonthetagattribute,whichspecifiestheattributestringasbounddata:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<attribution[author]="name"></attribution>
`
})
exportclassArticleComponent{
title:string='BelchingChoirBeginsWorldTour';
name:string='Jake';
}
Withthis,youhavesuccessfullypassedamemberpropertydown,fromaparenttoachildcomponent!
Howitworks...Recallthatthestartingpointofthisexamplewasthatwehadtwocomponentsthatdidn'tknowtheotherexists,eventhoughtheyaredefinedandexportedinsidethesamemodule.Theprocessdemonstratedinthisrecipeistoprovidethechildcomponenttotheparentcomponent,configurethechildcomponenttoexpectthatamemberwillbeboundtoaninputattribute,andfinallyprovidethatmemberinthetemplateoftheparentcomponent.
There'smore...Somepeoplehaveanissuewiththesquarebracketnotation.ItisvalidHTML,butsomedevelopersfeelitisunintuitiveandlooksodd.
Tip
Additionally,thebracketnotationisnotvalidXML.DevelopersusingHTMLgeneratedthroughXSLTwillnotbeabletoutilizethenewsyntax.Fortunately,everywherethenewAngular2syntaxutilizesnewnew[]or()syntax,thereisanequivalentsyntaxthattheframeworksupportswhichwillbehaveidentically.
Insteadofusingpairsofsquarebrackets,youcanprefixtheattributenamewithbind-anditwillbehaveidentically:
[app/article.component.ts]
import{Component,Input}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<attributionbind-author="name"></attribution>
`
})
exportclassArticleComponent{
title:string='BelchingChoirBeginsWorldTour';
name:string='Jake';
}
Angularexpressions
Notethatthevalueoftheattributenameisnotastringbutanexpression.Angularknowshowtoevaluatethisexpressioninthecontextoftheparentcomponent.AsisthecasewithAngularexpressionsthough,youaremorethanwelcometoprovideastaticvalueandAngularwillhappilyevaluateitandprovideittothechildcomponent.
Forexample,thefollowingchangewouldhardcodethechildcomponenttoassignthestring"MikeSnifferpippets"astheauthorproperty:
[app/article.component.ts]
import{Component,Input}from'@angular/core';
import{AttributionComponent}from'./attribution.component';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<attribution[author]="'MikeSnifferpippets'"></attribution>
`,
directives:[AttributionComponent]
})
exportclassArticleComponent{
title:string='BelchingChoirBeginsWorldTour';
name:string='Jake';
}
Unidirectionaldatabinding
Thedatabindingyouhavesetupinthisrecipeisactuallyunidirectional.Morespecifically,changesintheparentcomponentmemberwillpropagatedownwardstothechildcomponent,butchangestothechildcomponentmemberwillnotpropagateupwards.Thiswillbeexploredfurtherinanotherrecipe,butitisimportanttokeepinmindthattheAngular2dataflowis,bydefault,downwardsthroughthecomponenttree.
Membermethods
Angulardoesn'tcareaboutthenatureoftheboundvalue.TypeScriptwillenforcetypecorrectnessshouldyoudeviatefromthedeclaredtype,butyouarewelcometopassparentmethodstothechildwiththisstrategyaswell.
Tip
Keepinmindthatpassingamethodboundinthiswaydoesnotenforcethecontextinwhichitisevaluated.Iftheparentcomponentpassesamembermethodthatutilizesthethiskeywordandthechildcomponentevaluatesit,thiswillrefertothechildcomponentinstanceandnottheparentcomponent.Therefore,ifthemethodtriestoaccessthememberdataontheparentcomponent,itwillnotbeavailable.
Thereareanumberofwaystomitigatethisproblem.Generallythough,ifyoufindyouarepassingaparentmembermethoddowntothechildcomponentandinvokingit,thereisprobablyabetterwaytodesignyourapplication.
SeealsoUsingdecoratorstobuildandstyleasimplecomponentdescribesthebuildingblocksofimplementinganAngular2componentBindingtonativeelementattributesshowshowAngular2interfaceswithHTMLelementattributesRegisteringhandlersonnativebrowsereventsdemonstrateshowyoucaneasilyattachbehaviortobrowserevents.GeneratingandcapturingcustomeventsusingEventEmitterdetailshowtopropagateinformationupwardsbetweencomponents.UsingngForandngIfstructuraldirectivesformodel-basedDOMcontrolinstructsyouonhowtoutilizesomeofAngular2'scorebuilt-indirectives.
BindingtonativeelementattributesInAngular1,itwasexpectedthatthedeveloperwouldutilizethebuilt-inreplacementdirectivesforelementattributesthathadmeaningfulDOMbehaviorattachedtothem.ThiswasduetothefactthatmanyoftheseattributeshadbehaviorthatwasincompatiblewithhowAngular1databindingoperated.InAngular2,thesespecialattributedirectivesaredoneawaywith,andthebindingbehaviorandsyntaxissubsumedintothenormalbindingbehavior.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/8313/.
Howtodoit...Bindingtothenativeattributeisassimpleasplacingsquarebracketsaroundtheattributeandtreatingitasnormalbounddata:
[app/logo.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'logo',
template:'<img[src]="logoUrl">'
})
exportclassLogoComponent{
logoUrl:string=
'//angular.io/resources/images/logos/standard/logo-nav.png';
}
Withthissetup,the<img>elementwilldutifullyfetchandshowtheimagewhenitisprovidedbyAngular.
Howitworks...Thisisadifferentsolutiontothesameproblemthatng-srcsolvedinAngular1.Thebrowserislookingforansrcattributeonthetag.Sincethesquarebracketsareincludedaspartoftheattributestring,thebrowserwillnotfindoneandthereforenotmakearequest.[src]willonlymakeanimagerequestoncethevalueisfilledandprovidedtotheelement.
SeealsoPassingmembersfromaparentcomponentintoachildcomponentgoesthroughthebasicsofdownwarddataflowbetweencomponents.Registeringhandlersonnativebrowsereventsdemonstrateshowyoucaneasilyattachbehaviortobrowserevents.AttachingbehaviortoDOMelementswithdirectivesdemonstrateshowtoattachbehaviortoelementswithattributedirectives.ReferencingelementsusingtemplatevariablesdemonstratesAngular2'snewtemplatevariableconstruct.AttributepropertybindingshowsAngular2'scleverwayofdeepreferencingelementproperties.
RegisteringhandlersonnativebrowsereventsInAngular2,theotherhemisphereofbindingthatisneededforafullyfunctioningapplicationiseventbinding.TheAngular2eventbindingsyntaxissimilartothatofdatabinding.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/4437/.
GettingreadySupposeyouwantedtocreateanarticleapplicationthatcountedshares,andyoubeganwiththefollowingskeleton:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>Shares:{{shareCt}}</p>
<button>Share</button>
`
})
exportclassArticleComponent{
title:string='PoliceApprehendTiramisuThieves';
shareCt:number=0;
}
Howtodoit...TheAngular2eventbindingsyntaxisaccomplishedwithapairofparenthesessurroundingtheeventtype.Inthiscase,eventsthatyouwishtolistenforwillhaveatypepropertyofclick,andthisiswhattheywillbeboundagainst.Thevalueoftheboundeventattributeisanexpression,soyoucaninvokethemethodasahandlerwithinit:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>Shares:{{shareCt}}</p>
<button(click)="share()">Share</button>
`
})
exportclassArticleComponent{
title:string='PoliceApprehendTiramisuThieves';
shareCt:number=0;
share():void{
++this.shareCt;
}
}
Howitworks...Angularwatchesfortheeventbindingsyntax(click)andaddsaclicklistenertoArticleComponent,boundtotheshare()handler.Whenthiseventisobserved,itevaluatestheexpressionattachedtotheevent,whichinthiscasewillinvokeamethoddefinedonthecomponent.
There'smore...Sincecapturingtheeventmustoccurinanexpression,youareprovidedwithan$eventparameterintheexpression,whichwillusuallybepassedasanargumenttothehandlermethod.ThisissimilartotheprocessinAngular1.Inspectingthis$eventobjectrevealsitasthevanillaclickeventgeneratedbythebrowser:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>Shares:{{shareCt}}</p>
<button(click)="share($event)">Share</button>
`
})
exportclassArticleComponent{
title:string='PoliceApprehendTiramisuThieves';
shareCt:number=0;
share(e:Event):void{
console.log(e);//MouseEvent
++this.shareCt;
}
}
Note
Youwillalsonotethattheshare()methodhereisdemonstratinghowtypingcanbeappliedtotheparametersandthereturnvalueofthemethod:
myMethod(arg1:arg1type,arg2:arg2type,...):returnType
Aswithmemberbinding,youarealsoabletouseanalternateeventbindingsyntaxifyoudonotcaretouseasetofparentheses.Prefixingon-totheeventattributewillprovideyouwithidenticalbehavior:
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>Shares:{{shareCt}}</p>
<buttonon-click="share($event)">Share</button>
`
})
exportclassArticle{
title:string='PoliceApprehendTiramisuThieves';
shareCt:number=0;
share(e:Event):void{
++this.shareCt;
}
}
SeealsoBindingtonativeelementattributesshowshowAngular2interfaceswithHTMLelementattributes.GeneratingandcapturingcustomeventsusingEventEmitterdetailshowtopropagateinformationupwardsbetweencomponents.AttachingbehaviortoDOMelementswithdirectivesdemonstrateshowtoattachbehaviortoelementswithattributedirectives.AttributepropertybindingshowsAngular2'scleverwayofdeepreferencingelementproperties.
GeneratingandcapturingcustomeventsusingEventEmitterInthewakeofthedisappearanceof$scope,Angularwasleftwithavoidforpropagatinginformationupthecomponenttree.Thisvoidisfilledinpartbycustomevents,andtheyrepresenttheYintothedownwarddatabindingYang.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/8611/.
GettingreadySupposeyouhadanArticleapplicationasfollows:
[app/text-editor.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'text-editor',
template:`
<textarea></textarea>
`
})
exportclassTextEditorComponent{}
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>Wordcount:{{wordCount}}</p>
<text-editor></text-editor>
`
})
exportclassArticleComponent{
title:string=`
MaternityWardResortstoRockPaperScissorsFollowing
BabyMixup`;
wordCount:number=0;
updateWordCount(e:number):void{
this.wordCount=e;
}
}
Thisapplicationwillideallybeabletoreadthecontentoftextareawhenthereisachange,andalsocountthenumberofwordsandreportittotheparentcomponenttobeinterpolated.Asisthecase,noneofthisisimplemented.
Howtodoit...AdeveloperthinkingintermsofAngular1wouldattachng-modeltotextarea,use$scope.$watchonthemodeldata,andpassthedatatotheparentvia$scopeorsomeothermeans.Unfortunatelyforsuchadeveloper,theseconstructsareradicallydifferentornon-existentinAngular2.Fearnot!Thenewimplementationismoreexpressive,moremodular,andmuchcleaner.
Capturingtheeventdata
ngModelstillexistsinAngular2,anditwouldcertainlybesuitablehere.However,youdon'tactuallyneedtousengModelatall,andinthiscase,itallowsyoutobemoreexplicitaboutwhenyourapplicationtakesaction.First,youmustretrievethetextfromthetextareaelementandmakeitusableinTextEditorComponent:
[app/text-editor.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'text-editor',
template:`
<textarea(keyup)="emitWordCount($event)"></textarea>
`
})
exportclassTextEditorComponent{
emitWordCount(e:Event):void{
console.log(
(e.target.value.match(/\S+/g)||[]).length);
}
}
Excellent!Asclaimed,youdon'tneedtousengModeltoacquiretheelement'scontents.What'smore,youarenowabletoutilizenativebrowsereventstoexplicitlydefinewhenyouwantTextEditorComponenttotakeaction.
Withthis,youaresettingalisteneronthenativebrowser'skeyupevent,firedfromthetextareaelement.Thiseventhasatargetpropertythatexposesthevalueofthetextintheelement,whichisexactlywhatyouwanttouse.Thecomponentthenusesasimpleregularexpressiontocountthenumberofnon-whitespacesequences.Thisisyourwordcount.
Emittingacustomevent
console.logdoesnothelptoinformtheparentcomponentofthewordcountyouarecalculating.Todothis,youneedtocreateacustomeventandemititupwards:
[app/text-editor.component.ts]
import{Component,EventEmitter,Output}from'@angular/core';
@Component({
selector:'text-editor',
template:`
<textarea(keyup)="emitWordCount($event)"></textarea>
`
})
exportclassTextEditorComponent{
@Output()countUpdate=newEventEmitter<number>();
emitWordCount(e:Event):void{
this.countUpdate.emit(
(e.target.value.match(/\S+/g)||[]).length);
}
}
Usingthe@OutputdecoratorallowsyoutoinstantiateanEventEmittermemberonthechildcomponentthattheparentcomponentwillbeabletolistento.ThisEventEmittermember,likeanyotherclassmember,isavailableasthis.countUpdate.Thechildcomponentisabletosendeventsupwardbyinvokingtheemit()methodonthismember,andtheargumenttothismethodisthevaluewhichyouwishtosendtotheevent.Here,sinceyouwanttosendanintegercountofwords,youinstantiatetheEventEmittermemberbytypingitasa<number>emitter.
Listeningforcustomevents
Sofar,youarethroughwithonlyhalftheimplementation,asthesecustomeventsarebeingfiredoffintotheetherofthebrowserwithnolisteners.Sincethemethodyouneedtouseisalreadydefinedontheparentcomponent,allyouneedtodoishookintotheeventlistenertothatmethod.
The()templatesyntaxisusedtoaddlistenerstoevents,andAngulardoesnotdiscriminatebetweennativebrowsereventsandeventsthatoriginatefromEventEmitters.Thus,sinceyoudeclaredthechildcomponent'sEventEmitteras@Output,youwillbeabletoaddalistenerforeventsthatcomefromitontheparentcomponent,asfollows:
[app/article.component.ts]
import{Component}from'angular2/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>Wordcount:{{wordCount}}</p>
<text-editor(countUpdate)="updateWordCount($event)">
</text-editor>
`
})
exportclassArticleComponent{
title:string=`
MaternityWardResortstoRockPaperScissorsFollowing
BabyMixup`;
wordCount:number=0;
updateWordCount(e:number):void{
this.wordCount=e;
}
}
Withthis,yourapplicationshouldcorrectlycountthewordsintheTextEditorcomponentandupdatethevalueintheArticlecomponent.
Howitworks...Using@OutputinconjunctionwithEventEmitterallowsyoutocreatechildcomponentsthatexposeanAPIfortheparentcomponenttohookinto.TheEventEmittersendstheeventsupwardwithitsemitmethod,andtheparentcomponentcansubscribetothembybindingtotheemitteroutput.
Theflowofthisexampleisasfollows:
1. Thekeystrokeinsidetextareacausesthenativebrowser'skeyupevent.2. TheTextEditorcomponenthasalistenersetonthisevent,sotheattachedexpressionis
evaluated,whichwillinvokeemitWordCount.3. TheemitWordCountinspectstheEventobjectandextractsthetextfromtheassociated
DOMelement.ItparsesthetextforthenumberofcontainedwordsandinvokestheEventEmitter.emitmethod.
4. TheEventEmittermethodemitsaneventassociatedwiththedeclaredcountUpdate@Outputmember.
5. TheArticleComponentseesthiseventandinvokestheattachedexpression.TheexpressioninvokesupdateWordCount,passingintheeventvalue.
6. TheArticleComponentpropertyisupdated,andsincethisvalueisinterpolatedintheview,Angularhonorsthedatabindingprocessbyupdatingtheview.
There'smore...ThenameEventEmitterisabitdeceiving.Ifyou'repayingattention,youwillnoticethattheparentcomponentmembermethodinvokedinthehandlerdoesnothaveatypedparameter.Youwillalsonoticethatyouaredirectlyassigningthatparametertothemembertypedasnumber.Thisshouldseemoddasthetemplateexpressioninvokingthemethodispassing$event,whichyouusedearlierasabrowserEventobject.Thisseemslikeamismatchbecauseitisamismatch.Ifyoubindtonativebrowserevents,theeventyouwillobservecanonlybethenativebrowsereventobject.Ifyoubindtocustomevents,theeventyouwillobserveiswhateverwaspassedwhenemitwasinvoked.Here,theparametertoupdateWordCount()issimplytheintegeryouprovidedwiththis.countUpdate.emit().
Alsonotethatyouarenotrequiredtoprovideavaluefortheemittedevent.YoucanstilluseEventEmittertosignaltoaparentcomponentthataneventhasoccurredandthatitshouldevaluatetheboundexpression.Todothis,yousimplycreateanuntypedemitterwithnewEventEmitter()andinvokeemit()withnoarguments.$eventshouldbeundefined.
Itisnotpossibletopassmultiplevaluesascustomevents.Tosendmultiplepiecesofdata,youneedtocombinethemintoanobjectorarray.
SeealsoBindingtonativeelementattributesshowshowAngular2interfaceswithHTMLelementattributes.Registeringhandlersonnativebrowsereventsdemonstrateshowyoucaneasilyattachbehaviortobrowserevents.
AttachingbehaviortoDOMelementswithdirectivesInthecourseofcreatingapplications,youwilloftenfinditusefultobeabletoattachcomponent-stylebehaviortoDOMelements,butwithouttheneedtohavetemplating.IfyouweretoattempttoconstructanAngular2componentwithoutprovidingatemplateinsomeway,youwillmeetwithasternerrortellingyouthatsomeformoftemplateisrequired.
HereliesthedifferencebetweenAngular2componentsanddirectives:componentshaveviews(whichcantaketheformofatemplate,templateUrl,or@Viewdecorator),whereasdirectivesdonot.Theyotherwisebehaveidenticallyandprovideyouwiththesamebehavior.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/3292/.
GettingreadySupposeyouhavethefollowingapplication:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`<h1>{{title}}</h1>`,
styles:[`
h1{
text-overflow:ellipsis;
white-space:nowrap;
overflow:hidden;
max-width:300px;
}
`]
})
exportclassArticleComponent{
title:string=`PresidentialCandidatesRespondto
AllegationsInvolvingAbilitytoDunk`;
}
Currently,thisapplicationisusingCSStotruncatethearticletitlewithanellipsis.YouwouldliketoexpandthisapplicationsothattheArticlecomponentrevealstheentiretitlewhenclickedbysimplyaddinganHTMLattribute.
Howtodoit...Beginbydefiningthebasicclassthatwillpowertheattributedirectiveandaddittotheapplicationmodule:
[app/click-to-reveal.directive.ts]
exportclassClickToRevealDirective{
reveal(target){
target.style['white-space']='normal';
}
}
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{ArticleComponent}from'./article.component';
import{ClickToRevealDirective}
from'./click-to-reveal.directive';
@NgModule({
imports:[
BrowserModule
],
declarations:[
ArticleComponent,
ClickToRevealDirective
],
bootstrap:[
ArticleComponent
]
})
exportclassAppModule{}
First,youmustdecoratetheClickToRevealDirectiveclassas@DirectiveanduseitinsidetheArticlecomponent:
[app/click-to-reveal.directive.ts]
import{Directive}from'@angular/core';
@Directive({
selector:'[click-to-reveal]'
})
exportclassClickToRevealDirective{
reveal(target){
target.style['white-space']='normal';
}
}
Next,addtheattributetotheelementthatyouwishtoapplythedirectiveto:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`<h1click-to-reveal>{{title}}</h1>`,
styles:[`
h1{
text-overflow:ellipsis;
white-space:nowrap;
overflow:hidden;
max-width:300px;
}
`]
})
exportclassArticleComponent{
title:string;
constructor(){
this.title=`PresidentialCandidatesRespondto
AllegationsInvolvingAbilitytoDunk`;
}
}
Note
NotethattheDirectiveisusinganattributeCSSselectortoassociateitselfwithanyelementsthathaveclick-to-reveal.ThisofcourseapproximatesanAngular1attribute'sdirectivebehavior,butthisformisfarmoreflexiblesinceitcanwieldtheinnatematchabilityofselectors.
NowthattheArticlecomponentisawareofClickToRevealDirective,youmustprovideittheabilitytoattachitselftoclickevents.
AttachingtoeventswithHostListeners
Anattentivedeveloperwillhavenoticedthatupuntilthispointinthechapter,youhavecreatedcomponentsthatlistentotheeventsgeneratedbythechildren.Thisisnoproblemsinceyoucanexpressivelysetlistenersinaparentcomponenttemplateonthechildtag.
However,inthissituation,youarelookingtoaddalistenertothesameelementthatthedirectiveisbeingattachedto.What'smore,youdonothaveagoodwayofaddinganeventbindingexpressiontothetemplatefrominsideadirective.Ideally,youwouldliketonothavetoexposethismethodfrominsidethedirective.Howshouldyouproceedthen?
ThesolutionistoutilizeanewAngularconstructcalledHostListener.Simplyput,itallowsyoutocaptureself-originatingeventsandhandletheminternally:
[app/click-to-reveal.directive.ts]
import{Directive,HostListener}from'@angular/core';
@Directive({
selector:'[click-to-reveal]'
})
exportclassClickToRevealDirective{
@HostListener('click',['$event.target'])
reveal(target){
target.style['white-space']='normal';
}
}
Withthis,clickeventsonthe<h1>elementshouldsuccessfullyinvokethereveal()method.
Howitworks...Thedirectiveneedsawaytoattachtonativeclickevents.Furthermore,itneedsawaytocaptureobjectssuchas$eventthatAngularprovidestoyou;theseobjectswouldnormallybecapturedinthebindingexpression.
@HostListenerdecoratesadirectivemethodtoactasthedesignatedeventhandler.Thefirstargumentinitsinvocationistheeventidentificationstring(here,click,butitcouldjustaseasilybeacustomeventfromEventEmitter),andthesecondargumentisanarrayofstringargumentsthatareevaluatedasexpressions.
There'smore...YouarenotrestrictedtooneHostListenerinsideadirective.Usingitmerelyassociatesaneventwithadirectivemethod.SoyouareabletostackmultipleHostListenerdeclarationsonasinglehandler,forexample,tolistenforbothaclickandmouseoverevent.
SeealsoUsingdecoratorstobuildandstyleasimplecomponentdescribesthebuildingblocksofimplementinganAngular2componentPassingmembersfromaparentcomponentintoachildcomponentgoesthroughthebasicsofdownwarddataflowbetweencomponentsUsingngForandngIfstructuraldirectivesformodel-basedDOMcontrolinstructsyouinhowtoutilizesomeofAngular2'scorebuilt-indirectivesAttributepropertybindingshowsAngular2'scleverwayofdeepreferencingelementproperties
ProjectingnestedcontentusingngContentUtilizingcomponentsasstandalonetagsthatareself-containedandwhollymanagetheircontentsisacleanpattern,butyouwillfrequentlyfindthatyourcomponenttagsdemandthattheyenclosecontent.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/6172/.
GettingreadySupposeyouhadthefollowingapplication:
[app/ad-section.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'ad-section',
template:`
<ahref="#">{{adText}}</a>
`
})
exportclassAdSectionComponent{
adText:string='Selfiesticks40%off!';
}
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>U.S.senatorsareupinarmsfollowingtherecentruling
strippingthemoftheirbelovedselfiesticks.</p>
<p>Abipartisancommitteedraftedaresolutiontosmuggle
selfiesticksontothefloorbyanymeansnecessary.</p>
`
})
exportclassArticleComponent{
title:string='SelfieSticksBannedfromSenateFloor';
}
YourobjectivehereistomodifythissothattheAdSectioncomponentcanbeincorporatedintotheArticlecomponentwithoutinterferingwithitscontent.
Howtodoit...TheAdSectioncomponentwantstoincorporateanextraelementaroundtheexistingArticlecontent.Thisiseasytoaccomplish:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<ad-section>
<p>U.S.senatorsareupinarmsfollowingtherecentruling
strippingthemoftheirbelovedselfiesticks.</p>
<p>Abipartisancommitteedraftedaresolutiontosmuggle
selfiesticksontothefloorbyanymeansnecessary.</p>
</ad-section>
`
})
exportclassArticleComponent{
title:string='SelfieSticksBannedfromSenateFloor';
}
Youwillnoticethoughthatthisisadestructiveoperation.WhenrenderingAdSectionComponent,Angularisnotconcernedaboutanycontentthatisinsideit.ItseesthatAdSectionComponenthasatemplateassociatedwithit,anditdutifullysupplantstheelement'scontentswithit;[email protected],thatwipesoutthe<p>tagsthatyouwanttoretain.
Topreservethem,youmustinstructAngularhowitshouldmanagewrappedcontent.Thiscanbeaccomplishedwithan<ng-content>tag:
[app/ad-section.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'ad-section',
template:`
<ahref="#">{{adText}}</a>
<ng-contentselect="p"></ng-content>
`
})
exportclassAdSectionComponent{
adText:string='Selfiesticks40%off!';
}
Withthis,theadanchorelementisinsertedbeforethewrappedcontent.
Howitworks...Similartohowng-transcludeworkedinAngular1,ng-contentservestointerpolatethecomponenttag'swrappedcontentintoitstemplate.Thedifferencehereisthatng-contentusesaselectattributetotargetthewrappedelements.ThisissimplyaCSSselector,operatinginthesamewayinwhich@ComponentdecoratorshandletheselectorpropertyinComponentMetadata.
There'smore...Theselectattributeinthisexamplewassuperfluous,asitendedupselectingtheentiretyofthewrappedcontent.Ofcourse,iftheselectvalueonlymatchedsomeofthewrappedcontent,itwouldteaseoutonlythoseelementsandinterpolatethem.<ng-content>willbydefaultinserttheentiretyofthewrappedcontentifyoudeclinetoprovideitwithaselectvalue.
AlsonotethattheselectattributeisalimitedCSSselector.Itisnotcapableofperformingcomplexselectionssuchas:nth-child,anditisonlyabletotargettop-levelelementsinsidethewrappingtags.Forexample,inthisapplication,theparagraphtaginside<div><p>Blah</p></div>wouldnotbeincludedwithaselect="p"attributevalue.
SeealsoReferencingaparentcomponentfromachildcomponentdescribeshowacomponentcangainadirectreferencetoitsparentviainjectionConfiguringmutualparent-childawarenesswithViewChildandforwardRefinstructsyouonhowtoproperlyuseViewChildtoreferencechildcomponentobjectinstancesConfiguringMutualParent-ChildAwarenesswithContentChildandforwardRefinstructsyouonhowtoproperlyuseContentChildtoreferencechildcomponentobjectinstances
UsingngForandngIfstructuraldirectivesformodel-basedDOMcontrolAnydeveloperthathasusedaclientframeworkisintimatelyfamiliarwithtwobasicoperationsinanapplication:iterativerenderingfromacollectionandconditionalrendering.ThenewAngular2implementationslookabitdifferentbutoperateinmuchthesameway.
Note
Thecode,links,andaliveexampleareavailableathttp://ngcookbook.herokuapp.com/3211/.
GettingreadySupposeyouhadthefollowingapplication:
[app/article-list.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-list',
template:''
})
exportclassArticleListComponent{
articles:Array<Object>=[
{title:'Foo',active:true},
{title:'Bar',active:false},
{title:'Baz',active:true}
];
}
Yourobjectiveistoiteratethroughthisanddisplaythearticletitleonlyifitissetasactive.
Howtodoit...SimilartoAngular1,Angular2providesyouwithdirectivestoaccomplishthistask.ngForisusedtoiteratethroughthearticlescollection:
[app/article-list.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-list',
template:`
<div*ngFor="letarticleofarticles;leti=index">
<h1>
{{i}}:{{article.title}}
</h1>
</div>
`
})
exportclassArticleListComponent{
articles:Array<Object>=[
{title:'Foo',active:true},
{title:'Bar',active:false},
{title:'Baz',active:true}
];
}
SimilartongFor,ngIfcanbeincorporatedasfollows:
[app/article-list.component.ts]
import{Component}from'angular2/core';
@Component({
selector:'article-list',
template:`
<div*ngFor="letarticleofarticles;leti=index">
<h1*ngIf="article.active">
{{i}}:{{article.title}}
</h1>
</div>
`
})
exportclassArticleListComponent{
articles:Array<Object>=[
{title:'Foo',active:true},
{title:'Bar',active:false},
{title:'Baz',active:true}
];
}
Withthis,youwillseethatonlytheobjectsinthearticlesarraywithactive:truearerendered.
Howitworks...Atfirst,theasteriskandpoundsignnotationcouldbeconfusingformanydevelopers.Formostapplications,youwillnotneedtoknowhowthissyntacticsugaractuallyworksbehindthescenes.
Inreality,Angulardecomposesallthestructuraldirectivesprefixedwith*toutilizeatemplate.First,AngularbreaksdownngForandngIftousethetemplatedirectivesonthesameelement.Thesyntaxdoesnotchangemuchyet:
[app/article-list.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-list',
template:`
<divtemplate="ngForletarticleofarticles;leti=index">
<h1template="ngIfarticle.active">
{{i}}:{{article.title}}
</h1>
</div>
`
})
exportclassArticleList{
articles:Array<Object>,
constructor(){
this.articles=[
{title:'Foo',active:true},
{title:'Bar',active:false},
{title:'Baz',active:true}
];
}
}
Followingthis,Angulardecomposesthistemplatedirectiveintoawrapping<template>element:
[app/article-list.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-list',
template:`
<templatengForlet-article[ngForOf]="articles"let-i="index">
<div>
<template[ngIf]="article.active">
<h1>
{{i}}:{{article.title}}
</h1>
</template>
</div>
</template>
`
})
exportclassArticleList{
articles:Array<Object>,
constructor(){
this.articles=[
{title:'Foo',active:true},
{title:'Bar',active:false},
{title:'Baz',active:true}
];
}
}
Note
Notethatboththeversionsdisplayedhere—eitherusingthetemplatedirectiveorthe<template>element—willbehaveidenticallytousingtheoriginalstructuraldirectives.Thatbeingsaid,theregenerallywillnotbeareasontoeverdoitthisway;thisismerelyademonstrationtoshowyouhowAngularunderstandsthesedirectivesbehindthescenes.
Tip
WheninspectingtheactualDOMoftheseexamplesusingngForandngIf,youwillbeabletoseeAngular'sautomaticallyaddedHTMLcommentsthatdescribehowitinterpretsyourmarkupandtranslatesitintotemplatebindings.
There'smore...ThetemplateelementisbornoutoftheWebComponents'specification.TemplatesareadefinitionofhowaDOMsubtreecaneventuallybedefinedasaunit,buttheelementsthatappearwithinitarenotcreatedoractiveuntilthetemplateisactuallyusedtocreateaninstancefromthattemplate.Notallwebbrowserssupportwebcomponents,soAngular2doesapolyfilltoemulatepropertemplatebehavior.
Inthisway,thengFordirectiveisactuallycreatingawebcomponenttemplatethatutilizesthesubordinatengForOfbinding,whichisapropertyofNgFor.EachinstanceinarticleswillusethetemplatetocreateaDOMsection,andwithinthissection,thearticleandindextemplatevariableswillbeavailableforinterpolation.
SeealsoUsingdecoratorstobuildandstyleasimplecomponentdescribesthebuildingblocksofimplementinganAngular2componentPassingmembersfromaparentcomponentintoachildcomponentgoesthroughthebasicsofdownwarddataflowbetweencomponentsBindingtonativeelementattributesshowshowAngular2interfaceswithHTMLelementattributesAttachingbehaviortoDOMelementswithdirectivesdemonstrateshowtoattachbehaviortoelementswithattributedirectivesAttributepropertybindingshowsAngular2'scleverwayofdeepreferencingelementpropertiesUtilizingcomponentlifecyclehooksgivesanexampleofhowyoucanintegratewithAngular2'scomponentrenderingflow.
ReferencingelementsusingtemplatevariablesManydeveloperswillbeginwithAngular2andreachforsomethingthatresemblesthetrustworthyng-modelinAngular1.NgModelexistsinAngular2,butthereisanewwayofreferencingelementsinthetemplate:localtemplatevariables.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/5094/.
GettingreadySupposeyouhadthefollowingapplicationandwantedtodirectlyaccesstheinputelement:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<input>
<h1>{{title}}</h1>
`
})
exportclassArticleComponent{}
Howtodoit...Angular2allowsyoutohavea#assignmentwithinthetemplateitself,whichcanconsequentlybereferencedfrominsidethetemplate.Forexample,refertothefollowingcode:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<input#title>
<h1>{{title}}</h1>
`
})
exportclassArticleComponent{}
Withthis,youwillsee[objectHTMLInputElement](orsomethingsimilar,dependingonyourbrowser)interpolatedintothe<h1>tag.Thismeansthatthe#titleinsidethe<input>tagisnowdirectlyreferencingtheelementobject,whichofcoursemeansthatthevalueoftheelementshouldbeavailableforyou.
Don'tgettooexcitedjustyet!Ifyouattempttointerpolatetitle.valueandthenmanipulatetheinputfield,youwillnotseethebrowserupdate.ThisisbecauseAngular2nolongersupportsbidirectionaldatabindinginthisway.Fearnot,forthesolutiontothisproblemlieswithinthenewAngular2databindingpattern.
AngularwilldeclinetoupdatetheDOMuntilitthinksitneedsto.Thisneedisdeterminedbywhatbehaviorintheapplicationmightcausetheinterpolateddatatochange.Aboundevent,whichwillpropagateupwardsthroughthecomponenttree,maycauseadatachange.Thus,youcancreateaneventbindingonanelement,andthemerepresenceofthiseventbindingwilltriggerAngulartoupdatethetemplate:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<input#title(keyup)="0">
<h1>{{title.value}}</h1>
`
})
exportclassArticleComponent{}
Here,thekeyupeventfromthetextinputisboundtoanexpressionthatiseffectivelyano-op.
SincetheeventwilltriggeranupdateoftheDOM,youcansuccessfullypulloutthelatestvaluepropertyfromthetitleinputelementobject.Withthis,youhavesuccessfullyboundtheinputvaluetotheinterpolatedstring.
There'smore...Ifyouaren'tcrazyaboutthe#notation,youcanalwaysreplaceitwithval-andstillachieveidenticalbehavior:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<inputval-title(keyup)="0">
<h1>{{title.value}}</h1>
`
})
exportclassArticleComponent{}
Also,it'simportanttorecallthatthesetemplatevariablesareonlyaccessiblewithinthetemplate.Ifyouwanttopassthembacktothecontroller,you'llhavetouseitasahandlerargument:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<input#title(keyup)="setTitle(title.value)">
<h1>{{myTitle}}</h1>
`
})
exportclassArticleComponent{
myTitle:string;
setTitle(val:string):void{
this.myTitle=val;
}
}
SeealsoReferencingaparentcomponentfromachildcomponentdescribeshowacomponentcangainadirectreferencetoitsparentviainjectionConfiguringmutualparent-childawarenesswithViewChildandforwardRefinstructsyouonhowtoproperlyuseViewChildtoreferencechildcomponentobjectinstancesConfiguringmutualparent-childawarenesswithContentChildandforwardRefinstructsyouonhowtoproperlyuseContentChildtoreferencechildcomponentobjectinstances
AttributepropertybindingOneofthegreatnewbenefitsofthenewAngularbindingstyleisthatyouareabletomoreaccuratelytargetwhatyouarebindingto.Formerly,theHTMLattributethatwasusedasadirectiveordatatokenwassimplyusedasamatchingidentifier.Now,youareabletousepropertybindingswithinthebindingmarkupforboththeinputandoutput.
Note
Thecode,links,andaliveexampleofthisisavailableathttp://ngcookbook.herokuapp.com/8565/.
GettingreadySupposeyouhadthefollowingapplication:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<input#title(keydown)="setTitle(title.value)">
<h1>{{myTitle}}</h1>
`
})
exportclassArticleComponent{
myTitle:string;
setTitle(val:string):void{
this.myTitle=val;
}
}
Yourobjectiveistomodifythissothatitexhibitsthefollowingbehavior:
The<h1>tagisnotupdatedwiththevalueoftheinputfielduntiltheuserstrikestheEnterkey.Ifthe<h1>valuedoesnotmatchthevalueinthetitleinput(callthisstate"stale"),thetextcolorshouldbered.Ifitdoesmatch,itshouldbegreen.
Howtodoit...BothofthesebehaviorscanbeachievedwithAngular2'sattributepropertybinding.First,youcanchangetheeventbindingsothatonlyanEnterkeywillinvokethecallback:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<input#title(keydown.enter)="setTitle(title.value)">
<h1>{{myTitle}}</h1>
`
})
exportclassArticleComponent{
myTitle:string;
setTitle(val:string):void{
this.myTitle=val;
}
}
Next,youcanuseAngular'sstylebindingtodirectlyassignavaluetoastyleproperty.ThisrequiresaddingaBooleantothecontrollerobjecttomaintainthestate:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<input#title(keydown.enter)="setTitle(title.value)">
<h1[style.color]="isStale?'red':'green'">{{myTitle}}</h1>
`
})
exportclassArticleComponent{
myTitle:string='';
isStale:boolean=false;
setTitle(val:string):void{
this.myTitle=val;
}
}
Closer,butthisstillprovidesnowayofreachingastalestate.Toachievethis,addanotherkeydowneventbinding:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<input#title
(keyup.enter)="setTitle(title.value)"
(keyup)="checkStale(title.value)">
<h1[style.color]="isStale?'red':'green'">
{{myTitle}}
</h1>
`
})
exportclassArticleComponent{
myTitle:string='';
privateisStale:boolean=false;
setTitle(val:string):void{
this.myTitle=val;
}
checkStale(val:string):void{
this.isStale=val!==this.myTitle;
}
}
Withthis,thecolorofthe<h1>tagshouldcorrectlykeeptrackofwhetherthedataisstaleornot!
Howitworks...ThesimpleexplanationisthatAngularprovidesyouwithalotofsyntacticalsugar,butataverybasiclevelwithoutinvolvingalotofcomplexity.Ifyouweretoinspectthekeyupevent,youwouldofcoursenoticethatthereisnoenterpropertyavailable.AngularoffersyouanumberofthesepseudopropertiessothatcheckingthekeyCodeofthepressedkeyisnotnecessary.
Inasimilarway,Angularalsoallowsyoutobindtoandaccessstylepropertiesdirectly.Itisinferredthatthestylebeingaccessedreferstothehostelement.
There'smore...Noteherethatyouhaveassignedtwohandlerstowhatisessentiallythesameevent.Notonlythis,butrearrangingtheorderofthebindingmarkupwillbreakthisapplication'sdesiredbehavior.
Note
Whentwohandlersareassignedtothesameevent,Angularwillexecutethehandlersintheorderthattheyaredefinedinthemarkup.
SeealsoBindingtonativeelementattributesshowshowAngular2interfaceswithHTMLelementattributesRegisteringhandlersonnativebrowsereventsdemonstrateshowyoucaneasilyattachbehaviortobrowsereventsGeneratingandcapturingcustomeventsusingEventEmitterdetailshowtopropagateinformationupwardsbetweencomponentsAttachingbehaviortoDOMelementswithdirectivesdemonstrateshowtoattachbehaviortoelementswithattributedirectives
UtilizingcomponentlifecyclehooksAngular'scomponentrenderingprocesshasalargenumberoffacets,anddifferenttypesofdataandreferenceswillbecomeavailableatdifferenttimes.Toaccountforthis,Angular2allowscomponentstosetcallbacks,whichwillbeexecutedatdifferentpointsinthecomponent'slifecycle.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/2048/.
GettingreadySupposeyoubeganwiththefollowingapplication,whichsimplyallowstheadditionandremovalofarticlesfromasingleinput:
[app/article-list.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-list',
template:`
<input(keyup.enter)="add($event)">
<article*ngFor="lettitleoftitles;leti=index"
[articleTitle]="title">
<button(click)="remove(i)">X</button>
</article>
`
})
exportclassArticleListComponent{
titles:Array<string>=[];
add(e:Event):void{
this.titles.push(e.target.value);
e.target.value='';
}
remove(index:number){
this.titles.splice(index,1);
}
}
[app/article.component.ts]
import{Component,Input}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>
<ng-content></ng-content>{{articleTitle}}
</h1>
`
})
exportclassArticleComponent{
@Input()articleTitle:string;
}
Yourobjectiveistouselifecyclehookstokeeptrackoftheprocessofaddingandremovingoperations.
Howtodoit...Angularallowsyoutoimporthookinterfacesfromthecoremodule.Theseinterfacesaremanifestedasclassmethods,whichareinvokedattheappropriatetime:
[app/article.component.ts]
import{Component,Input,ngOnInit,ngOnDestroy}
from'@angular/core';
@Component({
selector:'article',
template:`
<h1>
<ng-content></ng-content>{{articleTitle}}
</h1>
`
})
exportclassArticleComponentimplementsOnInit,OnDestroy{
@Input()articleTitle:string;
ngOnInit(){
console.log('created',this.articleTitle);
}
ngOnDestroy(){
console.log('destroyed',this.articleTitle);
}
}
Withthis,youshouldseelogseachtimeanewArticleComponentisaddedorremoved.
Howitworks...Differenthookshavedifferentsemanticmeanings,buttheywilloccurinawell-definedorder.Eachhook'sexecutionguaranteesthatacertainbehaviorofacomponentisjustcompleted.
Thehooksthatarecurrentlyavailabletoyouintheorderofexecutionareasfollows:
ngOnChanges
ngOnInit
ngDoCheck
ngAfterContentInit
ngAfterContentChecked
ngAfterViewInit
ngAfterContentChecked
ngOnDestroy
Itisalsopossibleforthird-partylibrariestoextendtheseandaddtheirownhooks.
There'smore...Usinghooksisoptional,andAngularwillonlyinvokethemifyouhavedefinedthem.Theuseoftheimplementsinterfacedeclarationisoptional,butitwillsignaltotheTypeScriptcompilerthatacorrespondingmethodshouldbeexpected,whichisobviouslyagoodpractice.
SeealsoReferencingaparentcomponentfromachildcomponentdescribeshowacomponentcangainadirectreferencetoitsparentviainjectionConfiguringmutualparent-childawarenesswithViewChildandforwardRefinstructsyouonhowtoproperlyuseViewChildtoreferencechildcomponentobjectinstancesConfiguringmutualparent-childawarenesswithContentChildandforwardRefinstructsyouonhowtoproperlyuseContentChildtoreferencechildcomponentobjectinstances
ReferencingaparentcomponentfromachildcomponentInthecourseofbuildinganapplication,youmayencounterascenariowhereitwouldbeusefultoreferenceaparentcomponentfromachildcomponent,suchastoinspectmemberdataorinvokepublicmethods.InAngular2,thisisactuallyquiteeasytoaccomplish.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/4907/.
GettingreadySupposeyoubeginwiththefollowingArticleComponent:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<feedback[val]="likes"></feedback>
`
})
exportclassArticleComponent{
likes:number=0;
incrementLikes():void{
this.likes++;
}
}
Yourobjectiveistoimplementthefeedbackcomponentsothatitdisplaysthenumberoflikespassedtoit,buttheparentcomponentcontrolstheactuallikecountandpassesthatvaluein.
Howtodoit...Beginbyimplementingthebasicstructureofthechildcomponent:
[app/feedback.component.ts]
import{Component,Input}from'@angular/core';
@Component({
selector:'feedback',
template:`
<h1>Numberoflikes:{{val}}</h1>
<button(click)="likeArticle()">Likethisarticle!</button>
`
})
exportclassFeedbackComponent{
@Input()val:number;
likeArticle():void{}
}
Sofar,noneofthisshouldsoundsurprising.Clickingonthebuttoninvokesanemptymethod,andyouwantthismethodtoinvokeamethodfromtheparentcomponent.However,youcurrentlylackareferencetodothis.Listingthecomponentinthechildcomponentconstructorwillmakeitavailabletoyou:
[app/feedback.component.ts]
import{Component,Input}from'@angular/core';
import{ArticleComponent}from'./article.component';
@Component({
selector:'feedback',
template:`
<h1>Numberoflikes:{{val}}</h1>
<button(click)="like()">Likethisarticle!</button>
`
})
exportclassFeedbackComponent{
@Input()val:number;
privatearticleComponent:ArticleComponent;
constructor(articleComponent:ArticleComponent){
this.articleComponent=articleComponent;
}
like():void{
this.articleComponent.incrementLikes();
}
}
Withareferencetotheparentcomponentnowavailable,youareeasilyabletoinvokeitspublicmethod,namelyincrementLikes().Atthispoint,thetwocomponentsshouldcommunicatecorrectly.
Howitworks...Verysimply,Angular2recognizesthatyouareinjectingacomponentthatistypedinthesamewayastheparent,anditwillprovidethatparentforyou.Thisisthefullparentinstance,andyouarefreetointeractwithitasyouwouldnormallyinteractwithanycomponentinstance.
Tip
Noticethatitisrequiredthatyoustoreareferencetothecomponentinsidetheconstructor.Unlikewhenyouinjectaservice,thechildcomponentwillnotautomaticallymaketheArticleComponentinstanceavailabletoyouasthis.articleComponent;youneedtodothismanually.
There'smore...Anastutedeveloperwillnoticethatthiscreatesaveryrigiddependencyfromthechildcomponenttotheparentcomponent.Thisisindeedthecase,butnotnecessarilyabadthing.Often,itisusefultoallowcomponentstomoreeasilyinteractwitheachotherattheexpenseoftheirmodularity.Andgenerally,thiswillbeajudgmentcallonyourpart.
SeealsoPassingmembersfromaparentcomponentintoachildcomponentgoesthroughthebasicsofdownwarddataflowbetweencomponentsRegisteringhandlersonnativebrowsereventsdemonstrateshowyoucaneasilyattachbehaviortobrowsereventsGeneratingandcapturingcustomeventsusingEventEmitterdetailshowtopropagateinformationupwardsbetweencomponentsConfiguringmutualparent-childawarenesswithViewChildandforwardRefinstructsyouonhowtoproperlyuseViewChildtoreferencechildcomponentobjectinstancesConfiguringmutualparent-childawarenesswithContentChildandforwardRefinstructsyouonhowtoproperlyuseContentChildtoreferencechildcomponentobjectinstances
Configuringmutualparent-childawarenesswithViewChildandforwardRefDependingonyourapplication'sseparationofconcerns,itmightmakesenseforachildcomponentinyourapplicationtoreferenceaparent,andatthesametime,fortheparenttoreferencethechild.Therearetwosimilarimplementationsthatallowyoutoaccomplishthis:usingViewChildandContentChild.Thisrecipewilldiscussthemboth.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/1315/.
GettingreadyBeginwiththerecipesetupshowninReferencingaparentcomponentfromachildcomponent.Yourobjectiveistoaddtheabilitytoenableanddisablethelikebuttonfromtheparentcomponent.
Howtodoit...Theinitialsetuponlygivesthechildaccesstotheparent,whichisonlyhalfofwhatyouneed.Theotherhalfistogivetheparentaccesstothechild.
GettingareferencetoFeedbackComponentthatyouseeintheArticleComponenttemplateviewcanbedoneintwoways,andthefirstwaydemonstratedherewilluseViewChild.
ConfiguringaViewChildreference
UsingViewChildwillallowyoutoextractacomponentreferencefrominsidetheview.Moreplainly,inthisexample,usingViewChildwillgiveyoutheabilitytoreferencetheFeedbackComponentinstancefrominsidetheArticleComponentcode.
First,configureArticleComponentsothatitwillretrievethecomponentreference:
[app/article.component.ts]
import{Component,ViewChild}from'@angular/core';
import{FeedbackComponent}from'./feedback.component';
@Component({
selector:'article',
template:`
<inputtype="checkbox"
(click)="changeLikesEnabled($event)">
<feedback[val]="likes"></feedback>
`
})
exportclassArticleComponent{
@ViewChild(FeedbackComponent)
feedbackComponent:FeedbackComponent;
likes:number=0;
incrementLikes():void{
this.likes++;
}
changeLikesEnabled(e:Event):void{
this.feedbackComponent.setLikeEnabled(e.target.checked);
}
}
ThemainthemeinthisnewcodeisthattheViewChilddecoratorimplicitlyunderstandsthatitshouldtargettheviewofthiscomponent,findtheinstanceofFeedbackComponentthatisbeingrenderedthere,andassignittothefeedbackCompnentmemberoftheArticleComponentinstance.
CorrectingthedependencycyclewithforwardRef
Atthispoint,youshouldbeseeingyourapplicationthrowingnewerrors,mostlikelyaboutbeingunabletoresolvetheparametersforFeedbackComponent.Thisoccursbecauseyouhavesetupacyclicdependency:FeedbackComponentdependsonArticleComponentandArticleComponentdependsonFeedbackComponent.Thankfully,thisproblemexistsinthedomainofAngulardependencyinjection,soyoudon'treallyneedthemodule,justatokenthatrepresentsit.Forthispurpose,Angular2providesyouwithforwardRef,whichallowsyoutouseamoduledependencyinsideyourclassdefinitionbeforeitisdefined.Useitasfollows:
[app/feedback.component.ts]
import{Component,Input,Inject,forwardRef}
from'@angular/core';
import{ArticleComponent}from'./article.component';
@Component({
selector:'feedback',
template:`
<h1>Numberoflikes:{{val}}</h1>
<button(click)="likeArticle()">
Likethisarticle!
</button>
`
})
exportclassFeedbackComponent{
@Input()val:number;
privatearticleComponent:ArticleComponent;
constructor(@Inject(forwardRef(()=>ArticleComponent))
articleComponent:ArticleComponent){
this.articleComponent=articleComponent;
}
likeArticle():void{
this.articleComponent.incrementLikes();
}
}
Addingthedisablebehavior
Withthecycleproblemresolved,addthesetLikeEnabled()methodthattheparentcomponentisinvoking:
[app/feedback.component.ts]
import{Component,Input,Inject,forwardRef}
from'@angular/core';
import{ArticleComponent}from'./article.component';
@Component({
selector:'feedback',
template:`
<h1>Numberoflikes:{{val}}</h1>
<button(click)="likeArticle()"
[disabled]="!likeEnabled">
Likethisarticle!
</button>
`
})
exportclassFeedbackComponent{
@Input()val:number;
privatelikeEnabled:boolean=false;
privatearticleComponent:ArticleComponent;
constructor(@Inject(forwardRef(()=>ArticleComponent))
articleComponent:ArticleComponent){
this.articleComponent=articleComponent;
}
likeArticle():void{
this.articleComponent.incrementLikes();
}
setLikeEnabled(newEnabledStatus:boolean):void{
this.likeEnabled=newEnabledStatus;
}
}
Withthis,togglingthecheckboxshouldenableanddisablethelikebutton.
Howitworks...ViewChilddirectsAngulartofindthefirstinstanceofFeedbackComponentpresentinsidetheArticleComponentviewandassignittothedecoratedclassmember.Thereferencewillbeupdatedalongwithanyviewupdates.Thisdecoratedmemberwillrefertothechildcomponentinstanceandcanbeinteractedwithlikeanynormalobjectinstance.
Note
It'simportanttorememberthedualityofthecomponentinstanceanditsrepresentationinthetemplate.Forexample,FeedbackComponentisrepresentedbyafeedbacktag(pre-render)andaheadertagandabutton(post-render),butneitheroftheseformtheactualcomponent.TheFeedbackComponentinstanceisaJavaScriptobjectthatlivesinthememory,andthisistheobjectyouwantaccessto.Ifyoujustwantedareferencetothetemplateelements,thiscouldbeaccomplishedbyatemplatevariable,forexample.
There'smore...SinceAngularperformshierarchicalrendering,ViewChildwillnotbereadyuntiltheviewisinitialized,butrather,aftertheAfterViewInitlifecyclehook.Thiscanbedemonstratedasfollows:
[app/article.component.ts]
import{Component,ViewChild,ngAfterViewInit}
from'@angular/core';
import{FeedbackComponent}from'./feedback.component';
@Component({
selector:'article',
template:`
<inputtype="checkbox"
(click)="changeLikesEnabled($event)">
<feedback[val]="likes"></feedback>
`
})
exportclassArticleComponentimplementsAfterViewInit{
@ViewChild(FeedbackComponent)
feedbackComponent:FeedbackComponent;
likes:number=0;
constructor(){
console.log(this.feedbackComponent);
}
ngAfterViewInit(){
console.log(this.feedbackComponent);
}
incrementLikes():void{
this.likes++;
}
changeLikesEnabled(e:Event):void{
this.feedbackComponent.setLikeEnabled(e.target.checked);
}
}
Thiswillfirstlogundefinedinsidetheconstructorastheview,andtherefore,FeedbackComponentdoesnotyetexist.OncetheAfterViewInitlifecyclehookoccurs,youwillbeabletoseeFeedbackComponentloggedtotheconsole.
ViewChildren
Ifyouwouldliketogetareferencetomultiplecomponents,youcanperformanidenticalreferenceacquisitionusingViewChildren,whichwillprovideyouwithaQueryListofallthe
matchingcomponentsintheview.
Tip
AQueryListcanbeusedlikeanarraywithitstoArray()method.Italsoexposesachangesproperty,whichemitsaneventeverytimeamemberofQueryListchanges.
SeealsoUtilizingcomponentlifecyclehooksgivesanexampleofhowyoucanintegratewithAngular2'scomponentrenderingflowReferencingaparentcomponentfromachildcomponentdescribeshowacomponentcangainadirectreferencetoitsparentviainjectionConfiguringmutualparent-childawarenesswithContentChildandforwardRefinstructsyouonhowtoproperlyuseContentChildtoreferencechildcomponentobjectinstances
Configuringmutualparent-childawarenesswithContentChildandforwardRefThecompaniontoAngular'sViewChildisContentChild.Itperformsasimilarduty;itretrievesareferencetothetargetchildcomponentandmakesitavailableasamemberoftheparentcomponentinstance.ThedifferenceisthatContentChildretrievesthemarkupthatexistsinsidetheparentcomponent'sselectortags,whereasViewChildretrievesthemarkupthatexistsinsidetheparentcomponent'sview.
Thedifferenceisbestdemonstratedbyacomparisonofbehavior,sothisrecipewillconverttheexamplefromConfiguringMutualParent-ChildAwarenesswithViewChildandforwardReftouseContentChildinstead.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/7386/.
GettingreadyBeginwiththecodefromtheConfiguringmutualparent-childawarenesswithViewChildandforwardRefrecipe.
Howtodoit...Beforeyoubegintheconversion,you'llneedtonesttheArticleComponenttagsinsideanotherrootcomponent,asContentChildwillnotworkfortheroot-levelbootstrappedapplicationcomponent.CreateawrappedRootComponent:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<article></article>
`
})
exportclassRootComponent{}
ConvertingtoContentChild
ContentChildisintroducedtocomponentsinessentiallythesamewayasViewChild.InsideArticleComponent,performthisconversionandreplacethe<feedback>tagwith<ng-content>:
[app/article.component.ts]
import{Component,ContentChild}from'@angular/core';
import{FeedbackComponent}from'./feedback.component';
@Component({
selector:'article',
template:`
<inputtype="checkbox"
(click)="changeLikesEnabled($event)">
<ng-content></ng-content>
`
})
exportclassArticleComponent{
@ContentChild(FeedbackComponent)
feedbackComponent:FeedbackComponent;
likes:number=0;
incrementLikes():void{
this.likes++;
}
changeLikesEnabled(e:Event):void{
this.feedbackComponent.setLikeEnabled(e.target.checked);
}
}
Ofcourse,thiswillonlybeabletofindthechildcomponentifthe<article></article>taghascontentinsideofit:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<article>
<feedback></feedback>
</article>
`
})
exportclassRootComponent{}
Note
You'llnoticethatthelikecountvaluebeingpassedtothechildcomponentasaninputhasbeenremoved.Verysimply,thatconventionwillnotworkanymore,asbindingitherewoulddrawthelikecountfromRootComponent,whichdoesnothavethisinformation.
Correctingdatabinding
TheFeedbackComponentwillneedtoretrievethelikecountdirectly:
[app/feedback.component.ts]
import{Component,Inject,forwardRef}from'@angular/core';
import{ArticleComponent}from'./article.component';
@Component({
selector:'feedback',
template:`
<h1>Numberoflikes:{{val}}</h1>
<button(click)="likeArticle()"
[disabled]="!likeEnabled">
Likethisarticle!
</button>
`
})
exportclassFeedbackComponent{
privateval:number;
privatelikeEnabled:boolean=false;
privatearticleComponent:ArticleComponent;
constructor(@Inject(forwardRef(()=>ArticleComponent))
articleComponent:ArticleComponent){
this.articleComponent=articleComponent;
this.updateLikes();
}
updateLikes(){
this.val=this.articleComponent.likes;
}
likeArticle():void{
this.articleComponent.incrementLikes();
this.updateLikes();
}
setLikeEnabled(newEnabledStatus:boolean):void{
this.likeEnabled=newEnabledStatus;
}
}
That'sit!TheapplicationshouldbehaveidenticallytothesetupfromtheGettingreadysectionoftherecipe.
Howitworks...ContentChilddoesnearlythesamethingasViewChild;itjustlooksinadifferentplace.ContentChilddirectsAngulartofindthefirstinstanceofFeedbackComponentpresentinsidetheArticleComponenttags.Here,thisstepreferstoanythingthatisinterpolatedby<ng-content>.Itthenassignsthefoundcomponentinstancetothedecoratedclassmember.Thereferenceisupdatedalongwithanyviewupdates.Thisdecoratedmemberwillrefertothechildcomponentinstanceandcanbeinteractedwithlikeanynormalobjectinstance.
There'smore...SinceAngularperformshierarchicalrendering,ContentChildwillnotbereadyuntiltheviewisinitialized,butrather,aftertheAfterContentInitlifecyclehook.Thiscanbedemonstratedasfollows:
[app/article.component.ts]
import{Component,ContentChild,ngAfterContentInit}
from'@angular/core';
import{FeedbackComponent}from'./feedback.component';
@Component({
selector:'article',
template:`
<inputtype="checkbox"
(click)="changeLikesEnabled($event)">
<ng-content></ng-content>
`
})
exportclassArticleComponentimplementsAfterContentInit{
@ContentChild(FeedbackComponent)
feedbackComponent:FeedbackComponent;
likes:number=0;
constructor(){
console.log(this.feedbackComponent);
}
ngAfterContentInit(){
console.log(this.feedbackComponent);
}
incrementLikes():void{
this.likes++;
}
changeLikesEnabled(e:Event):void{
this.feedbackComponent.setLikeEnabled(e.target.checked);
}
}
Thiswillfirstlogundefinedinsidetheconstructorasthecontent,andthereforeFeedbackComponentdoesnotyetexist.OncetheAfterContentInitlifecyclehookoccurs,youwillbeabletoseeFeedbackComponentloggedtotheconsole.
ContentChildren
Ifyouwouldliketogetareferencetomultiplecomponents,youcanperformanidenticalreferenceacquisitionprocessusingContentChildren,whichwillprovideyouwithQueryList
ofallthematchingcomponentsinsidethecomponent'stags.
Tip
AQueryListcanbeusedlikeanarraywithitstoArray()method.Italsoexposesachangesproperty,whichemitsaneventeverytimeamemberofQueryListchanges.
SeealsoUtilizingcomponentlifecyclehooksgivesanexampleofhowyoucanintegratewithAngular2'scomponentrenderingflow.Referencingaparentcomponentfromachildcomponentdescribeshowacomponentcangainadirectreferencetoitsparentviainjection.Configuringmutualparent-childawarenesswithViewChildandforwardRefinstructsyouonhowtoproperlyuseViewChildtoreferencechildcomponentobjectinstances.
Chapter3.BuildingTemplate-DrivenandReactiveFormsThischapterwillcoverthefollowingrecipes:
Implementingsimpletwo-waydatabindingwithngModelImplementingbasicfieldvalidationwithaFormControlBundlingFormControlswithaFormGroupBundlingFormControlswithaFormArrayImplementingbasicformswithngFormImplementingbasicformswithFormBuilderandformControlNameCreatingandusingacustomvalidatorCreatingandusingacustomasynchronousvalidatorwithPromises
IntroductionFormsareimportantelementalconstructsfornearlyeverywebapplication,andtheyhavebeenreimaginedforthebetterinAngular2.Angular1formswereveryuseful,buttheyweretotallydependentontheconventionsofngModel.Angular2'snewfoundconventionsremoveitfromngModeldependenceandofferafreshapproachtoformandinformationmanagementthatultimatelyfeelscleanerandmoreapproachable.
Fundamentally,itisimportanttounderstandwhereandwhyformsareuseful.Therearemanyplacesinanapplicationwheremultitudinousinputdemandsassociation,andformsarecertainlyusefulinthiscontext.Angular2formsarebestusedwhenvalidatingthesaidinput,especiallysowhenmultiple-fieldandcross-fieldvalidationisrequired.Additionally,Angularformsmaintainthestateofvariousformelements,allowingtheusertoreasonthe"history"ofaninputfield.
ItisalsocriticaltorememberthattheAngular2formbehavior,muchinthesamewayasitseventanddatabinding,isgettingintegratedwiththealreadyrobustbrowserformbehavior.Browsersarealreadyverycapableofsubmittingdata,recallingdatauponapagereload,simplevalidation,andotherbehaviorsthatprettymuchallformsrelyupon.Angular2doesn'tredefinethese;rather,itintegrateswiththesebehaviorsinordertolinkinotherbehaviorsanddatathatarepartofeitheraframeworkoryourapplication.
Tip
Inthischapter,beawareofthedualityoftheuseofFormsModuleandReactiveFormsModule.Theybehaveverydifferentlyandarealmostalwaysusedseparatelywhenitcomestoformconstruction.
Implementingsimpletwo-waydatabindingwithngModelAngular2stillhastwo-waydatabinding,butthewayitbehavesisabitdifferentthanwhatyou'reusedto.Thisrecipewillbeginwithaverysimpleexampleandthenbreakitdownintopiecestodescribewhatit'sactuallydoing.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/0771/.
Howtodoit...Two-waydatabindingusesthengModeldirective,whichisincludedinFormsModule.Addthisdirectivetoyourapplicationmodule:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{FormsModule}from'@angular/forms';
import{ArticleEditorComponent}from'./article-editor.component';
@NgModule({
imports:[
BrowserModule,
FormsModule
],
declarations:[
ArticleEditorComponent
],
bootstrap:[
ArticleEditorComponent
]
})
exportclassAppModule{}
Next,fleshoutyourcomponent,whichwillhavetwoinstancesofinputboundtothesamecomponentmemberusingngModel:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-editor',
template:`
<input[(ngModel)]="title">
<input[(ngModel)]="title">
<h2>{{title}}</h2>
`
})
exportclassArticleEditorComponent{
title:string;
}
That'sallthat'srequired!Youshouldseeinputmodificationsinstantlyreflectedintheotherinputaswellasin<h2>itself.
Howitworks...Whatyou'rereallydoingisbindingtotheeventandpropertythatngModelassociateswiththisinput.Whenthecomponent'stitlememberchanges,theinputisboundtothatvalueandwillupdateitsownvalue.Whentheinput'svaluechanges,itemitsanevent,whichngModelwillbindtoandextractthevaluefrombeforepropagatingittothecomponent'stitlemember.
Note
Thebanana-in-a-boxsyntax[()]issimplyindicativeofthebindingdonetoboththeinputpropertywith[]andtheinputeventswith().
Inreality,thisisshorthandforthefollowing:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-editor',
template:`
<input[ngModel]="title"(ngModelChange)="title=$event">
<input[ngModel]="title"(ngModelChange)="title=$event">
<h2>{{title}}</h2>
`
})
exportclassArticleEditorComponent{
title:string;
}
Youwillfindthatthisbehavesidenticallytowhatwediscussedbefore.
There'smore...Youmightstillfindthatthere'sabittoomuchsyntacticalsugarhappeninghereforyourtaste.You'rebindingtongModel,butsomehow,itisequivalenttotheinputvalue.Similarly,you'rebindingtongModelChangeevents,whichareallemittinga$eventthatappearstobeonlyastring.
Thisisindeedcorrect.ThengModeldirectiveunderstandswhatitisapartofandisabletointegrate[ngModel]and(ngModelChange)correctlytoassociatethedesiredbindings.
Thecoreofthesebindingsisessentiallydoingthefollowing:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-editor',
template:`
<input[value]="title"(input)="title=$event.target.value">
<input[value]="title"(input)="title=$event.target.value">
<h2>{{title}}</h2>
`
})
exportclassArticleEditorComponent{
//Initializetitle,otherwiseyou'llget"undefined"
title:string='';
}
SeealsoImplementingsimpletwo-waydatabindingwithngModeldemonstratesthenewwayinAngular2tocontrolbidirectionaldataflowImplementingbasicfieldvalidationwithaFormControldetailsthebasicbuildingblockofanAngularformBundlingFormControlswithaFormGroupshowshowtocombineFormControlsBundlingFormControlswithaFormArrayshowshowtohandleiterableformelementsImplementingbasicformswithngFormdemonstratesAngular'sdeclarativeformconstructionImplementingbasicformswithFormBuilderandformControlNameshowshowtousetheFormBuilderservicetoquicklyputtogethernestedformsCreatingandusingacustomvalidatordemonstrateshowtocreateacustomdirectivethatbehavesasinputvalidationCreatingandusingacustomasynchronousvalidatorwithPromisesshowshowAngularallowsyoutohaveadelayedevaluationofaformstate
ImplementingbasicfieldvalidationwithaFormControlThesimplestformbehaviorimaginablewouldbethevalidationofasingleinputfield.Mostofthetime,utilizing<form>tagsandgoingthroughtherestoftheboilerplateisgoodpractice,butforthepurposeofcheckingasingleinput,it'spreferabletodistillthisdowntothebareminimumrequiredinordertouseinputchecking.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/4076/.
GettingreadySupposethefollowingisyourinitialsetup:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-editor',
template:`
<p>Articletitle(required):</p>
<inputrequired>
<button>Save</button>
<h1>{{title}}</h1>
`
})
exportclassArticleEditorComponent{
title:string;
}
Yourgoalistochangethisinawaythatclickingthesavebuttonwillvalidatetheinputandupdatethetitlememberonlyifitisvalid.
Howtodoit...ThemostelementalcomponentofAngularformsistheFormControlobject.Inordertobeabletoassessthestateofthefield,youfirstneedtoinstantiatethisobjectinsidethecomponentandassociateitwiththefieldusingtheformControldirective.FormControllivesinsideReactiveFormsModule.Additasamoduleimport:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{ReactiveFormsModule}from'@angular/forms';
import{ArticleEditorComponent}from'./article-editor.component';
@NgModule({
imports:[
BrowserModule,
ReactiveFormsModule
],
declarations:[
ArticleEditorComponent
],
bootstrap:[
ArticleEditorComponent
]
})
exportclassAppModule{}
Withthis,youcanuseFormControlinsideArticleEditorComponent.InstantiateFormControlinsidethecomponentandbindtheinputelementtoitusingtheformControldirective:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<p>Articletitle(required):</p>
<input[formControl]="titleControl"required>
<button>Save</button>
<h1>{{title}}</h1>
`
})
exportclassArticleEditorComponent{
title:string;
titleControl:FormControl=newFormControl();
}
NowthatyouhavecreatedaFormControlobjectandassociateditwithaninputfield,youwillbeabletouseitsvalidationAPItocheckthestateofthefield.Allthatisleftistouseitinsidethesubmitclickhandler:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<p>Articletitle(required):</p>
<input[formControl]="titleControl"required>
<button(click)="submitTitle()">Save</button>
<h1>{{title}}</h1>
`
})
exportclassArticleEditorComponent{
title:string;
titleControl:FormControl=newFormControl();
submitTitle():void{
if(this.titleControl.valid){
this.title=this.titleControl.value;
}else{
alert("Titlerequired");
}
}
}
Withthis,thesubmitclickhandlerwillbeabletochecktheinput'svalidationstateandvaluewiththesameobject.
Howitworks...TheformControldirectiveservesonlytobindanexistingFormControlobjecttoaDOMelement.TheFormControlobjectthatyouinstantiateinsidethecomponentconstructorcaneitherutilizevalidationattributesinsideanHTMLtag(asisdoneinthisexample),oracceptAngularvalidatorswheninitialized;or,itcandoboth.
Note
It'sextremelyimportanttonotethatjustbecausetheFormControlobjectisinstantiated,itdoesnotmeanthatitisabletovalidatetheinputimmediately.
Withoutaninitializedvalue,anemptyinputfieldwillbeginitslifewithavalueofnull,whichinthepresenceofarequiredattributeisofcourseinvalid.However,inthisexample,ifyouweretocheckwhethertheFormControlobjectbecomesvalidimmediatelyafteryouinstantiateitintheconstructor,theFormControlobjectwoulddutifullyinformyouthatthestateisvalidsinceithasnotbeenboundtotheDOMelementyet,andtherefore,novalidationsarebeingviolated.Sincetheinputelement'sformControlbindingwillnotoccuruntilthecomponenttemplatebecomespartoftheactualDOM,youwillnotbeabletochecktheinputstateuntilthebindingiscompleteorinsidethengAfterContentCheckedlifecyclehook.Notethatthispertainstotheexampleunderconsideration.
OncetheformControldirectivecompletesthebinding,theFormControlobjectwillexistasaninputwrapper,allowingyoutouseitsvalidandvaluemembers.
There'smore...ThisrecipeusesReactiveFormsModule,whichissimplertounderstandsinceallofthesetupisexplicit.WhenyouuseFormsModuleinstead,youdiscoverthatalotofwhatisaccomplishedinthisrecipecouldbedoneautomaticallyforyou,suchastheinstantiationandbindingofFormControlobjects.Italsorevolvesaroundthepresenceofa<form>tag,whichisthedefactotop-levelFormControlcontainer.ThisrecipeservestodemonstrateoneofthesimplestformsofAngularformbehavior.
Validatorsandattributeduality
Asmentionedinthisrecipe,validationdefinitionscancomefromtwoplaces.Here,youusedastandardizedHTMLtagattributethatAngularrecognizesandautomaticallyincorporatesintotheFormControlvalidationspecification.YoucouldhavejustaseasilyelectedtoutilizeanAngularValidatortoaccomplishthesametaskinstead.ThiscanbeaccomplishedbyimportingAngular'sdefaultValidatorsandinitializingtheFormControlobjectwiththerequiredvalidator:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl,Validators}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<p>Articletitle(required):</p>
<input[formControl]="titleControl">
<button(click)="submitTitle()">Save</button>
<h1>{{title}}</h1>
`
})
exportclassArticleEditorComponent{
title:string;
//Firstargumentisthedefaultinputvalue
titleControl:FormControl=
newFormControl(null,Validators.required);
submitTitle():void{
if(this.titleControl.valid){
this.title=this.titleControl.value;
}else{
alert("Titlerequired");
}
}
}
Taglesscontrols
Asyoumightsuspect,thereisnoreasonaFormControlmustbeboundtoaDOMelement.FormControlisanelementalpieceofformlogicthatactsasanatomicpieceofstatefulinformation,whetherornotthisinformationisderivedfrom<input>.SayyouwantedtoaddaFormControlthatwouldpreventquickformsubmissionbyonlybecomingvalidafter10seconds.YoucouldexplicitlycreateaFormControlobjectthatwouldtieintothecombinedformvalidationbutwouldnotbeassociatedwithaDOMelement.
SeealsoImplementingsimpletwo-waydatabindingwithngModeldemonstratesthenewwayinAngular2tocontrolbidirectionaldataflowBundlingFormControlswithaFormGroupshowshowtocombineFormControlobjectsBundlingFormControlswithaFormArrayshowshowtohandleiterableformelements
BundlingcontrolswithaFormGroupNaturally,formsinapplicationsfrequentlyexisttoaggregatemultipleinstancesofinputintoaunifiedbehavior.Onecommonbehavioristoassesswhetheraformisvalid,whichofcourserequiresthatallofitssubfieldsarevalid.ThiswillmostcommonlybeachievedbybundlingmultipleFormControlobjectsintoaFormGroup.Thiscanbedoneindifferentways,withvaryingdegreesofexplicitness.Thisrecipecoversanentirelyexplicitimplementation,thatis,everythingherewillbecreatedand"joined"manually.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/3052.
GettingreadySupposeyoubeganwiththefollowingskeletonapplication:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-editor',
template:`
<p>Title:<input></p>
<p>Text:<input></p>
<p><button(click)="saveArticle()">Save</button></p>
<hr/>
<p>Preview:</p>
<divstyle="border:1pxsolid#999;margin:50px;">
<h1>{{article.title}}</h1>
<p>{{article.text}}</p>
</div>
`
})
exportclassArticleEditorComponent{
article:{title:string,text:string}={};
saveArticle():void{}
}
Yourgoalistoupdatethearticleobject(andconsequentlythetemplate)onlyifalltheinputfieldsarevalid.
Howtodoit...First,addthenecessarycodetoattachnewFormControlobjectstoeachinputfieldandvalidatethemwiththebuilt-inrequiredvalidator:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl,Validators}
from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<p>Title:<input[formControl]="titleControl"></p>
<p>Text:<input[formControl]="textControl"></p>
<p><button(click)="saveArticle()">Save</button></p>
<hr/>
<p>Preview:</p>
<divstyle="border:1pxsolid#999;margin:50px;">
<h1>{{article.title}}</h1>
<p>{{article.text}}</p>
</div>
`
})
exportclassArticleEditorComponent{
article:{title:string,text:string}={};
titleControl:FormControl
=newFormControl(null,Validators.required);
textControl:FormControl
=newFormControl(null,Validators.required);
saveArticle():void{}
}
Atthispoint,youcouldindividuallyinspecteachinput'sFormControlobjectandcheckwhetheritisvalid.However,ifthisformgrowsto100fields,itwouldbecomeunbearablytedioustomaintainthem.Therefore,youcanbundletheseFormControlobjectsintoasingleFormGroupinstead:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl,FormGroup,Validators}
from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<p>Title:<input[formControl]="titleControl"></p>
<p>Text:<input[formControl]="textControl"></p>
<p><button(click)="saveArticle()">Save</button></p>
<hr/>
<p>Preview:</p>
<divstyle="border:1pxsolid#999;margin:50px;">
<h1>{{article.title}}</h1>
<p>{{article.text}}</p>
</div>
`
})
exportclassArticleEditorComponent{
article:{title:string,text:string}={};
titleControl:FormControl
=newFormControl(null,Validators.required);
textControl:FormControl
=newFormControl(null,Validators.required);
articleFormGroup:FormGroup=newFormGroup({
title:this.titleControl,
text:this.textControl
});
saveArticle():void{}
}
FormGroupobjectsalsoexposevalidandvaluemembers,soyoucanusethesetoverifyandassigndirectlyfromtheobject:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl,FormGroup,Validators}
from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<p>Title:<input[formControl]="titleControl"></p>
<p>Text:<input[formControl]="textControl"></p>
<p><button(click)="saveArticle()">Save</button></p>
<hr/>
<p>Preview:</p>
<divstyle="border:1pxsolid#999;margin:50px;">
<h1>{{article.title}}</h1>
<p>{{article.text}}</p>
</div>
`
})
exportclassArticleEditorComponent{
article:{title:string,text:string}={};
titleControl:FormControl
=newFormControl(null,Validators.required);
textControl:FormControl
=newFormControl(null,Validators.required);
articleFormGroup:FormGroup=newFormGroup({
title:this.titleControl,
text:this.textControl
});
saveArticle():void{
if(this.articleFormGroup.valid){
this.article=this.articleFormGroup.value;
}else{
alert("Missingfield(s)!");
}
}
Withthisaddition,yourformshouldnowbeworkingfine.
Howitworks...BothFormControlandFormGroupinheritfromtheabstractbaseclasscalledAbstractControl.Whatthismeansforyouisthatbothofthemexposethesamebaseclassmethods,butFormGroupwillaggregateitscompositionofAbstractControlobjectstobereadfromitsownmembers.Asyoucanseeintheprecedingcode,validactsasalogicalANDoperatorforallthechildren(meaningeverysinglechildmustreturntrueforittoreturntrue);valuereturnsanobjectofthesametopologyastheoneprovidedattheinstantiationofFormGroup,butwitheachFormControlvalueinsteadoftheFormControlobject.
Note
Asyoumightexpect,sinceFormGroupexpectsanobjectwithAbstractControlproperties,youarefreetonestaFormGroupinsideanotherFormGroup.
There'smore...YouareabletoaccessaFormGroup'scontainedFormControlmembersviathecontrolsproperty.ThestringthatyouusedtokeytheFormControlmembers—eitheruponFormGroupinstantiation,orwiththeaddControlmethod—isusedtoretrieveit.Inthisexample,thetextFormControlobjectcouldberetrievedinsideacomponentmethodviathis.articleCtrlGroup.controls.text.
Tip
TheAngulardocumentationwarnsyoutospecificallynottomodifytheunderlyingFormControlcollectiondirectly.Thismayleadtoanundefineddatabindingbehavior.So,alwaysbesuretousetheFormGroupmembermethodsaddControlandremoveControlinsteadofdirectlymanipulatingthecollectionofFormControlobjectsthatyoupassuponinstantiation.
FormGroupvalidators
LikeControl,aFormGroupcanhaveitsownvalidators.ThesecanbeprovidedwhentheFormGroupisinstantiated,andtheybehaveinthesamewaythataFormControlvalidatorwouldbehave.ByaddingvalidatorsattheFormGrouplevel,FormGroupcanoverridethedefaultbehaviorofonlybeingvalidwhenallitscomponentsarevalidoraddingextravalidationclauses.
Errorpropagation
Angularvalidatorsnotonlyhavetheabilitytodeterminewhethertheyarevalidornot,buttheyarealsocapableofreturningerrormessagesdescribingwhatiswrong.Forexample,whentheinputfieldsareempty,ifyouweretoexaminetheerrorspropertyofthetextFormControlobjectviathis.articleCtrlGroup.controls.text.errors,itwouldreturn{required:true}.Thisisthedefaulterrormessageofthebuilt-inrequiredvalidator.However,ifyouweretoinspecttheerrorspropertyontheparentFormGroupviathis.articleCtrlGroup.errors,youwillfindittobenull.
Thismaybecounter-intuitive,butitisnotamistake.ErrormessageswillonlyappearontheFormControlinstancethatiscausingthem.Ifyouwishtoaggregateerrormessages,youwillhavetotraversethenestedcollectionsofFormControlobjectsmanually.
SeealsoImplementingsimpletwo-waydatabindingwithngModeldemonstratesthenewwayinAngular2tocontrolbidirectionaldataflowImplementingbasicfieldvalidationwithaFormControldetailsthebasicbuildingblockofanAngularformBundlingFormControlswithaFormArrayshowshowtohandleiterableformelements
BundlingFormControlswithaFormArrayYouwillmostlikelyfindthatFormGroupsaremorethancapableofservingyourneedsforthepurposeofcombiningmanyFormControlobjectsintoonecontainer.However,thereisoneverycommonpatternthatmakesitssistertype,theFormArray,extremelyuseful:variablelengthclonedinputs.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/2816/.
GettingreadySupposeyouhadthefollowingskeletonapplication:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl,Validators}
from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<p>Tags:</p>
<ul>
<li*ngFor="lettoftagControls;leti=index">
<input[formControl]="t">
</li>
</ul>
<p><button(click)="addTag()">+</button></p>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
tagControls:Array<FormControl>=[];
addTag():void{}
saveArticle():void{}
}
Yourobjectiveistomodifythiscomponentsothatanarbitrarynumberoftagscanbeaddedandsoallthetagscanbevalidatedtogether.
Howtodoit...Inmanyways,aFormArraybehavesmoreorlessidenticallytoaFormGroup.ItisimportedinthesamewayandinheritedfromAbstractControl.Also,itisinstantiatedinasimilarwayandcanaddandremoveFormControlinstances.First,addtheboilerplatetoyourapplication;thiswillallowyoutoinstantiateaninstanceofaFormArrayandpassitthearrayofFormControlobjectsalreadyinsidethecomponent.SinceyoualreadyhaveabuttonthatismeanttoinvoketheaddTagmethod,youshouldalsoconfigurethismethodtopushanewFormControlontotagControl:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl,FormArray,Validators}
from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<p>Tags:</p>
<ul>
<li*ngFor="lettoftagControls;leti=index">
<input[formControl]="t">
</li>
</ul>
<p><button(click)="addTag()">+</button></p>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
tagControls:Array<FormControl>=[];
tagFormArray:FormArray=newFormArray(this.tagControls);
addTag():void{
this.tagFormArray
.push(newFormControl(null,Validators.required));
}
saveArticle():void{}
}
Note
Atthispoint,it'simportantthatyoudon'tconfuseyourselfwithwhatyouareworkingwith.InsidethisArticleEditorcomponent,youhaveanarrayofFormControlobjects(tagControls)andyoualsohaveasingleinstanceofFormArray(tagFormArray).TheFormArrayinstanceisinitializedbybeingpassedthearrayofFormControlobjects,whichitwillthenbeabletomanage.
NowthatyourFormArrayismanagingthetag'sFormControlobjects,youcansafelyuseitsvalidator:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl,FormArray,Validators}
from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<p>Tags:</p>
<ul>
<li*ngFor="lettoftagControls;leti=index">
<input[formControl]="t">
</li>
</ul>
<p><button(click)="addTag()">+</button></p>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
tagControls:Array<FormControl>=[];
tagFormArray:FormArray=newFormArray(this.tagControls);
addTag():void{
this.tagFormArray
.push(newFormControl(null,Validators.required));
}
saveArticle():void{
if(this.tagFormArray.valid){
alert('Valid!');
}else{
alert('Missingfield(s)!');
}
}
}
Howitworks...Becausethetemplateisreactingtotheclickevent,youareabletouseAngulardatabindingtoautomaticallyupdatethetemplate.However,itisextremelyimportantthatyounotetheasymmetryinthisexample.ThetemplateisiteratingthroughthetagControlsarray.However,whenyouwanttoaddanewFormControlobject,youpushittotagFormArray,whichwillinturnpushittothetagControlsarray.TheFormArrayobjectactsasthemanagerofthecollectionofFormControlobjects,andallmodificationsofthiscollectionshouldgothroughthemanager,notthecollectionitself.
Tip
TheAngulardocumentationwarnsyoutospecificallynotmodifytheunderlyingFormControlcollectiondirectly.Thismayleadtoundefineddatabindingbehavior,soalwaysbesuretousetheFormArraymemberspush,insert,andremoveAtinsteadofdirectlymanipulatingthearrayofFormControlobjectsthatyoupassuponinstantiation.
There'smore...Youcantakethisexampleonestepfurtherbyaddingtheabilitytoremovefromthislistaswell.SinceyoualreadyhavetheindexinsidethetemplaterepeaterandFormArrayoffersindex-basedremoval,thisissimpletoimplement:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl,FormArray,Validators}
from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<p>Tags:</p>
<ul>
<li*ngFor="lettoftagControls;leti=index">
<input[formControl]="t">
<button(click)="removeTag(i)">X</button>
</li>
</ul>
<p><button(click)="addTag()">+</button></p>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
tagControls:Array<FormControl>=[];
tagFormArray:FormArray=newFormArray(this.tagControls);
addTag():void{
this.tagFormArray
.push(newFormControl(null,Validators.required));
}
removeTag(idx:number):void{
this.tagFormArray.removeAt(idx);
}
saveArticle():void{
if(this.tagFormArray.valid){
alert('Valid!');
}else{
alert('Missingfield(s)!');
}
}
}
ThisallowsyoutocleanlyinsertandremoveFormControlinstanceswhilelettingAngulardatabindingdoalloftheworkforyou.
SeealsoImplementingsimpletwo-waydatabindingwithngModeldemonstratesthenewwayinAngular2tocontrolbidirectionaldataflowImplementingbasicformswithngFormdemonstratesAngular'sdeclarativeformconstructionImplementingbasicformswithFormBuilderandformControlNameshowshowtousetheFormBuilderservicetoquicklyputtogethernestedforms
ImplementingbasicformswithNgFormThebasicdenominationsofAngularformsareFormControl,FormGroup,andFormArrayobjects.However,itisoftennotdirectlynecessarytousetheseobjectsatall;Angularprovidesmechanismswithwhichyoucanimplicitlycreateandassigntheseobjectsandattachthemtotheform'sDOMelements.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/5116/.
GettingreadySupposeyoubeganwiththefollowingskeletonapplication:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-editor',
template:`
<p><inputplaceholder="Articletitle"></p>
<p><textareaplaceholder="Articletext"></textarea></p>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
saveArticle():void{}
}
YourobjectiveistocollectalloftheformdataandsubmititusingAngular'sformconstructs.
Howtodoit...Youshouldbeginbyreorganizingthisintoanactualbrowserform.Angulargivesyoualotofdirectivesandcomponentsforthis,andimportingtheFormsModulewillgiveyouaccesstoalltheonesyouneedmostofthetime:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{FormsModule}from'@angular/forms';
import{ArticleEditorComponent}from'./article-editor.component';
@NgModule({
imports:[
BrowserModule,
FormsModule
],
declarations:[
ArticleEditorComponent
],
bootstrap:[
ArticleEditorComponent
]
})
exportclassAppModule{}
Inaddition,youshouldreconfigurethebuttonsoitbecomesanactualsubmitbutton.Thehandlershouldbetriggeredwhentheformissubmitted,soyoucanreattachthelistenertotheform'snativesubmiteventinsteadofthebutton'sclickevent.AngularprovidesanngSubmitEventEmitterontopofthisevent,sogoaheadandattachthelistenertothis:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-editor',
template:`
<form(ngSubmit)="saveArticle()">
<p><inputplaceholder="Articletitle"></p>
<p><textareaplaceholder="Articletext"></textarea></p>
<p><buttontype="submit">Save</button></p>
</form>
`
})
exportclassArticleEditorComponent{
saveArticle():void{}
}
Next,youshouldconfiguretheformtopasstheformdatatothehandlerthroughatemplatevariable.
Note
TheformelementwillhaveanNgFormobject(andinsidethis,aFormGroup)automaticallyassociatedwithitwhenyouimportFormsModuleintotheencompassingmodule.AngularcreatesandassociatestheNgForminstancebehindthescenes.
OnewayyoucanaccessthisinstanceisbyassigningthengFormdirectiveasatemplatevariable.It'sabitofsyntacticalmagic,butusing#f="ngForm"signalstoAngularthatyouwanttobeabletoreferencetheform'sNgFormfromthetemplateusingthefvariable.
Onceyoudeclarethetemplatevariable,youareabletopassthengForminstancetothesubmithandlerasanargument,specificallyassaveArticle(f).
Thisleavesyouwiththefollowing:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{NgForm}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<form#f="ngForm"
(ngSubmit)="saveArticle(f)">
<p><inputplaceholder="Articletitle"></p>
<p><textareaplaceholder="Articletext"></textarea></p>
<p><buttontype="submit">Save</button></p>
</form>
`
})
exportclassArticleEditorComponent{
saveArticle(f:NgForm):void{
console.log(f);
}
}
Whenyoutestthismanually,youshouldseeyourbrowserlogginganNgFormobjecteverytimeyouclickontheSavebutton.Insidethisobject,youshouldseeashinynewFormGroupandalsothengSubmitEventEmitterthatyouarelisteningto.Sofar,sogood!
DeclaringformfieldswithngModel
Youmayhavenoticedthatnoneoftheformfieldshavebeencollected.This,ofcourse,isbecauseAngularhasnotbeeninstructedtopayattentiontothem.Forthis,FormsModuleprovidesyouwithngModel,whichwilldocertainthingsforyou:
InstantiateaFormControlobject.AttachittotheDOMelementthatincorporatesthengModelattribute.LocatetheFormGroupthattheelementlivesinsideandaddtoittheFormControlitjustcreated.ThestringvalueofthenameattributewillbeitskeyinsidetheFormGroup.
Note
Thislastbulletisimportant,asattemptingtousengModelwithoutanencompassingformcontrolconstructtoattachitselftowillresultinerrors.Thisformcontrolconstructcanbetheform'sFormGroupitself,oritcanevenbeachildFormGroupinstance.
Withthis,goaheadandaddngModeltoeachofthetextinputfields:
import{Component}from'@angular/core';
import{NgForm}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<form#f="ngForm"
(ngSubmit)="saveArticle(f)">
<p><inputngModel
name="title"
placeholder="Articletitle"></p>
<p><textareangModel
name="text"
placeholder="Articletext"></textarea></p>
<p><buttontype="submit">Save</button></p>
</form>
`
})
exportclassArticleEditorComponent{
saveArticle(f:NgForm):void{
console.log(f);
}
}
Yourformshouldnowbefullyfunctional.Inthesubmithandler,youcanverifythatFormGrouphastwoFormControlobjectsattachedtoitbyinspectingf.form.controls,whichshouldgiveyouthefollowing:
{
text:FormControl{...},
title:FormControl{...}
}
Howitworks...Inessence,youareusingthehierarchicalnatureoftheDOMtodirecthowyourFormControlarchitectureisstructured.ThetopmostNgForminstanceiscoupledwithaFormGroup;insidethis,therestoftheform'sFormControlobjectswillreside.
EachngModeldirectsitsreferencedFormControltotheFormGroupownedbytheNgForminstance.Withthisnestedstructurenowassembled,itispossibletoreadandreasonthestateoftheentireformfromtheNgFormobject.Thisbeingthecase,passingthisobjecttothesubmithandlerwillallowyoutomanageeveryaspectofforminspectionandvalidation.
There'smore...If,instead,youwantedtogroupsomeofthesefieldstogether,thiscanbeaccomplishedbysimplywrappingthemwithanngModelGroupdirective.SimilartongModel,thisautomaticallyinstantiatesaFormGroupandattachesittotheparentFormGroup;also,itwilladdanyenclosedFormControlorFormGroupobjectstoitself.Forexample,refertothefollowing:
[app/article.component.ts]
import{Component}from'@angular/core';
import{NgForm}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<form#f="ngForm"
(ngSubmit)="saveArticle(f)">
<divngModelGroup="article">
<p><inputngModel
name="title"
placeholder="Articletitle"></p>
<p><textareangModel
name="text"
placeholder="Articletext"></textarea></p>
</div>
<p><buttontype="submit">Save</button></p>
</form>
`
})
exportclassArticleEditorComponent{
saveArticle(f:NgForm):void{
console.log(f);
}
}
Now,inspectingf.form.controlswillrevealthatithasasingleFormGroupkeyedbyarticle:
{
article:FormGroup:{
controls:{
text:FormControl{...},
title:FormControl{...}
},
...
}
}
Sincethismatchesthestructureyousetupinthetemplate,itchecksout.
SeealsoImplementingsimpletwo-waydatabindingwithngModeldemonstratesthenewwayinAngular2tocontrolbidirectionaldataflowImplementingbasicformswithFormBuilderandformControlNameshowshowtousetheFormBuilderservicetoquicklyputtogethernestedforms
ImplementingbasicformswithFormBuilderandformControlNameOutofthebox,Angularprovidesawayforyoutoputtogetherformsthatdon'trelyonthetemplatehierarchyfordefinition.Instead,youcanuseFormBuildertoexplicitlydefinehowyouwanttostructuretheformobjectsandthenmanuallyattachthemtoeachinput.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/9302/.
GettingreadySupposeyoubeganwiththefollowingskeletonapplication:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article-editor',
template:`
<p><inputplaceholder="Articletitle"></p>
<p><textareaplaceholder="Articletext"></textarea></p>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
constructor(){}
saveArticle():void{}
}
YourobjectiveistocollectalloftheformdataandsubmititusingAngular'sformconstructs.
Howtodoit...FormBuilderisincludedinReactiveFormsModule,soyouwillneedtoimportthesetargetsintotheapplicationmodule:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{ReactiveFormsModule}from'@angular/forms';
import{ArticleEditorComponent}from'./article-editor.component';
@NgModule({
imports:[
BrowserModule,
ReactiveFormsModule
],
declarations:[
ArticleEditorComponent
],
bootstrap:[
ArticleEditorComponent
]
})
exportclassAppModule{}
Additionally,youwillneedtoinjectitintoyourcomponenttomakeuseofit.InAngular2,thiscansimplybeaccomplishedbylistingitasatypedconstructorparameter.TheFormBuilderusesthegroup()methodtoreturnthetop-levelFormGroup,whichyoushouldassigntoyourcomponentinstance.Fornow,youwillpassanemptyobjectasitsonlyargument.
Withallthis,youcanintegratethearticleGroupFormGroupintothetemplatebyattachingitinsideaformtagusingtheformGroupdirective:
[app/article-editor.component.ts]
import{Component,Inject}from'@angular/core';
import{FormBuilder,FormGroup}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<form[formGroup]="articleGroup"
(ngSubmit)="saveArticle()">
<p><inputplaceholder="Articletitle"></p>
<p><textareaplaceholder="Articletext"></textarea></p>
<p><buttontype="submit">Save</button></p>
</form>
`
})
exportclassArticleEditorComponent{
articleGroup:FormGroup;
constructor(@Inject(FormBuilder)formBuilder:FormBuilder){
this.articleGroup=formBuilder.group({});
}
saveArticle():void{}
}
Withallthis,youhavesuccessfullycreatedthestructureforyourform,butFormGroupisstillnotconnectedtothemultipleinput.Forthis,youwillfirstsetupthestructureofthecontrolsinsidethebuilderandconsequentlyattachthemtoeachinputtagwithformControlName,asfollows:
[app/article-editor.component.ts]
import{Component,Inject}from'@angular/core';
import{FormBuilder,FormGroup,Validators}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<form[formGroup]="articleGroup"
(ngSubmit)="saveArticle()">
<p><inputformControlName="title"
placeholder="Articletitle"></p>
<p><textareaformControlName="text"
placeholder="Articletext"></textarea></p>
<p><buttontype="submit">Save</button></p>
</form>
`
})
exportclassArticleEditorComponent{
articleGroup:FormGroup;
constructor(@Inject(FormBuilder)formBuilder:FormBuilder){
this.articleGroup=formBuilder.group({
title:[null,Validators.required],
text:[null,Validators.required]
});
}
saveArticle():void{
console.log(this.articleGroup);
}
}
Withthis,yourformwillhavetwoFormControlobjectsinstantiatedinsideit,andtheywillbeassociatedwithproperinputelements.WhenyouclickonSubmit,youwillbeabletoseetheinputFormControlsinsideFormGroup.However,youmayprefertonamespacetheseFormControlobjectsinsideanarticledesignation,andyoucaneasilydothisbyintroducinganngFormGroupandacorrespondinglevelofindirectioninsidetheformBuilderdefinition:
[app/article-editor.component.ts]
import{Component,Inject}from'@angular/core';
import{FormBuilder,FormGroup,Validators}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<form[formGroup]="articleGroup"
(ngSubmit)="saveArticle()">
<divformGroupName="article">
<p><inputformControlName="title"
placeholder="Articletitle"></p>
<p><textareaformControlName="text"
placeholder="Articletext"></textarea></p>
</div>
<p><buttontype="submit">Save</button></p>
</form>
`
})
exportclassArticleEditorComponent{
articleGroup:FormGroup;
constructor(@Inject(FormBuilder)formBuilder:FormBuilder){
this.articleGroup=formBuilder.group({
article:formBuilder.group({
title:[null,Validators.required],
text:[null,Validators.required]
})
});
}
saveArticle():void{
console.log(this.articleGroup);
}
}
Now,thetitleandtextFormControlobjectswillexistnestedinsideanarticleFormGroupandtheycanbesuccessfullyvalidatedandinspectedinthesubmithandler.
Howitworks...Asyoumightsuspect,thearrayslivinginsidetheformBuilder.groupdefinitionswillbeappliedasargumentstoaFormControlconstructor.ThisisnicesinceyoucanavoidthenewFormControl()boilerplatewhencreatingeachcontrol.ThestringthatkeystheFormControlislinkedtoitwithformControlName.BecauseyouareusingformControlNameandformGroupName,youwillneedtohavetheformBuildernestedstructurematchexactlytowhatisthereinthetemplate.
There'smore...ItistotallyunderstandablethathavingtoduplicatethestructureinthetemplateandtheFormBuilderdefinitionisalittleannoying.Thisisespeciallytrueinthiscase,asthepresenceofformGroupdoesn'treallyaddanyvaluablebehaviorsinceitisattachedtoaninertdivelement.Instead,youmightwanttobeabletodothisarticlenamespacegroupingwithoutmodifyingthetemplate.ThisbehaviorcanbeaccomplishedwithformControl,whosebehaviorissimilartoformModel(itbindstoanexistinginstanceonthecomponent).
Note
Notetheparadigmthatisbeingdemonstratedwiththesedifferentkindsofformdirectives.WiththingssuchasngForm,formGroup,formArray,andformControl,Angularisimplicitlycreatingandlinkingtheseinstances.IfyouchoosetonotuseFormBuildertodefinehowFormControlsbehave,thiscanbeaccomplishedbyaddingvalidationdirectivestothetemplate.Ontheotherhand,youalsohaveformModelandformControl,whichbindtotheinstancesofthesecontrolobjectsthatyoumustmanuallycreateonthecomponent.
[app/article-editor.component.ts]
import{Component,Inject}from'@angular/core';
import{FormBuilder,FormControl,FormGroup,Validators}
from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<form[formGroup]="articleGroup"
(ngSubmit)="saveArticle()">
<p><input[formControl]="titleControl"
placeholder="Articletitle"></p>
<p><textarea[formControl]="textControl"
placeholder="Articletext"></textarea></p>
<p><buttontype="submit">Save</button></p>
</form>
`
})
exportclassArticleEditorComponent{
titleControl:FormControl
=newFormControl(null,Validators.required);
textControl:FormControl
=newFormControl(null,Validators.required);
articleGroup:FormGroup;
constructor(@Inject(FormBuilder)formBuilder:FormBuilder){
this.articleGroup=formBuilder.group({
article:formBuilder.group({
title:this.titleControl,
text:this.textControl
})
});
}
saveArticle():void{
console.log(this.articleGroup);
}
}
Importantly,notethatyouhavecreatedanidenticaloutputoftheoneyoucreatedearlier.titleandtextarebundledinsideanarticleFormGroup.However,thetemplatedoesn'tneedtohaveanyreferencetothisintermediateFormGroup.
SeealsoImplementingsimpletwo-waydatabindingwithngModeldemonstratesthenewwayinAngular2tocontrolbidirectionaldataflowImplementingbasicfieldvalidationwithaFormControldetailsthebasicbuildingblockofanAngularformImplementingbasicformswithngFormdemonstratesAngular'sdeclarativeformconstruction
CreatingandusingacustomvalidatorThebasicbuilt-invalidatorsthatAngularprovideswillgetyouofftheground,butifyourapplicationreliesonforms,youwillundoubtedlycometoapointwhereyouwillwanttodefineyourownvalidatorlogic.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/8574/.
GettingreadySupposeyouhadstartedwiththefollowingskeletonapplication:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl,Validators}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<h2>PsychStudyonHumilityWinsMajorAward</h2>
<textarea[formControl]="bodyControl"
placeholder="Articletext"></textarea>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
articleBody:string='';
bodyControl:Control
=newFormControl(null,Validators.required);
saveArticle():void{
if(this.bodyControl.valid){
alert('Valid!');
}else{
alert('Invalid!');
}
}
}
Yourobjectiveistoaddanadditionalvalidationtothetextareathatwilllimititto10words.(Theeditorialstaffisbigonbrevity.)
Howtodoit...IfyoulookatthefunctionsignatureofanAbstractControl,youwillnoticethatthevalidatorargumentisjustaValidatorFn.ThisvalidatorfunctioncanbeanyfunctionthatacceptsanAbstractControlobjectasitssoleargumentandreturnsanobjectkeyedwithstringsfortheerrorobject.Thiserrorobjectactsasadictionaryoferrors,andavalidatorcanreturnasmanyerrorsasapplicable.Thevalueofthedictionaryentrycan(andshould)containmetadataaboutwhatiscausingtheerror.Iftherearenoerrorsfoundbythecustomvalidator,itshouldjustreturnnull.
Thesimplestwaytoimplementthisisbyaddingamembermethodtothecomponent:
[app/article-editor.component.ts]
exportclassArticleEditor{
articleBody:string
bodyCtrl:Control
constructor(){
this.articleBody='';
this.bodyCtrl=newControl('',Validators.required);
}
wordCtValidator(c:Control):{[key:string]:any}{
letwordCt:number=(c.value.match(/\S+/g)||[]).length;
returnwordCt<=10?
null:
{'maxwords':{'limit':10,'actual':wordCt}};
}
saveArticle(){
if(this.bodyCtrl.valid){
alert('Valid!');
}else{
alert('Invalid!');
}
}
}
Note
Here,you'reusingaregularexpressiontomatchanynon-whitespacestrings,whichcanbetreatedasa"word."YoualsoneedtoinitializetheFormControlobjecttoanemptystringsinceyouareusingthestringprototype'smatchmethod.Sincethisregularexpressionwillreturnnullwhentherearenomatches,afallback||[]clauseisaddedtoalwaysyieldsomethingthathasalengthmethod.
Nowthatthevalidatormethodisdefined,youneedtoactuallyuseitonFormControl.Angularallowsyoutobundleanarrayofvalidatorsintoasinglevalidator,evaluatingtheminorder:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl,Validators}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<h2>PsychStudyonHumilityWinsMajorAward</h2>
<textarea[formControl]="bodyControl"
placeholder="Articletext"></textarea>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
articleBody:string='';
bodyControl:FormControl=newFormControl(null,
[Validators.required,this.wordCtValidator]);
wordCtValidator(c:FormControl):{[key:string]:any}{
letwordCt:number
=((c.value||'').match(/\S+/g)||[]).length;
returnwordCt<=10?
null:
{maxwords:{limit:10,actual:wordCt}};
}
saveArticle():void{
if(this.bodyControl.valid){
alert('Valid!');
}else{
alert('Invalid!');
}
}
}
Withthis,yourFormControlshouldnowonlybevalidwhenthereare10wordsorfewerandtheinputisnotempty.
Howitworks...AFormControlexpectsaValidatorFnwithaspecifiedreturntype,butitdoesnotcarewhereitcomesfrom.Therefore,youwereabletodefineamethodinsidethecomponentclassandjustpassitalongwhenFormControlwasinstantiated.
TheFormControlobjectassociatedwithagiveninputmustbeabletohavevalidatorsassociatedwithit.Inthisrecipe,youfirstimplementedcustomvalidationusingexplicitassociationviatheinstantiationargumentsanddefiningthevalidatorasasimplestandaloneValidationFn.
There'smore...Yourinnersoftwareengineershouldbetotallydissatisfiedwiththissolution.Thevalidatoryoujustdefinedcannotbeusedoutsidethiscomponentwithoutinjectingtheentirecomponent,andexplicitlylistingeveryvalidatorwheninstantiatingtheFormControlisamajorpain.
Refactoringintovalidatorattributes
AsuperiorsolutionistoimplementaformalValidatorclass.Thishasseveralbenefits:youwillbeabletoimport/exporttheclassandusethevalidatorasanattributeinthetemplate,whichobviatestheneedforbundlingvalidatorswithValidators.compose.
Yourstrategyshouldbetocreateadirectivethatcanfunctionnotonlyasanattribute,butalsoassomethingthatAngularcanrecognizeasaformalValidatorandautomaticallyincorporateitassuch.ThiscanbeaccomplishedbycreatingadirectivethatimplementstheValidatorinterfaceandalsobundlesthenewValidatordirectiveintotheexistingNG_VALIDATORStoken.
Note
Fornow,don'tworryaboutthespecificsofwhatishappeningwiththeprovidersarrayinsidethedirectivemetadataobject.Thiswillbecoveredindepthinthechapterondependencyinjection.AllthatyouneedtoknowhereisthatthiscodeisallowingtheFormControlobjectboundtotextareatoassociatethecustomvalidatoryouarebuildingwithit.
First,movethevalidationmethodtoitsowndirectivebyperformingthestepsmentionedintheprecedingparagraph:
[app/max-word-count.validator.ts]
import{Directive}from'@angular/core';
import{Validator,FormControl,NG_VALIDATORS}
from'@angular/forms';
@Directive({
selector:'[max-word-count]',
providers:[{
provide:NG_VALIDATORS,
useExisting:MaxWordCountValidator,
multi:true
}]
})
exportclassMaxWordCountValidatorimplementsValidator{
validate(c:FormControl):{[key:string]:any}{
letwordCt:number=((c.value||'')
.match(/\S+/g)||[]).length;
returnwordCt<=10?
null:
{maxwords:{limit:10,actual:wordCt}};
}
}
Next,addthisdirectivetotheapplicationmodule:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{ReactiveFormsModule}from'@angular/forms';
import{ArticleEditorComponent}from'./article-editor.component';
import{MaxWordCountValidator}from'./max-word-count.validator';
@NgModule({
imports:[
BrowserModule,
ReactiveFormsModule
],
declarations:[
ArticleEditorComponent,
MaxWordCountValidator
],
bootstrap:[
ArticleEditorComponent
]
})
exportclassAppModule{}
Thismakesitavailabletoallthecomponentsinthismodule.What'smore,theproviderconfigurationyouspecifiedbeforeallowsyoutosimplyaddthedirectiveattributetoanyinput,andAngularwillbeabletoincorporateitsvalidationfunctionintothatFormControl.Theintegrationisasfollows:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<h2>PsychStudyonHumilityWinsMajorAward</h2>
<textarea[formControl]="bodyControl"
required
max-word-count
placeholder="Articletext"></textarea>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
articleBody:string='';
bodyControl:FormControl=newFormControl();
saveArticle():void{
if(this.bodyControl.valid){
alert('Valid!');
}else{
alert('Invalid!');
}
}
}
Thisisalreadyfarsuperior.TheMaxWordCountdirectivecannowbeimportedandusedanywhereinourapplicationbysimplylistingitasadirectivedependencyinacomponent.There'snoneedfortheValidator.composenastinesswheninstantiatingaFormControlobject.
Tip
ThisisespeciallyusefulwhenyouareimplicitlycreatingtheseFormControlobjectswithformControlandotherbuilt-informdirectives,whichformanyapplicationswillbetheprimaryformutilizationmethod.Buildingyourcustomvalidatorasanattributedirectivewillintegrateseamlesslyinthesesituations.
Youshouldstillbedissatisfiedthough,asthevalidatorishardcodedtocheckfor10words.Youwouldinsteadliketoleavethisuptotheinputthatisusingit.Therefore,youshouldchangethedirectivetoacceptasingleparameter,whichwilltaketheformoftheattribute'svalue:
[app/max-word-count.validator.ts]
import{Directive}from'@angular/core';
import{Validator,FormControl,NG_VALIDATORS}
from'@angular/forms';
@Directive({
selector:'[max-word-count]',
inputs:['rawCount:max-word-count'],
providers:[{
provide:NG_VALIDATORS,
useExisting:MaxWordCountValidator,
multi:true
}]
})
exportclassMaxWordCountValidatorimplementsValidator{
rawCount:string;
validate(c:FormControl):{[key:string]:any}{
letwordCt:number=
((c.value||'').match(/\S+/g)||[]).length;
returnwordCt<=this.maxCount?
null:
{maxwords:{limit:this.maxCount,actual:wordCt}};
}
getmaxCount():number{
returnparseInt(this.rawCount);
}
}
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<h2>PsychStudyonHumilityWinsMajorAward</h2>
<textarea[formControl]="bodyControl"
required
max-word-count="10"
placeholder="Articletext"></textarea>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
articleBody:string='';
bodyControl:FormControl=newFormControl();
saveArticle():void{
if(this.bodyControl.valid){
alert('Valid!');
}else{
alert('Invalid!');
}
}
}
Nowyouhavedefinedthevalueoftheattributeasaninputtothevalidator,whichyoucanthenusetoconfigurehowthevalidatorwilloperate.
SeealsoCreatingandusingacustomasynchronousvalidatorwithPromisesshowshowAngularallowsyoutohaveadelayedevaluationoftheformstate
CreatingandusingacustomasynchronousvalidatorwithPromisesAstandardvalidatoroperatesundertheassumptionthatthevalidityofacertaininputcanbecalculatedinashortamountoftimethattheapplicationcanwaittogetoverwithbeforeitcontinuesfurther.What'smore,Angularwillrunthisvalidationeverytimethevalidatorisinvoked,whichmightbequiteoftenifformvalidationisboundtorapid-fireeventssuchaskeypresses.
Therefore,itmakesgoodsensethataconstructexiststhatwillallowyoutosmoothlyhandlethevalidationproceduresthattakeanarbitraryamountoftimetoexecuteorproceduresthatmightnotreturnatall.Forthis,AngularoffersasyncValidator,whichisfullycompatiblewithPromises.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/7811/.
GettingreadySupposeyouhadstartedwiththefollowingskeletonapplication:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl}from'@angular/forms';
@Component({
selector:'article-editor',
template:`
<h2>NewYork-StylePizzaActuallySaucyCardboard</h2>
<textarea[formControl]="bodyControl"
placeholder="Articletext">
</textarea>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
articleBody:string='';
bodyControl:FormControl=newFormControl();
saveArticle():void{
if(this.bodyControl.valid){
alert('Valid!');
}else{
alert('Invalid!');
}
}
}
Yourobjectiveistoconfigurethisforminawaythatitwillbecomevalidonly5secondsaftertheuserenterstheinputinordertodetersimplespambots.
Howtodoit...First,createyourvalidatorclass,andinsideit,placeastaticvalidationmethod.Thisissimilartoasynchronousvalidationmethod,butitwillinsteadreturnaPromiseobject,passingtheresultdatatotheresolvemethod.TheFormControlobjectacceptstheasyncValidatorasitsthirdargument.Ifyouweren'tusinganynormalValidators,youcouldleaveitasnull.
Tip
AsyouwouldcombineseveralValidatorsintooneusingValidators.compose,asyncValidatorscanbecombinedusingValidators.composeAsync.
Createthevalidatorskeletoninitsownfile:
[app/delay.validator.ts]
import{FormControl,Validator}from'@angular/forms';
exportclassDelayValidatorimplementsValidator{
staticvalidate(c:FormControl):Promise<{[key:string]:any}>{
}
}
Thoughthevalidatordoesnotyetdoanything,youmaystilladdittothecomponent:
[app/article-editor.component.ts]
import{Component}from'@angular/core';
import{FormControl,Validators}from'@angular/forms';
import{DelayValidator}from'./delay.validator';
@Component({
selector:'article-editor',
template:`
<h2>NewYork-StylePizzaActuallySaucyCardboard</h2>
<textarea[formControl]="bodyControl"
placeholder="Articletext">
</textarea>
<p><button(click)="saveArticle()">Save</button></p>
`
})
exportclassArticleEditorComponent{
articleBody:string='';
bodyControl:FormControl=
newFormControl(null,null,DelayValidator.validate);
saveArticle():void{
if(this.bodyControl.valid){
alert('Valid!');
}else{
alert('Invalid!');
}
}
}
Thevalidatormustreturnapromise,butthispromisedoesn'teverneedtoberesolved.Furthermore,you'dliketosetthedelaytoonlyonetimeperrendering.Sointhiscase,youcanjustattachthepromisetotheFormControl:
[app/delay.validator.ts]
import{FormControl,Validator}from'@angular/forms';
exportclassDelayValidatorimplementsValidator{
staticvalidate(c:FormControl):Promise<{[key:string]:any}>{
if(c.pristine&&!c.value){
returnnewPromise;
}
if(!c.delayPromise){
c.delayPromise=newPromise((resolve)=>{
setTimeout(()=>{
console.log('resolve');
resolve();
},5000);
});
}
returnc.delayPromise;
}
}
Withthisaddition,theformwillremaininvaliduntil5secondsafterthefirsttimethevalueofthetextareaischanged.
Howitworks...Asynchronousvalidatorsarehandledindependentlyviaregular(synchronous)validators,butotherthantheirinternallatencydifferences,theyultimatelybehaveinnearlytheexactsameway.TheimportantdifferenceisthatanasyncValidator,apartfromthevalidandinvalidstatesthatitshareswithanormalValidator,hasapendingstate.TheFormControlwillremaininthisstateuntilapromiseismadeindicatingtheValidatorwillreturneitherresolvesorrejects.
Note
AFormControlinapendingstateistreatedasinvalidforthepurposeofcheckingthevalidityofaggregatingconstructs,suchasFormGrouporFormArray.
IntheValidatoryoujustcreated,checkingthepristinepropertyofFormControlisafinewayofascertainingwhetherornottheformis"fresh."Beforetheusermodifiestheinput,pristineistrue;followinganymodification(evenremovingalloftheenteredtext),pristinebecomesfalse.Therefore,itisaperfecttoolinthisexample,asitallowsustohavetheFormControlmaintaintheformstatewithoutovercomplicatingtheValidator.
There'smore...It'scriticaltonotetheformthatthisvalidatortakes.ThevalidationmethodinsidetheDelayValidatorclassisastaticmethodandnowhereistheDelayValidatorclassbeinginstantiated.Thepurposeoftheclassismerelytohousethevalidator.Therefore,youareunabletostoreinformationinsidethisclass,sincetherearenoinstancesinwhichyoucandoso.
Tip
Inthisexample,youmightbetemptedtoaddmemberdatatothevalidatorsinceyouwanttotrackwhethertheinputhasbeenmodifiedyet.Doingsoisverymuchananti-pattern!TheFormControlobjectshouldactasyoursolesourceofstatefulinformationinthisscenario.AFormControlobjectisinstantiatedforeachinputfield,andthereforeitistheideal"datastore"withwhichyoucantrackwhattheinputisdoing.
Validatorexecution
Ifyouweretoinspectwhenthevalidatormethodisbeingcalled,youwouldfindthatitexecutesonlyonakeypressinsidetextarea.Thismayseemarbitrary,butthedefaultFormControl/inputassignmentistoevaluatethevalidatorsofFormControlonachangeeventemittedfromtheinput.FormControlobjectsexposearegisterOnChangemethod,whichletsyouhookontothesamepointthatthevalidatorswillbeevaluated.
SeealsoCreatingandusingacustomvalidatordemonstrateshowtocreateacustomdirectivethatbehavesasinputvalidation
Chapter4.MasteringPromisesThischapterwillcoverthefollowingrecipes:
UnderstandingandimplementingbasicPromisesChainingPromisesandPromisehandlersCreatingPromisewrapperswithPromise.resolve()andPromise.reject()ImplementingPromisebarrierswithPromise.all()CancelingasynchronousactionswithPromise.race()ConvertingaPromiseintoanObservableConvertinganHTTPserviceObservableintoZoneAwarePromise
IntroductionInAngular1,promisesactedasstrangebirds.Theywereessentialforbuildingrobustasynchronousapplications,butusingthemseemedtocomeataprice.Theirimplementationbywayofthe$qserviceandthedualityofpromiseanddeferredobjectsseemedbizarre.Nonetheless,onceyouwereabletomasterthem,itwaseasytoseehowtheycouldbethefoundationofextremelyrobustimplementationsinthesingle-threadedevent-drivenworldofJavaScript.
Fortunately,fordeveloperseverywhere,ES6formallyembracesthePromisefeatureasacentralcomponent.SinceTypeScriptisasupersetofES6,youwillbepleasedtoknowthatyoucanwieldpromiseseverywhereinAngularwithoutextrabaggage.AlthoughObservablessubsumealotoftheutilityofferedbypromises,thereisstillverymuchaplacefortheminyourtoolkit.
Tip
BeingabletousePromisesnativelyisaprivilegeofTypeScripttoaJavaScripttranspilation.Asofnow,somebrowserssupportPromisesnatively,whilesomedonot.Goodnewsisthatifyou'rewritingyourapplicationsinTypeScriptandaretranspilingthemproperly,youdon'thavetoworryaboutthis!Really,theonlytimeyouwouldneedtoconsidertheactualtranspilationmechanicsiswhenyouneedinformationrelatedtotheperformanceorpayloadsizebenefitsofnativeimplementationsversustheirrespectivepolyfills,andthisshouldneverbeanissuefornearlyallapplications.
UnderstandingandimplementingbasicPromisesPromisesareveryusefulinmanyofthecoreaspectsofAngular.Althoughtheyarenolongerboundtothecoreframeworkservice,theystillmanifestthemselvesthroughoutAngular'sAPIs.TheimplementationisconsiderablysimplerthanAngular1,butthemainrhythmshaveremainedconsistent.
Note
Youcanrefertothecode,links,andaliveexampleofthisathttp://ngcookbook.herokuapp.com/5195.
GettingreadyBeforeyoustartusingpromises,youshouldfirstunderstandtheproblemtheyaretryingtosolve.Withoutworryingtoomuchabouttheinternals,youcanclassifytheconceptofaPromiseintothreedistinctstages:
Initialization:IhaveapieceofworkthatIwanttoaccomplish,andIwanttodefinewhatshouldhappenwhenthisworkiscompleted.Idonotknowwhetherthisworkwillbeevercompleted;also,theworkmayeitherfailorsucceed.Pending:Ihavestartedthework,butithasnotbeencompletedyet.Completed:Theworkisfinished,andthepromiseassumesafinalstate.The"completed"stateassumestwoforms:resolvedandrejected.Thesecorrespondtosuccessandfailure,respectively.
Thereismorenuancetohowpromiseswork,butfornow,thisissufficienttogetintosomeofthecode.
Howtodoit...Apromiseimplementationinoneofitssimplestformsisasfollows:
//promisesareinstantiatedwiththe'new'keyword
varpromise=newPromise(()=>{});
ThefunctionpassedtothePromiseconstructoristhepieceofworkthatisexpectedtoexecuteasynchronously.Theformaltermforthisfunctionisexecutor.
Note
ThePromiseconstructordoesn'tcareatallabouthowtheexecutorfunctionbehaves.Itmerelyprovidesitwiththetworesolveandrejectfunctions.Itisleftuptotheexecutorfunctiontoutilizethemappropriately.Notethattheexecutorfunctiondoesn'tneedtobeasynchronousatall;however,ifitisn'tasynchronous,thenyoumightnotneedaPromiseforwhatyouaretryingtoaccomplish.
Whenthisfunctionisexecuted,internallyitunderstandswhenitiscompleted;however,ontheoutside,thereisnoconstructthatrepresentstheconceptof"runthiswhentheexecutorfunctioniscompleted".Therefore,itsfirsttwoparametersaretheresolveandrejectfunctions.Thepromisewrappingtheexecutorfunctionisinthependingstateuntiloneoftheseisinvoked.Onceinvoked,thepromiseirreversiblyassumestherespectivestate.
Note
Theexecutorfunctionisinvokedimmediatelywhenthepromiseisinstantiated.Justasimportantly,itisinvokedbeforethepromiseinstantiationisreturned.Thismeansthatifthepromisereacheseitherafulfilledorrejectedstateinsidetheexecutorsynchronously,thenthereturnvalueofnewPromise(...)willbethefreshlyconstructedPromisewitharesolvedorrejectedstatus,skippingthependingstateentirely.
Thereturnvalueofexecutorisunimportant.Nomatterwhatitreturns,thepromiseconstructorwillalwaysreturnthefreshlycreatedpromise.
Thefollowingcodedemonstratesfivedifferentexamplesofwaysthatapromisecanbeinstantiated,resolved,orrejected:
//Thisexecutorispassedresolveandreject,butis
//effectivelyano-op,sothepromisep2willforever
//remaininthe'pending'state.
constp1=newPromise((resolve,reject)=>{});
//Thisexecutorinvokes'resolve'immediately,so
//p2willtransitiondirectlytothe'fulfilled'state.
constp2=newPromise((resolve,reject)=>resolve());
//Thisexecutorinvokes'reject'immediately,so
//p3willtransitiondirectlytothe'rejected'state.
//Atransitiontothe'rejected'statewillalsothrow
//anexception.Thisexceptionisthrownafterthe
//executorcompletes,soanylogicfollowingthe
//invocationofrejectwillstillbeexecuted.
constp3=newPromise((resolve,reject)=>{
reject();
//Thislog()printsbeforetheexceptionisthrown
console.log('Igotrejected!');
});
//Thisexecutorinvokes'resolve'immediately,so
//p4willtransitiondirectlytothe'fulfilled'state.
//Onceapromiseexitsthe'pending'state,itcannotchange
//again,soeventhoughrejectisinvokedafterwards,the
//finalstateofp4isstill'fulfilled'.
constp4=newPromise((resolve,reject)=>{
resolve();
reject();
});
//Thisexecutorassignsitsresolvefunctiontoavariable
//intheencompassinglexicalscopesoitcanbecalled
//outsidethepromisedefinition.
varouterResolve;
constp5=newPromise((resolve,reject)=>{
outerResolve=resolve();
});
//Stateofp5is'pending'
outerResolve();
//Stateofp5is'fulfilled'
Withwhatyou'vedonesofar,youwillnotfindthepromiseconstructtobeofmuchuse;thisisbecauseallthattheprecedingcodeaccomplishesisthesettingupofthestateofasinglepromise.Therealvalueemergeswhenyousetthesubsequentstatehandlers.APromiseobject'sAPIexposesathen()method,whichallowsyoutosethandlerstobeexecutedwhenthePromisereachesitsfinalstate:
//p1isasimplepromisetowhichyoucanattachhandlers
constp1=newPromise((resolve,reject)=>{});
//p1exposesathen()methodwhichacceptsa
//resolvehandler(onFulfilled),anda
//rejecthandler(onRejected)
p1.then(
//onFulfilledisinvokedwhenresolve()isinvoked
()=>{},
//onRejectedisinvokewhenreject()isinvoked
()=>{});
//Iflefthere,p1willforeverremain"pending"
//Usingthe'new'keywordstillallowsyoutocalla
//methodonthereturnedinstance,sodefiningthe
//then()handlersimmediatelyisallowed.
//
//Instantlyresolvesp2
constp2=newPromise((resolve,reject)=>resolve())
.then(
//Thismethodwillimmediatelybeinvokedfollowing
//thep2executorinvokingresolve()
()=>console.log('resolved!'));
//"resolved!"
//Instantlyrejectsp3
constp3=newPromise((resolve,reject)=>reject())
.then(
()=>console.log('resolved!'),
//Thissecondmethodwillimmediatelybeinvokedfollowing
//thep3executorinvokingreject()
()=>console.log('rejected!'));
//"rejected!"
constp4=newPromise((resolve,reject)=>reject())
//Ifyoudon'trequireuseoftheresolvehandler,
//catch()allowsyoutodefinejusttheerrorhandling
.catch(()=>console.log('rejected!'));
//executorparameterscanbecapturedoutsideitslexical
//scopeforlaterinvocation
varouterResolve;
constp5=newPromise((resolve,reject)=>{
outerResolve=resolve;
}).then(()=>console.log('resolved!'));
outerResolve();
//"resolved!"
Howitworks...PromisesinJavaScriptconfertothedevelopertheabilitytowriteasynchronouscodeinparallelwithsynchronouscodemoreeasily.InJavaScript,thiswasformerlysolvedwithnestedcallbacks,colloquiallyreferredtoas"callbackhell."Asinglecallback-orientedfunctionmightbewrittenasfollows:
//agenericasynchronouscallbackfunction
functionasyncFunction(data,successCallback,errorCallback){
//asyncFunctionwillperformsomeoperationthatmaysucceed,
//mayfail,ormaynotreturnatall,anyofwhich
//occursinanunknownamountoftime
//thispseudo-responsecontainsasuccessboolean,
//andthereturneddataifsuccessful
asyncOperation(data,function(response){
if(response.success===true){
successCallback(response.data);
}else{
errorCallback();
}
});
};
Ifyourapplicationdoesnotdemandanysemblanceofin-orderorcollectivecompletion,thenthefollowingwillsuffice:
functionsuccessCallback(data){
//asyncFunctionsucceeded,handledataappropriately
};
functionerrorCallback(){
//asyncFunctionfailed,handleappropriately
};
asyncFunction(data1,successCallback,errorCallback);
asyncFunction(data2,successCallback,errorCallback);
asyncFunction(data3,successCallback,errorCallback);
Thisisalmostneverthecasethough.Often,yourapplicationwilleitherdemandthatthisdataisacquiredinasequence,orthatanoperationthatrequiresmultipleasynchronouslyacquiredpiecesofdataexecutesonceallofthedatahasbeensuccessfullyacquired.Inthiscase,withoutaccesstopromises,thecallbackhellemerges:
asyncFunction(data1,(foo)=>{
asyncFunction(data2,(bar)=>{
asyncFunction(data3,(baz)=>{
//foo,bar,bazcannowallbeusedtogether
combinatoricFunction(foo,bar,baz);
},errorCallback);
},errorCallback);
},errorCallback);
Thisso-calledcallbackhellhereisreallyjustanattempttoserializethreeasynchronouscalls,buttheparametrictopologyoftheseasynchronousfunctionsforcesthedevelopertosubjecttheirapplicationtothisugliness.
There'smore...Animportantpointtorememberaboutpromisesisthattheyallowyoutobreakapartacalculationintotwoparts:thepartthatunderstandswhenthepromise's"execution"hasbeencompletedandthepartthatsignalstotherestoftheprogramthattheexecutionhasbeencompleted.
DecoupledandduplicatedPromisecontrol
BecauseapromisecangiveawaythecontrolofwhodecideswherethePromisewillbemadeready,multipleforeignpartsofthecodecansetthestateofthepromise.
Apromiseinstancecanbeeitherresolvedorrejectedatmultipleplacesinsidetheexecutor::
constp=newPromise((resolve,reject)=>{
//thefollowingarepseudo-methods,eachofwhichcanbecalled
//independentlyandasynchronously,ornotatall
functioncanHappenFirst(){resolve();};
functionmayHappenFirst(){resolve();}
functionmightHappenFirst(){reject();};
});
Apromiseinstancecanalsoberesolvedatmultipleplacesoutsidetheexecutor:
varouterResolve;
constp=newPromise((resolve,reject)=>{
outerResolve=resolve;
});
//thefollowingarepseudo-methods,eachofwhichcanbecalled
//independentlyandasynchronously,ornotatall
functioncanHappenFirst(){outerResolve();};
functionmayHappenFirst(){outerResolve();}
functionmightHappenFirst(){outerResolve();};
Note
OnceaPromise'sstatebecomesfulfilledorrejected,attemptstorejectorresolvethatpromisefurtherwillbesilentlyignored.Apromisestatetransitionoccursonlyonce,anditcannotbealteredorreversed.
ResolvingaPromisetoavalue
Partofthecentralconceptofpromiseconstructsisthattheyareableto"promise"thattherewillbeavalueavailablewhenthepromiseisresolved.
Statesdonotnecessarilyhaveadatavalueassociatedwiththem;theyonlyconfertothepromiseadefinedstateofevaluation:
varresolveHandler=()=>{},
rejectHandler=()=>{};
constp0=newPromise((resolve,reject)=>{
//statecanbedefinedwithanyofthefollowing:
//resolve();
//reject();
//resolve(myData);
//reject(myData);
}).then(resolveHandler,rejectHandler);
Anevaluatedpromise(resolvedorrejected)isassociatedwithahandlerforeachofthestates.Thishandlerisinvokeduponthepromise'stransitionintothatrespectivestate.Thesehandlerscanaccessthedatareturnedbytheresolutionorrejection:
constp1=newPromise((resolve,reject)=>{
//console.infoistheresolvehandler,
//console.erroristherejecthandler
resolve(123);
}).then(console.info,console.error);
//(info)123
//resettodemonstratereject()
constp2=newPromise((resolve,reject)=>{
//console.infoistheresolvehandler,
//console.erroristherejecthandler
reject(456);
}).then(console.info,console.error);
//(error)456
Delayedhandlerdefinition
Unlikecallbacks,handlerscanbedefinedatanypointinthepromiselifecycle,includingafterthepromisestatehasbeendefined:
constp3=newPromise((resolve,reject)=>{
//immediatelyresolvethepromise
resolve(123);
});
//subsequentlydefineahandler,willbeimmediately
//invokedsincepromiseisalreadyresolved
p3.then(console.info);
//(info)123
Multiplehandlerdefinition
Similartohowasingledeferredobjectcanberesolvedorrejectedatmultipleplacesintheapplication,asinglepromisecanhavemultiplehandlersthatcanbeboundtoasinglestate.For
example,asinglepromisewithmultipleresolvedhandlersattachedtoitwillinvokeallthehandlersiftheresolvedstateisreached;thesameistrueforrejectedhandlers:
constp4=newPromise((resolve,reject)=>{
//Invokeresolve()after1second
setTimeout(()=>resolve(),1000);
});
constcb=()=>console.log('called');
p4.then(cb);
p4.then(cb);
//After1second:
//"called"
//"called"
PrivatePromisemembers
AnextremelyimportantdeparturefromAngular1isthatthestateofapromiseistotallyopaquetotheexecution.Formerly,youwereabletoteaseoutthestateofthepromiseusingthepseudo-private$$stateproperty.WiththeformalES6Promiseimplementation,thestatecannotbeinspectedbyyourapplication.Youcan,however,gleanthestateofapromisefromtheconsole.Forexample,inspectingapromiseinGoogleChromeyieldssomethinglikethefollowing:
Promise{
[[PromiseStatus]]:"fulfilled",
[[PromiseValue]]:123
}
Note
PromiseStatusandPromiseValueareprivateSymbols,whichareanewconstructinES6.Symbolcanbethoughtofasauniquekeythatisusefulforsettingpropertiesonobjectsthatshouldn'tbeeasilyaccessedfromelsewhere.Forexample,ifapromiseweretousethe'PromiseStatus'stringtokeyaproperty,itcouldbeeasilyusedoutsidetheobject,evenifthepropertywassupposedtoremainprivate.WithES6privatesymbols,however,asymbolisuniquewhengenerated,andthereisnogoodwaytoaccessitinsidetheinstance.
SeealsoChainingPromisesandPromisehandlersdetailshowyoucanwieldthispowerfulchainingconstructtoserializeasynchronousoperationsCreatingPromisewrapperswithPromise.resolve()andPromise.reject()demonstrateshowtousethecorePromiseutilities
ChainingPromisesandPromisehandlersMuchofthepurposeofpromisesistoallowthedevelopertoserializeandreasonaboutindependentasynchronousactions.ThiscanbeaccomplishedbyutilizingthePromisechainingfeature.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/6828/.
Howtodoit...Thepromisehandlerdefinitionmethodthen()returnsanotherpromise,whichcanhavefurtherhandlersdefineduponit—inahandlercalledchain:
varsuccessHandler=()=>{console.log('called');};
varp=newPromise((resolve,reject)=>{resolve();})
.then(successHandler)
.then(successHandler)
.then(successHandler);
//called
//called
//called
Chainedhandlers'datahandoff
Chainedhandlerscanpassdatatotheirsubsequenthandlersinthefollowingmanner:
varsuccessHandler=(val)=>{
console.log(val);
returnval+1;
};
varp=newPromise((resolve,reject)=>{resolve(0);})
.then(successHandler)
.then(successHandler)
.then(successHandler);
//0
//1
//2
Rejectingachainedhandler
Returningnormallyfromapromisehandler(nottheexecutor)will,bydefault,signalchildpromisestatestobecomeresolved.However,ifeithertheexecutororthesubsequenthandlersthrowanuncaughtexception,theywill,bydefault,reject;thiswillservetocatchtheexception:
constp=newPromise((resolve,reject)=>{
//executorwillimmediatelythrowanexception,forcing
//areject
throw123;
})
.then(
//childpromiseresolvedhandler
data=>console.log('resolved',data),
//childpromiserejectedhandler
data=>console.log('rejected',data));
//"rejected",123
Note
Notethattheexception,hereanumberprimitive,isthedatathatispassedtotherejectionhandler.
Howitworks...APromisereachingafinalstatewilltriggerchildpromisestofollowitinturn.Thissimplebutpowerfulconceptallowsyoutobuildbroadandfault-tolerantpromisestructuresthatelegantlymeshcollectionsofdependentasynchronousactions.
There'smore...Thetopologyofpromiseslendsitselftosomeinterestingutilizationpatterns.
Promisehandlertrees
Promisehandlerswillexecuteintheorderthatthepromisesaredefined.Ifapromisehasmultiplehandlersattachedtoasinglestate,thenthatstatewillexecuteallitshandlersbeforeresolvingthefollowingchainedpromise:
constincr=val=>{
console.log(val);
return++val;
};
varouterResolve;
constfirstPromise=newPromise((resolve,reject)=>{
outerResolve=resolve;
});
//definefirstPromise'shandler
firstPromise.then(incr);
//appendanotherhandlerforfirstPromise,andcollect
//thereturnedpromiseinsecondPromise
constsecondPromise=firstPromise.then(incr);
//appendanotherhandlerforthesecondpromise,andcollect
//thereturnedpromiseinthirdPromise
constthirdPromise=secondPromise.then(incr);
//atthispoint,invokingouterResolve()will:
//resolvefirstPromise;firstPromise'shandlersexecutes
//resolvesecondPromise;secondPromises'shandlerexecutes
//resolvethirdPromise;nohandlersdefinedyet
//additionalpromisehandlerdefinitionorderis
//unimportant;theywillberesolvedasthepromises
//sequentiallyhavetheirstatesdefined
secondPromise.then(incr);
firstPromise.then(incr);
thirdPromise.then(incr);
//thesetupcurrentlydefinedisasfollows:
//firstPromise->secondPromise->thirdPromise
//incr()incr()incr()
//incr()incr()
//incr()
outerResolve(0);
//0
//0
//0
//1
//1
//2
Note
Sincethereturnvalueofahandlerdecideswhetherornotthepromisestateisresolvedorrejected,anyofthehandlersassociatedwithapromiseisabletosetthestate—which,asyoumayrecall,canonlybesetonce.Thedefiningoftheparentpromisestatewilltriggerthechildpromisehandlerstobeexecuted.
Itshouldnowbeapparenthowthetreesofthepromisefunctionalitycanbederivedfromthecombinationofpromisechainingandhandlerchaining.Whenusedproperly,theycanyieldextremelyelegantsolutionsfordifficultanduglyasynchronousactionserializations.
catch()
Thecatch()methodisashorthandforpromise.then(null,errorCallback).Usingitcanleadtoslightlycleanerpromisedefinitions,butitisnothingmorethansyntacticalsugar:
varouterReject;
constp=newPromise((resolve,reject)=>{
outerReject=reject;
})
.catch(()=>console.log('rejected!'));
outerReject();
//"rejected"
Tip
Itisalsopossibletochainp.then().catch().Anerrorthrownbytheoriginalpromisewillpropagatethroughthepromisecreatedbythen(),causeittoreject,andreachthepromisecreatedbycatch().Itcreatesoneextralevelofpromiseindirection,buttoanoutsideobserver,itwillbehavethesame.
SeealsoUnderstandingandimplementingbasicPromisesgivesanextensiverundownofhowandwhytousePromisesCreatingPromisewrapperswithPromise.resolve()andPromise.reject()demonstrateshowtousethecorePromiseutilities
CreatingPromisewrapperswithPromise.resolve()andPromise.reject()Itisusefultohavetheabilitytocreatepromiseobjectsthathavealreadyreachedafinalstatewithadefinedvalue,andalsotobeabletonormalizeJavaScriptobjectsintopromises.Promise.resolve()andPromise.reject()affordyoutheabilitytoperformboththeseactions.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/9315/.
Howtodoit...LikeallotherstaticPromisemethods,Promise.resolve()andPromise.reject()returnapromiseobject.Inthiscase,thereisnoexecutordefinition.
Ifoneofthesemethodsisprovidedwithanon-promiseargument,thereturnedpromisewillassumeeitherafulfilledorrejectedstate(correspondingtotheinvokedmethod).ThismethodwillpasstheargumenttoPromise.resolve()andPromise.reject(),alongwithanycorrespondinghandlers:
Promise.resolve('foo');
//Promise{[[PromiseStatus]]:"resolved",[[PromiseValue]]:"foo"}
Promise.reject('bar');
//Promise{[[PromiseStatus]]:"rejected",[[PromiseValue]]:"bar"}
//(error)Uncaught(inpromise)bar
Theprecedingcodeisbehaviorallyequivalenttothefollowing:
newPromise((resolve,reject)=>resolve('foo'));
//Promise{[[PromiseStatus]]:"resolved",[[PromiseValue]]:"foo"}
newPromise((resolve,reject)=>reject('bar'));
>>Promise{[[PromiseStatus]]:"rejected",[[PromiseValue]]:"bar"}
//(error)Uncaught(inpromise)bar
Promisenormalization
Promise.resolve()willuniquelyhandlescenarioswhereitispassedwithapromiseobjectasitsargument.Promise.resolve()willeffectivelyoperateasano-op,returningtheinitialpromiseargumentwithoutanymodification.Itwillnotmakeanattempttocoercetheargumentpromise'sstate:
consta=Promise.resolve('baz');
console.log(a);
//Promise{status:'resolved',value:'baz'}
constb=Promise.resolve(a);
console.log(b);
//Promise{status:'resolved',value:'baz'}
console.log(a===b);
//true
constc=Promise.reject('qux');
//Errorqux
console.log(c)
//Promise{status:'rejected',value:'qux'}
constd=Promise.resolve(c);
console.log(d);
//Promise{status:'rejected',value:'qux'}
console.log(c===d);
//true
Howitworks...WhenthinkingaboutPromisesinthecontextofthem"promising"toeventuallyassumeavalue,thesemethodsaresimplyamelioratinganylatentperiodseparatingthependingandfinalstates.
Thedichotomyisverysimple:
Promise.reject()willreturnarejectedpromisenomatterwhatitsargumentis.Evenifitisapromiseobject,thevalueofthereturnedpromisewillbethatofthepromiseobject.Promise.resolve()willreturnafulfilledpromisewiththewrappedvalueifthatvalueisnotapromise.Ifitisapromise,itbehavesasano-op.
There'smore...Importantly,thebehaviorofPromise.resolve()isnearlythesameashow$q.when()operatedinAngular1.$q.when()wasabletonormalizepromiseobjects,butitwouldalwaysreturnanewlycreatedpromiseobject:
//Angular1
consta=$q(()=>{});
console.log(a);
//Promise{...}
constb=$q.when(a);
console.log(b);
//Promise{...}
console.log(a===b);
//false
SeealsoUnderstandingandimplementingbasicPromisesgivesanextensiverundownofhowandwhytousePromisesImplementingPromisebarrierswithPromise.all()showyouhowPromisescanbecomposableCancelingasynchronousactionswithPromise.race()guidesyouthroughtheprocessofimplementingazero-failure-tolerantPromisesystem
ImplementingPromisebarrierswithPromise.all()Youmayfindyourapplicationrequirestheuseofpromisesinanall-or-nothingtypeofsituation.Thatis,itwillneedtocollectivelyevaluateagroupofpromises,andthiscollectionwillresolveasasinglepromiseifandonlyifallofthecontainedpromisesareresolved;ifanyoneofthemisrejected,theaggregatepromisewillberejected.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/8496/.
Howtodoit...ThePromise.all()methodacceptsaniterablecollectionofpromises(forexample,anarrayofPromiseobjectsoranobjectwithanumberofpromiseproperties),anditwillattempttoresolveallofthemasasingleaggregatepromise.Theparameteroftheaggregateresolvedhandlerwillbeanarrayorobjectthatmatchestheresolvedvaluesofthecontainedpromises:
varouterResolveA,outerResolveB;
constpromiseA=newPromise((resolve,reject)=>{
outerResolveA=resolve;
});
constpromiseB=newPromise((resolve,reject)=>{
outerResolveB=resolve;
});
constmultiPromiseAB=Promise.all([promiseA,promiseB])
.then((values)=>console.log(values));
outerResolveA(123);
outerResolveB(456);
//[123,456]
Ifanyofthepromisesinthecollectionarerejected,theaggregatepromisewillberejected.Theparameteroftheaggregaterejectedhandlerwillbethereturnedvalueoftherejectedpromise:
varouterResolveC,outerRejectD;
constpromiseC=newPromise((resolve,reject)=>{
outerResolveC=resolve;
});
constpromiseD=newPromise((resolve,reject)=>{
outerRejectD=reject;
});
constmultiPromiseCD=Promise.all([promiseC,promiseD])
.then(
values=>console.log(values),
rejectedValue=>console.error(rejectedValue));
//resolveacollectionpromise,nohandlerexecution
outerResolveC(123);
//rejectacollectionpromise,rejectionhandlerexecutes
outerRejectD(456);
//(error)456
Howitworks...Asdemonstrated,theaggregatepromisewillreachthefinalstateonlywhenalloftheenclosedpromisesareresolved,orwhenasingleenclosedpromiseisrejected.Usingthistypeofpromiseisusefulwhenthecollectionofpromisesdonotneedtoreasonaboutoneanother,butcollectivecompletionistheonlymetricofsuccessforthegroup.
Inthecaseofacontainedrejection,theaggregatepromisewillnotwaitfortheremainingpromisestocomplete,butthosepromiseswillnotbepreventedfromreachingtheirfinalstate.Onlythefirstpromisetoberejectedwillbeabletopassrejectiondatatotheaggregatepromiserejectionhandler.
There'smore...Promise.all()isinmanywaysextremelysimilartoanoperating-system-levelprocesssynchronizationbarrier.Aprocessbarrierisacommonpointinthethreadinstructionexecutionthatacollectionofprocesseswillreachindependentlyandatdifferenttimes,andnoprocesscanproceedfurtheruntilallhavereachedthispoint.Inthesameway,Promise.all()willnotproceedunlesseitherallofthecontainedpromiseshavebeenresolved—reachedthebarrier—orasinglecontainedrejectionwillpreventthatstatefromeverbeingachieved,inwhichcasethefailoverhandlerlogicwilltakeover.
SincePromise.all()allowsyoutohavearecombinationofpromises,italsoallowsyourapplication'sPromisechainstobecomeadirectedacyclicgraph(DAG).Thefollowingisanexampleofapromiseprogressiongraphthatdivergesfirstandconvergeslater:
Thislevelofcomplexityisuncommon,butitisavailableforuseshouldyourapplicationrequireit.
SeealsoCreatingPromiseWrapperswithPromise.resolve()andPromise.reject()demonstrateshowtousethecorePromiseutilitiesCancelingasynchronousactionswithPromise.race()guidesyouthroughtheimplementationofazero-failure-tolerantPromisesystem
CancelingasynchronousactionswithPromise.race()ES6introducesPromise.race(),whichisabsentfromthe$qspecinAngular1.LikePromise.all(),thisstaticmethodacceptsaniterablecollectionofpromiseobjects;whicheveroneresolvesorrejectsfirstwillbecometheresultofthepromisewrappingthecollection.Thismayseemlikeunusualbehavior,butitbecomesquiteusefulwhenyou'rebuildingacancellationbehaviorintothesystem.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/4362/.
GettingreadySupposeyoustartedwithasimplepromisethatresolvestoavalueafter3seconds:
constdelayedPromise=newPromise((resolve,reject)=>
setTimeout(resolve.bind(null,'foobar'),3000))
.then(val=>console.log(val));
Youwouldliketohavetheabilitytodetachapartofyourapplicationfromwaitingforthispromise.
Howtodoit...Asimplesolutionwouldbetoexposethepromise'srejecthandlerandjustinvokeitfromwhateveristoperformthecancelation.However,itispreferabletostopwaitingforthispromiseinsteadofdestroyingit.
Note
AconcreteexampleofthiswouldbeaslowbutcriticalHTTPrequestthatyourapplicationmakes.YoumightnotwanttheUItowaitforittocomplete,butyoumayhaveresolvehandlersattachedtotherequestthatyoustillwanttohandletheresult,onceitisreturned.
Instead,youcantakeadvantageofPromise.race()andintroduceacancellationpromisealongsidetheoriginalone:
//Usethismethodtocapturethecancellationfunction
varcancel;
constcancelPromise=newPromise((resolve,reject)=>{
cancel=reject;
});
constdelayedPromise=newPromise((resolve,reject)=>
setTimeout(resolve.bind(null,'foobar'),3000));
//Promise.race()createsanewpromise
Promise.race([cancelPromise,delayedPromise])
.then(
val=>console.log(val),
()=>console.error('cancelled!'));
//Ifyouinvokecancel()before3secondselapses
//(error)"cancelled!"
//Instead,if3secondselapses
//"foobar"
Now,ifdelayedPromiseresolvesfirst,thepromisecreatedbyPromise.race()willlogthevaluepassedtoithere,foobar.If,however,youinvokecancel()beforeithappens,thenthatsamePromisewillprintacancelled!error.
Howitworks...Promise.race()justwaitsforanyofitsinnerpromisestoarriveatthefinalstate.Itcreatesandreturnsanewpromisethatisbeholdentothestateofthecontainedpromises.Whenitobservesthatanyofthemtransitionstothefinalstate,thenewpromisealsoassumesthisstate.
Note
Inthisexample,theexecutorofcancelPromiseanddelayedPromiseareinvokedbeforePromise.race()iscalled.Sincepromisesonlycareaboutthestateofotherpromises,itisn'timportantthatthepromisespassedtoPromise.race()needtobealreadytechnicallystarted.
NotethattheuseofPromise.race()doesn'taffecttheimplementationofdelayedPromise.Evenwhencancel()isinvoked,delayedPromisewillstillberesolvedanditshandlerswillstillbeexecutednormally,unawarethatthesurroundingPromise.race()hasalreadybeenrejected.YoucanprovethistoyourselfbyaddingaresolvehandlertodelayedPromise,invokingcancel()andseeingtheresolvehandlerofdelayedPromisebeingexecutedanyway:
varcancel;
constcancelPromise=newPromise((resolve,reject)=>{
cancel=reject;
});
constdelayedPromise=newPromise((resolve,reject)=>
setTimeout(resolve.bind(null,'foobar'),3000))
.then(()=>console.log('stillresolved!'));
Promise.race([cancelPromise,delayedPromise])
.then(
val=>console.log(val),
()=>console.error('cancelled!'));
cancel();
//(error)cancelled!
//After3secondselapses
//"stillresolved!"
SeealsoCreatingPromisewrapperswithPromise.resolve()andPromise.reject()demonstrateshowtousethecorePromiseutilitiesImplementingPromisebarrierswithPromise.all()showyouhowPromisescanbecomposable
ConvertingaPromiseintoanObservableObservablesandPromisesservedifferentpurposesandaregoodatdifferentthings,butinaspecificpartofanapplication,youwillalmostcertainlywanttobedealingwithasingledenomination.Thismeansconvertingobservablesintopromisesandviceversa.ThankstoRxJS,thisisquitesimple.
Tip
FormoreonRxJSObservables,refertoChapter5,ReactiveXObservables,whichcoversthemindepth.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/5244/.
Howtodoit...ThereisagooddealofparitybetweenPromiseandObservable.Therearediscretesuccessanderrorcases,andtheconceptofsuccessfulcompletiononlycorrespondstothesuccesscase.
RxJSobservablesexposeafromPromisemethod,whichwrapsPromiseasanObservable:
import{Observable}from'rxjs/Rx';
varouterResolve,outerReject;
constp1=newPromise((resolve,reject)=>{
outerResolve=resolve;
outerReject=reject;
});
varo1=Observable.fromPromise(p1);
NowthatyouhaveanObservableinstance,youcanutilizeitssubscribe()events,whichcorrespondtothestateofthePromiseinstance:
import{Observable}from'rxjs/Rx';
varouterResolve,outerReject;
constp1=newPromise((resolve,reject)=>{
outerResolve=resolve;
outerReject=reject;
});
varo1=Observable.fromPromise(p1);
o1.subscribe(
//onNexthandler
()=>console.log('resolved!'),
//onErrorhandler
()=>console.log('rejected'),
//onCompletedhandler
()=>console.log('finished!'));
outerResolve();
//"resolved!"
//"finished!"
Howitworks...ThenewObservableinstancedoesn'treplacethepromise.ItjustattachesitselftothePromise'sresolvedandrejectedstates.Whenthishappens,itemitseventsandinvokestherespectivecallbacks.TheObservableinstanceisboundtothestateofthePromise,butPromiseisnotawarethatanythinghasbeenattachedtoitsinceitblindlyexposesitsresolveandrejecthooks.
Tip
NotethatonlyaresolvedPromisewillinvoketheonCompletedhandler;rejectingthepromisewillnotinvokeit.
There'smore...ObservablesandPromisesareinterchangeableifyouaresoinclined,butdoconsiderthattheyarebothappropriateindifferentsituations.
Observablesaregoodatstream-typeoperations,wherethelengthofthestreamisindeterminate.ItiscertainlypossibletohaveanObservablethatonlyeveremitsoneevent,butanObservablewillnotbroadcastthisstatetolistenersthatareattachedlater,unlessyouconfigureittodoso(suchasBehaviorObservable).
Promisesaregoodatmaskingasynchronousbehavior.TheyallowyoutowritecodeandsethandlersuponthePromiseasifthepromisedstateorvaluewasrealizedatthetimeofexecution.Ofcourseit'snot,buttheabilitytodefineahandlersynchronouslyatruntimeandhavethePromiseinstancedecidewhenit'sappropriatetoexecuteit,aswellastheabilitytochainthesehandlers,isextremelyvaluable.
SeealsoUnderstandingandimplementingbasicPromisesgivesanextensiverundownofhowandwhytousePromisesConvertinganHTTPserviceObservableintoZoneAwarePromisegivesyouanAngular-centricviewofhowPromises,Observables,andZonesintegrateinsideanAngularapplication
ConvertinganHTTPserviceObservableintoaZoneAwarePromiseInAngular2,theRxJSasynchronousobservablesarefirst-classcitizensandmuchofthecoretoolkithasbeenconfiguredtorelyuponthem.Nonetheless,itisstillvaluabletobeabletohaveconversionbetweenthem,especiallysincetheyhavesimilarduties.
Tip
FormoreonRxJSObservables,refertoChapter5,ReactiveXObservables,whichcoversthemindepth.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/0905/.
GettingreadyYou'llbeginwiththefollowingsimplisticapplication:
[app/article.component.ts]
import{Component}from'@angular/core';
import{Http}from'@angular/http';
@Component({
selector:'article',
template:`
<p></p>
`
})
exportclassArticleComponent{
constructor(privatehttp:Http){
//Fordemopurposes,havethisplunkrequestitselfto
//avoidcrossoriginerrors
console.log(
http.get('//run.plnkr.co/plunks/TBtcNDRelAOHDVpIuWw1'));
}
}
//Observable{...}
SupposeyourgoalwastoconvertthisHTTPcalltousepromisesinstead.
Howtodoit...Forthepurposesofthisrecipe,youdon'treallyneedtounderstandanydetailsaboutthehttpserviceorRxJSasynchronousobservables.Allthatyouneedtoknowisthatanymethodexposedbythehttpservicewillreturnanobservable.Happily,theRxJSimplementationcanexposea.toPromise()methodthatconvertstheobservableintoitsequivalentPromiseobject:
[app/article.component.ts]
import{Component}from'@angular/core';
import{Http}from'@angular/http';
import'rxjs/Rx';
@Component({
selector:'article',
template:`
<p></p>
`
})
exportclassArticleComponent{
constructor(privatehttp:Http){
//Fordemopurposes,havethisplunkrequestitselfto
//avoidcrossoriginerrors
console.log(
http.get('//run.plnkr.co/plunks/TBtcNDRelAOHDVpIuWw1')
.toPromise());
}
}
//ZoneAwarePromise{...}
Howitworks...TheHTTPservice,bydefault,returnsanobservable;however,withouttheimportedRxmodule,thiswillthrowanerror,sayingitcannotfindthetoPromise()method.
TheRxmoduleconferstoanobservabletheabilitytoconvertitselfintoapromiseobject.Angular2isintentionallynotutilizingtheObservablespecwithalltheRxJSoperatorstoallowyoutospecifyexactlywhichonesyouwant.Becausetheoperatorsexistasseparatemodules,thisleadstoasmallerpayloadsenttothebrowser.
Oncethe.toPromise()methodisinvoked,theobjectiscreatedtoaZoneAwarePromiseinstance.
Note
Thissoundsgnarly,butreally,it'sjustwrappingthePromiseimplementationaszone.jssothattheAngularzoneisawareofanyactionsthePromisecouldcausethatitshouldbeawareof.Foryourpurposes,thiscanbetreatedasaregularPromise.
SeealsoUnderstandingandimplementingbasicPromisesgivesanextensiverundownofhowandwhytousePromisesConvertingaPromiseintoanObservablegivesyouanexampleofhowRxJScanbeusedtoconvertbetweenthesetwopowerfultypes
Chapter5.ReactiveXObservablesThischapterwillcoverthefollowingrecipes:
BasicutilizationofObservableswithHTTPImplementingaPublish-SubscribemodelusingSubjectsCreatinganObservableAuthenticationServiceusingBehaviorSubjectsBuildingageneralizedPublish-Subscribeservicetoreplace$broadcast,$emit,and$onUsingQueryListsandObservablestofollowthechangesinViewChildrenBuildingafullyfeaturedAutoCompletewithObservables
IntroductionBeforeyougetintothemeatofAngular2Observables,itisimportanttofirstunderstandtheproblemyouaretryingtosolve.
Afrequentlyencounteredscenarioinsoftwareiswhereyouareexpectingsomeentitytobroadcastthatsomethinghappened;let'scallthisan"event"(distinctfromabrowserevent).Youwouldliketohookintothisentityandattachbehaviortoitwheneveraneventoccurs.Youwouldalsoliketobeabletodetachfromthisentitywhenyounolongercareabouttheeventsitisbroadcasting.
ThereismorenuanceandadditionalcomplexitytoObservablesthatthischapterwillcover,butthisconceptofeventsunderscoresthefundamentalpatternthatisusefultoyouasthedeveloper.
TheObserverPatternTheObserverPatternisn'talibraryorframework.ItisjustasoftwaredesignpatternuponwhichReactiveXObservablesarebuilt.Manylanguagesandlibrariesimplementthispattern,andReactiveXisjustoneoftheseimplementations;however,ReactiveXistheonethatAngular2hasformallyincorporatedintoitself.
TheObserverPatterndescribestherelationshipbetweensubject,whichwasdescribedasthe"entity"earlier,anditsobservers.Thesubjectisawareofanyobserversthatarewatchingit.Whenaneventisemitted,thesubjectisabletopassthiseventtoeachobserverviamethodsthatareprovidedwhentheobserverbeginstosubscribeit.
ReactiveXandRxJSTheReactiveXlibraryisimplementedinnumerouslanguages,includingPython,Java,andRuby.RxJS,theJavaScriptimplementationoftheReactiveXlibrary,isthedependencythatAngular2utilizestoincorporateObservablesintonativeframeworkbehavior.SimilartoPromises,youcancreateastandaloneObservableinstancethroughdirectinstantiation,butmanyAngular2methodsandserviceswillalsoutilizeanObservableinterfacebydefault.
ObservablesinAngular2Angular2integratesObservablesinawidevarietyofways.Ifyouarenewtothem,youmayinitiallyfeeloddusingthem.However,itisimportantyourecognizethatObservablesprovideasuperiorsoftwaredevelopmentpattern.
AlongwiththebulkRxJSmodulerxjs/Rx,youarealsoprovidedwiththestrippeddownObservablemodulerxjs/Observable.Thisminimalmoduleallowsindividualpiecesofnon-essentialbehaviortobeimportedasrequiredinordertoreducemodulebloat.Forexample,whenusingthislightweightObservablemodule,usingoperatorsorothersuchReactiveXconventionsnecessitatesthatyouexplicitlyincorporatethesemodules,inordertoextendtheavailableObservableinterface.
ObservablesandPromisesBothObservablesandPromisesoffersolutionstoasynchronousconstructs,butObservablesaremorerobust,extensible,anduseful.AlthoughPromisesareavailablebydefaultintheES6specification,youwillquicklyrealizethattheybecomebrittlewhenyouattempttoapplythemoutsidetherealmofbasicapplicationbehavior.
TheReactiveXlibraryofferspowerfultoolingtowhichPromisescannotcompare.Observablesarecomposable,allowingyoutotransformandcombinethemintonewObservables.Theyalsoencapsulatetheconceptofacontinuousstreamofevents-aparadigmthatisencounteredinclient-sideprogrammingextremelyfrequentlyandthatPromisesdonottranslatewellto.
BasicutilizationofObservableswithHTTPInAngular2,theHttpmodulenowbydefaultutilizestheObservablepatterntowrapXMLHttpRequest.Fordevelopersthatarefamiliarwiththepattern,itreadilytranslatestotheasynchronousnatureofrequeststoremoteresources.Fordevelopersthatarenewertothepattern,learningtheinsandoutsofHttpObservablesisagoodwaytowrapyourheadaroundthisnewparadigm.
Note
Thecode,links,andaliveexamplerelatedtothisareavailableathttp://ngcookbook.herokuapp.com/4121.
GettingreadyForthepurposeofthisexample,you'lljustserveastaticJSONfiletotheapplication.HowevernotethatthiswouldbenodifferentifyouweresendingrequeststoadynamicAPIendpoint.
Beginbycreatingaskeletoncomponent,includingallthenecessarymodulesformakingHTTPrequests:
[app/article.component.ts]
import{Component}from'@angular/core';
import{Http}from'@angular/http';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>{{author}}</p>
`
})
exportclassArticleComponent{
title:string;
body:string;
constructor(privatehttp:Http){
}
}
Forthisexample,assumethereisaJSONfileinsidethestaticdirectorynamedarticle.json:
[article.json]
{
"title":"OrthopedicDoctorsAskCityforMoreSidewalkCracks",
"author":"JakeHsu"
}
Howtodoit...SinceyouhavealreadyinjectedtheHttpservice,youcanbeginbydefiningthegetrequest:
[app/article.component.ts]
import{Component}from'@angular/core';
import{Http}from'@angular/http';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>{{author}}</p>
`
})
exportclassArticleComponent{
title:string;
body:string;
constructor(privatehttp_:Http){
http_.get('static/article.json');
}
}
ThiscreatesanObservableinstance,butyoustillneedtoaddinstructionsonhowtohandletherawstringoftheresponse.
Note
Atthispoint,youwillnoticethatthisdoesnotactuallyfireabrowserGETrequest.Thisiscoveredinthisrecipe'sThere'smoresection.
SinceyouknowtherequestwillreturnJSON,youcanutilizethejson()methodthataResponsewouldexpose.Thiscanbedoneinsidethemap()method.However,theObservabledoesnotexposethemap()methodbydefault,soyoumustimportitfromtherxjsmodule:
[app/article.component.ts]
import{Component}from'@angular/core';
import{Http}from'@angular/http';
import'rxjs/add/operator/map';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>{{author}}</p>
`
})
exportclassArticleComponent{
title:string;
author:string;
constructor(privatehttp_:Http){
http_.get('static/article.json')
.map(response=>response.json());
}
}
Sofarsogood,butyou'restillnotdone.TheprecedingcodewillcreatetheObservableinstance,butyoustillhavetosubscribetoitinordertohandleanydataitwouldemit.Thiscanbeaccomplishedwiththesubscribe()method,whichallowsyoutoattachthecallbackanderrorhandlingmethodsofobserver:
[app/article.component.ts]
import{Component}from'@angular/core';
import{Http}from'@angular/http';
import'rxjs/add/operator/map';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>{{author}}</p>
`
})
exportclassArticleComponent{
title:string;
author:string;
constructor(privatehttp_:Http){
http_.get('static/article.json')
.map(response=>response.json())
.subscribe(
article=>{
this.title=article.title;
this.author=article.author;
},
error=>console.error(error));
}
}
Withallofthis,theGETrequestwillreturntheJSONfile,andtheresponsedatawillbeparsedanditsdatainterpolatedintotheDOMbythecomponent.
Howitworks...Theprevioussectiongaveagoodhigh-leveloverviewofwhatwashappening,butitisusefultobreakthingsdownmorecarefullytounderstandwhateachindividualstepaccomplishes.
Observable<Response>
TheHttpserviceclassexposesthemethodsget(),post(),put(),andsoon—alltheHTTPverbsthatyouwouldexpect.EachofthesewillreturnObservable<Response>,whichwillemitaResponseinstancewhentherequestisreturned:
console.log(http_.get('static/article.json'));
//Observable{...}
Note
Itsoundsobvious,butObservablesareobservedbyanobserver.TheobserverwillwaitforObservabletoemitobjects,whichinthisexampletakestheformofResponse.
TheRxJSmap()operator
TheResponseinstanceexposesajson()method,whichconvertsthereturnedserializedpayloadstringintoitscorrespondingin-memoryobjectrepresentation.Youwouldliketobeabletopassaregularobjecttotheobserverhandler,sotheidealtoolhereisawedgemethodthatstillgivesyouanObservableintheend:
console.log(http_.get('static/article.json')
.map(response=>response.json()));
//Observable{source:Observable,operator:MapOperator,...}
RecallthatthecanonicalformofObservablesisastreamofevents.Inthiscase,weknowtherewillonlyeverbeoneevent,whichistheHTTPresponse.Nonetheless,allthenormaloperatorsthatwouldbeusedonastreamofeventscanjustaseasilybeusedonthissingle-eventObservable.
InthesamewaythatArray.map()canbeusedtotransformeachinstanceinthearray,Observable.map()allowsyoutotransformeacheventemittedfromObservable.Morespecifically,itcreatesanotherObservablethatemitsthemodifiedeventpassedfromtheinitialobservable.
Subscribe
Observableinstancesexposeasubscribe()methodthatacceptsanonNexthandler,anonErrorhandler,andanonCompletedhandlerasarguments.ThesehandlerscorrespondtotheeventsinthelifecycleoftheObservablewhenitemitsResponseinstances.TheparameterfortheonNextmethodiswhateverisemittedfromtheObservable.Inthiscase,theemitteddatais
thereturnedvaluefrommap(),soitwillbetheparsedobjectthathasreturnedafterinvokingjson()ontheResponseinstance.
Allthesemethodsareoptional,butinthisexample,theonNextandonErrormethodsareuseful.
Note
Together,thesemethodswhenprovidedtosubscribe()constitutewhatisidentifiedastheobserver.
http_.get('static/article.json')
.map(respose=>respose.json())
.subscribe(
article=>{
this.title=article.title;
this.body=article.body;
},
error=>console.error(error));
Withallofthistogether,thebrowserwillfetchtheJSONandparseit,andthesubscriberwillpassitsdatatotherespectivecomponentmembers.
There'smore...Whenconstructingthisrecipepiecebypiece,ifyouarewatchingyourbrowser'snetworkrequestsasyouassembleit,youwillnoticethattheactualGETrequestisnotfireduntilthesubscribe()methodisinvoked.ThisisbecausethetypeObservableyouareusingis"cold".
HotandcoldObservables
The"cold"designationmeansthattheObservabledoesnotbegintoemituntilanobserverbeginstosubscribetoit.Thisisdifferentfroma"hot"Observable,whichwillemititemseveniftherearenoobserverssubscribedtoit.Sincethismeansthateventsthatoccurbeforeanobserverisattachedarelost,HTTPObservablesdemandacolddesignation.
TheonNextmethodistermed"emission"sincethereisassociateddatathatisbeingemitted.TheonCompletedandonErrormethodsaretermed"notifications,"astheyrepresentsomethingofsignificance,buttheydonothaveanassociatedeventthatwouldbeconsideredpartofthestream.
SeealsoImplementingaPublish-SubscribemodelusingSubjectsshowsyouhowtoconfigureinputandoutputforRxJSObservablesBuildingafullyfeaturedAutoCompletewithObservablesgivesyouabroadtourofsomeoftheutilitiesofferedtoyouaspartoftheRxJSlibrary
ImplementingaPublish-SubscribemodelusingSubjectsAngular2willoftenprovideyouwithanObservableinterfacetoattachtoforfree,butitisimportanttoknowhowtheyarecreated,configured,andused.Morespecifically,itisvaluableforyoutoknowhowtotakeObservablesandapplythemtorealscenariosthatwillbeencounteredintheclient.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/4839/.
GettingreadySupposeyoustartedwiththefollowingskeletonapplication:
[app/click-observer.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'click-observer',
template:`
<button>
Emitevent!
</button>
<p*ngFor="letclickofclicks;leti=index">
{{i}}:{{click}}
</p>
`
})
exportclassClickObserverComponent{
clicks:Array<Event>=[];
}
Yourgoalistoconvertthissothatallthebuttonclickeventsareloggedintotherepeatedfield.
Howtodoit...Accomplishingthiswithacomponentmembermethodandusingitintheclickeventbindinginthetemplateispossible,butthisdoesn'tcapturetherealvalueofObservables.YouwanttobeabletoexposeanObservableonClickObserverComponent.Thiswillallowanyotherpartofyourapplicationtosubscribetotheseclickeventsandhandletheminitsownway.
Instead,youwouldliketobeabletofunneltheclickeventsfromthebuttonintotheObservable.WitharegularObservableinstance,thisisn'tpossiblesinceitisonlyactingastheSubscribepartofthePublish-Subscribemodel.ToaccomplishthePublishaspect,youmustuseaSubjectinstance:
[app/click-observer.component.ts]
import{Component}from'@angular/core';
import{Subject}from'rxjs/Subject';
@Component({
selector:'click-observer',
template:`
<button>
Emitevent!
</button>
<p*ngFor="letclickofclicks;leti=index">
{{i}}:{{click}}
</p>
`
})
exportclassClickObserverComponent{
clickEmitter:Subject<Event>=newSubject();
clicks:Array<Event>=[];
}
ReactiveXSubjectsactasboththeObservableandtheObserver.Therefore,itexposesboththesubscribe()method,usedfortheSubscribebehavior,andthenext()method,usedforthePublishbehavior.
Note
Inthisexample,thenext()methodisusefulbecauseyouwanttoexplicitlyspecifywhenanemissionshouldoccurandwhatthatemissionshouldcontain.TherearelotsofwaysofinstantiatingObservablesinordertoimplicitlygenerateemissions,suchas(butcertainlynotlimitedto)Observable.range().Inthesecases,Observableunderstandshowitsinputbehaves,andthusitdoesnotneeddirectionastowhenemissionsoccurandwhattheyshouldcontain.
Inthiscase,youcanpasstheeventdirectlytonext()inthetemplateclickhandlerdefinition.
Withthis,allthatisleftistopopulatethearraybydirectingtheemissionsintoit:
[app/click-observer.component.ts]
import{Component}from'@angular/core';
import{Subject}from'rxjs/Subject';
@Component({
selector:'click-observer',
template:`
<button(click)="clickEmitter.next($event)">
Emitevent!
</button>
<p*ngFor="letclickofclicks;leti=index">
{{i}}:{{click}}
</p>
`
})
exportclassClickObserverComponent{
clickEmitter:Subject<Event>=newSubject();
clicks:Array<Event>=[];
constructor(){
this.clickEmitter
.subscribe(clickEvent=>this.clicks.push(clickEvent));
}
}
That'sall!Withthis,youshouldseeclickeventspopulateinthebrowserwitheachsuccessivebuttonclick.
Howitworks...ReactiveXObservablesandObserversaredistinct,buttheirbehaviorismutuallycompatibleinsuchawaythattheirunion,Subject,canactaseitheroneofthem.Inthisexample,theSubjectisusedastheinterfacetofeedinEventobjectsasthePublishmodalityaswellastohandletheresultthatwouldcomeoutastheSubscribemodality.
There'smore...Thewaythisisconstructedmightfeelabitstrangetoyou.ThecomponentisexposingtheSubjectinstanceasthepointwhereyourapplicationwillattachobserverhandlers.
However,youwanttopreventotherpartsoftheapplicationfromaddingadditionalevents,whichisstillpossibleshouldtheychoosetousethenext()method.What'smore,theSubjectinstanceisreferenceddirectlyinsidethetemplateandexposingittheremayfeelabitodd.Therefore,itisdesirable,andcertainlygoodsoftwarepractice,toonlyexposetheObservablecomponentoftheSubject.
Todothis,youmustimporttheObservablemoduleandutilizetheSubjectinstance'smembermethod,namelyasObservable().ThismethodwillcreateanewObservableinstancethatwilleffectivelypipetheobservedemissionsfromtheSubjectintothenewObservable,whichwillbeexposedasapubliccomponentmember:
[app/article.component.ts]
import{Component}from'@angular/core';
import{Observable}from'rxjs/Observable';
import{Subject}from'rxjs/Subject';
@Component({
selector:'click-observer',
template:`
<button(click)="publish($event)">
Emitevent!
</button>
<p*ngFor="letclickofclicks;leti=index">
{{i}}:{{click}}
</p>
`
})
exportclassClickObserverComponent{
clickEmitter:Observable<Event>;
privateclickSubject_:Subject<Event>=newSubject();
clicks:Array<Event>=[];
constructor(){
this.clickEmitter=this.clickSubject_.asObservable();
this.clickEmitter.subscribe(clickEvent=>
this.clicks.push(clickEvent));
}
publish(e:Event):void{
this.clickSubject_.next(e);
}
}
NoweventhoughonlythiscomponentisreferencingclickEmitter,everycomponentthatusesclickEmitterwillnotneedorbeabletotouchthesource,Subject.
NativeRxJSimplementation
Thishasallbeenagreatexample,butthisissuchacommonpatterninthattheRxJSlibraryalreadyprovidesabuilt-inwayofimplementingit.TheObservableclassexposesastaticmethodfromEvent(),whichtakesinanelementthatisexpectedtogenerateeventsandtheeventtypetolistento.
However,youneedareferencetotheactualelement,whichyoucurrentlydonothave.Forthepresentimplementation,theAngular2ViewChildfacultieswillgiveyouaverynicereferencetothebutton,whichwillthenbepassedtothefromEvent()methodoncethetemplatehasbeenrendered:
[app/click-observer.component.ts]
import{Component,ViewChild,ngAfterViewInit}
from'@angular/core';
import{Observable}from'rxjs/Observable';
import'rxjs/add/observable/fromEvent';
@Component({
selector:'click-observer',
template:`
<button#btn>
Emitevent!
</button>
<p*ngFor="letclickofclicks;leti=index">
{{i}}:{{click}}
</p>
`
})
exportclassClickObserverComponentimplementsAfterViewInit{
@ViewChild('btn')btn;
clickEmitter:Observable<Event>;
clicks:Array<Event>=[];
ngAfterViewInit(){
this.clickEmitter=Observable.fromEvent(
this.btn.nativeElement,'click');
this.clickEmitter.subscribe(clickEvent=>
this.clicks.push(clickEvent));
}
}
Withallofthis,thecomponentshouldstillbehaveidentically.
SeealsoBasicutilizationofObservableswithHTTPdemonstratesthebasicsofhowtouseanobservableinterfaceCreatinganObservableauthenticationserviceusingBehaviorSubjectsinstructsyouonhowtoreactivelymanagethestateinyourapplicationBuildingafullyfeaturedAutoCompletewithObservablesgivesyouabroadtourofsomeoftheutilitiesofferedtoyouaspartoftheRxJSlibrary
CreatinganObservableauthenticationserviceusingBehaviorSubjectsOneofthemostobviousandusefulcasesoftheObserverPatternistheoneinwhichasingleentityinyourapplicationunidirectionallycommunicatesinformationtoafieldoflistenersontheoutside.Theselistenerswouldliketobeabletoattachanddetachfreelyfromthesinglebroadcastingentity.Agoodinitialexampleofthisisthelogin/logoutcomponent.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/6957/.
GettingreadySupposeyouhavethefollowingskeletonapplication:
[app/login.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'login',
template:`
<button*ngIf="!loggedIn"
(click)="loggedIn=true">
Login
</button>
<button*ngIf="loggedIn"
(click)="loggedIn=false">
Logout
</button>
`
})
exportclassLoginComponent{
loggedIn:boolean=false;
}
Asitpresentlyexists,thiscomponentwillallowyoutotogglebetweenthelogin/logoutbutton,butthereisnoconceptofsharedapplicationstate,andothercomponentscannotutilizetheloginstatethatthiscomponentwouldtrack.
YouwouldliketointroducethisstatetoasharedservicethatisoperatedusingtheObserverPattern.
Howtodoit...Beginbycreatinganemptyserviceandinjectingitintothiscomponent:
[app/authentication.service.ts]
import{Injectable}from'@angular/core';
@Injectable()
exportclassAuthService{
privateauthState_:AuthState;
}
exportconstenumAuthState{
LoggedIn,
LoggedOut
}
NoticethatyouareusingaTypeScriptconstenumtokeeptrackoftheuser'sauthenticationstate.
Note
Ifyou'renewtoES6andTypeScript,thesekeywordsmayfeelabitbizarretoyou.TheconstkeywordisfromtheES6specification,signifyingthatthisvalueisreadonlyoncedeclared.InvanillaES6,thiswillthrowanerror,usuallySyntaxError,atruntime.WithTypeScriptcompilationthough,constwillbecaughtatcompiletime.
TheenumkeywordisanofferingofTypeScript.Itisnotdissimilartoaregularobjectliteral,butnotethattheenummembersdonothavevalues.
Throughouttheapplication,youwillreferencetheseviaAuthState.LoggedInandAuthState.LoggedOut.IfyoureferencethecompiledJavaScriptthatTypeScriptgenerates,youwillseethattheseareactuallyassignedintegervalues.Butforthepurposesofbuildinglargeapplications,thisallowsustodevelopacentralizedrepositoryofpossibleAuthStatevalueswithoutworryingabouttheiractualvalues.
Injectingtheauthenticationservice
Astheskeletonservicecurrentlyexists,youaregoingtoinstantiateaSubjectthatwillemitAuthState,butthereisnowayavailablecurrentlytointeractwithit.Youwillsetthisupinabit.First,youmustinjectthisserviceintoyourcomponent:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{LoginComponent}from'./login.component';
import{AuthService}from'./authentication.service';
@NgModule({
imports:[
BrowserModule
],
declarations:[
LoginComponent
],
providers:[
AuthService
],
bootstrap:[
LoginComponent
]
})
exportclassAppModule{}
Thisisallwellandgood,buttheserviceisstillunusableasis.
Tip
NotethatthepathyouimportyourAuthServicefrommayvarydependingonwhereitliesinyourfiletree.
AddingBehaviorSubjecttotheauthenticationservice
Thecoreofthisserviceistomaintainaglobalapplicationstate.Itshouldexposeitselftotherestoftheapplicationbylettingotherpartssaytotheservice,"Letmeknowwheneverthestatechanges.Also,I'dliketoknowwhatthestateisrightnow."TheperfecttoolforthistaskisBehaviorSubject.
Note
RxJSSubjectsalsohaveseveralsubclasses,andBehaviorSubjectisoneofthem.Fundamentally,itfollowsalltherhythmsofSubjects,butthemaindifferenceisthatitwillemititscurrentstatetoanyobserverthatbeginstolistentoit,asifthateventisentirelynew.Incaseslikethis,whereyouwanttokeeptrackofthestate,thisisextremelyuseful.
AddaprivateBehaviorSubject(initializedtotheLoggedOutstate)andapublicObservabletoAuthService:
[app/authentication.service.ts]
import{Injectable}from'@angular/core';
import{BehaviorSubject}from'rxjs/BehaviorSubject';
import{Observable}from'rxjs/Observable';
@Injectable()
exportclassAuthService{
privateauthManager_:BehaviorSubject<AuthState>
=newBehaviorSubject(AuthState.LoggedOut);
privateauthState_:AuthState;
authChange:Observable<AuthState>;
constructor(){
this.authChange=this.authManager_.asObservable();
}
}
exportconstenumAuthState{
LoggedIn,
LoggedOut
}
AddingAPImethodstotheauthenticationservice
RecallthatyoudonotwanttoexposetheBehaviorSubjectinstancetooutsideactors.Instead,youwouldliketoofferonlyitsObservablecomponent,whichyoucanopenlysubscribeto.Furthermore,youwouldliketoallowoutsideactorstosettheauthenticationstate,butonlyindirectly.Thiscanbeaccomplishedwiththefollowingmethods:
[app/authentication.service.ts]
import{Injectable}from'@angular/core';
import{BehaviorSubject}from'rxjs/BehaviorSubject';
import{Observable}from'rxjs/Observable';
@Injectable()
exportclassAuthService{
privateauthManager_:BehaviorSubject<AuthState>
=newBehaviorSubject(AuthState.LoggedOut);
privateauthState_:AuthState;
authChange:Observable<AuthState>;
constructor(){
this.authChange=this.authManager_.asObservable();
}
login():void{
this.setAuthState_(AuthState.LoggedIn);
}
logout():void{
this.setAuthState_(AuthState.LoggedOut);
}
emitAuthState():void{
this.authManager_.next(this.authState_);
}
privatesetAuthState_(newAuthState:AuthState):void{
this.authState_=newAuthState;
this.emitAuthState();
}
}
exportconstenumAuthState{
LoggedIn,
LoggedOut
}
Outstanding!Withallofthis,outsideactorswillbeabletosubscribetoauthChangeObservableandwillindirectlycontrolthestatevialogin()andlogout().
Tip
NotethattheObservablecomponentofBehaviorSubjectisnamedauthChange.NamingthedifferentcomponentsoftheelementsintheObserverPatterncanbetricky.ThisnamingconventionwasselectedtorepresentwhataneventemittedfromtheObservableactuallymeant.Quiteliterally,authChangeistheanswertothequestion,"WhateventamIobserving?".Therefore,itmakesgoodsemanticsensethatyourcomponentsubscribestoauthChangeswhentheauthenticationstatechanges.
Wiringtheservicemethodsintothecomponent
LoginComponentdoesnotyetutilizetheservice,soaddinitsnewlycreatedmethods:
[app/login.component.ts]
import{Component}from'@angular/core';
import{AuthService,AuthState}from'./authentication.service';
@Component({
selector:'login',
template:`
<button*ngIf="!loggedIn"
(click)="login()">
Login
</button>
<button*ngIf="loggedIn"
(click)="logout()">
Logout
</button>
`
})
exportclassLoginComponent{
loggedIn:boolean;
constructor(privateauthService_:AuthService){
authService_.authChange.subscribe(
newAuthState=>
this.loggedIn=(newAuthState===AuthState.LoggedIn));
}
login():void{
this.authService_.login();
}
logout():void{
this.authService_.logout();
}
}
Withallofthisinplace,youshouldbeabletoseeyourlogin/logoutbuttonsfunctionwell.ThismeansyouhavecorrectlyincorporatedObservableintoyourcomponent.
Tip
Thisrecipeisagoodexampleofconventionsyou'rerequiredtomaintainwhenusingpublic/private.Notethattheinjectedserviceisdeclaredasaprivatememberandwrappedwithpubliccomponentmembermethods.Anythingthatanotherpartoftheapplicationcallsoranythingthatisusedinsidethetemplateshouldbeapublicmember.
Howitworks...CentraltothisimplementationisthateachcomponentthatislisteningtoObservablehasanidempotenthandlingofeventsthatareemitted.EachtimeanewcomponentisconnectedtoObservable,itinstructstheservicetoemitwhateverthecurrentstateis,usingemitAuthState().Necessarily,allcomponentsdon'tbehaveanydifferentlyiftheyseethesamestateemittedmultipletimesinarow;theywillonlyaltertheirbehavioriftheyseeachangeinthestate.
Noticehowyouhavetotallyencapsulatedtheauthenticationstateinsidetheauthenticationservice,andatthesametime,haveexposedandutilizedareactiveAPIfortheentireapplicationtobuildupon.
There'smore...Twocriticalcomponentsofhookingintoservicessuchasthesearethesetupandteardownprocesses.AfastidiousdeveloperwillhavenoticedthatevenifaninstanceofLoginComponentisdestroyed,thesubscriptiontoObservablewillstillpersist.This,ofcourse,isextremelyundesirable!
Fortunately,thesubscribe()methodofObservablesreturnsaninstanceofSubscription,whichexposesanunsubscribe()method.Youcanthereforecapturethisinstanceupontheinvocationofsubscribe()andtheninvokeitwhenthecomponentisbeingtorndown.
SimilartolistenerteardowninAngular1,youmustinvoketheunsubscribemethodwhenthecomponentinstanceisbeingdestroyed.Happily,theAngular2lifecycleprovidesyouwithsuchamethod,ngOnDestroy,inwhichyoucaninvokeunsubscribe():
[app/login.component.ts]
import{Component,ngOnDestroy}from'@angular/core';
import{AuthService,AuthState}from'./authentication.service';
import{Subscription}from'rxjs/Subscription';
@Component({
selector:'login',
template:`
<button*ngIf="!loggedIn"
(click)="login()">
Login
</button>
<button*ngIf="loggedIn"
(click)="logout()">
Logout
</button>
`
})
exportclassLoginComponentimplementsOnDestroy{
loggedIn:boolean;
privateauthChangeSubscription_:Subscription;
constructor(privateauthService_:AuthService){
this.authChangeSubscription_=
authService_.authChange.subscribe(
newAuthState=>
this.loggedIn=(newAuthState===AuthState.LoggedIn));
}
login():void{
this.authService_.login();
}
logout():void{
this.authService_.logout();
}
ngOnDestroy(){
this.authChangeSubscription_.unsubscribe();
}
}
Nowyourapplicationissafefrommemoryleaksshouldanyinstanceofthiscomponenteverbedestroyedinthelifetimeofyourapplication.
SeealsoBasicutilizationofObservableswithHTTPdemonstratesthebasicsofhowtouseanobservableinterfaceImplementingaPublish-SubscribemodelusingSubjectsshowsyouhowtoconfigureinputandoutputforRxJSObservablesBuildingageneralizedPublish-Subscribeservicetoreplace$broadcast,$emit,and$onassemblesarobustPubSubmodelforconnectingapplicationcomponentswithchannelsBuildingafullyfeaturedAutoCompletewithObservablesgivesyouabroadtourofsomeoftheutilitiesofferedtoyouaspartoftheRxJSlibrary
BuildingageneralizedPublish-Subscribeservicetoreplace$broadcast,$emit,and$onInAngular1,the$emitand$broadcastbehaviorswereindeedveryusefultools.Theygaveyoutheabilitytosendcustomeventsupwardsanddownwardsthroughthescopetreetoanylistenersthatmightbewaitingforsuchanevent.Thispushedthedevelopertowardsaveryusefulpattern:theabilityformanycomponentstobeabletotransmiteventstoandfromacentralsource.However,using$emitand$broadcastforsuchapurposewasgrosslyinappropriate;theyhadtheeffectoffeedingtheeventthroughhugenumbersofscopesonlytoreachthesingleintendedtarget.
Note
Inthepreviouseditionofthisbook,thecorrespondingrecipedemonstratedhowtobuildaPublish-Subscribeservicethatusedthe$emitand$rootScopeinjection.Theversioninthisrecipe,althoughdifferentinahandfulofways,achievessimilarresultsinasubstantiallycleanerandmoreelegantfashion.
Itispreferabletocreateasingleentitythatcanserveasagenericthroughwayforeventstopassfrompublisherstotheirsubscribers.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/2417/.
GettingreadyBeginwithaskeletonserviceinjectedintoacomponent:
[app/node.component.ts]
import{Component,Input}from'@angular/core';
import{PubSubService}from'./publish-subscribe.service';
@Component({
selector:'node',
template:`
<p>Heard{{count}}of{{subscribeChannel}}</p>
<button(click)="send()">Send{{publishChannel}}</button>
`
})
exportclassNodeComponent{
@Input()publishChannel:string;
@Input()subscribeChannel:string;
count:number=0;
constructor(privatepubSubService_:PubSubService){}
send(){}
}
[app/publish-subscribe.service.ts]
import{Injectable}from'@angular/core';
@Injectable()
exportclassPubSubService{
constructor(){}
publish(){}
subscribe(){}
}
Howtodoit...Thegroundworkforthisimplementationshouldbeprettyobvious.TheserviceisgoingtohostasingleSubjectinstancethatisgoingtofunneleventsofanytypeintotheserviceandoutthroughtheobserversoftheSubject.
First,implementthefollowingsothatsubscribe()andpublish()actuallydoworkwhenyouinvolvetheSubjectinstance:
[app/publish-subscribe.service.ts]
import{Injectable}from'@angular/core';
import{Subject}from'rxjs/Subject';
import{Observable}from'rxjs/Observable';
import{Observer}from'rxjs/Observer';
import{Subscriber}from'rxjs/Subscriber;
@Injectable()
exportclassPubSubService{
privatepublishSubscribeSubject_:Subject<any>=newSubject();
emitter_:Observable<any>;
constructor(){
this.emitter_=this.publishSubscribeSubject_.asObservable();
}
publish(event:any):void{
this.publishSubscribeSubject_.next(event);
}
subscribe(handler:NextObserver<any>):Subscriber{
returnthis.emitter_.subscribe(handler);
}
}
Thisisterrificforaninitialimplementation,butyieldsaproblem:everyeventpublishedtothisservicewillbebroadcastedtoallthesubscribers.
Introducingchannelabstraction
Itispossibleandinfactquiteeasytorestrictpublishandsubscribeinsuchawaythattheywillonlypayattentiontothechanneltheyspecify.First,modifypublish()tonesttheeventinsidetheemittedobject:
[app/publish-subscribe.service.ts]
import{Injectable}from'@angular/core';
import{Subject}from'rxjs/Subject';
import{Observable}from'rxjs/Observable';
import{Observer}from'rxjs/Observer';
import{Subscriber}from'rxjs/Subscriber;
@Injectable()
exportclassPubSubService{
privatepublishSubscribeSubject_:Subject<any>=newSubject();
emitter_:Observable<any>;
constructor(){
this.emitter_=this.publishSubscribeSubject_.asObservable();
}
publish(channel:string,event:any):void{
this.publishSubscribeSubject_.next({
channel:channel,
event:event
});
}
subscribe(handler:NextObserver<any>):Subscriber{
returnthis.emitter_.subscribe(handler);
}
}
Withthis,youarenowabletoutilizesomeObservablebehaviortorestrictwhicheventsthesubscriptionispayingattentionto.
Observableemissionscanhavefilter()andmap()appliedtothem.filter()willreturnanewObservableinstancethatonlyemitswhicheveremissionsevaluateastrueinitsfilterfunction.map()returnsanewObservableinstancethattransformsallemissionsintoanewvalue.
[app/publish-subscribe.service.ts]
import{Injectable}from'@angular/core';
import{Subject}from'rxjs/Subject';
import{Observable}from'rxjs/Observable';
import{Observer}from'rxjs/Observer';
import{Subscriber}from'rxjs/Subscriber;
import'rxjs/add/operator/filter';
import'rxjs/add/operator/map';
@Injectable()
exportclassPubSubService{
privatepublishSubscribeSubject_:Subject<any>=newSubject();
emitter_:Observable<any>;
constructor(){
this.emitter_=this.publishSubscribeSubject_.asObservable();
}
publish(channel:string,event:any):void{
this.publishSubscribeSubject_.next({
channel:channel,
event:event
});
}
subscribe(channel:string,handler:((value:any)=>void)):Subscriber{
returnthis.emitter_
.filter(emission=>emission.channel===channel)
.map(emission=>emission.event)
.subscribe(handler);
}
}
Hookingcomponentsintotheservice
Theserviceiscomplete,butthecomponentdoesn'tyethavetheabilitytouseit.Usetheinjectedservicetolinkthecomponenttothechannelsspecifiedbyitsinputstrings:
[app/node.component.ts]
import{Component,Input}from'@angular/core';
import{PubSubService}from'./publish-subscribe.service';
@Component({
selector:'node',
template:`
<p>Heard{{count}}of{{subscribeChannel}}</p>
<button(click)="send()">Send{{publishChannel}}</button>
`
})
exportclassNodeComponent{
@Input()publishChannel:string;
@Input()subscribeChannel:string;
count:number=0;
constructor(privatepubSubService_:PubSubService){}
send(){
this.pubSubService_
.publish(this.publishChannel,{});
}
ngAfterViewInit(){
this.pubSubService_
.subscribe(this.subscribeChannel,
event=>++this.count);
}
}
Tip
Thepublish()methodhasanemptyobjectliteralasitssecondargument.Thisisthepayloadforthepublishedmessage,whichisn'tusedinthisrecipe.Ifyouwanttosenddataalongwitha
message,thisiswhereitwouldgo.
Withallofthis,testyourapplicationwiththefollowing:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<nodesubscribeChannel="foo"
publishChannel="bar">
</node>
<nodesubscribeChannel="bar"
publishChannel="foo">
</node>
`
})
exportclassRootComponent{}
Youwillseethatchannelpublishingandsubscribingishappeningasyouwouldexpect.
Unsubscribingfromchannels
Ofcourse,youwanttoavoidmemoryleakswhereverpossible.Thisrequiresthatyouexplicitlycompletethecleanupprocesswhenyourcomponentinstanceisdestroyed:
[app/node.component.ts]
import{Component,Input,OnDestroy}from'@angular/core';
import{PubSubService}from'./publish-subscribe.service';
import{Subscription}from'rxjs/Subscription';
@Component({
selector:'node',
template:`
<p>Heard{{count}}of{{subscribeChannel}}</p>
<button(click)="send()">Send{{publishChannel}}</button>
`
})
exportclassNodeComponentimplementsOnDestroy{
@Input()publishChannel:string;
@Input()subscribeChannel:string;
count:number=0;
privatepubSubServiceSubscription_:Subscription;
constructor(privatepubSubService_:PubSubService){}
send(){
this.pubSubService_
.publish(this.publishChannel,{});
}
ngAfterViewInit(){
this.pubSubService_
.subscribe(this.subscribeChannel,
event=>++this.count);
}
ngOnDestroy(){
this.pubSubServiceSubscription_.unsubscribe();
}
}
Howitworks...Eachtimepublish()isinvoked,theprovidedeventiswrappedbytheprovidedchannelandsubmittedtoacentralSubject,whichisprivateinsidetheservice.Atthesametime,thefactthateachinvocationofsubscribe()wantstolistentoadifferentchannelpresentsaproblem.ThisisbecauseanObservabledoesnotdrawdistinctionsregardingwhatisbeingemittedwithoutexplicitdirection.
Youareabletoutilizethefilter()andmap()operatorstoestablishacustomizedviewoftheemissionsofSubjectandusethisviewintheapplicationoftheObserverhandler.Eachtimesubscribe()isinvoked,itcreatesanewObservableinstance;however,theseareallmerelypointsofindirectionfromtheonetrueObservable,whichisownedbytheprivateinstancehiddeninsidetheservice.
There'smore...It'simportanttounderstandwhythisserviceisnotbuiltinadifferentway.
AnimportantfeatureofObservablesistheirabilitytobecomposed.Thatis,severalObservableinstancesindependentlyemittingeventscanbecombinedintooneObservableinstance,whichwillemitalltheeventsfromacombinedsource.Thiscanbeaccomplishedinseveraldifferentways,includingflatMap()ormerge().ThisabilityiswhatisbeingreferredtowhenReactiveXObservablesaredescribedas"composable."
Therefore,adevelopermightseethiscompositionabilityandthinkitwouldbesuitableforaPublish-Subscribeentity.TheentitywouldacceptObservableinstancesfromthepublishers.TheywouldbecombinedtocreateasingleObservableinstance,andsubscriberswouldattachObservabletothiscombination.Whatcouldpossiblygowrong?
ConsiderationsofanObservable'scompositionandmanipulation
OneprimaryconcernisthatthecomposedObservablethatthesubscribersarebeingattachedtowillchangeconstantly.Asisthecasewithmap()andfilter(),anymodulationperformedonanObservableinstance,includingcomposition,willreturnanewObservableinstance.ThisnewinstancewouldbecometheObservablethatsubscriberswouldattachto,andthereinliestheproblem.
Let'sexaminethisproblemstepbystep:
1. PubSubserviceemitseventsfromObservableA.2. NodeXsubscribestotheserviceandreceiveseventsfromObservableA.3. SomeotherpartoftheapplicationaddsObservableBtothePubSubservice.4. ThePubSubservicecomposesObservableAandObservableBintoObservableAB.5. NodeYsubscribestotheserviceandreceiveseventsfromObservableAB.
Notethatinthiscase,NodeXwouldstillreceiveeventsfromonlyObservableAsincethatistheObservableinstancewhereitinvokedsubscribe().
Certainly,therearestepsthatcanbetakentomitigatethisproblem,suchashavinganadditionallevelofindirectionbetweenthesubscribeObservableandthecomposedObservable.However,awiseengineerwillstepbackatthispointandtakestockofthesituation.Publish-Subscribeissupposedtobearelatively"dumb"protocol,meaningthatitshouldn'tbedelegatedtoomuchresponsibilityaroundmanagingtheeventsithasbeenpassedwith—messagesinandmessagesout,withnorealconcernforwhatiscontainedaslongastheygetthere.OnecouldmakeaverystrongargumentthatintroducingObservablesinthePublishsidegreatlyovercomplicatesthings.
Inthecaseofthisrecipe,youhavedevelopedanelegantandsimpleversionofaPublish-Subscribemodule,anditfeelsrighttodelegatecomplexityoutsideofit.InthecaseofentitieswantingtousePublishwithObservables,asolutionmightbetojustpipetheObservableemissionsintotheservice'spublish()method.
SeealsoBasicutilizationofObservableswithHTTPdemonstratesthebasicsofhowtouseanobservableinterfaceImplementingaPublish-SubscribemodelusingSubjectsshowsyouhowtoconfigureinputandoutputforRxJSObservablesCreatinganObservableauthenticationserviceusingBehaviorSubjectsinstructsyouonhowtoreactivelymanagethestateinyourapplicationBuildingafullyfeaturedAutoCompletewithObservablesgivesyouabroadtourofsomeoftheutilitiesofferedtoyouaspartoftheRxJSlibrary
UsingQueryListsandObservablestofollowchangesinViewChildrenOneveryusefulpieceofbehaviorincomponentsistheabilitytotrackchangestothecollectionsofchildrenintheview.Inmanyways,thisisquiteanebuloussubject,asthenumberofwaysinwhichviewcollectionscanbealteredisnumerousandsubtle.Thankfully,Angular2providesasolidfoundationfortrackingthesechanges.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/4112/.
GettingreadySupposeyoubeginwiththefollowingskeletonapplication:
[app/inner.component.ts]
import{Component,Input}from'@angular/core';
@Component({
selector:'inner',
template:`<p>{{val}}`
})
exportclassInnerComponent{
@Input()val:number;
}
[app/outer.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'outer',
template:`
<button(click)="add()">Moar</button>
<button(click)="remove()">Less</button>
<button(click)="shuffle()">Shuffle</button>
<inner*ngFor="letioflist"
val="{{i}}">
</inner>
`
})
exportclassOuterComponent{
list:Array<number>=[];
add():void{
this.list.push(this.list.length)
}
remove():void{
this.list.pop();
}
shuffle():void{
//simpleassignmentshuffle
this.list=this.list.sort(()=>(4*Math.random()>2)?1:-1);
}
}
Asis,thisisaverysimplelistmanagerthatgivesyoutheabilitytoadd,remove,andshufflealistinterpolatedasInnerComponentinstances.Youwanttheabilitytotrackwhenthislistundergoeschangesandkeepreferencestothecomponentinstancesthatcorrespondtotheviewcollection.
Howtodoit...BeginbyusingViewChildrentocollecttheInnerComponentinstancesintoasingleQueryList:
[app/outer.component.ts]
import{Component,ViewChildren,QueryList}from'@angular/core';
import{InnerComponent}from'./inner.component';
@Component({
selector:'outer',
template:`
<button(click)="add()">Moar</button>
<button(click)="remove()">Less</button>
<button(click)="shuffle()">Shuffle</button>
<inner*ngFor="letioflist"
val="{{i}}">
</inner>
`
})
exportclassOuterComponent{
@ViewChildren(InnerComponent)innerComponents:
QueryList<InnerComponent>;
list:Array<number>=[];
add():void{
this.list.push(this.list.length)
}
remove():void{
this.list.pop();
}
shuffle():void{
//simpleassignmentshuffle
this.list=this.list.sort(()=>(4*Math.random()>2)?1:-1);
}
}
Easy!Now,oncetheviewofOuterComponentisinitialized,youwillbeabletousethis.innerComponentstoreferenceQueryList.
DealingwithQueryLists
QueryListsarestrangebirdsinAngular2,butlikemanyotherfacetsoftheframework,theyarejustaconventionthatyouwillhavetolearn.Inthiscase,theyareanimmutableanditerablecollectionthatexposesahandfulofmethodstoinspectwhattheycontainandwhenthesecontentsarealtered.
Inthiscase,thetwoinstancepropertiesyoucareaboutarelastandchanges.last,asyoumight
expect,willreturnthelastinstanceofQueryList—inthiscase,aninstanceofInnerComponentifQueryListisnotempty.changeswillreturnanObservablethatwillemitQueryListwheneverachangeoccursinsideit.InthecaseofacollectionofInnerComponentinstances,theaddition,removal,andshufflingoptionswillallberegisteredaschanges.
Usingtheseproperties,youcanveryeasilysetupOuterComponenttokeeptrackofwhatthevalueofthelastInnerComponentinstanceis:
import{Component,ViewChildren,QueryList}from'@angular/core';
import{InnerComponent}from'./inner.component';
@Component({
selector:'app-outer',
template:`
<button(click)="add()">Moar</button>
<button(click)="remove()">Less</button>
<button(click)="shuffle()">Shuffle</button>
<app-inner*ngFor="letioflist"
val="{{i}}">
</app-inner>
<p>Valueoflast:{{lastVal}}</p>
`
})
exportclassOuterComponent{
@ViewChildren(InnerComponent)innerComponents:
QueryList<InnerComponent>;
list:Array<number>=[];
lastVal:number;
constructor(){}
add(){
this.list.push(this.list.length)
}
remove(){
this.list.pop();
}
shuffle(){
this.list=this.list.sort(()=>(4*Math.random()>2)?1:-1);
}
ngAfterViewInit(){
this.innerComponents.changes
.subscribe(e=>this.lastVal=(e.last||{}).val);
}
}
Withallofthis,youshouldbeabletofindthatlastValwillstayuptodatewithanychangesyouwouldtriggerintheInnerComponentcollection.
Correctingtheexpressionchangederror
Ifyouruntheapplicationasis,youwillnoticethatanerroristhrownafteryouclickontheMoarbuttonthefirsttime:
Expressionhaschangedafteritwaschecked
ThisisanerroryouwillmostlikelyseefrequentlyinAngular2.Themeaningissimple:sinceyouare,bydefault,operatingindevelopmentmode,Angularwillchecktwicetoseethatanyboundvaluesdonotchangeafterallofthechangedetectionlogichasbeenresolved.Inthecaseofthisrecipe,theemissionbyQueryListmodifieslastVal,whichAngulardoesnotexpect.Thus,you'llneedtoexplicitlyinformtheframeworkthatthevalueisexpectedtochangeagain.ThiscanbeaccomplishedbyinjectingChangeDetectorRef,whichallowsyoutotriggerachangedetectioncycleoncethevalueischanged:
import{Component,ViewChildren,QueryList,ngAfterViewInit,
ChangeDetectorRef}from'@angular/core';
import{InnerComponent}from'./inner.component';
@Component({
selector:'outer',
template:`
<button(click)="add()">Moar</button>
<button(click)="remove()">Less</button>
<button(click)="shuffle()">Shuffle</button>
<inner*ngFor="letioflist"
val="{{i}}">
</inner>
<p>Valueoflast:{{lastVal}}</p>
`
})
exportclassOuterComponentimplementsAfterViewInit{
@ViewChildren(InnerComponent)innerComponents:
QueryList<InnerComponent>;
list:Array<number>=[];
lastVal:number;
constructor(privatechangeDetectorRef_:ChangeDetectorRef){}
add():void{
this.list.push(this.list.length)
}
remove():void{
this.list.pop();
}
shuffle():void{
//simpleassignmentshuffle
this.list=this.list.sort(()=>(4*Math.random()>2)?1:-1);
}
ngAfterViewInit(){
this.innerComponents.changes
.subscribe(innerComponents=>{
this.lastVal=(innerComponents.last||{}).val;
this.changeDetectorRef_.detectChanges();
});
}
}
Atthispoint,everythingshouldworkcorrectlywithnoerrors.
Howitworks...OncetheOuterComponentviewisinitialized,youwillbeabletointeractwithQueryListthatisobtainedusingViewChildren.EachtimethecollectionthatQueryListwrapsismodified,theObservableexposedbyitschangespropertywillemitQueryList,signalingthatsomethinghaschanged.
Hatetheplayer,notthegame
Importantly,Observable<QueryList>doesnottrackchangesinthearrayofnumbers.IttracksthegeneratedcollectionofInnerComponents.ThengForstructuraldirectiveisresponsibleforgeneratingthelistofInnerComponentinstancesintheview.ItisthiscollectionthatQueryListisconcernedwith,nottheoriginalarray.
Thisisagoodthing!ViewChildrenshouldonlybeconcernedwiththecomponentsastheyhavebeenrenderedinsidetheview,notthedatathatcausedthemtoberenderedinsuchafashion.
Oneimportantconsiderationofthisisthatuponeachemission,itisentirelypossiblethatQueryListwillbeempty.Asshownabove,sincetheObserveroftheQueryList.changesObservabletriestoreferenceapropertyoflast,itisnecessarytohaveafallbackobjectliteralintheeventthatlastreturnsundefined.
SeealsoBasicUtilizationofObservableswithHTTPdemonstratesthebasicsofhowtouseanobservableinterfaceBuildingafullyfeaturedAutoCompletewithObservablesgivesyouabroadtourofsomeoftheutilitiesofferedtoyouaspartoftheRxJSlibrary
BuildingafullyfeaturedAutoCompletewithObservablesRxJSObservablesaffordyoualotoffirepower,anditwouldbeashametomissoutonthem.Ahugelibraryoftransformationsandutilitiesarebakedrightinthatallowyoutoelegantlyarchitectcomplexportionsofyourapplicationinareactivefashion.
Inthisrecipe,you'lltakeanaïveautocompleteformandbuildarobustsetoffeaturestoenhancebehaviorandperformance.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/8629/.
GettingreadyBeginwiththefollowingapplication:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{SearchComponent}from'./search.component';
import{APIService}from'./api.service';
import{HttpModule}from'@angular/http';
@NgModule({
imports:[
BrowserModule,
HttpModule
],
declarations:[
SearchComponent
],
providers:[
APIService
],
bootstrap:[
SearchComponent
]
})
exportclassAppModule{}
[app/search.component.ts]
import{Component}from'@angular/core';
import{APIService}from'./api.service';
@Component({
selector:'search',
template:`
<input#queryField(keyup)="search(queryField.value)">
<p*ngFor="letresultofresults">{{result}}</p>
`
})
exportclassSearchComponent{
results:Array<string>=[];
constructor(privateapiService_:APIService){}
search(query:string):void{
this.apiService_
.search(query)
.subscribe(result=>this.results.push(result));
}
}
[app/api.service.ts]
import{Injectable}from'@angular/core';
import{Http}from'@angular/http';
import{Observable}from'rxjs/Rx';
@Injectable()
exportclassAPIService{
constructor(privatehttp_:Http){}
search(query:string):Observable<string>{
returnthis.http_
.get('static/response.json')
.map(r=>r.json()['prefix']+query)
//Belowisjustacleverwayofrandomly
//delayingtheresponsebetween0to1000ms
.concatMap(
x=>Observable.of(x).delay(Math.random()*1000));
}
}
YourobjectiveistodramaticallyenhancethisusingRxJS.
Howtodoit...Asis,thisapplicationislisteningforkeyupeventsinthesearchinput,performinganHTTPrequesttoastaticJSONfileandaddingtheresponsetoalistofresults.
UsingtheFormControlvalueChangesObservable
Angular2hasobservablebehavioralreadyavailabletoyouinanumberofplaces.OneofthemisinsideReactiveFormsModule,whichallowsyoutouseanObservablethatisattachedtoaforminput.ConvertthisinputtouseFormControl,whichexposesavalueChangesObservable:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{SearchComponent}from'./search.component';
import{APIService}from'./api.service';
import{HttpModule}from'@angular/http';
import{ReactiveFormsModule}from'@angular/forms';
@NgModule({
imports:[
BrowserModule,
HttpModule,
ReactiveFormsModule
],
declarations:[
SearchComponent
],
providers:[
APIService
],
bootstrap:[
SearchComponent
]
})
exportclassAppModule{}
[app/search.component.ts]
import{Component}from'@angular/core';
import{APIService}from'./api.service';
import{FormControl}from'@angular/forms';
@Component({
selector:'search',
template:`
<input[formControl]="queryField">
<p*ngFor="letresultofresults">{{result}}</p>
`
})
exportclassSearchComponent{
results:Array<string>=[];
queryField:FormControl=newFormControl();
constructor(privateapiService_:APIService){
this.queryField.valueChanges
.subscribe(query=>this.apiService_
.search(query)
.subscribe(result=>this.results.push(result)));
}
}
Debouncingtheinput
Eachtimetheinputvaluechanges,Angularwilldutifullyfireoffarequestandhandletheresponseassoonasitisready.Inthecasewheretheuserisqueryingaverylongterm,suchassupercalifragilisticexpialidocious,itmaybenecessaryforyoutoonlysendoffasinglerequestonceyouthinkthey'redonewithtyping,asopposedto34requests,oneforeachtimetheinputchanges.
RxJSObservableshavethisbuiltin.debounceTime(delay)willcreateanewObservablethatwillonlypassalongthelatestvaluewhentherehaven'tbeenanyothervaluesfor<delay>ms.ThisshouldbeaddedtothevalueChangesObservablesincethisisthesourcethatyouwishtodebounce.200mswillbesuitableforyourpurposes:
[app/search.component.ts]
import{Component}from'@angular/core';
import{APIService}from'./api.service';
import{FormControl}from'@angular/forms';
@Component({
selector:'search',
template:`
<input[formControl]="queryField">
<p*ngFor="letresultofresults">{{result}}</p>
`
})
exportclassSearchComponent{
results:Array<string>=[];
queryField:FormControl=newFormControl();
constructor(privateapiService_:APIService){
this.queryField.valueChanges
.debounceTime(200)
.subscribe(query=>this.apiService_
.search(query)
.subscribe(result=>this.results.push(result)));
}
}
Note
Theoriginofthetermdebouncecomesfromtheworldofcircuits.Mechanicalbuttonsorswitchesutilizemetalcontactstoopenandclosecircuitconnections.Whenthemetalcontactsareclosed,theywillbangtogetherandreboundbeforebeingsettled,causingbounce.Thisbounceisproblematicinthecircuit,asitwilloftenregisterasarepeattogglingoftheswitchorbutton—obviouslybuggybehavior.Theworkaroundforthisistofindawaytoignoretheexpectedbouncenoise—debouncing!Thiscanbeaccomplishedbyeitherignoringthebouncenoiseorintroducingadelaybeforereadingthevalue,bothofwhichcanbedonewithhardwareorsoftware.
Ignoringserialduplicates
Sinceyouarereadinginputfromatextbox,itisverypossiblethattheuserwilltypeonecharacter,thentypeanothercharacterandpressbackspace.FromtheperspectiveoftheObservable,sinceitisnowdebouncedbyadelayperiod,itisentirelypossiblethattheuserinputwillbeinterpretedinsuchawaythatthedebouncedoutputwillemittwoidenticalvaluessequentially.RxJSoffersexcellentprotectionagainstthis,distinctUntilChanged(),whichwilldiscardanemissionthatwillbeaduplicateofitsimmediatepredecessor:
[app/search.component.ts]
import{Component}from'@angular/core';
import{APIService}from'./api.service';
import{FormControl}from'@angular/forms';
@Component({
selector:'search',
template:`
<input[formControl]="queryField">
<p*ngFor="letresultofresults">{{result}}</p>
`
})
exportclassSearchComponent{
results:Array<string>=[];
queryField:FormControl=newFormControl();
constructor(privateapiService_:APIService){
this.queryField.valueChanges
.debounceTime(200)
.distinctUntilChanged()
.subscribe(query=>this.apiService_
.search(query)
.subscribe(result=>this.results.push(result)));
}
}
FlatteningObservables
YouhavechainedquiteafewRxJSmethodsuptothispoint,andseeingnestedsubscribe()
invocationsmightfeelabitfunnytoyou.ItshouldmakesensesincethevalueChangesObservablehandlerisinvokingaservicemethod,whichreturnsaseparateObservable.InTypeScript,thisiseffectivelyrepresentedasObservable<Observable<string>>.Gross!
Sinceyouonlyreallycareabouttheemittedstringscomingfromtheservicemethod,itwouldbemucheasiertojustcombinealltheemittedstringscomingoutofeachreturnedObservableintoasingleObservable.Fortunately,RxJSmakesthiseasywithflatMap,whichflattensalltheemissionsfromtheinnerObservablesintoasingleouterObservable.InTypeScript,usingflatMapwouldconvertthisintoObservable<string>,whichisexactlywhatyouneed:
[app/search.component.ts]
import{Component}from'@angular/core';
import{APIService}from'./api.service';
import{FormControl}from'@angular/forms';
@Component({
selector:'search',
template:`
<input[formControl]="queryField">
<p*ngFor="letresultofresults">{{result}}</p>
`
})
exportclassSearchComponent{
results:Array<string>=[];
queryField:FormControl=newFormControl();
constructor(privateapiService_:APIService){
this.queryField.valueChanges
.debounceTime(200)
.distinctUntilChanged()
.flatMap(query=>this.apiService_.search(query))
.subscribe(result=>this.results.push(result));
}
}
Handlingunorderedresponses
Whentestinginputnow,youwillsurelynoticethatthedelayintentionallyintroducedinsidetheAPIservicewillcausetheresponsestobereturnedoutoforder.Thisisaprettyeffectivesimulationofnetworklatency,soyou'llneedagoodwayofhandlingthis.
Ideally,youwouldliketobeabletothrowoutObservablesthatareinflightonceyouhaveamorerecentquerytoexecute.Forexample,considerthatyou'vetypedgandtheno.Nowoncethesecondqueryforgoisreturnedandifthefirstqueryforghasn'treturnedyet,you'dliketojustthrowitoutandforgetaboutitsincetheresponseisnowirrelevant.
RxJSalsomakesthisveryeasywithswitchMap.ThisdoesthesamethingsasflatMap,butit
willunsubscribefromanyin-flightObservablesthathavenotemittedanyvaluesyet:
[app/search.component.ts]
import{Component}from'@angular/core';
import{APIService}from'./api.service';
import{FormControl}from'@angular/forms';
@Component({
selector:'search',
template:`
<input[formControl]="queryField">
<p*ngFor="letresultofresults">{{result}}</p>
`
})
exportclassSearchComponent{
results:Array<string>=[];
queryField:FormControl=newFormControl();
constructor(privateapiService_:APIService){
this.queryField.valueChanges
.debounceTime(200)
.distinctUntilChanged()
.switchMap(query=>this.apiService_.search(query))
.subscribe(result=>this.results.push(result));
}
}
YourAutoCompleteinputshouldnowbedebouncedanditshouldignoreredundantrequestsandreturnin-orderresults.
Howitworks...Therearealotofmovingpiecesgoingoninthisrecipe,butthecorethemeremainsthesame:RxJSObservablesexposemanymethodsthatcanpipetheoutputfromoneobservableintoanentirelydifferentobservable.Itcanalsocombinemultipleobservablesintoasingleobservable,aswellasintroducestate-dependentoperationsintoastreamoftheinput.Attheendofthisrecipe,thepowerofreactiveprogrammingshouldbeobvious.
SeealsoBasicUtilizationofObservableswithHTTPdemonstratesthebasicsofhowtouseanobservableinterfaceImplementingaPublish-SubscribemodelusingSubjectsshowsyouhowtoconfigureinputandoutputforRxJSObservablesCreatinganObservableauthenticationserviceusingBehaviorSubjectsinstructsyouonhowtoreactivelymanagethestateinyourapplicationBuildingageneralizedPublish-Subscribeservicetoreplace$broadcast,$emit,and$onassemblesarobustPubSubmodelforconnectingapplicationcomponentswithchannels
Chapter6.TheComponentRouterThischapterwillcoverthefollowingrecipes:
SettingupanapplicationtosupportsimpleroutesNavigatingwithrouterLinksNavigatingwiththeRouterserviceSelectingLocationStrategyforPathConstructionBuildingstatefulRouterLinkbehaviorwithRouterLinkActiveImplementingnestedviewswithrouteparametersandchildroutesWorkingwithMatrixURLparametersandroutingarraysAddingrouteauthenticationcontrolswithrouteguards
IntroductionFewfeaturesofAngular2shouldbeanticipatedmorethantheComponentRouter.ThisnewroutingimplementationaffordsyouadazzlingarrayoffeaturesthatweremissingorseverelylackinginAngular1.
Angular2implementsmatrixparameters;thisisanentirelynewsyntaxforURLstructures.OriginallyproposedbyTimBerners-Leein1996,thissemicolon-basedsyntaxgivesyoutheabilitytorobustlyassociateparametersnotjustwithasingleURL,butwithdifferentlevelsinthatURL.YourapplicationcannowintroduceanadditionaldimensionofapplicationstateintheURLs.
Additionally,ComponentRoutergivesyouamethodofelegantlynestingviewswithineachotheraswellasasimplewayofdefiningroutesandlinkstothesecomponenthierarchies.Foryou,thismeansyourapplicationscantrulytakemaximaladvantageofdefininganapplicationasanindependentmodule.
Finally,ComponentRouterfullyembracesintegrationwithObservablestructuresandprovidesyouwithsomebeautifulwaysofnavigatingandcontrollingnavigationwithinyourapplication.
SettingupanapplicationtosupportsimpleroutesCentraltothebehaviorofsingle-pageapplicationsistheabilitytoperformnavigationwithoutaformalbrowserpagereload.Angular2iswell-equippedtoworkaroundthedefaultbrowserpagereloadbehaviorandallowyoutodefinearoutingstructurewithinit,whichwillmakeitlookandfeellikeactualpagenavigation.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/6214.
GettingreadySupposeyouhavethefollowingfunctiondefinedglobally:
functionvisit(uri){
//Forthisrecipe,youdon'tcareaboutthestateortitle
window.history.pushState(null,null,uri);
}
ThepurposeofthisistomerelyallowyoutonavigateinsidethebrowserfromJavaScriptusingtheHTML5HistoryAPI.
Howtodoit...InorderforAngulartosimulatepagenavigationinsidethebrowser,thereareseveralstepsyoumusttaketocreateanavigablesingle-pageapplication.
SettingthebaseURL
ThefirststepinconfiguringyourapplicationistospecifythebaseURL.ThisinstructsthebrowserwhatnetworkrequestsperformedwitharelativeURLshouldbeginwith.
AnytimethepagemakesarequestusingarelativeURL,whengeneratingthenetworkrequest,itwillusethecurrentdomainandthenappendtherelativeURL.ForrelativeURLs,aprepended"/"meansitwillalwaysusetherootdirectory.Iftheforwardslashisunavailable,therelativepathwillprependwhateverisspecifiedasthebasehref.AnyURLbehavior,includingrequestingstaticresources,anchorlinks,andthehistoryAPI,willexhibitthisbehavior.
Note
<base>isnotapartofAngularbutratheradefaultHTML5element.
Herearesomeexamplesofthis:
[Example1]
//<basehref="/">
//initialpagelocation:foo.com
visit('bar');
//newpagelocation:foo.com/bar
visit('bar');
//newpagelocation:foo.com/bar
//Thebrowserrecognizesthatthisisarelativepath
//withnoprepended/andsoitwillvisitthepageatthe
//same"depth"asbefore.
visit('bar/');
//newpagelocation:foo.com/bar/
//Sameasbefore,butthetrailingslashwillbeimportantonce
//youinvokethisagain.
visit('bar/');
//newpagelocation:foo.com/bar/bar/
//ThebrowserrecognizesthattheURLendswitha/,andso
//visitingarelativepathistreatedasanavigationintoa
//subpath
visit('/qux');
//newpagelocation:foo.com/qux
//Witha/prependedtotheURL,thebrowserrecognizesthatit
//shouldnavigatefromtherootdomain
[Example2]
//<basehref="xyz/">
//initialpagelocation:foo.com
visit('bar');
//newpagelocation:foo.com/xyz/bar
//BaseURLisprependedtotherelativeURL
visit('bar');
//newpagelocation:foo.com/xyz/bar
//Aswasthecasebefore,thelocalpathistreatedthesame
//bythebrowser
visit('/qux');
//newpagelocation:foo.com/qux
//Notethatinthiscase,youspecifiedarelativepath
//originatingfromtherootdomain,sothebasehrefisignored
Definingroutes
Next,youneedtodefinewhatyourapplication'sroutesare.Forthepurposeofthisrecipe,itismoreimportanttounderstandthesetupofroutingthanhowtodefineandnavigatebetweenroutes.So,fornow,youwilljustdefineasinglecatchallroute.
Asyoumightsuspect,routeviewsinAngular2aredefinedascomponents.Eachroutepathisrepresentedattheveryleastbythestringthatthebrowser'slocationwillmatchagainstandthecomponentthatitwillmapto.ThiscanbedonewithanobjectimplementingtheRoutesinterface,whichisanarrayofroutedefinitions.
Itmakessensethattheroutedefinitionsshouldhappenveryearlyintheapplicationinitialization,soyou'lldoitinsidethetop-levelmoduledefinition.
First,createyourviewcomponentthatthisroutewillmapto:
[app/default.component.ts]
import{Component}from'@angular/core';
@Component({
template:'Defaultcomponent!'
})
exportclassDefaultComponent{}
Next,whereveryourapplicationmoduleisdefined,importRouterModuleandtheRoutesinterface,namelyDefaultComponent,anddefineacatchallrouteinsidetheRoutesarray:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{DefaultComponent}from'./default.component';
constappRoutes:Routes=[
{path:'**',component:DefaultComponent}
];
@NgModule({
imports:[
BrowserModule
],
declarations:[
DefaultComponent,
RootComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Providingroutestotheapplication
You'vedefinedtheroutesinanobject,butyourapplicationstillisnotawarethattheyexist.YoucandothiswiththeforRootmethoddefinedinRouterModule.Thisfunctiondoesallthedirtyworkofinstallingyourroutesintheapplicationaswellaspassingalonganumberofroutingprovidersforuseelsewhereintheapplication:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{DefaultComponent}from'./default.component';
constappRoutes:Routes=[
{path:'**',component:DefaultComponent}
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
DefaultComponent,
RootComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Withthis,yourapplicationisfullyconfiguredtounderstandtherouteyouhavedefined.
RenderingroutecomponentswithRouterOutlet
Thecomponentneedsaplacetoberendered,andinAngular2,thistakestheformofaRouterOutlettag.Thisdirectivewillbetargetedbythecomponentattachedtotheactiveroute,andthecomponentwillberenderedinsideit.Tokeepthingssimple,inthisrecipe,youcanusethedirectiveinsidetherootapplicationcomponent:
[app/app.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<router-outlet></router-outlet>
`
})
exportclassRootComponent{}
That'sall!YourapplicationnowhasasingleroutedefinedthatwillrenderDefaultComponentinsideRootComponent.
Howitworks...Thisrecipedoesn'tshowaverycomplicatedexampleofrouting,sinceeverypossibleroutethatyoucanvisitwillleadyoutothesamecomponent.
Nonetheless,itdemonstratesseveralfundamentalprinciplesofAngularrouting:
Initsmostbasicform,arouteiscomprisedofastringpath(matchedagainstthebrowserpath)andthecomponentthatshouldberenderedwhenthisrouteisactive.RoutesareinstalledviaRouterModule.Inthisexample,sincethereisonlyonemodule,youcandothisonceusingforRoot().However,keepinmindthatyoucanbreakyourroutingstructureintopiecesandbetweendifferentNgModules.Navigatingtoaroutewillcauseacomponenttoberenderedinsideadifferentcomponent-morespecifically,whereverthe<router-outlet>tagexists.Therearemanywaysinwhichthiscanbeconfiguredandmademorecomplex,butforthepurposeofthissimplemodule,youdon'tneedtoworryaboutthesedifferentways.
There'smore...Angular2applicationswillnotraiseissueswhenoperatingwithnoformofrouting.IfyourapplicationdoesnotneedtounderstandandmanagethepageURL,thenfeelfreetototallydiscardtheroutingfilesandmodulesfromyourapplication.
Initialpageload
Theflowyouarehopingyouruserswouldgothroughisasfollows:
1. Theuservisitshttp://www.foo.com/.2. Theservermatchestheemptyroutetoindex.html.3. Thepageloads,requestingstaticfiles.4. TheAngularstaticfilesareloadedandapplicationisbootstrapped.5. Theuserclicksonthelinksandnavigatesaroundthesite.6. SinceAngulariswhollymanagingthenavigationandrouting,everythingworksasexpected.
Thisistheidealcase.Consideradifferentcase:
1. Theuserhasalreadyvisitedhttp://www.foo.com/beforeandbookmarkedit.2. Theuserentersfoo.com/barintheirURLbarandnavigatestoitfromtheredirectly.3. Theserverseestherequestpathas/barandtriestohandletherequest.
Dependingonhowyourserverisconfigured,thismightcauseproblemsforyou.Thisisbecausethelasttimetheuservisitedfoo.com/bar,norequestforthatresourcereachedtheserverbecauseAngularwasonlyemulatingarealnavigationevent.
Thisscenarioisdiscussedelsewhereinthischapter,butkeepinmindthatwithoutacorrectlyconfiguredserver,theuserinthesecondcasemightseea404pageerrorinsteadofyourapplication.
SeealsoNavigatingwithrouterLinksdemonstrateshowtonavigatearoundAngularapplicationsNavigatingwiththeRouterserviceusesanAngularservicetonavigatearoundanapplicationBuildingstatefulRouterLinkbehaviorwithRouterLinkActiveshowshowtointegrateapplicationbehaviorwithaURLstate
NavigatingwithrouterLinksNavigatingaroundasinglepageapplicationisafundamentaltask,andAngularoffersyouabuilt-indirective,routerLink,toaccomplishthis.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/9983/.
GettingreadyBeginwiththeapplicationsetupassembledintheSettingupanapplicationtosupportsimpleroutesrecipe.
Yourgoalistoaddanadditionalroutetothisapplicationaccompaniedbyacomponent;also,youwanttobeabletonavigatebetweenthemusinglinks.
Howtodoit...Tobegin,createanothercomponent,ArticleComponent,andanassociatedroute:
[app/article/article.component.ts]
import{Component}from'@angular/core';
@Component({
template:'Articlecomponent!'
})
exportclassArticleComponent{}
Next,installanarticlerouteaccompaniedbythisnewcomponent:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{DefaultComponent}from'./default.component';
import{ArticleComponent}from'./article.component';
constappRoutes:Routes=[
{path:'article',component:ArticleComponent},
{path:'**',component:DefaultComponent}
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
DefaultComponent,
ArticleComponent,
RootComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Withtheroutesdefined,youcannowbuildarudimentarynavbarcomprisedofrouterLinks.Themarkupsurroundingthe<router-outlet>tagwillremainirrespectiveoftheroute,sotherootappcomponentseemslikeasuitableplaceforthenavlinks.
TherouterLinkdirectiveisavailableaspartofRouterModule,soyoucangostraighttoadding
someanchortags:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<a[routerLink]="''">Default</a>
<a[routerLink]="'article'">Article</a>
<router-outlet></router-outlet>
`
})
exportclassRootComponent{}
Inthiscase,sincetheroutesaresimpleandstatic,bindingrouterLinktoastringisallowed.routerLinkalsoacceptsthearraynotation:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<a[routerLink]="['']">Default</a>
<a[routerLink]="['article']">Article</a>
<router-outlet></router-outlet>
`
})
exportclassRootComponent{}
Tip
Forthepurposeofthisrecipe,thearraynotationdoesn'taddanything.However,whendevelopingmorecomplicatedURLstructures,thearraynotationbecomesuseful,asitallowsyoutogeneratelinksinapiecewisefashion.
Howitworks...Atahighlevel,thisisnodifferentthanthebehaviorofavanillahrefattribute.Afterall,theroutesbehaveinthesamewayandarestructuredsimilarly.TheimportantdifferencehereisthatusingarouterLinkdirectiveinsteadofhrefallowsyoutomovearoundyourapplicationtheAngularway,withouteverhavingtheanchortagclickinterpretedbythebrowserasanon-Angularnavigationevent.
There'smore...Ofcourse,therouterLinkdirectiveisalsosuperiorasitismoreextensibleasatoolfornavigating.SinceitisanHTMLattributeafterall,there'snoreasonrouterLinkcan'tbeattachedto,forexample,abuttoninstead:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<button[routerLink]="['']">Default</button>
<button[routerLink]="['article']">Article</button>
<router-outlet></router-outlet>
`
})
exportclassRootComponent{}
What'smore,you'llalsonotethatthearraynotationallowsthedynamicgenerationoflinksviaallofthetremendousdatabindingthatAngularaffordsyou:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<a[routerLink]="[defaultPath]">Default</a>
<a[routerLink]="[articlePath]">Article</a>
<router-outlet></router-outlet>
`
})
exportclassRootComponent{
defaultPath:string='';
articlePath:string='article';
}
AstheURLstructuregetsevermoreadvanced,itwillbeeasytoseehowacleverapplicationofdatabindingcouldmakeforsomeveryelegantdynamiclinkgeneration.
Routeorderconsiderations
TheorderingofroutesinsidetheRoutesdefinitionspecifiesthedescendingpriorityofeachofthem.Inthisrecipe'sexample,supposeyouweretoreversetheorderoftheroutes:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{DefaultComponent}from'./default.component';
import{ArticleComponent}from'./article.component';
constappRoutes:Routes=[
{path:'**',component:DefaultComponent},
{path:'article',component:ArticleComponent}
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
DefaultComponent,
ArticleComponent,
RootComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Whenexperimenting,youwillfindthatthebrowser'sURLchangescorrectlywiththevariousrouterLinkinteractions,butbothrouteswilluseDefaultComponentastherenderedview.Thisissimplybecausealltheroutesmatchthe**catchall,andAngulardoesn'tbothertotraversetheroutesanyfurtheronceithasamatchingroute.Keepthisinmindwhenauthoringlargeroutetables.
SeealsoSettingupanapplicationtosupportsimpleroutesshowsyouthebasicsofAngularroutingNavigatingwiththeRouterserviceusesanAngularservicetonavigatearoundanapplicationBuildingstatefulRouterLinkbehaviorwithRouterLinkActiveshowshowtointegrateapplicationbehaviorwithaURLstateImplementingnestedviewswithrouteparametersandchildroutesgivesanexampleofhowtoconfigureAngularURLstosupportnestinganddatapassingWorkingwithmatrixURLparametersandroutingarraysdemonstratesAngular'sbuilt-inmatrixURLsupport
NavigatingwiththeRouterserviceThecompaniontousingrouterLinkinsidethetemplatetonavigateisdoingitfrominsideJavaScript.Angularexposesthenavigate()methodfrominsideaservice,whichallowsyoutoaccomplishexactlythis.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/8004/.
GettingreadyBeginwiththeapplicationthatexistsattheendoftheHowtodoit...sectionoftheNavigatingwithrouterLinksrecipe.
Yourgoalistoaddanadditionalrouteaccompaniedbyacomponenttothisapplication;also,youwishtobeabletonavigatebetweenthemusinglinks.
Howtodoit...InsteadofusingrouterLink,whichisthemostsensiblechoiceinthissituation,youcanalsotriggeranavigationusingtheRouterservice.First,addnavbuttonsandattachsomeemptyclickhandlerstothem:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<button(click)="visitDefault()">Default</button>
<button(click)="visitArticle()">Article</button>
<router-outlet></router-outlet>
`
})
exportclassRootComponent{
visitDefault():void{}
visitArticle():void{}
}
Next,importtheRouterserviceanduseitsnavigate()methodtochangethepagelocation:
[app/root.component.ts]
import{Component}from'@angular/core';
import{Router}from'@angular/router';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<button(click)="visitDefault()">Default</button>
<button(click)="visitArticle()">Article</button>
<router-outlet></router-outlet>
`
})
exportclassRootComponent{
constructor(privaterouter:Router){}
visitDefault():void{
this.router.navigate(['']);
}
visitArticle():void{
this.router.navigate(['article']);
}
}
Withthisaddition,youshouldbeabletonavigatearoundyourapplicationinthesamewayyou
didbefore.
Howitworks...TheRouterserviceexposesanAPIwithwhichyoucancontrolyourapplication'snavigationbehavior,amongmanyotherthings.Itsnavigate()methodacceptsanarray-structuredroute,whichoperatesidenticallytotheArraysboundtorouterLink.
There'smore...Obviously,thisisanutterantipatternforbuildingapplicationsthataredesignedtoscale.Inthisscenario,routerLinkisamuchmoresuccinctandeffectivechoiceforbuildingasimplenavbar.Nevertheless,theRouterserviceisanequallyeffectivetoolfortraversinganAngularapplication'sroutestructure.
SeealsoNavigatingwithrouterLinksdemonstrateshowtonavigatearoundAngularapplicationsBuildingstatefulRouterLinkbehaviorwithRouterLinkActiveshowshowtointegrateapplicationbehaviorwithaURLstateWorkingwithmatrixURLparametersandroutingarraysdemonstratesAngular'sbuilt-inmatrixURLsupportAddingrouteauthenticationcontrolswithrouteguardsdetailstheentireprocessofconfiguringprotectedroutesinyourapplication
SelectingaLocationStrategyforpathconstructionAsimplebutimportantchoiceforyourapplicationiswhichtypeofLocationStrategyyouwanttomakeuseof.ThefollowingtwoURLsareequivalentwhentheirrespectiveLocationStrategyisselected:
PathLocationStrategy:foo.com/barHashLocationStrategy:foo.com/#/bar
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/1355/.
Howtodoit...Angular2willdefaulttoPathLocationStrategy.ShouldyouwanttoselectHashLocationStrategy,itcanbeimportedfromthe@angular/commonmodule.Onceimported,itcanbelistedasaproviderinsideanobjectliteral:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{DefaultComponent}from'./default.component';
import{ArticleComponent}from'./article.component';
import{LocationStrategy,HashLocationStrategy}
from'@angular/common';
constappRoutes:Routes=[
{path:'article',component:ArticleComponent},
{path:'**',component:DefaultComponent},
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
DefaultComponent,
ArticleComponent,
RootComponent
],
providers:[
{provide:LocationStrategy,useClass:HashLocationStrategy}
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Withthisaddition,yourapplicationwilltransitiontoprefix#/toallapplication-definedURLs.Thiswilloccurintransparencewiththerestofyourapplication,whichcanuseitsroutingdefinitionsnormallywithouthavingtoworryaboutprefixing#/.
There'smore...Therearetradeoffsforeachofthesestrategies.AstheAngulardocumentationnotes,onceyouchooseone,itisinadvisabletoswitchtotheothersincebookmarks,SEO,anduserhistorywillallbecoupledtotheURLstrategyutilizedduringthatvisit.
PathLocationStrategy:
Here,theURLsappearnormaltotheenduserTheservermustbeconfiguredtohandlepageloadsfromanyapplicationpathThisallowsthehybridserver-siderenderingofroutesforimprovedperformance
HashLocationStrategy:
Here,theURLsmaylookfunnytotheenduser.Noserverconfigurationisrequirediftherootdomainservesindex.html.Thisisagoodoptionifyouonlywanttoservestaticfiles(forexample,anAmazonAWS-basedsite).Itcannotbeeasilyintermixedwithhybridserver-siderendering.
ConfiguringyourapplicationserverforPathLocationStrategy
Angularissmartenoughtorecognizethebrowserstateandmanageitaccordinglyoncebootstrappingoccurs.However,bootstrappingrequiresaninitialloadofthestaticcompiledJSassets,whichwillbootstrapAngularoncethebrowserloadsthem.Whentheuserinitiallyvisitsarootdomain,suchasfoo.com,theserverisnormallyconfiguredtorespondwithindex.html,whichwillinturnrequestthestaticassetsatrendertime.So,Angularwillwork!
However,incaseswheretheuserinitiallyvisitsanon-rootpath,suchasfoo.com/bar,thebrowserwillsendarequesttotheserveratfoo.com/bar.Ifyouaren'tcarefulwhensettingupyourserver,acommonmistakeyoumaycommitishavingonlytherootfoo.compathreturnindex.html.
InorderforPathLocationStrategytoworkcorrectlyinallcases,youmustconfigureyourwebservertosetupacatchallrouteforalltherequeststhathavepathsintendedforthesingle-pageapplication'srouteintheclient,andtoinvariablyreturnindex.html.Inotherwords,visitingfoo.com,foo.com/bar,orfoo.com/bar/bazasthefirstpageinthebrowserwillallreturnthesamething:index.html.Onceyoudothis,postbootstrapAngularwillexaminethecurrentbrowserpathandrecognizewhichpathitisonandwhatviewneedstobedisplayed.
BuildingstatefulroutebehaviorwithRouterLinkActiveItisoftenthecasewhenbuildingapplicationsthatyouwillwanttobuildfeaturesthatwouldinvolvewhichpagetheapplicationiscurrentlyon.Whenthisisaone-timeinspection,itisn'taproblem,asbothAngularanddefaultbrowserAPIsallowyoutoeasilyinspectthecurrentpage.
ThingsgetabitstickierwhenyouwantthestateofthepagetoreflectthestateoftheURL,forexample,ifyouwanttovisuallyindicatewhichlinkcorrespondstothecurrentpage.Afrom-scratchimplementationofthiswouldrequiresomesortofstatemachinethatwouldknowwhennavigationeventsoccurandwhatandhowtomodifyateachgivenroute.
Fortunately,Angular2givesyousomeexcellenttoolstodothisrightoutofthebox.
Note
Thecode,links,andaliveexampleofthisareallavailableathttp://ngcookbook.herokuapp.com/3308/.
GettingreadyBeginwiththeArrayandanchor-tag-basedimplementationshownintheNavigatingwithrouterLinksrecipe.
YourgoalistouseRouterLinkActivetointroducesomesimplestatefulroutebehavior.
Howtodoit...RouterLinkActiveallowsyoutoconditionallyapplyclasseswhenthecurrentroutematchesthecorrespondingrouterLinkonthesameelement.ProceeddirectlytoaddingitasanattributedirectivetoeachlinkaswellasamatchingCSSclass:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<a[routerLink]="['']"
[routerLinkActive]="'active-navlink'">Default</a>
<a[routerLink]="['article']"
[routerLinkActive]="'active-navlink'">Article</a>
<router-outlet></router-outlet>
`,
styles:[`
.active-navlink{
color:red;
text-transform:uppercase;
}
`]
})
exportclassRootComponent{}
Thisisallyouneedforlinkstobecomeactive!YouwillnoticethatAngularwillconditionallyapplytheactive-navlinkclassbasedonthecurrentroute.
However,whentestingthis,youwillnoticethatthe/articleroutemakesboththelinksappearactive.Thisisduetothefactthatbydefault,AngularmarksallrouterLinksthatmatchthecurrentrouteasactive.
Note
Thisbehaviorisusefulincaseswhereyoumaywanttoshowahierarchyoflinksasactiveforexample,atroute/user/123/detail,itcouldmakesensethattheseparatelinks/user,/user/123,and/user/123/detailareallshownasactive.
However,inthecaseofthisrecipe,thisbehaviorisnotusefultoyou,andAngularhasanotherrouterdirective,routerLinkActiveOptions,whichbindstoanoptionsobject.Theexactpropertyinsidetheoptionsobjectisusefulinthiscase;itcontrolswhethertheactivestateshouldonlybeappliedincasesofanexactmatch:
[app/root.component.ts]
import{Component}from'@angular/core';
import{Router}from'@angular/router';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<a[routerLink]="['']"
[routerLinkActive]="'active-navlink'"
[routerLinkActiveOptions]="{exact:true}">Default</a>
<a[routerLink]="['article']"
[routerLinkActive]="'active-navlink'"
[routerLinkActiveOptions]="{exact:true}">Article</a>
<router-outlet></router-outlet>
`,
styles:[`
.active-navlink{
color:red;
text-transform:uppercase;
}
`]
})
exportclassRootComponent{}
Nowyouwillfindthateachlinkwillonlybeactiveatitsrespectiveroute.
Howitworks...TherouterLinkActiveimplementationsubscribestonavigationchangeeventsthatAngularemitsfromtheRouterservice.WhenitseesaNavigationEndevent,itperformsanupdateofalltheattachedHTMLtags,whichincludesaddingandstrippingapplicable"active"CSSclassesthattheelementisboundtoviathedirective.
There'smore...IfyouneedtobindrouterLinkActivetoadynamicvalue,theprecedingsyntaxwillallowyoutodoexactlythat.Forexample,youcanbindtoa
component
memberand
modify
itelsewhere,andAngularwillhandleeverythingforyou.However,ifthisisnotrequired,AngularwillhandlerouterLinkActivewithoutthedatabindingbrackets.Inthiscase,thevalueofthedirectivenolongerneedstobeanAngularexpression,soyoucanremovethenestedquotes.
Thefollowingisbehaviorallyidentical:
[app/root.component.ts]
import{Component}from'@angular/core';
import{Router}from'@angular/router';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<a[routerLink]="['']"
routerLinkActive="active-navlink"
[routerLinkActiveOptions]="{exact:true}">
Default</a>
<a[routerLink]="['article']"
routerLinkActive="active-navlink"
[routerLinkActiveOptions]="{exact:true}">
Article</a>
<router-outlet></router-outlet>
`,
styles:[`
.active-navlink{
color:red;
text-transform:uppercase;
}
`]
})
exportclassRootComponent{}
SeealsoSettingupanapplicationtosupportsimpleroutesshowsyouthebasicsofAngularroutingNavigatingwithrouterLinksdemonstrateshowtonavigatearoundAngularapplicationsBuildingstatefulRouterLinkbehaviorwithRouterLinkActiveshowshowtointegrateapplicationbehaviorwithaURLstateImplementingnestedviewswithrouteparametersandchildroutesgivesanexampleofhowtoconfigureAngularURLstosupportnestinganddatapassingAddingrouteauthenticationcontrolswithrouteguardsdetailstheentireprocessofconfiguringprotectedroutesinyourapplication
ImplementingnestedviewswithrouteparametersandchildroutesAngular2'scomponentrouteroffersyouthenecessaryconceptofchildroutes.Asyoumightexpect,thisbringstheconceptofrecursivelydefinedviewstothetable,whichaffordsyouanincrediblyusefulandelegantwayofbuildingyourapplication.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/7892/.
GettingreadyBeginwiththeArrayandanchor-tag-basedimplementationshowninNavigatingwithrouterLinksrecipe.
Yourgoalistoextendthissimpleapplicationtoinclude/article,whichwillbethelistview,and/article/:id,whichwillbethedetailview.
Howtodoit...First,modifytheroutestructureforthissimpleapplicationbyextendingthe/articlepathtoincludeitssubpaths:/and/:id.Routesaredefinedhierarchically,andeachroutecanhavechildroutesusingthechildrenproperty.
Addingaroutingtargettotheparentcomponent
First,youmustmodifytheexistingArticleComponentsothatitcancontainchildviews.Asyoumightexpect,thechildviewisrenderedinexactlythesamewayasitisdonefromtherootcomponent,usingRouterOutlet:
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
template:`
<h2>Article</h2>
<router-outlet></router-outlet>
`
})
exportclassArticleComponent{}
Thiswon'tdoanythingyet,butaddingRouterOutletdescribestoAngularhowroutecomponenthierarchiesshouldberendered.
Definingnestedchildviews
Inthisrecipe,youwouldliketohavetheparentArticleComponentcontainachildview,eitherArticleListComponentorArticleDetailComponent.Forthesimplicityofthisrecipe,youcanjustdefineyourlistofarticlesasanarrayofintegers.
Definetheskeletonofthesetwocomponentsasfollows:
[app/article-list.component.ts]
import{Component}from'@angular/core';
@Component({
template:`
<h3>ArticleList</h3>
`
})
exportclassArticleListComponent{
articleIds:Array<number>=[1,2,3,4,5];
}
[app/article-detail.component.ts]
import{Component}from'@angular/core';
@Component({
template:`
<h3>ArticleDetail</h3>
<p>Showingarticle{{articleId}}</p>
`
})
exportclassArticleDetailComponent{
articleId:number;
}
Definingthechildroutes
Atthispoint,nothingintheapplicationyetpointstoeitherofthesechildroutes,soyou'llneedtodefinethemnow.
ThechildrenpropertyofarouteshouldjustbeanotherRoute,whichshouldrepresentthenestedroutesthatareappendedtotheparentroute.
Note
Inthisway,youaredefiningasortofrouting"tree,"whereeachrouteentrycanhavemanychildroutesdefinedrecursively.Thiswillbediscussedingreaterdetaillaterinthischapter.
Furthermore,youshouldalsousetheURLparameternotationtodeclare:articleIdasavariableintheroute.Thisallowsyoutopassvaluesinsidetherouteandthenretrievethesevaluesinsidethecomponentthatisrendered.
Addtheseroutedefinitionsnow:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{DefaultComponent}from'./default.component';
import{ArticleComponent}from'./article.component';
import{ArticleListComponent}from'./article-list.component';
import{ArticleDetailComponent}from'./article-detail.component';
constappRoutes:Routes=[
{path:'article',component:ArticleComponent,
children:[
{path:'',component:ArticleListComponent},
{path:':articleId',component:ArticleDetailComponent}
]
},
{path:'**',component:DefaultComponent},
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
DefaultComponent,
ArticleComponent,
ArticleListComponent,
ArticleDetailComponent,
RootComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
You'llnotethatArticleListComponentiskeyedbyanemptystring.Thisshouldmakesense,aseachoftheseroutesarejoinedtotheirparentroutestocreatethefullroute.Ifyouweretojoineachrouteinthistreewithitsancestralpathtogetthefullroute,theroutedefinitionyou'vejustcreatedwouldhavethefollowingthreeentries:
/article=>ArticleComponent
ArticleListComponent
/article/4=>ArticleComponent
ArticleDetailComponent<articleId=4>
/**=>DefaultComponent
Note
Notethatinthiscase,thenumberofactualroutescorrespondstothenumberofleavesoftheURLtreesincethearticleparentroutewillalsomaptothechildarticle's+''route.Dependingonhowyouconfigureyourroutestructure,theleaf/routeparitywillnotalwaysbethecase.
Definingchildviewlinks
Withtheroutesbeingmappedtothechildcomponents,youcanfleshoutthechildviews.StartingwithArticleList,createarepeatertogeneratethelinkstoeachofthechildviews:
[app/article-list.component.ts]
import{Component}from'@angular/core';
@Component({
template:`
<h3>ArticleList</h3>
<p*ngFor="letarticleIdofarticleIds">
<a[routerLink]="articleId">
Article{{articleId}}
</a>
</p>
`
})
exportclassArticleListComponent{
articleIds:Array<number>=[1,2,3,4,5];
}
Note
NotethatrouterLinkislinkingtotherelativepathofthedetailview.Sincethecurrentpathforthisviewis/article,arelativerouterLinkof4willnavigatetheapplicationto/article/4uponaclick.
Theselinksshouldwork,butwhenyouclickonthem,theywilltakeyoutothedetailviewthatcannotdisplayarticleIdfromtheroutesinceyouhavenotextractedityet.
InsideArticleDetailComponent,createalinkthatwilltaketheuserbacktothearticle/route.Sinceroutesbehavelikedirectories,youcanjustusearelativepaththatwilltaketheuseruponelevel:
[app/article-detail.component.ts]
import{Component}from'@angular/core';
@Component({
template:`
<h3>ArticleDetail</h3>
<p>Showingarticle{{articleId}}</p>
<a[routerLink]="'../'">Backup</a>
`
})
exportclassArticleDetailComponent{
articleId:number;
}
Extractingrouteparameters
AcrucialdifferencebetweenAngular1and2istherelianceonObservableconstructs.Inthecontextofrouting,Angular2wieldsObservablestoencapsulatethatroutingoccursasasequenceofeventsandthatvaluesareproducedatdifferentstatesintheseeventsandwillbereadyeventually.
Moreconcretely,routeparamsinAngular2arenotexposeddirectly,butratherthroughanObservableinsideActivatedRoute.YoucansetObserveronitsparamsObservabletoextracttherouteparamsoncetheyareavailable.
InjecttheActivatedRouteinterfaceandusetheparamsObservabletoextractarticleIdand
assignittotheArticleDetailComponentinstancemember:
[app/article-detail/article-detail.component.ts]
import{Component}from'@angular/core';
import{ActivatedRoute}from'@angular/router';
@Component({
template:`
<h3>ArticleDetail</h3>
<p>Showingarticle{{articleId}}</p>
<a[routerLink]="'../'">Backup</a>
`
})
exportclassArticleDetailComponent{
articleId:number;
constructor(privateactivatedRoute_:ActivatedRoute){
activatedRoute_.params
.subscribe(params=>this.articleId=params['articleId']);
}
}
Withthis,youshouldbeabletoseethearticleIdparameterinterpolatedintoArticleDetailComponent.
Howitworks...Inthisapplication,youhavenestedcomponents,AppComponentandArticleComponent,bothofwhichcontainRouterOutlet.Angularisabletotaketheroutinghierarchyyoudefinedandapplyittothecomponenthierarchythatitmapsto.Morespecifically,foreveryRouteyoudefineinyourroutinghierarchy,thereshouldbeanequalnumberofRouterOutletsinwhichtheycanrender.
There'smore...Tosome,itwillfeelstrangetoneedtoextracttherouteparamsfromanObservableinterface.Ifthissolutionfeelsabitclunkytoyou,therearewaysoftidyingitup.
Refactoringwithasyncpipes
RecallthatAngularhastheabilitytointerpolateObservabledatadirectlyintothetemplateasitbecomesready.EspeciallysinceyoushouldonlyeverexpecttheparamObservabletoemitonce,youcanuseittoinsertarticleIdintothetemplatewithoutexplicitlysettinganObserver:
[app/article-detail.component.ts]
import{Component}from'@angular/core';
import{ActivatedRoute}from'@angular/router';
@Component({
template:`
<h3>ArticleDetail</h3>
<p>Showingarticle
{{(activatedRoute.params|async).articleId}}</p>
<a[routerLink]="'../'">Backup</a>
`
})
exportclassArticleDetailComponent{
constructor(activatedRoute:ActivatedRoute){}
}
Eventhoughthisworksperfectlywell,usingaprivatereferencetoaninjectedservicedirectlyintothetemplatemayfeelabitfunnytoyou.AsuperiorstrategyistograbareferencetothepublicObservableinterfaceyouneedandinterpolatethatinstead:
[app/article-detail.component.ts]
import{Component}from'@angular/core';
import{Observable}from'rxjs/Observable';
import{ActivatedRoute,Params}from'@angular/router';
@Component({
template:`
<h3>ArticleDetail</h3>
<p>Showingarticle{{(params|async).articleId}}</p>
<a[routerLink]="'../'">Backup</a>
`
})
exportclassArticleDetailComponent{
params:Observable<Params>;
constructor(privateactivatedRoute_:ActivatedRoute){
this.params=activatedRoute_.params;
}
}
SeealsoNavigatingwithrouterLinksdemonstrateshowtonavigatearoundAngularapplicationsNavigatingwiththeRouterserviceusesanAngularservicetonavigatearoundanapplicationBuildingstatefulRouterLinkbehaviorwithRouterLinkActiveshowshowtointegrateapplicationbehaviorwithaURLstateWorkingwithmatrixURLparametersandroutingarraysdemonstratesAngular'sbuilt-inmatrixURLsupportAddingrouteauthenticationcontrolswithrouteguardsdetailstheentireprocessofconfiguringprotectedroutesinyourapplication
WorkingwithmatrixURLparametersandroutingarraysAngular2introducesnativesupportforanawesomefeaturethatseemstobefrequentlyoverlooked:matrixURLparameters.Essentially,theseallowyoutoattachanarbitraryamountofdatainsideaURLtoanyroutinglevelinAngular,andgivingyoutheabilitytoreadthatdataoutasaregularURLparameter.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/4553/.
GettingreadyBeginwiththecodecreatedattheendoftheHowtodoit...sectioninImplementingnestedviewswithrouteparametersandchildroutes.
YourgoalistopassarbitrarydatatoboththeArticleListandArticleDetaillevelsofthisapplicationviaonlytheURL.
Howtodoit...routerLinkarraysareprocessedserially,soanystringthatwillbecomepartoftheURLthatisfollowedbyanobjectwillhavethatobjectconvertedintomatrixURLparameters.Itwillbeeasiertounderstandthisbyexample,sobeginbypassinginsomedummydatatotheArticleListviewfromrouterLink:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<a[routerLink]="['']">
Default</a>
<a[routerLink]="['article',{listData:'foo'}]">
Article</a>
<router-outlet></router-outlet>
`
})
exportclassRootComponent{}
Now,ifyouclickonthislink,youwillseeyourbrowsernavigatetothefollowingpathwhilestillsuccessfullyrenderingtheArticleListview:
/article;listData=foo
Toaccessthisdata,simplyextractitfromtheActivatedRouteparams:
[app/article-list.component.ts]
import{Component}from'@angular/core';
import{ActivatedRoute}from'@angular/router';
@Component({
template:`
<h3>ArticleList</h3>
<p*ngFor="letarticleIdofarticleIds">
<a[routerLink]="[articleId]">
Article{{articleId}}
</a>
</p>
`
})
exportclassArticleListComponent{
articleIds:Array<number>=[1,2,3,4,5];
constructor(privateactivatedRoute_:ActivatedRoute){
activatedRoute_.params
.subscribe(params=>{
console.log('Listparams:');
console.log(window.location.href)
console.log(params);
});
}
}
Whentheviewisloaded,you'llseethefollowing:
Listparams:
/article;listData=foo
Object{listData:"foo"}
Awesome!Dothesameforthedetailview:
[app/article-list.component.ts]
import{Component}from'@angular/core';
import{ActivatedRoute}from'@angular/router';
@Component({
template:`
<h3>ArticleList</h3>
<p*ngFor="letarticleIdofarticleIds">
<a[routerLink]="[articleId,{detailData:'bar'}]">
Article{{articleId}}
</a>
</p>
`
})
exportclassArticleListComponent{
articleIds:Array<number>=[1,2,3,4,5];
constructor(privateactivatedRoute_:ActivatedRoute){
activatedRoute_.params
.subscribe(params=>{
console.log('Listparams:');
console.log(window.location.href)
console.log(params);
});
}
}
Addthesameamountofloggingtothedetailview:
[app/article-detail.component.ts]
import{Component}from'@angular/core';
import{ActivatedRoute}from'@angular/router';
@Component({
template:`
<h3>ArticleDetail</h3>
<p>Showingarticle{{articleId}}</p>
<a[routerLink]="'../'">Backup</a>
`
})
exportclassArticleDetailComponent{
articleId:number;
constructor(privateactivatedRoute_:ActivatedRoute){
activatedRoute_.params
.subscribe(params=>{
console.log('Detailparams:');
console.log(window.location.href)
console.log(params);
this.articleId=params['articleId']
});
}
}
Whenyouvisitadetailpage,you'llseethefollowinglogged:
Detailparams:
/article;listData=foo/1;detailData=bar
Object{articleId:"1",detailData:"foo"}
Veryinteresting!NotonlyisAngularabletoassociatedifferentmatrixparameterswithdifferentroutinglevels,butithascombinedboththeexpectedarticleIdparameterandtheunexpecteddetailDataparameterintothesameObservableemission.
Howitworks...AngularisabletoseamlesslyconvertfromaroutingarraycontainingamatrixparamobjecttoaserializedURLcontainingthematrixparams,thenbackintoadeserializedJavaScriptobjectcontainingtheparameterdata.ThisallowsyoutostorearbitrarydatainsideURLsatdifferentlevels,withouthavingtocramitallintoaquerystringattheend.
There'smore...NoticethatwhenyouclickonBackupinthedetailview,thelistDataURLparamispreserved.Angularwilldutifullymaintainthestateasyounavigatethroughouttheapplication,sousingmatrixparameterscanbeaveryeffectivewayofstoringstatefuldatathatsurvivesnavigationorpagereloads.
SeealsoNavigatingwithrouterLinksdemonstrateshowtonavigatearoundAngularapplicationsNavigatingwiththeRouterserviceusesanAngularservicetonavigatearoundanapplicationBuildingstatefulRouterLinkbehaviorwithRouterLinkActiveshowshowtointegrateapplicationbehaviorwithaURLstateImplementingnestedviewswithrouteparametersandchildroutesgivesanexampleofhowtoconfigureAngularURLstosupportnestinganddatapassing
AddingrouteauthenticationcontrolswithrouteguardsThenatureofsingle-pageapplicationswhollycontrollingtheprocessofroutingaffordsthemtheabilitytocontroleachstageoftheprocess.Foryou,thismeansthatyoucaninterceptroutechangesastheyhappenandmakedecisionsaboutwheretheusershouldgo.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/6135/.
GettingreadyInthisrecipe,you'llbuildasimplepseudo-authenticatedapplicationfromscratch.
Yougoalistoprotectusersfromcertainviewswhentheyarenotauthenticated,andatthesametime,implementasensiblelogin/logoutflow.
Howtodoit...Beginbydefiningtwoinitialviewswithroutesinyourapplication.OnewillbeaDefaultview,whichwillbevisibletoeverybody,andonewillbeaProfileview,whichwillbeonlyvisibletoauthenticatedusers:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{DefaultComponent}from'./default.component';
import{ProfileComponent}from'./profile.component';
constappRoutes:Routes=[
{path:'profile',component:ProfileComponent},
{path:'**',component:DefaultComponent}
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
DefaultComponent,
ProfileComponent,
RootComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
[app/default.component.ts]
import{Component}from'@angular/core';
@Component({
template:`
<h2>Defaultview!</h2>
`
})
exportclassDefaultComponent{}
[app/profile.component.ts]
import{Component}from'@angular/core';
@Component({
template:`
<h2>Profileview</h2>
Username:<input>
<button>Update</button>
`
})
exportclassProfileComponent{}
Obviously,thisdoesnotdoanythingyet.
ImplementingtheAuthservice
AsdoneintheObservableschapter,youwillimplementaservicethatwillmaintainthestateentirelywithinaBehaviorSubject.
Note
RecallthataBehaviorSubjectwillrebroadcastitslastemittedvaluewheneveranObserverissubscribedtoit.Thismeansitrequiressettingtheinitialstate,butforanauthenticationservicethisiseasy;itcanjuststartintheunauthenticatedstate.
Forthepurposeofthisrecipe,let'sassumethatausernameofnullmeanstheuserisnotauthenticatedandanyotherstringvaluemeanstheyareauthenticated:
[app/auth.service.ts]
import{Injectable}from'@angular/core';
import{BehaviorSubject}from'rxjs/BehaviorSubject';
import{Observable}from'rxjs/Observable';
@Injectable()
exportclassAuthService{
privateauthSubject_:BehaviorSubject<any>=
newBehaviorSubject(null);
usernameEmitter:Observable<string>;
constructor(){
this.usernameEmitter=this.authSubject_.asObservable();
this.logout();
}
login(username:string):void{
this.setAuthState_(username);
}
logout():void{
this.setAuthState_(null);
}
privatesetAuthState_(username:string):void{
this.authSubject_.next(username);
}
}
Notethatnowherearewestoringtheusernameasastring.ThestateoftheauthenticationlivesentirelywithinBehaviorSubject.
Wiringuptheprofileview
Next,makethisserviceavailabletotheentireapplicationandwireuptheprofileview:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{DefaultComponent}from'./default.component';
import{ProfileComponent}from'./profile.component';
import{AuthService}from'./auth.service';
constappRoutes:Routes=[
{path:'profile',component:ProfileComponent},
{path:'**',component:DefaultComponent}
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
DefaultComponent,
ProfileComponent,
RootComponent
],
providers:[
AuthService
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
[app/profile.component.ts]
import{Component}from'@angular/core';
import{AuthService}from'./auth.service';
import{Observable}from'rxjs/Observable';
@Component({
template:`
<h2>Profileview</h2>
Username:<input#unvalue="{{username|async}}">
<button(click)=update(un.value)>Update</button>
`
})
exportclassProfileComponent{
username:Observable<string>;
constructor(privateauthService_:AuthService){
this.username=authService_.usernameEmitter;
}
update(username:string):void{
this.authService_.login(username);
}
}
Tip
It'sveryhandytousetheasyncpipewheninterpolatingvalues.Recallthatwhenyouinvokesubscribe()onaserviceObservablefrominsideaninstantiatedviewcomponent,youmustinvokeunsubscribe()ontheSubscriptionwhenthecomponentisdestroyed;otherwise,yourapplicationwillhavealeakedlistener.MakingtheObservableavailabletotheviewsavesyouthistrouble!
Withtheprofileviewwiredup,addlinksandinterpolatetheusernameintotherootappviewinanavbar,togiveyourselftheabilitytonavigatearound.Youdon'thavetorevisitthefile;justaddallthelinksyou'llneedinthisrecipenow:
[app/root.component.ts]
import{Component}from'@angular/core';
import{Router}from'@angular/router';
import{AuthService}from'./auth.service';
import{Observable}from'rxjs/Observable';
@Component({
selector:'root',
template:`
<h3*ngIf="!!(username|async)">
Hello,{{username|async}}.
</h3>
<a[routerLink]="['']">Default</a>
<a[routerLink]="['profile']">Profile</a>
<a*ngIf="!!(username|async)"
[routerLink]="['login']">Login</a>
<a*ngIf="!!(username|async)"
[routerLink]="['logout']">Logout</a>
<router-outlet></router-outlet>
`
})
exportclassRootComponent{
username:Observable<string>;
constructor(privateauthService_:AuthService){
this.username=authService_.usernameEmitter;
}
}
Tip
Forconsistency,hereyouareusingtheasyncpipetomakethecomponentdefinitionsimpler.However,sinceyouhavefourinstancesinthetemplatereferencingthesameObservable,itmightbebetterdowntheroadtoinsteadsetonesubscribertoObservable,bindittoastringmemberinRootComponent,andinterpolatethisinstead.Angular'sdatabindingmakesthiseasyforyou,butyouwouldstillneedtoderegisterthesubscriberwhenthisisdestroyed.However,sinceitistheapplication'srootcomponent,youshouldn'treallyexpectthistohappen.
Restrictingrouteaccesswithrouteguards
Sofarsogood,butyouwillnoticethattheprofileviewisallowingtheusertoeffectivelyloginwilly-nilly.Youwouldinsteadliketorestrictaccesstothisviewandonlyallowtheusertovisititwhentheyarealreadyauthenticated.
Angulargivesyoutheabilitytoexecutecode,inspecttheroute,andredirectitasnecessarybeforethenavigationoccursusingaRouteGuard.
Note
Guardisabitofamisleadingtermhere.YoushouldthinkofthisfeatureasarouteshimthatletsyouaddlogicthatexecutesbeforeAngularactuallygoestothenewroute.Itcanindeed"Guard"aroutefromanunauthenticateduser,butitcanalsojustaseasilyconditionallyredirect,savethecurrentURL,orperformothertasks.
SincetheRouteGuardneedstohavethe@Injectabledecorator,itmakesgoodsensetotreatitasaservicetype.
StartoffwiththeskeletonAuthGuardServicedefinedinsideanewfileforrouteguards:
[app/route-guards.service.ts]
import{Injectable}from'@angular/core';
import{CanActivate}from'@angular/router';
@Injectable()
exportclassAuthGuardServiceimplementsCanActivate{
constructor(){}
canActivate(){
//Thismethodisinvokedduringroutechangesifthis
//classislistedintheRoutes
}
}
Beforehavingthisdoanything,importthemoduleandaddittoRoutes:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{DefaultComponent}from'./default.component';
import{ProfileComponent}from'./profile.component';
import{AuthService}from'./auth.service';
import{AuthGuardService}from'./route-guards.service';
constappRoutes:Routes=[
{
path:'profile',
component:ProfileComponent,
canActivate:[AuthGuardService]
},
{
path:'**',
component:DefaultComponent
}
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
DefaultComponent,
ProfileComponent,
RootComponent
],
providers:[
AuthService,
AuthGuardService
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Now,eachtimetheapplicationmatchesaroutetoaprofileandtriestonavigatethere,thecanActivatemethoddefinedinsideAuthGuardServicewillbecalled.Thereturnvalueoftruemeansthenavigationcanoccur;thereturnvalueoffalsemeansthenavigationiscancelled.
Tip
canActivatecaneitherreturnabooleanoranObservable<boolean>.Beaware,shouldyoureturnObservable,theapplicationwilldutifullywaitfortheObservabletoemitavalueandcompleteitbeforenavigating.
Sincetheapplication'sauthenticationstatelivesinsideBehaviorSubject,allthismethodneedstodoissubscribe,checktheusername,andnavigateifitisnotnull.ItsuitsthistoreturnObservable<boolean>:
[app/route-guards.service.ts]
import{Injectable}from'@angular/core';
import{CanActivate,Router}from'@angular/router';
import{AuthService}from'./auth.service';
import{Observable}from'rxjs/Observable';
@Injectable()
exportclassAuthGuardServiceimplementsCanActivate{
constructor(privateauthService_:AuthService,
privaterouter_:Router){}
canActivate():Observable<boolean>{
returnthis.authService_.usernameEmitter.map(username=>{
if(!username){
this.router_.navigate(['login']);
}else{
returntrue;
}
});
}
}
Onceyouimplementthis,youwillnoticethatthenavigationwillneveroccur,eventhoughtheserviceisemittingtheusernamecorrectly.ThisisbecausetherecipientofthereturnvalueofcanActivateisn'tjustwaitingforanObservableemission;itiswaitingfortheObservabletocomplete.SinceyoujustwanttopeekattheusernamevalueinsideBehaviorSubject,youcanjustreturnanewObservablethatreturnsonevalueandtheniscompletedusingtake():
[app/route-guards.service.ts]
import{Injectable}from'@angular/core';
import{CanActivate,Router}from'@angular/router';
import{AuthService}from'./auth.service';
import{Observable}from'rxjs/Observable';
import'rxjs/add/operator/take';
@Injectable()
exportclassAuthGuardServiceimplementsCanActivate{
constructor(privateauthService_:AuthService,
privaterouter_:Router){}
canActivate():Observable<Boolean>{
returnthis.authService_.usernameEmitter.map(username=>{
if(!username){
this.router_.navigate(['login']);
}else{
returntrue;
}
}).take(1);
}
}
Superb!However,thisapplicationstilllacksamethodtoformallyloginandlogout.
Addingloginbehavior
Sincetheloginpagewillneeditsownview,itshouldgetitsownrouteandcomponent.Oncetheuserlogsin,thereisnoneedtokeepthemontheloginpage,soyouwanttoredirectthemtothedefaultviewoncetheyaredone.
First,createthelogincomponentanditscorrespondingview:
[app/login.component.ts]
import{Component}from'@angular/core';
import{Router}from'@angular/router';
import{AuthService}from'./auth.service';
@Component({
template:`
<h2>Loginview</h2>
<input#un>
<button(click)="login(un.value)">Login</button>
`
})
exportclassLoginComponent{
constructor(privateauthService_:AuthService,
privaterouter_:Router){}
login(newUsername:string):void{
this.authService_.login(newUsername);
this.authService_.usernameEmitter
.subscribe(username=>{
if(!!username){
this.router_.navigate(['']);
}
});
}
}
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{DefaultComponent}from'./default.component';
import{ProfileComponent}from'./profile.component';
import{LoginComponent}from'./login.component';
import{AuthService}from'./auth.service';
import{AuthGuardService}from'./route-guards.service';
constappRoutes:Routes=[
{
path:'login',
component:LoginComponent
},
{
path:'profile',
component:ProfileComponent,
canActivate:[AuthGuardService]
},
{
path:'**',
component:DefaultComponent
}
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
LoginComponent,
DefaultComponent,
ProfileComponent,
RootComponent
],
providers:[
AuthService,
AuthGuardService
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Youshouldnowbeabletologin.Thisisallwellandgood,butyouwillnoticethatwiththisimplemented,updatingtheusernameintheprofileviewwillnavigatetothedefaultview,exhibitingthesamebehaviordefinedinthelogincomponent.ThisisbecausethesubscriberisstilllisteningtoAuthServiceObservable.YouneedtoaddinanOnDestroymethodtocorrectlyteardowntheloginview:
[app/login.component.ts]
import{Component,ngOnDestroy}from'@angular/core';
import{Router}from'@angular/router';
import{AuthService}from'./auth.service';
import{Subscription}from'rxjs/Subscription';
@Component({
template:`
<h2>Loginview</h2>
<input#un>
<button(click)="login(un.value)">Login</button>
`
})
exportclassLoginComponentimplementsOnDestroy{
privateusernameSubscription_:Subscription;
constructor(privateauthService_:AuthService,
privaterouter_:Router){}
login(newUsername:string):void{
this.authService_.login(newUsername);
this.usernameSubscription_=this.authService_
.usernameEmitter
.subscribe(username=>{
if(!!username){
this.router_.navigate(['']);
}
});
}
ngOnDestroy(){
//Onlyinvokeunsubscribe()ifthisexists
this.usernameSubscription_&&
this.usernameSubscription_.unsubscribe();
}
}
Addingthelogoutbehavior
Finally,youwanttoaddawayforuserstologout.Thiscanbeaccomplishedinanumberofways,butagoodimplementationwillbeabletodelegatethelogoutbehaviortoitsassociatedmethodswithoutintroducingtoomuchboilerplatecode.
Ideally,youwouldlikefortheapplicationtojustbeabletonavigatetothelogoutrouteandletAngularhandletherest.This,too,canbeaccomplishedwithcanActivate.First,defineanewRouteGuard:
[app/route-guards.service.ts]
import{Injectable}from'@angular/core';
import{CanActivate,Router}from'@angular/router';
import{AuthService}from'./auth.service';
import{Observable}from'rxjs/Observable';
import'rxjs/add/operator/take';
@Injectable()
exportclassAuthGuardServiceimplementsCanActivate{
constructor(privateauthService_:AuthService,
privaterouter_:Router){}
canActivate():Observable<boolean>{
returnthis.authService_.usernameEmitter.map(username=>{
if(!username){
this.router_.navigate(['login']);
}else{
returntrue;
}
}).take(1);
}
}
@Injectable()
exportclassLogoutGuardServiceimplementsCanActivate{
constructor(privateauthService_:AuthService,
privaterouter_:Router){}
canActivate():boolean{
this.authService_.logout();
this.router_.navigate(['']);
returntrue;
}
}
Thisbehaviorshouldbeprettyself-explanatory.
Tip
YourcanActivatemethodmustmatchthesignaturedefinedintheCanActivateinterface,soeventhoughitwillalwaysnavigatetoanewview,youshouldaddareturnvaluetopleasethecompilerandtohandleanycaseswheretheprecedingcodeshouldfallthrough.
Next,addthelogoutcomponentandtheroute.Thelogoutcomponentwillneverberendered,buttheroutedefinitionrequiresthatitismappedtoavalidcomponent.SoLogoutComponentwillconsistofadummyclass:
[app/logout.component.ts}
import{Component}from'@angular/core';
@Component({
template:''
})
exportclassLogoutComponent{}
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{DefaultComponent}from'./default.component';
import{ProfileComponent}from'./profile.component';
import{LoginComponent}from'./login.component';
import{LogoutComponent}from'./logout.component';
import{AuthService}from'./auth.service';
import{AuthGuardService,LogoutGuardService}
from'./route-guards.service';
constappRoutes:Routes=[
{
path:'login',
component:LoginComponent
},
{
path:'logout',
component:LogoutComponent,
canActivate:[LogoutGuardService]
},
{
path:'profile',
component:ProfileComponent,
canActivate:[AuthGuardService]
},
{
path:'**',
component:DefaultComponent
}
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
LoginComponent,
LogoutComponent,
DefaultComponent,
ProfileComponent,
RootComponent
],
providers:[
AuthService,
AuthGuardService,
LogoutGuardService
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Withthis,youshouldhaveafullyfunctionallogin/logoutbehaviorprocess.
Howitworks...ThecoreofthisimplementationisbuiltaroundObservablesandRouteGuards.ObservablesallowyourAuthServicemoduletomaintainthestateandexposeitsimultaneouslythroughBehaviorSubject,andRouteGuardsallowyoutoconditionallynavigateandredirectatyourapplication'sdiscretion.
There'smore...Applicationsecurityisabroadandinvolvedsubject.Therecipeshownhereinvolveshowtosmoothlymoveyouruseraroundtheapplication,butitisbynomeansarigoroussecuritymodel.
Theactualauthentication
Youshouldalwaysassumetheclientcanmanipulateitsownexecutionenvironment.Inthisexample,evenifyouprotectthelogin/logoutmethodsonAuthServiceaswellasyoucan,itwillbeeasyfortheusertogainaccesstothesemethodsandauthenticatethemselves.
Userinterfaces,whichAngularapplicationssquarelyfallinto,arenotmeanttobesecure.Securityresponsibilitiesfallontheserversideoftheclient/servermodelsincetheuserdoesnotcontrolthatexecutionenvironment.Inanactualapplication,thelogin()methodherewouldmakeanetworkrequestgetsomesortofatokenfromtheserver.Twoverypopularimplementations,JSONWebTokensandCookieauth,dothisindifferentways,buttheyareessentiallyvariationsofthesametheme.Angularorthebrowserwillstoreandsendthesetokens,butultimatelytheservershouldactasthegatekeeperofsecureinformation.
Securedataandviews
Anysecureinformationyoumightsendtotheclientshouldbebehindserver-basedauthentication.Formanydevelopers,thisisanobviousfact,especiallywhendealingwithanAPI.However,Angularalsorequeststemplatesandstaticfilesfromtheserver,andsomeoftheseyoumightnotwanttoservetothewrongpeople.Inthiscase,youwillneedtoconfigureyourservertoauthenticaterequestsforthesestaticfilesbeforeyouservethemtotheclient.
SeealsoNavigatingwiththeRouterserviceusesanAngularservicetonavigatearoundanapplicationBuildingstatefulRouterLinkbehaviorwithRouterLinkActiveshowshowtointegrateapplicationbehaviorwithaURLstateImplementingnestedviewswithrouteparametersandchildroutesgivesanexampleofhowtoconfigureAngularURLstosupportnestinganddatapassingWorkingwithmatrixURLparametersandroutingarraysdemonstratesAngular'sbuilt-inmatrixURLsupport
Chapter7.Services,DependencyInjection,andNgModuleThischapterwillcoverthefollowingrecipes:
InjectingasimpleserviceintoacomponentControllingserviceinstancecreationandinjectionwithNgModuleServiceinjectionaliasingwithuseClassanduseExistingInjectingavalueasaservicewithuseValueandOpaqueTokensBuildingaprovider-configuredservicewithuseFactory
IntroductionAngular1gaveyouahodgepodgeofdifferentservicetypes.Manyofthemhadagreatdealofoverlap.Manyofthemwereconfusing.Andallofthemweresingletons.
Angular2hastotallythrownawaythisconcept.Initsplace,thereisashinynewdependencyinjectionsystemthatisfarmoreextensibleandsensiblethanitspredecessor.Itallowsyoutohaveatomicandnon-atomicservicetypes,aliasing,factories,andallkindsofincrediblyusefultoolsforuseinyourapplication.
Ifyouarelookingtouseservicesmuchinthesamewayasearlier,youwillfindthatyourunderstandingofservicetypeswilleasilycarryovertothenewsystem.Butfordeveloperswhowantmoreoutoftheirapplications,thenewworldofdependencyinjectionisincrediblypowerfulandobviouslybuiltforapplicationsthatcanscale.
InjectingasimpleserviceintoacomponentThemostcommonusecasewillbeforacomponenttodirectlyinjectaserviceintoitself.Althoughtherhythmsofdefiningservicetypesandusingdependencyinjectionremainmostlythesame,it'simportanttogetagoodholdofthefundamentalsofAngular2'sdependencyinjectionschema,asitdiffersinseveralimportantways.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/4263.
GettingreadySupposeyouhadthefollowingskeletonapplication:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>rootcomponent!</h1>
<button(click)="fillArticle()">Showarticle</button>
<h2>{{title}}</h2>
`
})
exportclassRootComponent{
title:string;
constructor(){}
fillArticle(){}
}
Yourobjectiveistoimplementaservicethatcanbeinjectedintothiscomponentandreturnanarticletitletofillthetemplate.
Howtodoit...Asyoumightexpect,servicesinAngular2arerepresentedasclasses.Similartocomponents,servicesaredesignatedassuchwithan@Injectabledecorator.Createthisserviceinitsownfile:
[app/article.service.ts]
import{Injectable}from'@angular/core';
@Injectable()
exportclassArticleService{
privatetitle_:string=`
CFOYodelsQuarterlyEarningsCall,StockSkyrockets
`
}
Thisservicehasaprivatetitlethatyouneedtotransfertothecomponent,butfirstyoumustmaketheserviceitselfavailabletothecomponent.Thiscanbedonebyimportingtheservice,thenlistingitintheproviderspropertyoftheapplicationmodule:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RootComponent}from'./root.component';
import{ArticleService}from'./article.service';
@NgModule({
imports:[
BrowserModule
],
declarations:[
RootComponent
],
providers:[
ArticleService
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Nowthattheservicecanbeprovided,injectitintothecomponent:
[app/root.component.ts]
import{Component}from'@angular/core';
import{ArticleService}from'./article.service';
@Component({
selector:'root',
template:`
<h1>rootcomponent!</h1>
<button(click)="fillArticle()">Showarticle</button>
<h2>{{title}}</h2>
`
})
exportclassRootComponent{
title:string;
constructor(privatearticleService_:ArticleService){}
fillArticle(){}
}
ThisnewcodewillcreateanewinstanceofArticleServicewhenRootComponentisinstantiated,andtheninjectitintotheconstructor.Anythinginjectedintoacomponentwillbeavailableasacomponentinstancemember,whichyoucanusetoconnectaservicemethodtoacomponentmethod:
[app/article.service.ts]
import{Injectable}from'@angular/core';
@Injectable()
exportclassArticleService{
privatetitle_:string=`
CFOYodelsQuarterlyEarningsCall,StockSkyrockets
`;
getTitle(){
returnthis.title_;
}
}
[app/root.component.ts]
import{Component}from'@angular/core';
import{ArticleService}from'./article.service';
@Component({
selector:'root',
template:`
<h1>rootcomponent!</h1>
<button(click)="fillArticle()">Showarticle</button>
<h2>{{title}}</h2>
`
})
exportclassRootComponent{
title:string;
constructor(privatearticleService_:ArticleService){}
fillArticle():void{
this.title=this.articleService_.getTitle();
}
}
Howitworks...Withoutthedecorator,theserviceyouhavejustbuiltisratherplainincomposition.Withthe@Injectable()decoration,theclassisdesignatedtotheAngularframeworkasonethatwillbeinjectedelsewhere.
Note
Designationasaninjectablehasanumberofconsiderationsthatareimportantlydistinctfromjustbeingpassedinparametrically.Whenistheinjectedclassinstantiated?Howisitlinkedtothecomponentinstance?Howareglobalandlocalinstancescontrolled?Thesearealldiscussedinthemoreadvancedrecipesinthischapter.
Designationasaninjectableserviceisonlyonepieceofthepuzzle.Thecomponentneedstobeinformedoftheexistenceoftheservice.Youmustfirstimporttheserviceclassintothecomponentmodule,butthisaloneisnotsufficient.Recallthatthesyntaxusedtoinjectaservicewassimplyawaytolistitasaconstructorparameter.Behindthescenes,Angularissmartenoughtorecognizethatthesecomponentargumentsaretobeinjected,butitrequiresthefinalpiecetoconnecttheimportedmoduletoitsplaceasaninjectedresource.
ThisfinalpiecetakestheformoftheproviderspropertyoftheNgModuledefinition.Forthepurposeofthisrecipe,itisn'timportantthatyouknowthedetailsoftheproperty.Inshort,thisarraydesignatesthearticleServiceconstructorparameterasaninjectableandidentifiesthatArticleServiceshouldbeinjectedintotheconstructor.
There'smore...It'simportanttoacknowledgeherehowtheTypeScriptdecoratorshelpthedependencyinjectionsetup.Decoratorsdonotmodifyaninstanceofaclass;rather,theymodifytheclassdefinition.TheNgModulecontainingtheproviderslistwillbeinitializedpriortoanyinstanceoftheactualcomponentbeinginstantiated.Thus,Angularwillbeawareofalltheservicesthatyoumightwanttoinjectintotheconstructor.
SeealsoControllingserviceinstancecreationandinjectionwithNgModulegivesabroadoverviewofhowAngular2architectsproviderhierarchiesusingmodules
ControllingserviceinstancecreationandinjectionwithNgModuleInastarkdeparturefromAngular1.x,Angular2featuresahierarchicalinjectionscheme.Thishasasubstantialnumberofimplications,andoneofthemoreprominentoneistheabilitytocontrolwhen,andhowmany,servicesarecreated.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/2102/.
GettingreadySupposeyoubeginwiththefollowingsimpleapplication:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>rootcomponent!</h1>
<article></article>
<article></article>
`
})
exportclassRootComponent{}
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<p>Articlecomponent!</p>
`
})
exportclassArticleComponent{}
[app/article.service.ts]
import{Injectable}from'@angular/core';
@Injectable()
exportclassArticleService{
constructor(){
console.log('ArticleServiceconstructor!');
}
}
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RootComponent}from'./root.component';
import{ArticleComponent}from'./article.component;
@NgModule({
imports:[
BrowserModule,
],
declarations:[
RootComponent,
ArticleComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
YourobjectiveistoinjectasingleinstanceofArticleServiceintothetwochildcomponents.Inthisrecipe,console.loginsidetheArticleServiceconstructorallowsyoutoseewhenoneisinstantiated.
Howtodoit...BeginbyimportingtheserviceintoAppModule,thenprovidingitwiththefollowing:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RootComponent}from'./root.component';
import{ArticleComponent}from'./article.component;
import{ArticleService}from'./article.service;
@NgModule({
imports:[
BrowserModule,
],
declarations:[
RootComponent,
ArticleComponent
],
providers:[
ArticleService
]
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
SinceArticleServiceisprovidedinthesamemodulewhereArticleComponentisdeclared,youarenowabletoinjectArticleServiceintothechildArticleComponentinstances:
[app/article/article.component.ts]
import{Component}from'@angular/core';
import{ArticleService}from'./article.service';
@Component({
selector:'article',
template:`
<p>Articlecomponent!</p>
`
})
exportclassArticleComponent{
constructor(privatearticleService_:ArticleService){}
}
Withthis,youwillfindthatthesameserviceinstanceisinjectedintoboththechildcomponentsastheArticleServiceconstructor,namelyconsole.log,isonlyexecutedonce.
Splittinguptherootmodule
Astheapplicationgrows,itwillmakelessandlesssensetocrameverythingintothesametop-levelmodule.Instead,itwouldbeidealforyoutobreakapartmodulesintochunksthatmakesense.Inthecaseofthisrecipe,itwouldbepreferabletoprovideArticleServicetotheapplicationpiecesthatareactuallygoingtoinjectit.
DefineanewArticleModuleandmovetherelevantmoduleimportsintothatfileinstead:
[app/article.module.ts]
import{NgModule}from'@angular/core';
import{ArticleComponent}from'./article.component';
import{ArticleService}from'./article.service';
@NgModule({
declarations:[
ArticleComponent
],
providers:[
ArticleService
],
bootstrap:[
ArticleComponent
]
})
exportclassArticleModule{}
Then,importthisentiremoduleintoAppModuleinstead:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RootComponent}from'./root.component';
import{ArticleModule}from'./article.module';
@NgModule({
imports:[
BrowserModule,
ArticleModule
],
declarations:[
RootComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Ifyoustophere,you'llfindthattherearenoerrors,butAppModuleisn'tabletorenderArticleComponent.ThisisbecauseAngularmodules,likeothermodulesystems,needtoexplicitlydefinewhatisbeingexportedtoothermodules:
[app/article.module.ts]
import{NgModule}from'@angular/core';
import{ArticleComponent}from'./article.component';
import{ArticleService}from'./article.service';
@NgModule({
declarations:[
ArticleComponent
],
providers:[
ArticleService
],
bootstrap:[
ArticleComponent
],
exports:[
ArticleComponent
]
})
exportclassArticleModule{}
Withthis,youwillstillseethatArticleServiceisinstantiatedonce.
Howitworks...Angular2'sdependencyinjectiontakesadvantageofitshierarchystructurewhenprovidingandinjectingservices.Fromwhereaserviceisinjected,Angularwillinstantiateaservicewhereveritisprovided.Insideamoduledefinition,thiswillonlyeverhappenonce.
Inthiscase,youprovidedArticleServicetobothAppModuleandArticleModule.Eventhoughtheserviceisinjectedtwice(onceforeachArticleComponent),Angularusestheprovidersdeclarationtodecidewhentocreatetheservice.
There'smore...Atthispoint,acuriousdevelopershouldhavelotsofquestionsabouthowexactlythisinjectionschemabehaves.Therearenumerousdifferentconfigurationflavorsthatcanbeusefultothedeveloper,andtheseconfigurationsonlyrequireaminorcodeadjustmentfromtheprecedingresult.
Injectingdifferentserviceinstancesintodifferentcomponents
Asyoumightanticipatefromtheprecedingexplanation,youcanreconfigurethisapplicationtoinjectadifferentArticleServiceinstanceintoeachchild,twointotal.ThiscanbedonebymigratingtheprovidersdeclarationoutofthemoduledefinitionandintotheArticleComponentdefinition:
[app/article.module.ts]
import{NgModule}from'@angular/core';
import{ArticleComponent}from'./article.component';
@NgModule({
declarations:[
ArticleComponent
],
bootstrap:[
ArticleComponent
],
exports:[
ArticleComponent
]
})
exportclassArticleModule{}
[app/article.component.ts]
import{Component}from'@angular/core';
import{ArticleService}from'./article.service';
@Component({
selector:'article',
template:`
<p>Articlecomponent!</p>
`,
providers:[
ArticleService
]
})
exportclassArticleComponent{
constructor(privatearticleService_:ArticleService){}
}
Youcanverifythattwoinstancesarebeingcreatedbyobservingthetwoconsole.logstatements
calledfromtheArticleServiceconstructor.
Serviceinstantiation
Thelocationoftheprovidersalsomeansthatserviceinstanceinstantiationisboundtothelifetimeofthecomponent.Forthisapplication,thismeansthatwheneveracomponentiscreated,ifaserviceisprovidedinsidethatcomponentdefinition,anewserviceinstancewillbecreated.
Forexample,ifyouweretotoggletheexistenceofachildcomponentwithArticleServiceprovidedinsideit,itwillcreateanewArticleServiceeverytimeArticleComponentisconstructed:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>rootcomponent!</h1>
<button(click)="toggle=!toggle">Toggle</button>
<article></article>
<article*ngIf="toggle"></article>
`
})
exportclassRootComponent{}
YoucanverifythatnewinstancesarebeingcreatedeachtimengIfevaluatestotruebyobservingadditionalconsole.logstatementscalledfromtheArticleServiceconstructor.
SeealsoInjectingasimpleserviceintoacomponentwalksyouthroughthebasicsofAngular2'sdependencyinjectionschemaServiceinjectionaliasingwithuseClassanduseExistingdemonstrateshowtointerceptdependencyinjectionproviderrequests
ServiceinjectionaliasingwithuseClassanduseExistingAsyourapplicationbecomesmorecomplex,youmaycometoasituationwhereyouwouldliketouseyourservicesinapolymorphicstyle.Morespecifically,someplacesinyourapplicationmaywanttorequestServiceA,butaconfigurationsomewhereinyourapplicationwillactuallygiveitServiceB.Thisrecipewilldemonstrateonewayinwhichthiscanbeuseful,butthisbehaviorallowsyourapplicationtobemoreextensibleinmultipleways.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/1109/.
GettingreadySupposeyoubeginwiththefollowingskeletonapplication.
Dualservices
Youbeginwithtwoservices,ArticleServiceandEditorArticleService,andtheirsharedinterface,ArticleSourceInterface.EditorArticleServiceinheritsfromArticleService:
[app/article-source.interface.ts]
exportinterfaceArticleSourceInterface{
getArticle():Article
}
exportinterfaceArticle{
title:string,
body:string,
//?denotesanoptionalproperty
notes?:string
}
[app/article.service.ts]
import{Injectable}from'@angular/core';
import{Article,ArticleSourceInterface}
from'./article-source.interface';
@Injectable()
exportclassArticleServiceimplementsArticleSourceInterface{
privatetitle_:string=
"ResearchersDetermineHamSandwichNotTuringComplete";
privatebody_:string=
"Computersciencecommunityremainsskeptical";
getArticle():Article{
return{
title:this.title_,
body:this.body_
};
}
}
[app/editor-article.service.ts]
import{Injectable}from'@angular/core';
import{ArticleService}from'./article.service';
import{Article,ArticleSourceInterface}
from'./article-source.interface';
@Injectable()
exportclassEditorArticleServiceextendsArticleService
implementsArticleSourceInterface{
privatenotes_:string="Swingandamiss!";
constructor(){
super();
}
getArticle():Article{
//Combineobjectsandreturnthejoinedobject
returnObject.assign(
{},
super.getArticle(),
{
notes:this.notes_
});
}
}
Aunifiedcomponent
Yourobjectiveistobeabletousethefollowingcomponentsothatboththeseservicescanbeinjectedintothefollowingcomponent:
[app/article.component.ts]
import{Component}from'@angular/core';
import{ArticleService}from'./article.service';
import{Article}from'./article-source.interface';
@Component({
selector:'article',
template:`
<h2>{{article.title}}</h2>
<p>{{article.body}}</p>
<p*ngIf="article.notes">
<i>Notes:{{article.notes}}</i>
</p>
`
})
exportclassArticleComponent{
article:Article;
constructor(privatearticleService_:ArticleService){
this.article=articleService.getArticle();
}
}
Howtodoit...Whenlistingproviders,Angular2allowsyoutodeclareanaliasedreferencethatspecifieswhatserviceshouldactuallybeprovidedwhenoneofthecertaintypesisrequested.SinceAngular2injectionwillfollowthecomponenttreeupwardstofindtheprovider,onewaytodeclarethisaliasisbywrappingthecomponentwithaparentcomponentthatwillspecifythisalias:
[app/default-view.component.ts]
import{Component}from'@angular/core';
import{ArticleService}from'./article.service';
@Component({
selector:'default-view',
template:`
<h3>Defaultview</h3>
<ng-content></ng-content>
`,
providers:[ArticleService]
})
exportclassDefaultViewComponent{}
[app/editor-view.component.ts]
import{Component}from'@angular/core';
import{ArticleService}from'./article.service';
import{EditorArticleService}from'./editor-article.service';
@Component({
selector:'editor-view',
template:`
<h3>Editorview</h3>
<ng-content></ng-content>
`,
providers:[
{provide:ArticleService,useClass:EditorArticleService}
]
})
exportclassEditorViewComponent{}
Note
Notethatboththeseclassesareactingaspassthroughcomponents.Otherthanaddingaheader(whichismerelyforlearningthepurposeofinstructioninthisrecipe),theseclassesareonlyspecifyingaproviderandareunconcernedwiththeircontent.
Withthewrapperclassesdefined,youcannowaddthemtotheapplicationmodule,thenusethemtocreatetwoinstancesofArticleComponent:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RootComponent}from'./root.component';
import{ArticleComponent}from'./article.component';
import{DefaultViewComponent}from'./default-view.component';
import{EditorViewComponent}from'./editor-view.component';
import{ArticleComponent}from'./article.component';
import{ArticleService}from'./article.service';
import{EditorArticleService}from'./editor-article.service';
@NgModule({
imports:[
BrowserModule
],
declarations:[
RootComponent,
ArticleComponent,
DefaultViewComponent,
EditorViewComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<default-view>
<article></article>
</default-view>
<hr/>
<editor-view>
<article></article>
</editor-view>
`
})
exportclassRootComponent{}
Withthis,youshouldnowseethattheeditorversionofArticleComponentgetsthenotes,butthedefaultversiondoesnot.
Howitworks...InAngular1,theservicetypethatwassupposedtobeinjectedwasidentifiedfromafunctionparameterbydoingadirectmatchoftheparametersymbol.function(Article)wouldinjecttheArticleservice,function(User)theUserservice,andsoon.Thisledtonastiness,suchastheminification-proofingofconstructorsbyprovidinganarrayofstringstomatchagainst['Article',function(Article){}].
Thisisnolongerthecase.Whenaproviderisregistered,theuseClassoptionutilizesthetwo-partdependencyinjectionmatchingschemeinAngular2.Thefirstpartistheprovidertoken,whichistheparametertypeoftheservicebeinginjected.Inthiscase,privatearticleService_:ArticleServiceusestheArticleServicetokentorequestthataninstancebeinjected.Angular2takesthisandmatchesthistokenagainstthedeclaredprovidersinthecomponenthierarchy.Whenatokenismatched,Angular2willusethesecondpart,theprovideritself,toinjectaninstanceoftheservice.
Inreality,providers:[ArticleService]isashorthandforproviders:[{provide:ArticleService,useClass:ArticleService}].Theshorthandisusefulsinceyouwillalmostalwaysberequestingtheserviceclassthatwouldmatchtheinjectedclass.However,inthisrecipe,youareconfiguringAngular2torecognizeanArticleServicetokenandsousetheEditorArticleServiceprovider.
There'smore...AnattentivedeveloperwillhaverealizedbythispointthattheutilityofuseClassislimitedinthesensethatitdoesnotallowyoutoindependentlycontrolwheretheactualserviceisprovided.Inotherwords,theplacewhereyouintercepttheproviderdefinitionwithuseClassisalsotheplacewherethereplacementclasswillbeprovided.
Inthisexample,useClassissuitablesinceyouareperfectlyhappytoprovideEditorArticleServiceinthesameplacewhereyouarespecifyingthatitshouldbeusedtoreplaceArticleService.However,itisnotdifficulttoimagineascenarioinwhichyouwouldliketospecifythereplacementservicetypebuthaveitinjectedhigherupinthecomponenttree.This,afterall,wouldallowyoutoreuseinstancesofaserviceinsteadofhavingtocreateanewoneforeachuseClassdeclaration.
Forthispurpose,youcanuseuseExisting.Itrequiresyoutoexplicitlyprovidetheservicetypeseparately,butitwillreusetheprovidedinstanceinsteadofcreatinganewone.Fortheapplicationyoujustcreated,youcannowreconfigureitwithuseExisting,andprovideboththeservicesattheRootComponentlevel.
Todemonstratethatyourreasoningabouttheservicebehavioriscorrect,doublethenumberofArticlecomponents,andaddalogstatementtotheconstructorofArticleServicetoensureyouareonlycreatingoneofeachservice:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<default-view>
<article></article>
</default-view>
<editor-view>
<article></article>
</editor-view>
<default-view>
<article></article>
</default-view>
<editor-view>
<article></article>
</editor-view>
`
})
exportclassRootComponent{}
[app/article.service.ts]
import{Injectable}from'@angular/core';
import{Article,ArticleSourceInterface}from'./article-source.interface';
@Injectable()
exportclassArticleServiceimplementsArticleSourceInterface{
privatetitle_:string=
"ResearchersDetermineHamSandwichNotTuringComplete";
privatebody_:string=
"Computersciencecommunityremainsskeptical";
constructor(){
console.log('InstantiatedArticleService!');
}
getArticle():Article{
return{
title:this.title_,
body:this.body_
};
}
}
[app/editor-view.component.ts]
import{Component}from'@angular/core';
import{ArticleService}from'./article.service';
import{EditorArticleService}from'./editor-article.service';
@Component({
selector:'editor-view',
template:`
<h3>Editorview</h3>
<ng-content></ng-content>
`,
providers:[
{provide:ArticleService,useExisting:EditorArticleService}
]
})
exportclassEditorViewComponent{}
[app/default-view.component.ts]
import{Component}from'@angular/core';
import{ArticleService}from'./article.service';
@Component({
selector:'default-view',
template:`
<h3>Defaultview</h3>
<ng-content></ng-content>
`
//providersremoved
})
exportclassDefaultViewComponent{}
Inthisconfiguration,withuseClass,youwillseethatoneinstanceofArticleServiceandtwoinstancesofEditorArticleServicearecreated.WhenreplacedwithuseExisting,youwillfindthatonlyoneinstanceofeachiscreated.
Thus,inthisreconfiguredversionoftherecipe,yourapplicationisdoingthefollowing:
AttheRootComponentlevel,itisprovidingEditorArticleServiceAttheEditorViewComponentlevel,itisredirectingArticleServiceinjectiontokenstoEditorArticleService
AttheArticleComponentlevel,itisinjectingArticleServiceusingtheArticleServicetoken
Refactoringwithdirectiveproviders
Ifthisimplementationseemsclunkyandverbosetoyou,youarecertainlyontosomething.Theintermediatecomponentsareperformingtheirjobsquitewell,butaren'treallydoinganythingotherthanshimminginanintermediateprovider'sdeclaration.Insteadofwrappinginacomponent,youcanmigratetheprovider'sstatementintoadirectiveanddoawaywithboththeviewcomponents:
[app/root.component.ts]
import{Component}from'@angular/core';
import{ArticleComponent}from'./article.component';
import{ArticleService}from'./article.service';
import{EditorArticleService}from'./editor-article.service';
@Component({
selector:'root',
template:`
<article></article>
<articleeditor-view></article>
<article></article>
<articleeditor-view></article>
`
})
exportclassRootComponent{}
[app/editor-view.directive.ts]
import{Directive}from'@angular/core';
import{ArticleService}from'./article.service';
import{EditorArticleService}from'./editor-article.service';
@Directive({
selector:'[editor-view]',
providers:[
{provide:ArticleService,useExisting:EditorArticleService}
]
})
exportclassEditorViewDirective{}
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RootComponent}from'./root.component';
import{ArticleComponent}from'./article.component';
import{DefaultViewComponent}from'./default-view.component';
import{EditorViewDirective}from'./editor-view.directive';
import{ArticleComponent}from'./article.component';
import{ArticleService}from'./article.service';
import{EditorArticleService}from'./editor-article.service';
@NgModule({
imports:[
BrowserModule
],
declarations:[
RootComponent,
ArticleComponent,
DefaultViewComponent,
EditorViewDirective
],
providers:[
ArticleService,
EditorArticleService
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Yourapplicationshouldworkjustthesame!
SeealsoInjectingasimpleserviceintoacomponentwalksyouthroughthebasicsofAngular2'sdependencyinjectionschemaControllingserviceinstancecreationandinjectionwithNgModulegivesabroadoverviewofhowAngular2architectsproviderhierarchiesusingmodulesInjectingavalueasaservicewithuseValueandOpaqueTokensshowshowyoucanusedependency-injectedtokenstoinjectgenericobjectsBuildingaprovider-configuredservicewithuseFactorydetailstheprocessofsettingupaservicefactorytocreateconfigurableservicedefinitions
InjectingavalueasaservicewithuseValueandOpaqueTokensInAngular1,therewasabroadselectionofservicetypesyoucoulduseinyourapplication.Asubsetofthesetypesallowedyoutoinjectastaticvalueinsteadofaserviceinstance,andthisusefulabilityiscontinuedinAngular2.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/3032/.
GettingreadyBeginwiththefollowingsimpleapplication:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RootComponent}from'./root.component';
import{ArticleComponent}from'./article.component';
@NgModule({
imports:[
BrowserModule
],
declarations:[
RootComponent,
ArticleComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<article></article>
`
})
exportclassRootComponent{}
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<imgsrc="{{logoUrl}}">
<h2>FoolandHisMoneyReunitedatLast</h2>
<p>Author:JakeHsu</p>
`
})
exportclassArticleComponent{}
Howtodoit...Althoughaformalserviceclassdeclarationand@Injectabledecoratordesignationisnolongernecessaryforinjectingavalue,token/providermappingisstillneeded.Sincethereisnolongeraclassavailablethatcanbeusedtotypetheinjectable,somethingelsewillhavetoactasitsreplacement.
Angular2solvesthisproblemwithOpaqueToken.Thismoduleallowsyoutocreateaclasslesstokenthatcanbeusedtopairtheinjectedvaluewiththeconstructorargument.ThiscanbeusedalongsidetheuseValueprovideoption,whichsimplydirectlyprovideswhateveritscontentsareasinjectedvalues.
Defineatokenusingauniquestringinitsconstructor:
[app/logo-url.token.ts]
import{OpaqueToken}from'@angular/core';
exportconstLOGO_URL=newOpaqueToken('logo.url');
Incorporatethistokenintotheapplicationmoduledefinitionasyounormallywould.However,youmustspecifywhatitwillactuallypointtowhenitisinjected.Inthiscase,itshouldresolvetoanimageURLstring:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RootComponent}from'./root.component';
import{ArticleComponent}from'./article.component';
import{LOGO_URL}from'./logo-url.token';
@NgModule({
imports:[
BrowserModule
],
declarations:[
RootComponent,
ArticleComponent
],
providers:[
{provide:LOGO_URL,useValue:
'https://angular.io/resources/images/logos/standard/logo-nav.png'}
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Finally,you'llbeabletoinjectthisintoacomponent.However,sinceyou'reinjectingsomethingthatwasn'tdefinedwiththe@Injectable()decoration,you'llneedtouse@Inject()insidetheconstructortotellAngularthatitshouldbeprovided,usingdependencyinjection.Furthermore,theinjectionwillnotattachitselftothecomponent'sthis,soyou'llneedtodothismanuallyaswell:
[app/article.component.ts]
import{Component,Inject}from'@angular/core';
import{LOGO_URL}from'./logo-url.token';
@Component({
selector:'article',
template:`
<imgsrc="{{logoUrl}}">
<h2>FoolandHisMoneyReunitedatLast</h2>
<p>Author:JakeHsu</p>
`
})
exportclassArticleComponent{
logoUrl:string;
constructor(@Inject(LOGO_URL)privatelogoUrl_){
this.logoUrl=logoUrl_;
}
}
Withthis,youshouldbeabletoseetheimagerenderedinyourbrowser!
Howitworks...OpaqueTokenallowsyoutousenon-classtypesinsideAngular2'sclass-centricproviderschema.Itgeneratesasimpleclassinstancethatessentiallyisjustawrapperforthecustomstringyouprovided.Thisclassiswhatthedependencyinjectionframeworkwillusewhenattemptingtomapinjectiontokenstoproviderdeclarations.Thisgivesyoutheabilitytomorewidelyutilizedependencyinjectionthroughoutyourapplicationsinceyoucannowfeedanytypeofvaluewhereveraservicetypecanbeinjected.
There'smore...Oneotherwayinwhichinjectingvaluesisusefulisthatitgivesyoutheabilitytostuboutservices.Supposeyouwantedtodefineadefaultstubservicethatshouldbeoverriddenwithanexplicitprovidertoenableusefulbehavior.Insuchacase,youcanimagineadefaultarticleentitythatcouldbedifferentlyconfiguredviaadirectivewhilereusingthesamecomponent:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<article></article>
<articleeditor-view></article>
`
})
exportclassRootComponent{}
[app/editor-article.service.ts]
import{Injectable}from'@angular/core';
exportconstMockEditorArticleService={
getArticle:()=>({
title:"Mocktitle",
body:"Mockbody"
})
};
@Injectable()
exportclassEditorArticleService{
privatetitle_:string=
"ProminentVeganEmbroiledinScrambledEggsScandal";
privatebody_:string=
"TofuFarmingAllianceretractedtheirendorsement.";
getArticle(){
return{
title:this.title_,
body:this.body_
};
}
}
[app/editor-view.directive.ts]
import{Directive}from'@angular/core';
import{EditorArticleService}from'./editor-article.service';
@Directive({
selector:'[editor-view]',
providers:[EditorArticleService]
})
exportclassEditorViewDirective{}
[app/article.component.ts]
import{Component,Inject}from'@angular/core';
import{EditorArticleService}from'./editor-article.service';
@Component({
selector:'article',
template:`
<h2>{{title}}</h2>
<p>{{body}}</p>
`
})
exportclassArticleComponent{
title:string;
body:string;
constructor(privateeditorArticleService_:EditorArticleService){
letarticle=editorArticleService_.getArticle();
this.title=article.title;
this.body=article.body;
}
}
Withthis,yourArticleComponent,asdefinedintheprecedingcode,wouldusethemockservicewhenthedirectiveisnotattachedandtheactualservicewhenitisattached.
SeealsoControllingserviceinstancecreationandinjectionwithNgModulegivesabroadoverviewofhowAngular2architectsproviderhierarchiesusingmodulesServiceinjectionaliasingwithuseClassanduseExistingdemonstrateshowtointerceptdependencyinjectionproviderrequestsBuildingaprovider-configuredservicewithuseFactorydetailstheprocessofsettingupaservicefactorytocreateconfigurableservicedefinitions
Buildingaprovider-configuredservicewithuseFactoryOnefurtherextensionofdependencyinjectioninAngular2istheabilitytousefactorieswhendefiningyourproviderhierarchy.Aproviderfactoryallowsyoutoacceptinput,performarbitraryoperationstoconfiguretheprovider,andreturnthatproviderinstanceforinjection.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/0049/.
GettingreadyBeginagainwiththedualserviceandarticlecomponentsetupshowninServiceinjectionaliasingwithuseClassanduseExisting,earlierinthechapter.
Howtodoit...ProviderfactoriesinAngular2areexactlyasyoumightimaginetheywouldbe:functionsthatreturnaprovider.ThefactorycanbespecifiedinaseparatefileandreferencedwiththeuseFactoryprovideoption.
Beginbycombiningthetwoservicesintoasingleservice,whichwillbeconfiguredwithamethodcall:
[app/article.service.ts]
import{Injectable}from'@angular/core';
@Injectable()
exportclassArticleService{
privatetitle_:string=
"FlyingSpaghettiMonsterSighted";
privatebody_:string=
"Adherentsinsistwearemissingthepoint";
privatenotes_:string="Spoton!";
privateeditorEnabled_:boolean=false;
getArticle():Object{
vararticle={
title:this.title_,
body:this.body_
};
if(this.editorEnabled_){
Object.assign(article,article,{
notes:this.notes_
});
}
returnarticle;
}
enableEditor():void{
this.editorEnabled_=true;
}
}
Definingthefactory
YourobjectiveistoconfigurethisservicetohaveenableEditor()invokedbasedonabooleanflag.Withproviderfactories,thisispossible.Definethefactoryinitsownfile:
[app/article.factory.ts]
import{ArticleService}from'./article.service';
exportfunctionarticleFactory(enableEditor?:boolean):ArticleService{
return(articleService:ArticleService)=>{
if(enableEditor){
articleService.enableEditor();
}
returnarticleService;
}
}
InjectingOpaqueToken
Splendid!Next,you'llneedtoreconfigureArticleComponenttoinjectatokenratherthanthedesiredservice:
[app/article.token.ts]
import{OpaqueToken}from'@angular/core';
exportconstArticleToken=newOpaqueToken('app.article');
[app/article.component.ts]
import{Component,Inject}from'@angular/core';
import{ArticleToken}from'./article.token';
@Component({
selector:'article',
template:`
<h2>{{article.title}}</h2>
<p>{{article.body}}</p>
<p*ngIf="article.notes">
<i>Notes:{{article.notes}}</i>
</p>
`
})
exportclassArticleComponent{
article:Object;
constructor(@Inject(ArticleToken)privatearticleService_){
this.article=articleService_.getArticle();
}
}
CreatingproviderdirectiveswithuseFactory
Finally,you'llneedtodefinethedirectivesthatspecifyhowtousethisfactoryandincorporatethemintotheapplication:
[app/default-view.directive.ts]
import{Directive}from'@angular/core';
import{ArticleService}from'./article.service';
import{articleFactory}from'./article.factory';
import{ArticleToken}from'./article.token';
@Directive({
selector:'[default-view]',
providers:[
{provide:ArticleToken,
useFactory:articleFactory(),
deps:[ArticleService]
}
]
})
exportclassDefaultViewDirective{}
[app/editor-view.directive.ts]
import{Directive}from'@angular/core';
import{ArticleService}from'./article.service';
import{articleFactory}from'./article.factory';
import{ArticleToken}from'./article.token';
@Directive({
selector:'[editor-view]',
providers:[
{
provide:ArticleToken,
useFactory:articleFactory(true),
deps:[ArticleService]
}
]
})
exportclassEditorViewDirective{}
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<articledefault-view></article>
<articleeditor-view></article>
`
})
exportclassRootComponent{}
Withthis,youshouldbeabletoseeboththeversionsofArticleComponent.
Howitworks...Thearticlecomponentisredefinedtouseatokeninsteadofaserviceinjection.Withthetoken,Angularwillwalkupthecomponenttreetofindwherethattokenisprovided.Thedirectivesdeclarethatthetokenismappedtoaproviderfactory,whichisamethodinvokedtoreturntheactualprovider.
useFactoryisthepropertythatmapstothefactoryfunction.depsisthepropertythatmapstotheservicedependenciesthatthefactoryhas.
There'smore...Animportantdistinctionatthispointistorecognizethatallthesefactoryconfigurationsarehappeningbeforeanycomponentsareinstantiated.Theclassdecorationthatdefinestheproviderswillinvokethefactoryfunctiononsetup.
SeealsoControllingserviceinstancecreationandinjectionwithNgModulegivesabroadoverviewofhowAngular2architectsproviderhierarchiesusingmodulesServiceinjectionaliasingwithuseClassanduseExistingdemonstrateshowtointerceptdependencyinjectionproviderrequestsInjectingavalueasaservicewithuseValueandOpaqueTokensshowhowyoucanusedependencyinjectedtokenstoinjectgenericobjects
Chapter8.ApplicationOrganizationandManagementThischapterwillcoverthefollowingrecipes:
Composingpackage.jsonforaminimumviableAngular2applicationConfiguringTypeScriptforaminimumviableAngular2applicationPerformingin-browsertranspilationwithSystemJSComposingapplicationfilesforaminimumviableAngular2applicationMigratingtheminimumviableAngular2applicationtoWebpackbundlingIncorporatingshimsandpolyfillsintoWebpackHTMLgenerationwithhtml-webpack-pluginSettingupanapplicationwithAngular'sCLI
IntroductionTheAngular2project'sambitionsgoalsinvolvetheutilizationofadifferentlanguagewithdifferentsyntaxandconstructs,aswellasprovidinghighefficiencyandmodularity.WhatthismeansforyouisthattheprocessofmaintaininganAngular2applicationmaybedifficult.
TheultimategoalistoefficientlyserveHTML,CSS,andJStoawebbrowserandtomakeiteasytodevelopthesourcecomponentsofthesestaticfiles.Howonearrivesatthisendpointcanbeworkedoutinanumberofdifferentways,anditwouldbeanexerciseinfutilitytowriteachapteronallofthem.
Instead,thischapterwillprovideafewopinionatedwaysofarrangingyourAngular2applicationinawaythatitwouldreflectthemostpopularandeffectivestrategies.ItwillalsoshowyouhowtobuildandextendaminimumviableAngular2application.Forsome,thiswillseemabitsimpleandrudimentary.However,themajorityofQuickstartprojectsorcodegenerationframeworkssimplygiveyouarepositoryandafewcommandstoruninordertogetoutofthedoor,andthesecommandsrunwithouttellingyouwhatthey'redoingorhowthey'redoingit!Inthischapter,youwilllearnhowtobuildanAngular2applicationfromthegroundupalongwiththepackagesandtoolsthatwillhelpyoudoitandwhythesemethodswereselected.
Composingpackage.jsonforaminimumviableAngular2applicationWhenthinkingaboutaminimumviableAngular2application,theconfigurationfilesareasclosetothemetaloftheruntimeenvironmentasyou'llget.Inthiscase,therearetwoconfigurationfilesthatwillcontrolhownpmanditsinstalledpackageswillmanagethefilesandthestart-upprocesses:package.jsonandtsconfig.json.
Somepartofthisrecipemaybeareviewfordevelopersthataremoreexperiencedwithnpmanditsfaculties.However,it'simportanttounderstandhowaverysimpleAngular2projectconfigurationcanbestructured,sothatyouareabletowhollyunderstandmorecomplexconfigurationsthatarebuilduponitsfundamentals.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/1332/.
GettingreadyYou'llneedNode.jsinstalledforthisrecipetowork;you'llalsoneedanemptyprojectdirectory.Youshouldcreatethesetwoskeletonconfigurationfilesintherootprojectdirectory:
[package.json]
{
"name":"angular2-minimum-viable-application"
}
[tsconfig.json]
{
"compilerOptions":{
}
}
Tip
Foraquickandeasywaytoensureyouhavenpmsetupandreadytogo,usethefollowing:
npm--versionItshouldspitoutaversionnumberifeverythingissetupproperly.
Howtodoit...You'llstartwithpackage.json.Thepackage.jsonfileforaminimumviableapplicationcontainsthreesections:
dependencies:ThisisalistofpackagetargetsthattheproductionapplicationdirectlydependsupondevDependencies:Thisisalistofpackagetargetsthatthelocalenvironmentneedsforvariousreasons,suchascompilation,runningtests,orlintingscripts:Thesearecustom-definedcommand-lineutilitiesrunthroughnpm
package.jsondependencies
First,youneedtoaddinallthedependenciesthatyourapplicationwillneed.ThisincludesAngular2coremodules,whichliveinsidethenode_modules/@angulardirectory,aswellasahandfuloflibrarydependencies:
core-jsisthepolyfillfortheES6syntaxthattheTypeScriptcompilerdependsupon,suchasSet,Promise,andMap.reflect-metadataisthepolyfillfortheReflectMetadataAPI.ThisallowsyourTypeScripttousedecoratorsthatarenotpartofthestandardTypeScriptspecification,suchas@Component.rxjsisavailablefortheReactiveXJavaScriptobservableslibrary.Angular2nativelyusesObservables,andthisisadirectdependencyoftheframework.SystemJSisthedynamicmoduleloaderthatthisprojectneedsfortwopurposes:toimportandmapallthesourcefiles,andtobeabletoresolvetheES6import/exportdeclarations.zonejsistheZoneJSlibrarythatprovidesAngular2withtheabilitytouseasynchronousexecutioncontexts.Thisisadirectdependencyoftheframework.
Thisleavesyouwiththefollowing:
[package.json]
{
"name":"angular2-minimum-viable-application",
"dependencies":{
"@angular/common":"2.0.0",
"@angular/compiler":"2.0.0",
"@angular/core":"2.0.0",
"@angular/platform-browser":"2.0.0",
"@angular/platform-browser-dynamic":"2.0.0",
"core-js":"^2.4.1",
"reflect-metadata":"^0.1.3",
"rxjs":"5.0.0-beta.12",
"systemjs":"0.19.27",
"zone.js":"^0.6.23"
}
}
package.jsondevDependencies
Next,youneedtospecifythedevDependencies.
Note
Here'sannpmrefresher:devDependenciesaredependenciesthatarespecifictoadevelopmentenvironment.Buildscriptscanusethistodifferentiatebetweenpackagesthatneedtobeincludedinaproductionbundleandonesthatdon't.
lite-serveristhesimplefileserveryou'llusetotestthisapplicationlocally.Thiscouldbereplacedbyanynumberofsimplefileservers.typescriptistheTypeScriptcompiler.concurrentlyisasimplecommand-lineutilityforrunningsimultaneouscommandsfromannpmscript.
Thisleavesyouwiththefollowing:
[package.json]
{
"name":"angular2-minimum-viable-application",
"dependencies":{
"@angular/common":"2.0.0",
"@angular/compiler":"2.0.0",
"@angular/core":"2.0.0",
"@angular/platform-browser":"2.0.0",
"@angular/platform-browser-dynamic":"2.0.0",
"core-js":"^2.4.1",
"reflect-metadata":"^0.1.3",
"rxjs":"5.0.0-beta.12",
"systemjs":"0.19.27",
"zone.js":"^0.6.23"
},
"devDependencies":{
"concurrently":"^2.2.0",
"lite-server":"^2.2.2",
"typescript":"^2.0.2"
}
}
package.jsonscripts
Finally,youneedtocreatethescriptsthatyou'llusetogeneratecompiledfilesandrunthedevelopmentserver:
[package.json]
{
"name":"angular2-minimum-viable-application",
"scripts":{
"lite":"lite-server",
"postinstall":"npminstall-S@types/node@types/core-js",
"start":"tsc&&concurrently'npmruntsc:w''npmrunlite'",
"tsc":"tsc",
"tsc:w":"tsc-w"
},
"dependencies":{
"@angular/common":"2.0.0",
"@angular/compiler":"2.0.0",
"@angular/core":"2.0.0",
"@angular/platform-browser":"2.0.0",
"@angular/platform-browser-dynamic":"2.0.0",
"core-js":"^2.4.1",
"reflect-metadata":"^0.1.3",
"rxjs":"5.0.0-beta.12",
"systemjs":"0.19.27",
"zone.js":"^0.6.23"
},
"devDependencies":{
"concurrently":"^2.2.0",
"lite-server":"^2.2.2",
"typescript":"^2.0.2"
}
}
Eachofthesescriptsservesapurpose,butmostyouwillnotneedtoinvokemanually.Hereisabriefdescriptionofeachofthesescripts:
litestartsoffaninstanceoflite-server.postinstallisthehookdefinitionthatwillrunafternpminstalliscompleted.Inthiscase,afternpmhasinstalledalltheprojectdependencies,youwanttoinstallthedeclarationfilesformodulesthatdonothavethem.npmrecognizesthepre-andpost-prefixesforscriptstrings.Anytimeascriptisrun,npmwillcheckforscriptswithpre-andpost-prefixingthemandrunthembeforeandafterthescript,respectively.Inthisrecipe,prelitewouldrunbeforelite,andpostlitewouldrunafterliteisrun.startisthedefinitionofthedefaultvalueofnpmstart.ThisscriptrunstheTypeScriptcompileroncetocompletion,thensimultaneouslyinvokestheTypeScriptcompilerwatcherandstartsupadevelopmentserver.Itisareservedscriptkeywordinnpm,thusthereisnoneedfornpmrunstart,althoughthatdoeswork.tsckicksofftheTypeScriptcompiler.TheTypeScriptcompilerreadsitssettingsfromthetsconfig.jsonthatexistsinthesamedirectory.tsc:wsetsafilewatchertorecompileuponfilechanges.
SeealsoComposingpackage.jsonforaminimumviableAngular2applicationdescribeshowallthepiecesworkforthecorenodeprojectfileConfiguringTypeScriptforaminimumviableAngular2applicationtalksabouthowtoconfigurethecompilationtosupportanAngular2projectPerformingin-browsertranspilationwithSystemJSdemonstrateshowSystemJScanbeusedtoconnectuncompiledstaticfilestogetherComposingapplicationfilesforaminimumviableAngular2ApplicationwalksyouthroughhowtocreateanextremelysimpleAngular2appfromscratchMigratingtheminimumviableAngular2applicationtoWebpackbundlingdescribeshowtointegrateWebpackintoyourAngularapplicationbuildprocessIncorporatingshimsandpolyfillsintoWebpackgivesyouahandywayofmanagingAngular2polyfilldependenciesHTMLgenerationwithhtml-webpack-pluginshowsyouhowyoucanconfigureannpmpackagetoaddcompiledfilestoyourHTMLautomaticallySettingupanapplicationwithAngularCLIgivesadescriptionofhowtousetheCLI,whatitgivesyou,andwhattheseindividualpiecesdo
ConfiguringTypeScriptforaminimumviableAngular2applicationInordertouseTypeScriptalongsideAngular2,therearetwomajorconsiderations:moduleinteroperabilityandcompilation.You'llneedtohandlebothinordertotakeyourapplication's.tsfiles,mixthemwithexternallibraryfiles,andoutputthefilesthatwouldbecompatiblewithyourtargetdevice.
TypeScriptcomesreadyasannpmpackage,butyouwillneedtotellithowtointeractwiththefilesandmodulesyou'vewritten,andwithfilesfromotherpackagesthatyouwanttouseinyourmodules.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/1053/.
GettingreadyYoushouldfirstcompletetheinstructionsmentionedintheprecedingrecipe.ThiswillgiveyoutheframeworknecessarytodefineyourTypeScriptconfiguration.
Howtodoit...ToconfigureTypeScript,you'llneedtoadddeclarationfilestoincompatiblemodulesandgenerateaconfigurationfilethatwillspecifyhowthecompilershouldwork.
Declarationfiles
TypeScriptdeclarationfilesexisttospecifytheshapeofalibrary.Thesefilescanbeidentifiedbya.d.tssuffix.ThemajorityofnpmpackagesandotherJavaScriptlibrariesalreadyincludethesefilesinastandardizedlocation,sothatTypeScriptcanlocatethemandlearnabouthowthelibraryshouldbeinterpreted.Librariesthatdon'tincludetheseneedtobegiventhefiles,andfortunatelytheopensourcecommunityalreadyprovidesalotofthem.
Twolibrariesthatthisprojectusesdon'thavedeclarationfiles:nodeandcore-js.AsofTypeScript2.0,youareabletonativelyinstallthedeclarationfilesfortheselibrariesdirectlythroughnpm.The-Sflagisashorthandforsavingthemtopackage.json:
npminstall-S@types/node@types/core-js
Asensibleplaceforthisisinsidethepostinstallscript.
tsconfig.json
TheTypeScriptcompilerwilllookforthetsconfig.jsonfiletodeterminehowitshouldcompiletheTypeScriptfilesinthisdirectory.Thisconfigurationfileisn'trequired,asTypeScriptwillfallbacktothecompilerdefaults;however,youwanttomanageexactlyhowthe*.jsand*.map.jsfilesaregenerated.Modifythetsconfig.jsonfiletoappearasfollows:
[tsconfig.json]
{
"compilerOptions":{
"target":"es5",
"module":"commonjs",
"moduleResolution":"node",
"emitDecoratorMetadata":true,
"experimentalDecorators":true,
"noImplicitAny":false
}
}
ThecompilerOptionsproperty,asyoumightexpect,specifiesthesettingsthecompilershouldusewhenthecompilingprocessfindsTypeScriptfiles.Intheabsenceofafilesproperty,TypeScriptwilltraversetheentireprojectdirectorystructuresearchingfor*.tsand*.tsxfiles.
AllthecompilerOptionspropertiescanbespecifiedequivalentlyascommand-lineflags,butdoingsointsconfig.jsonisamoreorganizedwayofgoingaboutyourproject.
targetspecifiestheECMAScriptversionthatthecompilershouldoutput.Forbroadbrowsercompatibility,ES5isasensibledefaulthere.RecallthatECMAScriptisthespecificationuponwhichJavaScriptisbuilt.ThenewestfinishedspecificationisES6(alsocalledES2015),butmanybrowsersdonotfullysupportthisspecificationyet.TheTypeScriptcompilerwillcompileES6constructs,suchasclassandPromise,tonon-nativeimplementations.modulespecifieshowtheoutputfileswillhandlethemodulesintheoutputfiles.SinceyoucannotassumethatbrowsersareabletohandleES6modules,theTypeScriptcompilerwillhavetoconvertthemintoamodulesystemthatbrowsersareabletohandle.CommonJSisasensiblechoicehere.TheCommonJSmodulestyleinvolvesdefiningalltheexportsinasinglemoduleaspropertiesofasingle"exports"object.TheTypeScriptcompileralsosupportsAMDmodules(require.jsstyle),UMDmodules,SystemJSmodules,andofcourse,leavingthemodulesastheirexistingES6modulestyle.It'soutofthescopeofthisrecipetodivedeepintomodules.moduleResolutiondefineshowmodulepathswillberesolved.It'snotcriticalthatyouunderstandtheexactdetailsoftheresolutionstrategy,butthenodesettingwillgiveyoutheproperoutputformat.emitDecoratorMetadataandexperimentalDecoratorsenableTypeScripttohandleAngular2'suseofdecorators.Recalltheadditionofthereflect-metadatalibrarytosupportexperimentaldecorators.TheseflagsarethepointwhereitisabletotieintotheTypeScriptcompiler.noImplicitAnycontrolswhetherornotTypeScriptfilesmustbetyped.Whensettotrue,thiswillthrowanerrorifthereisanymissedtypinginyourproject.Thereisanongoingdiscussionregardingwhetherornotthisflagshouldbeset,asforcingobjectstobetypedisobviouslyusefultopreventerrorsthatmayarisefromambiguityincodebases.Ifyou'dliketoseeanexampleofthecompilerthrowinganerror,setnoImplicitAnytotrueandaddconstructor(foo){}insideAppComponent.Youshouldseethecompilercomplainaboutfoobeinguntyped.
Howitworks...RunningthefollowingcommandwillstartuptheTypeScriptcompilerfromthecommandlineattherootlevelofyourprojectdirectory:
npmruntsc
Thecompilerwilllookfortsconfig.jsonifitisthereandfallbacktoitsdefaultsotherwise.Thesettingswithindirectthecompilerhowtohandleandvalidatethefiles,whichiswhereeverythingyoujustsetupcomesintoplay.
TheTypeScriptcompilerdoesn'trunthecodeormeaningfullyunderstandwhatitdoes,butitcandetectwhendifferentpiecesoftheapplicationaretryingtointeractinawaythatdoesn'tmakesense.The.d.tsdeclarationfileforamodulegivesTypeScriptawaytoinspecttheinterfacethatthemodulewillmakeavailableforconsumptionwhenitisimported.
Forexample,supposethatauthisanexternalmodulethatcontainsaUserclass.Thiswouldthenbeimportedviathefollowing:
import{User}from'./auth';
Byaddingthedeclarationfiletotheimportedmodule,TypeScriptisabletocheckthattheUserclassexists;italsobehavesinthewayyouareattemptingtointhelocalmodule.Ifitseesamismatch,itwillthrowanerroratcompilation.
Compilation
Dependingonyourframeworkexperience,thismaybesomethingyouhaveorhavenothadexperiencewithpreviously.Angular2(amongmanyframeworks)operatesunderthenotionthatJavaScript,asitcurrentlyexists,isinsufficientforwritinggoodcode.Thedefinitionof"good"hereissubjective,butallframeworksthatrequirecompilationwanttoextendormodifyJavaScriptinsomeformoranother.
However,allplatformsthattheseapplicationsneedtorunon—foryourpurposes,webbrowsers—onlyhaveaJavaScriptexecutionenvironmentthatexecutesfromuncompiledcode.Itisn'tfeasibleforyoutoextendhowthebrowserhandlespayloadsordeliversacompiledbinary,sothefilesthatyousendtotheclientmustplaybyitsrules.
TypeScript,bydefinitionanddesign,isastrictsupersetofES6,buttheseextensionscan'tbeusednativelyinabrowser.Eventoday,themajorityofbrowsersstilldonotfullysupportES6.Therefore,asensibleobjectiveistoconvertTypeScriptintoES5.1,whichistheECMAScriptstandardthatissupportedonallmodernbrowsers.Howyouarriveatthisoutputcanoccurinoneoftwoways:
SendtheTypeScripttotheclientasis.Therearein-browsercompilationlibrariesthatcan
performacompilationontheclientandexecutetheresultingES5.1-compliantcodeasnormalJavaScript.Thismethodmakesdevelopmenteasiersinceyourbackenddoesn'tneedtodomuchotherthanservethefiles;however,itdeferscomputingtotheclient,whichdegradesperformanceandisthereforeconsideredabadpracticeforproductionapplications.CompiletheTypeScriptintoJavaScriptbeforesendingittotheclient.Theoverwhelmingmajorityofproductionapplicationswillelecttohandletheirbusinessthisway.EspeciallysincestaticfilesareoftenservedfromaCDNorstaticdirectory,itmakesgoodsensetocompileyourdescriptiveTypeScriptcodebaseintoJavaScriptfilesaspartofareleaseandthenservethosefilestotheclient.
Tip
WhenyoulookatthecompiledJavaScriptthatresultsfromcompilingTypeScript,itcanappearawfullybrutalandunreadable.Don'tworry!ThebrowserdoesnotcarehowmangledtheJavaScriptfilesareaslongastheycanbeexecuted.
Withthecompileroptionsyou'vespecifiedinthisrecipe,theTypeScriptcompilerwilloutputa.jsfileofthesamenamerightnexttoitssource,the.tsfile.
There'smore...BynomeansistheTypeScriptcompilerlimitedtoaone-off.tsfilegeneration.Ifoffersyouabroadrangeoftoolingfunctionsforspecifyingexactlyhowyouroutputfilesshouldappear.
Sourcemapgeneration
TheTypeScriptcompilerisalsocapableofgeneratingsourcemapstogoalongwithoutputfiles.Ifyou'renotfamiliarwiththem,theutilityofsourcemapsstemsfromthenatureofcompilationandminification:filesbeingdebuggedinthebrowsersarenotthefilesthatyouhavewritten.What'smore,whenusingacompiledTypeScript,thecompiledfileswon'tevenbeinthesamelanguage.
Sourcemapsareindexesthatpairwithcompiledfilestodescribehowtheyoriginallyappearedbeforetheywerecompiled.Morespecifically,the.js.mapfilescontainanencodingschemethatassociatesthecompiledand/orminifiedtokenswiththeiroriginalnameandstructureintheuncompiled.tsfile.Browsersthatunderstandhowtousesourcemapscanreconstructhowtheoriginalfileappearedandallowyoutosetbreakpoints,stepthrough,andinspectlexicalconstructsinsideitasifitweretheoriginal.
Sourcemapscanbespecifiedwithaspecialtokenaddedtothecompiledfile://#sourceMappingURL=/dist/example.js.map
Ifyouwanttogeneratesourcemapfilesfortheoutput,youcanspecifythisintheconfigurationfileaswellbyadding"sourceMap":true.Bydefault,the.js.mapfileswillbecreatedinthesameplaceastheoutput.jsfiles;alternatively,youcandirectthecompilertocreatethesourcemapsinsidethe.jsfileitself.
Tip
Eventhoughextraneousmapfileswon'taffecttheresultantapplicationbehavior,addingtheminlinemaybeundesirableifyoudon'twanttobloatyour.jspayloadsizeunnecessarily.Thisisbecauseclientsthatdon'twantorneedthemapfilescan'tdeclinetorequestthem.
Singlefilecompilation
SinceTypeScriptchecksallthelinkedmodulesagainsttheirimportsandexports,there'snoreasonyouneedtohaveallthecompiledfilesexistas1:1mappingstotheirinputfiles.TypeScriptisperfectlyhappytocombinethecompiledfilesintoasinglefileiftheoutputmoduleformatsupportsit.Specifythesinglefilewhereyouwishallthemodulestobecompiledwith"outFile":"/dist/bundle.js".
Note
Certainoutputmoduleformats,suchasCommonJS,won'tworkasconcatenatedmodulesina
singlefile,sousingtheminconjunctionwithoutFilewillnotwork.AsoftheTypeScript1.8release,AMDandsystemoutputformatsaresupported.
IfyouplanonusingSystemJS,thiscompileroptioncanpotentiallyhelpyou,asSystemworkswithvirtuallyanymoduleformat.If,however,you'reusingaCommonJS-basedbundler,suchasWebpack,it'sbesttodelegatethefilecombinationtothebundler.
SeealsoComposingpackage.jsonforaminimumviableAngular2applicationdescribeshowallthepiecesworkforthecorenodeprojectfilePerformingin-browsertranspilationwithSystemJSdemonstrateshowSystemJScanbeusedtoconnectuncompiledstaticfilestogetherComposingapplicationfilesforaminimumviableAngular2applicationwalksyouthroughhowtocreateanextremelysimpleAngular2appfromscratchMigratingtheminimumviableAngular2applicationtoWebpackbundlingdescribeshowtointegrateWebpackintoyourAngularapplicationbuildprocess
Performingin-browsertranspilationwithSystemJSItcanbeoftenusefultobeabletodeliverTypeScriptfilesdirectlytothebrowserandtodeferthetranspilationtoJavaScriptuntilthen.Whilethismethodhasperformancedrawbacks,itisextremelyusefulwhenprototypingandperformingexperimentations.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/2283/.
GettingreadyCreateanemptyprojectdirectoryandcreatethefollowingpackage.jsoninsideit:
[package.json]
{
"scripts":{
"lite-server":"lite-server"
},
"devDependencies":{
"lite-server":"^2.2.2",
"systemjs":"^0.19.38",
"typescript":"^2.0.3"
}
}
Runningnpminstallshouldgetyoureadytowritecode.
Howtodoit...TheTypeScriptnpmpackagecomesbundledwithatranspiler.WhencombinedwithSystemJSasthedesignatedtranspilationutility,thisallowsyoutoserveTypeScriptfilestotheclient;SystemJSwilltranspilethemintobrowser-compatibleJavaScript.
First,createtheindex.htmlfile.ThisfilewillimportthetworequiredJSlibraries:system.jsandtypescript.js.Next,itspecifiesthetypescriptasthedesiredtranspilerandimportsthetop-levelmain.tsfile:
[index.html]
<html>
<head>
<scriptsrc="node_modules/systemjs/dist/system.js">
</script>
<scriptsrc="node_modules/typescript/lib/typescript.js">
</script>
<script>
System.config({
transpiler:'typescript'
});
System.import('main.ts');
</script>
</head>
<body>
<h1id="text"></h1>
</body>
</html>
Next,createthetop-levelTypeScriptfile:
[main.ts]
import{article}from'./article.ts';
document.getElementById('text')
.innerHTML=article;
Finally,createthedependencyTypeScriptfile:
[article.ts]
exportconstarticle="Coolstory,bro";
Withthis,youshouldbeabletostartadevelopmentserverwithnpmrunlite-serverandseetheTypeScriptapplicationrunningnormallyinyourbrowseratlocalhost:3000.
Howitworks...SystemJSisabletoresolvemoduledependenciesaswellasapplythetranspilertothemodulebeforeitreachesthebrowser.Ifyoulookatthetranspiledfilesinabrowserinspector,youcanseetheemittedfilesexistasvanillaJavaScriptIIFEs(instantaneouslyinvokedfunctionexpressions)aswellastheircoupledsourcemaps.Withthesetools,itispossibletobuildasurprisinglycomplexapplicationwithoutanysortofbackendfilemanagement.
There'smore...Unlessyou'reexperimentingordoingaroughproject,doingtranspilationinthebrowserisn'tpreferred.Anycomputationyoucandoontheservershouldbedonewheneverpossible.Additionally,alltheclientstranspilingtheirownfilesallperformhighlyredundantoperationssinceallofthemtranspilethesamefiles.
SeealsoComposingpackage.jsonforaminimumviableAngular2applicationdescribeshowallthepiecesworkforthecorenodeprojectfileConfiguringTypeScriptforaminimumviableAngular2applicationtalksabouthowtoconfigurethecompilationtosupportanAngular2projectComposingapplicationfilesforaminimumviableAngular2applicationwalksyouthroughhowtocreateanextremelysimpleAngular2appfromscratchMigratingtheminimumviableAngular2applicationtoWebpackbundlingdescribeshowtointegrateWebpackintoyourAngularapplicationbuildprocessIncorporatingshimsandpolyfillsintowebpackgivesyouahandywayofmanagingAngular2polyfilldependenciesHTMLgenerationwithhtml-webpack-pluginshowsyouhowyoucanconfigureannpmpackagetoaddcompiledfilestoyourHTMLautomaticallySettingupanapplicationwithAngularCLIgivesadescriptionofhowtousetheCLI,whatitgivesyou,andwhattheseindividualpiecesdo
ComposingapplicationfilesforaminimumviableAngular2applicationWhenapproachingAngular2initially,itisusefultohaveanunderstandingofanapplicationstructurethatistorndowntothebaremetal.Inthecaseofaminimumviableapplication,itwillconsistofasinglecomponent.Sincethisisachapteronapplicationorganization,itisn'tsomuchaboutwhatthatcomponentwilllooklike,butratherhowtotaketheTypeScriptcomponentdefinitionandactuallygetittorenderinawebpage.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/6323/.
GettingreadyThisrecipeassumesyouhavecompletedallthestepsgivenintheComposingconfigurationfilesforaminimumviableAngular2applicationrecipe.Thenpmmoduleinstallationshouldsucceedwithnoerrors:
npminstall
Howtodoit...Thesimplestplacetostartisthecoreapplicationcomponent.
app.component.ts
Implementacomponentinsideanewapp/directoryasfollows;thereshouldbenosurprises:
[app/app.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'app-root',
template:'<h1>AppComponenttemplate!</h1>'
})
exportclassAppComponent{}
Thisisaboutassimpleacomponentcanpossiblyget.Oncethisissuccessfullyrenderedintheclient,thiscomponentshouldjustbeabiglineoftext.
app.module.ts
Next,youneedtodefinetheNgModulethatwillbeassociatedwiththiscomponent.Createanotherfileintheapp/directory,app.module.ts,andhaveitmatchthefollowing:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{AppComponent}from'./app.component';
@NgModule({
imports:[BrowserModule],
declarations:[AppComponent],
bootstrap:[AppComponent]
})
exportclassAppModule{}
There'sabitmoregoingonhere:
importsspecifiesthemoduleswhoseexporteddirectives/pipesshouldbeavailabletothismodule.
Tip
ImportingBrowserModulegivesyouaccesstocoredirectivessuchasNgIfandalsospecifiesthetypeofrenderer,eventmanagement,anddocumenttype.Ifyourapplicationisrenderinginawebbrowser,thismodulegivesyouthetoolsyouneedtodothis.
declarationsspecifieswhichdirectives/pipesarebeingexportedbythismodule.Inthiscase,AppComponentisthesoleexport.bootstrapspecifieswhichcomponentsshouldbebootstrappedwhenthismoduleisbootstrapped.Morespecifically,componentslistedherewillbedesignatedforrenderingwithinthismodule.AppComponentneedstobebootstrappedandrenderedsomewhere,andthisiswherethisspecificationwilloccur.
Thiscompletesthemoduledefinition.Atthispoint,youhavesuccessfullylinkedthecomponenttoitsmodule,butthismoduleisn'tbeingbootstrappedanywhereorevenincluded.
main.ts
You'llchangethisnextwithmain.ts,thetop-levelTypeScriptfile:
[app/main.ts]
import{platformBrowserDynamic}
from'@angular/platform-browser-dynamic';
import{AppModule}from'./app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
ThisfiledefinestheNgModuledecoratorthatwillbeusedforAppComponent.Insideit,youspecifythatthemodulemustimportBrowserModule.
Note
RecallthatAngular2isdesignedtobeplatform-independent.Morespecifically,itstrivestoallowyoutowritecodethatmightnotnecessarilyrunonaconventionalwebbrowser.Inthiscase,youaretargetingastandardwebbrowser,soimportingBrowserModulefromtheplatformBrowsertargetisthewayinwhichyoucaninformtheapplicationofthis.Ifyouweretargetingaseparateplatform,youwouldselectadifferentplatformtoimportintoyourrootapplicationcomponent.
ThisNgModuledeclarationalsospecifiesthatAppComponentexistsandshouldbebootstrapped.
Note
BootstrappingishowyoukickoffyourAngular2application,butithasaveryspecificdefinition.Invokingbootstrap()tellsAngulartomountthespecifiedapplicationcomponentontoDOMelementsidentifiedbythecomponent'sselector.Thiskicksofftheinitialroundofchangedetectionanditssideeffects,whichwillcompletethecomponentinitialization.
Sinceyou'vedeclaredthatthismodulewillbootstrapAppComponentwhenitisbootstrapped,thismodulewillinturnbetheonebootstrappedfromthetop-levelTypeScriptfile.Angular2pushesforthisconventionasamain.tsfile:
[app/main.ts]
import{platformBrowserDynamic}
from'@angular/platform-browser-dynamic';
import{AppModule}from'./app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
TheplatformBrowserDynamicmethodreturnsaplatformobjectthatexposesthebootstrapModulemethod.ItconfiguresyourapplicationtobebootstrappedwithAngular2'sjust-in-time(JIT)compiler.
Tip
Fornow,thedetailsofwhyyouarespecifyingjust-in-timecompilationaren'timportant.It'senoughtoknowthatJITcompilationisasimplerversion(asopposedtoahead-of-timecompilation)inAngular2'sofferings.
index.html
Finally,youneedtobuildanHTMLfilethatiscapableofbundlingtogetherallthesecompiledfilesandkickingofftheapplicationinitialization.Beginwiththefollowing:
[index.html]
<html>
<head>
<title>Angular2MinimumViableApplication</title>
<scriptsrc="node_modules/zone.js/dist/zone.js">
</script>
<scriptsrc="node_modules/reflect-metadata/Reflect.js">
</script>
<scriptsrc="node_modules/systemjs/dist/system.src.js">
</script>
</head>
<body>
<app-root></app-root>
</body>
</html>
Mostofthissofarshouldbeexpected.ZoneJSandReflectareAngular2dependencies.Themoduleloaderyou'lluseisSystemJS.<app-root>istheelementthatAppComponentwillrenderinside.
ConfiguringSystemJS
Next,SystemJSneedstobeconfiguredtounderstandhowtoimportmodulefilesandhowtoconnectmodulesfrombeingimportedinsideothermodules.Inotherwords,itneedstobegivenafiletobeginwithandadirectoryofmappingsfordependenciesofthatmainfile.Thiscanbe
accomplishedwithSystem.config()andSystem.import(),whicharemethodsexposedontheglobalSystemobject:
[index.html]
<html>
<head>
<title>Angular2MinimumViableApplication</title>
<scriptsrc="node_modules/zone.js/dist/zone.js">
</script>
<scriptsrc="node_modules/reflect-metadata/Reflect.js">
</script>
<scriptsrc="node_modules/systemjs/dist/system.src.js">
</script>
<script>
System.config({
paths:{
'ng:':'node_modules/@angular/'
},
map:{
'@angular/core':'ng:core/bundles/core.umd.js',
'@angular/common':'ng:common/bundles/common.umd.js',
'@angular/compiler':
'ng:compiler/bundles/compiler.umd.js',
'@angular/platform-browser':
'ng:platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic':
'ng:platform-browser-dynamic/bundles/platform-browser-
dynamic.umd.js',
'rxjs':'node_modules/rxjs'
},
packages:{
app:{
main:'./main.js'
},
rxjs:{
defaultExtension:'js'
}
}
});
System.import('app');
</script>
</head>
<body>
<app-root></app-root>
</body>
</html>
System.config()specifieshowSystemJSshouldhandlethefilespassedtoit.
Thepathspropertyspecifiesanaliastoshortenthepath'sinsidemap.Itactsasasimple
findandreplacefunction,soanyfoundinstancesofng:arereplacedwithnode_modules/@angular/.ThemappropertyspecifieshowSystemJSshouldresolvethemoduleimportsthatyouhavenotexplicitlydefined.Here,thistakestheformoffivecoreAngularmodulesandtheRxJSlibrary.Thepackagespropertyspecifiesthetargetsthatwillbeimportedbythispropertyandthefilestheyneedtomapto.
Tip
Forexample,theapppropertywillbeusedwhenamoduleimportsapp,andinsideSystemJS,thiswillmaptomain.js.Similarly,whenamodulerequiresanRxJSmodule,suchasSubject,SystemJSwilltaketherxjs/Subjectimportpath,recognizethatdefaultExtensionisspecifiedasjs,mapthemoduletoitsfilerepresentationnode_modules/rxjs/Subject.js,andimportit.
SeealsoComposingpackage.jsonforaminimumviableAngular2applicationdescribeshowallthepiecesworkforthecorenodeprojectfileConfiguringTypeScriptforaminimumviableAngular2applicationtalksabouthowtoconfigurecompilationtosupportanAngular2projectPerformingin-browsertranspilationwithSystemJSdemonstrateshowSystemJScanbeusedtoconnectuncompiledstaticfilestogetherMigratingtheminimumviableAngular2applicationtoWebpackbundlingdescribeshowtointegrateWebpackintoyourAngularapplicationbuildprocessIncorporatingshimsandpolyfillsintoWebpackgivesyouahandywayofmanagingAngular2polyfilldependenciesHTMLgenerationwithhtml-webpack-pluginshowsyouhowyoucanconfigureannpmpackagetoaddcompiledfilestoyourHTMLautomaticallySettingupanapplicationwithAngularCLIgivesadescriptionofhowtousetheCLI,whatitgivesyou,andwhattheseindividualpiecesdo
MigratingtheminimumviableapplicationtoWebpackbundlingItisadvantageousformanyreasonstomakeitaseasyandquickaspossiblefortheclienttoloadandrunthecodesentfromyourserver.Oneoftheeasiestandmosteffectivewaysofdoingthisisbybundlinglotsofcodeintoasinglefile.Innearlyallcases,itishighlyefficientforthebrowsertoloadasinglefilethatcontainsallthedependenciesrequiredtobootstrapanapplication.
WebpackoffersmanyusefultoolsandamongthemistheterrificJSbundler.Thisrecipedemonstrateshowyouwillbeabletocombineyourentireapplication(includingnpmpackagedependencies)intoasingleJavaScriptfilethatthebrowserwillbeserved.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/3310/.
GettingreadyYoushouldhavecompletedallthestepsgivenintheComposingconfigurationfilesforaminimumviableAngular2applicationandComposingapplicationfilesforaminimumviableAngular2applicationrecipes.npmstartshouldstartupthedevelopmentserver,anditshouldbevisibleatlocalhost:3000.
Howtodoit...Beginbyremovingtheapplication'sdependencyonSystemJS.webpackisabletoresolvedependenciesandbundleallyourfilesintoasingleJSfile.Beginbyinstallingwebpackwiththeglobalflag:
npminstallwebpack-g
webpack.config.js
webpacklooksforawebpack.config.jsfileforinstructionsonhowtobehave.Createthisnow:
[webpack.config.js]
module.exports={
entry:"./app/main.js",
output:{
path:"./dist",
filename:"bundle.js"
}
};
Nothingexceptionallycomplicatedisgoingonhere.Thistellswebpacktoselectmain.jsasthetop-levelapplicationfile,resolveallitsdependenciestothefilesthatdefinethem,andbundlethemintoasinglebundle.jsinsideadist/directory.
Tip
Atthispoint,youcancheckthatthisisworkingbyinvokingwebpackfromthecommandline,whichwillrunthebundler.Youshouldseebundle.jsappearinsidedist/withallthemoduledependenciesinsideit.
Thisisagoodstart,butthisgeneratedfilestillisn'tbeingusedanywhere.Next,you'llmodifyindex.htmltousethefile:
[index.html]
<html>
<head>
<title>Angular2MinimumViableApplication</title>
<scriptsrc="node_modules/zone.js/dist/zone.js">
</script>
<scriptsrc="node_modules/reflect-metadata/Reflect.js">
</script>
<scriptsrc="dist/bundle.js">
</script>
</head>
<body>
<app-root></app-root>
</body>
</html>
Probablynotwhatyouwereexpectingatall!Sincebundle.jsistheapplicationentrypointandSystemJSisnolongerneededtoresolveanymodules(becausewebpackisalreadydoingthisforyouwhenbundlingthefiles),youcanremovetheapplication'sdependencyonSystemJS.
Sincethisisthecase,youcanremovetheSystemdependencyfromyourpackage.jsonandaddthewebpackscriptsanddependency:
[package.json]
{
"name":"mva-bundling",
"scripts":{
"start":"tsc&&webpack&&concurrently'npmruntsc:w'
'npmrunwp:w''npmrunlite'",
"lite":"lite-server",
"postinstall":"npminstall-S@types/node@types/core-js",
"tsc":"tsc",
"tsc:w":"tsc-w",
"wp":"webpack",
"wp:w":"webpack--watch"
},
"dependencies":{
"@angular/common":"2.0.0",
"@angular/compiler":"2.0.0",
"@angular/core":"2.0.0",
"@angular/platform-browser":"2.0.0",
"@angular/platform-browser-dynamic":"2.0.0",
"core-js":"^2.4.1",
"reflect-metadata":"^0.1.3",
"rxjs":"5.0.0-beta.12",
"zone.js":"^0.6.23"
},
"devDependencies":{
"concurrently":"^2.2.0",
"lite-server":"^2.2.2",
"typescript":"^2.0.2",
"webpack":"^1.13.2"
}
}
WhetherornotwebpackandtypescriptbelongtodevDependencieshereisamatterofdisputeandislargelysubjecttohowyoumanageyourlocalenvironment.Ifyou'vealreadyinstalledthemwiththeglobalflag,thenyoudon'tneedtolistithereasadependency.Thisisbecausenpmwillsearchforgloballyinstalledpackagesandfindthemforyoutorunnpmscripts.Furthermore,listingitherewillinstalladuplicatewebpacklocaltothisproject,whichisobviouslyredundant.
Forthepurposeofthisrecipe,itishelpfultohaveithere.Thisisbecauseyoucanensurethatasinglenpminstallonthecommandlinewillfetchallthepackagesyouneedoffthebat,andthiswillletyouspecifytheversionyouwantwithintheproject.
Now,whenyouexecutenpmstart,thefollowingoccurs:
TypeScriptdoesaninitialcompilationof.tsfilesinto.jsfiles.WebpackdoesaninitialbundlingofalltheJSfilesintoasinglebundle.jsinthedist/directory.Simultaneously,lite-serverisstarted,theTypeScriptcompilerwatcherisstarted,andtheWebpackwatcherisstarted.Upona.tsfilechange,TypeScriptwillcompileitintoa.jsfile,andWebpackwillpickupthatfilechangeandrebundleitintobundle.js.Thelite-serverwillseethatbundle.jsischangedandreloadthepage,soyoucanseethechangesbeingupdatedautomatically.
Tip
Withoutspecifyingtheconfigurationsmoreclosely,theTypeScript,Webpack,andthelite-serverfilewatchlistswillusetheirdefaultsettings,whichmaybetoobroadandthereforewouldwatchfilestheydonotcareabout.Ideally,TypeScriptwouldonlywatch.tsfiles(whichdoesthiswithyourtsconfig.json),Webpackwouldonlywatch.html,.js,and.cssfiles,andlite-serverwouldonlywatchthefilesitactuallyservestotheclient.
SeealsoIncorporatingshimsandpolyfillsintoWebpackgivesyouahandywayofmanagingAngular2polyfilldependenciesHTMLgenerationwithhtml-webpack-pluginshowsyouhowyoucanconfigureannpmpackagetoaddcompiledfilestoyourHTMLautomatically
IncorporatingshimsandpolyfillsintoWebpackSofar,thishasbeenamuchcleanerimplementation,butyoustillhavethetwodanglingshimsinsidetheindex.htmlfile.You'vepareddownindex.htmlsuchthatitisnowrequestingonlyahandfulofJSfilesinsteadofeachmoduletargetindividually,butyoucangoevenfurtherandbundlealltheJSfilesintoasinglefile.
Thechallengeinthisisthatbrowsershimsaren'tdeliveredviamodules;inotherwords,therearen'tanyotherfilesthatwillimportthesetousethem.Theyjustassumetheiruseisavailable.Therefore,thestandardWebpackbundlingwon'tpickupthesetargetsandincludetheminthebundledfile.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/7479/.
GettingreadyYoushouldcompletetheMigratingtheminimumviableapplicationtoWebpackbundlingrecipefirst,whichwillgiveyouallthesourcefilesneededforthisrecipe.
Howtodoit...Thereareanumberofwaystogoaboutdoingthis,includingsomethatinvolvetheadditionofWebpackplugins,butthere'sanextremelysimplewayaswell:justaddtheimportsmanually.
Createanewpolyfills.ts:
[src/polyfills.ts]
import"reflect-metadata";
import"zone.js";
Importthismodulefrommain.ts:
[src/main.ts]
import'./polyfills';
import{platformBrowserDynamic}
from'@angular/platform-browser-dynamic';
import{AppModule}from'./app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
Finally,cleanupindex.html:
[index.html]
<html>
<head>
<title>Angular2MinimumViableApplication</title>
<body>
<app-root></app-root>
<scriptsrc="dist/bundle.js"></script>
</body>
</html>
Now,Webpackshouldbeabletoresolvetheshimimports,andalltheneededfileswillbeincludedinsidebundle.js.
Howitworks...TheonlyreasonthatthepolyfillsarenotdiscoveredbyWebpackisbecausetheyarenotrequiredanywhereintheapplication.Rather,anywheretheyareusedleadstotheassumptionthattheexposedtargets,suchasZone,havepreviouslybeenmadeavailable.Therefore,itiseasyforyoutosimplyimportthemattheverytopofyourapplication,whichhasawell-definedpointinthecode.WiththisWebpack,youwillbeabletodiscovertheexistenceofpolyfillsandincorporatethemintothegeneratedbundle.
SeealsoMigratingtheminimumviableAngular2applicationtoWebpackbundlingdescribeshowtointegrateWebpackintoyourAngularapplicationbuildprocessHTMLgenerationwithhtml-webpack-pluginshowsyouhowyoucanconfigureannpmpackagetoaddcompiledfilestoyourHTMLautomatically
HTMLgenerationwithhtml-webpack-pluginIdeally,youwouldliketobeabletohaveWebpackmanagethebundledfileanditsinjectionintothetemplate.Bydefault,Webpackisunabletodothis,asitisonlyconcernedwiththefilesrelatedtoscripting.Fortunately,WebpackoffersanextremelypopularpluginthatallowsyoutoexpandthescopeofWebpack'sfileconcerns.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/7185/.
GettingreadyInstallthepluginandaddittodevDependenciesofpackage.jsonwiththefollowing:
npminstallhtml-webpack-plugin--save-dev
Howtodoit...First,you'llneedtoincorporatethepluginintopackage.jsonifitisn'talready:
[package.json]
{
"name":"mva-bundling",
"scripts":{
"start":"tsc&&webpack&&concurrently'npmruntsc:w'
'npmrunwp:w''npmrunlite'",
"lite":"lite-server",
"postinstall":"npminstall-S@types/node@types/core-js",
"tsc":"tsc",
"tsc:w":"tsc-w",
"wp":"webpack",
"wp:w":"webpack--watch"
},
"dependencies":{
"@angular/common":"2.0.0",
"@angular/compiler":"2.0.0",
"@angular/core":"2.0.0",
"@angular/platform-browser":"2.0.0",
"@angular/platform-browser-dynamic":"2.0.0",
"core-js":"^2.4.1",
"reflect-metadata":"^0.1.3",
"rxjs":"5.0.0-beta.12",
"zone.js":"^0.6.23"
},
"devDependencies":{
"concurrently":"^2.2.0",
"html-webpack-plugin":"^2.22.0",
"imports-loader":"^0.6.5",
"lite-server":"^2.2.2",
"typescript":"^2.0.2",
"webpack":"^1.13.2"
}
}
Oncethismoduleisinstalled,defineitsoperationinsidetheWebpackconfig:
[webpack.config.js]
varHtmlWebpackPlugin=require('html-webpack-plugin');
module.exports={
entry:"./src/main.js",
output:{
path:"./dist",
filename:"bundle.js"
},
plugins:[newHtmlWebpackPlugin({
template:'./src/index.html'
})]
};
ThisspecifiestheoutputHTMLfilethatwillservetheentireapplication.SincethepluginwillautomaticallygeneratetheHTMLfileforyou,you'llneedtomodifytheexistingonethatisdesignatedasthetemplate:
[src/index.html]
<html>
<head>
<title>Angular2MinimumViableApplication</title>
</head>
<body>
<app-root></app-root>
</body>
</html>
Finally,becauseindex.htmlisnowservedoutofthedist/directory,you'llneedtoconfigurethedevelopmentservertoservefilesoutofthere.Sincelite-serverisjustawrapperforBrowserSync,youcanspecifybaseDirinsideabs-config.jsonfile,whichyoushouldcreatenow:
[bs-config.json]
{
"server":{"baseDir":"./dist"}
}
Howitworks...Webpackisverymuchawareofthebundlethatitiscreating,andsoitmakessensethatyouwouldbeabletomaintainareferencetothisbundle(orbundles)anddirectlypipethosepathsintoanindex.htmlfile.ThepluginwillappendthescriptsattheendofthebodytoensuretheentireinitialDOMispresent.
SeealsoMigratingtheminimumviableAngular2applicationtoWebpackbundlingdescribeshowtointegrateWebpackintoyourAngularapplicationbuildprocessIncorporatingshimsandpolyfillsintoWebpackgivesyouahandywayofmanagingAngular2polyfilldependencies
SettingupanapplicationwithAngularCLIIntandemwiththeAngular2framework,theAngularteamalsosupportsabuildtoolthatcancreate,build,andrunanAngular2applicationrightoutofthebox.What'smore,itincludesageneratorthatcancreatestyle-guide-compliantfilesanddirectoriesforvariousapplicationpiecesfromthecommandline.
Note
Thecode,links,andaliveexampleofthisareavailableathttp://ngcookbook.herokuapp.com/4068/.
GettingreadyAngular'sCLIisannpmmodule.You'llneedtohaveNode.jsinstalledonyoursystem—v7.0.0orlaterworksasasuitablerecentreleasethat'scompatiblewiththeAngularCLI.
Tip
Thereisanotheroptionyouhave:manageyourNodeenvironmentswithnvm,theNodeversionmanager.ThisgivesyouatransparentwrapperthatcanseparatelymanageenvironmentswiththeNodeversionaswellastheinstallednpmpackagesinthatenvironment.Ifyou'veeverdealtwithmessinessinvolvingsudonpminstall-g,youwillbedelightedbythistool.
OnceNodeisinstalled(andifyouusenvm,you'veselectedwhichenvironmenttouse),installtheAngularCLI:
npminstall-gangular-cli
Howtodoit...AngularCLIcomesreadytogenerateafullyworkingAngular2application.TocreateanapplicationnamedPublisherApp,invokethefollowingcommand:
ngnewpublisher
TheAngularCLIwilldutifullyassembleallthefilesneededforaminimalAngular2TypeScriptapplication,initializeaGitrepository,andinstallalltherequirednpmdependencies.Thecreatedfilelistshouldlookasfollows:
createREADME.md
createsrc/app/app.component.css
createsrc/app/app.component.html
createsrc/app/app.component.spec.ts
createsrc/app/app.component.ts
createsrc/app/app.module.ts
createsrc/app/index.ts
createsrc/app/shared/index.ts
createsrc/environments/environment.prod.ts
createsrc/environments/environment.ts
createsrc/favicon.ico
createsrc/index.html
createsrc/main.ts
createsrc/polyfills.ts
createsrc/styles.css
createsrc/test.ts
createsrc/tsconfig.json
createsrc/typings.d.ts
createangular-cli.json
createe2e/app.e2e-spec.ts
createe2e/app.po.ts
createe2e/tsconfig.json
create.gitignore
createkarma.conf.js
createpackage.json
createprotractor.conf.js
createtslint.json
Usecdpublishertomoveintotheapplication'sdirectory,whichwillallowyoutoinvokealltheproject-specificAngularCLIcommands.
Runningtheapplicationlocally
Torunthisapplication,startuptheserver:
ngserve
Thedefaultapplicationpagewillbeavailableonlocalhost:4200.
Testingtheapplication
Toruntheapplication'sunittests,usethis:
ngtest
Toruntheapplication'send-to-endtests,usethis:
nge2e
Howitworks...Let'sroughlygothroughwhateachofthesefilesoffertoyou:
Projectconfigurationfilesangular-cli.jsonistheconfigurationfilespecifyinghowtheAngularCLIshouldbundleandmanageyourapplication'sfilesanddirectories.package.jsonisthenpmpackageconfigurationfile.Insideit,you'llfindscriptsandcommand-linetargetsthattheAngularCLIcommandswilltieinto.
TypeScriptconfigurationfilestslint.jsonspecifiestheconfigurationforthetslintnpmpackage.TheAngularCLIcreatesforyoualintcommandfor.tsfileswithnpmrunlint.src/tsconfig.jsonispartoftheTypeScriptspecification;itinformsthecompilerthatthisistherootoftheTypeScriptproject.Itscontentsdefinehowthecompilationshouldoccur,anditspresenceenablesthetsccommandtousethisdirectoryastherootcompilationdirectory.e2e/tsconfig.jsonistheend-to-endTypeScriptcompilerconfigurationfile.src/typings.d.tsisthespecificationfileforthetypingsnpmmodule.ItallowsyoutodescribehowexternalmodulesshouldbewrappedandincorporatedintotheTypeScriptcompiler.Thistypings.d.tsfilespecifiestheSystemnamespaceforSystemJS.
Testconfigurationfileskarma.conf.jsistheconfigurationfileforKarma,thetestrunnerfortheprojectprotractor.conf.jsistheconfigurationfileforProtractor,theend-to-endtestframeworkfortheprojectsrc/test.tsdescribestotheKarmaconfigurationhowtostartupthetestrunnerandwheretofindthetestfilesthroughouttheapplication
Coreapplicationfilessrc/index.htmlistherootapplicationfilethatisservedtoruntheentiresingle-pageapplication.CompiledJSandotherstaticassetswillbeautomaticallyaddedtothisfilebythebuildscript.src/main.tsisthetop-levelTypeScriptfilethatservestobootstrapyourapplicationwithitsAppModuledefinition.src/polyfills.tsisjustafilethatkeepsthelonglistofimportedpolyfillmodulesoutofmain.ts.src/styles.cssistheglobalapplicationstylefile.
Environmentfilessrc/environments/environment.tsisthedefaultenvironmentconfigurationfile.Specifyingdifferentenvironmentswhenbuildingandtestingyourapplicationwilloverride
these.src/environments/environment.prod.tsistheprodenvironmentconfiguration,whichcanbeselectedfromthecommandlinewith--prod.
AppComponentfiles
EveryAngular2applicationhasatop-levelcomponent,andAngularCLIcallsthisAppComponent.
src/app/app.component.tsisthecoreTypeScriptcomponentclassdefinition.Thisiswhereallofthelogicthatcontrolsthiscomponentshouldgo.src/app/app.component.htmlandsrc/app/app.component.cssarethetemplatingandstylingfilesspecifictoAppComponent.RecallthatstylingspecifiedinComponentMetadataisencapsulatedonlytothiscomponent.src/app/app.module.tsistheNgModuledefinitionforAppComponent.src/app/index.tsisthefilethatinformstheTypeScriptcompilerwhichmodulesareavailableinsidethisdirectory.Anymodulesthatareexportedinthisdirectoryandusedelsewhereintheapplicationmustbespecifiedhere.
AppComponenttestfilessrc/app/app.component.spec.tsaretheunittestsforAppComponente2e/app.e2e-spec.tsaretheend-to-endtestsforAppComponente2e/app.po.tsisthepageobjectdefinitionforuseinAppComponentend-to-endtesting
There'smore...Whenlookingattheentireprojectcodebase,thebulkofthefilesbreakdownintofourcategories:
Filesthataresenttothebrowser:Thisincludesyouruncompiled/unminifiedapplicationfilesandalsothecompiled/minifiedfiles.Whendevelopingyourapplication,youwanttobeabletotestyourapplicationlocallywithuncompiledfiles.Youalsowanttobeabletoshipyourapplicationtoproductionwithcompiledandminifiedfiles,whichoptimizesbrowserperformance.Theuncompiled/unminifiedfilesarecollectedbythebuildscripts,tobecombinedintothecompiled/minifiedfiles.Filesusedfortesting:Thetestfilesthemselvesareusuallysprinkledthroughoutyourapplicationandarenotcompiled.Thiscategoryalsoincludesconfigurationfilesandtestscriptsthatcontrolwhatactuallyhappenswhenyourunthetestsandwherethetestrunnerscanfindthetestfilesinyourprojectdirectory.Filesthatcontrolyourdevelopmentenvironment:Dependingonyoursetup,yoursingle-pageapplicationmayrunbyitself(withnobackendcodebase),oritmaybebuiltalongsideasubstantialbackendcodebasethatexposesAPIsandotherserver-sidebehavior.Quickstartrepositoriesorapplicationgenerators(suchasAngularCLI)usuallyprovideyouwithaminimalHTTPservertogetyouoffthegroundandserveyourstaticassetstothebrowser.Howexactlyyourunyourdevelopmentenvironmentwillvary,butthefilesinthiscategorymanagehowyourapplicationwillworkbothlocallyandinproduction.Filesthatcompileyourapplication:Thefilesyoueditinyourcodeeditorofchoicearenottheonesthatreachthebrowserinaproductionapplication.Buildscriptsareusuallysetuptocombineallyourfilesintothesmallestandfewestfilespossible.Frequently,thiswillmeanasinglecompiledJSandcompiledCSSfiledeliveredtothebrowser.Thesefileswillminifyyourcodebase,compileTypeScriptintovanillaJavaScript,selectenvironmentfilesandothercontext-specificfiles,andorganizefileincludesandotherfilesandmoduledependenciessothatyourapplicationworkswhenit'scompiled.Usually,thefilestheycreatewillbedumpedintoadistdirectory,whichwillcontainfilesthatareservedtothebrowserinproduction.
SeealsoComposingpackage.jsonforaminimumviableAngular2applicationdescribeshowallthepiecesworkforthecorenodeprojectfileConfiguringTypeScriptforaminimumviableAngular2applicationtalksabouthowtoconfigurecompilationtosupportanAngular2projectPerformingin-browsertranspilationwithSystemJSdemonstrateshowSystemJScanbeusedtoconnectuncompiledstaticfilestogetherComposingapplicationfilesforaminimumviableAngular2applicationwalksyouthroughhowtocreateanextremelysimpleAngular2appfromscratchIncorporatingshimsandpolyfillsintoWebpackgivesyouahandywayofmanagingAngular2polyfilldependencies
Chapter9.Angular2TestingThischapterwillcoverthefollowingrecipes:
CreatingaminimumviableunittestsuitewithKarma,Jasmine,andTypeScriptWritingaminimumviableunittestsuiteforasimplecomponentWritingaminimumviableend-to-endtestsuiteforasimpleapplicationUnittestingasynchronousserviceUnittestingacomponentwithaservicedependencyusingstubsUnittestingacomponentwithaservicedependencyusingspies
IntroductionWritingtestsislikebrushingyourteeth.Youcangetawaywithskippingitforawhile,butit'llcatchupwithyoueventually.
Theworldoftestingisawashwithconflictingideologies,platitudes,andgrandstanding.What'smore,thereisadizzyingarrayoftoolsavailablethatallowyoutowriteandrunyourtestsindifferentways,automateyourtests,oranalyzeyourtestcoverageorcorrectness.Ontopofthat,eachdeveloper'sutilityandstyleoftestingisunique;someonehackingawayatapre-seedstartupwillnothavethesamerequirementsasadeveloperthatispartofalargeteaminsideaFortune500company.
ThegoalofthischapteristowalkyouthroughtheavailabletestingutilitiesthattheAngular2frameworkcomeswithoutofthebox,aswellassomestrategiesfordeployingtheseutilities.TherecipeswillfocusonunittestsratherthanE2Etests,asanoverwhelmingmajorityofrobusttestsuiteswillbeunittests.
CreatingaminimumviableunittestsuitewithKarma,Jasmine,andTypeScriptBeforeyoujumpintotheintricaciesoftestinganAngular2application,it'simportanttofirstexaminethesupportinginfrastructurethatwillmakerunningthesetestspossible.ThebulkofofficialAngularresourcesoffertestsontopofKarmaandJasmine,andthere'snoreasontorocktheboatonthisone,asthesearebothfinetestingtools.Thatsaid,it'sawholenewworldwithTypeScriptinvolved,andusingthemintestswillrequiresomeconsiderations.
Thisrecipewilldemonstratehowtoputtogetheraverysimpleunittestsuite.ItwilluseKarmaandJasmineasthetestinfrastructure,TypeScriptandWebpackforcompilationandmodulesupport,andPhantomJSasthetestbrowser.Forthoseunfamiliarwiththesetools,here'sabitaboutthem:
Karmaisaunittestrunner.YourunteststhroughKarmaonthecommandline.Ithastheabilitytostartupatestserverthatunderstandshowtofindtestfilesandservethemtothetestbrowser.Jasmineisatestframework.Whenyouusekeywordssuchas"it"and"describe,"rememberthattheyarepartofJasmineunittests.ItintegrateswithKarmaandunderstandshowtoexposeandrunthetestsyou'vewritten.PhantomJSisaheadlesswebkitbrowser.(HeadlessmeansitrunsasaprocessthatdoesnothaveavisibleuserinterfacebutstillconstructsaDOMandhasaJSruntime.)Unittestsrequireabrowsertorun,astheJavaScriptunittestsaredesignedtoexecuteinsideabrowserruntime.Karmasupportsalargenumberofbrowserpluginstorunthetestson,includingstandardbrowserssuchasChromeandFirefox.Ifyouweretoincorporatethesebrowserplugins,Karmawouldstartupaninstanceofthebrowserandrunthetestsinsideit.Forthepurposeofcreatingaminimumviableunittestsuite,youarefinedoingthetestinginsideaheadlessbrowser,whichwillcleanlyreportitsresultstothecommandline.Ifyouwanttorunyourtestsinsideanactualbrowser,Karmawillexposetheserverataspecifiedport,whichyoucanaccessdirectly,forexample,visitinghttp://localhost:9876inthedesiredtestbrowser.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/3998/.
GettingreadyStartoutwithapackage.jsonfile:
[package.json]
{}
Note
ThisstillneedstobeavalidJSONfile,asnpmneedstobeabletoparseitandaddtoit.
Howtodoit...Startoffbycreatingthefilethatwillbetested.YouintendtouseTypeScript,sogoaheadanduseitssyntaxhere:
[src/article.ts]
exportclassArticle{
title:string=
"LabMiceStrikeforImprovedWorkingConditions,Benefits"
}
Writingaunittest
WiththeArticleclassdefined,youcannowimportitintoanewtestfile,article.spec.ts,anduseit.
Note
Jasminetestfiles,byconvention,aresuffixedwith.spec.ts.TestfilesgeneratedbytheAngularCLIwillexistalongsidethefiletheytest,butbynomeansisthismandatory.YoucandefineyourconventioninsideyourKarmaconfigurationlateron.
StartoffbyimportingtheArticleclassandcreateanemptyJasminetestsuiteusingdescribe:
[src/article.spec.ts]
import{Article}from'./article';
describe('Articleunittests',()=>{
});
Note
describedefinesaspecsuite,whichincludesastringtitlecalledArticleunittests,andananonymousfunction,whichcontainsthesuite.Aspecsuitecanbenestedinsideanotherspecsuite.
Insideadescribesuitefunction,youcandefinebeforeEachandafterEach,whicharefunctionsthatexecutebeforeandaftereachunittestisdefinedinsidethesuite.Therefore,itispossibletodefinenestedsetuplogicforunittestsusingnesteddescribeblocks.
Insidethespecsuitefunction,writetheunittestthatisusingit:
[src/article.spec.ts]
import{Article}from'./article';
describe('Articleunittests',()=>{
it('Hascorrecttitle',()=>{
leta=newArticle();
expect(a.title)
.toBe("LabMiceStrikeforImprovedWorkingConditions,
Benefits");
});
});
NotethatboththecodeandthetestarewritteninTypeScript.
ConfiguringKarmaandJasmine
First,installKarma,theKarmaCLI,Jasmine,andtheKarmaJasmineplugin:
npminstallkarmajasmine-corekarma-jasmine--save-dev
npminstallkarma-cli-g
Alternately,ifyouwanttosaveafewkeystrokes,thefollowingisequivalent:
npmi-Dkarmajasmine-corekarma-jasmine
npmikarma-cli-g
Karmareadsitsconfigurationoutofakarma.conf.jsfile,socreatethatnow:
[karma.conf.js]
module.exports=function(config){
config.set({
})
}
KarmaneedstoknowhowtofindthetestfilesandalsohowtouseJasmine:
[karma.conf.js]
module.exports=function(config){
config.set({
frameworks:[
'jasmine'
],
files:[
'src/*.spec.js'
],
plugins:[
'karma-jasmine',
]
})
}
ConfiguringPhantomJS
PhantomJSallowsyoutodirecttestsentirelyfromthecommandline,butKarmaneedstounderstandhowtousePhantomJS.InstallthePhantomJSplugin:
npminstallkarma-phantomjs-launcher--save-dev
Next,incorporatethispluginintotheKarmaconfig:
[karma.conf.js]
module.exports=function(config){
config.set({
browsers:[
'PhantomJS'
],
frameworks:[
'jasmine'
],
files:[
'src/*.spec.js'
],
plugins:[
'karma-jasmine',
'karma-phantomjs-launcher'
]
})
}
KarmanowknowsithastorunthetestsinPhantomJS.
CompilingfilesandtestswithTypeScript
Ifyou'repayingattentionclosely,you'llnotethattheKarmaconfigisreferencingtestfilesthatdonotexist.Sinceyou'reusingTypeScript,youmustcreatethesefiles.InstallTypeScriptandtheJasminetypedefinitions:
npminstalltypescript@types/jasmine--save-dev
Addscriptdefinitionstoyourpackage.json:
[package.json]
{
"scripts":{
"tsc":"tsc",
"tsc:w":"tsc-w"
},
"devDependencies":{
"@types/jasmine":"^2.5.35",
"jasmine-core":"^2.5.2",
"karma":"^1.3.0",
"karma-cli":"^1.0.1",
"karma-jasmine":"^1.0.2",
"karma-phantomjs-launcher":"^1.0.2",
"typescript":"^2.0.3"
}
}
Createatsconfig.jsonfile.Sinceyou'refinewiththecompiledfilesresidinginthesamedirectory,asimpleonewilldo:
[tsconfig.json]
{
"compilerOptions":{
"target":"es5",
"module":"commonjs",
"moduleResolution":"node"
}
}
Tip
Youwouldprobablynotdoitthiswayforaproductionapplication,butforaminimumviablesetup,thiswilldoinapinch.Aproductionapplicationwouldmostlikelyputcompiledfilesintoanentirelydifferentdirectory,frequentlynameddist/.
IncorporatingWebpackintoKarma
Ofcourse,you'llneedawayofresolvingmoduledefinitionsforcodeandtests.Karmaisn'tcapableofdoingthisonitsown,soyou'llneedsomethingtodothis.Webpackisperfectlysuitableforsuchatask,andKarmahasaterrificpluginthatallowsyoutopreprocessyourtestfilesbeforetheyreachthebrowser.
InstallWebpackanditsKarmaplugin:
npminstallwebpackkarma-webpack--save-dev
ModifytheKarmaconfigtospecifyWebpackasthepreprocessor.Thisallowsyourmoduledefinitionstoberesolvedproperly:
[karma.conf.js]
module.exports=function(config){
config.set({
browsers:[
'PhantomJS'
],
frameworks:[
'jasmine'
],
files:[
'src/*.spec.js'
],
plugins:[
'karma-webpack',
'karma-jasmine',
'karma-phantomjs-launcher'
],
preprocessors:{
'src/*.spec.js':['webpack']
}
})
}
Writingthetestscript
YoucankickofftheKarmaserverwiththefollowing:
karmastartkarma.conf.js
Thiswillinitializethetestserverandrunthetests,watchingforchangesandrerunningthetests.However,thissidestepsthefactthattheTypeScriptfilesrequirecompilationinthefilesthatKarmaiswatching.TheTypeScriptcompileralsohasafilewatcherthatwillrecompileonthefly.Youwouldlikebothofthesetorecompilewheneveryousavechangestoasourcecodefile,soitmakessensetorunthemsimultaneously.Theconcurrentlypackageissuitableforthistask.
Note
concurrentlynotonlyallowsyoutorunmultiplecommandsatonce,butalsotokillthemallatonce.Withoutit,akillsignalfromthecommandlinewouldonlytargetwhicheverprocesswasrunmostrecently,ignoringtheprocessthatisrunninginthebackground.
Installconcurrentlywiththefollowing:
npminstallconcurrently--save-dev
Finally,buildyourtestscripttorunKarmaandtheTypeScriptcompilersimultaneously:
[package.json]
{
"scripts":{
"test":"concurrently'npmruntsc:w''karmastart
karma.conf.js'",
"tsc":"tsc",
"tsc:w":"tsc-w"
},
"devDependencies":{
"@types/jasmine":"^2.5.35",
"concurrently":"^3.1.0",
"jasmine-core":"^2.5.2",
"karma":"^1.3.0",
"karma-cli":"^1.0.1",
"karma-jasmine":"^1.0.2",
"karma-phantomjs-launcher":"^1.0.2",
"karma-webpack":"^1.8.0",
"typescript":"^2.0.3",
"webpack":"^1.13.2"
}
}
Withthis,youshouldbeabletorunyourtests:
npmtest
Ifeverythingisdonecorrectly,theKarmaservershouldbootupandrunthetests,outputtingthefollowing:
PhantomJS2.1.1(Linux0.0.0):Executed1of1SUCCESS(0.038secs/0.001
secs)
Howitworks...KarmaandJasmineworktogethertodelivertestfilestothetestbrowser.TypeScriptandWebpackaretaskedwithconvertingyourTypeScriptfilesintoaJavaScriptformatthatwillbeusablebythetestbrowser.
There'smore...AninterestingconsiderationofthissetupishowexactlyTypeScriptishandled.
BoththecodeandtestfilesarewritteninTypeScript,whichallowsyoutousetheES6modulenotation,asopposedtosomemix-and-matchstrategy.However,thisleavesyouwithsomechoicestomakeonhowthetestsetupshouldwork.
Thetestsneedtobeabletousedifferentpiecesofyourapplicationinapiecemealfashion,asopposedtothestandardapplicationsetupwhereallthemodulesgetpulledtogetheratonce.ThisrecipehadTypeScriptindependentlycompilethe.tsfiles,anditthendirectedKarmatowatchtheresultant.jsfiles.Thisisperhapseasiertocomprehendbysomeonewhoiseasingintotests,butitmightnotbethemostefficientwaytogoaboutit.KarmaalsosupportsTypeScriptplugins,whichallowyoutopreprocessthefilesintoTypeScriptbeforehandingthemofftotheWebpackpreprocessor.
Karmasupportsthechainingofpreprocesssteps,whichwillbeusefulifyouwanttocompiletheTypeScriptontheflyaspartofpreprocessing.
SeealsoWritingaminimumviableunittestsuiteforasimplecomponentshowsyouabasicexampleofunittestingAngular2componentsUnittestingasynchronousservicedemonstrateshowaninjectionismockedinunittestsUnittestingacomponentwithaservicedependencyusingStubsshowshowyoucancreateaservicemocktowriteunittestsandavoiddirectdependenciesUnittestingacomponentwithaservicedependencyusingSpiesshowshowyoucankeeptrackofservicemethodinvocationsinsideaunittest
WritingaminimumviableunittestsuiteforasimplecomponentUnittestsarethebreadandbutterofyourapplicationtestingprocess.Theyexistasacompaniontoyoursourcecode,andmostofthetime,thebulkofyourapplicationtestswillbeunittests.Theyarelightweight,runquickly,areeasytoreadandreasonabout,andcangivecontextastohowthecodeshouldbeusedandhowitmightbehave.
SettingupKarma,Jasmine,TypeScript,andAngular2alongwithalltheconnectingconfigurationsbetweenthemisabitofanimposingtask;itwasdeemedtobeoutofthescopeofthischapter.It'snotaveryinterestingdiscussiontogetallofthemtoworktogether,especiallysincetherearealreadysomanyexampleprojectsthathaveputtogethertheirownsetupsforyou.It'sfarmoreinterestingtodivedirectlyintotheteststhemselvesandseehowtheycanactuallyinteractwithAngular2.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/3935/.
GettingreadyThisrecipewillassumeyouareusingaworkingAngular2testingenvironment.TheoneprovidedintheapplicationgeneratedbytheAngularCLIisideal.Testscanberuninthisenvironmentwiththefollowingcommandinsidetheprojectdirectory:
ngtest
Beginwiththefollowingcomponent:
[src/app/article/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'app-article',
template:`
<h1>
{{title}}
</h1>
`
})
exportclassArticleComponent{
title:string='CaptainHookSuesOverSporkSnafu';
}
Yourgoalistoentirelyfleshoutarticle.component.spec.tstotestthisclass.
Howtodoit...ThesimplestpossibletestyoucanthinkofistheonethatwillsimplycheckthatyouareabletoinstantiateaninstanceofArticleComponent.Beginwiththattest:
[src/app/article/article.component.spec.ts]
import{ArticleComponent}from'./article.component';
describe('Component:Article',()=>{
it('shouldcreateaninstance',()=>{
letcomponent=newArticleComponent();
expect(component).toBeTruthy();
});
});
Nothingtrickyisgoingonhere.SinceArticleComponentisjustaplainoldTypeScriptclass,nothingispreventingyoufromcreatinganinstanceandinspectingitinthememory.
However,forittoactuallybehavelikeanAngular2component,you'llneedsomeothertools.
UsingTestBedandasync
WhenyoutrytopuppetanAngular2environmentforthecomponentinatest,thereareanumberofconsiderationsyou'llneedtoaccountfor.First,Angular2unittestsheavilyrelyuponTestBed,whichcanbethoughtofasyourtestingmultitool.
ThedenominationofunittestswhendealingwithacomponentinvolvesComponentFixture.TestBed.createComponent()willcreateafixturewrappinganinstanceofthedesiredcomponent.
Tip
Theneedforfixturesiscenteredinhowunittestsaresupposedtowork.AnArticleComponentdoesnotmakesensewheninstantiatedasitwaswiththeinitialtestyouwrote.ThereisnoDOMelementtoattachto,norunningapplication,andsoon.Itdoesn'tmakesenseforthecomponentunitteststohaveanexplicitdependencyonthesethings.So,ComponentFixtureisAngular'swayoflettingyoutestonlytheconcernsofthecomponentasitwouldnormallyexist,withoutworryingaboutallthemessinessofitsinnatedependencies.
TheTestBedfixture'sasynchronousbehaviormandatesthatthetestlogicisexecutedinsideanasync()wrapper.
Tip
Theasync()wrappersimplyrunsthetestinsideitsownzone.Thisallowsthetestrunnertowait
foralltheasynchronouscallsinsidethetesttocompletethembeforeendingthetest.
BeginbyimportingTestBedandasyncfromtheAngulartestingmoduleandputtogethertheskeletonfortwomoreunittests:
[src/app/article/article.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{ArticleComponent}from'./article.component';
describe('Component:Article',()=>{
it('shouldcreateaninstance',()=>{
letcomponent=newArticleComponent();
expect(component).toBeTruthy();
});
it('shouldhavecorrecttitle',async(()=>{
}));
it('shouldrendertitleinanh1tag',async(()=>{
}));
});
Nowthatyouhavetheskeletonsforthetwotestsyou'dliketowrite,it'stimetouseTestBedtodefinethetestmodule.Angular2componentsarepairedwithamoduledefinition,butwhenperformingunittests,you'llneedtousetheTestBedmodule'sdefinitionforthecomponenttoworkproperly.ThiscanbedonewithTestBed.configureTestModule(),andyou'llwanttoinvokethisbeforeeachtest.
Jasmine'sdescribeallowsyoutogroupbeforeEachandafterEachinsideit,anditisperfectforusehere:
[src/app/article/article.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{ArticleComponent}from'./article.component';
describe('Component:Article',()=>{
it('shouldcreateaninstance',()=>{
letcomponent=newArticleComponent();
expect(component).toBeTruthy();
});
describe('Async',()=>{
beforeEach(()=>{
TestBed.configureTestingModule({
declarations:[
ArticleComponent
],
});
});
it('shouldhavecorrecttitle',async(()=>{
}));
it('shouldrendertitleinanh1tag',async(()=>{
}));
});
});
CreatingaComponentFixture
TestBedgivesyoutheabilitytocreateafixture,butyouhaveyettoactuallydoit.You'llneedafixtureforboththeasynctests,soitmakessensetodothisinbeforeEachtoo:
[src/app/article/article.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{ArticleComponent}from'./article.component';
describe('Component:Article',()=>{
letfixture;
it('shouldcreateaninstance',()=>{
letcomponent=newArticleComponent();
expect(component).toBeTruthy();
});
describe('Async',()=>{
beforeEach(()=>{
TestBed.configureTestingModule({
declarations:[
ArticleComponent
],
});
fixture=TestBed.createComponent(ArticleComponent);
}));
afterEach(()=>{
fixture=undefined;
});
it('shouldhavecorrecttitle',async(()=>{
}));
it('shouldrendertitleinanh1tag',async(()=>{
}));
});
});
Tip
Here,fixtureisassignedtoundefinedintheafterEachteardown.Thisistechnically
superfluousforthepurposeofthesetests,butitisgoodtogetintothehabitofperformingarobustteardownofsharedvariablesinunittests.Thisisbecauseoneofthemostfrustratingthingstodebuginatestsuiteistestvariablebleed.Afterall,thesearejustfunctionsrunninginasequenceinabrowser.
Nowthatthefixtureisdefinedforeachtest,youcanuseitsmethodstoinspecttheinstantiatedcomponentindifferentways.
Forthefirsttest,you'dliketoinspecttheArticleComponentobjectitselffromwithinComponentFixture.ThisisexposedwiththecomponentInstanceproperty:
[src/app/article/article.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{ArticleComponent}from'./article.component';
describe('Component:Article',()=>{
letexpectedTitle='CaptainHookSuesOverSporkSnafu';
letfixture;
it('shouldcreateaninstance',()=>{
letcomponent=newArticleComponent();
expect(component).toBeTruthy();
});
describe('Async',()=>{
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations:[
ArticleComponent
],
});
fixture=TestBed.createComponent(ArticleComponent);
}));
afterEach(()=>{
fixture=undefined;
});
it('shouldhavecorrecttitle',async(()=>{
expect(fixture.componentInstance.title)
.toEqual(expectedTitle);
}));
it('shouldrendertitleinanh1tag',async(()=>{
}));
});
});
Forthesecondtest,youwantaccesstotheDOMthatthefixturehasattachedthecomponentinstanceto.TherootelementthatthecomponentistargetingisexposedwiththenativeElement
property:
[src/app/article/article.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{ArticleComponent}from'./article.component';
describe('Component:Article',()=>{
letexpectedTitle='CaptainHookSuesOverSporkSnafu';
letfixture;
it('shouldcreateaninstance',()=>{
letcomponent=newArticleComponent();
expect(component).toBeTruthy();
});
describe('Async',()=>{
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations:[
ArticleComponent
],
});
fixture=TestBed.createComponent(ArticleComponent);
}));
afterEach(()=>{
fixture=undefined;
});
it('shouldhavecorrecttitle',async(()=>{
expect(fixture.componentInstance.title)
.toEqual(expectedTitle);
}));
it('shouldrendertitleinanh1tag',async(()=>{
expect(fixture.nativeElement.querySelector('h1')
.textContent).toContain(expectedTitle);
}));
});
});
Ifyourunthesetests,youwillnoticethatthelasttestwillfail.Thetestseesanemptystringinside<h1></h1>.Thisisbecauseyouarebindingavalueinthetemplatetoacomponentmember.Sincethefixturecontrolstheentireenvironmentsurroundingthecomponent,italsocontrolsthechangedetectionstrategy—which,here,istonotrununtilitistoldtodoso.YoucantriggeraroundofchangedetectionusingthedetectChanges()method:
[src/app/article/article.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{ArticleComponent}from'./article.component';
describe('Component:Article',()=>{
letexpectedTitle='CaptainHookSuesOverSporkSnafu';
letfixture;
it('shouldcreateaninstance',()=>{
letcomponent=newArticleComponent();
expect(component).toBeTruthy();
});
describe('Async',()=>{
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations:[
ArticleComponent
],
});
fixture=TestBed.createComponent(ArticleComponent);
}));
afterEach(()=>{
fixture=undefined;
});
it('shouldhavecorrecttitle',async(()=>{
expect(fixture.componentInstance.title)
.toEqual(expectedTitle);
}));
it('shouldrendertitleinanh1tag',async(()=>{
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('h1')
.textContent).toContain(expectedTitle);
}));
});
});
Withthis,youshouldseeKarmarunandpassallthreetests.
Howitworks...Whenitcomestotestingcomponents,fixtureisyourfriend.Itgivesyoutheabilitytoinspectandmanipulatethecomponentinanenvironmentthatitwillbehavecomfortablyin.Youarethenabletomanipulatetheinstancesofinputmadetothecomponent,aswellasinspecttheiroutputandresultantbehavior.
Thisisthecoreofunittesting:the"thing"youaretesting—here,acomponentclass—shouldbetreatedasablackbox.Youcontrolwhatgoesintothebox,andyourtestsshouldmeasureanddefinewhattheyexpecttocomeoutofthebox.Ifthetestsaccountforallthepossiblecasesofinputandoutput,thenyouhaveachieved100percentunittestcoverageofthatthing.
SeealsoCreatingaminimumviableunittestsuitewithKarma,Jasmine,andTypeScriptgivesyouagentleintroductiontounittestswithTypeScriptUnittestingasynchronousservicedemonstrateshowaninjectionismockedinunittestsUnittestingacomponentwithaservicedependencyusingStubsshowshowyoucancreateaservicemocktowriteunittestsandavoiddirectdependenciesUnittestingacomponentwithaservicedependencyusingSpiesshowshowyoucankeeptrackofservicemethodinvocationsinsideaunittest
Writingaminimumviableend-to-endtestsuiteforasimpleapplicationEnd-to-endtesting(ore2eforshort)isontheotherendofthespectrumasfarasunittestingisconcerned.Theentireapplicationexistsasablackbox,andtheonlycontrolsatyourdisposal—forthesetests—areactionstheusermighttakeinsidethebrowser,suchasfiringclickeventsornavigatingtoapage.Similarly,thecorrectnessoftestsisonlyverifiedbyinspectingthestateofthebrowserandtheDOMitself.
Moreexplicitly,anend-to-endtestwill(insomeform)startupanactualinstanceofyourapplication(orasubsetofit),navigatetoitinanactualbrowser,dostufftoapage,andlooktoseewhathappensonthepage.It'sprettymuchascloseasyouaregoingtogettohavinganactualpersonsitdownanduseyourapplication.
Inthisrecipe,you'llputtogetheraverybasicend-to-endtestsuitesothatyoumightbetterunderstandtheconceptsinvolved.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/8985/.
GettingreadyYou'llbeginwiththecodefilescreatedintheminimumviableapplicationrecipefromChapter8,ApplicationOrganizationandManagement.Themostimportantfilesthatyou'llbeeditinghereareAppComponentandpackage.json:
[package.json]
{
"scripts":{
"start":"tsc&&concurrently'npmruntsc:w''npmrunlite'",
"lite":"lite-server",
"postinstall":"npminstall-s@types/node@types/core-js",
"tsc":"tsc",
"tsc:w":"tsc-w"
},
"dependencies":{
"@angular/common":"2.1.0",
"@angular/compiler":"2.1.0",
"@angular/core":"2.1.0",
"@angular/platform-browser":"2.1.0",
"@angular/platform-browser-dynamic":"2.1.0",
"core-js":"^2.4.1",
"reflect-metadata":"^0.1.3",
"rxjs":"5.0.0-beta.12",
"systemjs":"0.19.27",
"zone.js":"^0.6.23"
},
"devDependencies":{
"concurrently":"^2.2.0",
"lite-server":"^2.2.2",
"typescript":"^2.0.2"
}
}
[app/app.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'app-root',
template:'<h1>AppComponenttemplate!</h1>'
})
exportclassAppComponent{}
Howtodoit...TheAngularteammaintainstheProtractorproject,whichbymanyaccountsisthebestwaytogoaboutperformingend-to-endtestsonyourapplications,atleastinitially.Itcomeswithalargenumberofutilitiesoutoftheboxtomanipulatethebrowserwhenwritingyourtests,andexplicitintegrationswithAngular2,soit'saterrificplacetostart.
GettingProtractorupandrunning
ProtractorreliesonSeleniumtoautomatethebrowser.ThespecificsofSeleniumaren'tespeciallyimportantforthepurposeofcreatingaminimumviablee2etestsuite,butyouwillneedtoinstallaJavaruntime:
sudoapt-getinstallopenjdk-8-jre
Tip
IrunUbuntu,sotheOpenJDKJavaRuntimeEnvironmentV8issuitableformypurposes.Yourdevelopmentsetupmaydiffer.RuntimesfordifferentoperatingsystemscanbefoundonOracle'swebsite.
Protractoritselfcanbeinstalledfromnpm,butitshouldbeglobal.You'llbeusingitwithJasmine,soinstallitanditsTypeScripttypingsaswell:
npminstalljasmine-core@types/jasmine--save-dev
npminstallprotractor-g
Tip
Youmayneedtofiddlewiththisconfiguration.Sometimes,itmayworkifyouinstallprotractorlocallyratherthanglobally.Errorsinvolvingwebdriver-managerarepartoftheprotractorpackage,sotheywillmostlikelybeinvolvedwhereyourprotractorpackageinstallationisaswell.
Itshouldcomeasnosurprisethatprotractorisconfiguredwithafile,socreateitnow:
[protractor.conf.js]
exports.config={
specs:[
'./e2e/**/*.e2e-spec.ts'
],
capabilities:{
'browserName':'chrome'
},
baseUrl:'http://localhost:3000/',
framework:'jasmine',
}
Noneofthesesettingsshouldsurpriseyou:
Thee2etestfilesaregoingtoliveinane2e/directoryandwillbesuffixedwith.e2e-spec.ts
ProtractorisgoingtospinupaChromeinstancethatitwillpuppeteerwithSeleniumTheserveryou'regoingtospinupwillexistatlocalhost:3000,andalltheURLsinsidetheProtractortestswillberelativetothisTheProtractortestswillbewrittenwiththeJasminesyntax
Forsimplicity,theserveryouarestartingupfortheend-to-endtestswillbethesamelite-serveryou'vebeenusingallalong.Whenitstartsup,lite-serverwillopenupabrowserwindowofitsown,whichwillprovetobeabitannoyinghere.SinceitisathinwrapperforBrowserSync,youcanconfigureittonotdothisbysimplydirectingitnottodosoinaconfigfilethatisonlyusedwhenrunninge2etests.
Createthisfilenowinsidethetestdirectory:
[e2e/bs-config.json]
{
"open":false
}
Note
Thelite-serverwrapperwon'tfindthisautomatically,butyou'lldirectittothefileinamoment.
MakingProtractorcompatiblewithJasmineandTypeScript
First,createatsconfig.jsonfileinsidethetestdirectory:
[e2e/tsconfig.json]
{
"compilerOptions":{
"target":"es5",
"module":"commonjs",
"moduleResolution":"node"
}
}
Next,createtheactuale2etestfileskeleton:
[e2e/app.e2e-spec.ts]
describe('AppE2ETestSuite',()=>{
it('shouldhavethecorrecth1text',()=>{
});
});
ThisusesthestandardJasminesyntaxtodeclareaspecsuiteandanemptytestwithinit.
Beforefleshingoutthetest,youneedtoensurethatProtractorcanactuallyusethisfile.Installthets-nodepluginsothatProtractorcanperformthecompilationandusethesefilesine2etests:
npminstallts-node--save-dev
Next,instructProtractortousethistocompilethetestsourcefilesintoausableformat.Thiscanbedoneinitsconfigfile:
[protractor.conf.js]
exports.config={
specs:[
'./e2e/**/*.e2e-spec.ts'
],
capabilities:{
'browserName':'chrome'
},
baseUrl:'http://localhost:3000/',
framework:'jasmine',
beforeLaunch:function(){
require('ts-node').register({
project:'e2e'
});
}
}
Withallthis,you'releftwithaworkingbutemptyend-to-endtest.
Buildingapageobject
AnexcellentconventionthatIhighlyrecommendusingisthepageobject.
Note
Theideabehindthisisthatallofthelogicsurroundingtheinteractionwiththepagecanbeextractedintoitsownpageobjectclass,andtheactualtestbehaviorcanusethisabstractedpageobjectinsidetheclass.ThisallowstheteststobewrittenindependentlyoftheDOMstructureorroutingdefinitions,whichmakesforsuperiortestmaintenance.What'smore,itmakesyourteststotallyindependentofProtractor,whichmakesiteasiershouldyouwanttochangeyourend-to-endtestrunner.
Forthissimpleend-to-endtest,you'llwanttospecifyhowtoarriveatthispageandhowtoinspectittogetwhatyouwant.Definethepageobjectasfollowswithtwomembermethods:
[e2e/app.po.ts]
import{browser,element,by}from'protractor';
exportclassAppPage{
navigate(){
browser.get('/');
}
getHeaderText(){
returnelement(by.css('app-rooth1')).getText();
}
}
navigate()instructsSeleniumtotherootpath(which,asyoumayrecall,isbasedonlocalhost:3000),andgetHeaderText()inspectsaDOMelementforitstextcontents.
Note
Notethatbrowser,element,andbyareallutilitiesimportedfromtheprotractormodule.Moreonthislaterintherecipe.
Writingthee2etest
Withalloftheinfrastructureinplace,youcannoweasilywriteyourend-to-endtest.You'llwanttoinstantiateanewpageobjectforeachtest:
[e2e/app.e2e-spec.ts]
import{AppPage}from'./app.po';
describe('AppE2ETestSuite',()=>{
letpage:AppPage;
beforeEach(()=>{
page=newAppPage();
});
it('shouldhavethecorrecth1text',()=>{
page.navigate();
expect(page.getHeaderText())
.toEqual('AppComponenttemplate!');
});
});
Scriptingthee2etests
Finally,you'llwanttogiveyourselftheabilitytoeasilyruntheend-to-endtestsuite.Seleniumisoftenbeingupdated,soitbehovesyoutoexplicitlyupdateitbeforeyourunthetests:
[package.json]
{
"scripts":{
"pree2e":"webdriver-managerupdate&&tsc",
"e2e":"concurrently'npmrunlite---c=e2e/bs-config.json'
'protractorprotractor.conf.js'",
"start":"tsc&&concurrently'npmruntsc:w''npmrunlite'",
"lite":"lite-server",
"postinstall":"npminstall-s@types/node@types/core-js",
"tsc":"tsc",
"tsc:w":"tsc-w"
},
"dependencies":{
...
},
"devDependencies":{
...
}
}
Finally,Angular2needstointegratewithProtractorandbeabletotellitwhenthepageisreadytobeinteractedwith.ThisrequiresonemoreadditiontotheProtractorconfiguration:
[protractor.conf.js]
exports.config={
specs:[
'./e2e/**/*.e2e-spec.ts'
],
capabilities:{
'browserName':'chrome'
},
baseUrl:'http://localhost:3000/',
framework:'jasmine',
useAllAngular2AppRoots:true,
beforeLaunch:function(){
require('ts-node').register({
project:'e2e'
});
}
}
That'sall!Youshouldnowbeabletoruntheend-to-endtestsuitebyinvokingitwiththecorrespondingnpmscript:
npmrune2e
Thiswillstartupalite-serverinstance(withoutstartingupitsdefaultbrowser),andprotractorwillrunthetestsandexit.
Howitworks...Atthetopoftheapp.po.tspageobjectfile,youimportedthreetargetsfromProtractor:browser,element,andby.Here'sabitaboutthesetargets:
browserisaprotractorglobalobjectthatallowsyoutoperformbrowser-levelactions,suchasvisitingURLs,waitingforeventstooccur,andtakingscreenshots.elementisaglobalfunctionthattakesaLocatorandreturnsanElementFinder.ElementFinderisthepointofcontacttointeractwiththematchingDOMelement,ifitexists.byisaglobalobjectthatexposesseveralLocatorfactories.Here,theby.css()locatorfactoryperformsananalogueofdocument.querySelector().
Tip
TheentireProtractorAPIcanbefoundathttp://www.protractortest.org/#/api.
Thereasonforwritingtheteststhiswaymayfeelstrangetoyou.Afterall,it'sarealbrowserrunningarealapplication,soitmightmakesensetoreachforDOMmethodsandthelike.
ThereasonforusingtheProtractorAPIinsteadissimple:thetestcodeyouarewritingisnotbeingexecutedinsidethebrowserruntime.Instead,ProtractorishandingofftheseinstructionstoSelenium,whichinturnwillexecutetheminsidethebrowserandreturntheresults.Thus,thetestcodeyouwritecanonlyindirectlyinterfacewiththebrowserandtheDOM.
There'smore...Thepurposeofthisrecipewastoassembleaverysimpleend-to-endtestsuitesothatyoucangetafeelofwhatgoesonbehindthescenesinsomeform.Whiletheteststhemselveswillappearmoreorlessastheydohere,regardlessofthetestinfrastructuretheyarerunningon,theinfrastructureitselfisfarfrombeingoptimal;anumberofchangesandadditionscouldbemadetomakeitmorerobust.
Whenrunningunittests,itisoftenusefulfortheunitteststodetectthechangesinfilesandrunthemagainimmediately.Alargepartofthisisbecauseunittestsshouldbeverylightweight.Anydependenciesontherestoftheapplicationaremockedorabstractedawaysothataminimalamountofcodecanberuntoprepareyourunittestenvironment.Thus,thereislittlecosttorunningasuiteofunittestsinasequence.
End-to-endtests,ontheotherhand,behaveintheoppositeway.Theydoindeedrequiretheentireapplicationtobeconstructedandrun,whichcanbecomputationallyexpensive.Pagenavigations,resettingtheentireapplication,initializingandclearingauthentication,andotheroperationsthatmightcommonlybeperformedinanend-to-endtestcantakealongtime.Therefore,itdoesn'tmakeasmuchsenseheretoruntheend-to-endtestswithafilewatcherobservingforchangesmadetothetests.
SeealsoCreatingaminimumviableunittestsuitewithKarma,Jasmine,andTypeScriptgivesyouagentleintroductiontounittestswithTypeScript
UnittestingasynchronousserviceAngular2servicetypesareessentiallyclassesdesignatedforinjectability.Theyareeasytotestsinceyouhaveagreatdealofcontroloverhowandwheretheyareprovided,andconsequently,howmanyinstancesyou'llbeabletocreate.Therefore,testsforserviceswillexistlargelyastheywouldforanynormalTypeScriptclass.
It'llbebetterifyouarefamiliarwiththecontentofthefirstfewrecipesofthischapterbeforeyouproceedfurther.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/3107.
GettingreadySupposeyouwanttobuilda"magiceightball"service.Beginwiththefollowingcode,withaddedcommentsforclarity:
[src/app/magic-eight-ball.service.ts]
import{Injectable}from'@angular/core';
@Injectable()
exportclassMagicEightBallService{
privatevalues:Array<string>;
privatelastIndex:number;
constructor(){
//Initializethevaluesarray
//Musthaveatleasttwoentries
this.values=[
'Askagainlater',
'Outlookgood',
'Mostlikely',
'Don'tcountonit'
];
//Initializewithanyvalidindex
this.lastIndex=this.getIndex();
}
privategetIndex():number{
//Returnarandomindexforthis.values
returnMath.floor(Math.random()*this.values.length);
}
reveal():string{
//Generateanewindex
letnewIdx=this.getIndex();
.
//Checkiftheindexwasthesameoneusedlasttime
if(newIdx===this.lastIndex){
//Ifso,shiftupone(wrappingaround)inthearray
//Thisisstillrandombehavior
newIdx=(++newIdx)%this.values.length;
}
//Savetheindexthatyouarenowusing
this.lastIndex=newIdx;
//Accessthestringandreturnit
returnthis.values[newIdx];
}
}
Thereareseveralthingstonoteabouthowthisservicebehaves:
ThisservicehasseveralprivatemembersbutonlyonepublicmembermethodTheserviceisrandomlyselectedfromanarrayTheserviceshouldn'treturnthesamevaluetwiceinarow
Thewayyourunittestsarewrittenshouldaccountfortheseaswellascompletelytestthebehaviorofthisservice.
Howtodoit...Beginbycreatingtheframeworkofyourtestfile:
[src/app/magic-eight-ball.service.spec.ts]
import{TestBed}from'@angular/core/testing';
import{MagicEightBallService}from
'./magic-eight-ball.service';
describe('Service:MagicEightBall',()=>{
beforeEach(()=>{
TestBed.configureTestingModule({
providers:[
MagicEightBallService
]
});
});
});
Sofar,noneofthisshouldsurpriseyou.MagicEightBallServiceisaninjectable;itneedstobeprovidedinsideamoduledeclaration,whichisdonehere.However,toactuallyuseitinsideaunittest,youneedtoperformaformalinjectionsincethisiswhatwouldberequiredtoaccessitfrominsideacomponent.Thiscanbeaccomplishedwithinject:
[src/app/magic-eight-ball.service.spec.ts]
import{TestBed,inject}from'@angular/core/testing';
import{MagicEightBallService}from
'./magic-eight-ball.service';
describe('Service:MagicEightBall',()=>{
beforeEach(()=>{
TestBed.configureTestingModule({
providers:[
MagicEightBallService
]
});
});
it('shouldbeabletobeinjected',inject([MagicEightBallService],
(magicEightBallService:MagicEightBallService)=>{
expect(magicEightBallService).toBeTruthy();
})
);
});
Offtoagoodstart,butthisdoesn'tactuallytestanythingaboutwhattheserviceisdoing.Next,writeatestthatensuresthatastringofnon-zerolengthisbeingreturned:
[src/app/magic-eight-ball.service.spec.ts]
import{TestBed,inject}from'@angular/core/testing';
import{MagicEightBallService}from
'./magic-eight-ball.service';
describe('Service:MagicEightBall',()=>{
beforeEach(()=>{
TestBed.configureTestingModule({
providers:[
MagicEightBallService
]
});
});
it('shouldbeabletobeinjected',inject([MagicEightBallService],
(magicEightBallService:MagicEightBallService)=>{
expect(magicEightBallService).toBeTruthy();
})
);
it('shouldreturnastringwithnonzerolength',
inject([MagicEightBallService],
(magicEightBallService:MagicEightBallService)=>{
letresult=magicEightBallService.reveal();
expect(result).toEqual(jasmine.any(String));
expect(result.length).toBeGreaterThan(0);
})
);
});
Finally,youshouldwriteatesttoensurethatthetwovaluesreturnedarenotthesame.Sincethismethodisrandom,youcanrunituntilyouareblueinthefaceandstillnotbetotallysure.However,checkingthis50timesinarowisafinewaytobefairlycertain:
[src/app/magic-eight-ball.service.spec.ts]
import{TestBed,inject}from'@angular/core/testing';
import{MagicEightBallService}from
'./magic-eight-ball.service';
describe('Service:MagicEightBall',()=>{
beforeEach(()=>{
TestBed.configureTestingModule({
providers:[
MagicEightBallService
]
});
});
it('shouldbeabletobeinjected',inject([MagicEightBallService],
(magicEightBallService:MagicEightBallService)=>{
expect(magicEightBallService).toBeTruthy();
})
);
it('shouldreturnastringwithnonzerolength',
inject([MagicEightBallService],
(magicEightBallService:MagicEightBallService)=>{
letresult=magicEightBallService.reveal();
expect(result).toEqual(jasmine.any(String));
expect(result.length).toBeGreaterThan(0);
})
);
it('shouldnotreturnthesamevaluetwiceinarow',
inject([MagicEightBallService],
(magicEightBallService:MagicEightBallService)=>{
letlast;
for(leti=0;i<50;++i){
letnext=magicEightBallService.reveal();
expect(next).not.toEqual(last);
last=next;
}
})
);
});
Terrific!Allthesetestshavepassed;you'vedoneagoodjobbuildingsomeincrementalanddescriptivecodecoverageforyourservice.
Howitworks...Theinjecttestfunctionperformsdependencyinjectionforyoueachtimeitisinvoked,usingthearrayofinjectableclassespassedasthefirstargument.Thearrowfunctionthatispassedasitssecondargumentwillbehaveinessentiallythesamewayasacomponentconstructor,whereyouareabletousethemagicEightBallServiceparameterasaninstanceoftheservice.
Tip
Oneimportantdifferencefromhowitisinjectedcomparedtoacomponentconstructoristhatinsideacomponentconstructor,youwouldbeabletousethis.magicEightBallServicerightaway.Withrespecttoinjectionintounittests,itdoesnotautomaticallyattachtothis.
There'smore...Importantconsiderationsforunittestingarewhattestsshouldbewrittenandhowtheyshouldproceed.Respectingtheboundariesofpublicandprivatemembersisessential.Sincethesetestsarewritteninawaythatonlyutilizesthepublicmembersoftheservice,theauthorisfreetogoaboutchanging,extending,orrefactoringtheinternalsoftheservicewithoutworryingaboutbreakingorneedingtoupdatethetests.Awell-designedclasswillbefullytestablefromitspublicinterface.
Tip
Thisnotionbringsupaninterestingphilosophicalpointregardingunittesting.Youshouldbeabletodescribethebehaviorofawell-formedserviceasafunctionofitspublicmembers.Similarly,awell-formedserviceshouldthenberelativelyeasytowriteunittests,giventhattheformerstatementistrue.
Ifitisthenthecasethatyoufindyourunittestsaredifficulttowrite—forexample,youareneedingtoreachintoaprivatememberoftheservicetotestitproperly—thenconsiderthenotionthatyourservicemightnotbeaswelldesignedasitcouldbe.
Inshort,ifit'shardtotest,thenyoumighthavewrittenaclassinaweirdway.
Testingwithoutinjection
Anobservantdeveloperwillnoteherethattheserviceyouaretestingdoesn'thaveanymeaningfuldependenceoninjection.Injectingitintovariousplacesintheapplicationsurelyprovidesitwithaconsistentway,buttheservicedefinitioniswhollyunawareofthisfact.Afterall,instantiationisinstantiation,andthisservicedoesn'tappeartobemorethananinjectableclass.Therefore,itiscertainlypossibletonotbotherinjectingtheserviceatallandmerelyinstantiatingitusingthenewkeyword:
[src/app/magic-eight-ball.service.spec.ts]
import{MagicEightBallService}from
'./magic-eight-ball.service';
describe('Service:MagicEightBall',()=>{
letmagicEightBallService;
beforeEach(()=>{
magicEightBallService=newMagicEightBallService();
});
it('shouldbeabletobeinjected',()=>{
expect(magicEightBallService).toBeTruthy();
});
it('shouldreturnastringwithnonzerolength',()=>{
letresult=magicEightBallService.reveal();
expect(result).toEqual(jasmine.any(String));
expect(result.length).toBeGreaterThan(0);
});
it('shouldnotreturnthesamevaluetwiceinarow',()=>{
letlast;
for(leti=0;i<50;++i){
letnext=magicEightBallService.reveal();
expect(next).not.toEqual(last);
last=next;
}
});
});
Ofcourse,thisrequiresthatyoukeeptrackofwhethertheservicecaresaboutwhetherornotithasbeeninjectedanywhere.
SeealsoUnittestingacomponentwithaservicedependencyusingstubsshowshowyoucancreateaservicemocktowriteunittestsandavoiddirectdependenciesUnittestingacomponentwithaservicedependencyusingspiesshowshowyoucankeeptrackofservicemethodinvocationsinsideaunittest
UnittestingacomponentwithaservicedependencyusingstubsStandalonecomponenttestingiseasy,butyouwillrarelyneedtowritemeaningfultestsforacomponentthatexistsinisolation.Moreoftenthannot,thecomponentwillhaveoneormanydependencies,andwritinggoodunittestsisthedifferencebetweendelightanddespair.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/6651/.
GettingreadySupposeyoualreadyhavetheservicefromtheUnittestingasynchronousservicerecipe.Inaddition,youhaveacomponent,whichmakesuseofthisservice:
[src/app/magic-eight-ball/magic-eight-ball.component.ts]
import{Component}from'@angular/core';
import{MagicEightBallService}from
'../magic-eight-ball.service';
@Component({
selector:'app-magic-eight-ball',
template:`
<button(click)="update()">Clickme!</button>
<h1>{{result}}</h1>
`
})
exportclassMagicEightBallComponent{
result:string='';
constructor(privatemagicEightBallService_:MagicEightBallService){}
update(){
this.result=this.magicEightBallService_.reveal();
}
}
Yourobjectiveistowriteasuiteofunittestsforthiscomponentwithoutsettinganexplicitdependencyontheservice.
Howtodoit...Beginwithaskeletonofyourtestfile:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{MagicEightBallComponent}from
'./magic-eight-ball.component';
import{MagicEightBallService}from
'../magic-eight-ball.service';
describe('Component:MagicEightBall',()=>{
beforeEach(async(()=>{
}));
afterEach(()=>{
});
it('shouldbeginwithnotext',async(()=>{
}));
it('shouldshowtextafterclick',async(()=>{
}));
});
You'llfirstwanttoconfigurethetestmodulesothatitproperlyprovidestheseimportedtargetsinthetest:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{MagicEightBallComponent}from
'./magic-eight-ball.component';
import{MagicEightBallService}from
'../magic-eight-ball.service';
describe('Component:MagicEightBall',()=>{
letfixture;
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations:[
MagicEightBallComponent
],
providers:[
MagicEightBallService
]
});
fixture=TestBed.createComponent(MagicEightBallComponent);
}));
afterEach(()=>{
fixture=undefined;
});
it('shouldbeginwithnotext',async(()=>{
}));
it('shouldshowtextafterclick',async(()=>{
}));
});
Stubbingaservicedependency
Injectingtheactualserviceworksjustfine,butthisisn'twhatyouwanttodo.Youdon'twanttoactuallyinjectaninstanceofMagicEightBallServiceintothecomponent,asthatwouldsetadependencyontheserviceandmaketheunittestmorecomplicatedthanitneedstobe.However,MagicEightBallComponentneedstoimportsomethingthatresemblesaMagicEightBallService.Anexcellentsolutionhereistocreateaservicestubandinjectitinitsplace:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{MagicEightBallComponent}from
'./magic-eight-ball.component';
import{MagicEightBallService}from
'../magic-eight-ball.service';
describe('Component:MagicEightBall',()=>{
letfixture;
letmagicEightBallResponse='Answerunclear';
letmagicEightBallServiceStub={
reveal:()=>magicEightBallResponse
};
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations:[
MagicEightBallComponent
],
providers:[
{
provide:MagicEightBallService,
useValue:magicEightBallServiceStub
}
]
});
fixture=TestBed.createComponent(MagicEightBallComponent);
}));
afterEach(()=>{
fixture=undefined;
});
it('shouldbeginwithnotext',async(()=>{
}));
it('shouldshowtextafterclick',async(()=>{
}));
});
Acomponentcan'ttellthedifferencebetweentheactualserviceanditsmock,soitwillbehavenormallyinthetestconditionsyou'vesetup.
Next,youshouldwritethepreclicktestbycheckingthatthefixture'snativeElementcontainsnotext:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{MagicEightBallComponent}from
'./magic-eight-ball.component';
import{MagicEightBallService}from
'../magic-eight-ball.service';
describe('Component:MagicEightBall',()=>{
letfixture;
letgetHeaderEl=()=>
fixture.nativeElement.querySelector('h1');
letmagicEightBallResponse='Answerunclear';
letmagicEightBallServiceStub={
reveal:()=>magicEightBallResponse
};
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations:[
MagicEightBallComponent
],
providers:[
{
provide:MagicEightBallService,
useValue:magicEightBallServiceStub
}
]
});
fixture=TestBed.createComponent(MagicEightBallComponent);
}));
afterEach(()=>{
fixture=undefined;
});
it('shouldbeginwithnotext',async(()=>{
fixture.detectChanges();
expect(getHeaderEl().textContent).toEqual('');
}));
it('shouldshowtextafterclick',async(()=>{
}));
});
Triggeringeventsinsidethecomponentfixture
Forthesecondtest,youshouldtriggeraclickonthebutton,instructthefixturetoperformchangedetection,andtheninspecttheDOMtoseethatthetextwasproperlyinserted.Sinceyouhavedefinedthetextthatthestubwillreturn,youcanjustcompareitdirectlywiththat:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{MagicEightBallComponent}from
'./magic-eight-ball.component';
import{MagicEightBallService}from
'../magic-eight-ball.service';
import{By}from'@angular/platform-browser';
describe('Component:MagicEightBall',()=>{
letfixture;
letgetHeaderEl=()=>
fixture.nativeElement.querySelector('h1');
letmagicEightBallResponse='Answerunclear';
letmagicEightBallServiceStub={
reveal:()=>magicEightBallResponse
};
...
it('shouldbeginwithnotext',async(()=>{
expect(getHeaderEl().textContent).toEqual('');
}));
it('shouldshowtextafterclick',async(()=>{
fixture.debugElement.query(By.css('button'))
.triggerEventHandler('click',null);
fixture.detectChanges();
expect(getHeaderEl().textContent)
.toEqual(magicEightBallResponse);
}));
});
You'llnotethatthisneedstoimportandusetheBy.csspredicate,whichisrequiredtoperformDebugElementinspections.
Howitworks...Asdemonstratedinthedependencyinjectionchapter,providingastubtothecomponentisnodifferentthanprovidingaregularvaluetothecoreapplication.
Thestubhereisasinglefunctionthatreturnsastaticvalue.Thereisnoconceptofrandomlyselectingfromtheservice'sarrayofstrings,andtheredoesn'tneedtobe.Theunittestsfortheserviceitselfensurethatitisbehavingproperly.Instead,theonlyvalueprovidedbytheservicehereistheinformationitpassesbacktothecomponentforinterpolationbackintothetemplate.
SeealsoWritingaminimumviableunittestsuiteforasimplecomponentshowsyouabasicexampleofunittestingAngular2componentsUnittestingasynchronousservicedemonstrateshowinjectionismockedinunittestsUnittestingacomponentwithaservicedependencyusingspiesshowshowyoucankeeptrackofservicemethodinvocationsinsideaunittest
UnittestingacomponentwithaservicedependencyusingspiesTheabilitytostuboutservicesisuseful,butitcanbelimitinginanumberofways.Itcanalsobetedious,asthestubsyoucreatemustremainuptodatewiththepublicinterfaceoftheservice.Anotherexcellenttoolatyourdisposalwhenwritingunittestsisthespy.
Aspyallowsyoutoselectafunctionormethod.Italsohelpsyoucollectinformationaboutifandhowitwasinvokedaswellashowitwillbehaveonceitisinvoked.Itissimilarinconcepttoastubbutallowsyoutohaveamuchmorerobustunittest.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/3444/.
GettingreadyBeginwiththecomponenttestsyouwroteinthelastrecipe:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{MagicEightBallComponent}from
'./magic-eight-ball.component';
import{MagicEightBallService}from
'../magic-eight-ball.service';
import{By}from'@angular/platform-browser';
describe('Component:MagicEightBall',()=>{
letfixture;
letgetHeaderEl=()=>fixture.nativeElement.querySelector('h1');
letmagicEightBallResponse='Answerunclear';
letmagicEightBallServiceStub={
reveal:()=>magicEightBallResponse
};
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations:[
MagicEightBallComponent
],
providers:[
{
provide:MagicEightBallService,
useValue:magicEightBallServiceStub
}
]
});
fixture=TestBed.createComponent(MagicEightBallComponent);
}));
afterEach(()=>{
fixture=undefined;
});
it('shouldbeginwithnotext',async(()=>{
fixture.detectChanges();
expect(getHeaderEl().textContent).toEqual('');
}));
it('shouldshowtextafterclick',async(()=>{
fixture.debugElement.query(By.css('button'))
.triggerEventHandler('click',null);
fixture.detectChanges();
expect(getHeaderEl().textContent)
.toEqual(magicEightBallResponse);
}));
});
Howtodoit...Insteadofusingastub,configurethetestmoduletoprovidetheactualservice:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{MagicEightBallComponent}from
'./magic-eight-ball.component';
import{MagicEightBallService}from
'../magic-eight-ball.service';
import{By}from'@angular/platform-browser';
describe('Component:MagicEightBall',()=>{
letfixture;
letgetHeaderEl=()=>fixture.nativeElement.querySelector('h1');
letmagicEightBallResponse='Answerunclear';
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations:[
MagicEightBallComponent
],
providers:[
MagicEightBallService
]
});
fixture=TestBed.createComponent(MagicEightBallComponent);
}));
afterEach(()=>{
fixture=undefined;
});
it('shouldbeginwithnotext',async(()=>{
fixture.detectChanges();
expect(getHeaderEl().textContent).toEqual('');
}));
it('shouldshowtextafterclick',async(()=>{
fixture.debugElement.query(By.css('button'))
.triggerEventHandler('click',null);
fixture.detectChanges();
expect(getHeaderEl().textContent)
.toEqual(magicEightBallResponse);
}));
});
Settingaspyontheinjectedservice
Yourgoalistouseamethodspytointerceptcallstoreveal()ontheservice.Theproblemhere,however,isthattheserviceisbeinginjectedintothecomponent;therefore,youdon'thavea
directabilitytogetareferencetotheserviceinstanceandsetaspyonit.Fortunately,thecomponentfixtureprovidesthisforyou:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{MagicEightBallComponent}from
'./magic-eight-ball.component';
import{MagicEightBallService}from
'../magic-eight-ball.service';
import{By}from'@angular/platform-browser';
describe('Component:MagicEightBall',()=>{
letfixture;
letgetHeaderEl=()=>fixture.nativeElement.querySelector('h1');
letmagicEightBallResponse='Answerunclear';
letmagicEightBallService;
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations:[
MagicEightBallComponent
],
providers:[
MagicEightBallService
]
});
fixture=TestBed.createComponent(MagicEightBallComponent);
magicEightBallService=fixture.debugElement.injector
.get(MagicEightBallService);
}));
afterEach(()=>{
fixture=undefined;
magicEightBallService=undefined;
});
...
});
Next,setaspyontheserviceinstanceusingspyOn().Configurethespytointerceptthemethodcallandreturnthestaticvalueinstead:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{MagicEightBallComponent}from
'./magic-eight-ball.component';
import{MagicEightBallService}from
'../magic-eight-ball.service';
import{By}from'@angular/platform-browser';
describe('Component:MagicEightBall',()=>{
letfixture;
letgetHeaderEl=()=>fixture.nativeElement.querySelector('h1');
letmagicEightBallResponse='Answerunclear';
letmagicEightBallService;
letrevealSpy;
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations:[
MagicEightBallComponent
],
providers:[
MagicEightBallService
]
});
fixture=TestBed.createComponent(MagicEightBallComponent);
magicEightBallService=fixture.debugElement.injector
.get(MagicEightBallService);
revealSpy=spyOn(magicEightBallService,'reveal')
.and.returnValue(magicEightBallResponse);
}));
afterEach(()=>{
fixture=undefined;
magicEightBallService=undefined;
revealSpy=undefined;
});
...
});
Withthisspy,youarenowcapableofseeinghowtherestoftheapplicationinteractswiththiscapturedmethod.Addanewtest,andcheckthatthemethodiscalledonceandreturnsthepropervalueafteraclick(thisalsopullstheclickingactionintoitsowntesthelper):
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts]
import{TestBed,async}from'@angular/core/testing';
import{MagicEightBallComponent}from
'./magic-eight-ball.component';
import{MagicEightBallService}from
'../magic-eight-ball.service';
import{By}from'@angular/platform-browser';
describe('Component:MagicEightBall',()=>{
letfixture;
letgetHeaderEl=()=>fixture.nativeElement.querySelector('h1');
letmagicEightBallResponse='Answerunclear';
letmagicEightBallService;
letrevealSpy;
letclickButton=()=>{
fixture.debugElement.query(By.css('button'))
.triggerEventHandler('click',null);
};
beforeEach(async(()=>{
TestBed.configureTestingModule({
declarations:[
MagicEightBallComponent
],
providers:[
MagicEightBallService
]
});
fixture=TestBed.createComponent(MagicEightBallComponent);
magicEightBallService=fixture.debugElement.injector
.get(MagicEightBallService);
revealSpy=spyOn(magicEightBallService,'reveal')
.and.returnValue(magicEightBallResponse);
}));
afterEach(()=>{
fixture=undefined;
magicEightBallService=undefined;
revealSpy=undefined;
});
it('shouldbeginwithnotext',async(()=>{
fixture.detectChanges();
expect(getHeaderEl().textContent).toEqual('');
}));
it('shouldcallrevealafteraclick',async(()=>{
clickButton();
expect(revealSpy.calls.count()).toBe(1);
expect(revealSpy.calls.mostRecent().returnValue)
.toBe(magicEightBallResponse);
}));
it('shouldshowtextafterclick',async(()=>{
clickButton();
fixture.detectChanges();
expect(getHeaderEl().textContent)
.toEqual(magicEightBallResponse);
}));
});
Note
NotethatdetectChanges()isonlyrequiredtoresolvethedatabinding,nottoexecuteeventhandlers.
Howitworks...Jasminespiesactasmethodinterceptorsandarecapableofinspectingeverythingaboutthegivenmethodinvocation.Itcantrackifandwhenamethodwascalled,whatargumentsitwascalledwith,howmanytimesitwascalled,howitshouldbehave,andsoon.Thisisextremelyusefulwhentryingtoremovedependenciesfromcomponentunittests,asyoucanmockoutthepublicinterfaceoftheserviceusingspies.
There'smore...Spiesarenotbeholdentoreplacethemethodoutright.Here,itisusefultobeabletopreventtheexecutionfromreachingtheinternalsoftheservice,butitisnotdifficulttoimaginecaseswhereyouwouldonlywanttopassivelyobservetheinvocationofacertainmethodandallowtheexecutiontocontinuenormally.
Forsuchapurpose,insteadofusing.and.returnValue(),Jasmineallowsyoutouse.and.callThrough(),whichwillallowtheexecutiontoproceeduninterrupted.
SeealsoWritingaminimumviableunittestsuiteforasimplecomponentshowsyouabasicexampleofunittestingAngular2componentsUnittestingasynchronousservicedemonstrateshowinjectionismockedinunittestsUnittestingacomponentwithaservicedependencyusingstubsshowshowyoucancreateaservicemocktowriteunittestsandavoiddirectdependencies
Chapter10.PerformanceandAdvancedConceptsThischapterwillcoverthefollowingrecipes:
UnderstandingandproperlyutilizingenableProdModewithpureandimpurepipesWorkingwithzonesoutsideAngularListeningforNgZoneeventsExecutionoutsidetheAngularzoneConfiguringcomponentstouseexplicitchangedetectionwithOnPushConfiguringViewEncapsulationformaximumefficiencyConfiguringtheAngular2RendererServicetousewebworkersConfiguringapplicationstouseahead-of-timecompilationConfiguringanapplicationtouselazyloading
IntroductionAngular2wasatotalrebuildforanumberofreasons,butoneofthebiggestoneswascertainlyefficiency.Theframeworkthatemergedfromtheashesisasleekandelegantone,butnotwithoutitscomplexities.
Thischapterservestoexploresomeofthenewfeaturesthatitbuildsuponandhowtomosteffectivelyemploythemtostreamlineyourapplication.
UnderstandingandproperlyutilizingenableProdModewithpureandimpurepipesAngular2'schangedetectionprocessisanelegantbutficklebeastthatischallengingtounderstandatfirst.Whileitoffershugeefficiencygainsoverthe1.xframework,thegainscancomeatacost.ThedevelopmentmodeofAngularisactivatedbydefault,whichwillalertyouwhenyourcodeisindangerofbehavinginawaythatdefeatsthechangedetectionefficiencygains.Inthisrecipe,you'llimplementafeaturethatviolatesAngular'schangedetectionschema,correctit,andsafelyuseenableProdMode.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/0623/.
GettingreadyBeginwithasimplecomponent:
[src/app/app.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'app-root',
template:`
<input#t>
<button(click)="update(t.value)">Save</button>
<h1>{{title}}</h1>
`
})
exportclassAppComponent{
title:string='';
update(newTitle:string){
this.title=newTitle;
}
}
Whenthebuttoninthiscomponentisclicked,itgrabsthevalueoftheinputandinterpolatesittotheheadertag.Asis,thisimplementationisperfectlysuitable.
Howtodoit...TodemonstratetherelevanceofenableProdMode,you'llneedtointroduceabehaviorthatenableProdModewouldmask.Morespecifically,thismeansapieceofyourapplicationwillbehavedifferentlywhentwosequentialpassesofchangedetectionarerun.
Note
Thereareasignificantnumberofwaystoimplementthis,butforthepurposeofthisrecipe,you'llimplementanonsensicalpipethatchangeseverytimeit'sused.
Generatingaconsistencyerror
Createanonsensicalpipe,namelyAddRandomPipe:
[src/app/add-random.pipe.ts]
import{Pipe,PipeTransform}from'@angular/core';
@Pipe({
name:'addRandomPipe'
})
exportclassAddRandomPipeimplementsPipeTransform{
transform(value:string):string{
returnvalue+Math.random();
}
}
Next,takethispipeandintroduceittoyourcomponent:
[src/app/app.component.ts]
import{Component}from'@angular/core';
import{AddRandomPipe}from'./add-random.pipe';
@Component({
selector:'app-root',
template:`
<input#t>
<button(click)="update(t.value)">Save</button>
<h1>{{title|addRandomPipe}}</h1>
`
})
exportclassAppComponent{
title:string='';
update(newTitle:string){
this.title=newTitle;
}
}
Thiswon'tcreateanerroryet,though.
Note
Angularwillindeedrunthechangedetectionprocesstwice,soitmightseemmysteriousthatitdoesn'tcreateanerror.Eventhoughthepipewillgenerateanewoutputeverytimeitstransformmethodisinvoked,Angularissmartenoughtorecognizethattheinputoftheinterpolationaren'tchanging,andtherefore,reevaluatingthepipeisunnecessary.Thisiscalleda"pure"pipe,whichistheAngulardefault.
InordertogetAngulartoevaluatethepipeduringeachchangedetectioncycle,specifythepipeas"impure":
[src/app/add-random.pipe.ts]
import{Pipe,PipeTransform}from'@angular/core';
@Pipe({
name:'addRandomPipe',
pure:false
})
exportclassAddRandomPipeimplementsPipeTransform{
transform(value:string):string{
returnvalue+Math.random();
}
}
Nowthefunbegins.Whenyouruntheapplication,youshouldseesomethingsimilartothefollowingerrormessage:
EXCEPTION:Errorin./AppComponentclassAppComponent-inlinetemplate:3:8
causedby:Expressionhaschangedafteritwaschecked.Previousvalue:
'0.0495151713435904'.Currentvalue:'0.9266277919907477'.
Introducingchangedetectioncompliance
Theerrorisbeingthrownassoonastheapplicationstartsup,beforeyou'reevengivenachancetopressthebutton.ThismeansthatAngularisdetectingthebindingmismatchasthecomponentisbeinginstantiatedandrenderedforthefirsttime.
Note
You'vecreatedapipethatintentionallychangeseachtime,soyourgoalistoinstructAngulartonotbothercheckingthepipeoutputtwiceagainstitselfuponcomponentinstantiation.
Atthispoint,you'vemanagedtogetAngulartothrowaconsistencyerror,whichitdoesbecauseit'srunningchangedetectioncheckstwiceandgettingdifferentresults.SwitchingonenableProdModeatthispointwouldstoptheerrorsinceAngularwouldthenonlyrunchange
detectiononceandnotbothertocheckforconsistency.ThisisbecauseittrustsyoutohaveverifiedcompliancebeforeusingenableProdMode.TurningonenableProdModetomaskconsistencyerrormessagesisabitlikecominghometofindyourhouseisonfireandthendecidingtogoonavacation.
Angularallowsyoutocontrolthisbyspecifyingthechangedetectionstrategyforthecomponent.Thedefaultistoalwaysperformachangedetectioncheck,butthiscanbeoverriddenwiththeOnPushconfiguration:
[src/app/app.component.ts]
import{Component,ChangeDetectionStrategy}
from'@angular/core';
import{AddRandomPipe}from'./add-random.pipe';
@Component({
selector:'app-root',
template:`
<input#t>
<button(click)="update(t.value)">Save</button>
<h1>{{title|addRandomPipe}}</h1>
`,
changeDetection:ChangeDetectionStrategy.OnPush
})
exportclassAppComponent{
title:string='';
update(newTitle:string){
this.title=newTitle;
}
}
Nowwhenthecomponentinstanceisinitialized,youshouldnolongerseetheconsistencyerror.
SwitchingonenableProdMode
SinceyourapplicationisnowcompliantwithAngular'schangedetectionmechanism,you'refreetouseenableProdMode.Thisletsyourapplicationrunchangedetectiononceeachtime.Thisisbecauseitassumestheapplicationwillarriveinastablestate.
Inyourapplication'sbootstrappingfile,invokeenableProdModebeforeyoustartbootstrapping:
[src/main.ts]
import{platformBrowserDynamic}
from'@angular/platform-browser-dynamic';
import{enableProdMode}from'@angular/core';
import{AppModule}from'./app/';
enableProdMode();
platformBrowserDynamic().bootstrapModule(AppModule);
Howitworks...enableProdModewillconfigureyourapplicationinanumberofways,includingsilencingerrorandinformationalerrormessages,amongothers;however,noneofthesewaysisasvisibleasthesuppressionofthesecondarychangedetectionprocess.
There'smore...Thereareotherwaystomitigatethisconsistencyproblem.Forexample,supposeyouwanttogenerateapipethatwillappendarandomnumbertotheinput.Itdoesn'tnecessarilyneedtobeadifferentrandomnumbereverysingletime;rather,youcanhaveoneforeachuniqueinputwithinacertainperiodoftime.Suchasituationcouldallowyoutohaveapipethatutilizessomesortofcachingstrategy.Ifthepipecachesresultsforaperiodoftimelongerthanchangedetectiontakestocomplete(whichisnotverylong),thenalteringthechangedetectionstrategyofthecomponentisnotnecessary.Thisisbecausemultiplepipeinvocationswillyieldanidenticalresponse.Forexample,refertothefollowingcode:
[src/app/add-random.pipe.ts]
import{Pipe,PipeTransform}from'@angular/core';
@Pipe({
name:'addRandomPipe',
pure:false
})
exportclassAddRandomPipeimplementsPipeTransform{
cache:Object={};
transform(input:string):string{
letvalue=this.cache[input];
if(!value||value.expire<Date.now()){
value={
text:input+Math.random(),
//Expiresinonesecond
expire:Date.now()+1000
}
this.cache[input]=value;
}
returnvalue.text;
}
}
Withthis,youcansafelystrikechangeDetection:ChangeDetectionStrategy.OnPushfromComponentMetadataofyourAppComponentandyouwillnotseeanyconsistencyerrors.
SeealsoConfiguringtheAngular2rendererservicetousewebworkersguidesyouthroughtheprocessofsettingupyourapplicationtorenderonawebworkerConfiguringapplicationstouseahead-of-timecompilationguidesyouthroughhowtocompileanapplicationduringthebuild
WorkingwithzonesoutsideAngularWorkingwithzonesentirelyinsideoftheAngularframeworkconcealswhattheyarereallydoingbehindthescenes.Itwouldbeadisservicetoyou,thereader,tojustglossovertheunderlyingmechanism.Inthisrecipe,you'lltakethevanillazone.jsimplementationoutsideofAngularandmodifyitabitinordertoseehowAngularcanmakeuseofit.
TherewillbenoAngularusedinthisrecipe,onlyzone.jsinsideasimpleHTMLfile.Furthermore,thisrecipewillbewritteninplainES5JavaScriptforsimplicity.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/0591/.
GettingreadyBeginwiththefollowingsimpleHTMLfile:
[index.html]
<buttonid="button">Clickme</button>
<buttonid="add">Addlistener</button>
<buttonid="remove">Removelistener</button>
<script>
varbutton=document.getElementById('button');
varadd=document.getElementById('add');
varremove=document.getElementById('remove');
varclickCallback=function(){
console.log('click!');
};
setInterval(function(){
console.log('setinterval!');
},1000);
add.addEventListener('click',function(){
button.addEventListener('click',clickCallback);
});
remove.addEventListener('click',function(){
button.removeEventListener('click',clickCallback);
});
</script>
Eachofthesecallbackshaslogstatementsinsidethemsoyoucanseewhentheyareinvoked:
setIntervalcallsitsassociatedlistenereverysecondClickingonClickmecallsitslistenerifitisattachedClickingonAddlistenerattachesaclicklistenertothebuttonClickingonRemovelistenerremovestheclicklistener
Note
There'snothingspecialgoingonhere,becauseallofthesearedefaultbrowserAPIs.Themagicofzones,asyouwillsee,isthatthezonebehaviorcanbeintroducedaroundthiswithoutmodifyinganycode.
Howtodoit...First,addthezone.jsscripttothetopofthefile:
[index.html]
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.6.26/zone.js">
</script>
<buttonid="button">Clickme</button>
<buttonid="add">Addlistener</button>
<buttonid="remove">Removelistener</button>
...
Note
There'snospecialsetupneededforzone.js,butitneedstobeaddedbeforeyousetlistenersordoanythingthatcouldhaveasynchronousimplications.Angularneedsthisdependencytobeaddedbeforeitisinitialized.
AddingthisscriptintroducesZonetotheglobalnamespace.zone.jshasalreadycreatedaglobalzoneforyou.Thiscanbeaccessedwiththefollowing:
Zone.current
Forkingazone
Theglobalzoneisn'tdoinganythinginterestingyet.Tocustomizeazone,you'llneedtocreateyourownbyforkingtheonewehaveandrunningrelevantcodeinsideit.Dothisasfollows:
[index.html]
<script
src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.6.26/zone.js">
</script>
<buttonid="button">Clickme</button>
<buttonid="add">Addlistener</button>
<buttonid="remove">Removelistener</button>
<script>
varbutton=document.getElementById('button');
varadd=document.getElementById('add');
varremove=document.getElementById('remove');
Zone.current.fork({}).run(function(){
varclickCallback=function(){
console.log('click!');
};
setInterval(function(){
console.log('setinterval!');
},1000);
add.addEventListener('click',function(){
button.addEventListener('click',clickCallback);
});
remove.addEventListener('click',function(){
button.removeEventListener('click',clickCallback);
});
});
</script>
Behaviorally,thisdoesn'tchangeanythingfromtheperspectiveoftheconsole.fork()takesanemptyZoneSpecobjectliteral,whichyouwillmodifynext.
OverridingzoneeventswithZoneSpec
Whenapieceofcodeisruninazone,youareabletoattachbehaviorsatimportantpointsintheasynchronousbehaviorflow.Here,you'lloverridefourzoneevents:
scheduleTask
invokeTask
hasTask
cancelTask
You'llbeginwithscheduleTask.DefineanoverridemethodinsideZoneSpec.Overridesusetheeventnamesprefixedwithon:
[index.html]
<script
src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.6.26/zone.js">
</script>
<buttonid="button">Clickme</button>
<buttonid="add">Addlistener</button>
<buttonid="remove">Removelistener</button>
<script>
varbutton=document.getElementById('button');
varadd=document.getElementById('add');
varremove=document.getElementById('remove');
Zone.current.fork({
onScheduleTask:function(zoneDelegate,zone,targetZone,task){
console.log('schedule');
zoneDelegate
.scheduleTask(targetZone,task);
}
}).run(function(){
varclickCallback=function(){
console.log('click!');
};
setInterval(function(){
console.log('setinterval!');
},1000);
add.addEventListener('click',function(){
button.addEventListener('click',clickCallback);
});
remove.addEventListener('click',function(){
button.removeEventListener('click',clickCallback);
});
});
</script>
Withthisaddition,youshouldseethatthezonerecognizesthatthreetasksarebeingscheduled.Thisshouldmakesense,asyouaredeclaringthreeinstancesthatcangenerateasynchronousactions:setInterval,addEventListener,andremoveEventListener.IfyouclickonAddlistener,you'llseeitschedulethefourthtaskaswell.
Note
zoneDelegate.scheduleTask()isrequiredbecauseyouareactuallyoverwritingwhatthezoneisusingforthathandler.Ifyoudon'tperformthisaction,theschedulerhandlerwillexitbeforeactuallyschedulingthetask.
Theoppositeofschedulingataskiscancelingit,sooverridethateventhandlernext:
[index.html]
Zone.current.fork({
onScheduleTask:function(zoneDelegate,zone,targetZone,task){
console.log('schedule');
zoneDelegate
.scheduleTask(targetZone,task);
},
onCancelTask:function(zoneDelegate,zone,targetZone,task){
console.log('cancel');
zoneDelegate
.cancelTask(targetZone,task);
}
}).run(function(){
...
});
Acanceleventoccurswhenascheduledtaskisdestroyed,inthisexample,whenremoveEventListener()isinvoked.IfyouclickonAddlistenerandthenRemovelistener,
you'llseeacanceleventoccur.
Theschedulingoftasksisvisibleduringthestartup,buteachtimeabuttonisclickedorasetIntervalhandlerisexecuted,youdon'tseeanythingbeinglogged.Thisisbecauseschedulingatask,whichoccursduringregistration,isdistinctfrominvokingatask,whichiswhentheasynchronouseventactuallyoccurs.
Todemonstratethis,addaninvokeTaskoverride:
[index.html]
Zone.current.fork({
onScheduleTask:function(zoneDelegate,zone,targetZone,task){
console.log('schedule');
zoneDelegate
.scheduleTask(targetZone,task);
},
onCancelTask:function(zoneDelegate,zone,targetZone,task){
console.log('cancel');
zoneDelegate
.cancelTask(targetZone,task);
},
onInvokeTask:function(zoneDelegate,zone,targetZone,task,
applyThis,applyArgs){
console.log('invoke');
zoneDelegate
.invokeTask(targetZone,task,applyThis,applyArgs);
}
}).run(function(){
...
});
Withthisaddition,youshouldnowbeabletoseeaconsolelogeachtimeataskisinvoked—forabuttonclickorasetIntervalcallback.
Sofar,you'vebeenabletoseewhenthezonehastasksscheduledandinvoked,butnow,dothereverseofthistodetectwhenallthetaskshavebeencompleted.ThiscanbeaccomplishedwithhasTask,whichcanalsobeoverridden:
[index.html]
Zone.current.fork({
onScheduleTask:function(zoneDelegate,zone,targetZone,task){
console.log('schedule');
zoneDelegate
.scheduleTask(targetZone,task);
},
onCancelTask:function(zoneDelegate,zone,targetZone,task){
console.log('cancel');
zoneDelegate
.cancelTask(targetZone,task);
},
onInvokeTask:function(zoneDelegate,zone,targetZone,task,
applyThis,applyArgs){
console.log('invoke');
zoneDelegate
.invokeTask(targetZone,task,applyThis,applyArgs);
},
onHasTask:function(zoneDelegate,zone,targetZone,isEmpty){
console.log('has');
zoneDelegate.hasTask(targetZone,isEmpty);
}
}).run(function(){
...
});
TheisEmptyparameterofonHasTaskhasthreeproperties:eventTask,macroTask,andmicroTask.ThesethreepropertiesmaptoBooleansdescribingwhethertheassociatedqueueshaveanytasksinsidethem.
Withthesefourcallbacks,youhavesuccessfullyinterceptedfourimportantpointsinthecomponentlifecycle:
Whenatask"generator"isscheduled,whichmaygeneratetasksfrombrowserortimereventsWhenatask"generator"iscanceledWhenataskisactuallyinvokedHowtodeterminewhetheranytasksarescheduledandofwhattype
Howitworks...Theconceptthatformsthecoreofzonesistheinterceptionofasynchronoustasks.Moredirectly,youwanttheabilitytoknowwhenasynchronoustasksarebeingcreated,howmanythereare,andwhenthey'redone.
zone.jsaccomplishesthisbyshimmingalltherelevantbrowsermethodsthatareresponsibleforsettingupasynchronoustasks.Infact,allthemethodsusedinthismethod—setInterval,addEventListener,andremoveEventListener—areallshimmedsothatthezonetheyareruninsideisawareofanyasynchronoustaskstheymightaddtothequeue.
There'smore...TobegintorelatethistoAngular,you'llneedtotakeastepbacktoexaminethezoneecosystem.
Understandingzone.run()
You'llnoticeinthisexamplethatinvokeisprintedforeachasynchronousaction,evenforthosethatwereregisteredinsideanotherasynchronousaction.Thisisthepowerofzones.Anythingdoneinsidethezone.run()blockwillcascadewithinthesamezone.Thisway,thezonecankeeptrackofanunbrokensegmentofasynchronousbehaviorwithoutanoceanofboilerplatecode.
Microtasksandmacrotasks
Thisactuallyhasnothingtodowithzone.jsatallbutratherwithhowthebrowsereventloopworks.Alltheeventsgeneratedbyyouinthisexample—clicks,timerevents,andsoon—aremacrotasks.Thatis,thebrowserrespectstheirhandlersasasynchronous,blockingsegmentofcode.Thecodethatexecutesaroundthesetasks—thezone.jscallbacks,forexample—aremicrotasks.Theyaredistinctfrommacrotasksbutarestillsynchronouslyexecutedaspartoftheentire"turn"forthatmacrotask.
Note
Amacrotaskmaygeneratemoremicrotasksforitselfwithinitsownturn.
Oncethemicrotaskandmacrotaskqueuesareempty,thezonecanbeconsideredtobestable,sincethereisnoasynchronousbehaviortobeanticipated.ForAngular,thissoundslikeagreattimetoupdatetheUI.
Infact,thisisexactlywhatAngularisdoingbehindthescenes.Angularviewsthebrowserthroughthetask-centricgogglesofzone.jsandusesthisclevertooltodecidewhentogoaboutrendering.
SeealsoListeningforNgZoneeventsgivesabasicunderstandingofhowAngularisusingzonesExecutionoutsidetheAngularzoneshowsyouhowtoperformlong-runningoperationswithoutincurringazoneoverheadConfiguringcomponentstouseexplicitchangedetectionwithOnPushdescribeshowtomanuallycontrolAngular'schangedetectionprocess
ListeningforNgZoneeventsWiththeintroductionofAngular2comestheconceptofzones.Beforeyoubeginthisrecipe,IstronglyrecommendedyoutobeginbyworkingthroughtheWorkingwithzonesoutsideAngularrecipe.
zone.jszone.jsisalibrarythatAngular2directlydependsupon.ItallowsAngulartobebuiltuponazonethatallowstheframeworktointimatelymanageitsexecutioncontext.
Moreplainly,thismeansthatAngularcantellwhenasynchronousthingsarehappeningthatitmightcareabout.Ifthissoundsabitlikehow$scope.apply()wasrelevantinAngular1.x,youarethinkingintherightway.
NgZoneAngular2'sintegrationwithzonestakestheformoftheNgZoneservice,whichactsasasortofwrapperfortheactualAngularzones.ThisserviceexposesausefulAPIthatyoucantapinto.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/8676/.
GettingreadyAllthatisneededforthisrecipeisacomponentintowhichtheNgZoneservicecanbeinjected:
[src/app/app.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'app-root',
template:``
})
exportclassAppComponent{
constructor(){
}
}
Howtodoit...BeginbyinjectingtheNgZoneserviceintoyourcomponent,whichismadeavailableinsidethecoreAngularmodule:
[src/app/app.component.ts]
import{Component,NgZone}from'@angular/core';
@Component({
selector:'app-root',
template:``
})
exportclassAppComponent{
constructor(privatezone:NgZone){
}
}
TheNgZoneserviceexposesanumberofEventEmittersthatyoucanattachto.Sincezonesarecapableoftrackingasynchronousactivitywithinit,theNgZoneserviceexposestwoEventEmittersthatunderstandwhenanasynchronousactivitybecomesenqueuedanddequeuedasmicrotasks.
TheonUnstableEventEmitterletsyouknowwhenoneormoremicrotasksareenqueued;onStableisfiredwhenthethemicrotaskqueueisemptyandAngulardoesnotplantoenqueueanymore.
Addhandlerstoeach:
[src/app/app.component.ts]
import{Component,NgZone}from'@angular/core';
@Component({
selector:'app-root',
template:``
})
exportclassAppComponent{
constructor(privatezone:NgZone){
zone.onStable.subscribe(()=>console.log('stable'));
zone.onUnstable.subscribe(()=>console.log('unstable'));
}
}
Terrific!However,thelogoutputofthisisquiteboring.Atapplicationstartup,you'llseethattheapplicationisreportedasstable,butnothingfurther.
Demonstratingthezonelifecycle
IfyouunderstandhowAngularuseszones,thelackofloggingshouldn'tsurpriseyou.There'snothingtogenerateasynchronoustasksinthiszone.Goaheadandaddsomebycreatingabuttonwithahandlerthatsetsatimeoutlogstatement:
[src/app/app.component.ts]
import{Component,NgZone}from'@angular/core';
@Component({
selector:'app-root',
template:`<button(click)="foo()">foo</button>`
})
exportclassAppComponent{
constructor(privatezone:NgZone){
zone.onStable.subscribe(()=>console.log('stable'));
zone.onUnstable.subscribe(()=>console.log('unstable'));
}
foo(){
setTimeout(()=>console.log('timeouthandler'),1000);
}
}
Nowwitheachclick,youshouldseeanunstable-stablepair,followedbyanunstable-timeouthandler-stablesetonesecondlater.Thismeansyou'vesuccessfullytiedintoAngular'szoneemitters.
Howitworks...Inordertoobviatethenecessityofa$scope.apply()construct,Angularneedstheabilitytointelligentlydecidewhenitshouldchecktoseewhetherthestateoftheapplicationhaschanged.
Inanasynchronoussetting,suchasabrowserenvironment,thisseemslikeamessytaskatfirst.Somethingliketimedeventsandinputeventsare,bytheirverynature,difficulttokeeptrackof.Forexample,refertothefollowingcode:
element.addEventListener('click',_=>{
//dosomestuff
setInterval(_=>{
//dosomestuff
},1000);
});
Thiscodeiscapableofchangingthemodelintwodifferentplaces,bothofwhichareasynchronous.Codesuchasthisiswrittenallthetimeandinsomanydifferentplaces;it'sdifficulttoimagineawayofkeepingtrackofsuchcodewithoutsprinklingsomethinglike$scope.$apply()allovertheplace.
Theutilityofzone.js
Thebigideaofzonesistogiveyoutheabilitytograsphowandwhenthebrowserisperformingasynchronousactionsthatyoucareabout.
NgZoneiswrappingtheunderlyingzoneAPIforyouinsteadofexposingEventEmitterstothevariouspartsofthelifecycle,butthisshouldn'tconfuseyouonebit.Forthisexample,thelogoutputisdemonstratingthefollowing:
1. Theapplicationinitializesandexaminestheapplicationzone.Therearenotasksscheduled,soAngularemitsastableevent.
2. Youclickonthebutton.3. Thisgeneratesaclickevent,whichinturngeneratesataskinsidethezonetoexecutethe
clickhandler.Angularseesthisandemitsanunstableevent.4. Theclickhandlerisexecuted,schedulingataskin1second.5. Theclickhandleriscompleted,andtheapplicationonceagainhasnopendingtasks.Angular
emitsastableevent.6. OnesecondelapsesandthebrowsertimeraddsthesetTimeouthandlertothetaskqueue.
Sincethisisshimmedbythezone,Angularseesthisoccurandemitsanunstableevent.7. ThesetTimeouthandlerisexecuted.8. ThesetTimeouthandleriscompleted,andtheapplicationonceagainhasnopendingtasks.
Angularemitsastableevent.
SeealsoWorkingwithzonesoutsideAngularisanexcellentintroductiontohowzonesworkinthebrowserExecutionoutsidetheAngularzoneshowsyouhowtoperformlong-runningoperationswithoutincurringazoneoverheadConfiguringcomponentstouseexplicitchangedetectionwithOnPushdescribeshowtomanuallycontrolAngular'schangedetectionprocess
ExecutionoutsidetheAngularzoneTheutilityofzone.jsisterrific,sinceitworksautomatically,butaseasonedsoftwareengineerknowsthisoftencomesataprice.Thisisespeciallytruewhentheconceptofdatabindingcomesintoplay.
Inthisrecipe,you'lllearnhowtoexecuteoutsidetheAngularzoneandwhatbenefitsthisaffordsyou.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/3362/.
Howtodoit...Tocompareexecutionindifferentcontexts,createtwobuttonsthatrunthesamecodeindifferentzonecontexts.Thebuttonsshouldcountto100withsetTimeoutincrements.Usetheperformanceglobaltomeasurethetimeittakes:
[src/app/app.component.ts]
import{Component,NgZone}from'@angular/core';
@Component({
selector:'app-root',
template:`
<button(click)="runInsideAngularZone()">
RuninsideAngularzone
</button>
<button(click)="runOutsideAngularZone()">
RunoutsideAngularzone
</button>
`
})
exportclassAppComponent{
progress:number=0;
startTime:number=0;
constructor(privatezone:NgZone){}
runInsideAngularZone(){
this.start();
this.step(()=>this.finish('InsideAngularzone'));
}
runOutsideAngularZone(){
this.start();
this.step(()=>this.finish('OutsideAngularzone'));
}
start(){
this.progress=0;
this.startTime=performance.now();
}
finish(location:string){
this.zone.run(()=>{
console.log(location);
console.log('Took'+
(performance.now()-this.startTime)+'ms');
});
}
step(doneCallback:()=>void){
if(++this.progress<100){
setTimeout(()=>{
this.step(doneCallback);
},10);
}else{
doneCallback();
}
}
}
Atthispoint,thetwobuttonswillbehaveidentically,asbothofthemarebeingexecutedinsidetheAngularzone.
InordertoexecuteoutsidetheAngularzone,you'llneedtousetherunOutsideAngular()methodexposedbyNgZone:
runInsideAngularZone(){
this.start();
this.step(()=>this.finish('InsideAngularzone'));
}
runOutsideAngularZone(){
this.start();
this.zone.runOutsideAngular(()=>{
this.step(()=>this.finish('OutsideAngularzone'));
});
}
Atthispoint,youcanrunbothofthemagainsidebysideandverifythattheystilltake(roughly)thesameamountoftimetoexecute.Thisshouldnotsurpriseyou,astheyarestillperformingthesametask.Theinclusionofzone.jsmeansthatthebrowserAPIsareshimmedoutsideAngular,soevenrunningthisoutsidetheAngularzonemeansitisstillrunninginsideazone.
Inordertoseeaperformancedifference,you'llneedtointroducesomebindinginsidethetemplate:
[src/app/app.component.ts]
import{Component,NgZone}from'@angular/core';
@Component({
selector:'app-root',
template:`
<h3>Progress:{{progress}}%</h3>
<button(click)="runInsideAngularZone()">
RuninsideAngularzone
</button>
<button(click)="runOutsideAngularZone()">
RunoutsideAngularzone
</button>
`
})
exportclassAppComponent{
...
}
Nowyoushouldbegintoseeasubstantivedifferencebetweentheruntimesofeachbutton.Thisshowsthatwhentheoverheadofbindingsiscausingtheapplicationtoslowdown,runOutsideAngular()hasthepotentialtoyieldsurprisinglysubstantiveperformanceoptimizations.
Howitworks...WhenyouexaminetheNgZonesource,you'llfindthatthe"outer"zoneismerelythetopmostbrowserzone.AngularforksthiszoneuponinitializationandbuildsuponittoyieldtheniceNgZoneservicewrapper.
However,becausezonesdonotdiscriminateintherealmofasynchronouscallbacksanddatabinding,eachinvocationofthesetTimeouthandlerinsidetheAngularzoneisrecognizedasaneventthathasimplicationsonthetemplaterenderingprocess.Ineveryinvocation,Angularseesanupdatetothebounddatafollowinganasynchronoustask,andproceedstorerendertheview.Whenthisisdone100times,itaddsuptoseveralhundredextramillisecondsofexecution.
WhenthisisrunoutsidetheAngularzone,AngularisnolongerawareofthesetTimeouttasksthatarebeingexecutedandsodoesnotrequirearerender.Upontheveryfinaliterationthough,invokeNgZone.run();thiswillcausetheexecutiontorejointheAngularzone.Angularseesthetaskandthemodifieddataandupdatesthebindingsaccordingly;thistimethough,thisisdoneonlyonce.
There'smore...Inthisrecipe,thefinish()methodinvokestherun()methodforboththeAngularzoneandthenon-Angularzone.Whenthezonethatthisisinvokeduponisalreadythecontextualzoneinwhichthetaskisbeingexecuted,usingrun()becomesredundantandiseffectivelyano-op.
SeealsoWorkingwithzonesoutsideAngularisanexcellentintroductiontohowzonesworkinthebrowserListeningforNgZoneeventsgivesabasicunderstandingofhowAngularisusingzonesConfiguringcomponentstouseexplicitchangedetectionwithOnPushdescribeshowtomanuallycontrolAngular'schangedetectionprocess
ConfiguringcomponentstouseexplicitchangedetectionwithOnPushTheconventionofAngular'sdataflow,inwhichdataflowsdownwardthroughthecomponenttreeandeventsfloatupwards,engenderssomeinterestingpossibilities.OneoftheseinvolvescontrollingwhenAngularshouldperformchangedetectionatagivennodeinthecomponenttree.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/4909/.
GettingreadyBeginwiththefollowingsimpleapplication:
[app/root.component.ts]
import{Component}from'@angular/core';
import{Subject}from'rxjs/Subject';
import{Observable}from'rxjs/Observable';
@Component({
selector:'root',
template:`
<button(click)="shareSubject.next($event)">Share!</button>
<article[shares]="shareEmitter"></article>
`
})
exportclassRootComponent{
shareSubject:Subject<Event>=newSubject();
shareEmitter:Observable<Event>=
this.shareSubject.asObservable();
}
[app/article.component.ts]
import{Component,Input,ngOnInit}from'@angular/core';
import{Observable}from'rxjs/Observable';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>Shares:{{count}}</p>
`
})
exportclassArticleComponentimplementsOnInit{
@Input()shares:Observable<Event>;
count:number=0;
title:string=
'InsuranceFraudGrowsInWakeofApplePieHubbub';
ngOnInit(){
this.shares.subscribe((e:Event)=>++this.count);
}
}
Thisverysimpleapplicationisjustusinganobservabletopassshareeventsdown,fromaparentcomponenttoachildcomponent.Thechildcomponentkeepstrackoftheclickeventcountandinterpolatesthiscounttothepage.
Yourobjectiveistomodifythissetupsothatthechildcomponentonlydetectsachangewhenaneventisemittedfromtheobservable.
Howtodoit...Outofthebox,Angularalreadyhasaveryrobustwayofdetectingchange:zones.Wheneverthereisaneventinsideazone,Angularrecognizesthatthiseventhasthepotentialtomodifytheapplicationinameaningfulway.Itthenperformschangedetectionfromthetopofthecomponenttreedowntothebottom,checkingwhetheranythingneedstobeupdatedinthechangedmodel.Sincedataonlyflowsdownward,thisisalreadyanextremelyefficientwayofhandlingit.
However,youmightliketoexertsomecontrolinthissituationsinceyoumaybeabletohand-optimizewhenchangedetectionshouldoccurinsideacomponent.Mostlikelyyouwillveryeasilybeabletotellwhenacomponentmayormaynotchange,basedonwhatishappeningaroundit.
ConfiguringtheChangeDetectionStrategy
Angularoffersyoutheoptionofchanginghowthechangedetectionschemeworks.IfyouwouldpreferthatAngularrefrainfromlisteningtozoneeventstokickoffaroundofchangedetection,youcaninsteadconfigureacomponenttoonlyperformchangedetectionwhenaninputischanged.ThiscanbedonebyconfiguringthecomponenttousetheOnPushstrategyinsteadofthedefault.
Surely,thiswilloccurlessoftenthanthefirehoseofbrowserevents.Ifthecomponentwillonlychangewhenaninputischanged,thenthiswillsaveAngularthetroubleofdoinganiterationofchangedetectiononthatcomponent.
ModifyArticleComponenttoinsteaduseOnPush:
[app/article.component.ts]
import{Component,Input,ngOnInit,ChangeDetectionStrategy}
from'@angular/core';
import{Observable}from'rxjs/Observable';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>Shares:{{count}}</p>
`,
changeDetection:ChangeDetectionStrategy.OnPush
})
exportclassArticleComponentimplementsOnInit{
@Input()shares:Observable<Event>;
count:number=0;
title:string=
'InsuranceFraudGrowsInWakeofApplePieHubbub';
ngOnInit(){
this.shares.subscribe((e:Event)=>++this.count);
}
}
Thissuccessfullychangesthestrategy,butthereisonesignificantproblemnow:thecountwillnolongerbeupdated.Thishappens,ofcourse,becausetheinputtothiscomponentisnotbeingchanged.
ThecountonlyupdatedbeforebecauseAngularwasseeingclickeventsonthebutton,whichcausedchangedetectiononArticleComponent.Now,Angularhasbeeninstructedtoignoretheseclickevents,eventhoughthecountinsidethechildcomponentisstillbeingupdatedfromtheobservablehandlers.
Requestingexplicitchangedetection
Youareabletoinjectareferencetothecomponent'schangedetector.Withthis,itexposesamethodthatallowsyoutoforcearoundofchangedetectionwheneveryoulike.Sincethemodelisbeingupdatedinsideanobservablehandler,thisseemslikeafineplacetotriggerthechangedetectionprocess:
[app/article.component.ts]
import{Component,Input,ngOnInit,ChangeDetectionStrategy,
ChangeDetectorRef}from'@angular/core';
import{Observable}from'rxjs/Observable';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
<p>Shares:{{count}}</p>
`,
changeDetection:ChangeDetectionStrategy.OnPush
})
exportclassArticleComponentimplementsOnInit{
@Input()shares:Observable<Event>;
count:number=0;
title:string=
'InsuranceFraudGrowsInWakeofApplePieHubbub';
constructor(privatechangeDetectorRef_:ChangeDetectorRef){}
ngOnInit(){
this.shares.subscribe((e:Event)=>{
++this.count;
this.changeDetectorRef_.markForCheck();
});
}
}
Nowyouwillseethecountgettingupdatedonceagain.
Howitworks...UsingOnPushessentiallyconvertsacomponenttooperatelikeablackbox.Iftheinputdoesn'tchange,thenAngularassumesthatthestateinsidethecomponentwillremainconstant,andthereforethereisnoneedtoproceedfurtherwithregardtodetectingachange.
Sincechangedetectionalwaysflowsfromtoptobottominthecomponenttree,therequestforchangedetectionusesmarkForCheck(),marksthecomponent'schangedetectortorunonlywhenAngularreachesthatcomponentinsidethetree.
There'smore...Thisisausefulpatternifyou'relookingtosqueezeadditionalperformancefromyourapplication,butinsomecases,doingthiscandevelopintoananti-pattern.TheneedtoexplicitlydefinewhenAngularshouldperformchangedetectioncanbecometediousasyourcomponentcodegrowsinsize.TherecanpotentiallybebugsthatarisefrommissingoneplacewheremarkForCheck()shouldhavebeeninvokedbutwasnot.Angular'schangedetectionstrategyisalreadyquiteperformantandrobust,sousethisconfigurationwisely.
SeealsoWorkingwithzonesoutsideAngularisanexcellentintroductiontohowzonesworkinthebrowserListeningforNgZoneeventsgivesabasicunderstandingofhowAngularisusingzonesExecutionoutsidetheAngularzoneshowsyouhowtoperformlong-runningoperationswithoutincurringazoneoverheadConfiguringViewEncapsulationformaximumefficiencyshowshowyoucanconfigurecomponentstoutilizetheshadowDOM
ConfiguringViewEncapsulationformaximumefficiencyAlthoughitmaysoundclichéd,Angular2wasbuiltforthebrowsersoftomorrow.Youcanpointtowhythisisthecaseinalargenumberofways,butthereisonewaywherethisisextremelytrue:componentencapsulation.
TheidealcomponentmodelforAngular2istheoneinwhichcomponentsareentirelysandboxed,saveforthefewpiecesthatareexternallyvisibleandmodifiable.Inthisrespect,itdoesabang-upjob,buteventhemostmodernbrowserslimititsabilitytostriveforsuchefficacy.ThisisespeciallytrueintherealmofCSSstyling.
SeveralfeaturesofAngular'scomponentstylingareespeciallyimportant:
YouareabletowritestylesthatareguaranteedtobeonlyapplicabletoacomponentYoucanexplicitlyspecifystylesthatshouldbeinheriteddownwardthroughthecomponenttreeYoucanspecifyanencapsulationstrategyonapiecewisebasis
Thereareanumberofinterestingwaystoaccomplishanefficientcomponentstylingschema.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/1463/.
GettingreadyBeginwithasimpleapplicationcomponent:
[src/app/app.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'app-root',
template:`
<h1>
{{title}}
</h1>
`,
styles:[`
h1{
color:red;
}
`]
})
exportclassAppComponent{
title='appworks!';
}
Howtodoit...AngularwilldefaulttonotusingShadowDOMforcomponents,asthemajorityofbrowsersdonotsupportittoasatisfactorylevel.Thenextbestthing,whichitwilldefaultto,istoemulatethisstylingencapsulationbynestingyourselectors.Theprecedingcomponentiseffectivelytheequivalentofthefollowing:
[src/app/app.component.ts]
import{Component,ViewEncapsulation}from'@angular/core';
@Component({
selector:'app-root',
template:`
<h1>
{{title}}
</h1>
`,
styles:[`
h1{
color:red;
}
`],
encapsulation:ViewEncapsulation.Emulated
})
exportclassAppComponent{
title='appworks!';
}
Emulatedstylingencapsulation
HowexactlydoesAngularperformthisemulation?Lookattherenderedapplicationtofindout.Yourcomponentwillappearsomethinglikethefollowing:
<app-root_nghost-mqf-1="">
<h1_ngcontent-mqf-1="">
appworks!
</h1>
</app-root>
Intheheadofthedocument:
<style>
h1[_ngcontent-mqf-1]{
color:red;
}
</style>
Thepictureshouldbeabitclearernow.Angularisassigningthiscomponentclass(notaninstance)auniqueID,andthestylesdefinedfortheglobaldocumentwillonlybeapplicableto
tagsthathavethematchingattribute.
Nostylingencapsulation
Ifthisencapsulationisn'tnecessary,youarefreetouseencapsulation:ViewEncapsulation.None.AngularwillhappilyskiptheuniqueIDassignmentstepforyou,givingyouavanilla:
<app-root>
<h1>
appworks!
</h1>
</app-root>
Intheheadofthedocument:
<style>
h1{
color:red;
}
</style>
Nativestylingencapsulation
Thebest,mostfuturistic,andleastsupportedmethodofgoingaboutthisistouseShadowDOMinstancestogoalongwitheachcomponentinstance.Thiscanbeaccomplishedusingencapsulation:ViewEncapsulation.Native.Now,yourcomponentwillrender:
<app-root>
#shadow-root
<style>
h1{
color:red;
}
</style>
<h1>
appworks!
</h1>
</app-root>
Howitworks...Angularissmartenoughtorecognizewhereitneedstoputyourstylesandhowtomodifythemtomakethemworkforyourcomponentconfiguration:
ForNoneandEmulated,stylesgointothedocumentheadForNative,stylesgoinlinewiththerenderedcomponentForNoneandNative,nostylemodificationsareneededForEmulated,stylesarerestrictedbyattributeselectors
There'smore...AnimportantconsiderationofViewEncapsulationchoicesisCSSperformance.ItiswellknownandentirelyintuitivethatCSSstylingismoreefficientwhenithastotraverseasmallerpartoftheDOMandhastomatchusingasimplerselector.
Emulatingcomponentencapsulationaddsanattributeselectortoeachandeverystylethatisdefinedforthatcomponent.Atscale,itisn'thardtoseehowthiscandegradeperformance.ShadowDOMelegantlysolvesthisproblembyofferingunmodifiedstylesinsidearestrictedpieceofDOM.Itsstylescannotescapebutcanbeapplieddownwardtoothercomponents.Furthermore,ShadowDOMcomponentscanbenestedandstrategicallyapplied.
SeealsoUnderstandingandproperlyutilizingenableProdModewithpureandimpurepipesdescribeshowtotakethetrainingwheelsoffyourapplicationConfiguringcomponentstouseexplicitchangedetectionwithOnPushdescribeshowtomanuallycontrolAngular'schangedetectionprocess
ConfiguringtheAngular2RenderertousewebworkersOneofthemostcompellingintroductionsinthenewrenditionoftheAngularframeworkisthetotalabstractionoftherenderingexecution.ThisstemsfromoneofthecoreideasofAngular:youshouldbeabletoseamlesslysubstituteoutanybehaviormoduleandreplaceitwithanother.This,ofcourse,meansthatAngularcannothaveanydependencybleedoutsideofthemodules.
OneplacethatAngularputsemphasisonbeingconfigurableisthelocationwherecodeexecutiontakesplace.Thisismanifestedinanumberofways,andthisrecipewillfocusonAngular'sabilitytoperformrenderingexecutionatalocationotherthaninsidethemainbrowser'sJavaScriptruntime.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/1859/.
GettingreadyBeginwithsomesimpleapplicationelementsthatdonotyetformafullapplication:
[index.html]
<!DOCTYPEhtml>
<html>
<head>
<scriptsrc="zone.js"></script>
<scriptsrc="reflect-metadata.js"></script>
<scriptsrc="system.src.js"></script>
<scriptsrc="system-config.js"></script>
</head>
<body>
<article></article>
<script>
System.import('system-config.js')
.then(function(){
System.import('main.ts');
});
</script>
</body>
</html>
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<h2>{{title}}</h2>
`
})
exportclassArticleComponent{
title:string=
'SurveyIndicatesPlasticFunnelBestWaytoDrinkRareWine';
}
NotethatthisrecipeusesSystemJStohandlemodulesandTypeScripttranspilation,butthisismerelytokeepthedemonstrationsimple.AproperlycompiledapplicationcanuseregularJavaScripttoaccomplishthesamefeat,withnodependencyonSystemJS.
Thisrecipewillstartoffbyassumingyouhaveabasicknowledgeofwhatwebworkersareandhowtheywork.Thereisadiscussionoftheirpropertieslaterinthisrecipe.
Howtodoit...TheSystemJSstartupconfigurationkicksofftheapplicationfrommain.ts,soyou'llbeginthere.Insteadofbootstrappingtheapplicationinthisfileasyounormallywould,you'lluseanimportedAngularhelpertoinitializethewebworkerinstance:
[main.ts]
import{bootstrapWorkerUi}from"@angular/platform-webworker";
bootstrapWorkerUi(window.location.href+"loader.js");
Concernyourselfwiththeprecisedetailsofwhatthisisdoinglater,butthehigh-levelideaisthatthisiscreatingawebworkerinstancethatisintegratedwiththeAngularRenderer.
Note
RecallthatinitializingawebworkerrequiresapathtoitsstartupJSfile,andarelativepathinsidethisfilemightnotalwayswork;therefore,you'reusingthewindowlocationtoprovideanabsoluteURL.Thenecessityofthismaydifferbasedonyourdevelopmentsetup.
Sincethisfilereferencesawebworkerinitializationfilecalledloader.js,writeitnext.WorkerscannotbegivenaTypeScriptfile:
[loader.js]
importScripts(
"system.js",
"zone.js",
"reflect-metadata.js",
"./system-config.js");
System.import("./web-worker-main.ts");
Thisfilefirstimportsthesamefilesthatarelistedinsidethe<head>tagofindex.html.ThisshouldmakesensesincethewebworkerwillneedtoperformsomeofthedutiesofAngularbutwithoutdirectaccesstoanythingthatexistsinsidethemainJavaScriptruntime.Forexample,sincethiswebworkerwillberenderingacomponent,itneedstobeabletounderstandthe@Component({})notation,whichcannotbedonewithoutthereflect-metadataextension.
Justlikethemainapplication,thewebworkeralsohasaninitializationfile.Thistakestheformofweb-worker-main.ts:
[web-worker-main.ts]
import{AppModule}from'./app/app.module';
import{platformWorkerAppDynamic}
from'@angular/platform-webworker-dynamic';
platformWorkerAppDynamic().bootstrapModule(AppModule);
Comparedtoanormalmain.tsfile,thisfileshouldlookdelightfullyfamiliar.Angularprovidesyouwithatotallyseparateplatformmodule,butonethataffordsyouanidenticalAPI,whichyouareusingheretobootstraptheapplicationfromtheyet-to-be-definedAppModule.Definethisnext:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{WorkerAppModule}from'@angular/platform-webworker';
import{AppModule}from'./app.module';
import{ArticleComponent}from'./article.component';
@NgModule({
imports:[
WorkerAppModule
],
bootstrap:[
ArticleComponent
],
declarations:[
ArticleComponent
]
})
exportclassAppModule{}
SimilartohowyouwouldnormallyuseBrowserModulewithinaconventionaltop-levelappmodule,Angularprovidesaweb-worker-flavoredWorkerAppModulethathandlesallthenecessaryintegration.
Howitworks...Makenomistakeaboutwhatishappeninghere:yourapplicationisnowrunningontwoseparateJavaScriptthreads.
WebworkersarebasicallyjustreallydumbJavaScriptexecutionbuckets.Wheninitialized,theyaregivenaninitialpieceofJavaScripttorun,whichinthisexampletooktheformofloader.js/.Theyhavenounderstandingofwhatisgoingoninthemainbrowserruntime.Theycan'tinteractwiththeDOM,andyouareonlyabletocommunicatewiththemviaPostMessages.AngularbuildsanelegantabstractionontopofPostMessagestocreateabusinterface,anditisthisinterfacethatisusedtojointhetworuntimestogether.
IfyoulookintothePostMessagespecification,youwillnoticethatallofthedatapassedasthemessagemustbeserializedintoastring.HowthencanthisrenderingconfigurationpossiblyworkwiththeDOMinthemainbrowser,handlingeventsanddisplayingtheHTMLandthewebworkerperformingtherenderingonaDOMitcannottouch?
Theanswerissimple:Angularserializeseverything.Whentheinitialrenderingoccurs,oraneventinthebrowseroccurs,Angulargrabseverythingitneedstoknowaboutthecurrentstate,wrapsitupintoaserializedstring,andshipsitofftothewebworkerrendererontheproperchannel.Thewebworkerrendererunderstandswhat'sbeingpassedtoit.AlthoughitcannotaccessthemainDOM,itiscertainlyabletoconstructHTMLelements,understandhowtheserializedeventspassedtoitwillaffectthem,andperformtherendering.
Whenthetimecomestotellthebrowserwhattoactuallyrender,itwillinturnserializetherenderedcomponentandsenditbacktothemainbrowserruntime,whichwillunpackthestringandinsertitintotheDOM.
TotheAngularframework,becauseitisabstractedfromallthebrowserdependenciesthatmightgetinthewayofthis,everythingseemsnormal.Eventscomein,they'rehandled,andarendererservicetellsitwhattoputintotheDOM.EverythingthathappensinbetweenisunimportanttotheAngularframework,whichdoesn'tcarethateverythinghappenedinatotallyseparateJavaScriptruntime.
Note
Notethattheelementsyoubeginwithhavenothingunusualaboutthemtoallowwebworkercompatibility.ThisunderscorestheeleganceofAngular'swebworkerabstraction.
There'smore...Aswebworkersaremorefullysupportedandutilized,patternssuchasthesewillmostlikelybecomemoreandmorecommon,anditisextremelyprescientoftheAngularteamtosupportthisbehavior.Thereare,however,someconsiderations.
Optimizingforperformancegains
Oneoftheprimarybenefitsofusingwebworkersisthatyounowhaveaccesstoanexecutioncontextthatisnotblockedbyanythingrunningonthebrowserexecutioncontext.Forperformancedrags,suchasreflowandblockingofeventloophandlers,thewebworkerwillcontinuewiththeexecutionwithoutacareforwhatishappeningelsewhere.
Therefore,gettingaperformancebenefitbecomesaproblemofoptimization.CommunicationbetweenthetwoJavaScriptthreadsrequiresserializationandtransmissionofevents,whichisobviouslynotasfastashandlingtheminthesamethread.However,inanespeciallycomplicatedapplication,renderingcanquicklybecomeoneofthemostexpensivethingsyourapplicationwilldo.Therefore,youmayneedtoexperimentandmakeajudgmentcallforyourapplication,asnotallapplicationswillseeperformancegainsfromusingwebworkers—onlythosewhererenderingbecomesprohibitivelyexpensive.
Compatibilityconsiderations
Webworkershaveextremelygoodsupport,butthereisstillaverysignificantnumberofbrowsersthatdonotsupportthem.Ifyourapplicationneedstoworkuniversally,webworkersarenotrecommended;sincetheywillnotgracefullydegrade,yourapplicationwillmerelyfail.
SeealsoConfiguringtheAngular2rendererservicetousewebworkersguidesyouthroughthesettingupofyourapplicationtorenderonawebworkerConfiguringapplicationstouseahead-of-timecompilationguidesyouthroughhowtocompileanapplicationduringthebuildConfiguringanapplicationtouselazyloadingshowshowyoucandelayservingchunksofapplicationcodeuntilneeded
Configuringapplicationstouseahead-of-timecompilationAngular2introducestheconceptofahead-of-timecompilation(AOT).Thisisanalternateconfigurationinwhichyoucanrunyourapplicationstomovesomeprocessingtimefrominsidethebrowser(referredtoasjust-in-timecompilationorJIT)towhenyoucompileyourapplicationontheserver.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/9253/.
GettingreadyAOTcompilationisapplication-agnostic,soyoushouldbeabletoaddthistoanyexistingAngular2applicationwithminimalmodification.
Forthepurposesofthisexample,supposeyouhaveanexistingAppModuleinsideapp/app.module.ts.Youneedn'tconcernyourselfwithitscontentsinceitisirrelevantforthepurposeofAOT.
Howtodoit...UsingAOTmeansyouwillcompileandbootstrapyourapplicationdifferently.Dependingonhowitisconfigured,youwillprobablywanttousesiblingfileswith"aot"addedtothename.Bearinmindthatthisisonlyfororganizationalpurposes;Angularisnotconcernedwithwhatyounameyourfiles.
InstallingAOTdependencies
Angularrequiresanewcompilationtoolngc,whichisincludedinthe@angular/compiler-clipackage.ItwillalsomakeuseofplatformBrowsertobootstrap,whichisprovidedinsidethe@angular/platform-browserpackage.Installbothoftheseandsavethedependencytopackage.json:
npminstall@angular/compiler-cli@angular/platform-server--save
Configuringngc
ngcwillessentiallyperformasupersetofdutiesofthetsccompilationtoolyouareaccustomedtousing.Itiswisetoprovideitwithaseparateconfigfile,whichcanbenamedtsconfig-aot.json(butyouarenotbeholdentothisname).
Thefileshouldappearidenticaltoyourexistingtsconfig.jsonfilebutwiththefollowingimportantmodifications:
[tsconfig-aot.json]
{
"compilerOptions":{
(lotsofsettingsherealready,onlychange'module')
"module":"es2015",
},
"files":[
"app/app.module.ts",
"main.ts"
],
"angularCompilerOptions":{
"genDir":"aot",
"skipMetadataEmit":true
}
}
AligningcomponentdefinitionswithAOTrequirements
AOTcompilationrequiresyourapplicationtobeorganizedinafewspecificways.Nothingshouldbreakanexistingapplication,butthey'reimportanttodoandtakenoteof.Ifyou'vebeen
followingAngularbestpractices,theyshouldbeabreeze.
First,componentdefinitionsmusthaveamoduleIdspecified.YouwillfindthatcomponentsgeneratedwiththeAngularCLIalreadyhavethisincluded.Ifnot,themoduleIdshouldalwaysbemodule.id,asshownhere:
@Component({
moduleId:module.id,
(lotsofotherstuff)
})
SincethemoduleisundefinedwhencompilinginAOT,youcanprovideadummyvalueintheroottemplate:
<script>window.module='aot';</script>
Tip
AOTdoesn'tneedthemoduleID,butitallowsyoutohaveacompilationwithouterrors.NotethatthesestepsinvolvingmoduleIdmaybechangedoreliminatedentirelyinfutureversionsofAngular,astheyonlyexisttoallowyoutohavecompatibilitybetweenbothJITandAOTcompilations.
Next,you'llneedtochangethepathofthetemplates,bothCSSandHTML,toberelativetothecomponentdefinitionfile,notapplication-root-relative.IfyouareusingtheconventiongivenbytheAngularCLIorthestyleguide,youareprobablyalreadydoingthis.
Compilingwithngc
ngcisn'tavailabledirectlyonthecommandline;you'llneedtorunthebinaryfiledirectly.Additionally,sinceinthisexampleit'snotusingthetsconfig.jsonnamingconvention,you'llneedtopointthebinarytothelocationofthealternateconfigfile.Runthefollowingcommandfromtherootofyourapplicationtoexecutethecompilation:
node_modules/.bin/ngc-ptsconfig-aot.json
ThiscommandwilloutputacollectionofNgFactoryfileswith.ngfactory.tsinsidetheaotdirectory,asyouspecifiedearlierintheangularCompilerOptionssectionoftsconfig-aot.json.
Thecompilationisdone,butyourapplicationdoesn'tyetknowhowtousetheseNgFactoryinstances.
BootstrappingwithAOT
YourapplicationwillnowstartoffwithAppModuleNgFactoryinsteadofAppModule.Bootstrap
itusingplatformBrowser:
[main.ts]
import{platformBrowser}from'@angular/platform-browser';
import{AppModuleNgFactory}from'aot/app/app.module.ngfactory';
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
Withthis,you'llbeabletorunyourapplicationnormallybutthistimeusingprecompiledfiles.
Howitworks...CompilinginAngularisacomplexsubject,butthereareseveralmainpointsrelevanttoswitchinganapplicationtoanAOTbuildprocess:
TheAngularcompilerlivesinsidethe@angular/compilermodule,whichisquitelarge.UsingAOTmeansthismodulenolongerneedstobedeliveredtothebrowser,whichyieldssubstantialbandwidthsavings.TheAngularcompilationprocessinvolvestakingthetemplatestringsinsideyourcomponents,whichexistasdefinedbytemplateortemplateUrl,andconvertingthemintoNgFactoryinstances.TheseinstancesspecifyhowAngularunderstandshowyoudefinedthetemplate.TheconversiontoFactoryinstanceshappensbothinJITandAOTcompilation;switchingtoAOTsimplymeansyouaredoingthisprocessingontheserverinsteadoftheclientandservingNgFactorydefinitionsdirectlyinsteadofuncompiledtemplatestrings.AOTwillobviouslyslowdownyourbuildtimesincemorecomputationisrequiredeachtimethecodebaseneedsupdating.SinceyoucantrustthatAngularwillbeabletocorrectlyinterpretNgFactorydefinitionsthattheAOTcompilationgenerates,itisbesttodoJITcompilationwhendevelopingtheapplicationandAOTcompilationwhenbuildinganddeployingproductionapplications.
There'smore...AOTiscool,butitmightnotbeforeverybody.ItwillverylikelyreducetheloadtimeandinitializationtimeofyourAngularapplication,butyourbuildprocessisconsiderablymoreinvolvednow.What'smore,ifyouwanttouseJITindevelopmentbutAOTinproduction,younowhavetomaintaintwoversionsofthreedifferentfiles:index.html,main.ts,andtsconfig.json.Perhapsthisadditionalbuildcomplexityoverheadisworthit,butitshouldcertainlybeajudgmentcallbasedonyourdevelopmentsituation.
GoingfurtherwithTreeShaking
AngularalsosupportsaseparatestepofoptimizationcalledTreeShaking.Thisusesaseparatenpmlibrarycalledrollup.Essentially,thislibraryreadsinyourentireapplication(asJSfiles),figuresoutwhatmodulesarenotbeingused,andcutsthemoutofthecompiledcodebaseandthereforeofthepayloadthatisdeliveredtothebrowser.Thisisanalogoustoshakingatreetomakethedeadbranchesfallout,hence"treeshaking."
Note
Thees2015moduleconfigurationspecifiedearlierintsconfig-aot.jsonwasforsupportingtherolluplibrary,asitisarequirementforcompatibility.
Ifyourapplicationcodebaseiswell-maintained,youwillmostlikelyseelimitedbenefitsoftreeshakingsinceunusedimportsandthelikewillbecaughtwhencoding.What'smore,onecouldmaketheargumentthattreeshakingmightbeananti-patternsinceitsubtlyencouragesaliberaluseofmoduleinclusionbythedeveloperwiththeknowledgethattreeshakingwilldothedirtyworkingofcuttingoutanyunusedmodules.Thismaythenleadtoaclutteredcodebase.
UsingtreeshakingcanbeausefultoolwhenitcomestoAngular2applications,butitsusefulnessisinmanywaysevaporatedbykeepingacodebasetidy.
SeealsoUnderstandingandproperlyutilizingenableProdModewithpureandimpurepipesdescribeshowtotakethetrainingwheelsoffyourapplicationConfiguringtheAngular2rendererservicetousewebworkersguidesyouthroughtheprocessofsettingupyourapplicationtorenderonawebworkerConfiguringanapplicationtouselazyloadingshowshowyoucandelayservingchunksofapplicationcodeuntilneeded
ConfiguringanapplicationtouselazyloadingLazyloadedapplicationsarethosethatdefertheretrievalofrelevantresourcesuntiltheyareactuallynecessary.Onceapplicationsbegintoscale,thiscanyieldmeaningfulgainsinperformance,andAngular2supportslazyloadingrightoutofthebox.
Note
Thecode,links,andaliveexamplerelatedtothisrecipeareavailableathttp://ngcookbook.herokuapp.com/0279/.
GettingreadySupposeyoubeginwiththefollowingsimpleapplication:
[app/root.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<router-outlet></router-outlet>
`
})
exportclassRootComponent{}
[app/link.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'app-link',
template:`
<arouterLink="/article">article</a>
`
})
exportclassLinkComponent{}
[app/article.component.ts]
import{Component}from'@angular/core';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
`
})
exportclassArticleComponent{
title:string=
'Baboon'sStockPicksCrushTop-PerformingHedgeFund';
}
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{ArticleComponent}from'./article.component';
import{LinkComponent}from'./link.component';
constappRoutes:Routes=[
{
path:'article',
component:ArticleComponent
},
{
path:'**',
component:LinkComponent
}
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
ArticleComponent,
LinkComponent,
RootComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Thisapplicationhasonlytworoutes:thedefaultroute,whichdisplaysalinktothearticlepage,andthearticleroute,whichdisplayArticleComponent.Yourobjectiveistodefertheloadingoftheresourcesrequiredbythearticlerouteuntilitisactuallyvisited.
Howtodoit...LazyloadingmeanstheinitialapplicationmodulethatisloadedcannothaveanydependenciesonthemodulethatyouwishtolazilyloadsincenoneofthatcodewillbepresentbeforethenewURLisvisited.First,movetheArticleComponentreferencetoitsownmodule:
[app/article.module.ts]
import{NgModule}from'@angular/core';
import{ArticleComponent}from'./article.component';
@NgModule({
declarations:[
ArticleComponent
],
exports:[
ArticleComponent
]
})
exportclassArticleModule{}
Removingalldependenciesmeansmovingtherelevantroutedefinitionstothismoduleaswell:
[app/article.module.ts]
import{NgModule}from'@angular/core';
import{ArticleComponent}from'./article.component';
import{Routes,RouterModule}from'@angular/router';
constarticleRoutes:Routes=[
{
path:'',
component:ArticleComponent
}
];
@NgModule({
imports:[
RouterModule.forChild(articleRoutes)
],
declarations:[
ArticleComponent
],
exports:[
ArticleComponent
]
})
exportclassArticleModule{}
Next,removeallthesemodulereferencesfromAppModule.Inaddition,modifytheroutedefinition,sothatinsteadofspecifyingacomponenttorender,itsimplyreferencesapathtothelazilyloadedmodule,aswellasthenameofthemoduleusingaspecial#syntax:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{LinkComponent}from'./link.component';
constappRoutes:Routes=[
{
path:'article',
loadChildren:'./app/article.module#ArticleModule'
},
{
path:'**',
component:LinkComponent
}
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes)
],
declarations:[
LinkComponent,
RootComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
Thisisallthat'srequiredtosetuplazyloading.Yourapplicationshouldbehaveidenticallytowhenyoubeganthisrecipe.
Howitworks...Toverifythatitisinfactperformingalazyload,starttheapplicationandkeepaneyeontheNetworktabofyourbrowser'sdeveloperconsole.
WhenyouclickonthearticlerouterLink,youshouldseearticle.module.tsandarticle.component.tsrequestsgooutbeforetherenderingoccurs.ThismeansAngularisonlyfetchingtherequiredfilesonceyouactuallyvisittheroute.TheloadChildrenroutepropertytellsAngularthatwhenitvisitsthisroute,ifithasn'tloadedthemodulealready,itshouldusetherelativepathyoumayhaveprovidedtofetchthemodule.Oncethemodulefileisretrieved,Angularisabletoparseitandknowwhichotherfilesitneedstorequesttoloadallthemodule'sdependencies.
There'smore...You'llnotethatthisintroducesabitofadditionallatencytoyourapplicationsinceAngularwaitstoloadtheresourcesrightwhenitactuallyneedsthem.What'smore,inthisexample,ithastoactuallyperformtwoadditionalroundtripstotheserverwhenitvisitsthearticleURL:onetorequestthemodulefileandonetorequestthemodule'sdependencies.
Inaproductionenvironment,thislatencymightbeunacceptable.Aworkaroundmightbetocompilethelazilyloadedpayloadintoasinglefilethatcanbefetchedwithonerequest.Dependingonhowyourapplicationisbuilt,yourmileagemayvary.
Accountingforsharedmodules
Thelazilyloadedmoduleistotallyseparatedfromyourmainapplicationmodule,andthisincludesinjectables.Ifaserviceisprovidedtothetop-levelapplicationmodule,youwillfindthatitwillcreatetwoseparateinstancesofthatserviceforeachplaceitisprovided—certainlyunexpectedbehavior,giventhatanapplicationloadednormallywillonlycreateoneinstanceifitisonlyprovidedonce.
ThesolutionistopiggybackontheforRootmethodthatAngularusestosimultaneouslyprovideandconfigureservices.Morerelevanttothisrecipe,itallowsyoutotechnicallyprovideaserviceatmultiplelocations;however,Angularwillknowhowtoignoreduplicatesofthis,provideditisdoneinsideforRoot().
First,definetheAuthServicethatyouwishtocreateonlyasingleinstanceof:
[app/auth.service.ts]
import{Injectable}from'@angular/core';
@Injectable()
exportclassAuthService{
constructor(){
console.log('instantiatedAuthService');
}
}
Thisincludesalogstatementsoyoucanseethatonlyoneinstantiationoccurs.
Next,createanNgModulewrapperspeciallyforthisservice:
[app/auth.module.ts]
import{NgModule,ModuleWithProviders}from"@angular/core";
import{AuthService}from"./auth.service";
@NgModule({})
exportclassAuthModule{
staticforRoot():ModuleWithProviders{
return{
ngModule:AuthModule,
providers:[
AuthService
]
};
}
}
SincethisutilizestheforRoot()strategyasdetailedintheprecedingcode,you'refreetoimportthismodulebothinsidetheapplicationmoduleaswellasthelazilyloadedmodule:
[app/app.module.ts]
import{NgModule}from'@angular/core';
import{BrowserModule}from'@angular/platform-browser';
import{RouterModule,Routes}from'@angular/router';
import{RootComponent}from'./root.component';
import{LinkComponent}from'./link.component';
import{AuthModule}from'./auth.module';
constappRoutes:Routes=[
{
path:'article',
loadChildren:'./app/article.module#ArticleModule'
},
{
path:'**',
component:LinkComponent
}
];
@NgModule({
imports:[
BrowserModule,
RouterModule.forRoot(appRoutes),
AuthModule.forRoot()
],
declarations:[
LinkComponent,
RootComponent
],
bootstrap:[
RootComponent
]
})
exportclassAppModule{}
You'lladdittothelazilyloadedmoduletoo,butdon'tinvokeforRoot().Thismethodisreservedforonlytherootapplicationmodule:
[app/article.module.ts]
import{NgModule}from'@angular/core';
import{ArticleComponent}from'./article.component';
import{Routes,RouterModule}from'@angular/router';
import{AuthModule}from'./auth.module';
constarticleRoutes:Routes=[
{
path:'',
component:ArticleComponent
}
];
@NgModule({
imports:[
RouterModule.forChild(articleRoutes),
AuthModule
],
declarations:[
ArticleComponent
],
exports:[
ArticleComponent
]
})
exportclassArticleModule{}
Finally,injecttheserviceintoRootComponentandArticleComponentanduselogstatementstoseethatitdoesindeedreachboththecomponents:
[app/root.component.ts]
import{Component}from'@angular/core';
import{AuthService}from'./auth.service';
@Component({
selector:'root',
template:`
<h1>Rootcomponent</h1>
<router-outlet></router-outlet>
`
})
exportclassRootComponent{
constructor(privateauthService_:AuthService){
console.log(authService_);
}
}
[app/article.component.ts]
import{Component}from'@angular/core';
import{AuthService}from'./auth.service';
@Component({
selector:'article',
template:`
<h1>{{title}}</h1>
`
})
exportclassArticleComponent{
title:string=
'Baboon'sStockPicksCrushTop-PerformingHedgeFund';
constructor(privateauthService_:AuthService){
console.log(authService_);
}
}
Youshouldseeasingleserviceinstantiationandsuccessfulinjectionintoboththecomponents.
SeealsoConfiguringtheAngular2rendererservicetousewebworkersguidesyouthroughtheprocessofsettingupyourapplicationtorenderonawebworkerConfiguringapplicationstouseahead-of-timecompilationguidesyouthroughhowtocompileanapplicationduringthebuild