angular 2 cookbook - programmer booksdowngrading angular 2 components to angular 1 directives with...

Post on 25-May-2020

27 Views

Category:

Documents

0 Downloads

Preview:

Click to see full reader

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:customerreviews@packtpub.com.

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,simplye-mailfeedback@packtpub.com,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,youcancontactusatquestions@packtpub.com,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;thistemplateisdefinedinthe@Componentdecorator.Inthiscase,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

top related