Mastering Angular 2 Components -...
Transcript of Mastering Angular 2 Components -...
www.EBooksWorld.ir
MasteringAngular2Components
www.EBooksWorld.ir
TableofContents
MasteringAngular2ComponentsCreditsAbouttheAuthorAbouttheReviewerwww.PacktPub.com
eBooks,discountoffers,andmoreWhysubscribe?
PrefaceWhatthisbookcoversWhatyouneedforthisbookWhothisbookisforConventionsReaderfeedbackCustomersupport
DownloadingtheexamplecodeDownloadingthecolorimagesofthisbookErrataPiracyQuestions
1.Component-BasedUserInterfacesThinkingoforganismsComponents–Theorgansofuserinterfaces
EncapsulationComposabilityComponents,inventedbynature
MyUIframeworkwishlistTimefornewstandards
TemplateelementsShadowDOM
Angular'scomponentarchitectureEverythingisacomponent
YourfirstcomponentJavaScriptofthefuture
IspeakJavaScript,translate,please!ClassesModulesTemplatestringsECMAScriptorTypeScript?Decorators
ToolsNode.jsandNPM
www.EBooksWorld.ir
SystemJSandJSPMJSPMGettingstartedwithJSPM
Summary2.Ready,Set,Go!
ManagingtasksVision
StartingfromscratchBootstrappingRunningtheapplicationRecap
CreatingatasklistRecap
TherightlevelofencapsulationRecap
InputgeneratesoutputRecap
CustomUIelementsRecap
FilteringtasksSummary
3.ComposingwithComponentsData–FaketorealReactiveprogrammingwithobservabledatastructuresImmutabilityPurecomponents
PurifyingourtasklistRecap
CompositionusingcontentprojectionCreatingatabbedinterfacecomponent
RecapMixingprojectedwithgeneratedcontentSummary
4.NoComments,Please!Oneeditortorulethemall
CreatinganeditorcomponentRecap
BuildingacommentingsystemBuildingthecommentcomponentBuildingthecommentscomponentRecap
Summary5.Component-BasedRouting
AnintroductiontotheAngularrouter
www.EBooksWorld.ir
CompositionbyroutingRouterversustemplatecompositionUnderstandingtheroutetree
BacktotheroutesRoutabletabs
RefactoringnavigationSummary
6.KeepingUpwithActivitiesCreatingaserviceforloggingactivities
LoggingactivitiesLeveragingthepowerofSVG
StylingSVGBuildingSVGcomponents
BuildinganinteractiveactivityslidercomponentProjectionoftimeRenderingactivityindicatorsBringingittolifeRecap
BuildingtheactivitytimelineSummary
7.ComponentsforUserExperienceTagmanagement
TagdataentityGeneratingtagsCreatingatagsserviceRenderingtagsIntegratingthetaskserviceCompletionofthetagsservice
SupportingtaginputCreatingataginputmanagerCreatingatagsselectcomponentIntegratingtaginputwithintheeditorcomponentFinishingupourtaggingsystem
DraganddropImplementingthedraggabledirectiveImplementingadroptargetdirectiveIntegratingdraganddropintasklistcomponentRecapitulateondraganddrop
Toinfinityandbeyond!TheasterisksyntaxandtemplatesCreatinganinfinitescrolldirectiveDetectingchangewithinourtemplatedirectiveAddingandremovingembeddedviewsFinishingourinfinitescrolldirective
www.EBooksWorld.ir
Summary8.TimeWillTell
TaskdetailsEnablingtagsfortasksManagingefforts
ThetimedurationinputComponentstomanageeffortsThevisualeffortstimelineRecapitulatingoneffortsmanagement
SettingmilestonesCreatinganautocompletecomponent
Summary9.SpaceshipDashboard
IntroductiontoChartistProjectsdashboard
CreatingtheprojectsdashboardcomponentProjectsummarycomponent
CreatingyourfirstchartVisualizingopentasks
CreatinganopentaskschartCreatingachartlegendMakingtaskschartinteractive
Summary10.MakingThingsPluggable
PluginarchitecturePluggableUIcomponentsImplementingthepluginAPI
InstantiatingplugincomponentsFinalizingourpluginarchitecture
BuildinganAgilepluginAgiletaskinfocomponentAgiletaskdetailscomponentRecapitulatingonourfirstplugin
ManagingpluginsLoadingnewpluginsatruntime
Summary11.PuttingThingstotheTest
AnintroductiontoJasmineWritingourfirsttestSpyingoncomponentoutputsUtilitiestotestcomponents
InjectingintestsTestcomponentbuilder
Testingcomponentsinaction
www.EBooksWorld.ir
TestingcomponentinteractionTestingourpluginsystemSummary
A.TaskManagementApplicationSourceCodeDownloadPrerequisitesUsageTroubleshooting
CleaningIndexDBtoresetdataEnablingwebcomponentsinFirefox
Index
www.EBooksWorld.ir
MasteringAngular2Components
www.EBooksWorld.ir
MasteringAngular2ComponentsCopyright©2016PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:June2016
Productionreference:1280616
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
BirminghamB32PB,UK.
ISBN978-1-78588-464-1
www.packtpub.com
www.EBooksWorld.ir
CreditsAuthor
GionKunz
Reviewer
CarlosMorales
CommissioningEditor
SarahCrofton
AcquisitionEditors
AaronLazar
LarissaPinto
ContentDevelopmentEditor
SamanthaGonsalves
TechnicalEditor
MadhunikitaSunilChindarkar
CopyEditor
PriyankaRavi
ProjectCoordinator
SanchitaMandal
Proofreader
SafisEditing
Indexer
MonicaAjmeraMehta
Graphics
www.EBooksWorld.ir
DishaHaria
ProductionCoordinator
MelwynDsa
CoverWork
MelwynDsa
www.EBooksWorld.ir
AbouttheAuthorGionKunzhasyearsofexperiencewithwebtechnologiesandistotallyinlovewithwebstandards.Withover10yearsofexperienceofwritinginteractiveuserinterfacesusingJavaScript,heconstantlyevaluatesnewapproachesandframeworks.He'sworkedwithAngularJSforover3yearsnowandisoneoftheearliestadoptersofAngular2.GionspeaksaboutAngular2atconferences,andhehelpswiththeorganizationoftheZurichAngularMeetupgroupinSwitzerland.
Hecurrentlyworksforthestart-upcompanyoddEVENinZurich,wheretheyhelpcustomersbuildwebsitesandapplications.BesidesworkingforoddEVEN,GionisaheadinstructorattheSAEInstituteinZurichandlovestogethisstudentsenthusiasticabouttheWeb.
HeisalsothecreatoroftheresponsivechartinglibraryChartist,andhelovestocontributetotheopensourcecommunitywheneverhefindstime.
WhenGionisnotbusywithwebtechnologies,youcanprobablyfindhimathishomemusicstudio,outdoors,fishing,orspendingqualitytimewithhisgirlfriendandtheircutelittledog.
Iwouldliketothankmygirlfriend,Nathalie,forherongoingsupportandherpatiencewithallmyeffortsspentonthisbook.IreallyappreciateallthatyoudidforbothofusduringthistimeandthatyoucompensatedtheenergylossI'vebroughtintoourrelation.
www.EBooksWorld.ir
AbouttheReviewerCarlosMoralesstartedprogramminginBASICwhenhewas6yearsoldwithaSinclairZXSpectrum+thathestillowns.He'slovedthetechnologyeversince.Professionally,hehasworkedformorethan15yearsindifferentroles,alwaysaroundwebapplications.HefellinlovewithAngular,andhefoundedtheAngularMeetupgroupinZürich,whichisoneofthemostattendedMeetupsinthecity.
Soyingenieroinformáticograciasamidedicación,perosobretodoamispadres.
www.EBooksWorld.ir
www.PacktPub.com
www.EBooksWorld.ir
eBooks,discountoffers,andmoreDidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusat<[email protected]>formoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www2.packtpub.com/books/subscription/packtlib
DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt'sonlinedigitalbooklibrary.Here,youcansearch,access,andreadPackt'sentirelibraryofbooks.
www.EBooksWorld.ir
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
www.EBooksWorld.ir
PrefaceWebcomponentshavelongbeentoutedasthenextgreatleapforwardinwebdevelopment.WithAngular2,we'recloserthanever.Overthepastcoupleofyears,there'sbeenalotofbuzzaroundwebcomponentsforquitesometimeinthewebdevelopmentcommunity.Newcomponent-styledirectivesinAngular2willchangedevelopers'workflowsandtheirthinkingaboutsharedandreusableblocksofcustomHTMLintheshadowDOM.Oursisthefirstbookthatguidesdevelopersalongthispath.It'salsoapracticalwaytolearn,givingreadersthechancetobuildcomponentsoftheirown.WithMasteringAngular2Components,learnerswillgetaheadofthecurveinanewwaveofwebdevelopmentbytightlyfocusingonanareathat'sthekeytounlockingthepowersofAngulardevelopment.
MasteringAngular2Componentsteachesreaderstothinkcomponentially.Thisrichguidetothenewcomponent-centricwayofdoingthingsinAngularteachesreadershowtoinvent,build,andmanagesharedandreusablecomponentsfortheirwebprojects.ThisbookwillchangehowdevelopersthinkabouthowtoaccomplishthingsinAngular2,andthereaderwillbeworkingonusefulandfunexamplecomponentsthroughout.
www.EBooksWorld.ir
WhatthisbookcoversChapter1,Component-BasedUserInterfaces,looksatabitattheUIdevelopmenthistoryandprovidesabriefintroductiontocomponent-baseduserinterfacesingeneral.WewillseehowAngular2handlesthisconcept.
Chapter2,Ready,Set,Go!,willgetthereaderstartedontheirjourneytowardbuildinganAngular2component-basedapplication.Itcoversthebasicelementsofstructuringanapplicationwithcomponents.
Chapter3,ComposingwithComponents,iswherethereaderwillstarttostructuretheuserinterfaceintoitsbasicpieces.Thereaderwillthengoontocomposeanapplicationusingcomponentsbyorganizinganapplicationlayoutintocomponents,establishingthecompositionofcomponentsusingQueryList,andcreatingareusabletabcomponenttostructuretheapplicationinterface.
Chapter4,NoComments,Please!,iswherethereaderwilllearnhowtobuildacommentingsystemusingcomponents.Theywilllearntocreateacomponenttolistcommentsandalsotocreatenewcomments.
Chapter5,Component-BasedRouting,explainshowcomponentsreacttoroutingandwillenablethereadertoaddsimpleroutingtotheexistingcomponentsinthetaskmanagementapplication.Thereaderwillalsoworkontheloginprocessandbuildanunderstandingtoprotectcomponentsusingtherouter.
Chapter6,KeepingUpwithActivities,coversthecreationofcomponentsthatwillvisualizeactivitystreamsonprojectandtasklevel.
Chapter7,ComponentsforUserExperience,iswherethereaderwillcreatemanysmallreusablecomponentsthatwillhaveagreateffectontheoveralluserexperienceofthetaskmanagementapplication.Thisincludesin-placeeditingoftextfields,infinitescroll,popupnotification,anddraganddropsupport.
Chapter8,TimeWillTell,focusesoncreatingtime-trackingcomponentsthathelpsestimatetimeonaprojectandtasklevelbutalsoforuserstologthetimespentontasks.
Chapter9,SpaceshipDashboard,focusesoncreatingcomponentstovisualizesomedatainthetaskmanagementapplicationusingthethird-partylibraryChartist.
Chapter10,MakingThingsPluggable,iswherethereaderwilllearnaboutanapproachtomakecomponentspluggableusingasimplebutpowerfulpattern.WithaDIYpluginarchitectureforAngular2components,wemakeourtaskmanagementsystemextensible.
Chapter11,PuttingThingstotheTest,coverssomebasicapproachestotestingAngular2components.Wewillseetheoptionsformocking/overridingspecificpartsofacomponent
www.EBooksWorld.ir
fortesting.
Appendix,TaskManagementApplicationSourceCode,containsalltheinformationyou'llneedtodownloadandinstallthesourcecodethatcomeswiththisbook.You'llalsofindinstructionstouseandtroubleshootthecodeinthere.
www.EBooksWorld.ir
WhatyouneedforthisbookThisbookwillneedabasicinstallationofNode.jsonyourWindows,Mac,orLinuxmachine.
www.EBooksWorld.ir
WhothisbookisforThisbookisforAngulardeveloperswhoalreadyhaveagoodunderstandingofbasicfrontendwebtechnologies,suchasJavaScript,HTML,andCSS.Youwilllearnaboutthenewcomponent-basedarchitectureinAngular2andhowtouseittobuildmodernandcleanuserinterfaces.
www.EBooksWorld.ir
ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:"YouhaveaFisherclassandaDeveloperclass,bothofwhichholdspecificbehaviors."
Ablockofcodeissetasfollows:
classFruit{
constructor(name){this.name=name;}
}
constapple=newFruit('Apple');
Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:
<body>
<templateid="template">
<h1>Thisisatemplate!</h1>
</template>
</body>
Anycommand-lineinputoroutputiswrittenasfollows:
npminstalljspm--save-dev
jspminit
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:"Ifeverythinggoeswell,youwillhaveanopenwebbrowserthatshowsHelloWorld!."
Note
Warningsorimportantnotesappearinaboxlikethis.
Tip
Tipsandtricksappearlikethis.
www.EBooksWorld.ir
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook—whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.
Tosendusgeneralfeedback,simplye-mail<[email protected]>,andmentionthebook'stitleinthesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
www.EBooksWorld.ir
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
www.EBooksWorld.ir
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforthisbookfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Youcandownloadthecodefilesbyfollowingthesesteps:
1. Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.2. HoverthemousepointerontheSUPPORT tabatthetop.3. ClickonCodeDownloads&Errata.4. EnterthenameofthebookintheSearchbox.5. Selectthebookforwhichyou'relookingtodownloadthecodefiles.6. Choosefromthedrop-downmenuwhereyoupurchasedthisbookfrom.7. ClickonCodeDownload.
YoucanalsodownloadthecodefilesbyclickingontheCodeFilesbuttononthebook'swebpageatthePacktPublishingwebsite.Thispagecanbeaccessedbyenteringthebook'snameintheSearchbox.PleasenotethatyouneedtobeloggedintoyourPacktaccount.
Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux
ThecodebundleforthebookisalsohostedonGitHubathttps://github.com/PacktPublishing/Mastering-Angular-2-Components.Wealsohaveothercodebundlesfromourrichcatalogofbooksandvideosavailableathttps://github.com/PacktPublishing/.Checkthemout!
www.EBooksWorld.ir
DownloadingthecolorimagesofthisbookWealsoprovideyouwithaPDFfilethathascolorimagesofthescreenshots/diagramsusedinthisbook.Thecolorimageswillhelpyoubetterunderstandthechangesintheoutput.Youcandownloadthisfilefromhttps://www.packtpub.com/sites/default/files/downloads/MasteringAngular2Components_ColorImages.pdf
www.EBooksWorld.ir
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.
www.EBooksWorld.ir
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusat<[email protected]>withalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
www.EBooksWorld.ir
QuestionsIfyouhaveaproblemwithanyaspectofthisbook,youcancontactusat<[email protected]>,andwewilldoourbesttoaddresstheproblem.
www.EBooksWorld.ir
Chapter1.Component-BasedUserInterfacesAlthoughwe'llcoveralotofAngular-relatedtopicsinthisbook,thefocuswillbemainlyoncreatingcomponent-baseduserinterfaces.It'sonethingtounderstandaframework,suchasAngular2,butit'sawholedifferentthingtoestablishaneffectiveworkflowusingacomponent-basedarchitecture.Inthisbook,I'lltrytoexplainthecoreconceptsbehindAngular2componentsandhowwecanleveragethisarchitecturetocreatemodern,efficient,andmaintainableuserinterfaces.
BesideslearningallthenecessaryconceptsbehindAngular2,wewilltogethercreateatask-managementapplicationfromscratch.ThiswillallowustoexploredifferentapproachestosolvecommonUIproblemsusingthecomponentsystemthatisprovidedbyAngular2.
Apreviewofthetaskmanagementapplicationthatwearegoingtobuild
Inthischapter,wewilltakealookathowcomponent-baseduserinterfaceshelpusbuildgreaterapplications.Overthecourseofthisbook,wewillbuildanAngular2applicationtogether,wherewewillusethecomponent-basedapproachtoitsfullpotential.Thischapterwillalsointroduceyoutothetechnologiesthatareusedinthisbook.Thetopicsthatwewillcoverinthischapterareasfollows:
Anintroductiontocomponent-baseduserinterfacesEncapsulationandcompositionusingcomponent-baseduserinterfacesEvolutionofUIframeworksThestandardandWebcomponents
www.EBooksWorld.ir
AnintroductiontotheAngular2componentsystemWritingyourfirstAngular2componentAnoverviewandhistoryofECMAScriptandTypeScriptECMAScript7decoratorsasmetaannotationsAnintroductiontoNode.js-basedtoolingusingJSPMandSystemJS
www.EBooksWorld.ir
ThinkingoforganismsToday'suserinterfacesdonotconsistofjustabunchofformelementsthatarecobbledtogetherontoascreen.Modernusersexperiencedesignandinnovativevisualpresentationsofinteractivecontentchallengestechnologymorethanever.
Sadly,wealmostalwaystendtothinkinpageswhenwefleshoutconceptsforwebapplications,suchasthepageswithinaprintedbook.Well,thisisprobablythemostefficientwaytoconveyinformationforthiskindofcontentandmedium.Youcanskimthroughthepagesonebyonewithoutanyrealphysicaleffort,readparagraphbyparagraph,andjustscanthroughthechaptersthatyoudon'tfindinteresting.
Theproblemwiththinkinginpagestoomuchisthatthisconcept,whichisborrowedfrombooks,doesnotreallytranslatewelltohowthingsworkintherealworld.Theworldiscreatedfromorganismsthatformasystemoforganismstogether.Thissystemitselfformsanorganismagain,justonahigherlevel.
Takeourbodiesasanexample.Wemostlyconsistofindependentorgansthatinteractwitheachotherusingelectricalandchemicalsignals.Organsthemselvesconsistofproteinsthatontheirownworklikeamachinetoformasystem.Downtothemolecules,atoms,protons,andquarks,wecan'treallytellwhereonestartsandwhereitends.Whatwecantellforsureisthatit'sallaboutsystemsoforganismswithinterdependencies,anditisnotaboutpages.
Iliketoviewuserinterfacesassystemsoforganisms.Whetherif,where,andhowtheyaredistributedtopagesissubordinatewhiledesigningthem.Also,theyshouldworkindependently,andtheyshouldinteractwitheachotheronaninterdependentlevel.
www.EBooksWorld.ir
Components–Theorgansofuserinterfaces "We'renotdesigningpages,we'redesigningsystemsofcomponents."
--StephenHay
ThisquotebyStephenHayfromBDConfinOrlando2012bringsittothepoint.Interfacedesignisreallynotaboutpages.Tocreateefficientuserinterfacesfornotonlytheusersbutalsothedeveloperswhomaintainthem,weneedtothinkinsystemsofcomponents.Componentsareindependent,buttheycaninteractwitheachotherandcreatelargercomponentswhentheyarearrangedtogether.Weneedtolookatuserinterfacesholisticallyandusingcomponentsenablesustodothis.
Inthefollowingtopics,we'regoingtoexploreafewfundamentalaspectsofcomponents.Someofthesearealreadyknownfromotherconcepts,suchasobject-orientedprogramming(OOP),buttheyappearinaslightlydifferentlightwhenthinkingaboutcomponents.
www.EBooksWorld.ir
EncapsulationEncapsulationisaveryimportantfactorwhenthinkingaboutmaintenanceinasystem.HavingaclassicalOOPbackground,I'velearnedthatencapsulationmeansbundlinglogicanddatatogetherintoanisolatedcontainer.Thisway,wecanoperateonthecontainerfromtheoutsideandtreatitlikeaclosedsystem.
Therearemanypositiveaspectsofthisapproachwhenitcomestomaintainabilityandaccessibility.Dealingwithclosedsystemsisimportantfortheorganizationofourcode.However,thisisevenmoreimportantlybecausewecanorganizeourselveswhileworkingwithcode.
Ihaveaprettybadmemory,andit'sveryimportantformetofindtherightfocuslevelwhenworkingoncode.Immediatememoryresearchtoldusthatthehumanbraincanrememberaboutsevenitemsatonceonanaverage.Therefore,it'scrucialforustowritecodeinsuchawaythatitallowsustofocusonfewerandsmallerpiecesatonce.
Aclearencapsulationhelpsusinorganizingourcode.Wecanmaybeforgetalltheinternalsoftheclosedsystemandaboutthekindoflogicanddatathatwe'veputintoit.Wecanfocusonlyonitssurface,whichallowsustoworkonahigher-abstractionlevel.Similartothepreviousfigure,withoutusingahierarchyofencapsulatedcomponents,we'dhaveallourcodecobbledtogetheronthesamelevel.
Encapsulationencouragesustoisolatesmallandconcisecomponentsandbuildasystemofcomponents.Duringdevelopment,wecanfocusontheinternalsofonecomponentandonlydealwiththeinterfaceofothercomponents.
Sometimes,weforgetthatalltheorganizationofthecodingweactuallyperformisforourselvesandnotforthecomputerthatrunsthiscode.Ifthiswasforthecomputer,thenwe
www.EBooksWorld.ir
wouldprobablyallstartwritinginmachinelanguageagain.Astrongencapsulationhelpsusaccessspecificcodeeasily,focusononelayerofthecode,andtrusttheunderlyingimplementationswithincapsules.
ThefollowingJavaScriptexampleshowsyouhowtouseencapsulationtowritemaintainableapplications.Let'sassumethatweareinaT-shirtfactory,andweneedsomecodetoproduceT-shirtswithabackgroundandforegroundcolor.ThisexampleusessomenewlanguagefeaturesofECMAScript6.Ifyou'renotfamiliarwiththelanguagefeaturesofECMAScript6,don'tworrytoomuchatthispoint.Wewilllearnabouttheselaterinthischapter:
//Thisclassimplementsdataandlogictorepresentacolour
//whichestablishescleanencapsulation.
classColour{
constructor(red,green,blue){
Object.assign(this,{red,green,blue});
}
//Usingthisfunctionwecanconverttheinternalcolourvalues
//toahexcolourstringlike#ff0000(red).
getHex(){
return'#'+Colour.getHexValue(this.red)+Colour.getHexValue(this.green)+
Colour.getHexValue(this.blue);
}
//StaticfunctiononColourclasstoconvertanumberfrom
//0to255toahexadecimalrepresentation00toff
staticgetHexValue(number){
consthex=number.toString(16);
returnhex.length===2?hex:'0'+hex;
}
}
//OurTShirtclassexpectstwocolourstobepassedduring
//constructionthatwillbeusedtorendersomeHTML
classTShirt{
constructor(backgroundColour,foregroundColour){
Object.assign(this,{backgroundColour,foregroundColour});
}
//Functionthatreturnssomemarkupwhichrepresentsour
//T-Shirts
getHtml(){
return`
<t-shirtstyle="background-color:${this.backgroundColour.getHex()}">
<t-shirt-textstyle="color:${this.foregroundColour.getHex()}">
AwesomeShirt!
</t-shirt-text>
</t-shirt>
`;
}
}
//Instantiateabluecolour
constblue=newColour(0,0,255);
www.EBooksWorld.ir
//Instantiatearedcolour
constred=newColour(255,0,0);
//Createanewshirtusingtheabovecolours
constawesomeShirt=newTShirt(blue,red);
//Addingthegeneratedmarkupofourshirttoourdocument
document.body.innerHTML=awesomeShirt.getHtml();
Usingacleanencapsulation,wecannowworkwiththeabstractionofcolorinourT-shirt.Wedon'tneedtoworryabouthowtocalculatethehexadecimalrepresentationofcolorsattheT-shirtlevelbecausethisisalreadydonebytheColourclass.Thismakesyourapplicationmaintainableandkeepsitveryopenforchange.
IreallyrecommendthatyoureadabouttheSOLIDprinciplesifyouhaven'tdonesoalready.Asthenamealreadysuggests,thisassemblyofprinciplesisasolidpowertoolthatcanchangethewayyouorganizecodetremendously.YoucanlearnmoreabouttheSOLIDprinciplesinthebook,AgilePrinciples,Patterns,andPractices,byRobertC.Martin.
www.EBooksWorld.ir
ComposabilityCompositionisaspecialkindofreusability.Youdon'textendanexistingcomponent,butyoucreateanewlargercomponentbycomposingmanysmallercomponentstogetherintoasystemofcomponents.
InOOPlanguages,compositionisoftenusedtogetaroundthemultipleinheritanceissuesthatmostOOPlanguageshave.Subclasspolymorphismisalwaysgreatuntilyoureachthepointwhereyourdesigndoesnotmatchthelatestrequirementsinyourproject.Let'slookatasimpleexamplethatillustratesthisproblem.
YouhaveaFisherclassandaDeveloperclass,bothofwhichholdspecificbehaviors.Now,you'dwanttocreateaFishingDeveloperclassthatinheritsbothfromFisherandDeveloper.Unlessyou'reusingalanguagethatsupportsmultipleinheritance(suchasC++doestoacertainextent),youwillnotbeabletoreusethisfunctionalityusinginheritance.Thereisnowaytotellthelanguagethatyournewclassshouldinheritfrombothsuperclasses.Usingcomposition,youcaneasilysolvethisproblem.Insteadofusinginheritance,you'recomposinganewFishingDeveloperclassthatdelegatesallbehaviortoaninternalDeveloperandFisherinstance:
classDeveloper{
code(){
console.log(`${this.name}writessomecode!`);
}
}
classFisher{
fish(){
console.log(`${this.name}catchesabigfish!`);
}
}
classFishingDeveloper{
constructor(name){
this.name=name;
this.developerStuff=newDeveloper();
this.fisherStuff=newFisher();
}
code(){
this.developerStuff.code.bind(this)();
}
fish(){
this.fisherStuff.fish.bind(this)();
}
}
varbob=newFishingDeveloper('Bob');
bob.code();
bob.fish();
www.EBooksWorld.ir
Experiencehastaughtusthatcompositionisprobablythemostefficientwaytoreusecode.Incontrasttoinheritance,decoration,andotherapproachestogainreusability,compositionisprobablytheleastintrusiveandthemostflexible.
Recentversionsofsomelanguagesalsosupportapatterncalledtraits,thatis,mixins.Traitsallowyoutoreusecertainfunctionalityandattributesfromotherclassesinawaythatissimilartomultipleinheritance.
Ifwethinkabouttheconceptofcomposition,it'snothingmorethandesigningorganisms.WehavethetwoDeveloperandFisherorganisms,andweunifytheirbehaviorsintoasingleFishingDeveloperorganism.
www.EBooksWorld.ir
Components,inventedbynatureComponents,embracingencapsulation,andcompositionareaneffectivewaytobuildmaintainableapplications.Composedfromcomponents,applicationsareveryresistanttothenegativeimplicationsofchange,andchangeisanecessarythingthatwillhappentoeveryapplication.It'sonlyamatteroftimeuntilyourdesignwillbechallengedbytheeffectsofchange;therefore,it'sveryimportanttowritecodethatcanhandlechangeassmoothlyaspossible.
Natureisthebestteacher.Almostalltheachievementsintechnologicaldevelopmentshavetheirorigininobservationsofhownaturesolvesproblems.Ifwelookatevolution,it'sanongoingredesignofmatterbyadaptingtoouterforcesandconstraints.Naturesolvesthisbyconstantchangeusingmutationandnaturalselection.
Ifweprojecttheconceptofevolutionontodevelopinganapplication,wecansaythatnaturedoesactuallyrefactoritscodeineverysinglemoment.Thisisactuallythedreamofeveryproductmanager—anapplicationthatcanundergoconstantchangebutdoesnotloseanyofitsefficiency.
Ibelievethattherearetwokeyconceptsthatplayamajorroleinnaturethatallowsittoapplyconstantchangeinitsdesignwithoutlosingmuchefficiency.Thisusesencapsulationandcomposition.Comingbacktotheexampleofourbodies,wecanactuallytellthatourorgansuseaveryclearencapsulation.Theyusemembranestocreateisolation,veinstotransportnutrition,andsynapsestosendmessages.Also,theyhaveinterdependencies,andtheycommunicatewithelectricalandchemicalmessages.Mostobviously,theyformlargersystems,whichisthecoreconceptofcomposition.
Ofcourse,therearemanyotherfactors,andI'mnotaprofessorinbiology.However,Ithinkit'safascinatingthingtoseethatwehavelearnedtoorganizeourcodeverysimilarlytohownatureorganizesmatter.
TheideaofcreatingreusableUIcomponentsisquiteold,anditwasimplementedinvariouslanguagesandframeworks.OneoftheearliestsystemsthatusedUIcomponentswasprobablytheXeroxAltosystembackin1970s.ItusedreusableUIcomponentsthatalloweddeveloperstocreateanapplicationbycomposingthemonascreenwhereuserscouldinteractwiththem.
www.EBooksWorld.ir
TheuserinterfaceoffilemanagerontheXeroxAltosystemfromthe1970s.
EarlyfrontendUIframeworks,suchasDHTMLX,ExtJS,orjQueryUIimplementedcomponentsinamorelimitedfashionthatdidn'tprovidegreatflexibilityorextensibility.Mostoftheseframeworksjustprovidedwidgetlibraries.TheproblemwithUIwidgetsisthat
www.EBooksWorld.ir
theymostlydon'tembracethepatternofcompositionenough.Youcanarrangewidgetsonapageandtheyprovideencapsulation,butwithmosttoolkits,youcan'tcreatelargercomponentsbynestingtheminsideeachother.Sometoolkitssolvethisbyprovidingaspecialkindofwidgetwhichwasmostlycalledacontainer.However,thisisnotthesameasafull-fledgedcomponenttreethatallowsyoutocreatesystemswithinsystems.Containerswereactuallymeanttoprovideavisuallayoutcontainerratherthanacompositecontainertoformalargersystem.
Usuallywhenworkingwithwidgetsonapageofourapplication,we'dhavealargecontrollerthatcontrolsallthesewidgets,userinput,andstates.However,weareleftwithtwolevelsofcomposition,andthere'snowaythatwecanstructureourcodemoregranularly.Thereisthepageandtherearethewidgets.HavingabunchofUIwidgetsissimplynotenough,andwearealmostbacktothestatewherewecreatepagesplasteredwithformelements.
I'vebeenauserofJavaServerFacesforyears,andbesidesallitsproblems,theconceptofhavingreusablecustomelementswasgroundbreaking.UsingXHTML,onecouldwriteso-calledcompositecomponentsthatconsistedofothercompositecomponentsornativeHTMLelements.Adevelopercouldgainafantasticlevelofreusabilityusingcomposition.Thebigissueinmyviewwiththistechnologywasthatitdidnotaddresstheconcernsinthefrontendenoughtobecomereallyusableforcomplexuserinteractions.Infact,aframeworklikethisshouldlivecompletelywithinthefrontend.
MyUIframeworkwishlist
UsuallywhenUIframeworksgetcompared,theygetmeasuredagainsteachotherbasedonmetrics,suchaswidgetcount,themingcapabilities,andasynchronousdataretrievalfeatures.Everyframeworkhasitsstrengthsandweaknesses,butleavingalltheextrafeaturesasideandreducingittothecoreconcernsofaUIframework,IonlyhaveafewmetricsleftthatI'dliketobeassessed.Thesemetricsare,ofcourse,nottheonlyonesthatareimportantintoday'sUIdevelopment,buttheyalsoarethemainfactorstowardbuildingacleanarchitecturethatsupportstheprincipleofchange:
IcancreateencapsulatedcomponentswithclearinterfacesIcancreatelargercomponentsbycompositionIcanmakecomponentsinteractwitheachotherwithintheirhierarchy
Ifyou'relookingforaframeworkwhichenablesyoutotakefulladvantageofcomponent-basedUIdevelopment,youshouldlookforthesethreekeymeasures.
Firstofall,Ithinkit'sveryimportanttounderstandthemainpurposeofthewebandhowitevolved.Ifwethinkofthewebinitsearlydaysinthe1990s,itwasprobablyonlyabouthypertext.Therewereverybasicsemanticsthatcouldbeusedtostructureinformationanddisplaythemtoauser.HTMLwascreatedtoholdstructureandinformation.TheneedforcustomvisualpresentationofinformationledtothedevelopmentofCSSrightafterHTMLstartedbeingwidelyused.
www.EBooksWorld.ir
Itwasinthemid1990swhenBrendanEichinventedJavaScript,anditwasfirstimplementedinNetscapeNavigator.Byprovidingawaytoimplementbehaviorandstate,JavaScriptwasthelastmissingpieceforafullwebcustomization:
Technology Concern
HTML Structureandinformation
CSS Presentation
JavaScript Behaviorandstate
Wehavelearnedtokeeptheseconcernsasseparateaspossibleinordertomaintainacleanarchitecture.Althoughtherearedifferentopinionsonthisandsomerecenttechnologiesalsomoveawayfromthisprinciple,Ibelievethatacleanseparationoftheseconcernsisveryimportanttocreateamaintainableapplication.
Leavingthisviewaside,thestandarddefinitionofencapsulationfromOOPisjustconcernedaboutcouplingandisolationoflogicanddata.Thisprobablyapplieswelltoclassicsoftwarecomponents.However,assoonasweconsiderauserinterfaceaspartofanarchitecture,thereisanewdimensionthatisadded.
ClassicalMVCframeworksareviewcentric,anddevelopersorganizetheircodebasedonpages.You'llprobablygoaheadandcreateanewviewthatrepresentsapage.Ofcourse,yourviewneedsacontrollerandmodel,soyou'llalsocreatethem.Theproblemwithorganizationbypagesisthatthere'slittletonogainofreusability.Onceyou'vecreatedapageandyou'dliketoreuseonlysomepartsofthepage,youwillneedawaytoencapsulateonlyaspecificpartofthismodel—theviewandthecontroller.
UIcomponentssolvethisproblemnicely.IliketoseethemasamodularapproachtoMVC.AlthoughtheystillembracetheMVCpattern,theyalsoestablishencapsulationandcomposability.Thiswayaviewisacomponentitself,butitalsoconsistsofcomponents.Bycomposingviewsofcomponents,onecangainamaximumamountofreusability:
www.EBooksWorld.ir
UIcomponentsembraceMVC,buttheyalsosupportencapsulationandcompositiononamuchlowerlevel
Technically,therearesomechallengeswhenimplementingcomponentswithwebtechnologies.JavaScriptwasalwaysflexibleenoughtoimplementdifferentpatternsandparadigms.Workingwithencapsulationandcompositionisn'tanissueatall,andthecontrollingpartandthemodelofcomponentscaneasilybeimplemented.Approaches,suchastherevealingmodulepattern,namespaces,prototypes,ortherecentECMAScript6modules,provideallthetoolsthatareneededfromtheJavaScriptside.
However,fortheviewpartofourcomponents,wefacesomelimitations.AlthoughHTMLsupportsgreatflexibilityintermsofcomposabilitybecausetheDOMtreeisnothingelsethanabigcomposition,wehavenowaytoreusethesecompositions.Wecanonlycreateonelargecomposition,whichisthepageitself.HTMLbeingonlythefinalviewthatwasdeliveredfromtheserver,thiswasneverreallyarealconcern.Today'sapplicationsaremuchmoredemanding,andweneedtohaveafully-encapsulatedcomponentrunninginthebrowser,whichalsoconsistsofapartialview.
WefacethesameproblemwithCSS.ThereisnorealmodularizationandencapsulationwhilewritingCSS,andweneedtousenamespacesandprefixesinordertosegregateourCSSstyles.Still,thewholecascadingnatureofCSScaneasilydestroyallencapsulationthatwetrytobringinplaceusingCSS-structuringpatterns.
www.EBooksWorld.ir
TimefornewstandardsWebstandardshavebeenevolvingimmenselyinthelastcoupleofyears.Therearesomanynewstandards,andthebrowserbecamesuchabigmultimediaframework,thatit'shardforotherplatformstocompetewiththis.
I'devengoasfarastosaythatwebtechnologywillactuallyreplaceotherframeworksinthefuture,anditprobablywillberenamedtomultimediatechnologyorsomethingsimilar.There'snoreasonwhyweneedtousedifferentnativeframeworkstocreateuserinterfacesandpresentations.Webtechnologiesembedsomanyfeaturesthatit'shardtofindareasonnottousethemforanykindofapplication.JustlookattheFirefoxOSortheChromeOS,whicharedesignedtorunwithwebtechnologies.Ithinkit'sonlyamatteroftimeuntilmoreoperatingsystemsandembeddeddevicesmakeuseofwebtechnologiestoimplementtheirsoftware.ThisiswhyIbelievethatatsomepointitwillbequestionablewhetherthetermwebtechnologiesisstillappropriateorwhetherweshouldreplaceitwithamoregeneralterm.
Althoughweusuallyjustseenewfeaturesappearinbrowsers,thereisaveryopenandlong-windedstandardizationprocessbehindthem.It'sveryimportanttostandardizefeatures,butthistakesalotoftime,especiallywhenpeopledisagreeaboutdifferentapproachestosolvingproblems.
Comingbacktotheconceptofcomponents,thisissomethingwherewereallyneedsupportfromwebstandardstobreakthecurrentlimitations.Fortunately,theW3Cthoughtthesame,andagroupofdevelopersstartedtoworkonspecificationsunderthehoodofanumbrellaspecificationcalledwebcomponents.
ThefollowingtopicswillgiveyouabriefoverviewovertwospecificationsthatalsoplayaroleinAngular2components.OneofAngular2'scorestrengthsisthatitactsmorelikeasupersetofwebstandardsratherthanbeingacompleteisolatedframework.
Templateelements
TemplateelementsallowyoutodefineregionswithinyourHTML,whichwillnotberenderedbythebrowser.YoucantheninstantiatethesedocumentfragmentswithJavaScriptandthenplacetheresultingDOMwithinyourdocument.
Whilethebrowserisactuallyparsingthetemplatecontent,itonlydoessoinordertovalidatetheHTML.Anyimmediateactionsthattheparserwouldusuallyexecutewillnotbetaken.Withinthecontentoftemplateelements,imageswillnotbeloadedandscriptswon'tbeexecuted.Onlyafteratemplateisinstantiated,theparserwilltakethenecessaryactions,asfollows:
<body>
<templateid="template">
<h1>Thisisatemplate!</h1>
</template>
www.EBooksWorld.ir
</body>
ThissimpleHTMLexampleofatemplateelementwon'tdisplaytheheadingonyourpage.Astheheadingisinsideatemplateelement,wefirstneedtoinstantiatethetemplateandaddtheresultingDOMintoourdocument:
vartemplate=document.querySelector('#template');
varinstance=document.importNode(template.content,true);
document.body.appendChild(instance);
UsingthesethreelinesofJavaScript,wecaninstantiatethetemplateandappenditintoourdocument.
ShadowDOM
ThispartofthewebcomponentsspecificationwasthemissingpiecetocreateproperDOMencapsulationandcomposition.WithshadowDOM,wecancreateisolatedpartsoftheDOMthatareprotectedagainstregularDOMoperationsfromtheoutside.Also,CSSwillnotreachintoshadowDOMautomatically,andwecancreatelocalCSSwithinourcomponent.
Tip
IfyouaddastyletaginsideshadowDOM,thestylesarescopedtotherootwithintheshadowDOM,andtheywillnotleakoutside.ThisenablesaverystrongencapsulationforCSS.
ContentinsertionpointsmakeiteasytocontrolcontentfromtheoutsideofashadowDOMcomponent,anditprovidessomekindofaninterfacetopassincontent.
Atthetimeofwritingthisbook,shadowDOMissupportedbymostbrowsersalthoughitstillneedstobeenabledinFirefox.
www.EBooksWorld.ir
Angular'scomponentarchitectureForme,theconceptofdirectivesfromthefirstversionofAngularchangedthegameinfrontendUIframeworks.ThiswasthefirsttimethatIfeltthattherewasasimpleyetpowerfulconceptthatallowedthecreationofreusableUIcomponents.DirectivescouldcommunicatewithDOMeventsormessagingservices.Theyallowedyoutofollowtheprincipleofcomposition,andyoucouldnestdirectivesandcreatelargerdirectivesthatsolelyconsistedofsmallerdirectivesarrangedtogether.Actually,directiveswereaveryniceimplementationofcomponentsforthebrowser.
Inthissection,we'lllookintothecomponent-basedarchitectureofAngular2andhowthethingswe'velearnedaboutcomponentswillfitintoAngular.
www.EBooksWorld.ir
EverythingisacomponentAsanearlyadopterofAngular2andwhiletalkingtootherpeopleaboutit,Igotfrequentlyaskedwhatthebiggestdifferenceistothefirstversion.Myanswertothisquestionwasalwaysthesame.Everythingisacomponent.
Forme,thisparadigmshiftwasthemostrelevantchangethatbothsimplifiedandenrichedtheframework.Ofcourse,therearealotofotherchangeswithAngular2.However,asanadvocateofcomponent-baseduserinterfaces,I'vefoundthatthischangeisthemostinterestingone.Ofcourse,thischangealsocamewithalotofarchitecturalchanges.
Angular2supportstheideaoflookingattheuserinterfaceholisticallyandsupportingcompositionwithcomponents.However,thebiggestdifferencetoitsfirstversionisthatnowyourpagesarenolongerglobalviews,buttheyaresimplycomponentsthatareassembledfromothercomponents.Ifyou'vebeenfollowingthischapter,you'llnoticethatthisisexactlywhataholisticapproachtouserinterfacesdemands.Nomorepagesbutsystemsofcomponents.
Tip
Angular2stillusestheconceptofdirectives,althoughdirectivesarenowreallywhatthenamesuggests.Theyareordersforthebrowsertoattachagivenbehaviortoanelement.
www.EBooksWorld.ir
Componentsareaspecialkindofdirectivesthatcomewithaview.
www.EBooksWorld.ir
YourfirstcomponentKeepingupthetradition,beforewestartbuildingarealapplicationtogether,weshouldwriteourfirsthelloworldcomponentwithAngular:
//Decoratorsallowustoseparatedeclarativelogicfromour
//componentimplementationlogic.
@Component({
selector:'hello-world',
template:'<div>Hello{{name}}</div>'
})
classHelloWorld{
constructor(){
this.name='World';
}
}
Thisisalreadyafully-workingAngular2application.WeusedECMAScript6classestocreatethenecessaryencapsulationrequiredforacomponent.Youcanalsoseeameta-annotationthatisusedtodeclarativelyconfigureourcomponent.Thisstatement,whichlookslikeafunctioncallthatisprefixedwithanatsymbolactuallycomesfromtheECMAScript7decoratorproposal.
Note
ECMAScript7decoratorsarestillveryexperimentalatthetimeofwritingthisbook.Forthecodeinthisbook,weactuallyusetheTypeScripttranspilerversion1.5thatalreadyimplementsdecoratorswithaslighttwisttotheoriginalspecification.TypeScript1.5isalsousedbytheAngular2teamtodevelopthecoreofAngular.
It'simportanttounderstandthatanelementcanonlybeboundtoonesinglecomponent.Asacomponentalwayscomeswithaview,thereisnowaythatwecanbindmorethanonecomponenttoanelement.Ontheotherhand,anelementcanbeboundtomanydirectives,asdirectivesdon'tcomewithaviewbuttheyonlyattachbehavior.
IntheComponentdecorator,weneedtoconfigureeverythingthatisrelevanttodescribeourcomponentforAngular.This,ofcourse,alsoincludesourtemplatefortheview.Intheprecedingexample,wearespecifyingourtemplatedirectlywithinJavaScriptasastring.WecanalsousethetemplateUrlpropertytospecifyaURLwherethetemplateshouldbeloadedfrom.
Now,let'senhanceourexamplealittlebitsothatwecanseehowwecancomposeourapplicationfromsmallercomponents:
//Usingdecoratorswecandeclarativelydefineourcomponentused
//towriteboldtext
@Component({
selector:'shout-out',
www.EBooksWorld.ir
template:'<strong>{{words}}</strong>'
})
classShoutOut{
@Input()words;
}
//Thiscomponentwillbeourmainapplicationcomponentthat
//makesuseoftheaboveshout-outcomponent(composition)
@Component({
selector:'hello-world'
template:'<shout-outwords="Hello,{{name}}!"></shout-out>',
directives:[ShoutOut]
})
classHelloWorld{
constructor(){
this.name='World';
}
}
Youcanseethatwehavenowcreatedasmallcomponentthatallowsustoshoutoutwordsaswelike.InourHelloWorldapplication,wemakeuseofthiscomponenttoshoutoutHello,World!
Tip
Everydirectiveorcomponentthatisusedinsideacomponentsviewtemplateneedstobeexplicitlydeclaredinthedirectivespropertyoftheviewannotation.Otherwise,thecompilerwillnotrecognizethedirectivewhenitencounterstheelementinthetemplate.
Overthecourseofthisbookandwhilewritingourtaskmanagementapplication,wewilllearnalotmoreabouttheconfigurationandimplementationofcomponents.However,beforewestartwiththisinthesecondchapter,weshouldtakealookatsometoolsandlanguagefeaturesthatwe'lluseduringthisbook.
www.EBooksWorld.ir
JavaScriptofthefutureItwasnotsolongagothatsomebodyaskedmewhetherweshouldreallyusethebindfunctionofECMAScript5.1,asthenwe'dprobablyrunintobrowsercompatibilityissues.Thewebmovesveryfast,andweneedtokeepupthepace.Wecan'twritecodethatdoesnotusethelatestfeaturesevenifthiswouldcauseissuesinoldbrowsers.
ThefantasticpeoplefromTC39,thetechnicalcommitteethatisresponsibleforwritingtheECMAScriptspecification,havedoneagreatjobprogressivelyenhancingtheJavaScriptlanguage.This,andthefactthatJavaScriptissoflexible,allowsustouseso-calledpolyfillsandshimstomakeourcoderuninolderbrowsers.
ECMAScript6(alsoreferredtoasECMAScript2015)waspublishedinJune2015,exactlyfouryearsafteritspredecessor.ThereisamassiveamountofnewAPIadditionsaswellasawholebunchofnewlanguagefeatures.Thelanguagefeaturesaresyntacticsugar,andECMAScript6canbetranspiledtoitspreviousversionwhereitrunsperfectlyinolderbrowsers.Atthetimeofwritingthisbook,noneofthecurrentbrowserversionshavefullyimplementedECMAScript6,butthere'sabsolutelynoreasonnottouseitforproductionapplications.
Tip
Syntacticsugarisadesignapproachwhereweevolveaprogramminglanguagewhilenotbreakingbackwardscompatibility.Thisallowslanguagedesignerstocomeupwithnewsyntax,whichenrichesdeveloperexperiencebutdoesnotbreaktheweb.Everynewfeatureneedstobetranslatabletotheoldsyntax.Thisway,so-calledtranspilerscanbeusedtoconvertcodetoolderversions.
IspeakJavaScript,translate,please!
Whilecompilerscompilefromahigher-levellanguagetoalower-levellanguage,atranspilerortranscompileractsmorelikeaconverter.Itisasource-to-sourcecompilerthattranslatescodetoruninadifferentinterpreter.
Recently,there'sarealbattleamongnewlanguagesthataretranspiledtoJavaScriptandcanruninthebrowser.IusedGoogleDartforquitesometime,andImustadmit,Ireallylovedthelanguagefeatures.Theproblemwithnonstandardizedlanguagesisthattheydependheavilyoncommunityadoptionandthehype.Also,it'salmostcertainthattheywillneverrunnativelywithinthebrowser.ThisisalsothereasonwhyIpreferstandardJavaScript,andtheJavaScriptofthefutureusestranspilersthatallowmetodothis.
Somepeoplearguethattranspilersintroducecodethatdoesnotrunveryperformantand,therefore,recommendthatyoudonotuseECMAScript6andtranspilersatall.Idon'tagreewiththisbecauseofmanyreasons.Usually,thisisaboutperformanceinmicroorevennanosecondareaswherethisoftenreallydoesnotmatterformostapplications.
www.EBooksWorld.ir
Idon'tsayperformancedoesn'tmatter,butperformanceneedstoalwaysbediscussedwithinacontext.Ifyou'retryingtooptimizealoopwithinyourapplicationbyreducingprocessingtimefrom10microsecondstofivemicrosecondswhereyou'dneveriterateovermorethan100items,thenyou'reprobablyspendingyourtimeonthewrongthings.
Also,averyimportantfactisthattranspiledcodeisdesignedbypeoplewhounderstandmicroperformanceoptimizationmuchbetterthanIdo,andI'msuretheircoderunsfasterthanmine.Ontopofthis,atranspilerisprobablyalsotherightplacewhereyou'dwanttodoperformanceoptimizationbecausethiscodeisautomaticallygeneratedandyoudon'tlosemaintainabilityofyourcodethroughperformancequirks.
I'dliketoquoteDonaldKnuthhereandsaythatprematureoptimizationistherootofallevil.Ireallyrecommendthatyoureadhispaperonthistopic(DonaldKnuth,December1974,StructuredProgrammingwithgotoStatements).Justbecausethegotostatementsgotbanishedfromallmodernprogramminglanguages,itdoesn'tmeanthisislessofagoodread.
Lateroninthischapter,you'lllearnabouttoolsthathelpyouusetranspilerseasilywithinyourproject,andwe'lltakealookatthedecisionsanddirectionsAngularwentwiththeirsourcecode.
Let'slookatafewlanguagefeaturesthatcomewithECMAScript6andmakeourlifemucheasier.
Classes
ClasseswereamongonethemostrequestedfeaturesinJavaScript,andIwasoneofthepeoplevotingforit.Well,comingfromanOOPbackgroundandbeingusedtoorganizingeverythingwithinclasses,itwashardformetoletgo.Although,afterworkingwithmodernJavaScriptforsometime,you'llreducetheirusetothebareminimumandtoexactlywhattheyaremadefor—inheritance.
ClassesinECMAScript6provideyouwithsyntacticsugartodealwithprototypes,constructorfunctions,supercalls,andobjectpropertydefinitionsinawaythatyouhavetheillusionthatJavaScriptcouldbeaclass-basedOOPlanguage:
classFruit{
constructor(name){this.name=name;}
}
constapple=newFruit('Apple');
Aswelearnedintheprevioustopicabouttranspilers,ECMAScript6canbede-sugaredtoECMAScript5.Let'stakealookatwhatatranspilerwouldproducefromthissimpleexample:
functionFruit(name){this.name=name;}
varapple=newFruit('Apple');
ThissimpleexamplecaneasilybebuiltusingECMAScript5.However,onceweusethemore
www.EBooksWorld.ir
complexfeaturesofclass-basedobject-orientedlanguages,thede-sugaringgetsquitecomplicated.
ECMAScript6classesintroducesimplifiedsyntaxtowriteclassmemberfunctions(staticfunctions),theuseofthesuperkeyword,andinheritanceusingtheextendskeyword.
IfyouwouldliketoreadmoreaboutthefeaturesinclassesandECMAScript6,IhighlyrecommendthatyoureadthearticlesofDr.AxelRauschmayer(http://www.2ality.com/).
Modules
Modulesprovideawaytoencapsulateyourcodeandcreateprivacy.Inobject-orientedlanguages,weusuallyuseclassesforthis.However,Iactuallybelievethisisanantipatternratherthanagoodpractice.Classesshouldbeusedwhereinheritanceisdesiredandnotjusttostructureyourcode.
I'msurethatyou'veencounteredalotofdifferentmodulepatternsinJavaScriptalready.Oneofthemostpopularonesthatcreatesprivacyusingafunctionclosureofanimmediatelyinvokedfunctionexpression(IIFE)isprobablytherevealingmodulepattern.Ifyou'dliketoreadmoreaboutthisandmaybeothergreatpatterns,Irecommendthebook,LearningJavaScriptDesignPatterns,byAddyOsmani.
WithinECMAScript6,wecannowusemodulestoservethispurpose.Wesimplycreateonefilepermodule,andthenweusetheimportandexportkeywordstoconnectourmodulestogether.
WithintheECMAScript6modulespecification,wecanactuallyexportasmanythingsaswelikefromeachmodule.Wecanthenimportthesenamedexportsfromanyothermodule.Wecanhaveonedefaultexportpermodule,whichisespeciallyeasytoimport.Defaultexportsdon'tneedtobenamed,andwedon'tneedtoknowtheirnamewhenimportingthem:
importSomeModulefrom'./some-module.js';
varsomething=SomeModule.doSomething();
exportdefaultsomething;
Therearemanycombinationsonhowtousemodules.Wewilldiscoversomeofthesetogetherwhileworkingonourtaskmanagementapplicationduringtheupcomingchapters.Ifyou'dliketoseemoreexamplesonhowtousemodules,IcanrecommendtheMozillaDeveloperNetworkdocumentation(https://developer.mozilla.org)ontheimportandexportkeywords.
Templatestrings
Templatestringsareaverysimple,buttheyareanextremelyusefuladditiontotheJavaScriptsyntax.Theyservethreemainpurposes:
Writingmultilinestrings
www.EBooksWorld.ir
StringinterpolationTaggedtemplatestrings
Beforetemplatestrings,itwasquiteverbosetowritemultilinestrings.Youneededtoconcatenatepiecesofstringsandappendanew-linecharacteryourselftothelineendings:
constheader='<header>\n'+
'<h1>'+title+'</h1>\n'+
'</header>';
Usingtemplatestrings,wecansimplifythisexamplealot.Wecanwritemultilinestrings,andwecanalsousethestringinterpolationfunctionalityforourtitlevariablethatweusedtoconcatenateearlier:
constheader='
<header>
<h1>${title}</h1>
</header>
`;
Notethebackticksinsteadoftheprevioussinglequotes.Templatestringsarealwayswrittenbetweenbackticks,andtheparserwillinterpretallcharactersinbetweenthemaspartoftheresultingstring.Thisway,thenew-linecharacterspresentinoursourcefilewillalsobepartofthestringautomatically.
Youcanalsoseethatwehaveusedthedollarsign,followedbycurlybracketstointerpolateourstrings.ThisallowsustowritearbitraryJavaScriptwithinstringsandhelpsalotwhileconstructingHTMLtemplatestrings.
YoucanreadmoreabouttemplatestringsontheMozillaDeveloperNetwork.
ECMAScriptorTypeScript?
TypeScriptwascreatedin2012byAndersHejlsbergwiththeintentiontoimplementthefuturestandardofECMAScript6butalsotoprovideasupersetofsyntaxandfeaturesthatwasnotpartofthespecification.
TherearemanyfeaturesinTypeScriptasasupersettotheECMAScript6standard,including,butnotlimitedtothefollowing:
OptionalstatictypingwithtypeannotationsInterfacesEnumtypesGenerics
It'simportanttounderstandthatallofthefeaturesthatTypeScriptprovidesasasupersetareoptional.YoucanwritepureECMAScript6andnottakeadvantageoftheadditionalfeaturesthatTypeScriptprovides.TheTypeScriptcompilerwillstilltranscompilepureECMAScript6
www.EBooksWorld.ir
codetoECMAScript5withoutanyerrors.
Note
MostofthefeaturesthatareseeninTypeScriptareactuallyalreadypresentinotherlanguages,suchasJavaandC#.OnegoalofTypeScriptwastoprovidelanguagefeaturesthatsupportworkflowsandbettermaintainabilityforlarge-scaleapplications.
Theproblemwithanynonstandardlanguageisthatnobodycantellhowlongthelanguagewillbemaintainedandhowfastthemomentumofthelanguagewillbeinthefuture.Intermsofsupport,thechancesarehighthatTypeScript,withitssponsorMicrosoft,willactuallyhavealonglife.However,there'sstillnoguaranteethatthemomentumandtrendofthelanguagewillkeepmovingatareasonablepace.ThisproblemdoesobviouslynotexistforstandardECMAScript6becauseit'swhatthewebofthefutureismadeofandwhatbrowserswillspeaknatively.
Still,therearevalidreasonstousetheextendedfeaturesofTypeScriptifyou'dwanttoaddressthefollowingconcernsthatclearlyoutweighthenegativeimplicationsofanuncertainfutureinyourproject:
LargeapplicationsthatundergoahugeamountofchangesandrefactoringLargeteamsthatrequireastrictgovernancewhileworkingoncode
Inthisbook,we'lluseaTypeScriptcompiler,butwewillworkwithstandardECMAScript6codewithoneexceptionthatiscoveredinthenexttopicaboutdecorators.
Decorators
DecoratorsarenotpartoftheECMAScript6specification,buttheywereproposedtotheECMAScript7standardfor2016.Theyprovideuswithawaytodecorateclassesandpropertiesduringdesigntime.Thisallowsadevelopertousemeta-annotationswhilewritingclasses,anddeclarativelyattachfunctionalitytotheclassanditsproperties.
DecoratorsarenamedafterthedecoratorpatternthatwasinitiallydescribedinthebookDesignPatterns:ElementsofReusableObject-OrientedSoftwareofErichGammaandhiscolleagues,alsoknownastheGangofFour(GoF).
Theprincipleofdecorationisthatanexistingprocedureisinterceptedandthedecoratorhasthechancetoeitherdelegate,provideanalternativeprocedure,ordoamixfromboth.
www.EBooksWorld.ir
Visualizationofdecorationinadynamicenvironmentwiththeexampleofasimpleaccessprocedure
DecoratorsinECMAScript7canbeusedtoannotateclassesandclassproperties.Notethatthisalsoincludesclassmethods,asclassmethodsarealsopropertiesoftheclassprototypeobject.Decoratorsgetdefinedasregularfunctions,andtheycanbeattachedtoclassesorclasspropertieswiththeatsymbol.Ourdecoratorfunctionwillthenbecalledwithcontextualinformationaboutthelocationofinclusioneverytimethatthedecoratorisplaced.
Let'stakealookatasimpleexamplethatillustratestheuseofadecorator:
functionlogAccess(obj,prop,descriptor){
constdelegate=descriptor.value;
descriptor.value=function(){
console.log(`${prop}wascalled!`);
returndelegate.apply(this,arguments);
};
}
classMoneySafe{
@logAccess
openSafe(){
this.open=true;
}
}
www.EBooksWorld.ir
constsafe=newMoneySafe();
safe.openSafe();//openSafewascalled!
WehavecreatedalogAccessdecoratorthatwilllogallfunctioncallsthataretaggedwiththedecorator.IfwelookattheMoneySafeclass,youcanseethatwehavedecoratedtheopenSafemethodwithourlogAccessdecorator.
ThelogAccessdecoratorfunctionwillbeexecutedforeachannotatedpropertywithinourcode.Thisenablesustointerceptthepropertydefinitionofthegivenproperty.Let'stakealookatthesignatureofourdecoratorfunction.Decoratorfunctionsthatareplacedonclasspropertieswillbecalledwiththetargetobjectofthepropertydefinitionasafirstparameter.Thesecondparameteristheactualpropertynamethatisdefined,followedbythelastparameter,whichisthedescriptorobjectthatissupposedtobeappliedtotheobject.
Thedecoratorgivesustheopportunitytointerceptthepropertydefinition.Inourcase,weusethisabilitytoexchangethedescriptorvalue(whichistheannotatedfunction)withaproxyfunctionthatwilllogthefunctioncallbeforecallingtheoriginfunction(delegation).Forsimplificationpurposes,we'veimplementedaverysimpleyetincompletefunctionproxy.Forreal-worldscenarios,itwouldbeadvisabletouseabetterproxyimplementation,suchastheECMAScript6proxyobject.
Decoratorsareagreatfeaturetoleverageaspect-orientedconceptsanddeclarativelyaddbehaviortoourcodeatdesigntime.
Let'slookatasecondexamplewhereweuseanalternativewaytodeclareandusedecorators.Wecantreatdecoratorslikefunctionexpressionswhereourdecoratorfunctionisrewrittenasafactoryfunction.Thisformofusageisespeciallyusefulwhenyouneedtopassalongconfigurationtothedecorator,whichismadeavailableinthedecoratorfactoryfunction:
functiondelay(time){
returnfunction(obj,prop,descriptor){
constdelegate=descriptor.value;
descriptor.value=function(){
constcontext=this;
constargs=arguments;
returnnewPromise(function(success){
setTimeout(function(){
success(delegate.apply(context,arguments));
},time);
});
};
};
}
classDoer{
@delay(1000)
doItLater(){
console.log('Ididit!');
}
www.EBooksWorld.ir
}
constdoer=newDoer();
doer.doItLater();//Ididit!(after1second)
WehavenowlearnedhowECMAScript7decoratorscanhelpyoutowritedeclarativecodethathasanaspect-orientedtwisttoit.Thissimplifiesdevelopmentalotbecausewecannowthinkofbehaviorthatweaddtoourclassesduringdesigntimewhenweactuallythinkabouttheclassasawholeandwritetheinitialstuboftheclass.
DecoratorsinTypeScriptareslightlydifferentthanthedecoratorsfromECMAScript7.Theyarenotlimitedtoclassesandclassproperties,buttheycanalsobeplacedonparameterswithintheclassmethods.Thisallowsyoutoannotatefunctionparameters,whichcanbeusefulinsomecases:
classTypeScriptClass{
constructor(@ParameterDecorator()param){}
}
Angularusesthisfeaturetosimplifydependencyinjectiononclassconstructors.Asalldirective,component,andserviceclassesgetinstantiatedfromAngulardependencyinjectionandnotbyusdirectly,theseannotationshelpAngularfindthecorrectdependencies.Forthisuse-case,functionparameterdecoratorsactuallymakealotofsense.
Note
Currently,therearestillissueswiththeimplementationofdecoratorsonclassmethodparameters,whichisalsowhyECMAScript7doesnotsupportit.AsthisfeatureiscrucialtobuildanAngular2application,we'llusetheTypeScriptcompilertotranspilethecodeofourapplication.ThisistheonlyTypeScript-specificfeaturethatwe'lluseinthisbook.
www.EBooksWorld.ir
ToolsInordertomakeuseofallthesefuturetechnologies,weneedsometoolstosupportus.WewerealreadytalkingaboutECMAScript6anddecorators,whereweactuallypreferTypeScriptdecorators,astheysupportthefunctionparameterdecoratorsthatareusedbyAngular2.AlthoughtheECMAScript6syntaxsupportsmodules,westillneedsomesortofamoduleloaderthatwillactuallyloadtherequiredmodulesinthebrowserorhelpusgenerateanexecutablebundle.
www.EBooksWorld.ir
Node.jsandNPMNode.jsisJavaScriptonsteroids.Initially,aforkoftheV8JavaScriptenginefromtheGoogleChromebrowser,Node.jswasextendedwithmorefunctionality,specificallytomakeJavaScriptusefulontheserver-side.Filehandling,streams,systemAPIs,andahugeecosystemofuser-generatedpackagesarejustsomeofthefactsthatmakethistechnologyanoutstandingpartnerforyourwebdevelopment.
Thenodepackagemanager,NPM,isadoortoover200,000packagesandlibrariesthathelpyoubuildyourownapplicationorlibrary.TheNode.jsphilosophyisverysimilartotheUNIXphilosophy,wherepackagesshouldstaysmallandsharp,buttheyshouldusecompositiontoachievegreatergoals.
Tobuildourapplication,wewillrelyonNode.jsasthehostforthetoolsthatwe'regoingtouse.Weshould,therefore,makesurethatweinstallNode.jsonourmachinesothatwearepreparedforthenextchapter,wherewestarttocraftourtaskmanagementapplication.
Note
YoucangetNode.jsfromtheirwebsiteathttps://nodejs.org,anditshouldbeabreezetoinstallthisonanykindofoperatingsystembyfollowingtheinstructionsontheirwebsite.
Onceyou'veinstalledNode.js,wecanperformasimpletesttocheckwhethereverythingisupandrunning.Openaterminalconsoleandexecutethefollowingcommand:
node-e"console.log('HelloWorld');"
www.EBooksWorld.ir
SystemJSandJSPMTherearemanymoduleformatsandmoduleloadersoutthere,butthere'sonethatrulesthemallinmyopinion.SystemJSisbuiltontopofanES6moduleloaderpolyfilland,therefore,movesveryclosetoanupcomingstandard.Istronglybelieveinstandardizationand,therefore,preferSystemJSoverothermoduleloaders,suchasRequireJS,Browserify,orwebpack.Weshouldstopusinglibrarieswherepossibleandrelyonpolyfillsthatmakeourbrowsercapableofrunningthecodeofthefuture.
SystemJSisauniversalmoduleloaderthatiscapableofloadingmanydifferentmoduleformats,suchasAMD,CommonJS,andECMAScript6,anditalsosupportsaveryflexibleshimingmechanismtomodularizeglobalJavaScript.
SystemJSalsosupportsthemostpopulartranspilers,includingECMAScript6andTypeScript.ThismeansthatyoucanactuallyloadECMAScript6codedirectlyinyourbrowser,whereit'stranspiledbySystemJSinruntime.Thisisgreatduringdevelopment,especiallybecauseyou'reallowedtoloadmodulesfromanylocation,includingremoteHTTPlocations,suchasGitHubortheNPMrepository.
JSPM
TheJavaScriptpackagemanagerisnotjustanotherpackagemanagerforJavaScript.ThisisbasicallyamediatorandmanagerforSystemJSthathelpsyoulookuppackagesfrompackagerepositories,suchasBowerorNPM,anditcreatesthenecessaryconfigurationforSystemJS.JSPMiswritteninNode.jsanddoesnotcomewithitsownremotepackagerepository.AsSystemJSneedsURLsandmodulemappingstoknowwheretoloadmodulesfrom,JSPMisyourtooltocreatethisnecessaryconfigurationandsimplifypackageinstallation.
GettingstartedwithJSPM
Let'screateasimpleapplicationtogetherusingJSPM.Firstofall,weneedtoinstalltwoglobalmoduleswithNPM.BesidesJSPM,we'llalsoinstallatoolcalledlive-server,whichwillhelpusduringdevelopmentbyprovidinganHTTPserverthatservesstaticfiles.Italsohasfilechangedetectionbuilt-in,anditwillreloadyourbrowserautomaticallyonceafilechangehasbeendetected.Thisprovidesaveryshortfeedbackloopandmakesdevelopmentaveryfastprocess:
1. Runthefollowingcommandonyourcommandline:
npminstall-gjspmlive-server
Tip
NotethatonUNIX-likesystems,suchasLinuxorMacOSX,it'ssometimesrequiredtorunNPMasasuperuser.It'salsorecommendedthatyouusetheNodeVersionManager(NVM)togetaroundthoseissues(https://github.com/creationix/nvm).
www.EBooksWorld.ir
2. AfterinstallingJSPMandthelive-serverpackage,wecangoaheadandcreateourfirstapplicationusingJSPM.
3. Createanewdirectoryfortheapplication,andopenaterminalconsolewithinthisdirectory.
4. YoucannowexecutethefollowingcommandonyourterminalconsoletoinstallJSPMlocallyandinitializeanewJSPMproject:
npminstalljspm--save-dev
jspminit
5. JSPMwillstartawizardthatguidesyouthoughtheinitializationsteps.Youcananswerallquestionswiththedefaultanswer(justhitEnter)exceptforthequestionaboutwhichtranspileryou'dliketousethatyoushouldanswerwithTypeScript.
6. AfterJSPMinstallsallthenecessarypackages,wecangoaheadandcreateourindex.htmlfile.Navigatetoyourprojectfolderandcreateanewfile,index.html,inyourfavoriteeditor:
<!doctypehtml>
<scriptsrc="jspm_packages/system.js"></script>
<scriptsrc="config.js"></script>
<script>
System.import('main.js');
</script>
7. ThisveryminimalisticHTMLisalreadythefoundationforourJSPMHelloWorldapplication.AfterincludingtheSystemJSlibraryandtheconfig.jsfilethatwasgeneratedbyJSPM,weonlyneedtobootstrapourapplicationbytellingSystemJSwhichfiletoimport.
8. Beforewecreateourmainapplicationfile,wewillquicklyinstalljQueryasapackage,justtodemonstratehoweasilythird-partylibrariescanbeinstalledandusedwithSystemJSandJSPM:
jspminstalljquery
9. AfterinstallingjQuery,wecangoaheadandcreateourmain.jsfileinsideoftheapplicationfolder:
import$from'jquery';
classHelloWorld{
constructor(){
$(document.body).append('<h1>HelloWorld!</h1>');
}
}
consthelloWorld=newHelloWorld();
10. Inordertorunthisexampleinthebrowser,wecannowstartourliveserverwiththefollowingcommandexecutedinsideoftheapplicationfolder:
live-server
www.EBooksWorld.ir
Afterfollowingtheprecedingsteps,youshouldhaveaworkingexamplewithECMAScript6andSystemJSusingtheTypeScripttranspiler.UsingLiveReload,yourbrowsershouldautomaticallyopenanddisplayourHelloWorldapplication.YoucanalsotrynowtomodifythecodeabitandchangethesentencethatiswrittentotheDOM.You'llnoticethatonceyousaveyourchanges,thebrowserwillimmediatelyreloadthepage.
www.EBooksWorld.ir
SummaryInthischapter,welookedatacomponent-basedapproachtostructureuserinterfaces,andwetalkedaboutthenecessaryaspectsofitsbackgroundtounderstandwhywearemovinginthisdirectionwiththewebstandardandframeworks,suchasAngular.Wealsoensuredthatwearepreparedwithallthetechnologythatwewilluseintheupcomingchaptersinthisbook.YoucreatedyourfirstsimpleexampleusingJSPM,SystemJS,ECMAScript6,andtheTypeScripttranspiler.Now,wearereadytostartbuildingourtask-managementsystemusingacomponent-basedarchitecturetoitsfullpotential.
Inthenextchapter,we'regoingtostartbuildingourtaskmanagementapplicationusingAngular2components.We'lllookattheinitialstepsthatarerequiredtocreateanAngular2applicationfromscratchandfleshoutthefirstfewcomponentsinordertobuildatasklist.
www.EBooksWorld.ir
Chapter2.Ready,Set,Go!Inthischapter,wewillstartbuildingourtaskmanagementapplication.We'lljumprightintothecoreoftheapplicationandcreatetheinitialcomponentsrequiredtomanageasimpletasklist.Intheprocessofgoingthroughthischapter,you'lllearnaboutthefollowingtopics:
BootstrappinganAngularapplicationusingamaincomponentComponentinputandoutputHostpropertybindingStylingandviewencapsulationImportingHTMLtemplatesusingtheSystemJStextloaderUsingEventEmittertoemitcustomeventsTwo-waydatabindingComponentlifecycle
www.EBooksWorld.ir
ManagingtasksAfterpickingupthebasicsfromthepreviouschapter,wewillnowgoonandcreateataskmanagementapplicationtogetherintheupcomingchapters.You'lllearnaboutsomeconceptsduringthechaptersandthenusethemwithpracticalexamples.You'llalsolearnhowtostructureanapplicationusingcomponents.Thisbeginswiththefolderstructureandendswithsettinguptheinteractionbetweencomponents.
www.EBooksWorld.ir
VisionThetaskmanagementapplicationshouldenableuserstomanagetaskseasilyandhelpthemorganizesmallprojects.Usabilityisthecentralaspectofanyapplication;therefore,you'llneedtodesignamodernandflexibleuserinterfacethatwillsupporttheuser.
Apreviewofthetaskmanagementapplicationwearegoingtobuild
Ourtaskmanagementapplicationwillconsistofcomponentsthatwillallowustodesignaplatformprovidingagreatuserexperienceforthepurposeofmanagingtasks.Let'sdefinethecorefeaturesofourapplication:
ManagingtaskswithinmultipleprojectsandprovidingaprojectoverviewSimpleschedulingaswellasatime-and-effort-trackingmechanismOverviewingthedashboardusinggraphicalchartsTrackingactivitiesandprovidingavisualauditlogAsimplecommentingsystemthatwouldworkacrossdifferentcomponents
Thetaskmanagementapplicationisthemainexampleinthisbook.Therefore,thebuildingblockswithinthebookshouldonlycontainthecodethatisrelevanttothethemeofthebook.Ofcourse,otherthancomponents,anapplicationneedsotherfunctionalities,suchasvisualdesign,data,sessionmanagement,andotherimportantparts,towork.Whiletherequiredcodeforeachchaptercanbedownloadedonline,we'llonlydiscussthecoderelevanttothetopicslearnedwithinthebook.
www.EBooksWorld.ir
StartingfromscratchLet'sstartoutbycreatinganewfoldercalledangular-2-componentsinordertocreateourapplication:
1. OpenaconsolewindowinsideournewlycreatedfolderandrunthefollowingcommandtoinitializeanewNode.jsproject:
npminit
2. FinishtheinitializationwizardbyconfirmingallthestepswiththeEnterkey(defaultsettings).
3. Sincewe'reusingJSPMtomanageourdependencies,weneedtoinstallitasaprojectNode.jspackage:
npminstalljspm--save-dev
4. Let'salsoinitializeanewJSPMprojectwithinourprojectfolder.Besuretousethedefaultsettings(justhittheEnterkey)forallsettings,exceptforthestepwhereyouareaskedwhichtranspileryou'dliketouse.EnterTypeScriptatthisstage:
jspminit
5. We'llnowuseJSPMtoinstalltherelevantAngular2packagesintoourprojectasdependencies.We'llalsoinstallaSystemJSloaderplugintoloadtextfilesasmodules.We'llprovidesomedetailsaroundthislateron:
jspminstallnpm:@angular/corenpm:@angular/commonnpm:@angular/compiler
npm:@angular/platform-browser-dynamicnpm:rxjstext
Let'sexaminewhatwe'vebeencreatingsofarbyusingtheNPMandJSPMcommand-linetools.
Thepackage.jsonfileisourNode.jsconfigurationfilethatwe'reusingasthebasetoworkwithJSPM(thepackagemanager)andSystemJS(themoduleloaderwithtranspiler).Ifyoucheckoutthepackage.jsonfile,youwillseeanadditionalsectionforJSPMdependencies:
"jspm":{
"dependencies":{
"@angular/common":"npm:@angular/[email protected]",
"@angular/compiler":"npm:@angular/[email protected]",
"@angular/core":"npm:@angular/[email protected]",
"@angular/platform-browser-dynamic":"npm:@angular/platform-browser-
"text":"github:SystemJS/[email protected]"
},
"devDependencies":{
"typescript":"npm:[email protected]",
}
}
www.EBooksWorld.ir
Let'stakeaquicklookatthedependencieswehaveinstalledusingJSPMandtheirpurpose:
Package Description
@angular/core
ThisisthecorepackageofAngular2,hostedonNPM.IfyourememberfromChapter1,Component-BasedUserInterfaces,JSPMisonlyabroker,anditdelegatestootherpackagerepositories.ThecorepackagecontainsallAngular-coremodules,suchasthe@Componentdecorator,changedetection,dependencyinjection,andmore.
@angular/common
TheAngularcommonpackageprovidesuswithbasedirectives,suchasNgIfandNgFor.Italsocontainsallthebasepipesandthedirectivesthatareusedtocontrolforms.
@angular/compiler
Thecompilerpackagecontainsalltheartifactsrequiredtocompileviewtemplates.Angularnotonlyprovidestheabilitytoprecompiletemplatestogainfasterbootingtime,butitalsousesthecompileratruntimetoconverttexttemplatesintocompiledtemplates.Thispackageisrequiredifwe'recompilingtemplatesatruntime.
@angular/platform-
browser-dynamic
Thispackageincludesthebootstrappingfunctionalitythatwillhelpusstartourapplication.Thebootstrapinitiatedbytheplatform-browser-dynamicpackageisdynamicinthesenseofcompilingtemplatesatruntime.
typescript
ThisdevelopmentdependencyistheTypeScripttranspilerforSystemJS.IttranspilesourECMAScript6andTypeScriptcodetoECMAScript5,fromwhereitcanruninthebrowser.
text
ThisSystemJSloadersupportstheloadingoftextfilesintheformofJavaScriptstrings.ThisisespeciallyusefulifyouliketoloadHTMLtemplatesandavoidasynchronousrequests.
Ourmainentrypointfordisplayingourapplicationwithinthebrowserisourindexsite.Theindex.htmlfilecompletesthefollowingfiveactions:
LoadingECMAScript6polyfilles6-shimfromaCDN.ThisscriptisrequiredtomakesurethebrowserunderstandsthelatestECMAScript6APIs.LoadingtheAngular2polyfillsrequiredbytheframework.ThisincludesvariouspatchesforthebrowserthatarerequiredtorunanAngular2application.It'simportant
www.EBooksWorld.ir
toloadthesepolyfillsbeforeweloadanyothercodewithinourapplication.LoadingSystemJSandtheSystemJSconfig.jsfilethatcontainsthemappinginformationgeneratedbyJSPM.UsingtheSystem.importfunctiontoloadandexecutethemainentrypoint,whichisourboostrap.jsfile.
Let'screateanewindex.htmlfilewithintherootfolderofourproject:
<!doctypehtml>
<html>
<headlang="en">
<title>Angular2Components</title>
</head>
<body>
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.0/es6-
shim.min.js"></script>
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-
beta.15/angular2-polyfills.js"></script>
<scriptsrc="jspm_packages/system.js"></script>
<scriptsrc="config.js"></script>
<script>
System.import('lib/bootstrap.js');
</script>
</body>
</html>
Let'smoveontoourapplicationcomponent.Youcanthinkofitastheoutermostcomponentofyourapplication.It'sthemaincomponentinthatitrepresentsyourwholeapplication.Everyapplicationneedsoneandjustonemaincomponent.Thisiswhereyourcomponenttreehasitsroots.
We'llnameourmaincomponentAppbecauseitrepresentsourwholeapplication.Let'sgoaheadandcreatethecomponentwithinanewlibfolderinourprojectfolder.Createafile,app.js,withthefollowingcontent:
//WeneedtheComponentannotationaswellasthe
//ViewEncapsulationenumeration
import{Component,ViewEncapsulation}from'@angular/core';
//Usingthetextloaderwecanimportourtemplate
importtemplatefrom'./app.html!text';
//Thiscreatesourmainapplicationcomponent
@Component({
//TellsAngulartolookforanelement<ngc-app>tocreatethis
//component
selector:'ngc-app',
//Let'susetheimportedHTMLtemplatestring
template,
//TellAngulartoignoreviewencapsulation
encapsulation:ViewEncapsulation.None
})
www.EBooksWorld.ir
exportclassApp{}
There'snothingdifferentherefromwhatwealreadyknowaboutstructuringacomponent,somethingthatwelearnedinthepreviouschapter.However,therearetwomaindifferencesherecomparedtohowwecreatedthecomponentsbefore.Ifyoulookathowweconfiguredthetemplateproperty,youcouldtellthatwedidn'twritetheHTMLtemplatedirectlywithintheJavaScriptfileinsidetheECMAScript6templatestrings.Instead,we'regoingtoloadthetemplateintoaJavaScriptstringusingthetextloaderplugininSystemJS.Wecanjustloadanytextfilefromthefilesystembyappending!texttoourregularECMAScript6imports:
importtemplatefrom'./app.html!text';
Thiswillloadthefile,app.html,fromthecurrentdirectoryandmakeadefaultexportwithitscontentasastring.
Theseconddifferenceisthatwe'reusingViewEncapsulationtospecifyhowAngularshouldhandleviewencapsulation.Angularhasthreeways,tohandleviewencapsulation,whichprovidesdifferentlevelsofgranularityandhastheirownprosandcons.Theyareasfollows:
Encapsulationtype Description
ViewEncapsulation.Emulated
Ifacomponentissettoemulatedviewencapsulation,itwillemulatestyleencapsulationbyattachingthegeneratedattributestothecomponentelementandmodifyingCSSselectorstoincludetheseattributeselectors.Thiswillenablecertainformsofencapsulation,althoughtheouterstylescanstillleakintothecomponentifthereareotherglobalstyles.
Thisviewencapsulationmodeisthedefaultmode,ifnotspecifiedotherwise.
ViewEncapsulation.Native
NativeviewencapsulationissupposedtobetheultimategoaloftheviewencapsulationconceptwithinAngular.ItmakesuseofShadowDOM,asdescribedinthepreviouschapter,tocreateanisolatedDOMforthewholecomponent.ThismodedependsonthebrowsertosupportShadowDOMnatively,andtherefore,can'talwaysbeused.It'salsoimportanttonotethatglobalstyleswillnolongerberespectedandlocalstylesneedtobeplacedwithinthecomponentininlinestyletags(orusethestylespropertyonthecomponentannotation).
ThismodetellsAngularnottoprovideanytemplateorstyleencapsulation.Withinourapplication,wemainlyrelyon
www.EBooksWorld.ir
ViewEncapsulation.None stylescomingfromaglobalCSS;therefore,weusethismodeformostofthecomponents.NeitherShadowDOM,norattributeswillbeusedtocreatestyleencapsulation;wecansimplyusetheclassesspecifiedwithinourglobalCSSfile.
Asthiscomponentisnowrelyingonatemplatetobeloadedfromthefilesystem,weneedtocreatetheapp.htmlfileinthelibfolderwithsomeinitialcontent:
<div>HelloWorld!</div>
Forthetimebeing,that'severythingweputinourtemplate.Ourdirectoryshouldlooksimilartothis:
angular-2-components
├──node_modules/
├──jspm_packages/
├──config.js
├──index.html
├──lib
│├──app.html
│└──app.js
└──package.json
Nowthatwehavecreatedourmainapplicationcomponent,wecanaddthecomponent'shostelementtoourindex.htmlfile:
<!DOCTYPEhtml>
<html>
<headlang="en">
<title>Angular2Components</title>
</head>
<body>
<ngc-app></ngc-app>
...
www.EBooksWorld.ir
BootstrappingTheindex.htmlfilewillloadthebootstrap.jsmoduleusingSystemJSinaninlinescripttag.It'sabestpracticetohaveamainentrypointforyourscriptswhenworkingwithSystemJS.Ourbootstrap.jsfileisresponsibleforloadingallthenecessaryJavaScriptdependenciesforourapplicationaswellasbootstrappingtheAngularframework.
WecangoaheadandbootstrapourAngularapplicationbyprovidingourmainapplicationcomponent,App.Weneedtoimportthebootstrapfunctionfromtheangular2module.WecanthenimportourAppcomponentandcallthebootstrapfunction,passingitasparameter:
//ImportAngularbootstrapfunction
import{bootstrap}from'@angular/platform-browser-dynamic';
//Importourmainappcomponent
import{App}from'./app';
//WearebootstrappingAngularusingourmainapplication
//component
bootstrap(App);
www.EBooksWorld.ir
RunningtheapplicationThecodewe'veproducedsofarshouldnowbeinastatewherewecanrunit.Beforewerunourcodeusingthelive-servermodule,let'sensurewehaveallthefilesready.Atthisstage,ourdirectoryshouldlooksomethinglikethis:
angular-2-components
├──jspm_packages/
├──node_modules/
├──config.js
├──index.html
├──lib
│├──app.html
│├──app.js
│└──bootstrap.js
└──package.json
Nowlet'sstartliveservertostartaserverandabrowserwithlivereload.Forthis,weneedtosimplyexecutethefollowingcommandonthecommandlinewithinourprojectfolder:
live-server
Ifeverythinggoeswell,youwillhaveanopenwebbrowserthatshowsHelloWorld!.
www.EBooksWorld.ir
RecapLet'srecapwhatwehavedonesofar:
1. WeinitializedanewprojectusingNPMandJSPMandinstalledtheAngulardependenciesusingJSPM.
2. Wecreatedourmainapplicationcomponentinapp.js.3. Wealsocreatedabootstrap.jsscripttoincludetheAngularframeworkbootofour
application.4. Weaddedourcomponenttotheindex.htmlfilebyincludinganelementthatmatches
ourcomponentselectorproperty.5. Finally,weusedliveservertostartabasicwebserverandlaunchawebbrowser.
www.EBooksWorld.ir
CreatingatasklistNowthatwehaveourmainapplicationcomponentsetup,wecangoonandfleshoutourtaskapplication.Thesecondcomponentthatwe'regoingtocreatewillberesponsibleforlistingtasks.Followingtheconceptofcomposition,we'llcreateatask-listcomponentasasubcomponentofourmainapplicationcomponent.
Let'screateafolderwithinthelibfoldercalledtask-listandanewJavaScriptfilecalledtask-list.js,wherewewillwriteourcomponentcode:
import{Component,ViewEncapsulation}from'@angular/core';
importtemplatefrom'./task-list.html!text';
@Component({
selector:'ngc-task-list',
//Thehostpropertyallowsustosetsomepropertiesonthe
//HTMLelementwhereourcomponentisinitialized
host:{
class:'task-list'
},
template,
encapsulation:ViewEncapsulation.None
})
exportclassTaskList{
constructor(){
this.tasks=[
{title:'Task1',done:false},
{title:'Task2',done:true}
];
}
}
We'vecreatedaverysimpletask-listcomponentthathasalistoftasksstoredinternally.ThiscomponentwillbeattachedtoHTMLelementsthatmatchtheCSSelementselectorngc-task-list.
Nowlet'screateaviewforthiscomponenttodisplaythetasks.AsyoucanseefromtheimportwithinthecomponentJavaScriptfile,wearelookingforafilecalledtask-list.html:
<div*ngFor="lettaskoftasks"class="task">
<inputtype="checkbox"[checked]="task.done">
<divclass="task__title">{{task.title}}</div>
</div>
WeusetheNgFordirectivetorepeatthe<div>elementwiththeclasstaskforasmanytasksaswehaveinthetasklistofourcomponent.TheNgFordirectiveinAngularwillcreateatemplateelementfromitsunderlyingcontentandinstantiateasmanyelementsfromthetemplateastheexpressionevaluatesto.Wecurrentlyhavetwotasksinourtask-listcomponent,sothiswillcreatetwoinstancesofourtemplate.
www.EBooksWorld.ir
Yourfolderstructureinsidethelibfoldershouldnowlooksimilartothis:
angular-2-components
└──lib
├──app.html
├──app.js
├──bootstrap.js
└──task-list
├──task-list.html
└──task-list.js
Allthat'slefttodoinordertomakeourtasklistworkistheinclusionofthetask-listcomponentwithinthemainapplicationcomponent.Wecangoaheadandmodifyourapp.jsfileandaddthefollowinglineontopofit:
import{TaskList}from'./task-list/task-list';
Aswewanttoaddthetask-listcomponenttoourmainapplicationviewtemplate,wealsoneedtomakesurethatAngularknowsaboutthecomponentwhencompilingtheview.Forthis,weneedtoaddthedirectivespropertytoourmainapplicationcomponentwithintheapp.jsfileandincludeourimportedTaskListcomponentclasswithinthelistofdirectives:
...
//TellAngulartoignoreviewencapsulation
encapsulation:ViewEncapsulation.None,
directives:[TaskList]
})
...
Finally,weneedtoincludethehostelementofourtask-listcomponentinthetemplateofthemainapplication,whichislocatedwithintheapp.htmlfile:
<ngc-task-list></ngc-task-list>
Thesewerethelastchangesweneededtomakeinordertomakeourtask-listcomponentwork.Toviewyourchanges,youcanstarttheliveserverbyexecutingthelive-servercommandwithinyourangular-2-componentsdirectory.
www.EBooksWorld.ir
RecapLet'slookatwhatwehavedoneinthepreviousbuildingblock.Weachievedasimplelistingoftaskswithinanencapsulatedcomponentbyfollowingthesesteps:
1. WecreatedthecomponentJavaScriptfilethatcontainsthelogicofourcomponent.2. Wecreatedthecomponent'sviewwithinaseparateHTMLfile.3. Weincludedthecomponentclasswithintheconfigurationofourmainapplication
component.4. WeincludedthecomponentHTMLelementwithinourmainapplicationviewtemplate.
www.EBooksWorld.ir
TherightlevelofencapsulationOurtasklistisdisplayedcorrectlyandthecodeweusedtoachievethislooksquiteokay.However,ifwewanttofollowabetterapproachforcomposition,weshouldrethinkthedesignofourtask-listcomponent.Ifwedrawalineatenlistingthetasklistresponsibilities,wewouldcomeupwiththingssuchaslistingtasks,addingnewtaskstothelist,andsortingandfilteringthetasklist;however,operationsarenotperformedonanindividualtaskitself.Also,renderingthetaskitselffallsoutsideoftheresponsibilitiesofthetasklist.Thetask-listcomponentshouldonlyserveasacontainerfortasks.
Ifwelookatourcodeagain,wewillseethatwe'reviolatingthesingleresponsibilityprincipleandrenderingthewholetaskbodywithinourtask-listcomponent.Let'stakealookathowwecanfixthisbyincreasingthegranularityoftheencapsulation.
Thegoalnowistodoacoderefactoringexercise,alsoknownasextraction.Wearepullingourtask'srelevanttemplateoutofthetasklisttemplateandcreatinganewcomponentthatencapsulatesthetasks.
Forthis,weneedtocreateanewsubfolderwithinthetask-listfoldercalledtask.Withinthisfolder,wewillcreateatemplatefilewiththenametask.html:
<inputtype="checkbox"[checked]="task.done">
<divclass="task__title">{{task.title}}</div>
Thecontentofournewtask.htmlfileisprettymuchthesameaswhatwealreadyhavewithinourtask-list.htmltemplate.Theonlydifferenceisthatwewillnowrefertoanewmodelcalledtask.
Now,withinthetaskfolder,let'screatetheJavaScriptfile,task.js,whichwillcontainthecontrollerclassofourcomponent:
import{Component,Input,ViewEncapsulation}from'@angular/core';
importtemplatefrom'./task.html!text';
@Component({
selector:'ngc-task',
host:{
class:'task'
},
template,
encapsulation:ViewEncapsulation.None
})
exportclassTask{
//Ourtaskmodelcanbeattachedonthehostwithintheview
@Input()task;
}
Inthepreviouschapterofthisbook,wespokeaboutencapsulationandthepreconditionsto
www.EBooksWorld.ir
establishacleanencapsulationforUIcomponents.Oneofthesepreconditionsisthepossibilitytodesignproperinterfacesinandoutofthecomponent.Suchinputandoutputmethodsarenecessarytomakethecomponentworkwithincompositions.That'showacomponentwillreceiveandpublishinformation.
Asyoucanseefromourtaskcomponentimplementation,wearenowbuildingsuchaninterfaceusingthe@Inputannotationonaclassinstancefield.Inordertousethisannotation,wewillfirstneedtoimportitfromtheangularcoremodule.
InputpropertiesinAngularallowustobindtheexpressionsinourtemplatestoclassinstancefieldsonourcomponents.Thisway,wecanpassdatafromtheoutsideofthecomponenttothecomponentinside,usingthecomponentstemplate.Thiscanbethoughtofasanexampleofone-waybinding,fromtheviewtothecomponent.
Ifwe'reusingpropertybindingonaregularDOMproperty,Angularwillcreateabindingoftheexpressiondirectlytotheelement'sDOMproperty.We'reusingsuchatypeofbindingtobindthetaskcompletedflagtothecheckedpropertyofthecheckbox'sinputelement:
Usage Description
@Input()
inputProp;
ThisallowsustobindtheinputPropattributetothecomponentelementwithintheparentcomponent.
Angularassumesthattheattributeoftheelementhasthesamenameasthatoftheinputproperty.
@Input('inp')
inputProp;
Youcanalsooverridethenameoftheattributethatshouldbemappedtothisinput.Here,theinpattributeofthecomponent'sHTMLelementismappedtothecomponent'sinputproperty,inputProp.
Thelastmissingpiecetouseournewlycreatedtaskcomponentisthemodificationoftheexistingtemplateofthetasklist.
Weincludethetaskcomponentwithinourtasklisttemplatebyusingan<ngc-task>element,asspecifiedintheselectorwithinourtaskcomponent.Also,wecreateapropertybindingonthetaskelement.There,wepassthetaskobjectfromthecurrentNgForiterationtothetaskinputofthetaskcomponent.Weneedtoreplacealltheexistingcontentinthetask.htmlfilewiththefollowinglinesofcode:
<ngc-task*ngFor="lettaskoftasks"
[task]="task"></ngc-task>
Inordertomakeourtask-listcomponentrecognizethetaskcomponentelement,weneedto
www.EBooksWorld.ir
addittothetask-listcomponent'sdirectivespropertywithinthetask-list.jsfile:
...
import{Task}from'./task/task';
@Component({
...
directives:[Task]
})
...
Congratulations!You'vesuccessfullyrefactoredyourtasklistbyextractingthetaskintoitsowncomponentandhaveestablishedacleanencapsulation.Also,wecannowsaythatourtasklistisacompositionoftasks.
Ifyouthinkaboutmaintainabilityandreusability,thiswasactuallyaveryimportantstepintheprocessofbuildingourapplication.Youshouldconstantlylookoutforsuchencapsulationopportunities,andifyoufeelsomethingcouldbearrangedintomultiplesubcomponents,youshouldprobablygoforit.Ofcourse,youcanalsooverdothis.There'ssimplynogoldenruletodeterminewhatgranularityofencapsulationistherightone.
Tip
Therightgranularityofencapsulationforacomponentarchitecturealwaysdependsonthecontext.MypersonaltiphereistouseknownprinciplesfromOOP,suchassingleresponsibility,tolaythegroundworkforagooddesignofyourcomponenttree.Alwaysmakesureyourcomponentsareonlydoingthingsthattheyaresupposedtodoastheirnamessuggest.Atasklisthastheresponsibilityoflistingtasksandprovidingsomefiltersorothercontrolsforthelist.Theresponsibilityofoperatingonindividualtaskdataandrenderingthenecessaryviewclearlybelongstoataskcomponentandnotthetasklist.
www.EBooksWorld.ir
RecapInthisbuildingblock,wecleanedupourcomponenttreeandestablishedcleanencapsulationusingsubcomponents.Then,wesetuptheinterfacesprovidedbyAngularusinginputbindings.Weperformedtheseactionsbyfollowingtheensuingsteps:
1. Wecreatedatasksubcomponent.2. Weusedthetasksubcomponentwiththetask-listcomponent.3. WeusedinputbindingsandDOMelementpropertybindingstoestablishone-waydata
bindinginthetaskcomponent.
www.EBooksWorld.ir
InputgeneratesoutputOurtasklistlooksnicealready,butitwouldbequiteuselessiftheuserisunabletoaddnewtaskstothelist.Let'screateacomponentforenteringnewtaskstogether.Asthiscomponentbelongstothetask-listcomponent,we'regoingtocreateanewfoldercalledenter-taskwithinthetask-listfolder.TheresponsibilitiesofthiscomponentwillbetohandlealltheUIlogicnecessaryforenteringanewtask.
Usingthesamenamingconventionaswiththerestofourcomponents,let'screateafilecalledenter-task.htmltostorethetemplateofourcomponent:
<inputtype="text"class="enter-task__title-input"
placeholder="Enternewtasktitle..."
#titleInput>
<buttonclass="button"(click)="enterTask(titleInput)">
AddTask
</button>
Thistemplateconsistsofaninputfieldaswellasabuttontoenteranewtask.Here,we'remakinguseoftheso-calledlocalviewvariablesbyspecifyingthatourinputfieldshouldhavethereferencename#titleInput.WecanreferencethisvariablewithinthecurrentcomponentviewbythenametitleInput.
Inthiscase,weareactuallyusingthevariabletopasstheinputfieldDOMelementtotheenterTaskfunctionthatwecallonaclickeventontheAddTaskbutton.
Let'stakealookattheimplementationofourComponentclassforenteringanewtaskbyusingthefollowingcodeinanewly-createdenter-task.jsfile:
import{Component,Output,ViewEncapsulation,EventEmitter}from
'@angular/core';
importtemplatefrom'./enter-task.html!text';
@Component({
selector:'ngc-enter-task',
host:{class:'enter-task'},
template,
encapsulation:ViewEncapsulation.None
})
exportclassEnterTask{
//Eventemitterthatgetsfiredonceataskisentered.
@Output()taskEntered=newEventEmitter();
//ThisfunctionwillfirethetaskEnteredeventemitter
//andresetthetasktitleinputfield.
enterTask(titleInput){
this.taskEntered.next(titleInput.value);
titleInput.value='';
titleInput.focus();
}
}
www.EBooksWorld.ir
Forthiscomponent,we'vechosenadesignapproachwhereweusealooserelationtoourtasklistwheretheactualtaskwillbecreated.Althoughthiscomponentiscloselyrelatedtothetasklist,it'sbettertokeepthecomponentsaslooselycoupledaspossible.
Oneofthesimplestformsofinversionofcontrol,acallbackfunctionoreventlistenerisagreatprincipletoestablishloosecoupling.Inthiscomponent,weareusingthe@Outputannotationtocreateaneventemitter.Theoutputpropertiesneedtobeinstancefieldsthatholdaneventemitterwithinthecomponent.Onthecomponent'sHTMLelement,wecanthenuseeventbindingstocaptureanyeventsemitted.Thisgivesusgreatflexibilitythatwecanusetocreateacleanapplicationdesign,wherewegluecomponentstogetherthroughthebindingwithintheview:
Usage Description
@Output()
outputProp=
new
EventEmitter();
WhenoutputProp.next()iscalled,acustomeventwiththenameoutputPropwillbeemittedonthecomponent.Angularwilllookforeventbindingsonthecomponent'sHTMLelement(wherethecomponentisused)andexecutethem:
<my-comp(output-prop)="doSomething()">
Withintheexpressionsineventbindings,youwillalwayshaveaccesstoasyntheticvariablecalled$event.ThisvariableisareferencetothedataemittedbyEventEmitter.
@Output('out')
outputProp=
new
EventEmitter();
Usethiswayofdeclaringyouroutputpropertiesifyou'dwanttonameyoureventsdifferentlyfromwhatyourpropertynameis.Inthisexample,acustomeventwiththenameoutwillbefiredwhenoutputProp.next()iscalled:
<my-comp(out)="doSomething()">
Okay,let'susethisnewlycreatedcomponenttoaddnewtaskswithinourtask-listcomponent.First,let'smodifytheexistingtemplateofthetask-listcomponent.Openthefile,task-list.html,inthetask-listcomponentfolder.WeneedtoaddtheEnterTaskcomponenttothetemplateandalsohandlethecustomeventthatwe'regoingtoemit,onceanewtaskisenteredwithinthecomponent:
<ngc-enter-task(taskEntered)="addTask($event)">
</ngc-enter-task>
<ngc-task*ngFor="lettaskoftasks"
[task]="task"></ngc-task>
Sincetheoutputpropertywithintheenter-taskcomponentiscalledtaskEntered,wecan
www.EBooksWorld.ir
binditwiththeeventbindingattribute,(taskEntered)="",onthehostelement.
Withintheeventbindingexpression,wethencallafunctiononourtask-listcomponentcalledaddTask.Also,weusethesyntheticvariable$event,whichcontainsthetasktitleemittedfromtheenter-taskcomponent.Now,wheneverwepushthebuttoninourenter-taskcomponentandaneventgetsemittedfromthecomponent,wecatchtheeventinoureventbindingandhandleitwithinthetask-listcomponent.
Wealsoneedtomakesomeminorchangestothetask-listcomponent'sJavaScriptfile.Let'sopentask-list.jsandmodifyitwiththefollowingchanges:
...
//Thecomponentforenteringnewtasks
import{EnterTask}from'./enter-task/enter-task';
@Component({
...
directives:[Task,EnterTask]
})
exportclassTaskList{
...
//Functiontoaddataskfromtheview
addTask(title){
this.tasks.push({
title,done:false
});
}
}
Theonlythingwechangedwithinthetask-listcomponentmoduleisitsabilitytodeclaretheEnterTaskcomponentinthedirectivespropertysothatthecompilerrecognizesourenter-taskcomponentcorrectly.
Wehavealsoaddedafunction,addTask,whichwilladdanewtasktoourtasklistwithatitlethatispassedtothefunction.Nowthecircleisclosedandoureventfromtheenter-taskcomponentisroutedtothisfunctionwithintheviewofthetask-listcomponent.
Youcannowstartliveserverfromyourprojectdirectoryinordertotestthenewlyaddedfunctionalityusingthelive-servercommand.
www.EBooksWorld.ir
RecapWehaveaddedanewsubcomponentofthetasklistthatisresponsibleforprovidingtheUIlogictoaddnewtasks.Inotherwords,wehavecoveredthefollowingtopics:
1. Wecreatedasubcomponentthatislooselycoupledusingoutputpropertiesandeventemitters.
2. Welearnedaboutthe@Outputannotationandhowtouseittocreateoutputproperties.3. Weusedeventbindingstolinkthebehaviortogether,fromtheviewofacomponent.
www.EBooksWorld.ir
CustomUIelementsThestandardUIelementsinthebrowseraregreat,butsometimes,modernwebapplicationsrequiremorecomplexandintelligentinputelementsthantheonesavailablewithinthebrowser.
We'llnowcreatetwospecificcustomUIelementsthatwe'llusewithinourapplicationgoingforwardinordertoprovideaniceuserexperience:
Checkbox:There'salreadyanativecheckboxinputinthebrowser,butsometimes,it'shardtofititintothevisualdesignofanapplication.Nativecheckboxesarelimitedintheirstylingpossibilities,andtherefore,it'shardtomakethemlookgreat.Sometimes,it'sthoseminordetailsthatmakeanapplicationlookappealing.Togglebuttons:Thisisalistoftogglebuttons,whereonlyonebuttoncanbetoggledwithinthelist.Theycouldalsoberepresentedwithanativeradiobuttonlist.However,likewithnativecheckboxes,radiobuttonsaresometimesnotreallythenicestvisualsolutiontotheproblem.Alistoftogglebuttonsthatalsorepresentsaselect-one-userinputelementismuchmoremodernandprovidesthevisualaspectthatwearelookingfor.Besides,whodoesnotliketopushbuttons?
Let'screateourcustomcheckboxUIelementfirst.Aswe'llprobablycomeupwithafewcustomUIelements,firstlet'screateanewsubfoldercalleduiwithinthelibfolder.
Withintheuifolder,wenowcreateafolderwiththenamecheckboxforourcheckboxcomponent.Startingwiththetemplateofournewcomponent,wenowcreateafilewiththenamecheckbox.htmlwithinthecheckboxfolder:
<inputtype="checkbox"
[checked]="checked"
(change)="onChecke
dChange($event.target.checked)">
{{label}}
Onthecheckboxinput,wehavetwobindings.First,wehaveapropertybindingforthecheckedpropertyontheDOMelement.WearebindingtheDOMpropertytothecheckedmemberfieldonourcomponent,whichwearegoingtocreateinamoment.
Also,wehaveaneventbindingontheinputelementwherewelistenforthecheckboxchangeDOMeventandcallthemethodonCheckedChangeonourcomponentclass.Weusethesyntheticvariable$eventtopassthecheckedpropertyonthecheckboxDOMelementwherethechangeeventisoriginated.
Movingontoourcomponentclassimplementation,weneedtocreateafilewiththenamecheckbox.jswithinthecheckboxfolder:
import{Component,Input,Output,ViewEncapsulation,EventEmitter}from
'@angular/core';
www.EBooksWorld.ir
importtemplatefrom'./checkbox.html!text';
@Component({
selector:'ngc-checkbox',
host:{class:'checkbox'},
template,
encapsulation:ViewEncapsulation.None
})
exportclassCheckbox{
//Anoptionallabelcanbesetforthecheckbox
@Input()label;
//Ifthecheckboxischeckedorunchecked
@Input()checked;
//Eventemitterwhencheckedischangedusingtheconvention
//fortwowaybindingwith[(checked)]syntax.
@Output()checkedChange=newEventEmitter();
//Thisfunctionwilltriggerthecheckedeventemitter
onCheckedChange(checked){
this.checkedChange.next(checked);
}
}
There'snothingspecialaboutthiscomponentclassifwefirstlookatit.Itusesaninputpropertytosetthecheckedstatefromtheoutside,anditalsohasanoutputpropertywithaneventemitterthatallowsustonotifytheoutercomponentaboutthechangesofthecheckedstateusingacustomevent.However,there'sanamingconventionthatmakesthiscomponentabitspecial.Theconventionofusinganinputpropertynamealsoasanoutputpropertynamebutappendingthewordchangeisactuallyenablingadeveloperwhousesthecomponenttomakeuseofthetwo-waydatabindingtemplateshorthand.
Angulardoesnotcomewithtwo-waydatabindingoutofthebox.However,creatingtwo-waybindingisquiteeasy.Actually,two-waydatabindingisnodifferentthancombiningapropertybindingwithaneventbinding.
Thefollowingexamplecreatesaverysimpletwo-waydatabindingprocessonaninputfield:
<inputtype="text"(input)="value=$event.target.value"
[value]="value">
ThesimplicityofAngularandthegeneralapproachofextendingthenativefunctionalityofthebrowsermakesimplementingthismechanismabreeze.
Implementingtwo-waydatabindingbetweenacomponentanditssubcomponentisn'treallytoodifficult.Theonlythingweneedtotakecareaboutisthatthereareinputandoutputpropertiesofthesubcomponentinvolved.
Pleasehavealookatthefollowingscreenshot:
www.EBooksWorld.ir
Atwo-waydatabindingbetweenmembervariablesofacomponentandasubcomponent
Sincetwo-waydatabindingwasahighlyrequestedfeatureinAngular,there'sahandyshorthandtowriteit.Let'slookatsomeexamplesonhowtoimplementdatabindingsbetweenatemplateofacomponentanditssubcomponent:
Subcomponentproperties Bindingsincomponenttemplate
@Input()text;
@Output()
textOut=new
EventEmitter();
<sc[text]="myText"
(textOut)="myText=$event">
Webindthecomponent'smyTextpropertytothesubcomponent'stextinput.Also,wecapturethetextOuteventemittedfromthesubcomponentandupdateourmyTextproperty.
@Input()text;
@Output()
textChange=
new
<sc[(text)]="myText">
Wecansimplifythistwo-waydatabindingbyusingthenamingconventiontoappendtheword"change"tooureventemitteridentifier.Thisway,wecanusethetwo-waydatabindingshorthandwithinour
www.EBooksWorld.ir
EventEmitter(); templateusingthe[(property)]notation.
Ifwelookatourcheckboxcomponentimplementationagain,wewillseethatweareusingthetwo-waydatabindingnamingconventionforthecheckedpropertyofourcomponent.Thisway,weenabletheuseofthetemplateshorthandfortwo-waydatabindingwhereverweuseourcustomcheckboxUIcomponent.
Let'sintegrateourcheckboxinthetaskcomponenttoreplacethenativecheckboxinputwe'recurrentlyusingthere.Forthis,weneedtomodifythetask.htmlfilewithinthetask-list/taskfolder,byreplacingthenativeinputcheckboxthatwehaveinthetask.htmlfilewiththefollowinglineofcode:
<ngc-checkbox[(checked)]="task.done"></ngc-checkbox>
Asalways,wealsoneedtotellthetaskcomponentthatwe'dliketousethecomponentwithinthetemplate.Let'schangethecodewithinthetask.jsfileaccordingly:
...
import{...,HostBinding}from'@angular/core';
//Eachtaskhasacheckboxcomponentformarkingtasksasdone.
import{Checkbox}from'../../ui/checkbox/checkbox';
@Component({
...
//WeneedtospecifythatthiscomponentreliesontheCheckbox
//componentwithintheview.
directives:[Checkbox]
})
exportclassTask{
//Ourtaskmodelcanbeattachedonthehostwithintheview
@Input()task;
@HostBinding('class.task--done')
getdone(){
returnthis.task&&this.task.done;
}
}
We'vealreadylearnedaboutthehostpropertyoncomponents.Itallowsustosetpropertyandeventbindingsonourcomponenthostelement.ThehostelementistheDOMelementwhereourcomponentisinitializedwithintheparentcomponent.
There'sanotherwaythroughwhichwecansetpropertiesonourcomponenthostelement,whichbecomeshandywhenwewanttosetapropertybasedonsomedatawithinourcomponent.
Usingthe@HostBindingannotation,wecancreatepropertybindingsonthecomponenthostelementbasedonthememberswithinourcomponent.Let'susethisannotationinordertocreateabindingthatwillconditionallysetthetask--doneclassonthecomponent'sHTML
www.EBooksWorld.ir
element.Thisisusedtomakesomevisualdistinctionsoffinishedtaskswithinourstyles.
ThiswasjustthelaststeptointegrateourcustomcheckboxUIcomponentwithinthetaskcomponent.Youcannowstartlive-serverinordertoviewyourchangesandplayaroundwiththeselargenewcheckboxesinthetasklist.Isn'tthatmuchmorefuntodothanactivatingregularcheckboxes?Don'tunderestimatetheeffectofauserinterfacethatispleasingtouse.Thiscanhaveaverypositiveimpactontheusageofyourproduct.
Ourtasklistafteraddingourcustomcheckboxcomponent
Nowthatwe'vecreatedourcheckboxcomponent,let'sgoaheadandcreateanotherUIcomponentfortogglebuttonsthatwe'lluseinthenexttopic.Weneedtocreateafoldernamedtogglewithintheuifolderandcreateatemplatecalledtoggle.htmlwithinthetogglefolder:
<buttonclass="buttonbutton--toggle"
*ngFor="letbuttonofbuttonList"
[class.button--active]="button===selectedButton"
(click)="onButtonActivate(button)">{{button}}</button>
Nothingspecialhere,really!Werepeatabuttonbyiteratingoveraninstancefieldcalled
www.EBooksWorld.ir
buttonListusingtheNgFordirective.Thisbuttonlistwillcontainthelabelsofourtogglebuttons.Conditionally,wesetaclasscalledbutton--activeusingapropertybindingandcheckingitagainstourcurrentbuttonwithintheiterationagainstaninstancefieldcalledselectedButton.Whenthebuttonisclicked,wecallamethod,onButtonActivate,onourcomponentclassandpassthecurrentbuttonlabelfromtheiteration.
Let'screatetoggle.jsinsidethetogglefolderandimplementthecomponentclass:
import{Component,Input,Output,ViewEncapsulation,EventEmitter}from
'@angular/core';
importtemplatefrom'./toggle.html!text';
@Component({
selector:'ngc-toggle',
host:{
class:'toggle'
},
template,
encapsulation:ViewEncapsulation.None
})
exportclassToggle{
//Alistofobjectsthatwillbeusedasbuttonvalues.
@Input()buttonList;
//Inputandstateofwhichbuttonisselectedneedstoreferto
//anobjectwithinbuttonList
@Input()selectedButton;
//EventemitterwhenselectedButtonischangedusingthe
//conventionfortwowaybindingwith[(selected-button)]
//syntax.
@Output()selectedButtonChange=newEventEmitter();
//Callbackwithinthecomponentlifecyclethatwillbecalled
//aftertheconstructorandinputshavebeenset.
ngOnInit(){
if(this.selectedButton===undefined){
this.selectedButton=this.buttonList[0];
}
}
//Methodtosetselectedbuttonandtriggereventemitter.
onButtonActivate(button){
this.selectedButton=button;
this.selectedButtonChange.next(button);
}
}
Withinourtogglecomponent,werelyonthebuttonListmembertobeanarrayofobjects,asweareusingthisarraywithinourtemplateonanNgFordirective.ThebuttonListmemberisannotatedtobeaninputproperty;thisway,wecanpassthearrayintothecomponent.
FortheselectedButtonmember,whichholdstheobjectofthebuttonListarraythatiscurrentlyselected,weuseatwo-waydatabindingapproach.Thisway,wecannotonlysetthetoggledbuttonfromtheoutsideofthecomponent,butalsogetnotifiedviathetoggle
www.EBooksWorld.ir
component,whenabuttonistoggledintheUI.
WithintheonButtonActivatefunction,wearesettingtheselectedButtonmemberaswellastriggeringtheeventemitter.
ThengOnInitmethodisactuallycalledbyAngularwithinthelifecycleofdirectivesandcomponents.InthecasewheretheselectedButtoninputpropertywasnotspecified,we'lladdacheckandselectthefirstbuttonfromtheavailablebuttonlist.SinceselectedButtonaswellasbuttonListareinstancefieldsthatarealsoinputpropertiesatthesametime,weneedtowaitforthemtobeinitializedinordertoexecutethislogic.It'simportantnottoperformthisinitializationwithinthecomponentconstructor.Thelifecyclehook,OnInit,willbecalledafterthedirectiveinputandoutputpropertieshavebeencheckedforthefirsttime.Itisinvokedonlyoncewhenthedirectiveisconstructed.
Tip
Angularwillcallanylifecyclehooksthathavebeenimplementedonyourcomponentautomatically.
ThenextdiagramillustratesthelifecycleofanAngularcomponent.Uponcomponentconstruction,allthelifecyclehookswillbecalledaspertheordershowninthediagram,excepttheOnDestroyhook,whichwillbecalleduponcomponentdestruction.
Changedetectionwillalsostartasubsetoflifecyclehooks,wheretherewillbeatleasttwocyclesinthefollowingorder:
doCheck
afterContentChecked
afterViewChecked
onChanges(ifanychangesaredetected)
AdetaileddescriptionofthelifecyclehooksandtheirpurposeisavailableontheAngulardocumentationwebsiteathttps://angular.io/docs/ts/latest/guide/lifecycle-hooks.html.
www.EBooksWorld.ir
AnillustrationofthelifecycleofanAngularcomponent
www.EBooksWorld.ir
www.EBooksWorld.ir
RecapInthisblock,youlearnedhowtobuildcustomUIcomponentsthataregenericandlooselycoupledsothattheycanbeusedinothercomponentsassubcomponents.Wealsocompletedthefollowingtasks:
1. Wecreatedasubcomponentthatislooselycoupledusingoutputpropertiesandeventemitters.
2. Welearnedwhatthe@Outputannotationisandhowtouseittocreateoutputproperties.3. Weusedthe@HostBindingannotationtocreatepropertybindingsdeclarativelyfrom
withinourcomponentclass.4. Weusedeventbindingstolinkthebehaviortogetherfromtheviewofacomponent.5. Webuilttwo-waydatabindingusingabindingshorthand.6. WelearnedabouttheworkingoftheAngularcomponentlifecycleandhowwecanuse
theOnInitlifecyclehooktoinitializethecomponentaftertheinputandoutputhavebeenprocessedforthefirsttime.
www.EBooksWorld.ir
FilteringtasksThisisthelastbuildingblockofthischapter.Wehavealreadylearnedalotaboutbuildingbasiccomponentsandhowtocomposethemtogetherinordertoformlargercomponents.Inthepreviousbuildingblock,wecreatedgenericUIcomponentsthatcouldbeusedinothercomponents.Inthistopic,wewillusethetogglebuttoncomponentnotonlytocreateafilterforourtasklist,butalsotoimprovethewaywereceiveandstoretasksbyusingdataservices.
Let'scontinuewithanotherrefactoringexercise.Sofar,wehavestoredourtasklistdatadirectlywithinthetask-listcomponent,butlet'schangethathereanduseaservicethatwillprovidetasksforus.
Ourservicewillstillnotuseadatabase,butwe'llgetthetaskdataoutofourcomponent.Inordertousetheservice,we'remakinguseofAngular'sdependencyinjectionforthefirsttime.
Let'screateanewfilecalledtask-list-service.jswithinthelib/task-listfolderofourapplication:
//Classeswhichwe'dliketoprovidefordependencyinjection
//needtobeannotatedusingthisdecorator
import{Injectable}from'@angular/core';
@Injectable()
exportclassTaskListService{
constructor(){
this.tasks=[
{title:'Task1',done:false},
{title:'Task2',done:false},
{title:'Task3',done:true},
{title:'Task4',done:false}
];
}
}
We'vemovedallourtaskdataintothenewlycreatedservice.Inordertomakeourserviceclassinjectable,weneedtodecorateitwiththe@Injectableannotation.
Let'sapplysomechangestoourtask-listcomponentandmodifythetask-list.jsfilewithinthetask-listfolder.Themodifiedcodeinthefileishighlightedinthefollowingcodeexcerpt:
import{...,Inject}from'@angular/core';
//Thedummytaskservicewherewegetourtasksfrom
import{TaskListService}from'./task-list-service';
...
//WealsoneedaToggleUIcomponenttoprovideafilter
import{Toggle}from'../ui/toggle/toggle';
www.EBooksWorld.ir
@Component({
...
//SettheTaskListServiceashostprovider
providers:[TaskListService],
//Specifyalldirectives/componentsthatareusedintheview
directives:[Task,EnterTask,Toggle]
})
exportclassTaskList{
//InjecttheTaskListServiceandsetourfilterdata
constructor(@Inject(TaskListService)taskListService){
this.taskListService=taskListService;
this.taskFilterList=['all','open','done'];
this.selectedTaskFilter='all';
}
//Methodthatreturnsafilteredlistoftasksbasedonthe
//selectedtaskfilterstring.
getFilteredTasks(){
returnthis.taskListService.tasks?
this.taskListService.tasks.filter((task)=>{
if(this.selectedTaskFilter==='all'){
returntrue;
}elseif(this.selectedTaskFilter==='open'){
return!task.done;
}else{
returntask.done;
}
}):[];
}
//Methodtoaddataskfromtheview
addTask(title){
this.taskListService.tasks.push({
title,
done:false
});
}
}
Intheimportsectionofourmodule,we'regoingtoimportthetasklistservice.WewillusedependencyinjectiontoreceiveaninstanceoftheTaskListServiceclasswithinourcomponentconstructor.Forthis,we'lluseanewannotation,whichletsusspecifythetypewe'dliketoinject.TheInjectdecoratorneedstobeimportedfromtheAngularcoremoduleinordertousethe@Injectannotation.Ifyoutakealookatourconstructor,you'llfindthatwe'reusingthe@Injectannotationtheretospecifywhatinstancetypewe'dliketoinject.
Inadditiontothe@Injectannotationontheconstructor,weneedonelastthingtomaketheinjectionwork.WeneedtoregisterTaskListServiceasaproviderwithintheproviderspropertyofour@Componentannotation.
NowwegettheTaskListServiceinjectedwhenthedirectiveisconstructed,andwecanstoreareferencetoitinsideaninstancefield.
Withintheconstructorofthecomponent,wealsowanttostorealistofstatesthetaskstatus
www.EBooksWorld.ir
filtercanhave.Thislistwillalsoserveasinputforourtogglebuttonlist.Ifyourecalltheinputpropertiesonourtogglebutton,wehaveabuttonListinputthatacceptsalistofbuttonlabels.Tostorethecurrentlyselectedfiltertype,weuseaninstancefieldcalledselectedTaskFilter.
Thelastpiecethatweneedtoaddtoourtask-listcomponentisthemethod,getFilteredTasks.Wenolongerneedtostorethetasklistdirectlywithinaninstancefield,andtasksshouldonlybereceivedwithinthecomponentusingthismethod.ThelogicinsidethemethodcheckstheselectedTaskFilterpropertyandreturnsafilteredlistthatmeetsthiscondition.
Sincewewanttousethetogglebuttoncomponentthatwe'vecreatedwithintheprevioustopictocreateafilterbuttonlist,wewillneedtoimportthetogglecomponentwithintheimportsectionandalsoaddtheToggleclasstoourdirectivesproperty.Nowwecanusethetogglecomponentwithinthetemplateofourtask-listcomponent.
Okay,that'sallwearegoingtochangeinourcomponentimplementation.Wewanttochangeourviewtemplatethoughtousethefilteredtasklistcomingfromthedataserviceandshowatogglebuttonlisttoactivatethedifferentfiltertypes.Let'sopenthetemplatefile,task-list.html,insidethetask-listfolderandmodifyitwiththefollowingcontent:
<ngc-toggle[buttonList]="taskFilterList"
[(selectedButton)]="selectedTaskFilter">
</ngc-toggle>
<ngc-enter-task(taskEntered)="addTask($event)">
</ngc-enter-task>
<ngc-task*ngFor="lettaskofgetFilteredTasks()"
[task]="task"></ngc-task>
Sincewe'veaddedthetogglecomponentwithinthedirectivespropertyofourtask-listcomponent,wecanuseitnowwithinourviewtemplate.WebindtheinputpropertybuttonListtotaskFilterListthatwestorewithinourtask-listcomponent.Also,we'reusingtwo-waydatabindingtobindtheselectedButtoninputpropertyofthetogglebuttonlisttotheselectedTaskFilterinstancefieldofthetasklist.Thisway,wecannotonlyupdatetheselectedtaskfilerfromourtask-listcomponentprogrammatically,butalsoallowausertochangethevalueusingthetogglebuttonlist.
NowweonlyneedtomakeasmallchangetotheNgFordirectivethatrepeatsourtaskelementswithinthetasklist.Sinceweneedtoaccessthetasksofthetask-listcomponentwiththegetFilteredTasksmethodnow,wealsoneedtousethatmethodwithinourrepeaterexpression.
That'sitalready,congratulations!You'vesuccessfullyaddedafilteringmechanismtoyourtasklistbyreusingthetogglecomponentthatwecreatedintheprevioustopic.Youcannowstartyourliveserver(usingthelive-servercommand)andshouldseeafullyfunctionaltasklistwhereyoucanenternewtasksandalsofilterthetasklist:
www.EBooksWorld.ir
Screenshotofthetasklistwiththenewlyaddedtogglebuttoncomponentforfilteringthetaskstate
www.EBooksWorld.ir
SummaryInthischapter,youlearnedalotofnewconceptsonbuildingUI-component-basedapplicationswithAngular.Also,webuiltthecorecomponentofourtaskmanagementapplication,whichisthetasklistitself.Youlearnedabouttheconceptofinputandoutputpropertiesandhowtousethemtobuildtwo-waydatabinding.
WealsocoveredthebasicsoftheAngularcomponentlifecycleandhowtouselifecyclehookstoexecutepostinitializationsteps.
Asthelaststep,weintegratedatogglebuttonlistcomponentwithinourtasklisttofilterthetaskstates.Werefactoredourtask-listcomponenttouseaserviceinordertoobtaintaskdata.Forthis,weusedAngular'sdependencyinjection.
www.EBooksWorld.ir
Chapter3.ComposingwithComponentsInthischapter,wewillgoonestepfurtherinstructuringourapplicationandworkingonthelayoutandarchitecturethatservesasthebaseforourtaskmanagementsystem.Besidesintroducingnewcomponentsandcreatinglargercompositionswiththeexistingcomponents,we'llalsolookatthewaythatwedealwithdata.Sofar,we'veobtainedtaskdatasynchronouslyfromtheTaskListServicethatwecreatedinthepreviouschapter.However,inreal-worldscenarios,thiswouldrarelybethecase.Inarealapplication,dataismostlyretrievedinanasynchronousform.Usually,weacquiredatathroughaRESTfulwebservice,andweuseXMLHttpRequestortherecentlystandardizedfetchAPI.However,aswe'retryingtobuildacutting-edgeapplication,wewillgoonestepfurther.Inthischapter,we'lllookathowwecanrestructureourapplicationtodealwithobservabledatastructuresusingRxJS—afunctionalandreactiveprogramminglibrarythatisusedinAngular.
Inthischapter,wewilllookatthefollowingtopics:
RestructuringourapplicationtodealwithobservabledatastructuresThebasicsofRxJSanditsoperatorsinordertobuildareactivedatamodelUsingpurecomponentsinAngularUsingChangeDetectionStrategy.OnPushforpurecomponentsUsingcontentprojectionpointsand@ContentChildrentocreateaTabcomponentCreatingasimplenavigationcomponentInjectingparentcomponentsandestablishingdirectcomponentcommunicationCombininginternalandexternalcontenttocreateaflexiblecomponentAPI
www.EBooksWorld.ir
Data–FaketorealStartingwiththischapter,weareswitchingtoadocument-baseddatabasetostoreourtasksandprojectdata.Asadatastore,weusethePouchDBproject,whichisanin-browserdatabasethatisdesignedtorunwithIndexedDBandvariousfallbackstrategies.PouchDBisdesignedsimilarlytoApacheCouchDB,anditcanevenbesynchronizedwithit.
Inordertoprovideaqualityexperienceforyouwhileyoubuildyourapplication,it'simportantthatweworkinreal-lifeconditions.ThismeansthatweshoulduseasynchronousdatainourcomponentsandnotrelyonasimpleJavaScriptarrayofdata.Inordertomakethisassmoothaspossible,thewholedatalayerisalreadysetupforyou,andyoudon'tneedtoworryabouttheinternalstoomuch.Ofcourseifyou'restillinterested,I'mnotholdingyoubackfromexploringthesourcecodethatisinthedata-accessfolder.
www.EBooksWorld.ir
ReactiveprogrammingwithobservabledatastructuresSofar,weusedsimplearraydatastructuresinthetasklistthatwecreated.Thisisnotreallywhatwe'llfindinreal-worldscenarios.Inrealapplications,wehavetodealwithasynchronousdataandthechangesofthedatathatneedstobesynchronizedbetweenusers.Therequirementsformodernapplicationssometimesevengofurtherandalsoprovideviewupdatesonthechangeddatainrealtime.Aswe'rebuildingamoderntaskmanagementsystemhere,weshouldtrytokeepupwiththeserequirements.
Bothofthese,handlingasynchronousdataandhandlingreal-timedataupdates,requireamajorredesignofthedataflowinourapplication.Usingobservabledatastructures,weenableourapplicationtomasterthechallengesofasynchronousdatawhereweneedtoreacttochange.
Handlingdatainapplicationsbehavesverysimilarlytostreams.Youtakeinput,transformit,combineit,mergeit,andfinally,writeitintooutput.Insystemssuchasthis,it'salsoverylikelythatinputisinacontinuousformandsometimesevenofinfiniteduration.Justtakealivefeedasanexample;thistypeofdataflowscontinuously,andthedataalsoflowsinfinitely.Functionalandreactiveprogrammingareparadigmstohelpusdealwiththiskindofdatainacleanerway.
Asimpleobservablesubscriptionwithvalueemissionandatransformation
www.EBooksWorld.ir
Angular2isreactiveatitsverycoreandthewholeofthechangedetectionandbindingsarebuiltusingareactivearchitecture.Theinputandoutputofcomponents,whichwe'velearnedaboutinthepreviouschapter,isnothingbutadataflowthatisestablishedusingareactiveevent-drivenapproach.AngularusesRxJS,afunctionalandreactiveprogramminglibraryforJavaScript,toimplementthisdataflow.Infact,theEventEmitter,whichwe'veusedtosendcustomeventsfromwithinourcomponents,isjustawrapperaroundanRxJSobservable.
Reactiveandfunctionalprogrammingisexactlywhatwearelookingfortoredesignourapplicationinordertohandleasynchronousdataanddatachanges.AswealreadyhaveRxJSathandfromtheproductiondependencyofAngular,let'suseittoestablishacontinuousdataflowfromourdatasourceintoourapplication.TheDataProviderservicethatispresentinthedata-accessfolderofourprojectprovidesanicewrapperaroundourdatastoreusingRxJS.Aswewillusethisserviceinourwholeapplication,wecandirectlyprovideittothebootstrapinthebootstrap.jsfile,asfollows:
//ImportAngularbootstrapfunction
import{bootstrap}from'@angular/platform-browser-dynamic';
import{DataProvider}from'../data-access/data-provider';
//Importourmainappcomponent
import{App}from'./app';
bootstrap(App,[
DataProvider
]);
AsasecondargumenttothebootstrapfunctionofAngular,wecanprovideapplication-leveldependencies,whichwillbeavailableforinjectioninallcomponentsanddirectives.
Let'snowusetheDataProviderserviceasanabstractiontoobtaindatafromthePouchDBdatastoreandcreateanewserviceresponsibletoprovideprojectdata.
WewillcreateanewProjectServiceclassonthelib/project/project-service/project-service.jspath,asfollows:
import{Injectable,Inject}from'@angular/core';
import{ReplaySubject}from'rxjs/Rx';
import{DataProvider}from'../../../data-access/data-provider';
@Injectable()
exportclassProjectService{
constructor(@Inject(DataProvider)dataProvider){
…
}
}
Lookingattheimportsectionofournewmodule,youcanseethatweimportthenecessarydependenciesfromtheAngularcoremodulefordependencyinjection.Ourserviceclassusesthe@Injectabledecoratorsothatwecanprovidethistotheinjectorsofcomponents.Wealso
www.EBooksWorld.ir
injecttheDataProviderserviceintotheconstructorofournewly-createdservice.
TheReplaySubjectclass,whichweimportfromtheRxJSlibrary,isusedtomakeourservicereactive.AsubjectintheRxJSworldisbothanobserveraswellasanobservable.Itcanobservesomethingforchangesandthenemitfurtherontoallitssubscribers.Youcanthinkofasubjectlikeaproxy,whereitsitsinthemiddlebetweenasourceforchangesandagroupofobservers.Wheneverthesourceemitschanges,thesubjectwillnotifyallsubscribersaboutthesechanges.
Now,theReplaySubjectclassisaspecialkindofsubjectthatallowsyoutoreplayabufferofchangeswhennewsubscribersgetadded.Thisisespeciallyusefulifyoualwaysneedtoprovidesomeinitialdatatosubscribers.Imagineourdata,whichwe'dliketogetpropagatedintotheUI.Wewanttoimmediatelygettheinitialdatawhenwesubscribetoourserviceandthengoingforward,wealsowanttogetnotifiedaboutchanges.UsingaReplySubjectclass,whichisbufferingjustonechange,suitsthisuse-caseperfectly.
Let'slookatthefollowingfigure,whichillustratesthebehaviorofReplaySubject:
www.EBooksWorld.ir
AsourceconnectedtoanobserverusingaReplaySubjectclass,whichbuffersthemostrecentvalueandemitsonsubscription
Intheprecedingfigure,youcanseethatwe'reconnectingaReplaySubjectclasstoasourcethatisemittingvaluechangesovertime.Aftertwoemissions,anobserversubscribestoourReplaySubjectclass.ReplaySubjectwillthenreplayallbufferedchangestothenewsubscriberasiftheseeventsjustoccurred.Inthisexample,weuseareplaybufferlengthofone.Onsubsequentvalueemissions,thesewillbedirectlyre-emittedtothesubscribersoftheReplaySubjectclass.
Let'sgobacktoourProjectServiceclassandaddsomelogictotheconstructorfunctioninordertoemitprojectdatausingaReplaySubjectclass.
Wewillstartoffwithsomememberfieldinitialization,forwhichwe'regoingtoneedto
www.EBooksWorld.ir
implementthelogic:
this.dataProvider=dataProvider;
this.projects=[];
//We'reexposingareplaysubjectthatwillemiteventswhenever
//theprojectslistchange
this.change=newReplaySubject(1);
NotethatwecreatedanewReplaySubjectclasswithabufferlengthofone,andassignittothememberfiledwiththenamechange.
WealsoassigntheDataProviderservice,whichwaspreviouslyinjectedintheconstructorparameters,tothedataProvidermemberfield.
Now,it'stimetomakeuseoftheDataProviderserviceinordertosubscribeforanychangeswithinourdatastore.ThisestablishesareactiveconnectiontoourdatathatisstoredinPouchDB:
//Settingupourfunctionalreactivesubscriptiontoreceive
//projectchangesfromthedatabase
this.projectsSubscription=this.dataProvider.getLiveChanges()
//Firstconvertthechangerecordstoactualdocuments
.map((change)=>change.doc)
//Filtersothatweonlyreceiveprojectdocuments
.filter((document)=>document.type==='project')
//Finallywesubscribetothechangeobserveranddealwith
//projectchangesinthefunctionparameter
.subscribe((changedProject)=>{
this.projects=this.projects.slice();
//Oneveryprojectchangeweneedtoupdateourprojectslist
constprojectIndex=this.projects.findIndex(
(project)=>project._id===changedProject._id
);
if(projectIndex===-1){
this.projects.push(changedProject);
}else{
this.projects.splice(projectIndex,1,changedProject);
}
//Emitaneventonourreplaysubject
this.change.next(this.projects);
});
TheobservablethatisreturnedbythegetLiveChanges()functionemitsdatainourdatastoreaschanges.Inadditiontothis,thiswillalsoemitanyfuturechangesthatareappliedtoourstoreafterwe'vereceivedtheinitialdata.Youcanimagineapersistentconnectiontothedatabase,andwheneveradocumentisupdatedinthedatabase,ourobserverwillreceivethischangeasavalue.
Observablesprovidealargeamountofso-calledoperatorsthatallowyoutotransformthedatastreamthatoriginatedattheobservable.YoumightalreadyknowaboutsomeofthesefunctionaloperatorsfromtheECMAScript5arrayextrafunctions,suchasmapandfilter.
www.EBooksWorld.ir
Usingoperators,youcanmodelawholetransformationflowuntilyoufinallysubscribetothedata.
Aswereceivechangedobjectsfromthedatastore,wefirstneedtotransformthemintodocumentobjects.Thisisfairlyeasybecauseeachchangeobjectcontainsadocpropertythatactuallyholdsthewholedataofthechangeddocumentforwhichwehavereceivedanupdate.Usingthemapfunction,wecantransformthechangedobjectsintoprojectobjectsbeforewereturnthembackintothedataflow:
.map((change)=>change.doc)
DataProviderwillprovideuswithdataforallthedocumentsinthestore.Asweareonlyinterestedinprojectdataatthemoment,wealsoapplyafilterthatfiltersoutallthedocumentsthatarenotoftheprojecttype:
.filter((document)=>document.type==='project')
Finally,wecansubscribetothetransformedstream,asfollows:
.subscribe((changedProject)=>{})
Thesubscribeoperatoriswhereweterminateourobservationpath.Thisislikeanendpointatwhichwesitandobserve.Insideoursubscribefunction,welistenforprojectdocumentupdatesandincorporatethemintotheprojectsmemberpropertyofourAppcomponent.Thisincludesnotonlyaddingthenewprojectsthatwereaddedtothedocumentstore,butitalsoincludesupdatingexistingprojects.Eachprojectcontainsauniqueidentifierthatcanbeaccessedbythe_idproperty.Thisallowsustofindtherightactioneasily.
Afterupdatingouractualviewoftheprojectsandstoringthelistofprojectsintheprojectsmemberfield,wecanemittheupdatedlistusingourReplaySubjectclass:
this.change.next(this.projects);
OurProjectServiceclassisnowreadytobeused,andapplicationscomponentsthatneedtoobtainprojectdatacansubscribetotheexposedReplaySubjectclassinordertoreactonthesedatachanges.
Let'srefactorourAppcomponentinlib/app.jsandgetridofthefakeTaskListServicethatwe'veusedsofar:
import{Component,ViewEncapsulation,Inject}from'@angular/core';
import{ProjectService}from'./project/project-service/project-service';
importtemplatefrom'./app.html!text';
@Component({
selector:'ngc-app',
template,
encapsulation:ViewEncapsulation.None,
providers:[ProjectService]
})
www.EBooksWorld.ir
exportclassApp{
constructor(@Inject(ProjectService)projectService){
this.projectService=projectService;
this.projects=[];
//Settingupourfunctionalreactivesubscriptiontoreceive
//projectchanges
this.projectsSubscription=projectService.change
//Wesubscribetothechangeobserverofourservice
.subscribe((projects)=>{
this.projects=projects;
});
}
//Ifthiscomponentgetsdestroyed,weneedtorememberto
//cleanuptheprojectsubscription
ngOnDestroy(){
this.projectsSubscription.unsubscribe();
}
}
InourAppcomponent,wenowobtainalistofprojectsusingthechangedobservableonourProjectServiceclass.UsingareactivesubscriptiontoReplaySubjectontheservice,wemakesurethatourprojectslistthatisstoredintheAppcomponentwillalwaysbeupdatedafteranychanges.
WeusetheOnDestroylifecyclehooktounsubscribefromtheProjectServicechangeobservable.Thisisanecessarymanualstepifyouliketodoproperhousekeepinginyourapplication.Dependingonthesource,forgettingtounsubscribecouldleadtomemoryleaks.
Withtheprecedingcode,wealreadyestablishedthebaseforareactivedataarchitecture.Weobserveourdatastoreforchangesandouruserinterfacewillreactonthem:
Thisfigureshowsthereactivedataflowend-to-endfromourdata-storeintotheview
www.EBooksWorld.ir
ImmutabilityImmutabledatawasoriginallyacoreconceptoffunctionalprogramming.Thistopicwillnotcoverimmutabledatainmuchdepth,butitwillexplainthecoreconceptsothatwecantalkabouthowtoapplythisconcepttoAngularcomponents.
Immutabledatastructuresforceyoutocreateafullcopyofthedatathatyouwanttomodifybeforeyoucandothis.You'llneveroperateonthedatadirectlybutonacopyofthissamedata.Thishasmanybenefitsoverstandardmutabledata,themostobviousprobablybeingtheapplicationstate.Whenyoualwaysoperateonnewcopiesofdata,there'snochancethatyou'remessingupthedatathatyouactuallydidn'twanttomodify.
Let'stakethissimpleexample,whichillustratestheissuesobjectreferencescancause:
constlist=[1,2,3];
console.log(list===list.reverse());//true
Althoughthisseemsoddatfirst,itactuallymakessensethattheoutputofthisexampleistrue.Array.reverse()isamutableoperation,anditwillmodifytheinnardsofthearray.TheactualreferencewillstaythesamebecauseJavaScriptwillnotcreateacopyofthearraytoreverseit.Althoughtechnicallythismakesalotofsense,thisisnotwhatweexpectedinthefirstplacewhenwelookatthiscode.
Wecaneasilychangethisexampletoanimmutableprocedurebycreatingacopyofthearraybeforewereverseit:
constlist=[1,2,3];
console.log(list===list.slice().reverse());//false
Theissuewithreferencesisthattheycancausealotofunexpectedside-effects.Also,ifwecomebacktoourencapsulationtopicfromChapter1,Component-BasedUserInterfaces,theyarecompletelyagainsttheconceptofencapsulation.Althoughwemightthinkthatitwouldbesafetopasscomplexdatatypesintoacapsule,it'sactuallynot.Aswe'redealingwithreferenceshere,thedatacanstillbemodifiedfromtheoutside,andourcapsulewillnothavethecompleteownership.Considerthefollowingexample:
classSum{
constructor(data){
this.data=data;
this.data.sum=data.a+data.b;
}
getSum(){
returnthis.data.sum;
}
}
constdata={a:5,b:8};
varsum=newSum(data);
console.log(sum.getSum());//13
www.EBooksWorld.ir
console.log(data.sum);//13
EvenifweonlywantedtostorethedatainternallyinourSumclass,wewouldhavecreatedanunwantedside-effectofreferencingandmodifyingthedataobjectthat'soutsidetheinstance.Multiplesuminstanceswouldalsosharethesamedatafromoutsideandcausemoreside-effects.Asadeveloper,you'velearnedtotreatobjectreferencesright,buttheystillcancausealotofproblems.
Wedon'thavetheseproblemswithimmutabledata,whichcanbeillustratedeasilywithprimitivedatatypesinJavaScript.Primitivedatatypesdon'tusereferences,andtheyareimmutablebydesign:
letoriginalString='Hellothere!';
letmodifiedString=originalString.replace(/e/g,3);
console.log(originalString);//Hellothere!
console.log(modifiedString);//H3lloth3r3!
There'snowaywecanmodifyaninstanceofastring.Everymodificationthatweperformonastringwillgenerateanewstring,andthispreventstheunwantedside-effects.
So,whydowestillhaveobjectreferenceswithinprogramminglanguages,eventhoughtheycausesomanyissues?Whyaren'tweperformingalltheseoperationsonimmutabledata,andwhyareweonlydealingwithvaluesratherthanobjectreferences?
Ofcourse,imperativedatastructuresalsocomewiththeirbenefits,anditalwaysdependsonthecontextifimmutabledatabringsvalue.
Oneofthemainreasonsthatisoftenusedagainstimmutabledataisbadperformance.Ofcourse,itcostssomeperformanceifweneedtocreatetonsofcopiesofourdataeverytimewewanttomodifyit.However,therearegreatoptimizationtechniques,whichfullyeliminatetheperformanceissuesthatwewouldusuallyexpectfromimmutabledatastructures.Usingatreedatastructurethatallowsinternalstructuralsharing,copiesofthedatawillbesharedinternally.Thisallowsveryefficientmemorymanagement,whichinsomesituations,evenoutperformsmutabledatastructures.IcanhighlyrecommendthepaperbyChrisOkasakiaboutPurelyFunctionalDataStructuresifyouwouldliketoreadmoreaboutperformanceinimmutabledatastructures.
Tip
JavaScriptdoesnotsupportimmutabledatastructuresoutofthebox.However,youcanuselibraries,suchasImmutable.jsbyFacebook,whichprovideyouwithaneasyAPItodealwithimmutabledata.Immutable.jsevenimplementsstructuralsharingandmakesitaperfectpowertoolifyoudecidetobuildonanimmutablearchitectureinyourapplication.
Aswitheveryparadigm,thereareprosandcons,anddependingonthecontext,oneconceptmayfitbetterthananotherone.Inourapplication,wewon'tuseimmutabledatastructuresthatareprovidedbythird-partylibraries,butwe'llborrowsomeofthebenefitsthatyougetfrom
www.EBooksWorld.ir
immutabledatabythefollowingimmutableidioms:
It'smucheasiertoreasonaboutimmutabledata:Youcanalwaystellwhyyourdataisinagivenstatebecauseyouknowtheexacttransformationpath.Thismaysoundirrelevant,butinpractice,thisisahugebenefitnotonlyforhumanstowritecode,butalsoforcompilersandinterpreterstooptimizeit.Usingimmutableobjectsmakeschangedetectionmuchfaster:Ifwerelyonimmutablepatternstotreatourdata,wecanrelyonobjectreferencecheckstodetectchange.Wenolongerneedtoperformcomplexdataanalysisandcomparisonfordirtychecking,andcanfullyrelyoncheckingreferences.Wehavetheguaranteethatobjectpropertiesdon'tchangewithouttheobjectidentitychangingaswell.ThismakeschangedetectionaseasyasoldObject===newObject.
www.EBooksWorld.ir
PurecomponentsTheideaofa"pure"componentisthatitswholestateisrepresentedbyitsinputs,whereallinputsareimmutable.Thisiseffectivelyastatelesscomponent,butadditionally,alltheinputsareimmutable.
Iliketocallsuchcomponents"pure"becausetheirbehaviorcanbecomparedtotheconceptofpurefunctionsinfunctionalprogramming.Apurefunctionisafunctionwhichhasthefollowingproperties:
ItdoesnotrelyonanystateoutsideofthefunctionscopeItalwaysbehavethesameifinputparametersdon'tchangeItneverchangesanystateoutsidethefunctionscope(side-effect)
Withpurecomponents,wehaveasimpleguarantee.Apurecomponentwillneverchangewithoutitsinputparametersbeingchanged.Wecanignoreacomponentanditssubcomponentsinchangedetectionuntiloneofthecomponentinputschanges.Stickingtothisideaaboutcomponentsgivesusseveraladvantages.
It'sveryeasytoreasonaboutpurecomponentsandtheirbehaviorcanbepredictedveryeasily.Let'slookatasimpleillustrationofacomponenttreewhereweusepurecomponents:
www.EBooksWorld.ir
Acomponenttreewithimmutablecomponents
Ifwehavetheguaranteethateachcomponentinourtreehasastablestateuntilanimmutableinputpropertychanges,wecansafelyignorechangedetectionthatwouldusuallybetriggeredbyAngular.Theonlywaythatsuchacomponentcouldchangeisifaninputofthecomponentchanges.Let'ssaythatthere'saneventthatcausestheArootcomponenttochangetheinputbindingvalueoftheBcomponent,whichwillchangethevalueofabindingontheEcomponent.Thisevent,andtheresultingprocedure,wouldmarkacertainpathinourcomponenttreetobecheckedbychangedetection:
www.EBooksWorld.ir
Figurethatshowsamarkedpathforchangedetection(inblack)with"pure"components.
Althoughthestateoftherootcomponentchanged,whichalsochangedinputpropertiesofthesubcomponentsontwolevels,weonlyneedtobeconcernedaboutagivenpathwhenthinkingaboutpossiblechangesinthesystem.Purecomponentsgiveusthepromisethattheywillnotchangeiftheirinputswillnot.Immutabilityplaysabigrolehere.Imaginethatyou'rebindingamutableobjecttothecomponent,B,andtheAcomponentwouldchangeapropertyofthisobject.Asweuseobjectreferencesandmutableobjects,thepropertywouldalsobechangedfortheBcomponent.However,there'snowayfortheBcomponenttonoticethischange,aswecan'ttrackwhoknowsaboutourobjectwithinthecomponenttree.Basically,we'dneedtogobacktoregulardirtycheckingofthewholetreeagain.
Byknowingthatallourcomponentsarepureandthattheirinputsareimmutable,wecantellAngulartodisablechangedetectionuntilaninputpropertyvaluechanges.Thismakesourcomponenttreeveryefficient,andAngularcanoptimizechangedetectioneffectively.Whenthinkingaboutlargecomponenttrees,thiscanmakethedifferencebetweenastunningly-fastapplicationandaslowone.
www.EBooksWorld.ir
ThechangedetectionofAngularisveryflexible,andeachcomponentgetsitsownchangedetector.WecanconfigurethechangedetectionofacomponentbyspecifyingthechangeDetectionpropertyofthecomponentdecorator.
UsingChangeDetectionStrategy,wecanchoosefromalistofstrategiesthatapplyforthechangedetectionofourcomponent.InordertotellAngularthatourcomponentshouldonlybecheckedifanimmutableinputwaschanged,wecanusetheOnPushstrategy,whichisdesignedexactlyforthispurpose.
Let'stakealookatthedifferentconfigurationpossibilitiesofcomponentchange-detectionstrategiesandsomepossibleusecases:
Change-detectionstrategy
Description
CheckAlways
ThisstrategytellsAngulartocheckthiscomponentduringeverychange-detectioncycle,andthisisobviouslythemostexpensivestrategy.Thisistheonlystrategythatguaranteesthatacomponentgetscheckedforchangesoneverypossibleapplicationstatechange.Ifwe'renotworkingwithstatelessorimmutablecomponents,orweareusinganinconsistentdataflowwithinourapplication,thisisstillthemostreliablechange-detectionmethod.Changedetectionwillbeexecutedoneverybrowsereventthatrunswithinthezoneofthiscomponent.
Detached
ThisstrategytellsAngulartocompletelydetachacomponentsubtreefromchangedetection.Thisstrategycanbeusedtocreateamanualchange-detectionmechanism.
OnPush
ThisstrategytellsAngularthatagivencomponentsubtreewillonlychangeunderoneofthefollowingconditions:
OneoftheinputpropertieschangeswherechangesneedtobeimmutableAneventbindingwithinthecomponentsubtreeisreceivinganevent
Default ThisstrategysimplyevaluatestoCheckAlways
www.EBooksWorld.ir
PurifyingourtasklistIntheprevioustopic,wechangedourmainapplicationcomponenttouseRxJSObservablesinordertogetnotifiedaboutthedatachangesinourdatastore.
WealsolookedintothebasicsofusingimmutabledatastructuresandthatAngularcanbeconfiguredtoassumecomponentchangesonlyoccurwhencomponentinputchanges("pure"components).Aswe'dliketogettheperformancebenefitsthatresultfromthisoptimization,let'srefactorourtasklistcomponenttomakeuseofthis.
Inthepreviouschapter,webuiltourTaskListcomponentanddirectlyplacedthetaskdatainthecomponent.Wethenrefactoredourcodesothatwecouldplacethetaskdataintoaserviceanduseinjectiontoobtaindata.
Now,we'rerebuildingourTaskListcomponenttomakeit"pure"andonlydependentonitsinputproperties.Aswe'llmakesurethatthedataflowingintothecomponentisalwaysimmutable,wecanusetheOnPushchange-detectionstrategyonourrefactoredcomponent.Thiswillcertainlygiveourtasklistaperformanceboost.
Equallyimportantasperformance,arethestructuralbenefitsthatwegetfromusingpurecomponents.A"pure"componentdoesnotchangeanydatadirectlybecauseit'snotallowedtomodifyapplicationstate.Instead,itusesoutputpropertiestoemiteventsonchangeddata.Thisallowsourparentcomponenttoreacttotheseeventsandperformthenecessarystepstohandlethechanges.Asaresultofthis,theparentcomponentwillpossiblychangetheinputpropertiesofthepurecomponent.Thiswilltriggerchangedetectionandeffectivelychangethestateofthepurecomponent.
Whatmightsoundabitovercomplicatedatfirstisactuallyanimmensebenefittothestructureofourapplication.Thisallowsustoreasonaboutourcomponentwithhighconfidence.Theunidirectionaldataflowaswellasstatelessnaturemakesiteasytounderstand,examine,andtestourcomponents.Also,theloosenatureofinputsandoutputsmakesourcomponentextremelyportable.Wecandecideonaparentcomponent,whatdatawe'dliketorunintoourcomponent,andhowwe'dliketohandlechanges.
Let'stakealookatourTaskListcomponentandhowwechangeittoconformtoourconceptof"pure"components:
import{Component,ViewEncapsulation,Input,Output,EventEmitter,
ChangeDetectionStrategy}from'@angular/core';
importtemplatefrom'./task-list.html!text';
import{Task}from'./task/task';
import{EnterTask}from'./enter-task/enter-task';
import{Toggle}from'../ui/toggle/toggle';
@Component({
selector:'ngc-task-list',
host:{
www.EBooksWorld.ir
class:'task-list'
},
template,
encapsulation:ViewEncapsulation.None,
directives:[Task,EnterTask,Toggle],
changeDetection:ChangeDetectionStrategy.OnPush
})
exportclassTaskList{
@Input()tasks;
//Eventemitterforemittinganeventoncethetasklisthas
//beenchanged
@Output()tasksUpdated=newEventEmitter();
constructor(){
this.taskFilterList=['all','open','done'];
this.selectedTaskFilter='all';
}
ngOnChanges(changes){
if(changes.tasks){
this.taskFilterChange(this.selectedTaskFilter);
}
}
taskFilterChange(filter){
this.selectedTaskFilter=filter;
this.filteredTasks=this.tasks?this.tasks.filter((task)=>{
if(filter==='all'){
returntrue;
}elseif(filter==='open'){
return!task.done;
}else{
returntask.done;
}
}):[];
}
//Functiontoaddanewtask
addTask(title){
consttasks=this.tasks.slice();
tasks.push({created:+newDate(),title,done:null});
this.tasksUpdated.next(tasks);
}
}
Alltheoperationsinourtasklistcomponentarenowimmutable.Weneverdirectlymodifyourtask'sdatathatwaspassedinasinput,butratherwecreatenewtaskdataarraystoperformmutableoperations.
Fromwhatwe'velearnedfromtheprevioussection,thiseffectivelymakesourcomponenta"pure"component.Thiscomponentitselfisonlyrelyingonitsinputandmakesourcomponentveryeasytoreasonabout.
You'veprobablynoticedthatwe'vealsoconfiguredthechange-detectionstrategyofour
www.EBooksWorld.ir
component.Aswehavea"pure"componentnow,wecanconfigureourchange-detectionstrategyaccordinglytosavesomeperformance:
@Component({
selector:'ngc-task-list',
…
changeDetection:ChangeDetectionStrategy.OnPush
})
Aswe'rerenderingaTaskcomponentforeachdatarecordinourtasklist,weshouldalsocheckwhatwecanchangethereinordertoroundthisout.
Let'slookatthechangesinourTaskcomponent:
import{Component,Input,Output,EventEmitter,ViewEncapsulation,HostBinding,
ChangeDetectionStrategy}from'@angular/core';
importtemplatefrom'./task.html!text';
import{Checkbox}from'../../ui/checkbox/checkbox';
@Component({
selector:'ngc-task',
host:{
class:'task'
},
template,
encapsulation:ViewEncapsulation.None,
directives:[Checkbox],
changeDetection:ChangeDetectionStrategy.OnPush
})
exportclassTask{
@Input()task;
//Weareusinganoutputtonotifyourparentaboutupdates
@Output()taskUpdated=newEventEmitter();
@HostBinding('class.task--done')
getdone(){
returnthis.task&&this.task.done;
}
//Weusethisfunctiontoupdatethecheckedstateofourtask
markDone(checked){
this.taskUpdated.next({
title:this.task.title,
done:checked?+newDate():null
});
}
}
WealsousetheOnPushstrategyforourTaskcomponent,andwecandothisbecausewealsohaveapurecomponenthere.Thiscomponentonlydependsonitsinputs.Bothinputsexpectnativevalues(aStringfortitleandaBooleanfordone),whichactuallymakesthemimmutablebynature.ChangesonthetaskwillbecommunicatedusingthetaskUpdatedoutputproperty.
www.EBooksWorld.ir
Now,thisisagoodtimetothinkaboutwheretoplaceourtasklistintheapplication.Aswe'rewritingataskmanagementsystemthatgivesouruserstheabilitytomanagetaskswithinprojects,weneedtohaveacontainerthatwillencapsulatetheconcernsofprojects.WecreateanewProjectcomponent,onthepathlib/project/project.js,whichwilldisplayprojectdetailsandrenderstheTaskListcomponentasasubcomponent:
import{Component,ViewEncapsulation,Input,Output,EventEmitter,
ChangeDetectionStrategy}from'@angular/core';
importtemplatefrom'./project.html!text';
import{TaskList}from'../task-list/task-list';
@Component({
selector:'ngc-project',
host:{
class:'project'
},
template,
encapsulation:ViewEncapsulation.None,
directives:[TaskList],
changeDetection:ChangeDetectionStrategy.OnPush
})
exportclassProject{
@Input()title;
@Input()description;
@Input()tasks;
@Output()projectUpdated=newEventEmitter();
//Thisfunctionshouldbecalledifthetasklistofthe
//projectwasupdated
updateTasks(tasks){
this.projectUpdated.next({
title:this.title,
description:this.description,
tasks
});
}
}
Again,wemakethestateofthiscomponentdependentonlyonitsimmutableinputs,andweusetheOnPushstrategyinordertogetthepositiveperformanceimplicationsofusingapurecomponent.It'salsoimportanttonotethattheupdateTasksfunctionactsassomesortofadelegatefromourTaskListcomponent.WhenweupdateataskinsidetheTaskListcomponent,wecatchtheeventintheprojecttemplateandcalltheupdateTasksfunctionwiththenewupdatedtasklist.Fromhere,we'rejustemittingtheupdatedprojectdatawiththenewtasklistfurtherupinthecomponenttree.
Let'salsotakealookatthetemplateofourProjectcomponentquicklytounderstandthewiringbehindthiscomponent:
<divclass="project__l-header">
<h2class="project__title">{{title}}</h2>
<p>{{description}}</p>
</div>
www.EBooksWorld.ir
<ngc-task-list[tasks]="tasks"
(tasksUpdated)="updateTasks($event)">
</ngc-task-list>
Thebindinglogicinthetemplatetellsushowthewholedataflowwithourpurifiedcomponentswork.WhiletheProjectcomponentitselfreceivesthelistoftasksasinput,itdirectlyforwardsthisdatatothetasksinputoftheTaskListcomponent.IftheTaskListcomponentfiresatasksUpdatedevent,we'recallingtheupdateTasksmethodontheProjectcomponent,whichinfactjustemitsaprojectUpdatedeventagain.
www.EBooksWorld.ir
RecapTherefactoringofourtasklistisnowcompleted,andweappliedourknowledgeaboutimmutablecomponentsandobservabledatastructurestogainsomeperformancewinsinthisstructure.Therewon'tbeunnecessarydirtycheckingonourTaskcomponentanymorebecauseweswitchedtotheOnPushchange-detectionstrategy.
WehavealsoreducedthecomplexityoftheTaskListandTaskcomponentsalot,andit'snowfareasiertoreasonaboutthesecomponentsandtheirstate.
Afurtherbenefitofthisrefactoringisthegreatencapsulationlevelthatweachievedusingimmutableinputs.OurTaskListcomponentisnotrelyingonanytaskcontainerasaproject.Wecanalsopassitalistoftasksacrossalltheprojects,anditcanstillworkasexpected.
www.EBooksWorld.ir
CompositionusingcontentprojectionInthissection,wewillcreateatabbedinterfaceforourProjectcomponentthatwillhelpusfurtherorganizethestructureofourapplicationuserinterface.InordertocreateaTabscomponent,we'lllookatcontentprojectionandcontentchildinjectionusingobservablequerylists.
Inputandoutputpropertiesaregreattoestablishencapsulation,andthisisamainpropertyofpropercomposition.However,sometimestherequirementsarenottoonlypassdatabuttoalsopasscontentfromtheoutsideofacomponentintothecomponent.InShadowDOM,thisisdoneusingso-calledslots.InAngularcomponents,wecancreatecontentprojectionpointsusingthe<ng-content>element.
Let'slookatasimplecontentprojectionexamplethathelpsusunderstandwhatthisisgoodfor:
@Component({
selector:'child',
template:`
<article>
<header>
<h1><ng-contentselect="[data-header]"></ng-content></h1>
</header>
<ng-content></ng-content>
</article>
`
})
exportclassChild{}
@Component({
selector:'app',
template:`
<child>
<headerdata-header>Contentprojectionisgreat</header>
<p>Insertcontentinacontrolledmanner</p>
</child>
`,
directives:[Child]
})
exportclassApp{}
LookingattheAppcomponentinthisexample,wecanseethatwe'veputelementsinsidetheactual<child>element.Usually,thiscontentwouldbeignoredandremovedbyAngularbeforeitrenderstheChildcomponentwithitstemplate.
However,whenusingcontentprojection,theelementsthatareplacedinsideourcomponentHTMLelementcanbesuckedintotheChildcomponent.Thisiswhatcontentprojectionisallabout.ContentprojectionisactuallyverysimilartotheconceptoftransclusionfromAngular1.
www.EBooksWorld.ir
AllthatweneedtodoinordertoenablecontentprojectionwithinaChildcomponentistoplacea<ng-content>elementwithinitstemplate.Inthisway,wespecifyatwhatlocationsinourcomponenttemplatewewanttoinsertthecontentthatissuckedinfromtheparentcomponent.
Additionally,wecanusetheselectattributeonthe<ng-content>elementtosetaCSS-likeselector.Thisselectorwillbeusedtoonlysuckinspecificelements,whichmatchthisselector.Inthisway,youcanhavemultipleinsertionpointsthatcoverdifferentcontentrequirements.
Elementsfromthecomponentelementcanonlybeinsertedonce,andthecontentprojectionworksbygoingthroughallthe<ng-content>elementsinsequentialorderbyprojectmatchingelements.Ifyouhavemultiplecompetingcontentprojectionpointsinyourtemplatethatareinterestedinthesameelements,thefirstonewillactuallywin.
www.EBooksWorld.ir
CreatingatabbedinterfacecomponentLet'sintroduceanewUIcomponentinouruifolderintheprojectthatwillprovideuswithatabbedinterfacethatwecanuseforcomposition.Weusewhatwelearnedaboutcontentprojectioninordertomakethiscomponentreusable.
We'llactuallycreatetwocomponents,oneforTabs,whichitselfholdsindividualTabcomponents.
First,let'screatethecomponentclasswithinanewtabs/tabfolderinafilecalledtab.js:
import{Component,Input,ViewEncapsulation,HostBinding}from'@angular/core';
importtemplatefrom'./tab.html!text';
@Component({
selector:'ngc-tab',
host:{
class:'tabs__tab'
},
template,
encapsulation:ViewEncapsulation.None
})
exportclassTab{
@Input()name;
@HostBinding('class.tabs__tab--active')active=false;
}
TheonlystatethatwestoreinourTabcomponentiswhetherthetabisactiveornot.Thenamethatisdisplayedonthetabwillbeavailablethroughaninputproperty.
Weuseaclasspropertybindingtomakeatabvisible,basedontheactiveflagwesetaclass;withoutthis,ourtabsarehidden.
Let'stakealookatthetab.htmltemplatefileofthiscomponent:
<ng-content></ng-content>
Thisisitalready?Actually,yesitis!TheTabcomponentisonlyresponsibleforthestorageofitsnameandactivestate,aswellastheinsertionofthehostelementcontentinthecontentprojectionpoint.There'snoadditionaltemplatingthatisneeded.
Now,we'llmoveonelevelupandcreatetheTabscomponentthatwillberesponsibleforgroupingalltheTabcomponents.Aswewon'tincludeTabcomponentsdirectlywhenwewanttocreateatabbedinterfacebutusetheTabscomponentinstead,thisneedstoforwardcontentthatweputintotheTabshostelement.Let'slookathowwecanachievethis.
Inthetabsfolder,wewillcreateatabs.jsfilethatcontainsourTabscomponentcode,asfollows:
www.EBooksWorld.ir
import{Component,ViewEncapsulation,ContentChildren}from'@angular/core';
importtemplatefrom'./tabs.html!text';
//WerelyontheTabcomponent
import{Tab}from'./tab/tab';
@Component({
selector:'ngc-tabs',
host:{
class:'tabs'
},
template,
encapsulation:ViewEncapsulation.None,
directives:[Tab]
})
exportclassTabs{
//Thisqueriesthecontentinside<ng-content>andstoresa
//querylistthatwillbeupdatedifthecontentchanges
@ContentChildren(Tab)tabs;
//ThengAfterContentInitlifecyclehookwillbecalledoncethe
//contentinside<ng-content>wasinitialized
ngAfterContentInit(){
this.activateTab(this.tabs.first);
}
activateTab(tab){
//Toactivateatabwefirstconvertthelivelisttoan
//arrayanddeactivatealltabsbeforewesetthenew
//tabactive
this.tabs.toArray().forEach((t)=>t.active=false);
tab.active=true;
}
}
Let'sobservewhat'shappeninghere.Weusedanew@ContentChildrenannotation,inordertoqueryourinsertedcontentfordirectivesthatmatchthetypethatwepasstothedecorator.ThetabspropertywillcontainanobjectoftheQueryListtype,whichisanobservablelisttypethatwillbeupdatedifthecontentprojectionchanges.Youneedtorememberthatcontentprojectionisadynamicprocess,asthecontentinthehostelementcanactuallychange,forexample,usingtheNgFororNgIfdirectives.
WeusetheAfterContentInitlifecyclehook,whichwe'vealreadybrieflydiscussedintheCustomUIelementssectionofChapter2,Ready,Set,Go!ThislifecyclehookiscalledafterAngularhascompletedcontentprojectiononthecomponent.OnlythendowehavetheguaranteethatourQueryListobjectwillbeinitialized,andwecanstartworkingwithchilddirectivesthatwereprojectedascontent.
TheactivateTabfunctionwillsettheTabcomponent'sactiveflag,deactivatinganypreviousactivetab.AstheobservableQueryListobjectisnotanativearray,wefirstneedtoconvertitusingtoArray()beforewestartworkingwithit.
Let'snowlookatthetemplateoftheTabscomponentthatwecreatedinafilecalledtabs.html
www.EBooksWorld.ir
inthetabsdirectory:
<ulclass="tabs__tab-list">
<li*ngFor="lettaboftabs">
<buttonclass="tabs__tab-button"
[class.tabs__tab-button--active]="tab.active"
(click)="activateTab(tab)">{{tab.name}}</button>
</li>
</ul>
<divclass="tabs__l-container">
<ng-contentselect="ngc-tab"></ng-content>
</div>
ThestructureofourTabscomponentisasfollows.
Firstwerenderallthetabbuttonsinanunorderedlist.Aftertheunorderedlist,wehaveatabscontainerthatwillcontainallourTabcomponentsthatareinsertedusingcontentprojectionandthe<ng-content>element.NotethattheselectorthatweuseisactuallytheselectorweuseforourTabcomponent.TabsthatarenotactivewillnotbevisiblebecausewecontrolthisusingCSSonourTabcomponentclassattributebinding(refertotheTabcomponentcode).
Thisisallthatweneedtocreateaflexibleandwell-encapsulatedtabbedinterfacecomponent.NowwecangoaheadandusethiscomponentinourProjectcomponenttoprovideasegregationofourprojectdetailinformation.
Wewillcreatethreetabsfornow,wherethefirstonewillembedourtasklist.Wewilladdressthecontentoftheothertwotabsinalaterchapter.
Let'smodifyourProjectcomponenttemplateintheproject.htmlfileasafirststep.
InsteadofincludingourTaskListcomponentdirectly,wenowusetheTabsandTabcomponentstonestthetasklistintoourtabbedinterface:
<ngc-tabs>
<ngc-tabname="Tasks">
<ngc-task-list[tasks]="tasks"
(tasksUpdated)="updateTasks($event)">
</ngc-task-list>
</ngc-tab>
<ngc-tabname="Comments"></ngc-tab>
<ngc-tabname="Activities"></ngc-tab>
</ngc-tabs>
Youshouldhavenoticedbynowthatweareactuallynestingtwocomponentswithinthistemplatecodeusingcontentprojection,asfollows:
First,theTabscomponentusescontentprojectiontoselectallthe<ngc-tab>elements.Astheseelementshappentobecomponentstoo(ourTabcomponentwillattachtoelementswiththisname),theywillberecognizedassuchwithintheTabscomponentoncetheyareinserted.
www.EBooksWorld.ir
Inthe<ngc-tab>element,wethennestourTaskListcomponent.IfwegobacktoourTaskcomponenttemplate,whichwillbeattachedtoelementswiththenamengc-tab,wewillhaveagenericprojectionpointthatinsertsanycontentthatispresentinthehostelement.OurtasklistwilleffectivelybepassedthroughtheTabscomponentintotheTabcomponent.
www.EBooksWorld.ir
RecapInthistopic,wecreatedaveryhandytabbedinterfacecomponentthatwecanusetosegregateouruserinterfaceandprovideafocusedcontextforourusers.Weusedcontentprojectionpointsusingthe<ng-content>elements.Wealsolearnedhowtoaccessinsertedcomponentsusingthe@ContentChildrenannotationandobservablelistsusingtheQueryListtype.
www.EBooksWorld.ir
MixingprojectedwithgeneratedcontentOurtaskmanagementapplicationsupportsthelistingofmultipleprojectswhereausercanmanagetasks.Weneedtoprovideanavigationthatenablesausertobrowsethroughtheexistingprojects.Asprojectscomefromourdatastore,thenavigationwillneedtobegenerateddynamically.However,wealsowouldliketohavethepossibilityofspecifyingsomenavigationitemswithinournavigation,asstaticcontentwithpuretemplating.
Inthissection,wewillcreateasimplenavigationcomponent,whichwillusecontentprojection,sothatwecanaddstaticnavigationitems.Atthesametime,navigationitemscanbegeneratedfromdataandmixedwiththestaticcontent-basednavigationitems.
Let'sfirsttakealookatanillustrationofthearchitecturaldesignandcompositionthatwe'regoingtousetoimplementournavigation:
Anillustrationofthenavigationcomponenttreeandinteractions
We'lluseanintermediatecomponentbetweentheNavigationandNavigationItemcomponents.TheNavigationSectioncomponentisresponsibleforthedivisionofmultipleitemsintoasection.Thenavigationsectionsalsohaveatitlethatwillbedisplayedontopoftheitemlist.
www.EBooksWorld.ir
TheillustrationshowstwoNavigationSectioncomponents,wheretheleftoneusespurecontentprojectiontocreateitems,aswehavelearnedintheprevioussection.TherightNavigationSectioncomponentgeneratesitemsusinganinputdatastructure,whichisalistofnavigationitemmodels.
AswehaveintermediatecomponentsbetweentheNavigationandNavigationItemscomponents(wecanonlyhaveoneselectednavigation),wealsoestablishadirectcommunicationpathbetweenthem.Wewillachievethisusingancestorcomponentinjection.
Note
Thearchitecturalapproachforthisnavigationisjustoneofmanypossibleapproaches.Wechoosethisapproachinordertoshowyouhowwecaneasilymixcontentprojectionandgeneratedcontent.Inthisexample,wedon'tusetheAngularroutertoprovidenavigationstateandroutemapping.Thiswillbepartofalaterchapter.
Let'sstartbottomupwiththeNavigationItemcomponentandcreateanewnavigation-item.jsfileinanewly-creatednavigation/navigation-section/navigation-itempath:
//Werelyonthenavigationcomponenttoknowifweareactive
import{Navigation}from'../../navigation';
@Component({
selector:'ngc-navigation-item'
})
exportclassNavigationItem{
@Input()title;
@Input()link;
constructor(@Inject(Navigation)navigation){
this.navigation=navigation;
}
//Here,wearedelegatingtothenavigationcomponenttoseeif
//weareactiveornot
isActive(){
returnthis.navigation.isItemActive(this);
}
//Ifthislinkisactivatedweneedtotellthenavigationcomponent
onActivate(){
this.navigation.activateLink(this.link);
}
}
FromtheNavigationItemcomponentcode,wecanseethatwe'redirectlycommunicatingwiththeNavigationancestorcomponent.WecansimplyinjecttheNavigationComponent,asthisisachildofthecomponent.AstheNavigationitemswillneverexistwithoutaNavigationcomponent,weshouldbefinewiththisdirectdependency.
Let'smoveontotheNavigationSectioncomponentthatistheintermediatecomponent
www.EBooksWorld.ir
betweentheNavigationcomponentandtheitemsandisresponsibleforthegroupingofitemstogether.
Wewillcreateafilecallednavigation-section.jsinthenavigation/navigation-sectionpath:
@Component({
selector:'ngc-navigation-section',
directives:[NavigationItem]
})
exportclassNavigationSection{
@Input()title;
@Input()items;
}
Holdon!That'sallthatthisneeds?Didn'twesaythatwewantourNavigationSectioncomponenttoalsoberesponsiblefornotonlyprovidingawaytoinsertcontent,butalsoacceptingdatainordertogenerateitems?Well,thisistrue.However,thisisactuallypuretemplatinglogic,anditcanbedonesolelywithinthetemplatefileofthecomponent.AllthatweneedisanoptionalinputwithitemdatathatwewillusetogeneratetheNavigationItemcomponents.
Let'screatetheviewtemplateforthiscomponentinafilenamednavigation-section.html:
<h2class="navigation-section__title">{{title}}</h2>
<ulclass="navigation-section__list">
<ng-contentselect="ngc-navigation-item"></ng-content>
<ngc-navigation-item*ngFor="letitemofitems"
[title]="item.title"
[link]="item.link"></ngc-navigation-item>
</ul>
Well,thiswasn'trocketscience,wasit?However,thisshowsthegreatflexibilitythatwehaveinAngularcomponenttemplates:
Firstly,wecreateacontentprojectionpointthatselectsalltheelementsfromthehostelementthatmatchthenamengc-navigation-item.ThismeansthattheNavigationItemcomponentscanbeplacedoutsidethecomponentinaverystaticfashiontocreate,forexample,staticlinks.AsthemodelpropertiesofnavigationitemsaredirectlyexposedasbindableattributesontheNavigationItemelement,wecanalsoplacethemstaticallyintoapureHTMLtemplatewithregularDOMattributes.Secondly,wecanusetheNgFordirectivetogeneratetheNavigationItemcomponentsinsidethecomponent.Here,wejustiterateoverthelistofnavigationitemmodelsthatactsasanoptionalinputtoourcomponent.Weusebindingsintheitemsmodelsothatwecanevenpropagatechangeintoournavigationitemcomponents.
Asafinalstep,wecreatetheNavigationcomponentitselfthatusescontentprojectionpointssothatwecanmanagetheNavigationSectioncomponentfromoutside.Wecreateafilecallednavigation.jstowritethecodeoftheNavigationcomponent:
www.EBooksWorld.ir
import{NavigationSection}from'./navigation-section/navigation-section';
@Component({
selector:'ngc-navigation',
directives:[NavigationSection]
})
exportclassNavigation{
@Input()activeLink;
//Checksifagivennavigationitemiscurrentlyactivebyits
//link.Thisfunctionwillbecalledbynavigationitemchild
//components.
isItemActive(item){
returnitem.link===this.activeLink;
}
//Ifalinkwantstobeactivatedwithinthenavigation,this
//functionneedstobecalled.Thiswaychildnavigationitem
//componentscanactivatethemselves.
activateLink(link){
this.activeLink=link;
this.activeLinkChange.next(this.activeLink);
}
}
IntheNavigationcomponent,westorethestateofwhichnavigationitemisactivated.Thisisalsoprovidedasinputtothecomponentsothatwecansettheactivatedlinkwithaninputbindingfromoutside.TheisItemActiveandactivateLinkfunctionsaretheretomonitorandchangethestateoftheactiveitemwithinthenavigation.ThesefunctionsaredirectlyusedwithintheNavigationItemcomponents,whichinjectthenavigationusingancestorcomponentinjection.
Now,theonlybitthatismissingistoincludeournavigationinthemainapplication.Forthis,wewilledittheapp.htmltemplateofthecomponent:
<divclass="app">
<divclass="app__l-side">
<ngc-navigation
[activeLink]="getSelectedProjectLink()"
(activeLinkChange)="selectProjectByLink($event)">
<ngc-navigation-section
title="Projects"
[items]="getProjectNavigationItems()">
</ngc-navigation-section>
</ngc-na
vigation>
</div>
<divclass="app__l-main">
…
</div>
</div>
Here,weonlyusethegenerativeapproachtowriteaNavigationSectioncomponentwhere
www.EBooksWorld.ir
weactuallypassalistofnavigationitemmodelsintothenavigationcomponent.ThislistisgeneratedbythegetProjectNavigationItemsfunctiononourmainapplicationcomponentusingtheavailableprojectsfromourobservabledatastructure:
Ascreenshotofthenewly-createdprojectnavigation
www.EBooksWorld.ir
SummaryInthischapter,welearnedabouthowwecanprofitfromconcepts,suchasreactiveprogramming,observabledatastructures,andimmutableobjects,inordertomakeourapplicationperformbetter,andmostimportantly,simpleandeasytoreasonabout.
Wetouchedonthedifferentchange-detectionstrategiesandlearnedhowtousetheOnPushstrategytogainbetterperformanceincombinationwithimmutabledata.
Webuiltatabbeduserinterfacecomponentthatwecanreusewhereverweneedit,andwelearnedabouttheconceptofcontentprojection.Wealsocreatedasimplenavigationcomponenttreethatusesamixofcontentprojectionandgeneration.ThenavigationitemsalsodirectlycommunicatewiththeirancestorNavigationcomponent,inordertomanagetheirstateusingancestorcomponentinjection.
Asweswitchedtoareactiveapproachtomanagedatawithinourapplication,Iwantyoutoperformalittleexperiment.Ifyou'vedownloadedthefinalchapter'scode,goaheadandopentwobrowserwindowsthatpointtothetaskmanagementapplication.Youwillbeamazedthatwealreadyhaveaworkingreal-timesynchronizationinplacethatallowsustoworkinbothbrowserwindowsandhavebothofthemupdatedatthesametime.Thishasallbeenmadepossiblebecauseofthereactiveandfunctionalwaythatweworkwithdatainourcomponents.
www.EBooksWorld.ir
Chapter4.NoComments,Please!Duringthecourseofthischapter,wewillcreatereusablecomponentstoenablecommentingnotonlyonprojects,butalsoonanyotherentitywithinourapplication.We'llbuildourcommentingsysteminawaythatitwillallowustoplaceitanywherewe'dlikeforouruserstoputcomments.Inordertoprovideouruserswithafeaturetoeditexistingcommentsandalsoaseamlessauthoringexperience,we'llcreateaneditorUIcomponentthatcouldbeusedtomakearbitrarycontentwithinourapplicationeditable.
Discussingsecurityandproperusermanagementinthischapterisstilloutofscope,butwe'regoingtocreateadummyuserservicethatwillhelpussimulatealogged-inuser.Thisservicewillbeusedbythecommentingsystem,andwe'llrefactorourexistingcomponenttomakeuseofittoo.
We'llcoverthefollowingtopicsinthischapter:
Usingcontenteditabletocreateanin-placeeditorUsing@HostBindingand@HostListenertobindcomponentmemberstohostelementpropertiesandeventsCommunicatingdirectlywithviewchildrenusingthe@ViewChildannotationPerformingDOMoperationsbyinjectingandusingElementRefCreatingadummyuserserviceandusingthe@InjectableannotationtoserveitasadependencyinjectionproviderApplyingcustomactionsoncomponentinputchanges,usingtheOnChangeslifecyclehookCreatingasimplepipetoformatrelativetimeintervalsusingtheMoment.jslibrary
www.EBooksWorld.ir
OneeditortorulethemallSincewewillbeprocessingalotofuserinputwithinourapplication,it'scrucialtoprovideaniceauthoringexperiencetoourusers.Withinthecommentingsystemwe'reabouttocreateinthischapter,weneedawaythroughwhichuserscouldeditexistingcomments,aswellasaddnewcomments.Wecoulduseregulartextareainputandworkwithdialogboxestoeditcomments,butthisseemstooold-fashionedforamodernuserinterface,whichwe'regoingtobuild,anddoesnotreallyprovideagreatuserexperience.Whatwe'relookingforisawaytoeditstuffinplace.Thecommentingsystemwillnotonlybenefitfromsuchanin-placeeditor,butitwillalsohelpuscreatetheeditorcomponentinsuchawaythatwecanuseitforanycontentwithinourapplicationthatwe'dliketomakeeditable.
Inordertobuildourin-placeeditor,we'regoingtousethecontenteditableAPIthatwillenableausertomodifythecontentwithintheHTMLelementsdirectlyinthesitedocument.
ThefollowingexampleillustrateshowwecanusethecontenteditableattributetomakeHTMLelementseditable:
<h1contenteditable>I'maneditabletitle</h1>
<p>Ican'tbeedited</p>
RuntheprecedingexampleonablankHTMLpageandclickontheh1text.Youwillseethattheelementhasbecomeeditableandyoucantypetomodifyitscontent.
Gettingnotifiedaboutchangeswithineditableelementsisfairlyeasy.There'saninputeventemittedoneveryDOMelementthatiseditable,andthiswillallowustoreacttoachangeeasily:
consth1=document.querySelector('h1');
h1.addEventListener('input',(event)=>console.log(h1.textContent);
Withthisexample,wehavealreadycreatedanaiveimplementationofanin-placeeditorwherewe'reabletomonitorchangesappliedbytheuser.Withinthistopic,we'llusethisstandardtechnologytobuildareusablecomponentthatwecanusewhereverwewanttomakethingseditable.
www.EBooksWorld.ir
CreatinganeditorcomponentFirst,let'screateanewfoldernamededitorwithinouruifolder.Inthisfolder,we'regoingtocreateanewcomponentfilenamededitor.js:
import{Component,ViewChild,Input,Output,ViewEncapsulation,EventEmitter,
HostBinding,HostListener}from'@angular/core';
importtemplatefrom'./editor.html!text';
@Component({
selector:'ngc-editor',
host:{
class:'editor'
},
template,
encapsulation:ViewEncapsulation.None
})
exportclassEditor{
//Usingviewchildreferencewithlocalviewvariablename
@ViewChild('editableContentElement')editableContentElement;
//Contentthatwillbeeditedanddisplayed
@Input()content;
//Creatingahostelementclassattributebindingfromthe
//editModeproperty
@Input()@HostBinding('class.editor--edit-mode')editMode;
@Input()showControls;
@Output()editSaved=newEventEmitter();
@Output()editableInput=newEventEmitter();
//Weneedtomakesuretoreflecttooureditableelementif
//contentgetsupdatedfromoutside
ngOnChanges(){
if(this.editableContentElement&&this.content){
this.setEditableContent(this.content);
}
}
ngAfterViewInit(){
this.setEditableContent(this.content);
}
//Thisreturnsthecontentofourcontenteditable
getEditableContent(){
returnthis.editableContentElement.nativeElement.textContent;
}
//Thissetsthecontentofourcontenteditable
setEditableContent(content){
this.editableContentElement.nativeElement.textContent=
content;
}
//Thisannotationwillcreateaclickeventlisteneronthe
//hostelementthatwillinvoketheunderlyingmethod
@HostListener('click')
www.EBooksWorld.ir
focusEditableContent(){
if(this.editMode){
this.editableContentElement.nativeElement.focus();
}
}
//Methodthatwillbeinvokedifoureditableelementis
//changed
onInput(){
//EmitaeditableInputeventwiththeeditedcontent
this.editableInput.next(this.getEditableContent());
}
//Onsavewereflectthecontentoftheeditableelementinto
//thecontentfieldandemitanevent
save(){
this.editSaved.next(this.getEditableContent());
this.setEditableContent(this.content);
//SettingeditModetofalsetoswitchtheeditorbackto
//viewingmode
this.editMode=false;
}
//Cancelingtheeditwillnotreflecttheeditedcontentand
//switchbacktoviewingmode
cancel(){
this.setEditableContent(this.content);
this.editableInput.next(this.getEditableContent());
this.editMode=false;
}
//Theeditmethodwillinitializetheeditableelementandset
//thecomponentintoeditmode
edit(){
this.editMode=true;
}
}
Okay,that'squitealotofnewcode.Let'sdissectthedifferentpartsoftheEditorcomponentandgothrougheachpartstepbystep.
WithinourEditorcomponent,we'llneedtointeractwiththenativeDOMelement,whichiseditable.Theeasiestandalsothesafestmethodtodothisistousethe@ViewChilddecoratorinordertoretrieveanelementwithalocalviewreference:
@ViewChild('editableContentElement')editableContentElement;
Inthepreviouschapter,welearnedaboutthe@ContentChildrenannotation,whichhelpsusobtainalistofallthechildcomponentswithincontentprojectionpoints.Ifwewouldliketodothesamewithregularviewchildren,[email protected]@ContentChildrensearchesforcomponentswithincontentprojectionpoints,@ViewChildrenhuntsfortheregularsub-treeofacomponent.
www.EBooksWorld.ir
Ifwewanttosearchthecomponentsub-treeforonesinglecomponent,wecanusethe@ViewChildannotation(pleasenotethat@ViewChildand@ViewChidrenaredifferent).
Queryannotation Description
@ViewChildren(selector)Willquerythecurrentcomponent'sviewforeitherdirectivesorcomponentsandreturnanobjectofthetypeQueryList.Iftheviewisdynamicallyupdated,thislistwillbeupdatedaswell.
@ViewChild(selector) Willqueryforonlythefirstmatchingcomponentordirectiveandreturnaninstanceofit.
Note
Aselectorcanbeeitheradirectiveorcomponenttype,orastringthatcontainsthenameofalocalviewvariable.Ifalocalviewvariablenameisprovided,Angularwillsearchfortheelementcontainingtheviewvariablereference.
Ifyouneedtocommunicatewithviewchildcomponentsdirectly,using@ViewChildand@ViewChildrenannotationsshouldbeyourpreferredway.
Tip
Sometimesyouneedtoruntheinitializationcodeonviewchildrenafteryourcomponentisinitialized.Insuchcases,youcanusetheAfterViewInitlifecyclehook.Whiletheviewchildpropertiesofyourcomponentclasswillstillbeundefinedwithintheconstructorofyourcomponent,theywillbepopulatedandinitializedaftertheAfterViewInitlifecyclecallback.
The@ViewChildand@ViewChildrendecoratorsaregreattoolstointeractwithinyourviewdirectly.Itdoesn'treallymatterwhetheryou'dliketointeractwithaDOMelementoracomponentinstance.BothusecasesarenicelycoveredusingthisdeclarativeAPI.
Let'smovebacktoourEditorcomponentcode.Thenextthingwe'regoingtolookintoarethecomponent'sinputfunctions:
@Input()content;
@Input()@HostBinding('class.editor--edit-mode')editMode;
@Input()showControls;
Thecontentinputpropertyisthemaininterfaceforinteractingwiththecomponentfromoutside.Usingpropertybindings,wecanhaveanypreexistingtextcontentsetupintheeditorcomponent.
TheeditModepropertyisaBooleanvaluethatcontrolswhethertheeditorisineditordisplay
www.EBooksWorld.ir
mode.Oureditorcomponentwilldependonthisflagtoknowwhethercontentshouldbeeditedornot.Thisallowsustoswitchfromread-onlymodetoeditmodeandbackinteractively.
Thoughaninputproperty,thisflagcanbecontrolledfromoutsidethecomponent.Atthesametime,itcanalsobeusedtocreatepropertybindingofahostelement.Specifically,wecanusetheflagtocreateaclassattributebindingtoaddorremovethemodifierclass,editor--edit-mode.Thisclassisusedtocontrolsomedifferencesinthevisualappearanceoftheeditorwhileineditmode.
Thelastofthethreeinputpropertiesinoureditorcomponent,showControls,controlswhethertheeditorshouldshowthecontrolfunctions.Therearethreecontrolsthatwillbeshownwhenthispropertyevaluatestoatruevalue:
Editbutton:Thiswillbeshownwhenthecomponentisindisplaymode,anditwillswitchthecomponenttoeditmodeusingtheeditModeflag.Savebutton:Thiswillbeshownonlyifthecomponentisineditmode.Thiscontrolwillsavethechangesappliedwithinthecurrenteditmodeandswitchthecomponentbacktodisplaymode.Cancelbutton:Thisisthesameasthesavebutton,andthiscontrolisshownonlywhenthecomponentisineditmode.Ifactivated,thecomponentwillswitchbacktodisplaymode,revertinganychangesthatyoumayhavemade.
Besidesourinputproperties,wealsoneedsomeoutputpropertiestonotifytheouterworldaboutthechangeswithinoureditor.Thefollowingpieceofcodehelpsusdothis:
@Output()editSaved=newEventEmitter();
@Output()editableInput=newEventEmitter();
TheeditSavedeventwillbeemittedoncetheeditedcontentissavedusingthesavebuttoncontrol.Also,it'llbebetterifaneventisemitteduponeveryinputchangewithinoureditablecontentelement.Forthis,weusedtheeditableInputoutputproperty.
Oureditorcomponentworksinasimpleway.Ifthecomponentisineditmode,itshowsanelementthatcanbeedited.However,oncetheeditorswitchesbacktodisplaymode,weseeadifferentelementthatcannotbeedited.ThevisibilityiscontrolledwiththemodifierclasssetbythehostelementpropertybindingtotheeditModeflag.
Angularhasnocontroloverthecontentwithinoureditableelement.WecontrolthiscontentmanuallybyusingnativeDOMoperations.Let'slookathowwedidthis.Firstofall,weneededtousedelegatestoaccesstheelement,sincewe'remostlikelygoingtochangehowwewillreadandwritetoandfromtheeditableelement.Weusedthefollowingtodothis:
getEditableContent(){
returnthis.editableContentElement.nativeElement.textContent;
}
setEditableContent(content){
www.EBooksWorld.ir
this.editableContentElement.nativeElement.textContent=
content;
}
Tip
NotethatweusedthenativeElementpropertyonoureditableContentElementfield,previouslysetbythe@ViewChilddecorator.
AngulardoesnotdirectlyprovideuswithaDOMelementreferencebutawrapperobjectofthetypeElementRef.It'sbasicallyawrapperaroundthenativeDOMelement,holdingadditionalinformationthatisrelevanttoAngular.
UsingthenativeElementaccessor,wecanobtainareferencetotheunderlyingDOMelement.
Note
TheElementRefwrapperplaysanimportantpartinAngular'splatform-agnosticarchitecture.ItallowsyoutorunAngularindifferentenvironments(forexample,nativemobile,webworkers,orothers).It'spartofanabstractionlayerbetweenthecomponentsandtheirviews.
Wealsoneededawaytosetthecontentoftheeditableelementbasedontheinputthatwewouldreceivefromthecontentinputproperty.WecouldusethelifecyclehookOnInit,whichwillbecalledonlyaftertheinputpropertiesarecheckeduponcomponentinitialization.However,thislifecyclehookfiresonlyonceaftertheinitialization,andweneededawaythatwouldhavehelpedusreacttosubsequentinputchangesofthecontentproperty.Havealookatthefollowingcodesnippet:
ngOnChanges(){
if(this.editableContentElement&&this.content){
this.setEditableContent(this.content);
}
}
TheOnChangeslifecyclehookisexactlywhatweneededhere.Withthis,onceachangeinthecontentinputpropertyisdetected(thisalsoincludesthefirstchangeaftertheinitialization),wecanreflectthechangedcontentontooureditableelement.
Nowwehavealreadyimplementedthereflectionofthecomponentcontentinputpropertyontotheeditablefield.Butwhatabouttheoppositedirection?Weneedtofindawaytoreflectthechangesinoureditableelementontoourcomponentcontentproperty.That'salsocloselyrelatedtotheactionsperformedonthecomponentusingtheavailablecontrolswithineditmode,whichareasfollows:
Inthesaveoperation:Here,wereflecttheeditedcontentfromtheeditableelementbacktothecomponent'scontentproperty.Inthecanceloperation:Here,weignorewhathasbeeneditedbytheuserwithintheeditableelementandsetitscontentbacktothevalueinthecomponent'scontent
www.EBooksWorld.ir
property:
Let'slookatthecodeforthosetwooperations:
save(){
this.editSaved.next(this.getEditableContent());
this.setEditableContent(this.content);
this.editMode=false;
}
cancel(){
this.setEditableContent(this.content);
this.editableInput.next(this.getEditableContent());
this.editMode=false;
}
Inadditiontothehighlightedcode,whichshowsthereflectionbetweenthecomponent'scontentpropertyandtheeditableelement,weemittedcertaineventsthatwouldhelpusnotifytheoutsideworldaboutthechanges.Inboththeoperations,wesettheeditModeflagtofalseaftercompletion.Thisensuresthatoureditorwillswitchtodisplaymodeafteranyoneoftheoperationsiscompleted.
Theeditmethodwillbecalledfromtheeditcontrolbuttonwhenthecomponentisindisplaymode.Theonlythingitdoesisthatitswitchesthecomponentbacktoeditmode:
edit(){
this.editMode=true;
}
Whateverwe'vediscussedthusfarinrelationtothecodeisgoodenoughforustosetupafullyfunctionalcomponent.However,thelastpartofthecode,whichwehaven'tdiscussedyet,relatestoensuringbetteraccessibilityofoureditor.Sinceoureditorcomponentisabitlargerthantheeditableelement,wealsowanttomakesurethataclickanywhereinsidetheeditorcomponentwillcausetheeditableelementtobefocused.Thefollowingcodemakesthishappen:
@HostListener('click')
focusEditableContent(){
if(this.editMode){
this.editableContentElement.nativeElement.focus();
}
}
Usingthe@HostListenerdecorator,weregisteredaneventbindingonourcomponentelementthatcalledthefocusEditableContentmethod.Insidethismethod,weusedthereferencetotheeditableDOMelementandtriggeredafocus.
Let'slookatthetemplateofourcomponentthatislocatedwithintheeditor.htmlfileinordertoseehowwecouldinteractwiththelogicwithinourcomponent:
<div(input)="onInput($event)"
www.EBooksWorld.ir
class="editor__editable-content"
contenteditable="true"
#editableContentElement></div>
<divclass="editor__output">{{content}}</div>
<div*ngIf="showControls&&!editMode"class="editor__controls">
<button(click)="edit()"class="editor__icon-edit"></button>
</div>
<div*ngIf="showControls&&editMode"class="editor__controls">
<button(click)="save()"class="editor__icon-save"></button>
<button(click)="cancel()"class="editor__icon-cancel"></button>
</div>
Thelogicwithintheeditorcomponenttemplateisquitestraightforward.Ifyou'vebeenfollowingthecomponentcode,you'llnowbeabletoidentifythedifferentelementsthatcomposethiscomponent'sview.
Thefirstelementwiththeeditor__editable-contentclassisoureditableelementthathasthecontenteditableattribute.Theuserwillbeabletotypeintothiselementwhentheeditorisineditmode.It'simportanttonotethatwe'veannotateditwithalocalviewvariablereference,#editableContentElement,whichwe'reusinginourviewchildqueries.
Thesecondelementwiththeeditor__outputclassisonlytodisplaytheeditorcontentandisonlyvisiblewhentheeditorisindisplaymode.ThevisibilityofboththeelementsiscontrolledusingCSS,basedontheeditor--edit-modemodifierclass,which,ifyourecallfromthecomponentclasscode,issetthroughhostpropertybindingbasedontheeditModeproperty.
ThethreecontrolbuttonsareshownusingtheNgIfdirectiveconditionally.TheshowControlsinputpropertyneedstobetrue,anddependingontheeditModeflag,thescreenwilleithershowtheeditbuttonorthesaveandthecancelbutton:
Screenshotofoureditorcomponentinaction
www.EBooksWorld.ir
RecapWithinthisbuildingblock,wehavecreatedanin-placeeditorwidget,whichwecanusetograbuserinputforanycontentwithinourapplication.Itallowsustoprovidetheuserwithcontextualeditingcapabilities,whichwillresultinagreatuserexperience.
Wehavealsolearnedaboutthefollowingtopics:
1. UsingcontenteditableHTML5attributetoenablein-placeediting.2. Using@[email protected]. UsingtheElementRefdependencytoperformnativeDOMoperations.4. Implementingthelogic,usingtheOnChangelifecyclehook,toreflectdatabetween
AngularandthecontentthatisnotinimmediatecontrolofAngular.
www.EBooksWorld.ir
BuildingacommentingsystemIntheprevioustopic,wecreatedaneditorcomponentthatwillsupportusersineditingcontentwithinourapplication.Here,we'regoingtocreateacommentingsystemthatwillenableuserstowritecommentsinvariousareasofourapplication.Thecommentingsystemwilluseoureditorcomponenttomakecommentseditable,andtherebyhelpuserscreatenewcomments:
Anillustrationofthecomponentsub-treeofacommentingsystem
Theprecedingdiagramillustratesthearchitectureofthecomponenttreewithinthecommentingsystemthatweareabouttocreate.
TheCommentscomponentwillberesponsibleforlistingalltheexistingcomments,aswellascreatingnewcomments.
www.EBooksWorld.ir
EachcommentitselfisencapsulatedintoaCommentcomponent.Commentcomponentsthemselvesuseaneditorthatenablesuserstoeditcommentsoncetheyarecreated.
TheEditorcomponent,whichwebuiltintheprevioustopic,isusedbytheCommentcomponentdirectly,toprovideaninputcontrolforaddingnewcomments.Thisallowsustoreusethefunctionalityofoureditorcomponenttocaptureuserinput.
TheEditorcomponentemitsaneditSavedeventonceeditablecontentissavedusingthecontrolbuttonsoftheeditor.IntheCommentcomponent,wewillcapturetheseeventsandpropagateaneweventupwardtoourCommentscomponent.There,wewilldothenecessaryupdatesbutthenagainemitaneweventtonotifyourparentaboutthechange.Inacompositionofcomponents,eachcomponentwillreactonchangeanddelegatetotheparentcomponentifnecessary.
www.EBooksWorld.ir
BuildingthecommentcomponentLet'sstartbuildingourcommentingsystembyfleshingouttheCommentcomponentfirst.Inadditiontothecommentitself,we'dliketodisplaytheuser'sprofilewhocommented,andofcourse,thetimeofthecomment.
Todisplaythetime,wewillmakeuseofrelativetimeformatting,asthiswillgiveourusersabetterfeeloftime.Relativetimeformattingdisplaystimestampsintheformat"5minutesago"or"1monthago",incontrasttoabsolutetimestamps,suchas"25.12.201518:00".UsingtheMoment.jslibrary,we'llcreateapipethatwecanusewithincomponenttemplatestoconverttimestampsanddatesintorelativetimeintervals.
Let'screateanewpipewithinanewfoldernamedpipes.Thepipeneedstobecreatedwithinafilenamedfrom-now.js,whichiscreatedunderthepipesfolder:
import{Pipe}from'@angular/core';
//WeusetheMoment.jslibrarytoconvertdatestorelativetimes
importMomentfrom'moment';
@Pipe({
//Specifyingthenametobeusedwithintemplates
name:'fromNow'
})
//Ourpipewilltransformdatesandtimestampstorelativetimes
//usingMoment.js
exportclassFromNowPipe{
//Thetransformmethodwillbecalledwhenthepipeisused
//withinatemplate
transform(value){
if(value&&(valueinstanceofDate||
typeofvalue==='number')){
returnnewMoment(value).fromNow();
}
}
}
Thispipecannowbeusedwithinthetemplatesofcomponentstoformattimestampsanddatesintorelativetimeintervals.
Let'susethispipeandtheEditorcomponentwecreatedintheprevioustopictocreateourCommentcomponent.Withinafilenamedcomment.html,whichislocatedwithinanewcommentfolderinthecommentsfolder,we'llcreatethetemplateforourCommentcomponent:
<divclass="comment__l-meta">
<divclass="comment__user-picture">
<img[attr.src]="user.pictureDataUri"src="">
</div>
<divclass="comment__user-name">{{user.name}}</div>
<divclass="comment__time">
{{time|fromNow}}
</div>
www.EBooksWorld.ir
</div>
<divclass="comment__l-main">
<divclass="comment__message">
<ngc-editor[content]="content"
[showControls]="true"
(editSaved)="onContentSaved($event)">
</ngc-editor>
</div>
</div>
Fromtheuserobject,wewillgettheuser'sprofileimageaswellastheusername.Todisplaythetimeofthecommentinarelativeformat,we'llusethefromNowpipethatwecreatedearlier.
Finally,wewillmakeuseofthein-placeeditorcomponenttodisplaythecontentofthecommentandmakeiteditableatthesametime.Wewillbindthecommentcontentpropertytothecontentinputpropertyoftheeditor.Atthesametime,wewilllistenfortheeditSavedeventoftheeditorandcalltheonContentSavedmethodonourcommentcomponentclass.Ifyoulookatourcomponentcodeagain,you'llnoticethatwearere-emittingtheeventwithinthemethodsothattheoutsideworldisalsonotifiedaboutthechangeinthecomment.
Let'stakealookatthecomponentclassthatwewillcreateinafilenamedcomment.js:
import{Component,Input,Output,ViewEncapsulation,EventEmitter}from
'@angular/core';
import{Editor}from'../../ui/editor/editor';
importtemplatefrom'./comment.html!text';
//WeuseourfromNowpipethatconvertstimestampstorelative
//times
import{FromNowPipe}from'../../pipes/from-now';
@Component({
selector:'ngc-comment',
host:{
class:'comment'
},
template,
encapsulation:ViewEncapsulation.None,
directives:[Editor],
pipes:[FromNowPipe]
})
exportclassComment{
//Thetimeofthecommentasatimestamp
@Input()time;
//Theuserobjectoftheuserwhocreatedthecomment
@Input()user;
//Thecommentcontent
@Input()content;
//Ifacommentwaseditedthiseventwillbeemitted
@Output()commentEdited=newEventEmitter();
onContentSaved(content){
this.commentEdited.next(content);
}
}
www.EBooksWorld.ir
Thecomponentcodeisprettystraightforward.Theonlynoticeabledifferencetoothercomponentswe'vecreatedsofaristhepipespropertywithinthecomponent'sannotation.Here,wespecifythatwe'dliketousetheFromNowPipeclassthatwe'vejustcreated.Pipesalwaysneedtobedeclaredwithinthecomponent;otherwise,theycan'tbeusedwithinthecomponent'stemplate.
Asinput,weexpectauserobjectthatispassedalongwiththeuserinputproperty.Thecontentinputpropertyshouldbefilledwiththeactualcommentasastring,whilethetimeinputpropertyshouldbesettoatimestampthatreflectstheactualtimeofthecomment.
WealsohaveanoutputpropertycalledcommentEdited,whichwewillusetonotifythechangesonthecomment.TheonEditSavedmethodwillbecalledbytheeventbindingonourEditorcomponent,whichwillthenemitaneventusingthecommentEditedoutputproperty.
www.EBooksWorld.ir
BuildingthecommentscomponentWenowhaveallthecomponentsreadyinordertofinishbuildingourcommentingsystem.ThelastmissingpieceofthepuzzleistheCommentscomponent,whichwilllistallthecommentsandprovideaneditortocreatenewcomments.
First,let'stakealookatthetemplateofourCommentscomponentthatwewillcreateinafilenamedcomments.htmlwithinafoldernamedcomments:
<divclass="comments__title">Addnewcomment</div>
<divclass="comments__add-comment-section">
<divclass="comments__add-comment-box">
<ngc-editor[editMode]="true"
[showControls]="false"></ngc-editor>
</div>
<button(click)="addNewComment()"
class="button">Addcomment</button>
</div>
<div*ngIf="comments?.length>0">
<divclass="comments__title">Allcomments</div>
<ulclass="comments__list">
<li*ngFor="letcommentofcomments">
<ngc-comment[content]="comment.content"
[time]="comment.time"
[user]="comment.user"
(commentEdited)="onCommentEdited(comment,$event)">
</ngc-comment>
</li>
</ul>
</div>
YoucanseethedirectusageofanEditorcomponentwithinthecomponent'stemplate.Weareusingthisin-placeeditortoprovideaninputcomponenttocreatenewcomments.Wecouldalsouseatextareahere,butwe'vedecidedtoreuseourEditorcomponent.WewillsettheeditModepropertytotruesoitwillbeinitializedineditmode.WewillalsosettheshowControlsinputtofalsebecausewedon'twanttheeditortobecomeautonomous.Wewillonlyuseitsin-placeeditingcapabilities,butcontrolitfromourCommentscomponent.
Toaddanewcomment,wewilluseabuttonthathasaclickeventbinding,whichcallstheaddNewCommentmethodonourcomponentclass.
Belowthesectionwhereuserscanaddnewcomments,wewillcreateanothersectionthatwilllistalltheexistingcomments.Ifnocommentsexist,wesimplydon'trenderthesection.WiththehelpoftheNgFordirective,wecoulddisplayalltheexistingcommentsandcreateaCommentcomponentforeachrepetition.WewillbindallthecommentdatapropertiestoourCommentcomponentandalsoaddaneventbindingtohandleupdatedcomments.
Let'screatethecomponentclasswithinanewfilenamedcomments.jsinthecommentsfolder:
www.EBooksWorld.ir
import{Component,Inject,Input,Output,ViewEncapsulation,ViewChild,
EventEmitter}from'@angular/core';
importtemplatefrom'./comments.html!text';
import{Editor}from'../ui/editor/editor';
import{Comment}from'./comment/comment';
import{UserService}from'../user/user-service/user-service';
@Component({
selector:'ngc-comments',
host:{
class:'comments'
},
template,
encapsulation:ViewEncapsulation.None,
directives:[Comment,Editor]
})
exportclassComments{
//Alistofcommentobjects
@Input()comments;
//Eventwhenthelistofcommentshavebeenupdated
@Output()commentsUpdated=newEventEmitter();
//Weareusinganeditorforaddingnewcommentsandcontrolit
//directlyusingareference
@ViewChild(Editor)newCommentEditor;
//We'reusingtheuserservicetoobtainthecurrentlylogged
//inuser
constructor(@Inject(UserService)userService){
this.userService=userService;
}
//Weuseinputchangetrackingtopreventdealingwith
//undefinedcommentlist
ngOnChanges(changes){
if(changes.comments&&
changes.comments.currentValue===undefined){
this.comments=[];
}
}
//AddinganewcommentfromthenewCommentContentfieldthatis
//boundtotheeditorcontent
addNewComment(){
constcomments=this.comments.slice();
comments.splice(0,0,{
user:this.userService.currentUser,
time:+newDate(),
content:this.newCommentEditor.getEditableContent()
});
//Emiteventsotheupdatedcommentlistcanbepersisted
//outsidethecomponent
this.commentsUpdated.next(comments);
//Weresetthecontentoftheeditor
this.newCommentEditor.setEditableContent('');
}
www.EBooksWorld.ir
//Thismethoddealswitheditedcomments
onCommentEdited(comment,content){
constcomments=this.comments.slice();
//Ifthecommentwaseditedwithezerolengthcontent,we
//willdeletethecommentfromthelist
if(content.length===0){
comments.splice(comments.indexOf(comment),1);
}else{
//Otherwisewe'rereplacingtheexistingcomment
comments.splice(comments.indexOf(comment),1,{
user:comment.user,
time:comment.time,
content
});
}
//Emiteventsotheupdatedcommentlistcanbepersisted
//outsidethecomponent
this.commentsUpdated.next(comments);
}
}
Let'sgothroughindividualcodepartsagainanddiscusswhateachofthemdoes.First,wedeclaredaninputpropertynamedcommentsinourcomponentclass:
@Input()comments;
Thecommentsinputpropertyisalistofcommentobjectsthatcontainsallofthedataassociatedwiththecomments.Thisincludestheuserwhoauthoredthecommentandthetimestamp,aswellasthecontentofthecomment.
Wealsoneedtobeabletoemitaneventonceacommentisaddedoranexistingcommentismodified.Forthispurpose,weusedanoutputpropertynamedcommentsUpdates:
@Output()commentsUpdated=newEventEmitter();
Onceanewcommentisaddedoranexistingoneismodified,wewillemitaneventfromthisoutputpropertywiththeupdatedlistofcomments.
TheEditorcomponentwe'regoingtousetoaddnewcommentswillnothaveitsowncontrolbuttons.WewillusetheshowControlsinputpropertytodisablethem.Instead,wewillcontroltheeditorfromourCommentscomponentdirectly.Therefore,weneedawaytocommunicatewiththeEditorcomponentwithinourcomponentclass.
[email protected],thistime,wedidnotreferenceaDOMelement,whichcontainsalocalviewvariablereference.Wedirectlypassedourcomponenttypeclasstothedecorator.AngularwillsearchforanyEditorcomponentswithinthecommentsviewandprovideuswithareferencetotheinstanceoftheeditor.Thisisshowninthefollowinglineofcode:
@ViewChild(Editor)newCommentEditor;
www.EBooksWorld.ir
SincetheCommentscomponentonlyhostsoneeditordirectlywithinthecomponenttemplate,wecanusethe@ViewChildannotationtoobtainareferencetoit.Usingthisreference,wecandirectlyinteractwiththechildcomponent.ThiswillallowustocontroltheeditordirectlyfromourCommentscomponent.
Let'smoveontothenextpartofthecode,whichistheCommentscomponentconstructor.Theonlythingwe'vedonehereisinjectauserservicethatwillprovideuswithawaytoobtaininformationofthecurrentlylogged-inuser.Asofnow,thisfunctionalityisonlymocked,andwewillreceiveinformationofadummyuser.WeneedthisinformationintheCommentscomponent,sinceweneedtoknowwhichuserhasactuallyenteredanewcomment:
constructor(@Inject(UserService)userService){
this.userService=userService;
}
Inthenextpartofthecode,wecontrolledhowweshouldreacttothechangesofthecommentsinputproperty.Actually,wewouldneverwantthelistofcommentstoremainundefined.Itshouldbeanemptylistincasetherearenocomments,buttheinputpropertycommentsshouldneverbeundefined.WecontrolledthisbyusingtheOnChangelifecyclehookandoverridingourcommentspropertyifitwassettoundefinedfromoutside:
ngOnChanges(changes){
if(changes.comments&&
changes.comments.currentValue===undefined){
this.comments=[];
}
}
Thissmallchangemakestheinternalhandlingofourcommentdatamuchcleaner.Wedon'tneedadditionalcheckswhenworkingforarraytransformationfunctions,andwecanalwaystreatthecommentspropertyasanarray.
SincetheCommentscomponentisalsoresponsibleforhandlingthelogicthatdealswiththeprocessofaddingnewcomments,weneededamethodthatcouldimplementthisrequirement.Inrelationtothis,weusedsomeimmutablepracticeswelearnedaboutinthepreviouschapter:
addNewComment(){
constcomments=this.comments.slice();
comments.splice(0,0,{
user:this.userService.currentUser,
time:+newDate(),
content:this.newCommentEditor.getEditableContent()
});
this.commentsUpdated.next(comments);
this.newCommentEditor.setEditableContent('');
}
Thereareafewkeyaspectsinthispartofthecode.ThismethodwillbecalledfromourcomponentviewwhentheAddcommentbuttonisclicked.Thisiswhentheuserwillhave
www.EBooksWorld.ir
alreadyenteredsometextintotheeditorandanewcommentwillhavebeencreated.
First,wewillusetheuserservicethatweinjectedwithintheconstructortoobtaininformationrelatedtothecurrentlylogged-inuser.ThecontentofthenewlycreatedcommentwillbeobtaineddirectlyfromtheEditorcomponentwesetupusingthe@ViewChildannotation.And,thegetEditableContentmethodwillallowustoreceivethecontentoftheeditableelementwithinthein-placeeditor.
Thenextthingwewantedtodowastocommunicateanupdateofthecommentlistwiththeoutsideworld.WeusedthecommentsUpdatedoutputpropertytoemitaneventwiththeupdatedcommentlist.
Finally,wewantedtocleartheeditorusedtoaddnewcomments.Asthein-placeeditorintheviewoftheCommentscomponentisonlyusedtoaddnewcomments,wecanalwaysclearitafteracommentisadded.Thiswillgivetheusertheimpressionthathiscommenthasbeenmovedfromtheeditorintothelistofcomments.Then,onceagain,wecanaccesstheEditorcomponentdirectlyusingournewCommentEditorpropertyandcallthesetEditableContentmethodwithanemptystringtocleartheeditor.Andthisiswhatwe'vedonehere.
OurCommentscomponentwillholdthelistofallthecomments,anditsviewwillcreateaCommentcomponentforeachcommentinthatlist.EachCommentcomponentwilluseanEditorcomponenttoprovidein-placeeditingofitscontent.Theseeditorsworkautonomouslyusingtheirowncontrols,andtheyemitaneventifthecontentischangedoralteredinanyway.Totakecareofthis,weneedtore-emitthiseventwiththenamecommentEditedfromtheCommentcomponent.NowweonlyneedtocatchthiseventwithinourCommentscomponentinordertoupdatethelistofcommentswiththechanges.Thisisillustratedinthefollowingpartofthecode:
onCommentEdited(comment,content){
constcomments=this.comments.slice();
if(content.length===0){
comments.splice(comments.indexOf(comment),1);
}else{
comments.splice(comments.indexOf(comment),1,{
user:comment.user,
time:comment.time,
content
});
}
this.commentsUpdated.next(comments);
}
ThismethodwillbecalledforeachindividualCommentcomponentthatisrepeatedusingtheNgFordirective.Fromtheview,wepassareferencetothecommentobjectconcerned,aswellastheeditedcontentwewouldreceivefromtheCommentcomponentevent.
Thecommentobjectwillonlybeusedtodeterminethepositionoftheupdatedcommentwithinthecommentlist.Ifthenewcommentcontentisempty,wewillremovethecomment
www.EBooksWorld.ir
fromthelist.Otherwise,wewilljustcreateacopyofthepreviouscommentobject,changethecontentwiththeneweditedcontent,andreplacetheoldcommentobjectinthelistwiththecopy.
Finally,sincewewantedtocommunicatethechangeinthecommentlist,weemittedaneventusingthecommentUpdatedoutputproperty.
Withthis,wehavecompletedourcommentingsystem,andnowit'stimetomakeuseofit.Wealreadyhaveanemptytabpreparedforourprojectcomments,andthisisgoingtobethespotwherewewilladdcommentingcapabilitiesusingourcommentingsystem.
First,let'samendourProjectcomponenttemplate,project/project.html,toincludethecommentingsystem:
...
<ngc-tabs>
<ngc-tabname="Tasks">...</ngc-tab>
<ngc-tabname="Comments">
<ngc-comments[comments]="comments"
(commentsUpdated)="updateComments($event)">
</ngc-comments>
</ngc-tab>
<ngc-tabname="Activities"></ngc-tab>
</ngc-tabs>
Thisisaseasyasitgets.Sincewearepayingattentiontoacleancomponentarchitecture,theinclusionofourcommentingsystemreallyworkslikeabreeze.Theonlythingwenowneedtoensureisthatweprovideapropertyonourprojectwithalistofcomments.WealsoneedawaytoreacttochangesifcommentsareupdatedwithinourCommentscomponent.Forthispurpose,wewillcreateanupdateCommentsmethod.
Let'slookatthecomponentclasschangeswithintheproject/project.jsfile:
exportclassProject{
...
@Input()comments;
...
updateComments(comments){
this.projectUpdated.next({
comments
});
}
}
SincewearealreadydealingwithprojectupdatesinageneralwayandourProjectcomponentisemittingdirectlytoourAppcomponent,whereprojectsdatawillbepersisted,theonlythingweneedtoimplementisanadditionalinputproperty,aswellasamethodtohandlecommentupdates:
www.EBooksWorld.ir
Screenshotofthecommentingsystemintegratedwithinourprojectcomponent
www.EBooksWorld.ir
RecapWithinthistopic,wehavesuccessfullycreatedafull-fledgedcommentingsystemthatcanbeplacedinvariousareasofourapplicationtoenablecommenting.Userscaninteractwithin-placeeditorstoeditthecontentincomments,whichgivesthemagreatuserexperience.
Whilewritingthecodeforourcommentingsystem,welearnedaboutthefollowingtopics:
1. Implementingasimplepipeusingthe@PipeannotationandtheMoment.jslibrarytoproviderelativetimeformatting
2. UsingtheOnChangeslifecyclehooktopreventunwantedorinvalidvalueswithininputproperties
3. Using@ViewChildtoobtainareferencetothecomponentswithinthecomponentsub-treeinordertoestablishdirectcommunication
4. ReusingtheEditorcomponentasaninputfieldreplacementandasanautonomousin-placeeditorwithintheCommentcomponent
www.EBooksWorld.ir
SummaryInthischapter,wecreatedasimplein-placeeditorthatwecanuseformakingcontenteditablewithinourapplication.Goingforward,wecanusetheEditorcomponentwhereverwewanttomakecontenteditableforourusers.Theywillnothavetojumpintonastydialogsorseparateconfigurationpages,butwillbeinapositiontoeditdirectly,aspertheircurrentcontext.Thisisagreattoolforenhancingtheuserexperienceforourusers.
BesidesourshinynewEditorcomponent,wecreatedawholecommentingsystemthatcanbeeasilyincludedinareasofourapplicationwherewe'dliketoprovidecommentingcapabilities.WehavenowincludedthecommentingsystemwithinourProjectcomponent,anduserscannowcommentonprojectsbynavigatingtotheCommentstabontheprojectdetails.
Inthenextchapter,we'llusethecomponent-basedrouterofAngulartomakeourapplicationnavigable.
www.EBooksWorld.ir
Chapter5.Component-BasedRoutingRoutingisanintegralpartoftoday'sfrontendapplications.Ingeneral,arouterservestwomainpurposes:
Makingyourapplicationnavigablesothatuserscanusetheirbrowser'sbackbuttonandstoreandsharelinkswithintheapplicationOffloadpartsoftheapplicationcompositionsothattheroutertakesresponsibilitytocomposeyourapplication,basedonroutesandrouteparameters
TherouterthatcomeswithAngularsupportsmanydifferentuse-cases,anditcomeswithaneasy-to-useAPI.ThissupportschildroutersthataresimilartotheAngularUI-Routernestedstates,Ember.jsnestedroutes,orchildroutersintheDurandalframework.Tiedtothecomponenttree,thisalsomakesuseofitsowntreestructuretostorestatesandtoresolverequestedURLs.
Inthischapter,werefactorourcodetousethecomponent-basedrouterofAngular.Wewilllookintothecoreelementsoftherouterandhowtousethemtoenableroutinginourapplication.
Thefollowingtopicswillbecoveredinthischapter:
AnintroductiontotheAngularrouterAnoverviewoftherefactoringtoenabletherouterinourapplicationCompositionbytemplate,compositionbyrouting,andhowtomixthemUsingtheRoutesdecoratortoconfigureroutesandchildroutesUsingtheOnActivaterouterlifecyclehooktoobtainrouteparametersUsingtheRouterOutletdirectivetocreateinsertionpointsthatarecontrolledbytherouterUsingtheRouterLinkdirectiveandtherouterDSLtocreatenavigationlinksQueryingfortheRouterLinkdirectivesusingthe@ChildViewdecoratorinordertoobtainthelink'sactivestate
www.EBooksWorld.ir
AnintroductiontotheAngularrouterTherouterinAngulariscloselycoupledtoourcomponenttree.ThedesignoftheAngularrouterisbuiltontheassumptionthatacomponenttreeisdirectlyrelatedtoourURLstructure.Thisiscertainlytrueformostofthecases.IfwelookatacomponentB,whichisnestedwithinacomponentA,theURLtorepresentourlocationwouldverylikelybe/a/b.
Inordertospecifythelocationinourtemplatewherewe'dliketoenabletheroutertoinstantiatecomponents,wecanuseso-calledoutlets.Simplybyincludinga<router-outlet>element,wecanmakeuseoftheRouterOutletdirectivetomarktherouterinsertionpointinourtemplate.
Basedonsomerouteconfigurationthatwecanplaceonourcomponent,therouterthendecideswhichcomponentsneedtobeinstantiatedandplacedintotherouteroutlets.Routescanalsobeparameterized,andwecanaccesstheseparameterswithintheinstantiatedcomponents.
Basedonourcomponenttreeandtherouteconfigurationsoncomponentsinthistree,wecanbuildahierarchicalroutinganddecouplechildroutesfromtheirparentroutes.Suchnestedroutesmakeitpossibletospecifyrouteconfigurationonmultiplelayersinourcomponenttreeandreuseparentcomponentsformultiplechildroutes.
www.EBooksWorld.ir
Routerhierarchyestablishedthroughacomponenttree
Let'slookattheelementsoftherouteragaininmoredetail:
Routeconfiguration:Therouteconfigurationisplacedatcomponentlevel,anditcontainsthedifferentroutespossibleforthislevelinthecomponenttree.Byplacingmultiplerouteconfigurationsondifferentcomponentsinthecomponenttree,wecanbuilddecouplednestedrouteseasily.Routeroutlets:Outletsarethelocationsincomponentsthatwillbemanagedbytherouter.Instantiatedcomponentsthatarebasedontherouteconfigurationwillbeplacedintotheseoutlets.Routerlink:ThesearelinksbuiltwithaDSLstylenotationthatenablethedevelopertobuildcomplexlinksthroughtheroutingtree.
www.EBooksWorld.ir
CompositionbyroutingSofar,weachievedcompositionbyincludingsubcomponentsincomponenttemplates.However,we'dnowliketogivethecontroltotheroutertodecidewhichcomponentshouldbeincludedandwhere.
Thefollowingillustrationprovidesanoverviewofthecomponentarchitectureofourapplication,whichwe'regoingtoenabletoroute:
Acomponenttreedisplayingroutingcomponents(solidline)androuteroutlets
TheProjectcomponentisnownotdirectlyincludedwithourAppcomponent.Instead,weusearouteroutletinthetemplateofourAppcomponent.Thisway,wecangivecontroltotherouter,andletitdecidewhichcomponentshouldbeplacedintotheoutlet.TheAppcomponent'srouterconfigurationwillcontainalltop-levelroutes.Inthecurrentapplication,weonlyhavetheProjectcomponentasasecondary-levelcomponent,butthiswillchangeinfurtherchapters.
TheProjectcomponentcontainschildrouteconfigurationtonavigatetothetasksandcommentsview.However,itdoesnotdirectlycontainarouteroutlet.WeusetheTabs
www.EBooksWorld.ir
componentasanavigationelementforanysubviews.Asaresult,we'llplacetherouteroutletintotheTabscomponentandincludethecomponentdirectlyinthetemplateoftheProjectcomponent.
www.EBooksWorld.ir
RouterversustemplatecompositionThecompositionthatwedealtwithsofarwaspurelybasedoninstantiationviatemplateinclusion.Weusedinputandoutputpropertiestodecoupleandencapsulatecomponents,andfollowednicereusablepatterns.
Withtherouter,wefaceaproblemthathasnotyetbeensolvedbyAngularandrequiresthatwefindourownsolution.Aswegivecontroltotheroutertoinstantiateandinsertcomponentsintoourcomponenttree,wecan'tcontrolanybindingsonourinstantiatedcomponent.Whilewepreviouslyreliedonthecleandecouplingofcomponentsusinginputandoutputproperties,wecannolongerdothis.Theonlythingthatarouterprovidesusarerouteparametersthatmayhavebeensetalongtheactivatedroute.
Thisputsusinquiteanastysituation.Basically,weneedtodecidebetweentwodesignswhenwritingcomponents,whichareasfollows:
Weuseagivencomponentpurelyintemplatecompositionand,therefore,relyoninputandoutputpropertiesasthegluebetweentheparentcomponentWeuseacomponentinstantiatedbytherouterandrelyoninput-providedviewrouteparametersanddon'trequirecommunicationwiththeparentcomponent
Well,bothoftheprecedingdesignapproachesaren'tverynice,arethey?Inanidealworld,wewouldnotneedtoapplyanychangestoacomponentwhenweenableitforrouting.Theroutershouldjustenablethecomponentforrouting,butitshouldnotrequireanychangesonthecomponentitself.Unfortunately,there'snoagreementforasolutiontothisproblematthetimeofwritingthisbook.
Aswedon'twanttoloseanycompositioncapabilitiesthatwegainfromrelyingoninputsandoutputsinourTaskListandCommentscomponents,weneedtoafindabettersolutiontoenableroutinginourapplication.
ThefollowingsolutionallowsustoleavetheTaskListandCommentscomponentsuntouchedwhiletheycanstillrelyoninputandoutputproperties.Insteadofexposingthemdirectlytotherouter,wewillbuildwrappercomponentsthatweaddressfromourroutes.Thesewrappersfollowsomemechanicstobridgethisgapbetweentherouterandourcomponents.Thewrappercomponentdealswithanyrouteparametersorroutedatathatmighthavebeensetintheactivatedroute.Theirtemplateshouldonlyincludethecomponentthatiswrappedanditsinputandoutputbindings.TheyhandletherequireddataandfunctionalitytoprovidetheinputandoutputbindingsoftheconcernedcomponentTheymayuseparentcomponentinjectiontoestablishcommunicationwiththeparentandpropagateanyactionthatisrequiredbyemittedevents.Parentcomponentinjectionshouldbeusedwithcautionasitsomewhatbreaksourdecouplingofcomponents.
www.EBooksWorld.ir
UnderstandingtheroutetreeAngularusestreedatastructurestorepresenttherouterstate.Youcanimaginethateverynavigationinyourapplicationactivatesabranchinthistree.Let'slookatthefollowingexample.
Wehaveanapplicationthatconsistsoffourpossibleroutes:
/:Thisistherootrouteoftheapplication,whichishandledinacomponentcalledA./b/:id:Thisistheroutewherewecanaccessabdetailview,whichishandledinacomponentcalledB.IntheURL,wecanpassanidparameter(thatis,/b/100)./b/:id/c:Thisistheroutewherethebdetailviewhasanothernavigationpossibility,whichrevealsmorespecificdetailsthatwecallc.ThisishandledinaCcomponent./b/:id/d:Thisistheroutewherewecanalsonavigatetoadviewinthebdetailview.Thisishandledbyacomponent,calledD:
Aroutetreeconsistingofanactivebranchofroutesegmentsforanactivatedroute/b/100/d
Let'sassumethatweactivatearouteinourexamplebynavigatingtheURL,/b/100/d.Inthiscase,we'dactivatearoutethatreflectsthestatethatisoutlinedintheprecedingfigure.NotethattheroutesegmentBactuallyconsistsoftwoURLsegments.Thereasonforthisisthat
www.EBooksWorld.ir
we'vespecifiedthatourrouteBactuallyconsistsofthebidentifierandan:idrouteparameter.
Usingthistreedatastructure,wehaveaperfectabstractiontodealwithnavigationtrees.Wecancomparetrees,checkwhethercertainsegmentsexistinatree,andextractparameterspresentonresolvedroutesegments.
Todemonstratetheuseofroutingtrees,let'stakealookattheOnActivaterouterlifecyclehookthatwecanimplementonournavigablecomponents:
routerOnActivate(currentRouteSegment,
previousRouteSegment,
currentTree,
previousTree)
Whenweimplementthislifecyclehookonourcomponents,wecanrunsomecodeaftertheroutewasactivated.ThecurrentRouteSegmentargumentwillpointtotheRouteSegmentinstancethatwasactivatedonourcomponent.
Let'stakealookatourexampleagainandassumethatwewanttoaccessthe:idparameterintherouterOnActivatehookofourBcomponent:
routerOnActivate(currentRouteSegment){
this.id=currentRouteSegment.getParam('id');
}
UsingthegetParamfunctionontheRouteSegmentinstance,weobtainanyparametersthatareresolvedonthegivensegment.Inourexamplecase,thiswouldreturna100string.
Let'stakealookatamorecomplexexample.Whatifwewanttoaccessthe:idparameterfromtheDcomponentontheddetailview?IntheOnActivatelifecyclehook,we'llreceiveonlytheroutesegmentthatisrelevanttotheDcomponent.ThisonlyconsistsofthedURLsegmentandthisdoesnotincludethe:idparameterfromtheparentroute.WecannowmakeuseoftheRouteTreeinstancetofindtheparentroutesegmentandobtaintheparameterfromthere:
routerOnActivate(currentRouteSegment,
previousRouteSegment,
currentTree){
this.id=currentTree.parent(currentRouteSegment).getParam('id');
}
UsingthecurrentRouteTreeinstance,wecanobtaintheparentofthecurrentroutesegment.Asaresult,we'llreceivetheparentroutesegment(RouteSegmentBintheprecedingfigure)fromwherewecanobtainthe:idparameter.
Asyoucansee,therouterAPIisquiteflexible,anditallowsustoinspectrouteactivityonaveryfinegranularity.Thetreestructuresthatareusedintheroutermakeitpossibletocomparecomplexrouterstatesinourapplicationwithoutbotheringabouttheunderlying
www.EBooksWorld.ir
complexity.
www.EBooksWorld.ir
BacktotheroutesAllright,nowit'stimetoimplementroutingforourapplication!Inthefollowingtopics,we'llcreatethefollowingroutesforourapplication:
Routepath Description
/projects/:projectId
ThisroutewillactivatetheProjectcomponentintheoutletofourmainapplicationcomponent.ThisconsistsoftheprojectsURLsegmentaswellasthe:projectIdURLsegmenttospecifytheprojectID.
/projects/:projectId/tasks
ThisroutewillactivatetheTaskListcomponent.WewillcreateaProjectTaskListwrappercomponentinordertodecoupleourTaskListcomponentfromrouting.We'llapplytheproceduredescribedintheprevioussection,Routerversustemplatecomposition.
/projects/:projectId/comments
ThisroutewillactivatetheCommentscomponent.We'llcreateaProjectCommentswrappercomponentinordertodecoupleourCommentscomponentfromrouting.We'llapplytheproceduredescribedintheprevioussection,Routerversustemplatecomposition.
InordertousetherouterofAngular,thefirstthingthatweneedtodoistoaddtherouteprovidertoourapplication.We'lldothisonbootstrapinordertomakesuretherouterprovidersareonlyloadedonce.Let'sopenourboostrap.jsfileandaddthenecessarydependencies:
...
import{bootstrap}from'@angular/platform-browser-dynamic';
import{provide}from'@angular/core';
//Importrouterdependencies
import{HashLocationStrategy,LocationStrategy}from'@angular/common';
import{ROUTER_PROVIDERS}from'@angular/router';
...
bootstrap(App,[
...
ROUTER_PROVIDERS,
provide(LocationStrategy,{
useClass:HashLocationStrategy
})
]);
www.EBooksWorld.ir
Fromtheroutermodule,weimporttheROUTER_PROVIDERSconstantthatcontainsalistofmodulesthatarerequiredtobeexposedasproviderswhenusingtherouter.WealsoimporttheLocationStrategyandHashLocationStrategytypefromthecommonmodulethatneedtobeprovidedmanually.
Usingtheprovidefunction,weprovidetheHashLocationStrategyclassasasubstitutionfortheLocationStrategyabstractclass.Thisway,therouterwillknowwhichstrategytousewhenresolvingURLs.
Thefollowingtwostrategiesexistatthemoment:
HashLocationStrategy:ThiscanbeusedwhentheroutershouldusehashURLs,suchaslocalhost:8080#/child/something.Thislocationstrategymakessenseifyou'reworkinginanenvironmentwheretheHTML5pushstatecan'tbeusedduetobrowserorserverconstraints.ThewholenavigationstatewillbemanagedinthefragmentidentifieroftheURL.PathLocationStrategy:Thisstrategycanbeusedifyou'dliketousetheHTML5pushstatetohandleapplicationURLs.ThismeansthatyourapplicationnavigationbecomestheactualpathoftheURL.Usingtheprecedingexampleofahash-basedURL,thisstrategywouldenablethedirectuseoflocalhost:8080/child/something.AstheinitialrequestswillhittheserverifthestateisencodedinthepathofaURL,you'llneedtoenablethecorrectroutingontheservertomakethisworkproperly.
Afterenablingtherouterforourapplication,wewillneedtomakeourrootcomponentroutable.WecandothisbyincludingarouteconfigurationonourAppcomponent.Let'slookatthenecessarycodetodothis.Weedittheapp.jsfileinourlibfolder:
...
import{Project}from'./project/project';
import{Routes,Route}from'@angular/router';
@Component({
selector:'ngc-app',
...
})
@Routes([
newRoute({path:'projects/:projectId',component:Project})
])
exportclassApp{
...
}
Intheprecedingcode,weimportedtheRoutesdecoratoraswellastheRoutetypefromtheroutermodule.
Inordertoconfigureroutesonourcomponent,wecanusethe@RoutesdecoratorbypassinganarrayofRouteobjectsthatdescribethepossiblechildroutesonthiscomponent.
Let'slookattheavailableoptionsthatwecanpasstotheRouteconstructor:
www.EBooksWorld.ir
Routeproperty Description
path
Thispropertyisrequired.Usingthepath,wecandescribethenavigationURLinthebrowserusingtheroutematcherDSL.Thiscancontainrouteparameterplaceholders.
Someexamplesareasfollows:
Thefollowingroutegetsactivatediftheusernavigatesto/homeinthebrowser:
path:'/home'
Thefollowingroutegetsactivatedwhentheusernavigatestoa/child/somethingURLwheresomethingwillbeavailableasrouteparameterwiththename,id:
path:'/child/:id'
component
Thispropertyisrequired,anditdefineswhichcomponentshouldbeinstantiatedbytherouter.Asalreadyexplainedintheprevioussection,therouterdoesnotallowusheretospecifyanybindingstotheinstantiatedcomponent.
TherouteconfigurationonourAppcomponentcoverstheProjectcomponentbeinginstantiatedontheprojects/:projectIdroutepath.ThismeansthatweuseaprojectIdparameteronthechildroute,whichwillbeavailabletotheProjectcomponent.
WealsoneedtomodifyourAppcomponenttemplateandremovethedirectinclusionoftheProjectcomponentthere.Wenowgivecontroltotheroutertodecidewhichcomponenttodisplay.Forthis,weneedtomakeuseoftheRouterOutletdirectivetoprovideaslotinourtemplatewheretherouterwillinstantiatecomponents.
TheRouterOutletdirectiveispartoftheROUTER_DIRECTIVESconstantthatisexportedbytheroutermodule.Let'simportandaddtheconstanttothedirectiveslistonourcomponent:
...
import{Routes,Route,ROUTER_DIRECTIVES}from'@angular/router';
...
@Component({
selector:'ngc-app',
...
directives:[...,ROUTER_DIRECTIVES],
...
})
...
www.EBooksWorld.ir
exportclassApp{
...
}
Now,wecanusetheRouterOutletdirectiveinourtemplatetoindicatetheinsertionpositionofinstantiatedcomponentsbytherouter.Let'sopenourAppcomponenttemplatefile,app.html,andmakethenecessarymodifications:
<divclass="app">
...
<divclass="app__l-main">
<router-outlet></router-outlet>
</div>
</div>
ThenextstepistorefactorourProjectcomponentsothatitcanbeusedinrouting.Aswealreadyoutlinedintheprevioussection,theroutercomeswithcertainconstraintswhenitcomestocomponentdesign.FortheProjectcomponent,wedecidetoredesignitinawaysothatwecanonlyuseitwithrouting.Thisisn'tabadthingherebecausewecanexcludethepossibilitythatitwillbereusedsomewhereelseinourapplication.
TheredesignoftheProjectcomponentincludesthefollowingsteps:
1. Gettingridofallinputandoutputpropertiesofthecomponent.2. UsingtheOnActivaterouterlifecyclehooktoobtaintheprojectIdparameterfromthe
activatedroutesegmentoftheAppcomponent.3. ObtainingtheprojectdatadirectlyfromthedatastoreusingtheprojectIdparameter.4. Handlingupdatesontheprojectdatadirectlyonthecomponentinsteadofdelegatingto
theAppcomponent.
Let'smodifytheComponentclasslocatedinlib/project/project.jstoimplementtheprecedingdesignchanges:
import{Component,ViewEncapsulation,Inject}from'@angular/core';
importtemplatefrom'./project.html!text';
import{Tabs}from'../ui/tabs/tabs';
import{DataProvider}from'../../data-access/data-provider';
import{LiveDocument}from'../../data-access/live-document';
@Component({
selector:'ngc-project',
host:{class:'project'},
template,
encapsulation:ViewEncapsulation.None,
directives:[Tabs]
})
exportclassProject{
constructor(@Inject(DataProvider)dataProvider){
this.dataProvider=dataProvider;
this.tabItems=[
{title:'Tasks',link:['tasks']},
www.EBooksWorld.ir
{title:'Comments',link:['comments']}
];
}
routerOnActivate(currentRouteSegment){
this.id=currentRouteSegment.getParam('projectId');
this.document=newLiveDocument(this.dataProvider,{
type:'project',
_id:this.id
});
this.document.change.subscribe((data)=>{
this.title=data.title;
this.description=data.description;
this.tasks=data.tasks;
this.comments=data.comments;
});
}
updateTasks(tasks){
this.document.data.tasks=tasks;
this.document.persist();
}
updateComments(comments){
this.document.data.comments=comments;
this.document.persist();
}
ngOnDestroy(){
this.document.unsubscribe();
}
}
Besidesimplementingthesechangesthatarealreadydescribedintheredesigningsteps,wealsomadeuseofanewLiveDocumentutilityclassthatweimportedfromthedata-accessfolder.Thishelpsustokeepourprogrammingreactivewhenwe'reconcernedaboutchangesonasingledataentity.UsingtheLiveDocumentclass,wecanquerythedatabaseforasingleentity,whilethechangepropertyoftheLiveDocumentinstanceisanobservablethatkeepsusnotifiedaboutchangesontheentity.ALiveDocumentinstancealsoexposesthedataoftheentityintoadataproperty,whichcanbeaccesseddirectly.Ifwe'dliketomakeanupdateontheentity,wecanadd,modify,orremovepropertiesonthedataobjectandthenstorethechangesbycallingpersist().
AsourProjectcomponentisnowactivatedbytherouterintheAppcomponent,wecanmakeuseoftheOnActivaterouterlifecyclehookbyimplementingamethod,namedrouterOnActivate.WeusethegetParamfunctionofthecurrentroutesegmenttoobtainthe:projectIdparameteroftheroute.
InthesubscribefunctiononthechangeobservableofourLiveDocumentinstance,weexposetheprojectdatadirectlyontheProjectcomponent.Thissimplifieslateruseintheview.
IntheOnDestroylifecyclehook,wemakesurethatweunsubscribefromthedocumentchange
www.EBooksWorld.ir
observable.
Now,wecanrelyontheprojectIdrouteparametertobepassedintoourcomponent,whichmakestheProjectcomponentdependontherouter.Wegotridofallinputproperties,andthenwesetthenecessarydatabyqueryingourdatastoreusingtheprojectID.
Now,it'stimetobuildthewrappercomponentsthatwetalkedaboutinordertoroutetoourTaskListandCommentscomponents.
Let'screateanewcomponentcalledProjectTaskList,whichwillserveasawrappertoenabletheTaskListcomponentinrouting.Wewillcreateaproject-task-list.jsfileinthelib/project/project-task-listpath,asfollows:
import{Component,ViewEncapsulation,Inject,forwardRef}from'@angular/core';
importtemplatefrom'./project-task-list.html!text';
import{TaskList}from'../../task-list/task-list';
import{Project}from'../project';
@Component({
selector:'ngc-project-task-list',
...
directives:[TaskList]
})
exportclassProjectTaskList{
constructor(@Inject(forwardRef(()=>Project))project){
this.project=project;
}
updateTasks(tasks){
this.project.updateTasks(tasks);
}
}
Let'salsotakealookatthetemplateintheproject-task-list.htmlfile:
<ngc-task-list[tasks]="project.tasks"
(tasksUpdated)="updateTasks($event)"></ngc-task-list>
WeinjecttheProjectparentcomponentintoourwrappercomponent.Aswecan'trelyonoutputpropertiesanymoretoemitevents,thisistheonlywaytocommunicatewiththeparentProjectcomponent.We'redealingwithacircularreferencehere(ProjectdependsonProjectTaskList,andProjectTaskListdependsonProject),henceweneedtouseaforwardRefhelperfunctiontopreventtheProjecttypeevaluatingtoundefined.
IfwereceiveatasksUpdatedeventinthetemplate,wewillcalltheupdateTasksmethodonourwrappercomponent.Thewrapperthensimplydelegatesthecalltotheprojectcomponent.
Similarly,weusetheprojectdatatoobtainthelistoftasksandcreateabindingtothetasksinputpropertyoftheTaskListcomponent.
www.EBooksWorld.ir
Usingthiswrapperapproachforrouting,we'reabletoleaveourcomponentsunmodifiedwhenenablingthemforrouting.Thisismuchbetterthantheoptiontomakeourtasklistonlyavailablefortherouter.Wewouldlosethefreedomtouseatasklistoutsideofthecontextofaproject,andthenuseitwithpuretemplatecomposition.
FortheCommentscomponent,weperformtheexactsametask,andcreateawrapperonthelib/project/project-commentspath.Besidesdealingwithcommentsinsteadoftasks,thecodelooksexactlythesameaswiththeProjectTaskListwrappercomponent.
Aftercreatingthetwowrappercomponents,wecannowcreatetherouterconfigurationonourProjectcomponent.Let'smodifytheproject/project.jsfiletoenablerouting:
...
import{ProjectTaskList}from'./project-task-list/project-task-list';
import{ProjectComments}from'./project-comments/project-comments';
import{Routes,Route}from'@angular/router';
...
@Component({
selector:'ngc-project',
...
})
@Routes([
newRoute({path:'tasks',component:ProjectTaskList}),
newRoute({path:'comments',component:ProjectComments})
])
exportclassProject{
...
}
Toenablethetasklistandmakecommentsnavigableusingtherouter,wesimplycreatearouterconfigurationthatinstantiatesourwrappercomponents.Wealsospecifythatthetasksrouteshouldbethedefaultrouteifnochildroutewasselected.
www.EBooksWorld.ir
RoutabletabsOkay,ifyou'vereadthroughthischaptersofar,younowmaywonderwheretherouterwillinstantiatethecomponentsofthechildroutes.We'venotyetincludedarouteroutletintheProjectcomponenttemplatesothattherouterknowswheretoinstantiatecomponents.
Wewon'tincludetheoutletfortheprojectrouterdirectlyintheProjectcomponent.Instead,wewilluseourTabscomponenttotakeoverthisjob.InsteadofusingcontentinsertioninourTabscomponentlikewedidsofar,wenowusearouteroutlettocomposeitscontent.ThiswillmakeourTabscomponentunusablefornonroutingcases,butwecanestablishanicedecouplingbyonlyprovidingtherouteroutlet.ThiswaywecanstillreusetheTabscomponentinotherroutingsituations:
TheAppcomponentincludesarouteroutletdirectly;however,theProjectcomponentreliesontheTabscomponenttoprovidearouteroutlet.
Onahigherlevel,wecandescribethenewdesignofourTabscomponent,asfollows:
Itrendersalltabbuttons,basedonalistofrouterlinksandtitlestoprovidearouternavigationItprovidesarouteroutletthatwillbeusedbytheparentcomponenttoinstantiatenavigatedcomponents
Let'smodifyourTabscomponentinlib/ui/tabs/tabs.jstoimplementthesechanges:
www.EBooksWorld.ir
...
import{ROUTER_DIRECTIVES}from'@angular/router';
@Component({
selector:'ngc-tabs',
...
directives:[ROUTER_DIRECTIVES]
})
exportclassTabs{
@Input()items;
}
TheROUTER_DIRECTIVESconstantfromtheroutermodulecontainstheRouterOutletdirectiveaswellastheRouterLinkdirective.Byimportingtheconstantandprovidingittothecomponentsdirectiveslist,weenablebothrouterdirectivestobeusedinourtemplate.
TheRouterOutletdirectiveisusedinsidetheTabscomponenttemplatetoindicatetheinstantiationpointfortherouter.
TheRouterLinkdirectivecanbeusedtogenerateroutingURLsfromthetemplateusingtherouterlinkDSL.Thisallowsyoutogeneratenavigationlinksinyourapplicationanditcanbeplacedbothonanchortagsaswellasotherelementswhereitwilltriggernavigationonclick.
Theitemsinputisanarrayoflinkitemsthatcontainatitleandarouterlink.Onourparentprojectcomponent,wealreadypreparetheseitemsintheconstructor.
Let'salsotakeaquicklookatthetemplateofourcomponentinthetabs.htmlfile:
<ulclass="tabs__tab-list">
<li*ngFor="letitemofitems">
<aclass="tabs__tab-button"
[routerLink]="item.link">{{item.title}}</a>
</li>
</ul>
<divclass="tabs__l-container">
<divclass="tabs__tabtabs__tab--active">
<router-outlet></router-outlet>
</div>
</div>
Aswelettherouterdealwiththeactiveviewusingarouteroutlet,there'snoneedanymoretousemultipletabcomponentsthatareswitchedactive.Wewillalwayshaveoneactivetab,andlettherouterhandlethecontent.
Let'sseehowwecanmakeuseofthenewTabscomponentinourProjectcomponenttomaketheconfiguredroutesnavigable.First,weneedtoaddthefollowingcodetoourProjectcomponentconstructortoprovidethenecessarynavigationitemstoourTabscomponent:
this.tabItems=[
{title:'Tasks',link:['tasks']},
{title:'Comments',link:['comments']}
www.EBooksWorld.ir
];
Inthelinkpropertyofournavigationitems,weusetherouterlinkDSLtospecifywhichrouteshouldbenavigated.Asthenavigationisrelativetotheparentroutesegmentandwe'realreadyinthe/projects/:projectIdroute,theonlythinginourrouterlinkDSLshouldbearelativepathtothetasksandcommentschildroutes.
InthetemplateofourProjectcomponent,wecannowusethetabItemspropertytocreateabindingtotheinputpropertyoftheTabscomponent:
<divclass="project__l-header">
<h2class="project__title">{{title}}</h2>
<p>{{description}}</p>
</div>
<ngc-tabs[items]="tabItems"></ngc-tabs>
www.EBooksWorld.ir
RefactoringnavigationAsafinalstep,wealsoneedtorefactorournavigationcomponentstorelyontherouter.Sofar,weusedourownroutingthatwasimplementedinacomplexnested-navigationcomponentstructure.WecansimplifythisalotusingtheAngularrouter.
Let'sstartwiththesmallestcomponentfirst,andeditourNavigationItemcomponenttemplateinthelib/navigation/navigation-section/navigation-item/navigation-item.htmlfile:
<aclass="navigation-section__link"
[class.navigation-section__link--active]="isActive()"
[routerLink]="link">{{title}}</a>
Insteadofcontrollingthelinkbehaviorourselves,wenowusetheRouterLinkdirectivetogeneratealinkthatisbasedonthecomponentlinkinputproperty.Tosettheactiveclassonthenavigationlink,westillrelyontheisActivemethodonourcomponent,andthere'snochangerequiredinthetemplate.
Let'slookatthechangestotheComponentclassinthenavigation-item.jsfile:
...
import{RouterLink}from'@angular/router/src/directives/router_link';
@Component({
selector:'ngc-navigation-item',
...
directives:[RouterLink]
})
exportclassNavigationItem{
@Input()title;
@Input()link;
@ViewChild(RouterLink)routerLink;
isActive(){
returnthis.routerLink?
this.routerLink.isActive:false;
}
}
InsteadofrelyingontheNavigationcomponenttomanagetheactivestateofnavigationitems,wenowrelyontheRouterLinkdirective.EachRouterLinkdirectiveprovidesanaccessorproperty,isActive,whichtellsuswhetherthisspecificrouteaddressedbytherouterlinkiscurrentlyactivatedwithinthebrowsersURL.Usingthe@ViewChilddecorator,wecanqueryfortheRouterLinkdirectiveinourviewandthenquerytheisActivepropertytofindoutifthecurrentnavigationitemisactiveornot.
Now,weonlyneedtomakesurethatwepassthenecessaryitemstotheNavigationcomponentinourAppcomponentinordertomakeournavigationworkagain.
www.EBooksWorld.ir
ThefollowingcodeneedstobechangedintheAppcomponentconstructorintheapp.jsfile:
this.projectsSubscription=projectService.change
.subscribe((projects)=>{
this.projects=projects;
//Wecreatenewnavigationitemsforourprojects
this.projectNavigationItems=this.projects
//Wefirstfilterforprojectsthatarenotdeleted
.filter((project)=>!project.deleted)
.map((project)=>{
return{
title:project.title,
link:['/projects',project._id]
};
});
});
Byfilteringandmappingtheavailableprojects,wecancreatealistofnavigationitemsthatcontainatitleandlinkproperty.ThelinkpropertycontainsaroutelinkDSLthatpointstotheprojectdetailsroutethatisconfiguredintheAppcomponentrouterconfiguration.Bypassinganobjectliteralasasiblingtotheroutename,wecanspecifysomeparametersalongtheroute.Here,wesimplysettheexpectedprojectIdparametertotheIDoftheprojectintheprojectslist.
Now,ournavigationcomponentsmakeuseoftheroutertoenablenavigation.WegotridofourcustomroutingfunctionalityintheNavigationcomponent,andweuserouterlinkDSLtocreatenavigationitems.
www.EBooksWorld.ir
SummaryInthischapter,welearnedaboutthebasicconceptsoftherouterinAngular.Welookedathowwecanusetheexistingcomponenttreetoconfigurechildroutesinnested-routerscenarios.Usingnested-childroutes,weenabledthereuseofcomponentswithrouteconfigurations.
Wealsolookedattheproblemofrouterversustemplatecompositionandhowtomitigatethisproblemusingwrappercomponents.Inthisway,weclosethegapbetweentherouterandunderlyingcomponentsusinganin-betweenlayer.
WelookedintorouteconfigurationspecificsandthebasicsoftherouterlinkDSL.WealsocoveredthebasicsoftheRouteTreeandRouteSegmentclassesandhowtousethemtoperformin-depthrouteanalysis.
Inthenextchapter,wewilllearnaboutSVGandhowtousethiswebstandardinordertodrawgraphicsinourAngularapplications.WewillvisualizeanactivitylogofourapplicationactivitiesusingSVGandseehowAngularmakesthistechnologyevengreaterbyenablingcomposability.
www.EBooksWorld.ir
Chapter6.KeepingUpwithActivitiesInthischapter,we'llbuildanactivityloginourtaskmanagementsystemusingScalableVectorGraphics(SVG)tobuildgraphicalcomponentsusingAngular.SVGistheperfectcandidatewhenitcomestocomplexgraphicalcontent,andusingAngularcomponents,wecanbuildnicelyencapsulatedandreusablecontent.
Sincewewanttologalltheactivitieswithinourapplication,suchasaddingcommentsorrenamingtasks,wearegoingtocreateacentralrepository.WecanthendisplaytheseactivitiesandrenderthemasanactivitytimelineusingSVG.
Toaddanoverviewofalltheactivitiesandtoprovideauserinputtonarrowtherangeofdisplayedactivities,we'regoingtocreateaninteractiveslidercomponent.Thiscomponentwilluseaprojectiontorendertimestamps,intheformofticksandactivities,directlyontotheslider'sbackground.We'llalsouseSVGtorendertheelementswithinthecomponent.
We'llcoverthefollowingtopicsinthischapter:
AbasicintroductiontoSVGMakingSVGcomposablewithAngularcomponentsUsingnamespacesincomponenttemplatesCreatingasimplepipetoformatcalendartimesusingMoment.jsUsingthe@HostListenerannotationstohandleuserinputeventstocreateaninteractivesliderelementMakinguseofShadowDOMusingViewEncapsulation.Nativeinordertocreatenative-styleencapsulation
www.EBooksWorld.ir
CreatingaserviceforloggingactivitiesThegoalofthischapteristoprovideawaytokeeptrackofalluseractivitieswithinthetaskmanagementapplication.Forthispurpose,we'llneedasystemthatwillallowustologactivitieswithincomponentsandtoaccessalreadyloggedactivities.
Activities,asentities,shouldbequitegenericandshouldhavethefollowingfieldswiththeirrespectivepurposes:
Subject:Thisfieldshouldbeusedtoreferencethesubjectoftheactivity.Thiscanbeanyidentifierthatidentifiesaforeignentity.Inthecontextofprojects,we'llstoretheprojectIDinthisfield.Servicesandcomponentsthatusetheactivityserviceshouldusethisfieldtofilterspecificactivitiesfurther.Category:Thisfieldprovidesanadditionalwayoftaggingtheactivityfurther.Forprojects,wewillcurrentlyusetwocategories:commentsandtasks.Title:Thisreferstothetitleoftheactivitythatwillprovideaverybriefsummaryofwhattheactivityisabout.ThiscouldbesomethinglikeNewtaskwasaddedorCommentwasdeleted.Message:Thisisthefieldwheretherealbeefoftheactivitygoesinto.Itshouldcontainenoughinformationtoprovidegoodtraceabilityofwhathappenedduringtheactivity.
Inordertodevelopoursystem,we'llcreateanewfilenamedactivity-service.jsundertheactivities/activity-servicepathinourlibfolder.Inthisfile,wewillcreateouractivityserviceclass,whichwe'reenablingfordependencyinjection,byusingthe@Injectableannotation:
@Injectable()
constructor(@Inject(DataProvider)dataProvider,
@Inject(UserService)userService){
exportclassActivityService{
//We'reexposingareplaysubjectthatwillemitevents
//whenevertheactivitieslistchange
this.change=newReplaySubject(1);
this.dataProvider=dataProvider;
this.userService=userService;
this.activities=[];
//We'recreatingasubscriptiontoourdatastoretoget
//updatesonactivities
this.activitiesSubscription=this.dataProvider.getLiveChanges()
.map((change)=>change.doc)
.filter((document)=>document.type==='activity')
.subscribe((changedActivity)=>{
this.activities=this.activities.slice();
//Sinceactivitiescanonlybeaddedwecanassumethat
//thischangeisanewactivity
this.activities.push(changedActivity);
//Sortingtheactivitiesbytimetomakesurethere'sno
//syncissuemessingwiththeordering
this.activities.sort((a,b)=>
www.EBooksWorld.ir
a.time>b.time?-1:a.time<b.time?1:0);
this.change.next(this.activities);
});
}
//Thismethodislogginganewactivity
logActivity(subject,category,title,message){
//UsingtheDataProvidertocreateanewdocumentinour
//datastore
this.dataProvider.createOrUpdateDocument({
type:'activity',
user:this.userService.currentUser,
time:newDate().getTime(),
subject,
category,
title,
message
});
}
}
Intheconstructorofouractivityservice,we'vesubscribedtochangestoourdatastoreandhavefilteredanyincomingchangebytypesowewillonlyreceiveactivityupdates.
Sinceactivitiescan'tbeeditedordeleted,weonlyneedtobeconcernedaboutnewlyaddedactivities.Weupdatetheinternalarrayofactivitieswithanyaddedactivityinthesubscription.Thisway,we'llnotonlyreceivealltheinitialactivities,butalsotheactivitiesthataresubsequentlyaddeddirectlyfromthedatastore.Otherservicesandcomponentscanthendirectlyaccesstheactivitylistofthesystem.
Inorderforotherapplicationcomponentstoreacttochangesintheactivitylist,we'veexposedaReplaySubjectobservableonthechangememberfield.
InthelogActivitymethod,we'vesimplyaddedanewactivitytothedatastore.UserServicewillprovideuswithinformationonthecurrentlylogged-inuser,andwecanuseDataProvidertowritetothedatastore.
So,wehavecreatedasimpleplatformthatwillhelpuskeeptrackofactivitieswithinourapplication.SincewewantonlyoneinstanceofActivityServicewithinourapplication,let'saddittotheproviderslistonourrootAppcomponent.You'llfindthiscomponentintheapp.jsfile,locatedwithinourlibfolder:
@Component({
selector:'ngc-app',
…
providers:[ProjectService,UserService,ActivityService]
})
BecausealldependencyinjectorswillinheritthedependenciesfromourAppcomponent,wecaninjectitinanycomponentofourapplicationgoingforward.
www.EBooksWorld.ir
LoggingactivitiesWehavecreatedanicesystemtologactivities.Nowlet'sgoaheadanduseitwithinourcomponentstokeepanauditofalltheactivities.
First,let'suseActivityServicetologactivitieswithintheTaskListcomponent.ThefollowingcodeexcerpthighlightsthechangesmadetotheTaskListcomponentwithinthetask-list/task-list.jsfileinourlibfolder:
...
import{ActivityService}from'../activities/activity-service/activity-
service';
import{limitWithEllipsis}from'../utilities/string-utilities';
@Component({
selector:'ngc-task-list',
...
})
exportclassTaskList{
…
//Subjectforloggingactivities
@Input()activitySubject;
onTaskUpdated(task,updatedData){
...
//Creatinganactivitylogfortheupdatedtask
this.activityService.logActivity(
this.activitySubject.id,
'tasks',
'Ataskwasupdated',
'Thetask"${limitWithEllipsis(oldTask.title,30)}"wasupdatedon
#${this.activitySubject.document.data._id}.'
);
}
onTaskDeleted(task){
...
//Creatinganactivitylogforthedeletedtask
this.activityService.logActivity(
this.activitySubject.id,
'tasks',
'Ataskwasdeleted',
'Thetask"${limitWithEllipsis(removed.title,30)}"wasdeletedfrom
#${this.activitySubject.document.data._id}.'
);
}
addTask(title){
...
//Creatinganactivitylogfortheaddedtask
this.activityService.logActivity(
this.activitySubject.id,
'tasks',
'Ataskwasadded',
www.EBooksWorld.ir
'Anewtask"${limitWithEllipsis(title,30)}"wasaddedto
#${this.activitySubject.document.data._id}.'
);
}
...
}
UsingthelogActivitymethodofActivityService,wecaneasilyloganynumberofactivitieswithinthealreadyexistingTaskListmethodstomodifytasks.
Inthemessagebodyofouractivities,we'veusedanewutilityfunction,limitWithEllipsis,whichwe'veimportedfromanewmodule,namelystring-utilities.Thisfunctiontakesastringandanumberasparameters.Thereturnedstringisatruncatedversionoftheinputstring,whichiscutoffatthepositionspecifiedwiththesecondparameter.Inaddition,there'sanellipsischaracter(...)appendedtothestring.Iwon'tbotheryouwiththerathersimplecodewithinthishelper.Ifyou'dliketoknowhowit'simplemented,youcanalwayschecktheimplementationafterdownloadingthischapter'scode.
Ifyougobacktothespecificationofouractivitylogs,youwillseethatwealwaysneedtospecifyasubjectinordertologactivities.We'veimplementedthisonourTaskListcomponentbyintroducinganewinputparametercalledactivitySubject.TheassumptionhereisthateachactivitysubjectcontainsLiveDocumentstoredunderthedocumentmember.Fromthere,wecanobtaintheIDinthedatastoreanduseitforouractivitymessage.
IfwerevisitourProjectcomponent,youwillseethatwe'realreadyfollowingtheprerequisitesofbeinganactivitysubject.We'vestoredareferencetotheunderlyingLiveDocumentinstanceunderthedocumentmemberfield.
AllweneedtodonowischangethetemplateofourProjectTaskListwrappercomponenttopasstheactivitySubjectprojectinputoftheTaskListcomponent.Let'slookatthechangesinthelib/project/project-task-list/project-task-list.htmlfilequickly:
<ngc-task-list[tasks]="project.tasks"
[activitySubject]="project"
(tasksUpdated)="updateTasks($event)"></ngc-task-list>
Youmightwonderwhywecareaboutthisrathercumbersomewayofdealingwithourtasklist,ifwecouldjustpassinahardreferencetotheprojectanduseprojecttasksandtheprojectIDdirectly.Thebeautifulaspectofourcurrentsolutionisthatwedonothaveanydependencyonaprojectassuch.WecouldalsouseourTaskListcomponentwithoutthecontextofaproject.Andwecanstillpassalistoftaskstothetasksinputanduseadifferentactivitysubjectfortheactivitylogs.
We'realsogoingtouseActivityServicewithintheCommentscomponenttocreatelogsforadded,edited,anddeletedcomments.Sincethestepsinvolvedareverysimilartowhatwe'vejustdonefortheTaskListcomponent,we'regoingtoskipthis.YoucanalwaystakealookatthefinalcodebaseforthischaptertoaddactivitylogsfortheCommentscomponent.
www.EBooksWorld.ir
LeveragingthepowerofSVGSVGhasbeenapartoftheOpenWebPlatformstandardssince1999andwasfirstrecommendedin2001undertheSVG1.0standard.SVGisaconsolidationoftwoindependentproposalsforanXML-basedvectorimageformat.PrecisionGraphicsMarkupLanguage(PGML)—mainlydevelopedbyAdobeandNetscape—aswellasVectorMarkupLanguage(VML)—whichwasmainlyrepresentedbyMicrosoftandMacromedia—werebothdifferentXMLformatsthatservedthesamepurpose.TheW3CconsortiumdeclinedboththeproposalsinfavorofthenewlydevelopedSVGstandardthatunifiedthebestofbothworldsintoasinglestandard:
TimelineshowingthedevelopmentoftheSVGstandard
Allthreestandardshadacommongoal,whichwastoprovideaformatfortheWebtodisplayvectorgraphicsinthebrowser.SVGisadeclarativelanguagethatspecifiesgraphicalobjectsusingXMLelementsandattributes.
Let'slookatasimpleexampleonhowtocreateanSVGimagewithablackcircle,usingSVG:
<?xmlversion="1.0"encoding="utf-8"?>
<svgversion="1.1"xmlns="http://www.w3.org/2000/svg"
width="20px"height="20px">
<circlecx="10"cy="10"r="10"fill="black"/>
www.EBooksWorld.ir
</svg>
ThisrathersimpleexamplerepresentsanSVGimagewithablackcircle,whosecenterislocatedatx=10pxandy=10px.Theradiusofthecircleis10px,whichmakesthiscircle20pxinwidthandheight.
TheoriginofthecoordinatesysteminSVGsitsonthetop-leftcorner,wheretheyaxisfacesthesouthdirectionandthexaxiseastward:
ThecoordinatesystemwithinSVG
Usingnotonlyprimitiveshapes,suchascircles,lines,andrectangles,butalsocomplexpolygons,thepossibilitiesforcreatinggraphicalcontentarenearlyunlimited.
SVGisnotonlyusedwithintheWeb,buthasalsobecomeaveryimportantintermediateformatforexchangingvectorgraphicsbetweendifferentapplications.AlmostanyapplicationthatsupportsvectorgraphicsalsosupportstheimportofSVGfiles.
TherealpowerofSVGcomestothesurfacewhenwedonotincludeanSVGfileasanHTMLimage,butratherincludetheSVGcontentdirectlywithinourDOM.SinceHTML5directlysupportstheSVGnamespacewithinanHTMLdocumentandwillrenderthegraphicswedefinewithinourHTML,awholebunchofnewpossibilitiesspringup.WecannowstyleourSVGwithCSS,manipulatetheDOMwithJavaScript,andeasilymakeourSVGinteractive.
www.EBooksWorld.ir
Takingthepreviousexampleofourcircleimagetothenextlevel,wecouldmakeitinteractivebychangingthecirclecolorbyclickingit.First,let'screateaminimalHTMLdocumentandincludeourSVGelementsdirectlywithintheDOM:
<!doctypehtml>
<title>MinimalisticCircle</title>
<svgwidth="20px"height="20px">
<circleid="circle"cx="10"cy="10"r="10"fill="black">
</svg>
<script>
document
.getElementById('circle')
.addEventListener('click',function(event){
event.target.setAttribute('fill','red');
});
</script>
Asyoucansee,wecangetridoftheversionandtheXMLnamespacedeclarationwhenweuseSVGdirectlywithintheDOMofourHTMLdocument.What'sinterestinghereisthatwecantreatSVGverymuchlikeregularHTML.WecanassignanIDandevenclassestoSVGelementsandaccessthemfromJavaScript.
WithinthescripttagofourHTMLdocument,wecandirectlyaccessourcircleelementusingtheIDwe'vepreviouslyassignedtoit.Wecanaddeventlisteners,thewaywealreadyknow,fromregularHTMLelements.Inthisexample,weaddedaclickeventlistenerandchangedthecolorofourcircletored.
Forthesakeofsimplicity,weusedaninlinescripttaginthisexample.ItwouldofcoursebemuchcleanertohaveaseparateJavaScriptfiletodothescripting.
www.EBooksWorld.ir
StylingSVGI'mapuristwhenitcomestotheseparationofconcernswithintheWeb.Istillstronglybelieveintheseparationofstructure(HTML),appearance(CSS),andbehavior(JavaScript),aswellasproducingthemostmaintainableapplicationswhenfollowingthispractice.
First,itseemsweirdtohaveSVGinyourHTML,andyoumightthinkthatthisbreaksthecontractofacleanseparation.Whyisthisgraphicalcontent,consistingofonlyappearance-relevantdata,sittinginmyHTMLthatissupposedtoonlycontainrawinformation?AfterdealingwithalotofSVGswithinaDOM,IhavecometotheconclusionthatwecanestablishacleanseparationwhenusingSVGbydividingourappearanceresponsibilitiesintothefollowingtwosubgroups:
Graphicalstructure:Thissubgroupdealswiththeprocessofdefiningthebasicstructureofyourgraphicalcontent.Thisisaboutshapesandlayout.Visualappearance:Thissubgroupdealswiththeprocessofdefiningthelookandfeelofourgraphicalstructures,suchascolors,linewidths,linestyles,andtextalignment.
IfweseparatetheconcernsofSVGintothesegroups,wecanactuallygaingreatmaintainability.GraphicalstructureisdefinedbytheSVGshapesthemselves.TheyaredirectlywrittenwithinourHTMLbutdon'thaveaparticularlookandfeel.WeonlystorethebasicstructuralinformationwithinHTML.
Luckily,allthepropertiesofvisualappearance,suchascolors,cannotonlybeexpressedthroughtheattributesinourSVGelements;however,there'sacorrespondingCSSpropertythatallowsustooffloadallthelook-and-feel-relevantaspectsofthestructuretoCSS.
Gobacktotheexamplewherewedrewablackcircle;we'lltweakthisabittofitourdemandsofseparationofconcernssothatwecandistinguishgraphicalstructurefromgraphicalappearance:
<!doctypehtml>
<title>MinimalisticCircle</title>
<svgwidth="20px"height="20px">
<circleclass="circle"cx="10"cy="10"r="10">
</svg>
StylingourgraphicalstructurescannowbeachievedusingCSSbyincludingastylesheetwiththefollowingcontent:
.circle{
fill:black;
}
Thisisfantastic,aswecannownotonlyreusesomegraphicalstructures,butalsoapplydifferentvisualappearanceparametersusingCSS,similartothoseenlighteningmomentswhenwemanagedtoreusesomesemanticHTMLbyonlychangingsomeCSS.
www.EBooksWorld.ir
Let'slookatthemostimportantCSSpropertieswecanusetostyleSVGshapes:
fill:WhileworkingwithsolidSVGshapes,there'salwaysashapefillandstrokeoptionavailable;thefillpropertyspecifiesthecoloroftheshapefill.stroke:ThispropertyspecifiesthecoloroftheSVGshape'soutline.stroke-width:ThispropertyspecifiesthewidthoftheSVGshape'soutlineonsolidshapes.Fornonsolidshapes,suchaslines,thiscanbethoughtofaslinewidth.stroke-dasharray:Thisspecifiesadashpatternforstrokes.Dashpatternsarespace-separatedvaluesthatdefineapattern.stroke-dashoffset:Thisspecifiesanoffsetforthedashpattern,whichisspecifiedwiththestroke-dasharrayproperty.stroke-linecap:Thispropertydefineshowlinecapsshouldberendered.Theycanberenderedassquare,butt,orroundedcaps.stroke-linejoin:Thispropertyspecifieshowlinesarejoinedtogetherwithinapath.shape-rendering:Usingthisproperty,youcanoverridetheshape-renderingalgorithmthat,asthenamesuggests,isusedtorendershapes.Thisisparticularlyusefulifyouneedcrispyedgesonyourshapes.
Foracompletereferenceoftheavailableappearance-relevantSVGattributes,visittheMozillaDeveloperwebsiteathttps://developer.mozilla.org/en-US/docs/Web/SVG/Attribute.
IhopethisbriefintroductiongaveyouabetterfeelingaboutSVGandthegreatpoweritcomeswith.Inthischapter,we'regoingtousesomeofthatpowertocreatenice,interactivegraphicalcomponents.IfyouwouldliketolearnmoreaboutSVG,IstronglyrecommendthatyougothroughthegreatarticlesbySaraSoueidan.
www.EBooksWorld.ir
BuildingSVGcomponentsWhenbuildingAngularcomponentswithSVGtemplates,thereareacoupleofthingsweneedtobeawareof.Thefirstandmostobviousone,isXMLnamespaces.ModernbrowsersareveryintelligentwhenparsingHTML.Besidesbeingprobablythemostfault-tolerantparserinthehistoryofcomputerscience,DOMparsersareverysmartinrecognizingmarkupandthendecidinghowtotreatit.Theywillautomaticallydecidethecorrectnamespacesforus,basedonelementnames,sowedon'tneedtodealwiththemwhenwritingHTML.
Ifyou'vemessedaroundwiththeDOMAPIabit,youwould'veprobablyrecognizedthattherearetwomethodsforcreatingnewelements.Inthedocumentobject,forexample,there'sacreateElementfunction,butthere'salsocreateElementNSthatacceptsanadditionalnamespaceURIparameter.Also,everycreatedelementhasanamespaceURIpropertythattellsyouthenamespaceofthespecificelement.ThisisimportantsinceHTML5isastandardthatconsistsofatleastthreenamespaces:
HTML:ThisisthestandardHTMLnamespacewiththehttp://www.w3.org/1999/xhtmlURI.SVG:ThisembracesallSVGelementsandattributesandusesthehttp://www.w3.org/2000/svgURI.YoucansometimesseethisnamespaceURIinanxmlnsattributeofthesvgelements.Infact,thisisnotreallyrequired,asthebrowserissmartenoughtodecideonthecorrectnamespaceitself.MathML:ThisisanXML-basedformattodescribemathematicalformulasandissupportedinmostmodernbrowsers.Itusesthehttp://www.w3.org/1998/Math/MathMLnamespaceURI.
Wecanmixalltheseelementsfromdifferentstandardsandnamespaceswithinasingledocument,andourbrowserwillfigureoutthecorrectnamespaceitselfwhenitcreateselementswithintheDOM.
Tip
Ifyouwantmoreinformationonnamespaces,IrecommendthatyougothroughtheNamespacesCrashCoursearticleontheMozillaDeveloperNetworkathttps://developer.mozilla.org/en/docs/Web/SVG/Namespaces_Crash_Course.
AsAngularwillcompiletemplatesforusandrenderelementsintotheDOMusingtheDOMAPI,itneedstobeawareofthenamespaceswhendoingthat.Similartothebrowser,Angularprovidessomeintelligencefordecidingthecorrectnamespacewhilecreatingelements.However,therearesomesituationswhereyouneedtohelpAngularrecognizethecorrectnamespace.
Toillustratesomeofthisbehavior,let'stransformourcircleexamplethatwe'vebeenworkingonintoanAngularcomponent:
@Component({
www.EBooksWorld.ir
selector:'awesome-circle',
template:`
<svg[attr.width]="size"[attr.height]="size">
<circle[attr.cx]="size/2"[attr.cy]="size/2"
[attr.r]="size/2"fill="black"/>
</svg>
`
})
exportclassAwesomeCircle{
@Input()size;
}
We'vewrappedourcircleSVGgraphicsintoasimpleAngularcomponent.ThesizeinputparameterdeterminestheactualwidthandheightofthecirclebycontrollingtheSVG'swidthandheightattributesandthecircle'scx,cy,andrattributes.
TouseourCirclecomponent,simplyusethefollowingtemplatewithinanothercomponent:
<awesome-circle[size]="20"></awesome-circle>
Note
It'simportanttonotethatweneedtouseattributebindingsonSVGelements,andwecan'tsetDOMelementpropertiesdirectly.ThisisduetothenatureofSVGelementsthathavespecialpropertytypes—forexample,SVGAnimatedLength—thatcanbeanimatedwithSynchronizedMultimediaIntegrationLanguage(SMIL).Insteadofinterferingwiththeserathercomplexelementproperties,wecansimplyuseattributebindingstosettheattributevaluesoftheDOMelement.
Let'sgobacktoournamespacediscussion.AngularwouldknowthatitneedstousetheSVGnamespacetocreatetheelementswithinthistemplate.Itwillfunctioninthiswaysimplybecausewe'reusingthesvgelementasarootelementwithinourcomponent,anditcouldswitchthenamespacewithinthetemplateparserforanychildelementsautomatically.
However,therearecertainsituationswhereweneedtohelpAngulardeterminethecorrectnamespacefortheelementswe'dliketocreate.Thisstrikesusifwe'recreatingnestedSVGcomponentsthatdon'tcontainarootsvgelement:
@Component({
selector:'[awesomeCircle]',
template:`
<svg:circle[attr.cx]="size/2"[attr.cy]="size/2"
[attr.r]="size/2"fill="black"/>
'
})
exportclassAwesomeCircle{
@Input('awesomeCircle')size;
}
@Component({
selector:'app'
www.EBooksWorld.ir
template:`
<svgwidth="20"height="20">
<g[awesomeCircle]="20"></g>
</svg>
`,
directives:[AwesomeCircle]
})
exportclassApp{}
Inthisexample,we'renestingSVGcomponents,andourAwesomeCirclecomponentdoesnothaveansvgrootelementtotellAngulartoswitchthenamespace.Thisiswhywe'vecreatedthesvgelementwithinourAppcomponentandthenincludedtheAwesomeCirclecomponentinanSVGgroup.
WeneedtoexplicitlytellAngulartoswitchtotheSVGnamespacewithinourCirclecomponent,andwecandothisbyincludingthenamespacenameasaprefixseparatedbyacolon,asyoucanseeinthehighlightedsectionoftheprecedingcodeexcerpt.
IfyouhavemultipleelementsthatneedtobecreatedwithintheSVGnamespaceexplicitly,youcanrelyonthefactthatAngulardoesapplythenamespaceforchildelementstooanddoesgroupallyourelementswithanSVGgroupelement.So,youonlyneedtoprefixthegroupelement<svg:g>...</svg:g>,butnoneofthecontainedSVGelements.
ThisisenoughtoknowaboutAngularinternalswhendealingwithSVG.Let'smoveonandcreatesomerealcomponents!
www.EBooksWorld.ir
BuildinganinteractiveactivityslidercomponentIntheprevioustopics,we'vecoveredthebasicsofworkingwithSVGanddealingwithSVGinAngularcomponents.Nowit'stimetoapplyourknowledgetothetaskmanagementapplicationandcreatesomecomponentsusingSVG.
Thefirstcomponentwe'llbecreatinginthiscontextisaninteractivesliderthatallowstheusertoselectthetimerangeofactivitiesthatheorsheisinterestedtocheckout.DisplayingasimpleHTML5rangeinputcouldbeasolution,butsincewe'vegainedsomeSVGsuperpower,wecandobetter!We'lluseSVGtorenderourownsliderthatwillshowexistingactivitiesasticksontheslider.Let'slookatamock-upoftheslidercomponentthatwe'regoingtocreate:
Amockupoftheactivityslidercomponent
Ourslidercomponentwillactuallyservetwopurposes.Itshouldbeausercontrolandshouldprovideawaytoselectatimerangeforfilteringactivities.However,itshouldalsoprovideanoverviewofalltheactivitiessothatausercanfiltertherangemoreintuitively.Bydrawingverticalbarsthatrepresentactivities,wecanalreadygivetheuserafeelingoftherangeheorsheisinterestedin.
Firstofall,we'llcreateanewfileforourActivitySlidercomponentcalledactivity-slider.jswithintheactivities/activity-sliderpathanddefineourcomponentclass:
importstylesfrom'./activity-slider.css!text';
@Component({
selector:'ngc-activity-slider',
host:{
class:'activity-slider'
},
styles:[styles],
encapsulation:ViewEncapsulation.Native,
…
})
exportclassActivitySlider{
www.EBooksWorld.ir
//Theinputexpectsalistofactivities
@Input()activities;
//Iftheselectionofdaterangechangeswithinourslider
//component,we'llemitachangeevent
@Output()selectionChange=newEventEmitter();
constructor(@Inject(ElementRef)elementRef){
//We'llusethehostelementformeasurementwhendrawing
//theSVG
this.sliderElement=elementRef.nativeElement;
//Thepaddingoneachsideoftheslider
this.padding=20;
}
ngAfterViewInit(){
//We'llneedareferencetotheoverlayrectanglesowecan
//updateitspositionandwidth
this.selectionOverlay=this.sliderElement
.shadowRoot.querySelector('.selection-overlay');
}
}
Thefirstthingweshouldmention,andwhichdiffersfromalltheothercomponentswe'vewrittensofar,isthatwe'reusingViewEncapsulation.Nativeforthiscomponent.AswelearnedfromtheCreatingourapplicationcomponentsectioninChapter2,Ready,Set,Go!,whenweuseViewEncapsulation.Nativeforourcomponentencapsulation,AngularactuallyusesShadowDOMtocreatethecomponent.WebrieflylookedatthisintheShadowDOMsectioninChapter1,Component-BasedUserInterfacesaswell.
UsingShadowDOMforourcomponentwillgiveusthisadvantage:ourcomponentwillbefullyencapsulatedfromtheCSSsideofthings.ThisnotonlymeansthatnoneoftheglobalCSSeswillleakintoourcomponent,butitalsomeansthatwe'llneedtocreatelocalstylesinordertostyleourcomponent.
Sofar,we'veusedaCSSnamingconventioncalledBEMthatprovidesuswithsomenecessaryprefixestoavoidnameclasheswithinCSSandestablishacleanandsimpleCSSspecificity.However,whenusingShadowDOM,wecanforegoprefixestoavoidnameclashes,sincewe'reonlyapplyingstyleslocallywithinthecomponent.
Becausewe'reusingShadowDOMforthiscomponent,weneedtohaveawaytodefinelocalstyles.Angularprovidesuswithanoptiontopassstylesintothecomponentusingthestylespropertyofthecomponentannotation.
Tip
ChromesupportsShadowDOMnativelysinceversion35.WithinFirefox,ShadowDOMcanbeenabledbyvisitingtheabout:configpageandturningonthedom.webcomponents.enabledflag.IE,Edge,andSafaridon'tsupportthisstandardatall;however,wecansetthingsupinawaythattheycoulddealwithShadowDOM,byincludingapolyfillnamedwebcomponents.js.Youcanfindmoreinformationonthispolyfillat
www.EBooksWorld.ir
https://github.com/webcomponents/webcomponentsjs.
UsingthetextpluginofSystemJS,wecanimportastylesheetcontainingonlythelocalstylesofourcomponentandthenpassthemtothestylesproperty.Byappendinga!textpostfixtotheimportofourCSSfile,wetellSystemJStoloadourCSSfileasrawtext.Notethatthestylespropertyisexpectinganarray,whichiswhywewrapourimportedstylesintoanarrayliteral.
IfyoutakealookatthestylesheetfortheActivitySlidercomponent,youcanimmediatelyseethatwe'renolongerprefixingtheclasseswiththecomponentname:
.slide{
fill:#f9f9f9;
}
.activity{
fill:#3699cb;
}
.time{
fill:#bbb;
font-size:14px;
}
.tick{
stroke:#bbb;
stroke-width:2px;
stroke-dasharray:3px;
}
.selection-overlay{
fill:#d9d9d9;
}
Usually,suchshortclassnameswouldprobablyleadtonameclasheswithinourproject,butsincethestyleswillbelocaltotheShadowDOMofourcomponent,wedon'tneedtoworryaboutnameclashesanymore.
Asaninputparameter,wedefinethelistofactivitiesthatwillbeusednotonlytodeterminetheavailablerangeintheslider,butalsotorenderactivitiesonthebackgroundoftheslider.
Onceaselectionismadebytheuser,ourcomponentwillusetheselectionChangeeventemittertonotifytheoutsideworldaboutthechange.
Withintheconstructor,we'resettingasidethecomponentDOMelementforsomemeasurementweneedtomakeinordertodrawlateron:
this.sliderElement=elementRef.nativeElement;
ByinjectingtheElementRefinstancetotheconstructor,wecaneasilyaccessthenativeDOMelementofourcomponent.
www.EBooksWorld.ir
ProjectionoftimeOurslidercomponentneedstobeabletoprojecttimestampsintothecoordinatesystemofSVG.Also,whenauserclicksonthetimelinetoselectarange,we'llneedtobeabletoprojectcoordinatesbackintotimestamps.Forthispurpose,weneedtocreatetwoprojectionfunctionswithinourcomponentthatwilluseafewhelperfunctionsandstatestocalculatethevalues,fromcoordinatestotimeandvice-verse:
Visualizationofimportantvariablesandfunctionsforourcalculations
WhilewewillusepercentagetopositionourSVGelementsontheslidercomponent,thepaddingonthesideswillneedtobespecifiedinpixels.ThetotalWidthfunctionwillreturnthetotalwidthoftheareainpixels;thisiswherewe'lldrawtheactivityindicators.ThetimeFirst,timeLast,andtimeSpanvariableswillalsobeusedbythecalculationsandarespecifiedinmilliseconds.
Let'saddsomecodetoourslidertodealwiththeprojectionofouractivitiesonthesliderintheactivity-slider.jsfile:
//Gettingthetotalavailablewidthoftheslider
totalWidth(){
returnthis.sliderElement.clientWidth-this.padding*2;
}
//Projectsatimestampintopercentageforpositioning
projectTime(time){
letposition=this.padding+
(time-this.timeFirst)/this.timeSpan*this.totalWidth();
returnposition/this.sliderElement.clientWidth*100;
}
//Projectsapixelvaluebacktoatimevalue.Thisisrequired
//forcalculatingtimestampsfromuserselection.
projectLength(length){
returnthis.timeFirst+(length-this.padding)/this.totalWidth()*
this.timeSpan;
}
www.EBooksWorld.ir
SincewehaveputasidethereferencetotherootelementasthesliderElementmembervariable,wecanuseitsclientWidthpropertytogetthefullwidthofthecomponentandsubtractthepadding.Thiswillgiveusthefullwidthoftheareawherewe'dliketodrawactivityindicators,inpixels.
IntheprojectTimefunction,wewillfirsttransformthetimestampintoapositionbyasimpleruleofthree.Becausewehaveaccesstothetimestampofthefirstactivityaswellasthetotaltimespan,thiswillbequiteasimpletask.Oncewedothis,wecanconvertourpositionvalue,whichisofunitpixels,intopercentage,bydividingitbythetotalcomponentwidthandthenmultiplyingitby100.
Toprojectapixelvaluebacktoatimestamp,wecandomoreorlessthereverseofprojectTime,exceptthatwe'renotdealingwithpercentageherebutassumingthelengthparameteroftheprojectLengthfunctionisinpixelunit.
We'veusedsomemembervariables—timeFirst,timeLast,andtimeSpan—withinourprojectioncode,buthowdowesetthesemembervariables?Sincewehaveanactivitiescomponentinput,whichisexpectedtobealistofrelevantactivities,wecanobservetheinputforchangesandsetthevaluesbasedontheinput.Toobservecomponentinputforchanges,wecanusethengOnChangeslifecyclehook:
ngOnChanges(changes){
//Iftheactivitiesinputchangesweneedtore-calculateand
//re-draw
if(changes.activities&&changes.activities.currentValue){
constactivities=changes.activities.currentValue;
//Forlatercalculationswesetasidethetimesofthe
//firstandthelastactivity
if(activities.length===1){
//Ifweonlyhaveoneactivityweusethesametimefor
//firstandlast
this.timeFirst=this.timeLast=activities[0].time;
}elseif(activities.length>1){
//Takefirstandlasttime
this.timeFirst=activities[activities.length-1].time;
this.timeLast=activities[0].time;
}else{
//Noactivitiesyet,soweusethecurrenttimeforboth
//firstandlast
this.timeFirst=this.timeLast=newDate().getTime();
}
//Thetimespanisthetimefromthefirstactivitytothe
//lastactivity.Weneedtolimittolower1fornotmessing
//uplatercalculations.
this.timeSpan=Math.max(1,this.timeLast-this.timeFirst);
}
}
First,weneedtocheckwhetherthechangesincludechangestotheactivitiesinputandthatthecurrentvalueoftheinputisvalid.Aftercheckingfortheinputvalue,wecandetermine
www.EBooksWorld.ir
ourmembervariables,namelytimeFirst,timeLast,andtimeSpan.WelimitthetimeSpanvariableto1atleast,asourprojectioncalculationswouldbemessedupotherwise.
Theprecedingcodewillensurethatwewillalwaysrecalculateourmembervariableswhentheactivitiesinputchangesandthatwe'dbeusingthemostrecentdata-renderingactivities.
www.EBooksWorld.ir
RenderingactivityindicatorsWe'vealreadyimplementedthebasicsofthecomponentandlaidthegroundworkfordrawingtimeinformationintothecoordinatesystemofourcomponent.It'stimetouseourprojectionfunctionsanddrawouractivitiesasindicatorsonthesliderusingSVG.
First,let'stakealookattherequiredtemplatethatwearegoingtocreateinafilecalledactivity-slider.htmlwithinouractivity-sliderdirectory:
<svgwidth="100%"height="70px">
…
<rectx="0"y="30"width="100%"height="40"
class="slide"></rect>
<rect*ngFor="letactivityofactivities"
[attr.x]="projectTime(activity.time)+'%'"
height="40"width="2px"y="30"class="activity"></rect>
</svg>
Sinceweneedtocreateanindicatorforeveryactivitywithinouractivitieslist,wecansimplyusetheNgFordirectivetorepeattherectanglethatrepresentsouractivityindicator.
AsweknowfrombuildingourActivityServiceclassinaprevioustopic,activitiesalwayscontainatimefieldwiththetimestampoftheactivity.Withinourcomponent,wehavealreadycreatedaprojectionfunctionthatconvertstimeintopercentage,relativetoourcomponentwidth.WecansimplyusetheprojectTimefunctionwithinourattributebindingforthexattributeoftherectelementtopositionouractivityindicatorsatthecorrectpositions.
ByusingonlyanSVGtemplateandourbackingfunctiontoprojecttime,wehavecreatedanicelittlechartthatdisplaysactivityindicatorsonatimeline.
Youcanimaginethatifwehavealotofactivities,oursliderwillactuallylookprettystuffed,anditwillbehardtogetafeelingforwhenthoseactivitiesmayhaveoccurred.Weneedtohavesomesortofagridthatwillhelpusassociatethechartwithatimeline.
Asalreadyshowninthemock-upofourslidercomponent,we'renowgoingtointroducesometicksonthesliderbackgroundthatwilldividethesliderintosections.We'llalsolabeleachtickwithacalendartime.Thiswillgiveourusersaroughsensefortimewhenlookingattheactivityindicatorsontheslider.
Let'slookatthecodechangeswithinourActivitySliderclassthatwillenabletherenderingofourticks:
ngOnChanges(changes){
//Iftheactivitiesinputchangesweneedtore-calculateand
//re-draw
if(changes.activities&&changes.activities.currentValue){
...
//Re-calculatetheticksthatwedisplayontopoftheslider
this.computeTicks();
www.EBooksWorld.ir
...
}
//Thisfunctioncomputes5tickswiththeirtimeandpositionon
//theslider
computeTicks(){
constcount=5;
consttimeSpanTick=this.timeSpan/count;
this.ticks=Array.from({length:count}).map(
(element,index)=>{
returnthis.timeFirst+timeSpanTick*index;
});
}
...
Firstofall,weneedtocreateafunctionthatcomputessometicksforusthatwecanplaceontothetimeline.Forthispurpose,weneedtocreatethecomputeTicksmethodthatwilldividethewholetimelineintofiveequalsegmentsandgeneratetimestampsthatrepresentthepositionintimeforindividualticks.Westoretheseticksinanewticksmembervariable.Withthehelpofthesetimestamps,wecanrenderthetickswithinourvieweasily.
Tip
WeusetheArray.fromES6functiontocreateanewarraywiththedesiredlength,andusethefunctionalarrayextrafunctionmaptogeneratetickmodelobjectsfromthisarray.UsingArray.fromisanicetricktocreateaninitialarrayofagivenlengththatcanbeusedtoestablishfunctionalstyle.
Let'slookatthetemplateofourcomponentandhowwecanuseourarrayoftimestampstorenderticksonourslidercomponent.We'regoingtomodifyourcomponenttemplateinactivity-slider.html:
...
<g*ngFor="lettickofticks">
<text[attr.x]="projectTime(tick)+'%'"y="14"class="time">
{{tick|calendarTime}}</text>
<line[attr.x1]="projectTime(tick)+'%'"
[attr.x2]="projectTime(tick)+'%'"
y1="30"y2="70"class="tick"></line>
</g>
...
Torenderourticks,we'veusedanSVGgroupelementtoplaceourNgFordirectivethatrepeatstheticktimestampswe'vestoredintheticksmembervariable.
Foreachtick,weneedtoplacealabelaswellasalinethatspansoverthesliderbackground.WecanusetheSVGtextelementtorenderourlabelwiththetimestampontopoftheslider.Withintheattributebindingforthexattributeofourtextelement,we'veusedourprojectTimeprojectionfunctiontoreceivetheprojectedpercentagevaluefromourtimestamp.Theycoordinateofourtextelementisfixedatapositionwherethelabelswill
www.EBooksWorld.ir
justsitontopofourslider.
SVGlinesconsistoffourcoordinates:x1,x2,y1,andy2.Togethertheydefinetwocoordinatepointswherealinewillbedrawnfromonepointtotheother.
Nowwearegettingclosertothefinalsliderthatwespecifiedinthemock-upatthebeginningofthistopic.Thelastmissingpieceofthepuzzleistomakeoursliderinteractivesoausercanselectarangeofactivities.
www.EBooksWorld.ir
BringingittolifeSofar,we'vecoveredtherenderingofthesliderbackgroundaswellastherenderingoftheactivityindicators.We'vealsogeneratedticksanddisplayedthemwithagridlineandalabeltodisplaythecalendartimeofeachtick.
Well,thatdoesnotreallymakeaslider,doesit?Ofcourse,wealsoneedtohandleuserinputandmakethesliderinteractivesouserscanselectatimerangetheywanttodisplaytheactivitiesfor.
Todothis,addthefollowingchangestothecomponentclass:
ngOnChanges(changes){
//Iftheactivitiesinputchangesweneedtore-calculateand
//re-draw
if(changes.activities&&changes.activities.currentValue){
...
//Settingtheselectiontothefullrange
this.selection={
start:this.timeFirst,
end:this.timeLast
};
//Selectionchangedsoweneedtoemitevent
this.selectionChange.next(this.selection);
}
}
WhenwedetectachangeintheactivitiesinputpropertywithinthengOnChangeslifecyclehook,weinitializeamodelfortheuserselectioninourslidercomponent.Itconsistsofastartandendproperty,bothcontainingtimestampsthatrepresenttheselectedrangeonouractivityslider.
Oncewe'vesetourinitialselection,weneedtousetheselectionChangeoutputpropertytoemitanevent.Thisway,wecanletourparentcomponentknowthattheselectionwithinthesliderhaschanged.
Todisplaytheselectedrange,weuseanoverlayrectanglewithinourtemplatethatwillbeplacedabovethesliderbackground.Ifyoulookatthemock-upimageoftheslideragain,you'llnoticethatthisoverlayispaintedingray:
<rect*ngIf="selection"
[attr.x]="projectTime(selection.start)+'%'"
[attr.width]="projectTime(selection.end)-
projectTime(selection.start)+'%'"
y="30"height="40"class="selection-overlay"></rect>
Thisrectanglewillbeplacedjustaboveoursliderbackgroundandwilluseourprojectionfunctiontocalculatethexandwidthattributes.AsweneedtowaitforchangedetectiontoinitializeourselectionwithinthengOnChangeslifecyclehook,we'lljustcheckforavalid
www.EBooksWorld.ir
selectionobjectbymakinguseoftheNgIfdirective.
NowweneedtostarttacklinguserinputinourActivitySlidercomponent.Themechanicsforstoringthestateandrenderingourselectionisalreadyinplace,sowecanimplementtherequiredhostlistenerstohandleuserinput:
...
//Ifthecomponentreceivesamousedownevent,weneedtostarta
//newselection
@HostListener('mousedown',['$event'])
onMouseDown(event){
//Startinganewselectionbysettingselectionstartandend
//totheprojectedtimeoftheclickedposition.
this.selection.start=this.selection.end=
this.projectLength(event.offsetX);
//Selectionchangedsoweneedtoemiteventan
this.selectionChange.next(this.selection);
//Settingaflagsoweknowthattheuseriscurrentlymoving
//theselection
this.modifySelection=true;
}
//Wealsoneedtotrackmousemoveswithinourslidercomponent
@HostListener('mousemove',['$event'])
onMouseMove(event){
//Weshouldonlymodifytheselectionifthecomponentisin
//thecorrectmode
if(this.modifySelection){
//Updatetheselectionendwiththeprojectedtimefromthe
//mousecoordinates
this.selection.end=Math.max(this.selection.start,
this.projectLength(event.offsetX));
//Selectionchangedsoweneedtoemiteventan
this.selectionChange.next(this.selection);
//Topreventsideeffects,weshouldstoppropagationand
//preventbrowserdefault
event.stopPropagation();
event.preventDefault();
}
}
//Iftheuserisreleasingthemousebutton,weshouldstopthe
//modifyselectionmode
@HostListener('mouseup')
onMouseUp(){
this.modifySelection=false;
}
//Iftheuserisleavingthecomponentwiththemouse,weshould
//stopthemodifyselectionmode
@HostListener('mouseleave')
onMouseLeave(){
this.modifySelection=false;
}
...
www.EBooksWorld.ir
Intheprecedingcodeexcerpt,wehandledatotaloffoureventsonthesliderhostelement:
onMouseDown:Wesetourselectionmodel'sstartandendpropertieswiththesamevalue.Sincewe'reusingtimestampsfortheseproperties,weprojectedthemousepositionintothetimespacefirst.Themousepositioncomesinpixelsrelativetotheslidercomponent'sorigin.Sinceweknowtheslider'swidthandthetotaltimedurationdisplayed,wecanconvertthisintotimestampseasily.We'reusingtheprojectLengthmethodforthispurpose.Bypassingasecondargumenttothe@HostListenerdecorator,wespecifiedthatwe'dliketopasstheDOMeventtoouronMouseDownmethod.Wealsosetastateflag,modifySelection,inourcomponenttoindicatethataselectionisunderprogress.onMouseMove:Ifthecomponentisinselectionmode(themodifySelectionflagistrue),youcanadjusttheendpropertyoftheselectionobject.Here,wealsomadesurethatweruledoutthepossibilityofcreatinganegativeselectionbyusingMath.maxandlimitingtheendoftheselectiontonotbesmallerthanthestart.onMouseUp:Whentheuserreleasesthemousebutton,thecomponentexitstheselectionmode.ThiscanbedonebysettingthemodifySelectionflagtofalse.onMouseLeave:ThisisthesameastheonMouseUpevent;thedifferenceisthatherethecomponentwilljustexittheselectionmode.
Usingthe@HostListenerdecorator,wewereabletohandleallofthenecessaryuserinputtocompleteourcomponentwiththeinteractiveelementsthatwerestillmissing.
www.EBooksWorld.ir
RecapInthistopic,welearnedhowtouseSVGinordertocreategraphicalandinteractivecomponentswithAngular.BycreatingattributebindingsonourSVGelementsandcontrollingtheinstantiationofgraphicalelementsusingtheNgForandNgIfdirectives,webuiltacustomslidercomponentthatprovidesaniceoverviewofouractivities.Atthesametime,wealsolearnedhowtohandleuserinputusingthe@HostListenerdecoratorinordertomakeourcomponentinteractive:
Ascreenshotofthefinishedactivityslidercomponent
Tosumthingsup,welearnedaboutthefollowingconcepts:
EncapsulatingcomponentviewsusingViewEncapsulation.NativeandimportinglocalstylesCoveringsomebasicprojectionsoftimestampsontoscreencoordinatestobeusedwithSVGelementsHandlinguserinputandcreatingacustomselectionmechanismusingthe@HostListenerdecorator
www.EBooksWorld.ir
BuildingtheactivitytimelineSofar,we'vebuiltaservicetologactivitiesandaslidercomponenttoselectatimerangeandprovideanoverviewusingactivityindicators.Sinceweneededtoperformalotofdrawingtaskswithintheslidercomponent,SVGwasaperfectfitforthisusecase.TocompleteourActivitiescomponenttree,westillneedtorendertheactivitiesthatwereselectedusingtheActivitySlidercomponent.
Let'scontinuetoworkonouractivitiescomponenttree.Wellcreateanewcomponentthatwillberesponsibleforrenderinganindividualactivitywithinanactivitytimeline.Let'sstartwiththetemplateoftheActivitycomponent,whichwewillcreateinanewactivities/activity/activity.htmlfile:
<img[attr.src]="activity.user.pictureDataUri"
[attr.alt]="activity.user.name"
class="activity__user-image">
<div[class.activity__info--align-right]="isAlignedRight()"
class="activity__info">
<h3class="activity__title">{{activity.title}}</h3>
<pclass="activity__author">
by{{activity.user.name}}{{activity.time|fromNow}}
</p>
<p>{{activity.message}}</p>
</div>
Eachactivitywillconsistofauserimageaswellasaninformationboxthatwillcontaintheactivitytitle,message,andauthoringdetails.
Ouractivitywilluseaninputtodetermineitsalignment.Thisallowsustoaligntheactivityfromoutsidethecomponent.TheisAlignedRightmethodhelpsussetanadditionalCSSclass,activity__info--align-right,ontheactivityinformationbox.
WealsoneedtocreateacomponentclassforourActivitycomponent,whichwewillcreateunderanewactivities/activity/activity.jsfile:
import{FromNowPipe}from'../../pipes/from-now';
@Component({
selector:'ngc-activity',
…
//WeareusingtheFromNowpipetodisplayrelativetimes
//withinourtemplate
pipes:[FromNowPipe]
})
exportclassActivity{
@Input()activity;
//Inputthatshouldbeastring'left'or'right'andwill
//determinetheactivityalignmentusingCSS
@Input()alignment;
@Input()@HostBinding('class.activity--start-mark')startMark;
www.EBooksWorld.ir
@Input()@HostBinding('class.activity--end-mark')endMark;
//Functionwiththatwilltellusiftheactivityshouldbe
//alignedtotheright.It'susedforsettingamodifierclass
//ontheinfoelement.
isAlignedRight(){
returnthis.alignment==='right';
}
}
OurActivitycomponentexpectsfourinputs:
activity:Thispropertytakesthedatamodeloftheactivitythatneedstoberenderedwiththecomponent.ThisistheactivitythatwecreatedusingActivityService.alignment:Thisinputpropertyshouldbesettoastringcontainingthewordleftorright.WeusedthistodeterminewhetherweneededtoaddanadditionalCSSclasstoourtemplateinordertoaligntheactivityinformationboxtotheright.startMark:Thisinputpropertyactsasaninputandahostbindingatthesametime.Ifthisinputissettotrue,theactivitywillgetanadditionalCSSclass,activity--start-mark,whichwillcauseasmallmarkontopofthetimelinetoindicatethetimelinetermination.endMark:InthesamewayasstartMark,thisinputusesahostbindingtosetanadditionalCSSclass,activity--end-mark,whichwillcauseasmallmarkonthebottomofthetimelinetoindicatethetimelinetermination.
TheisAlignedRightmethodisusedwithinthetemplatetodeterminewhetherweneedtoaddanadditionalCSSclasstotheinformationboxinordertoalignittotheright.
WeformattedthetimestampoftheactivityusingtheFromNowpipe,whichwecreatedinChapter4,NoComments,Please!.Inordertousethepipeinthetemplate,weneedtoimportitandaddittothepipespropertyofourcomponentannotation.
Wenowhavealmostallthecomponentstodisplayouractivities.Butstill,there'ssomethingmissing,whichisthegluetocombineActivitySliderwithourActivitycomponentsandalsomakeourcomponentsubtreenavigable.Forthis,we'llcreateanewcomponentcalledActivities.Let'screateanactivities/activities.jsfiletowriteourcomponentclass:
@Component({
selector:'ngc-activities',
...
directives:[ActivitySlider,Activity]
})
exportclassActivities{
@Input()activitySubject;
constructor(@Inject(ActivityService)activityService){
this.activityService=activityService;
}
ngOnChanges(changes){
if(changes.activitySubject){
//Ifwehaveasubscriptiontotheactivitiesservice
www.EBooksWorld.ir
//alreadyweneedtounsubscribefirst
if(this.activitiesChangeSubscription){
this.activitiesChangeSubscription.unsubscribe();
}
//Whentheprojectdataisupdatedweneedtofilterfor
//activitiesagain
this.activitiesChangeSubscription=
this.activityService.change.subscribe((activities)=>{
//FilterforallactivitiesthathavetheprojectIDassubject
this.activities=activities
.filter((activity)=>activity.subject===
this.activitySubject.document.data._id);
this.onSelectionChange();
});
}
}
Firstofall,weneedtoknowwhichactivitieswewanttodisplaywithinourcomponent.Forthis,weneedtoprovideacomponentinput,namelyactivitySubject.Oncethisisdone,wecanpassanactivitysubjectfromtheparentcomponentanduseittofilteractivitieswe'reinterestedin.
Sincewe'veusedactivitysubjectstologactivitiesaswell,wecanusethesamesubjectstodisplayactivities.InthengOnChangeslifecyclehook,wesetupasubscriptionontheActivityServiceinstancetoreacttonewlycreatedactivities.Becausetheactivityservicewillnotifyuswithanupdatedlistofactivities,wecansimplyusetheArray.prototype.filterfunctiontofilteronlyrelevantitems.We'vemadeuseoftheactivitySubjectinputtoobtaintheIDfromthesubject.
Next,weneedtocreateamethodtoapplyadaterangefiltertoouractivities.TheonSelectionChangemethodwillbecalledfromouractivitiestemplate,wherewecreatedabindingtoourActivitySlidercomponent:
//Iftheselectionwithintheactivitysliderchanges,weneed
//tofilteroutactivitiesagain
onSelectionChange(selection=this.selection){
this.selection=selection;
//Storefilteredactivitiesthatfallintothedaterange
//selectionspecifiedbytheslider
this.selectedActivities=this.selection?this.activities.filter(
(activity)=>activity.time>=this.selection.start
&&activity.time<=this.selection.end
);
}
Wheneverthetimerangeisupdatedbytheuserwithintheslider,we'lloverridetheselectedActivitiesmembervariablewithanewfilteredversionoftheactivities,whichwe'dobtainfromActivityService.Thefilterwillnarrowdowntheactivitiesbycomparingtheactivitytimeagainsttheselectionrangefromtheslidercomponent.
www.EBooksWorld.ir
Nowwewillsetupsomehelperfunctionstobeusedwithinourtemplate:
//Getanalignmentstringbasedontheindex.Activitieswith
//evenindexgetalignedleftwhileoddsgetalignedright.
getAlignment(index){
returnindex%2===0?'left':'right';
}
//Functiontodetermineifanactivityindexisfirst
isFirst(index){
returnindex===0;
}
//Functiontodetermineifanactivityindexislast
isLast(index){
returnindex===this.selectedActivities.length-1;
}
Thethreemethods,namelygetAlignment,isFirst,andisLast,areusedwithinthetemplateasinputfortheActivitycomponent.IfyoutakealookatthecodeofActivityComponentagain,youwillseethatweneedtoprovidesomeinputinordertosetsomeCSSclassesforappearance.Thethreemethodswecreatedherewillbeusedforthispurpose:
//Ifthecomponentgetsdestroyed,weneedtounsubscribefrom
//theactivitieschangeobserver
ngOnDestroy(){
this.activitiesChangeSubscription.unsubscribe();
}
}
Finally,weaddedanOnDestroylifecyclehookthatwillunsubscribeusfromtheactivity'schangeobservable.
Thetemplateforthiscomponentisrathersimple.TheonlythingweneedtodoisrendertheActivitySlidercomponent,aswellasiterateovertheselectedactivitiesandwireintheActivitycomponentforeachiteration:
<ngc-activity-slider[activities]="activities"
(selectionChange)="onSelectionChange($event)">
</ngc-activity-slider>
<divclass="activities__l-container">
<ngc-activity
*ngFor="letactivityofselectedActivities,letindex=index"
[activity]="activity"
[alignment]="getAlignment(index)"
[startMark]="isLast(index)"
[endMark]="isFirst(index)">
</ngc-activity>
</div>
There'snotmuchweneedtoexplainhere.We'veboundactivitiesandouronSelectionChangemethodtotheslidercomponentanditeratedoveralltheselectedactivitiestorenderourActivitycomponents.We'vecreatedalocalviewvariable,index,whichwe
www.EBooksWorld.ir
willusefortheappearanceinputoftheActivitycomponent.
That'sitforouractivitiespage!We'vecreatedthreecomponentsthatarecomposedtogetheranddisplayanactivitystream,whichprovidesaslidertofilteractivitiesfordates:
Ascreenshotofthefinishedactivitiesview
www.EBooksWorld.ir
SummaryInthischapter,wecreatedaninteractiveslidercomponentusingSVG.Whiledoingthis,welearnedaboutsomeSVGbasicsandthepowerofSVGwithintheDOM.UsingAngular,wewereabletomakeSVGcomposable,whichitisn'tbynature.Welearnedaboutnamespaces,howAngularhandlesthem,andhowwecantellAngularthatwe'dliketousenamespacesexplicitly.
BesidesusingSVGforourslidercomponent,wealsolearnedhowtouseShadowDOMtocreatenativeviewencapsulation.Asaresultofthis,wewereabletouselocalstylesforourcomponent.Wedon'tneedtoworryaboutCSSnameclashes,specificity,andglobalCSSsideeffectsanymorewhenusinglocalstyles.
ThewholecodeforthischaptercanbefoundintheZIPfileofthebookresourcesthatyoucandownloadfromthePacktPublishingwebsite.
Inthenextchapter,we'regoingtoenhancewhatwe'vebuiltthroughoutthechapterssofar.Wewillcreatesomecomponentstoenrichtheuserexperiencewithinourapplication.
www.EBooksWorld.ir
Chapter7.ComponentsforUserExperienceUserexperienceisacoreconcernfordevelopersbuildingtoday'sapplications.Wearenolongerlivinginaworldwhereusersarecontentedwithanapplicationthatjustworks.Theexpectationsaremuchhigher.Now,anapplicationneedstobehighlyusableandshouldprovideanefficientworkflow;usersalsoexpectittobringthempleasurewhileperformingtasks.
Inthischapter,we'regoingtolookatbuildingsomecomponentsthatwillincreasetheoverallusabilityofourtaskmanagementsystem.Thesefeatureswillenrichthecurrentfunctionalityandprovidemoreefficientworkflows.
Wewilldevelopthefollowingthreetechnicalfeaturesandembedthemintoourcurrentapplication,whereverapplicable:
Tagmanagement:We'llenabletheuseoftagswithingeneratedcontent,suchascomments,activities,andotherareaswheretheycanbeofanyuse.Tagswillhelpusersbuildlinksbetweencontentandnavigationshortcuts.Draganddrop:We'llbuildgenericcomponentsthatwillmakeuseofdraganddropfeaturesabreeze.Byenablingdraganddropfeatures,we'llallowuserstofulfillcertaintaskswithmuchhigherefficiency.Infinitescrolling:We'llbuildacomponentthatwillrevealthecontentoflistswhilescrolling.Thisfeatureisnotgoingtodirectlyincreasetheworkflowperformance,butitwillhelpusincreasetheoverallapplicationperformance.Itwillalsonarrowdowntheuser'scontextbyonlyshowingrelevantinformation.
We'llcoverthefollowingtopicsinthischapter:
CreatingatagmanagementsystemtoenteranddisplaytagsCreatingastatefulpipetorendertagsusingaserviceUsingthesanitize-htmlmoduletosanitizepotentiallyunsafecontentCreatingacomponenttoautocompletetagsduringuserinputGoingthroughthebasicsoftheHTML5draganddropAPICreatingdirectivesfordraggableelementsanddroptargetsUsingdataTransferobjectsandacustomattributetoenableselectivedroptargetsCreatingacustomForOfrepeaterusingtheasterisktemplatesyntaxtoenableinfinitescrollingImplementingcustomchangedetectionusingtheDoChecklifecyclehook,andusingIterableDiffertoapplyDOMchangesPerformingdynamicviewinstantiationusingViewContainer
www.EBooksWorld.ir
TagmanagementTheclassicalformoftaggingenablesyoutoassociateataxonomywithelementswithinasystemandhelpsyouorganizeyourproject.Itallowsyoutohaveamany-to-manyassociationthatcanbequicklymanaged,andyoucanuseitlatertofilterrelevantinformation.
Inourtaskmanagementsystem,we'regoingtouseaslightlydifferentversionoftags.Ourgoalistoprovideawaytohavesemanticshortcutswithintheapplication.Withthehelpoftags,ausershouldbeabletocross-referenceinformationbetweendifferentpartsofthedata,providingasummaryofthereferencedentityaswellasanavigationshortcutfortheentity.
Forexample,wecanincludeaprojecttagwithinausercomment.AusercanenterthetagbysimplytypingintheprojectID.Whenacommentisdisplayed,weseethetitleoftheprojectandthenumberofopentaskswithintheproject.Butwhenweclickonthetag,wedirectlyreachtheprojectdetailpagewherethetaskislocated.
Inthissection,we'lldeveloptherequiredelementstoprovideawaytouseprojecttagsthatwillenabletheusertocross-referenceotherprojectswithincomments.We'llalsousetagmanagementinouractivities,whichwecreatedinthepreviouschapter.
www.EBooksWorld.ir
TagdataentityLet'sstartwiththetagentitythatshowshowwecanrepresenttagswithinoursystem.We'llcreateanewTagclassinafileundertags/tag.js:
//Classthatrepresentsatag
exportclassTag{
constructor(textTag,title,link,type){
//ThetextTagpropertyisthetextrepresentationofthetag
this.textTag=textTag;
this.title=title;
this.link=link;
this.type=type;
}
}
Thisclassrepresentstags;wheneverwestoretaginformation,we'llusethisentityasadatavehicle.Let'slookattheindividualfieldsandelaborateontheiruse:
textTag:Thisisthetextrepresentationofatag.Allourtagsneedtobeidentifieduniquelyusingthistextrepresentation.Wecandefinethetextrepresentationoftagsasfollows:
Texttagsalwaysstartwithahashsymbol(#)Texttagsonlycontainwordcharactersortheminussymbol(-)Alltheinnardsofatag,definedbyotherproperties(title,link,andtype),canbeextrapolatedfromthetextTagproperty.ItcanthereforebeconsideredanID.
title:Thisisacomparativelylongertextrepresentationofatag.Itshouldcontainasmuchdetailaboutthesubjectaspossible.Inthecaseofprojecttags,thiscouldmeantheprojecttitle,opentagscount,assignee,andotherimportantinformation.Sincethisisthefieldthatwillberenderedifatagisparsed,it'llbebeneficialifthecontentstaysrelativelycondensed.link:AvalidURL,whichwillbeusedwhenthetagisrendered.ThisURLwillmakelinksclickableandenabletheshortcutnavigation.Inthecaseoftheprojectstagswe'regoingtocreate,thiswillbeaURLfragmentidentifierthatwilllinktothegivenprojectpage.type:Thisisusedtodistinguishbetweendifferenttagsandprovideusawaytoorganizetagsatahighergranularitylevel.
Sofar,sogood.Wenowhaveadatavehiclewecaneasilyconstructtotransferinformationabouttags.
www.EBooksWorld.ir
GeneratingtagsOurnextstepistocreateafactorythatwillgeneratetagsforus.Allwe'dliketopasstothefactoryisasubject,whichcanbebasicallyanything.Thefactorywillthendeterminethetypeofthesubjectandexecutethenecessarylogictogenerateatagfromit.Thismightsoundabitabstractatfirst,butlet'slookatthecodeofthegenerateTagfunctionwe'llcreateinamoduleundertags/generate-tag.js:
import{Tag}from'./tag';
import{limitWithEllipsis}from'../utilities/string-utilities';
exportconstTAG_TYPE_PROJECT='project';
//ThegenerateTagfunctionisresponsibleforgeneratingnewtag
//objectsdependingonthepassedsubject
exportfunctiongenerateTag(subject){
if(subject.type===TAG_TYPE_PROJECT){
//Ifwe'redealingwithaprojecthere,wegeneratethe
//accordingtagobject
constopenTaskCount=subject.tasks.filter((task)=>!task.done).length;
returnnewTag(
`#${subject._id}`,
`${limitWithEllipsis(subject.title,20)}(${openTaskCount}opentasks)`,
`#/projects/${subject._id}/tasks`,
TAG_TYPE_PROJECT
);
}
}
Let'sexaminethegenerateTagfunctionandwhatwe'retryingtoachievehere.
First,wedeterminedthesubjecttypebycheckingthetypeattributeofthesubjectobject.Inthecaseofprojectdataobjects,weknowthatthetypewillbesetto"project".Thefollowingthreepointssuccinctlyexplainwhatwe'vedone:
1. Sinceweweresurethatweweredealingwithaprojecthere,wegeneratedanewtag.Inthefuture,we'lldealwithothersubjecttypesaswell,sothischeckwillberequired.
2. Wewantedtouseanindicatorforalltheopentasksinthetagtitle.Forthisreason,wedidaquickfilteringofopentaskswithintheprojectandstoredthelengthofthefilteredarrayintheopenTaskCountconstant.
3. NowwecaninstantiateanewTagobjectusingtheprojectIDastextTag.Forthetitlefield,weusedahelperfunction,limitWithElipsis,whichtruncatesprojecttitlesthatarelongerthan20characters.Wealsoappendedtheopentaskscounttothetagtitle.ForthelinkfieldoftheTaginstance,wespecifyaURLthatwillnavigatetotheprojectdetailsview.Finally,weusedtheTAG_TYPE_PROJECTconstanttodefinethetagtypefield.
www.EBooksWorld.ir
CreatingatagsserviceOkay,we'redonewithsettingupallthesupportingstructuresweneed;wecannowmoveforwardtocreateatagsservice.Atagsservicewillhavethefollowingresponsibilities:
Generatingandcachingtags:Wewon'tcreatetagsinoursystemadhocifweonlywanttorenderthem.Themechanicsofatagsserviceismorelikegeneratedcache.Initially,atagsservicegathersalltherequiredinformationtogenerateallthepossibletagswithinthesystem.Italsoreactstochangesandupdatesthelistoftagsifrequired.Withthis,we'llnotonlysaveonsomeprocessingneeds,butwe'llalsohaveareadilyavailablelisttosearchforexistingtags.Thiswillbeparticularlyusefulifweliketopresenttheavailabletagstotheusersotheycanchoosefromthem.Renderingtags:AtagsserviceisalsoresponsibleforturningtagsintoHTML.ItusesthetitleandlinkfieldsoftheTaginstancestogeneratetheirHTMLrepresentation.Parsingtextcontent:Theparsingfunctionalityofthetagsserviceisresponsibleforfindingtextrepresentationsoftagswithinastring.ItthenusestherenderingfunctiontorenderthesetagsintoHTML.
Let'screateamoduleforourtagsserviceinanewfileundertags/tags-service.js.
First,weneedtocreatetwoutilityfunctionsthatwillhelpusprocesstagsandstringscontainingthetextualrepresentationsoftags.
ThereplaceAllfunctionisasimplesubstituteforamissingJavaScriptfunctiontoreplacemultipletextoccurrenceswithinastringwithoutusingregularexpressions:
//Utilityfunctiontoreplacealltextoccurrencesinastring
functionreplaceAll(target,search,replacement){
returntarget.split(search).join(replacement);
}
ThefindTagsfunctionwillextractanypossibletagfromatextstring.Itdoesthisbyapplyingaregularexpressionthatwillfindmatchesfortagsintheformatdiscussedatthebeginningofthetopic.Thisformatassumesthatourtagsalwaysstartwithahashsymbol,followedbyanywordcharacterordashsymbols.Thisfunctionreturnsalistofallthepossibletexttags:
//Functiontofindanytagswithinastringandreturnanarray
//ofdiscoveredtags
functionfindTags(str){
constresult=[];
constregex=/#[\w\/-]+/g;
letmatch;
while(match=regex.exec(str)){
result.push(match[0]);
}
returnresult;
}
Forourtagsservice,wewillnowdefineanewclassthatwillbeannotatedwith@Injectable
www.EBooksWorld.ir
sowecanuseitasaproviderinourcomponents:
@Injectable()
exportclassTagsService{
...
}
Let'slookattheconstructorofourTagsServiceclass:
constructor(@Inject(ProjectService)projectService){
//Iftheavailabletagswithinthesystemchanges,wewill
//emitthisevent
this.change=newReplaySubject(1);
//Inordertogenerateprojecttags,we'remakinguseofthe
//ProjectService
this.projectService=projectService;
this.projectService.change.subscribe((projects)=>{
//Onprojectchangeswestorethenewprojectlistandre-
//initializeourtags
this.projects=projects;
this.initializeTags();
});
}
Inordertogenerateandcacheprojecttags,weobviouslyneedProjectService,whichprovidesuswithalistofalltheprojects.InsteadofgrabbingthelistdatafromProjectServiceonce,we'reobservingthelistforchanges.Thisbringsustheadvantagethatwe'llnotonlygettheinitiallistofprojects,butwe'llalsobemadeawareofanychangesmadeintheprojectlist.
WesubscribedtoProjectServiceusingthechangefield.ThisexposesReplaySubject,whichemitstheprojectlist.Afterstoringthecurrentprojectlistasamemberfield,weneedtocalltheinitializeTagsmethod:
//Thismethodisusedinternallytoinitializeallavailabletags
initializeTags(){
//We'recreatingtagsfromallprojectsusingthegenerateTag
//function
this.tags=this.projects.map(generateTag);
//Sincewe'veupdatedthelistofavailabletagsweneedto
//emitachangeevent
this.change.next(this.tags);
}
Asweonlysupportprojecttagscurrently,theonlythingweneedtoconsiderwhilegeneratingtagsistheprojectswehavestoredinourservice.WecansimplymaptheprojectlistwehavestoredintheprojectsmemberfieldusingourgenerateTagfunction.TheArray.prototype.mapfunctionwillreturnanewarraythatisalreadyalistofgeneratedtasksfortheprojects.
www.EBooksWorld.ir
RenderingtagsOkay,wenowhaveaservicethatusesareactiveapproachtogeneratetagsfromtheavailableprojects.Thisisalreadyaddressingthefirstconcernofourservice.Let'slookatitsotherresponsibilities,whichareparsingtextcontentfortagsandrenderingHTML.
Renderingtagsisnotabigdealsincewehavealreadyabstractedthedatamodeloftagsinacleanway.Weneedtowriteamethodforrenderingtagsthatwillactasapass-throughfunctioniftheargumentisnotavalidTaginstance.Thisway,wecanpassunrecognizedtextrepresentationsoftagsasstrings,anditwilljustreturnusthestring.
SincetagshaveURLsthatpointtoalocation,we'regoingtouseanchorHTMLelementstorepresentourtags.Theseelementsalsohaveclassesthatwillhelpusstyletagsdifferentlythanregularcontent.Let'screateanothermethodwithinthetagsservicethatcanbeusedtorendertagobjectsintoHTML:
renderTag(tag){
if(taginstanceofTag){
return`<aclass="tags__tagtags__tag--${tag.type}"
href="${tag.link}">${tag.title}</a>`;
}else{
returntag;
}
}
Thefollowingmethodcanbeusedtofindatagbyitstextualrepresentation.Thisfunctionwilltrytofindthetagwithinourgeneratedcache,andifunsuccessful,willreturnthetextTagargument.Thisisalsoapass-throughmechanismthatsimplifiesthehandlingwhenweparseawholepieceoftextfortags:
//Thismethodwilllookupatagviaitstextrepresentationor
//returntheinputargumentifnotfound
parseTag(textTag){
returnthis.tags.find(
(tag)=>tag.textTag===textTag
)||textTag;
}
Lastbutnotleast,let'simplementthemainmethodoftheservice.TheparsefunctionscansthewholetextfortagsandreplacesthemwiththeirHTMLrepresentation:
//Thismethodtakessometextinputandreplacesanyfoundand
//validtextrepresentationsoftagswiththegeneratedHTML
//representationofthosetags
parse(value){
//Firstwefindallpossibletagswithinthetext
consttags=findTags(value);
//Foreachfoundtexttag,we'reparsingandrenderingthem
//whilereplacingthetexttagwiththeHTMLrepresentation
//ifapplicable
tags.forEach(
www.EBooksWorld.ir
(tag)=>value=replaceAll(value,tag,
this.renderTag(this.parseTag(tag))));
);
//Afteralltagshavebeenrendered,we'reusingasanitizer
//toensuresomebasicsecurity
returnvalue;
}
First,weneedtousethefindTagsutilityfunction;thiswillreturnalistofallthetexttagsthatitwouldfindinthestringcontentpassedtotheparsefunction.Usingthistexttaglist,wecantheniteratethroughthelistandsuccessivelyreplaceallthetexttagsinthecontentwiththegeneratedHTMLusingtherenderTagmethod.
www.EBooksWorld.ir
IntegratingthetaskserviceAlltheconcernsofourtaskservicehavenowbeentakencareof,anditisalreadystoringtagsfortheavailableprojects.Wecannowgoaheadandintegrateourserviceintotheapplication.
SinceourtagsserviceturnstextwithsimplehashtagsintoHTMLwithlinks,apipewouldbeaperfecthelpertointegratethefunctionalitywithinourcomponents.
Let'screateatags.jsfileinourpipesfolderandcreateanewpipeclass,namely,Tags:
import{Pipe,Inject}from'@angular/core';
import{TagsService}from'../tags/tags-service';
@Pipe({
name:'tags',
//Sinceourpipeisdependingonservices,we'redealingwitha
//statefulpipeandthereforesetthepureflagtofalse
pure:false
})
exportclassTagsPipe{
constructor(@Inject(TagsService)tagsService){
this.tagsService=tagsService;
}
//Thetransformmethodwillbecalledwhenthepipeisusedwithinatemplate
transform(value){
if(typeofvalue!=='string'){
returnvalue;
}
//ThepipeisusingtheTagsServicetoparsetheentiretext
returnthis.tagsService.parse(value);
}
}
Wehavealreadycreatedafewpipessofar.However,thispipeisabitdifferentinthatitisn'tapurepipe.Pipesareconsideredpureiftheirtransformfunctionalwaysreturnsthesameoutputforagiveninput.Thisimpliesthatthetransformfunctionshouldnotbedependentonanyotherexternalsourcethatcaninfluencetheoutcomeofthetransform,andtheonlydependenciesaretheinputvalues.ThisisnottrueforourTagspipethough.ItdependsonTagsServicetotransformtheinput,andnewtagscanbestoredinthetagsserviceatanytime.Successivetransformationscansuccessfullyrendertagsthatwerenotexistentjustamomentago.
BytellingAngularthatourpipeisnotpure,wecandisabletheoptimizationitperformsonpurepipes.ThisalsomeansthatAngularwillneedtorevalidatetheoutputofthepipeoneverychangedetection.Thiscanleadtoperformanceissues;therefore,thepureflagshouldbeusedwithcaution.
Allright,asfarasrenderingtagsisconcerned,weareallset.Let'sintegrateourtagsfunctionalityintoourEditorcomponentsowecanmakeuseofthemwithinthecommenting
www.EBooksWorld.ir
system.
Let'sstartbyeditingtheEditormodulelocatedunderui/editor/editor.js:
...
import{TagsPipe}from'../../pipes/tags';
@Component({
selector:'ngc-editor',
...
pipes:[TagsPipe]
})
exportclassEditor{
...
@Input()enableTags;
...
}
First,weimportedtheTagsPipeclassandreferencedittothepipesconfigurationofthe@Componentannotation.
We'vealsoaddedanewinputtotheenableTagscomponent,whichwillallowustocontrolwhetherweshouldhandletagswithinthecontentoftheeditororignorethem.
That'sit,asfaraschangestothecomponentfileisconcerned.Let'sapplysomechangestothetemplateofthecomponentbyeditingtheui/editor/editor.htmlfile:
...
<div*ngIf="enableTags"class="editor__output"
[innerHtml]="(content||'-')|tags"></div>
<div*ngIf="!enableTags"class="editor__output">
{{content||'-'}}
</div>
...
Theonlychangewe'vemadeinthetemplateiswherewedisplaytheeditorcontent.We'veusedtwotemplateelementsbyemployingtheNgIfasterisktemplatesyntax.Thelatterone,iftagsaredisabled,rendersthecontentasbefore.Iftagsareenabled,we'llbeusingapropertybindingtotheinnerHTMLpropertyofoureditor'soutputHTMLelement.ThisallowsustorendertheHTMLcontent.Inthebinding,we'veusedourTagspipethatwillparsethecontentfortagsusingTagService.
www.EBooksWorld.ir
CompletionofthetagsserviceLet'sdigressforamomentatthispoint.We'vealreadycreatedataggingsystem,andwejustintegrateditintoourEditorcomponentbyusingtheTagspipe.Ifauserwritesprojecttagsinanycommentnow,theywillberenderedbyTagsService.Thisisfantastic!Userscannowestablishcross-linkstootherprojectswithincomments,whichwillbeautomaticallyrenderedaslinksshowingtheprojecttitleandopentasks.Allauserneedstodoisaddthetextrepresentationsofprojecttagstoacomment.Inthedefaultdatasetofthebook,thiscouldbethe#project-1string.
Thefollowingtwoimagesshowyouanexampleofthecommentingsystem.Thefirstimageisanexampleofaneditorineditmode,underthecommentingsystem,whereatexttagisentered:
Anexamplewhereatexttagisentered
Thesecondimageisanexampleofarenderedtagenabledinthecommentingsystemthroughoureditorintegration:
Anexampleofrenderedtagthrougheditorintegration
We'renotdoneyetwhenitcomestoenteringtags.Wecannotexpectouruserstoknowallthe
www.EBooksWorld.ir
availabletagswithinthesystemandthenenterthemmanuallywithincomments.Let'slookathowwecanimprovethisinthenextsection:
Inthissection,welookedatthefollowingconcepts:
1. Webuiltatagsservicethatgenerates,caches,andrenderstags.2. Webuiltastatefulpipeusingthepureflag.3. Weusedthe[innerHTML]propertybindingtorenderHTMLcontentintoanelement.
www.EBooksWorld.ir
SupportingtaginputHere,we'regoingtobuildacomponentanditssupportingstructurestomaketheprocessofenteringtagsasmoothexperienceforourusers.Sofar,theycanwriteprojecttags,butitrequiresthemtoknowtheprojectIDs,whichmakesourtagmanagementquiteuseless.Whatwe'dliketodoisprovidetheuserwithsomechoiceswhentheyareabouttowriteatag.Ideally,weshowthemtheavailabletags,assoonastheystartwritingatagbytypingthehash(#)symbol.
Whatsoundssimpleinthefirstplaceisactuallyquiteatrickythingtoimplement.Ourtaginputneedstodealwiththefollowingchallenges:
Handlinginputeventstomonitortagcreation.Somehow,weneedtoknowwhenauserstartswritingatag,andweneedtoknowwhenthetypedtagnameisupdatedorcanceledbyusinganinvalidtagcharacter.Calculatingthepositionoftheinputcaretoftheuser.Yeah,Iknowthissoundsprettysimple,butitactuallyisn't.Calculatingtheviewportoffsetpositionofauser'sinputcaretrequirestheuseofthebrowser'sSelectionAPI,whichisquitelow-levelandneedssomeabstraction.
Inordertotacklethesechallenges,wearegoingtointroduceautilityclasswecandelegatetheuserinputto.Itwillhelpusfigureoutthedetailswe'reinterestedinanddealwithlow-levelAPIs.
www.EBooksWorld.ir
CreatingataginputmanagerCreateamoduleinanewfileundertags/tag-input-manager.js.Thefirstbitofcodeisafunctionthatwillhelpusfigureoutthepositionoftheuserinputcaretwhentheuserstartstypingatag:
//Thisfunctioncanbeusedtofindthescreencoordinatesofthe
//inputcursorposition
functiongetRangeBoundlingClientRect(){
constselection=window.getSelection();
if(!selection.rangeCount)return;
constrange=selection.getRangeAt(0);
if(!range.collapsed){
returnrange.getBoundingClientRect();
}
constdummy=document.createElement('span');
range.insertNode(dummy);
constpos=dummy.getBoundingClientRect();
dummy.parentNode.removeChild(dummy);
returnpos;
}
Let'snotgointotoomuchdetailhere.WhatthiscodebasicallydoesisthatittriestofindtheboundingboxDOMRectobject,whichdescribesthetop,right,bottom,andleftoffsetsofthecaretpositionrelativetotheviewport.TheproblemisthattheSelectionAPIdoesnotallowustogetthepositionofthecaretdirectly;itonlyallowsustogetthepositionofthecurrentselection.Incasethecaretisnotplacedcorrectly,wewillneedtoinsertadummyelementatthelocationofthecaretandreturntheboundingboxDOMRectobjectofthedummyelement.Ofcourse,we'dneedtoremovethedummyelementagainbeforewereturntheDOMRectobject.
Nowlet'screateanewclass,TagInputManager,underlib/tags/tag-input-manager.js,whichwilldealwiththeuserinputhandlingfortagcreation:
exportclassTagInputManager{
constructor(){
this.reset();
}
...
Intheconstructor,weneedtocallaninternalresetmethod.ThisresetmethodwillresetthetwomemberfieldsthatTagInputManagerwillexpose.Thepositionmemberwillstorethepositionofthelatestcaret,wheretheuserhadstartedwritingatag.ThetextTagmemberwillstorethecurrenttag,whichisrecognizedbyTagInputManager:
reset(){
this.textTag='';
this.position=null;
www.EBooksWorld.ir
}
Nowlet'screateamethodtodetermineifauserisintheprogressofenteringatag.IfthetextTagmembercontainsahashsymbolatthebeginning,wecanassumethatthereisatagenteringinprogress:
hasTextTag(){
returnthis.textTag[0]==='#';
}
Wealsoneedamethodthatwillallowustoupdateboththecurrenttexttag,whichisentered,aswellastheupdatedcaretposition:
updateTextTag(textTag,position=this.position){
this.textTag=textTag;
this.position=position;
}
WithintheonKeyDownmethod,weexpecttoreceivedelegatedkeydownevents.Weareconcernedaboutthebackspace,whichshouldalsoremovethelastcharacterofthetagthatiscurrentlyentered.
onKeyDown(event){
//Ifwereceiveabackspace(keycodeis8),weneedto
//removethelastcharacterfromthetexttag
if(event.which===8&&this.hasTextTag()){
this.updateTextTag(this.textTag.slice(0,-1));
}
}
IntheonKeyPressmethod,weexpecttoreceivedelegatedkeypressevents.Thisiswherethemainlogicofthissupportingclasslies.Here,wehandletwodifferentcases:
Ifthepressedkeyisahashsymbol,wewillstartoverwithanewtag.Ifthepressedkeyisnotavalidwordcharacterorahashsymbol,wewillresetittoitsinitialstate,whichwillcancelthetagentry.Otherwise,it'dmeanthatwearedealingwithavalidtagcharacter,andwe'lladdittothecurrenttexttagstring.
Thecodeforthisisasfollows:
onKeyPress(event){
constchar=String.fromCharCode(event.which);
if(char==='#'){
//Ifthecurrentcharacterfromuserinputisahashsymbol
//wecaninitiateanewtexttagandsetthecurrent
//position
this.updateTextTag('#',getRangeBoundlingClientRect());
}elseif((/[\w-]/i).test(this.textTag[0])){
//Ifthecurrentcharacterisnotavalidtagcharacterwe
//resetourstateandassumethetagentrywascanceled
this.reset();
}elseif(this.hasTextTag()){
//Ifwehaveanyothervalidtagcharacterinput,we're
www.EBooksWorld.ir
//updatingourtexttag
this.updateTextTag(this.textTag+char);
}
}
Okay,sonowwehaveallthesupportweneedtohandletaginput.However,westillneedawaytoshowtheavailabletagsfromTagsServicetotheuser.Forthispurpose,we'llcreateanewTagsSelectcomponent.
www.EBooksWorld.ir
CreatingatagsselectcomponentTosupporttheuserinfindingtherighttag,we'llprovidethemwithadropdownwiththeavailabletags.Todothis,weneedtouseourTagInputManagerclasstorecognizetagswithinuserinputaswellasfiltertheavailabletagswithuserinput.Let'sbrieflylookattherequirementsofthiscomponent:
DisplaytheavailabletagsgatheredfromTagsServiceinatooltip/calloutboxItshouldsupportalimitationofdisplayedtagsItshouldsupportaninputtofiltertheavailabletagsThecomponentshouldacceptaninputparametertopositionthecalloutboxItshouldemitaneventoncetheuserclicksonataginthelistedtagsThecomponentshouldhideitselfifthefilterisinvalidoriftherearenoelementsmatchingthefilter:
Finishedtagsselectcomponentfilteredwithuserinput
Let'sstartwiththecomponentclassandseehowwefulfilltheserequirements.First,createanewfilecalledtags-select.jsundertags/tags-select:
...
@Component({
selector:'ngc-tags-select',
...
})
exportclassTagsSelect{
...
}
[email protected]'sstartwithimplementingtheinnardsofourcomponent.First,we'lldefinethefollowinginputinthecomponent:
@Input()filter;
www.EBooksWorld.ir
Usingthefilterinput,wecanpassafiltertagtotheTagsSelectcomponent.Thismeansthatwe'llusethefilterinputtofiltertheavailabletagsbytitleandtexttags.
Thelimitinputcanbesettoanynumber.Thisinputisusedtolimitthenumberoffilteredtagsthatcouldbedisplayedwithinthecomponent:
@Input()limit;
ThepositioninputshouldbesettoavalidDOMRectobjectthatcontainsthetopandleftproperties.Theywillbeusedtopositionourcomponent:
@Input()position;
ThetagSelectedoutputpropertyisusedtoemitaneventoncetheuserhasclickedonatagwithinthelistoftags:
@Output()tagSelected=newEventEmitter();
Thefollowingaccessorpropertyisboundtothehostelement'sdisplaystyleproperty.Itwillcontrolwhetherthecomponentisdisplayedorhidden.Weonlydisplaythecomponentifthefilterisvalidandthefilteredtagscontainatleastonetag:
@HostBinding('style.display')
getisVisible(){
if(this.filter[0]==='#'&&this.filteredTags.length>0){
return'block';
}else{
return'none';
}
}
Thefollowingtwoaccessorpropertiesusehostbindingstosetthetopandleftstylesofourhostelementbasedonthepositioninputofthecomponent:
@HostBinding('style.top')
gettopPosition(){
returnthis.position?`${this.position.top}px`:0;
}
@HostBinding('style.left')
getleftPosition(){
returnthis.position?`${this.position.left}px`:0;
}
Let'sinjectTagsServiceintoourcomponentsowecanaccessthelistofavailabletags:
constructor(@Inject(TagsService)tagsService){
this.tagsService=tagsService;
//Thismemberisstoringthefilteredtaglist
this.filteredTags=[];
this.filter='';
}
www.EBooksWorld.ir
WeneedtousetheOnInitlifecyclehooktosetupasubscriptiontotheTagServicechangeobservable.Thisway,we'llgetaccesstotheinitiallistoftagsaswellasanychangesinthelist.Afterwereceiveanewlistoftags,wewillneedtoreapplythefiltering:
ngOnInit(){
//TheTagsServiceisprovidinguswithallavailabletags
//withintheapplication
this.tagsSubscription=this.tagsService.change.subscribe(
(tags)=>{
//Iftheavailabletagschangewestorethenewlistand
//executefilteringagain
this.tags=tags;
this.filterTags();
}
);
}
Thefollowingisthemethodthatwillbecalledfromthetemplateifatagisclicked.We'lljustre-emitthattagusingthetagSelectedoutput:
onTagClick(tag){
this.tagSelected.next(tag);
}
ThefilterTagsmethodisresponsibleforfilteringandlimitingourtaglistbasedonthefilterandlimitinputpropertiesandtheavailabletagsfromTagsService.Asaresult,itwillstorethefilteredandlimitedlistinthefilteredTagsmemberfield:
filterTags(){
this.filteredTags=this.tags
.filter((tag)=>{
returntag.textTag.indexOf(this.filter.slice(1))!==-1||
tag.title.indexOf(this.filter.slice(1))!==-1;
})
.slice(0,this.limit);
}
Iftheinputpropertiesfilterorlimitchanges,wewillneedtoreapplyourfilteringmethod.ByimplementingthengOnChangeslifecyclehook,wecaneasilymanagethisrequirement:
ngOnChanges(changes){
//Ifthefilterorthelimitinputchanges,we'refilteringthe
//availabletagsagain
if(this.tags&&(changes.filter||changes.limit)){
this.filterTags();
}
}
Finally,weshouldunsubscribefromtheTagsServicechangeobservableiftheTagsSelectcomponentisdestroyed:
ngOnDestroy(){
this.tagsSubscription.unsubscribe();
www.EBooksWorld.ir
}
Thetemplateforourcomponentisrathersimple.Let'slookattheviewtemplatethatisstoredundertags/tags-select/tags-select.html:
<ulclass="tags-select__list">
<li*ngFor="lettagoffilteredTags"
(click)="onTagClick(tag)"
class="tags-select__item">{{tag.title}}</li>
</ul>
WeusedtheNgFordirectivetoiterateoverallthetagswithinthefilteredTagsmember.Ifatagisclicked,wewillneedtoexecutetheonTagClickedmethodandpassthetagofthecurrentiteration.Inthelisting,we'llonlydisplaythetagtitlethatshouldhelptheuseridentifythetagtheywouldliketouse.
Nowwehavebuiltallthepiecesthatweneedtoenablesmoothtagenteringforourusers.Let'spatchourEditorcomponentagaintoincludeourchanges.
www.EBooksWorld.ir
IntegratingtaginputwithintheeditorcomponentAsthefirststep,weshouldamendourEditorcomponenttoutilizetheTagInputManagerclass.Weneedtodelegatetheuserinputinsidethecontent-editableelementtothetaginputmanagersoitcandetectanytagentering.Then,we'llusetheinformationfromTagInputManagertocontrolaTagsSelectorcomponent.
First,let'slookattherequiredchangestobemadeinsidetheComponentclasslocatedunderui/editor/editor.js:
...
import{TagsSelect}from'../../tags/tags-select/tags-select';
import{TagInputManager}from'../../tags/tag-input-manager';
@Component({
selector:'ngc-editor',
...
directives:[TagsSelect]
})
exportclassEditor{
...
//We'reusingaTagInputManagertohelpusdealingwithtag
//creation
this.tagInputManager=newTagInputManager();
}
...
//Thismethodiscalledwhentheeditableelementreceivesa
//keydownevent
onKeyDown(event){
//We'redelegatingthekeydowneventtotheTagInputManager
this.tagInputManager.onKeyDown(event);
}
//Thismethodiscalledwhentheeditableelementreceivesa
//keypressevent
onKeyPress(event){
//We'redelegatingthekeypresseventtotheTagInputManager
this.tagInputManager.onKeyPress(event);
}
//ThismethodiscalledifthechildTagSelectcomponentis
//emittinganeventforaselectedtag
onTagSelected(tag){
//Wereplacethepartialtexttagwithintheeditorwiththe
//textrepresentationofthetagthatwasselectedinthe
//TagSelectcomponent.
this.setEditableContent(
this.getEditableContent().replace(
this.tagInputManager.textTag,tag.textTag
)
);
this.tagInputManager.reset();
}
...
www.EBooksWorld.ir
}
Inour@Componentannotation,weaddedtheTagsSelectcomponenttothedirectivespropertysowecouldusethecomponentwithinthetemplate.
Tohelpusdoallthelow-levelprocessingfortagentry,weusedTagInputManagerandcreatedanewinstanceofitwithinthecomponentconstructor.
Wehavenowcreatedtwomethodsforhandlingkeypressandkeydowneventscomingfromourcontent-editableelement.ThesemethodsdelegatetheeventstoTagInputManager,whichwillhandlealloftheprocessingtoextractatexttagandthepositionofthecaret.
Finally,weaddedamethodthatwillbecalledonceatagisclickedwithintheTagsSelectcomponent.Here,wesimplyreplacedthetexttagthatiscurrentlyenteredwiththetextrepresentationofthetagthatwasclicked.Thisprovidesanaiveimplementationofsomesortofautocomplete.Afterweaddedthetextrepresentationoftheclickedtagtothecontent-editableelement,weresetTagInputManagertoclearitsstate.
TheonlybitleftnowistotoeditthetemplateoftheEditorcomponentinordertoincludetheTagsSelectcomponent.
Intheui/editor/editor.htmlfile,weneedtomakethefollowingchanges:
...
<ngc-tags-select*ngIf="enableTags"
[filter]="tagInputManager.textTag"
[position]="tagInputManager.position"
[limit]="5"
(tagSelected)="onTagSelected($event)">
</ngc-tags-select>
TheNgIfdirectivehelpsusavoidthecomponentfrombeingcreatediftagsarenotenabledwithintheeditor.
WesetthefilterandpositioninputoftheTagsSelectcomponentfromthedatawehaveinourTagInputManagerinstance.
OntheemittedtagSelectedeventoftheTagsSelectcomponent,wecalledtheonTagSelectedmethodontheEditorcomponentwecreatedamomentago.
That'sallweneedtodowiththetemplateoftheEditorcomponent.
www.EBooksWorld.ir
FinishingupourtaggingsystemCongratulations!You'venowsuccessfullyimplementedthefirstofthethreeusabilitycomponents.
WiththehelpofaTagInputManagerclass,weoffloadedheavylow-levelhandlingofuserinputandtheprocessingoftheusercaretposition.Then,wecreatedacomponenttodisplaytheavailabletagstotheuserandprovidedawayforthemtoselectatagbyclickingonit.InourEditorcomponent,weusedtheTagInputManagerclasstogetherwiththeTagsSelectcomponenttoenablethesmoothenteringoftagswhileeditingcommentsandotherareaswherewe'veenabledtagging.
We'vecoveredthefollowingconceptsinthissection:
1. Weprocessedcomplexuserinputwithinadesignatedmanagerclasstooffloadlogicfromourcomponents.
2. Weusedhostbindingstosetpositionalstyleattributes.3. Weimplementedfullyreactivecomponentsthatrelyonobservablesanddon'tcreateside
effectsduringchangedetection.
www.EBooksWorld.ir
DraganddropWehavelearnedtouseourcomputermouseandkeyboardwithgreatefficiency.Usingkeyboardshortcuts,differentclickactionsandcontextualmousemenussupportusnicelywhenperformingtasks.However,thereisonepatternthathasgainedmoreattentionagaininapplicationslately,giventhecurrentmobileandtouchdeviceshype.Draganddropactionsareaveryintuitiveandlogicalwaytoexpressactionssuchasmovingorcopyingitems.Onetaskperformedonuserinterfacesbenefitsfromdraganddropparticularly,whichisorderingitemswithinalist.Ifweneedtoorderitemsviaactionmenus,itgetsveryconfusing.Movingitemsstepbystepusingtheupanddownbuttonsworksgreat,butittakesalotoftime.Ifyoucandragitemsaroundanddroptheminaplacewhereyou'dlikethemtobereordered,youcansortalistofitemsextremelyfast.
Inthistopic,wewillbuildtherequiredelementstoenabledraganddropselectively.Wewillusethedraganddropfeaturetoenableuserstoreordertheirtasklists.Bydevelopingreusabledirectivestoprovidethisfunctionality,wecanenablethefeatureatanyotherspotwithinourapplicationlateron.
Toimplementourdirectives,wewillmakeuseoftheHTML5draganddropAPI,whichissupportedinallthemajorbrowsersatthetimeofwritingthisbook.
Sincewewouldliketoreuseourdraganddropbehavioronmultiplecomponents,wewillusedirectivesfortheimplementation.Wearegoingtocreatetwodirectivesinthissection:
Draggabledirective:Thisdirectiveshouldbeattachedtocomponents,whichshouldbeenabledfordraggingDraggabledropzonedirective:Thisdirectiveshouldbeattachedtocomponentsthatwillactasadroptarget
We'llalsoimplementafeaturewherewecanbeselectiveaboutwhatcanbedraggedwhere.Forthis,wewilluseatypeattributeonourdraggabledirectivesaswellanacceptedtypeattributeonourdropzones.
www.EBooksWorld.ir
ImplementingthedraggabledirectiveThedraggabledirectivewillbeattachedtotheelementthatcanbedraggedontootherelements.Let'sgetstartedwithcreatinganewdirectiveclassunderdraggable/draggable.js:
...
@Directive({
selector:'[draggable]',
host:{
class:'draggable',
//AdditionallytotheclasswealsoneedtosettheHTML
//attributedraggabletoenabledraggablebrowserbehavior
draggable:'true'
}
})
exportclassDraggable{
...
}
Insteadofusingthe@Componentannotation,we'venowusedthe@DirectiveannotationtoletAngularknowthatthefollowingclassisadirectiveclass.BysettingtheHTMLattributedraggabletotrue,wetellthebrowserthatwe'reconsideringthiselementadraggableelement.
Tip
Thebigdifferenceofusingdirectivesincomparisontocomponentsisthattheydon'tembraceaviewbutonlybehavior.Therefore,it'salsopossibletousemanydirectivesonthesameelement,whichisnotpossiblewithcomponents.
Let'slookattheinputforournewlycreatedcomponentclass:
@Input()draggableData;
ThedraggableDatainputisusedtospecifythedatathatrepresentstheelementwhichcanbedragged.ThisdatawillbeserializedtoJSONandtransferredtoourdropzonesonceadragactioniscompleted.
Byspecifyingadraggabletype,wecanbemoreselectivewhentheelementisdraggedoveradropzone.Withinthedropzone,wecanhaveacounterpartthatcontrolswhattypesareacceptabletobedropped.
@Input()draggableType;
Additionallytoourinput,wealsowanttouseahostbindingtosetaspecialclassiftheelementiscurrentlydragged:
@HostBinding('class.draggable--dragging')dragging;
Thisbindingwillsetadraggable--draggingclass,whichwillapplysomespecialstylesthat
www.EBooksWorld.ir
willmakeiteasytorecognizethatanelementisdragged.
Nowweneedtohandletwoeventswithinourdirectivetoimplementthebehaviorofadraggableelement.ThefollowingDOMeventsaretriggeredbythedraganddropDOMAPI:
dragstart:Thiseventisemittedonelementsthataregrabbedandmovedacrossthescreendragend:Ifthepreviouslyinitiateddraggingoftheelementisended,becauseofasuccessfuldroporareleaseoutsideofavaliddroptarget,thisDOMeventwillbetriggered.
Let'slookattheimplementationofHostListenerforthedragstartevent:
//We'relisteningforthedragstarteventandinitializethe
//dataTransferobject
@HostListener('dragstart',['$event'])
onDragStart(event){
event.dataTransfer.effectAllowed='move';
//SerializeourdatatoJSONandsetitonourdataTransfer
//object
event.dataTransfer.setData(
'application/json',
JSON.stringify(this.draggableData));
//ByaddingthedraggableTypeasadatatypekeywithinour
//ThedataTransferobject,weenabledropzonestoobservethetype
//beforereceivingtheactualdrop.
event.dataTransfer.setData(
`draggable-type:${this.draggableType}`,'');
this.dragging=true;
}
Nowlet'sdiscussthedifferentactionswewillperformintheimplementationofourhostlistener:
1. WewillneedtoaccesstheDOMeventobjectinourhostlistener.Ifweweretocreatethisbindingwithinthetemplate,wewouldprobablyneedtowritesomethingsimilartothis:(dragstart)="onDragStart($event)".Withineventbindings,wecanmakeuseofthesyntheticvariable$event,whichisareferencetotheeventthatwouldhavetriggeredtheeventbinding.Ifweweretocreateaneventbindingonourhostelementusingthe@HostListenerannotation,wewouldneedtoconstructtheparameterlistforthebindingusingthesecondargumentofthedecorator.
2. ThefirstactioninoureventlisteneristosetthedesiredeffectAllowedpropertyonthedataTransferobject.Currently,weonlysupportthemoveeffectasourmainconcernistoreordertaskswithinthetasklistusingdraganddrop.ThedraganddropAPIisverysystem-specific,butusuallytherearedifferentdrageffectsifauserholdsamodifierkey(suchasCtrlorShift)whileinitiatingthedragging.Withinourdraggabledirective,wecanforcethemoveeffectforalldragactions.
3. Inthenextcodesnippet,wewillsetthedatathatshouldbetransferredbydragging.It'simportanttounderstandthecorepurposeofthedraganddropAPI.Itdoesnotonly
www.EBooksWorld.ir
provideawaytoimplementdraganddropforelementssolelyinyourDOM,butitalsosupportsthedraggingoffilesandotherobjectsintoyourbrowser.Becauseofthis,theAPIundergoessomeconstraints,whereoneofthemismakingitimpossibletotransferdataotherthansimplestringvalues.Inorderforustotransfercomplexobjects,wewillserializethedatafromthedraggableDatainputusingJSON.stringify.
4. AnotherlimitationcausedbysomesecurityconstraintswithintheAPIisthatdatacanonlybereadafterasuccessfuldrop.Thismeansthatwecannotinspectthedataiftheuserisjusthoveringoveranelement.However,weneedtoknowsomefactsaboutthedatawhenhoveringdropzones.Weneedtoknowthetypeofthedraggableelementwhenenteringadropzonesowecanmakethedropzonesignalifthetypeisaccepted.We'reusingasmallworkaroundforthisissue.ThedraganddropAPIhidesthedatawhenwedragdataoveradroptarget.However,ittellsuswhattypeofdataitis.Knowingthisfact,wecanusethesetDatafunctiontoencodeourdraggabletype.Accessingthedatakeysonlyisconsideredsecureandthereforecanbedoneinalldropzoneevents.
5. Finally,we'llsetthedraggingflagtotrue,whichwillcausetheclassbindingtorevalidateandaddthedraggable--draggingclasstotheelement.
Afterdealingwiththedragstartevent,weonlyneedtohandlethedragendeventtocompleteourDraggabledirective.TheonlythingwedowithintheonDragEndmethodthatisboundtothedragendeventissetthedraggingmembertofalse.Thiswillcausethedraggable--draggingclasstoberemovedfromthehostelement:
@HostListener('dragend')
onDragEnd(){
this.dragging=false;
}
That'sitforthebehaviorofourDraggabledirective.Nowweneedtocreateitscounterpartdirectivetoprovidethebehaviorofadropzone.
www.EBooksWorld.ir
ImplementingadroptargetdirectiveDropzoneswillactascontainerswheredraggableelementscanbedropped.Forthis,we'llcreateanewdirectivecalledDraggableDropZoneunderdraggable/draggable-drop-zone.js:
@Directive({
selector:'[draggableDropZone]'
})
exportclassDraggableDropZone{
...
}
There'snothingspecialaboutthis@Directiveannotation.WeusedanattributeselectorsoitcanbeattachedusingadraggableDropZoneattributeonanyHTMLelement.Usingthefollowinginput,wecanspecifywhattypesofdraggableelementsweacceptinthisdropzone.Thiswillhelptheuseridentifywhethertheyareabletodropoffthedraggableelementswhenapproachingthedropzone:
@Input()dropAcceptType;
Uponsuccessfuldropsintothedropzone,wewillneedtoemitaneventsothatthecomponentsusingourdraganddropfunctionalitycanreactaccordingly.Forthispurpose,let'screateadropDraggableoutputproperty:
@Output()dropDraggable=newEventEmitter();
Theovermemberfieldwillstorethestateifanacceptedelementisintheprocessofbeingdraggedoverthedropzone:
@HostBinding('class.draggable--over')over;
ThefollowingmethodwillbeusedtocheckwhetherourdropzoneshouldacceptanygivendraganddropeventbycheckingagainstourdropAcceptTypemember.IfyourememberthesecurityproblemsweneededtoworkaroundwithwhencreatingtheDraggabledirective,youwillunderstandwhythisdeterminationisrathersimple:
typeIsAccepted(event){
constdraggableType=
Array.from(event.dataTransfer.types).find(
(key)=>key.indexOf('draggable-type')===0
);
returndraggableType&&
draggableType.split(':')[1]===this.dropAcceptType;
}
WecanonlyreadthetypesofthedatawithindataTransferobjectsforcertainevents,wherethedataitselfishiddenuntilasuccessfuldropeventisoccurred.Tobypassthissecuritylimitation,we'veencodedthedraggabletypeinformationintoadatakeyitself.Sincewecanlistallthedatatypessafely,it'snottoohardtoextracttheencodeddraggabletypeinformation.Wewillsearchforadatatypekeythatstartswith"draggable-type"andthen
www.EBooksWorld.ir
splititbythecolumncharacter.Thevalueafterthecolumncharacterisourtypeinformation,whichwewillthencompareagainstthedropAcceptTypedirectiveinputproperty.
Wewillusetwoeventstodeterminewhetheradraggableelementismovedtoourdropzone:
dragenter:Thisisfiredbyanelementifanotherelementisdraggedoveritdragleave:Thisisfiredbyanelementifthepreviouslyenteredelementhasleftagain
There'soneproblemwiththeprecedingevents,whichisthattheyactuallybubble,andwewillreceiveadragleaveeventifthedraggedelementismovedtoachildelementwithinourdropzone.Becauseofthebubbling,wethenalsoreceivedragenteranddragleaveeventsfromthechildelements.Thisisnotdesiredinourcase,andweneedtobuildsomefunctionalitytoimprovethisbehavior.WewillmakeuseofacountermemberfielddragEnterCount,whichwillcountuptoallthedragentereventsandcountdowntodragleaveevents.Thisway,wecannowsaythatonlyondragleaveevents,wherethecounterbecomeszero,wewillactuallyleavetheinsideofourdropzone.Let'slookatthefollowingdiagramthatillustratestheproblem:
Visualizationofimportantvariablesandfunctionsforourcalculations
Let'simplementthislogictobuildaproperenterandleavebehaviorofourdropzonewithinthedraggable/draggable-drop-zone.jsfile:
constructor(){
//Weneedthiscountertoknowifadraggableisstilloverour
//dropzone
www.EBooksWorld.ir
this.dragEnterCount=0;
}
//Thedragentereventiscapturedwhenadraggableisdragged
//intoourdropzone
@HostListener('dragenter',['$event'])
onDragEnter(event){
//Onlyhandleeventifthedraggableisacceptedbyourdrop
//zone
if(this.typeIsAccepted(event)){
this.over=true;
//Weusethiscountertodetermineifweloosefocusbecause
//ofchildelementorbecauseoffinalleave
this.dragEnterCount++;
}
}
//Thedragleaveeventiscapturedwhenthedraggableleavesour
//dropzone
@HostListener('dragleave',['$event'])
onDragLeave(event){
//UsingdragEnterCount,wedetermineifthedragleaveeventis
//becauseofchildelementsorbecausethedraggablewasmoved
//outsidethedropzone
if(this.typeIsAccepted(event)&&--this.dragEnterCount===0){
this.over=false;
}
}
Withinboththeevents,wefirstcheckwhethertheeventiscarryingadataTransferobjectofwhichweacceptthetype.AftervalidatingthetypeusingourtypeIsAcceptermethod,wedealwiththecounterandsettheovermemberfieldifrequired.
Weneedtohandleanothereventthatisimportantfordraganddropfunctionality,whichisthedragoverevent.Withinthedragoverevent,wecansettheaccepteddropEffectofthecurrentdraggingaction.Thiswilltellourbrowserthattheinitiateddraggingactionfromourdraggableissuitableforthisdropzone.It'salsoimportantthatwepreventthedefaultbrowserbehaviorsothere'snothinginthewayofourcustomdraganddropbehavior.Let'saddanotherfunctiontocoverthoseconcerns:
@HostListener('dragover',['$event'])
onDragOver(event){
//Onlyhandleeventifthedraggableisacceptedbyourdrop
//zone
if(this.typeIsAccepted(event)){
//Preventanydefaultdragactionofthebrowserandsetthe
//dropEffectofthedataTransferobject
event.preventDefault();
event.dataTransfer.dropEffect='move';
}
}
Finally,weneedtohandlethemostimportanteventinthedropzone,whichisthedropevent
www.EBooksWorld.ir
thatistriggeredifauserdropsadraggableintoourdropzone:
//Thiseventwillbecapturedifadraggableelementisdropped
//ontoourdropzone
@HostListener('drop',['$event'])
onDrop(event){
//Onlyhandleeventifthedraggableisacceptedbyourdrop
//zone
if(this.typeIsAccepted(event)){
//Firstobtainthedataobjectthatcomeswiththedropevent
constdata=JSON.parse(
event.dataTransfer.getData('application/json')
);
//Aftersuccessfuldrop,wecanresetourstateandemitan
//eventwiththedata
this.over=false;
this.dragEnterCount=0;
this.dropDraggable.next(data);
}
}
Aftercheckingwhetherthedroppedelementisofanacceptedtype,wecannowgoaheadandreadthedataTransferobjectdatafromtheevent.ThisdatawaspreviouslysetbytheDraggabledirectiveandneedstobedeserializedusingJSON.parse.
Sincethedropwassuccessful,wecanresetourdragEnterCountmemberandsettheoverflagtofalse.
Finally,wewillemitthedeserializeddatafromthedraggableelementusingourdropDraggableoutputproperty.
That'sallweneedtohaveahighlyreusabledraganddropbehaviorthatwecannowattachtoanycomponentswithinourapplicationwherewefeeltheneed.
www.EBooksWorld.ir
IntegratingdraganddropintasklistcomponentWecannowusetheDraggableandDraggableDropZonedirectivesinourTaskListcomponentsowecanenablethereorderingoftasksusingdraganddrop.
Thewaywe'regoingtodothisisbyattachingboththedirectivestothetaskelementswithintheTaskListcomponenttemplate,wherewe'llrenderthem.Yeah,that'sright!WewanttomakeourTaskcomponentadraggablebutalsoadropzoneatthesametime.Thisway,wecandroptasksintoothertasks,andthisgivesusthefoundationforreordering.Whatwewilldoisreorderthelistinadropsothatthedroppedtaskwillbesqueezedintothepositionrightbeforethetaskwhereitwasdropped.
First,let'sapplythedirectivestothe<ngc-task>elementintheTaskListcomponenttemplate,namelytask-list/task-list.html:
<divclass="task-list__l-container">
...
<ngc-task*ngFor="lettaskoffilteredTasks"
line:[task]="task
(taskUpdated)="onTaskUpdated(task,$event)"
(taskDeleted)="onTaskDeleted(task)"
draggable
draggableType="task"
[draggableData]="task"
draggableDropZone
dropAcceptType="task"
(dropDraggable)="onTaskDrop($event,task)">
</ngc-task>
...
</div>
Alright,usingtheprecedingattributes,wehavemadeourtasksnotonlyadraggable,butalsoadropzone.ByspecifyingbothdraggableTypeanddropAcceptTypetothe"task"string,wearetellingourdraganddropbehaviorthatthesetaskelementscanbedroppedintoothertaskelements.OurDraggableDropZonedirectiveissettoemitadropDraggableeventwheneveravaliddraggableisdroppedoff.Tohandledroppedtasks,wecansimplyusethiseventandcreateabindingtoamethodinourTaskListcomponent.
Let'sseewhatweneedtochangewithinourComponentclass,locatedundertask-list/task-list.js,tomakethiswork:
...
import{Draggable}from'../draggable/draggable';
import{DraggableDropZone}from'../draggable/draggable-drop-zone';
@Component({
selector:'ngc-task-list',
...
directives:[...,Draggable,DraggableDropZone]
})
www.EBooksWorld.ir
exportclassTaskList{
...
onTaskDrop(source,target){
if(source.position===target.position){
return;
}
lettasks=this.tasks.slice();
constsourceIndex=tasks.findIndex(
(task)=>task.position===source.position
);
consttargetIndex=tasks.findIndex(
(task)=>task.position===target.position
);
tasks.splice(targetIndex,
0,
tasks.splice(sourceIndex,1)[0]);
tasks=tasks.map((task,index)=>{
returnObject.assign({},task,{
position:index
});
});
this.tasksUpdated.next(tasks);
}
...
}
Let'selaborateonthebehaviorwe'llseewithintheonTaskDropmethodthatisboundtotheDropZone'sdropDraggableeventinourtemplate:
1. Ifyoucheckthetemplateagain,youwouldseethatweboundtotheonTaskDropmethodwiththefollowingexpression:(dropDraggable)="onTaskDrop($event,task)".Sincethedropzoneemittedaneventwithde-serializeddatathatwasboundusingthedraggableinputpropertydraggableData,wecansafelyassumethatwewillreceiveacopyofthetaskthatwasdroppedintothedropzone.Asasecondparametertoourbinding,weaddedthelocalviewvariabletask,whichisactuallythetaskthatactsasthedropzone.Therefore,wecansaythatthefirstparameterofouronTaskDropmethodrepresentsthesource,whilethesecondrepresentsthetargettask.
2. Asafirstcheckinourmethod,wecomparethesourcepositionwiththetargetposition,andiftheymatch,wecanassumethatthetaskwasdroppedbyitselfandwedon'tneedtoperformanyfurtheractions.
3. Nowwecangetthesourceandtargettaskindiceswithinourtasksarrayandexecuteanestedsplicesothatwecanremovethesourcefromitsoldpositionwithinthearrayandadditrightbeforethepositionofthetarget.
4. Allthat'slefttodonowisrecalculatethepositionfieldsofthetaskssothattheyreflectthereorderedarray.WecandothiseasilybyusingArray.prototype.map.
5. Asthelaststep,weneedtonotifyourparentcomponentthatwe'veupdatedthetasklist.WecansimplyusethetaskUpdatedeventtodoso.Wehaveusedthatsameeventwhentaskswereaddedorremoved.
www.EBooksWorld.ir
Howgreatisthat?Wehavesuccessfullyimplementeddraganddroponourtasklisttoprovideaveryusablefeaturetoreordertasks.
www.EBooksWorld.ir
RecapitulateondraganddropWiththeuseofthelow-leveldraganddropAPI,usingeventsanddataTransferobjects,wehaveimplementedtwodirectivesthatcannowbeusedtoexecutesmoothdraganddropfunctionalitywithinourapplicationwhereverwedesire.
Withalmostnoeffort,wehaveimplementedourdraganddropbehavioronthetasklisttoprovideanicefeaturetoreorderthetaskswithinthelist.Theonlythingweneededtodo,besideshookingupthedirectives,wastoimplementamethodwherewecouldreorderthetasksbasedontheinformationfromtheDraggableDropZoneevent.
Wehaveworkedwiththefollowingconceptsinthissection:
1. WelearnedthebasicsofHTML5draganddropAPI.2. WeusedthedataTransferobjecttosecurelytransferdatawithindraganddropevents.3. Builtreusablebehaviorpatternsusingdirectives.4. EnrichedthestandarddraganddropAPIbyprovidingourowncustomselection
mechanismsusingacustomdatatypethatencodesdraggable-typeinformation.
www.EBooksWorld.ir
Toinfinityandbeyond!Displayingasimplelistwithanaveragesizedoesnotcomewithalotofchallenges.Assoonasthelistsstartstogrow,challengesstarttoappear.Wecaneasilyoverwhelmauserwithaverylonglist.Longlistscanalsohaveaperformanceimpactonourapplication,especiallywhenitdisplaysdynamiccontent.
Onewaytoaddressthechallengefacedwhendisplayinglonglistsistoprovidepagination.However,pagesdonotalwaystranslateverywell.Whileusingpaginationonadesktopdevicewithamouseseemsveryintuitive,itbecomescumbersomeonmobiledeviceswithtouchsupport.
Inthischapter,we'lllookatadifferentapproachthatcanhelpusmitigatetheperformanceimplicationsoflonglistswhileprovidingasmoothexperienceonmobiledevices.Weareusingapatternsometimesreferredtoasinfinitescrolling.Thegoalistodisplayonlyenoughitemswithinthelisttofillthescreen,andloadmoreitemsondemandiftheuserscrollsdown.
Toimplementsuchabehavior,wecouldwriteawrappercomponentthatwillprovideaninfinitescrollpaneandusecontentinsertiontoembraceourlist.However,wewilluseadifferentapproachtoimplementourinfinitescrollbehaviorandbuildacustomtemplatedirectivesuchasNgFor.
www.EBooksWorld.ir
TheasterisksyntaxandtemplatesWehaveusedtheNgForandNgIfdirectivesquitealotsofarusingtheasterisk(*)symboltoindicatewe'redealingwithadirectivethatcreatesatemplate.However,wehaven'tlookedattheanatomyoftheasterisktemplatesyntax.Imaginethatitwillcreatesomesortofsyntacticsugarforourtemplate.
CheckoutthisexampleofusingtheNgFordirectivewiththeasterisktemplatesyntax:
<div*ngFor="letiof[1,2,3]">{{i}}</div>
ThetemplateparserofAngularwillhandlealltheattributesthatstartwithanasteriskinaspecialway.Theprecedingexampleisaneasierandmoreconcisewritingstyleofthefollowing:
<templatengFor#i[ngForOf]="[1,2,3]">
<div>{{i}}</div>
</template>
Boththeprecedingexamplesareabsolutelyidentical.Templatedirectives,suchasNgFororNgIf,makeuseofHTMLtemplateelements,whichwe'vebrieflydiscussedinChapter1,Component-BasedUserInterfaces.ThereasonthattheAngularcommondirectivesNgFor,NgIf,andNgSwitchuseHTMLtemplateelementsisactuallyquiteobviousifyouthinkabouttheirnature.AllthreedirectivesneedtoinsertandremovelargeregionsofourDOMdynamically.NgIf,forexample,insertsorremovestheelementit'sattachedto,basedonacondition.Byleveragingtemplateelements,thiscanbesupportedbythebrowser'snativefunctionality.
Note
Ifyoucomparetheexamplesdiscussedhere,it'sobviousthatthefirstwritingstyleismuchsimplertodealwith.Askingyoutowriteaseparatetemplateelementeverytimeyou'dwanttouseNgFororNgIfwouldbequiteapain.Thisistheonlyreasonwhytheasterisksyntaxexists,itsraisond'êtreifyoulike.Insteadofwritingatemplateelementdirectly,wecanuseanasteriskonanattributeandAngularwilltransformtheHTMLportionintoatemplateelementforus.
TheNgFordirectiveusesaTemplateRefdependency,whichcanbeinjectedintotheconstructorofthedirective,toinstantiatethetemplateormultipleinstancesofit,asdesired.The[ngForOf]propertybindingisgeneratedduringde-sugaringbyappendingthewordOfwithintheNgForexpressiontothedirectivename,ngFor.ThebindingiscreatedbytheNgFordirective,whichacceptsaninput,ngForOf.
Considerthefollowingexample:
<div*test="letvariablewithSugartrue">{{variable}}</div>
www.EBooksWorld.ir
Angularwouldde-sugarthisintothefollowingcode:
<templatetest#variable[testWithSugar]="true">
<div>{{variable}}</div>
</template>
That'sjustthewayAngularde-sugarstheasterisktemplatesyntax.It'sashortcuttoattachdirectivesaswellasoneinputbindingstotemplateelements.
There'sstillonethingthatmightlookconfusing,whichisthevariableattributeinthetemplate.Let'slookatanotherexampleofusingtheNgFordirectivebyaliasingtheexposedlocalvariablesofadirectivelikethecurrentindex:
<div*ngFor="letnof[1,2,3];leti=index">{{i}}:{{n}}</div>
Thisexamplewillbede-sugaredtothefollowingtemplate:
<templatengFor#n#i="index"[ngForOf]="[1,2,3]">
<div>{{i}}:{{n}}</div>
</template>
Sowecannowtellfromde-sugaringthatadditionalaliasesormappingswillgetcreatedasvariablemappingsinourtemplateelement.TheindexthatisexposedwithinthecodeoftheNgFordirectiveclassasalocalviewvariableismappedtoalocalviewvariablewithintheinstantiatedcontentofthetemplate.
Sowhat'sgoingonwiththelocalviewvariablenwithinourinstantiatedtemplates?Whycanweaccessn,whenthere'sjustonevariableattributewithoutanyvaluethatwouldtelluswhereit'smappedto?
Wehavelearnedthatwhenweusehashsymbolattributesonregularelements,wecreatealocalviewreference.Wecanusethisreferenceasanidentifierintheviewdirectly,[email protected],whentheviewcompilerofAngulardiscoverswhatlookslikealocalviewreferenceonatemplate,thebehaviorisabitdifferent.
WhatisinvisibletousisthatAngularactuallyimpliesadefaultvalueforvariableattributesontemplateelementsthatdon'thaveanattributevalue.Itwillcreateamappingforalocalviewvariablecalled$implicit.Youcanthinkof$implicitasadefaultvaluethatcanbeexposedindirectivesaslocalviewvariablesandwillprovidesomeeaseofusewhendealingwithtemplateelements.
Itwouldalsobetotallyvalidtowritetheprecedingexampleasfollows:
<templatengFor#n="$implicit"#i="index"[ngForOf]="[1,2,3]">
<div>{{i}}:{{n}}</div>
</template>
Here,theNgFordirectiveisexposingalocalviewvariable$implicit,whichisareferencetothecurrentvalueassociatedwiththeinstanceduringtheiterationoverthearrayitreceives
www.EBooksWorld.ir
withinthengForOfinput.Usingaplainvariableattributewithoutavalue,Angularwilldefaultamappingto$implicit.Becausewedon'twanttowritethismappingallthetimeourselves,wecanjustspecifyanemptyvariableattributeandAngularwillassumeitforus.
www.EBooksWorld.ir
CreatinganinfinitescrolldirectiveSincewenowknowabitmoreabouttemplateelementsandhowAngulardealswiththeasterisksyntax,wecanactuallycreateourowncopyofNgFor,whichisadditionallydealingwiththebehaviorofinfinitescrolling.
Let'screateanewfileforourdirectiveunderinfinite-scroll/infinite-scroll.js:
...
@Directive({
selector:'[ngcInfiniteScroll]'
})
exportclassInfiniteScroll{
...
//Thisinputwillbesetbytheforoftemplatesyntax
@Input('ngcInfiniteScrollOf')
setinfiniteScrollOfSetter(value){
this.infiniteScrollOf=value;
...
}
...
applyChanges(changes){
...
this.bulkInsert(insertTuples).forEach((tuple)=>
tuple.view.context.$implicit=tuple.record.item);
}
...
}
WestartoffbydeclaringaregulardirectivethatissensitivetotheattributeselectorngcInfiniteScroll.Theprecedingcodeexcerptonlyshowstherelevantcodeforthetemplateelementhandlingwediscussedintheprevioustopic.Therearesomecodepartsthatwewillcoverlateroninthistopic.YoucanseethatweusedaninputpropertyngcInfiniteScrollOf,whichisusedtopassinthelistoftimesusedwithintheinfinitescrolling.Forinsertedtemplateinstances,wesetthelocalviewvariable$implicittotheactualitemwithinthelistwewereiteratingover.
Wewilldiscusshowwegettoallofthesurroundingcodeshortly,butfirstlet'stakealookathowwecouldusethisdirectivewithinatemplate:
<div*ngcInfiniteScroll="#itemofitems">{{item}}</div>
Theprecedingcode,asperthemechanismsdescribedintheprevioustopic,willde-sugarintothefollowingtemplateelement:
<templatengcInfiniteScroll#item[ngcInfiniteScrollOf]="items">
<div>{{item}}</div>
</template>
Sowhatwecantellnowisthattheitemsarraywillbeplacedasapropertybindingontoour
www.EBooksWorld.ir
templateelement.ThesameelementalsocontainstheInfiniteScrolldirective.
Afterdiscussinghowthedirectivewillbeusedandhowwecangettherequiredinputintothedirective,let'slookattheimplementationdetailsthatenabletheinfinitescrollbehavior.
Ourdirectiveneedstodealwithquitealotofconcerns.Let'scheckoutahigh-levelrequirementlist:
Itneedstodynamicallycreatenewchildviewsbasedonthetemplateelementandalsoremovechildviewsthatarenolongerrequired.ItneedstodetectchangesontheinputpropertyngcInfiniteScrollOf,whichisboundtoanarraywithinthetemplate.It'snotsufficienttouseasimpleidentitycheck,becausewe'dliketocreateacomparisonofthepreviousarraytothenewarrayandonlyperformviewchangesbasedonthedifferences.Forthispurpose,wewillneedtoimplementtheDoChecklifecyclecallback.Itneedstostoreacountofitemsthatshouldbedisplayedinitially,andbydetectingscrollevents,thedisplayeditemcountshouldincreasesomoreitemscouldbemadevisible.Atthesametime,scrollingshouldtriggerthechangedetectionsothatwecancreatenewinstancesofthetemplatewithintheview.
Let'sstartwiththeconstructorofourdirective:
constructor(@Inject(ViewContainerRef)viewContainerRef,
@Inject(TemplateRef)templateRef,
@Inject(IterableDiffers)iterableDiffers,
@Inject(ChangeDetectorRef)cdr){
//UsingObject.assignwecaneasilyaddallconstructor
//argumentstoourinstance
Object.assign(this,
{viewContainerRef,templateRef,iterableDiffers,cdr});
//Howmanyitemswillbeshowninitially
this.shownItemCount=3;
//Howmanyitemsshouldbedisplayedadditionally,whenwe
//scrolltothebottom
this.increment=3;
}
Weneededtousequitealotofinjecteddependenciesinordertoperformalltheoperationsrequiredtofulfilltheoutlinedrequirementsofourdirective:
TheViewContainerRefdependencyhelpsuscreatenewembeddedviewsbasedonourtemplateelementaswellasdetachorcompletelyremoveexistingviews.TheTemplateRefdependencyisareferencetothetemplateelement,andwecanuseitinconjunctionwiththeViewContainerRefdependencyinordertocreatenewinstances.TheIterableDiffersdependencyisusedtocreateadiffofourinputproperty,whichisthearrayofitemswe'reconcernedaboutinourinfinitescrollrepeater.Itsupportsusinfindingthecreated,removed,anddeleteditems.TheChangeDetectorRefdependencyisusedtotriggerchangedetectionmanuallywhenweactuallyneedit.
www.EBooksWorld.ir
Asthefirststep,weusedObject.assigntostoreallourfunctionparametersintheinstanceofthedirective.Then,wesettwo-membervariablesthatwillstoreinformationrelatedtothenumberofitemsthatshouldbedisplayedandalsothenumberofdisplayeditemsweshouldincreaseuponscrolling.
That'sitfortheconstructor.Wealsoneedtoperformsomeactionsaftertheviewwithinourdirectivehasbeeninitialized.We'llusethengOnInitlifecyclehookforthispurpose:
ngOnInit(){
this.scrollableElement=findScrollableParent(
this.viewContainerRef.element.nativeElement.parentElement);
this.scrollableElement.addEventListener('scroll',this._onScrollListener);
}
Let'slookatthesetwolinesofcodeinmoredetail:
Thewayourinfinitescrollingworksisthatitdetectswhetherthescrollableparentelementhasalreadyscrolledtothebottom.Ifthat'sthecase,we'dneedtorendermoreitemsfromthelist.Inordertocheckwhetherourparentelementhasalreadyscrolledtothebottom,wewillneedareferencetoit.Asscrolleventsdon'tbubble,weneedtobeveryprecisewheretomonitorthem.That'sthereasonwhyweuseautilityfunctiontoscantheDOMtreetofindthenextscrollableparentelement.ThefindScrollableParentfunctionlooksforthefirstparentelementthathasscrollbarsorthewindowobject.Youcancheckthesourcecodeofthischapterifyou'dliketoseetheinternalsofthefunction.Nowwe'veaddedaneventhandlertothefoundscrollableparentelementandregisteredourinternalonScrollmethodasacallback.
www.EBooksWorld.ir
DetectingchangewithinourtemplatedirectiveNowlet'slookatthecompletecodeofthengcInfiniteScrollOfpropertysetter,whichwehavebrieflylookedatalready:
@Input('ngcInfiniteScrollOf')
setinfiniteScrollOfSetter(value){
this.infiniteScrollOf=value;
//Createanewiterabledifferfortheiterable`value`,ifthe
//differisnotalreadypresent
if(value&&!this.differ){
this.differ=this.iterableDiffers.find(value).create(this.cdr);
}
}
OurpropertysetterwillbecalledbyAngulareverytimethengcInfiniteScrollOfinputpropertychanges.Sincethispropertyisboundbythede-sugaringoftheasterisktemplatesyntaxtothelistwerefertowithinourtemplate,wecanassumethatthevaluewillalwaysbeanarrayorasimilariterablestructure.
Besidesstoringthenewvaluefromtheinputpropertyontoourdirectiveinstance,wealsolazyinitializeamemberfieldcalleddiffer.UsingthefindmethodontheIterableDiffersobject,wecanobtainafactorythatmatchesthetypeofiterableyou'redealingwith(inourcase,thiswillbeplainarrays).Ontheobtainedfactory,wecanthencallthecreatemethodtocreateanewdiffer.ThecreatemethodexpectsaChangeDetectorRefobjecttobepassed.Luckily,wehavethatreadilyavailablethroughaninjectionwithintheconstructor.
Thedifferwillhelpusinalatersteptodetectchangesbetweentheexistingvalueofourarrayandanupdatedone.Wecanthenperformadditions,removals,andmovementsinaveryperformantway.
IfwecallthediffmethodonIterableDiffer,itwillreturnanewIterableDifferobjectthatcontainsallthechangesrelativetothepreviousIterableDifferobject.Inadiffer,wecanthencalloneofthefollowingmethodstoiterateovertherelevantCollectionChangeRecord:
forEachItem:ThisiteratesovereachCollectionChangeRecordwithinthedifferbyprovidingacallbackfunction.Thefirstargumenttothecallbackwillbeachangerecord.forEachPreviousItem:ThisonlyiteratesovereachCollectionChangeRecordwithinthedifferthatalreadyexistedinthepreviousdiffer.forEachAddedItem:Thisonlyiteratesovereachchangerecordthatwasaddedfromthepreviousdifftothecurrentone.forEachMovedItem:Thisonlyiteratesovereachchangerecordthatwasmoved.forEachRemovedItem:Thisonlyiteratesoverchangerecordsthatwereremoved
TheCollectionChangeRecordobjectscontainthefollowingthreemainproperties:
item:Areferencetotheitemwithinthelistwhichwe'reobservingforchangesusingthe
www.EBooksWorld.ir
differpreviousIndex:TheindexoftheitemwithinthelistbeforethedifferiterablehappenedcurrentIndex:Theindexoftheitemwithinthelistafterthedifferiterable
WecanalsosolelytellfromtheconstellationofpreviousIndexandcurrentIndexwhathappenedtotheitem.ThefollowingmethodsarepresentonanIterableDifferobject:
Addeditems:ThiscanbeidentifiedifpreviousIndexisnullandcurrentIndexissettoavalidnumberMoveditems:ThiscanbeidentifiedifpreviousIndexandcurrentIndexarebothsettoavalidnumberRemoveditems:ThiscanbeidentifiedifpreviousIndexissettoavalidnumberbutcurrentIndexissettonull
Nowlet'slookattheonScrollmethod,whichwillbeinvokedbythescrolleventcallbackofthescrollablecontainerelement.Inthismethod,weneedtohandlethelogicofourbehaviorthatshouldbeexecutedwhenauserscrollsdown:
onScroll(){
//Ifthescrollableparentisscrolledtothebottom,wewill
//increasethecountofdisplayeditems
if(this.scrollableElement&&isScrolledBottom(this.scrollableElement)){
this.shownItemCount=Math.min(this.infiniteScrollOf.length,
this.shownItemCount+this.increment);
//Afterincrementingthenumberofitemsdisplayed,weneed
//totellthechangedetectiontorevalidate
this.cdr.markForCheck();
}
}
IntheonScrollmethod,wefirstcheckedwhetherthescrollbarofthescrollableparentelementhasalreadyscrolledtothebottom.Ifthat'sthecase,wecanassumethatweshoulddisplaymoreitemsfromourlist.
WeincrementedtheshowItemCountmemberbythedefaultincrementvalue,whichwehavesetto3,andaftermodifyingthenumberofdisplayeditems,weusedthechangedetectortomarkoursubtreestructuretobechecked.
Sincewewouldliketousethedifferthatwehavelazyinitializedwithinourinputsettertodetectchangesandperformanyactionsmanually,wewillneedtoimplementtheDoChecklifecyclecallbackonourdirective.Byimplementingthis,wewilldisablethedefaultchangedetectionofAngularandimplementourownwaytodealwithchanges:
ngDoCheck(){
if(this.differ){
//Wearecreatinganewslicebasedonthedisplayeditem
//countandthencreateachangesobjectcontainingthe
//differencesusingtheIterableDiffer
constupdatedList=this.infiniteScrollOf
.slice(0,this.shownItemCount);
www.EBooksWorld.ir
constchanges=this.differ.diff(updatedList);
if(changes){
//Ifwehaveanychanges,wecallour`applyChanges`method
this.applyChanges(changes);
}
}
}
First,weusedthediffertoobtainachangerecordsetfromthecurrentinfiniteScrollOfarraytothepreviousone.Thedifferwillactuallyalwaysstorethepreviousvalue,soweonlyneedtopassitthecurrentvalue.Thechangerecordswillthenhelpustoperformdifferentactionsforadded,removed,andmoveditems.It'salsoimportanttonotethatwedidnotusethewholelistheretocreateadiff,butasliceofthelistwhereourshowItemCountmembercomesintoplay.Thiswillonlymakethelistthatwe'reconcernedaboutavailableinourinfinitescrollbehavior.
www.EBooksWorld.ir
AddingandremovingembeddedviewsIfthereareanychangesdetectedbythediffer,wecancalltheapplyChangesmethod,whichdealswiththedetailsofhowtoperformviewupdateswithchangeditems:
applyChanges(changes){
//Firstwecreatearecordlistthatcontainsallmovedand
//removedchangerecords
constrecordViewTuples=[];
changes.forEachRemovedItem((removedRecord)=>
recordViewTuples.push({record:removedRecord}));
changes.forEachMovedItem((movedRecord)=>
recordViewTuples.push({record:movedRecord}));
//Wecannowbulkremoveallmovedandremovedviewsandasa
//resultwegetallmovedrecordsonly
constinsertTuples=this.bulkRemove(recordViewTuples);
//Inadditiontoallmovedrecordswealsoaddarecordforall
//newlyaddedrecords
changes.forEachAddedItem((addedRecord)=>
insertTuples.push({record:addedRecord}));
//Nowwehavestoredallmovedandaddedrecordswithin`
//insertTuples`whichweusetodoabulkinsert.Asaresult
//wegetthelistofthenewlycreatedviews.Onthoseviews
//we'rethencreatingaviewlocalvariable`$implicit`that
//willbindthelistitemstothevariablenameusedwithinthe
//foroftemplatesyntax.
this.bulkInsert(insertTuples).forEach((tuple)=>
tuple.view.context.$implicit=tuple.record.item);
}
Let'slookattheinnardsoftheapplyChangesmethod.ItneedstobecalledfromtheOnChangelifecyclehookwithaparameterthatreflectstherecordchangeswithintheobservedinputarraycalledinfiniteScrollOf.IntheconstantrecordViewTuples,westoredallthechangerecordsthatwereeithermovedorremovedcompletely.NowyoucancallthebulkRemovemethodbypassingtherecordViewTuplesarray.ThebulkRemovemethodwilleitherdetachtheview,incasethereismovement,orcompletelyremovetheview.Thereturnedvalueisalistthatwillcontainonlythetuplesthatweremoved.WestoredthesewithinaconstantcalledinsertTuples.Becausetheyweredetachedfromtheviewcontainer,wewillneedtoreattachthematadifferentpositionwithintheviewcontainer.
NowwecangoaheadandaddalltherecordstotheinsertTuplesarraythatwereaddedaccordingtothelatestdiff.TheinsertTuplesarraynowcontainsallthemovedaswellasaddedrecords.
Usingthislist,wecallthebulkInsertmethod,whichwillreinsertmovedviewsandcreatenewembeddedviewsforaddedrecords.Asaresult,wegetalistofalltheinsertedrecords(movedandadded),whereeachrecordalsocontainsaviewpropertythatpointstotheinsertedview.
www.EBooksWorld.ir
ThelaststepwithinourapplyChangesmethodshouldnowringabell.Weiteratedthroughthelistofnewlyinsertedviewsandsetthelocalviewvariable$implicitontheviewcontext.Thisway,wecansettherequiredvariablethatisusedtocreatethedefaultvariablemappingsonourtemplateelements,asdiscussedintheprevioustopic.
Inordertounderstandhowwecaninstantiatenewviewsfromourtemplateelement,moveviewsaround,andremoveexistingviews,weneedtounderstandtheviewcontainer.TheViewContainerRefdependencyisprovidedtoourdirectiveorcomponentusinginjectionintheconstructor.Itstoresalistofviewsandprovidessomemethodstoaddnewandremoveexistingviews.EachcomponentwithinAngularcontainsoneviewcontainer.Wecanthenaccessthemethodsontheviewcontainerinordertoprogrammaticallymodifytheview.
TherearefourmainmethodsinViewContainerRefthatwe'reinterestedin:
Method Description
createEmbeddedView
Thismethodwillcreateanewembeddedviewusingatemplatereferenceandinsertthenewlycreatedviewatagivenindexwithintheviewcontainer.Embeddedviewsareviewsinstantiatedfromtemplateelements.
Thefollowingareitsparameters:
templateRef:Thefirstparametershouldbethetemplatereference,whichshouldbeinstantiatedintoanembeddedview.context:Thisisanoptionalcontextobject,whichwillbeavailablefortheinstantiatedtemplateview.Allpropertieswithinthecontextcanbeusedwithintheviewtemplateaslocalviewvariables.index:Theoptionalindexparametercanbeusedtoplacetheinstantiatedviewatagivenpositionwithintheviewcontainer.
Thismethodreturnsthecreatedembeddedview.
detach
Thedetachmethodwillremoveanembeddedviewfromtheviewcontaineratagivenindexwithoutdestroyingtheviewsoitcanbereattachedlaterusingtheinsertmethod.
Thefollowingisitsparameter:
index:Thisistheindexoftheembeddedview,whichshouldbedetached
Thismethodreturnsthedetachedembeddedview.
www.EBooksWorld.ir
remove
Theremovemethodwillcompletelyremoveanembeddedviewfromtheviewcontainerandalsodestroytheview.Aviewthathasbeendestroyedcan'tsimplybereattachedusingtheinsertmethod.
Thefollowingisitsparameter:
index:Thisistheindexoftheembeddedview,whichshouldberemoved
Thismethodreturnstheremovedembeddedview.
insert
Thismethodwillinsertanexistingviewintotheviewcontainer.
Thefollowingareitsparameters:
viewRef:Theembeddedviewthatshouldbeinsertedintotheviewcontainer.index:Theoptionalindexparameterthatcanbeusedtoplacetheembeddedviewatagivenpositionwithintheviewcontainer.
Thismethodreturnstheinsertedembeddedview.
Let'squicklylookatthebulkRemoveandbulkInsertmethodstoseehowwecanusetheviewcontainertomodifythecontainingviewuponchanges:
bulkRemove(tuples){
...
//Reducingthechangerecordssowecanreturnonlymoved
//records
returntuples.reduceRight((movedTuples,tuple)=>{
//Ifanindexispresentonthechangerecord,itmeansthat
//itsoftype"moved"
if(tuple.record.currentIndex!=null){
//Formovedrecordsweonlydetachtheviewfromtheview
//containerandpushitintothereducedrecordlist
tuple.view=this.viewContainerRef.detach(tuple.record.previousIndex);
movedTuples.push(tuple);
}else{
//Ifwe'redealingwitharecordoftype"removed",we
//completelyremovetheview
this.viewContainerRef.remove(tuple.record.previousIndex);
}
returnmovedTuples;
},[]);
}
WeuseViewContainerReftodetachviewsincasetherecordcontainsavalidcurrentIndexfield.Ifthat'sthecase,weknowthatwearedealingwithaviewthatwillbemoved.Weusethedetachmethodtoexcludetheviewfromitspositionwithintheviewcontainer,butthiswill
www.EBooksWorld.ir
notdestroytheview.It'simportanttonoteherethatwestoredthereturnedviewfromthedetachmethodontothetuplebeforeweaddedittothemovedTupleslist.Thisway,wewereabletoidentifyitlaterasamoveditem,andwecouldusetheviewtoreattachitusingtheinsertmethodontheviewcontainer.
Inthecasewherethere'snovalidcurrentIndex,wearedealingwithanelementthatwasremovedfromthelist.Insuchcases,we'dneedtousetheremovemethodtocompletelydestroytheviewandremoveitfromtheviewcontainer.
Nowwe'llcallthebulkInsertmethodwithanymovedorinsertedviews.Let'salsolookatthecodeofthismethodbrieflytoseehowwecanhandleviewupdatesthere:
bulkInsert(tuples){
...
tuples.forEach((tuple)=>{
if(tuple.view){
//We'reinsertingbackthedetachedviewatthenewpositionwithinthe
viewcontainer
this.viewContainerRef.insert(tuple.view,
tuple.record.currentIndex);
}else{
//We'redealingwithanewlycreatedviewsowecreateanewembedded
viewontheviewcontainerandstoreitinthechangerecord
tuple.view=
this.viewContainerRef.createEmbeddedView(
this.templateRef,
{},
tuple.record.currentIndex);
}
});
returntuples;
}
Ifthetuplecontainsaviewproperty,weknowwehavepreviouslydetacheditfromadifferentposition.WeareusingtheinsertmethodoftheviewcontainertoreattachitatthenewpositionusingtheinformationfromCollectionChangeRecord.
Ifthere'snoviewproperty,wearedealingwithanewlyaddedrecord.Inthatcase,wesimplyusethecreateEmbeddedViewmethodtocreateanewtemplateinstance.Forthecontextparameter,weneedtopassanewemptyobject.However,we'veupdatedthecontextobjectalreadywithinourapplyChangesmethod.There,weaddedthe$implicitlocalviewvariableforeverycreatedview.
That'sallweneedforourInfiniteScrolldirective,andwecannowaddittothetemplateswherewe'replanningtousethisfunctionality.Let'suseitwithinthetasklistbyaddingthedirectivetothedirectivelistoftheTaskListcomponentwithinthetask-list/task-list.jsfile:
...
import{InfiniteScroll}from'../infinite-scroll/infinite-scroll';
www.EBooksWorld.ir
@Component({
selector:'ngc-task-list',
...
directives:[...,InfiniteScroll]
})
exportclassTaskList{
...
}
Nowwecansimplyeditthetasklisttemplateintask-list/task-list.htmlandreplacetheNgFordirectivewithourInfiniteScrolldirective:
<ngc-task*ngcInfiniteScroll="lettaskoffilteredTasks"
[[task]="task"
(taskUpdated)="onTaskUpdated(task,$event)"
(taskDeleted)="onTaskDeleted(task)"
draggable
draggableType="task"
[draggableData]="task"
draggableDropZone
dropAcceptType="task"
(dropDraggable)="onTaskDrop($event,task)"></ngc-task>
That'sallweneedtouseourinfinitescrollfunctionality.Thisishighlyreusable,andwecanplaceitwhereverwe'dliketouseitinsteadoftheregularNgForrepeater.
www.EBooksWorld.ir
FinishingourinfinitescrolldirectiveInthistopic,wecreatedaninfinitescrollingbehaviorbyimplementingatemplatedirectivesimilartoNgFor.WereplacedtheNgFordirectiveinourtasklisttousetheInfiniteScrolldirectiveinstead.Nowwedon'tdisplayallthetasksrightatthebeginning,butassoonastheuserstartstoscroll,newtasksappear.Inscenarioswherewerelyonalistthatispartiallyloadedfromtheserver,ourdirectivecouldevenbeextendedsoitcouldrequestformoreitemsfromtheserverondemand.
We'vecoveredthefollowingsubtopicshere:
Theasterisksyntaxandde-sugaringtotemplateelementsThelocalviewvariable,$implicitImplementingtheOnChangelifecyclehooktoprovidecustomchangedetectionUsingIterableDiffertoanalyzethedifferenceofchangeswithinourarrayinputpropertyandhandlingCollectionChangeRecordobjectstoreactonchangesUsingViewContainerReftoupdatetheviewofacomponentprogrammaticallyUsingTemplateRefasareferencetothetemplateelementwithintemplatedirectives
www.EBooksWorld.ir
SummaryInthischapter,webuiltthreecomponentstoenhancetheusabilityofourapplication.Userscannowmakeuseoftagstoeasilyannotatecommentswithnavigableitemsthatprovidesummariestothesubject.Theycanusedraganddroptoreordertasksandbenefitfromaninfinitescrollbehavioronthetasklist.
Usabilityisakeyassetintoday'sapplications,andbyprovidinghighlyencapsulatedandreusablecomponentstoaddressusabilityconcerns,wecanmakeourlivesaloteasierwhenbuildingthoseapplications.Thinkingintermsofcomponentswhendealingwithusabilityisaverygoodthing,whichnotonlyeasesdevelopment,butalsoestablishesconsistency.Theconsistencyitselfthenplaysamajorroleinmakinganapplicationusable.
Inthenextchapter,we'regoingtocreatesomeniftycomponentstomanagetimewithinourtaskmanagementsystem.Thiswillalsoincludesomenewuserinputcomponentstoenablesimpleworktime-entryfields.
www.EBooksWorld.ir
Chapter8.TimeWillTellOurtask-managementsystemiscomingintoshape.However,wewerenotconcernedaboutonecrucialaspectofmanagingourprojectssofar.Timeplaysamajorroleinallprojects,andthisisthethingthatisoftenthemostcomplicatedtomanage.
Inthischapter,wewilladdafewfeaturestoourtaskmanagementsystemthatwillhelpouruserstomanagetimemoreefficiently.Reusingsomecomponentsthatwecreatedearlier,wewillbeabletoprovideaconsistentuserexperiencetomanagetime.
Onahigherlevel,wewilldevelopthefollowingfeaturestoenabletimemanagementinourapplication:
Taskdetails:Sofar,wedidnotincludeadetailspageoftasksbecauseallthenecessaryinformationabouttaskscouldbedisplayedonthetasklistofourprojectpage.Whileourtimemanagementwillincreasethecomplexityofourtasksquiteabit,wewillcreateanewdetailviewofprojecttasksthatwillalsobeaccessiblethroughrouting.Effortsmanagement:Wewillincludesomenewdataonourtaskstomanageeffortsontasks.Effortsarealwaysrepresentedbyanestimateddurationoftimeandaneffectivedurationofspenttime.Wewillmakebothpropertiesofeffortsoptionalsothattheycanexistindependently.Wewillcreatenewcomponentstoenableuserstoprovidetimedurationinputeasily.Milestonemanagement:Wewillincludeawaytomanageprojectmilestonesandthenmapthemtoprojecttasks.Thiswillhelpuslatergainanoverviewovertheprojectstatus,anditenablestheusertogrouptasksintosmallerchunksofwork.
Thefollowingtopicswillbecoveredinthischapter:
CreatingaprojecttaskdetailcomponenttoedittaskdetailsandenableanewrouteModifyingourtagmanagementsystemtoincludetasktagsCreatingnewpipestodealwithformattingtimedurationsCreatingtaskinformationcomponentstodisplaytaskoverviewinformationontheexistingtaskcomponentsCreatingatimedurationuseinputcomponentthatenablesuserstoeasilyinputtimedurationsCreatinganSVGcomponenttodisplayprogressontasksCreatinganautocompletecomponenttomanagemilestonesontasks
www.EBooksWorld.ir
TaskdetailsSofar,ourtasklistwassufficientenoughtodisplayalldetailsoftasksdirectlyinthelisting.However,aswewilladdmoredetailstotasksinthischapter,it'stimetoprovideadetailviewwhereuserscaneditthetask.
WealreadylaidthegroundworkonprojectnavigationusingtherouterinChapter5,Component-BasedRouting,ofthisbook.Addinganewroutablecomponentthatwe'lluseinthecontextofourprojectswillbeabreeze.
Let'screateanewcomponentclassforourprojecttaskdetailviewintheproject/project-task-details/project-task-details.jspath:
…
@Component({
selector:'ngc-project-task-details',
…
})
exportclassProjectTaskDetails{
…
}
AsthiscomponentwillneverexistwithoutaparentProjectcomponent,wecansafelyrelyonthattoobtainthedataweuse.Thiscomponentisn'tusedinpureUIcompositioncases,soit'snotrequiredtocreatearoutablewrappercomponentlikewedidforothercomponentsinChapter5,Component-BasedRouting.WecandirectlyrelyonrouteparametersandobtaintherelevantdatafromtheparentProjectcomponent.
First,weusedependencyinjectioninordertogetareferencetotheparentprojectcomponent:
constructor(@Inject(forwardRef(()=>Project))project){
this.project=project;
}
Similarlytoourroutingwrappercomponents,wemakeuseofparentcomponentinjectiontoobtainareferencetotheparentProjectcomponent.
Now,we'llusetheOnActivatelifecyclehookoftherouteragaintoobtainthetasknumberfromtheactiveroutesegment:
routerOnActivate(currentRouteSegment){
consttaskNr=currentRouteSegment.getParam('nr');
this.projectChangeSubscription=
this.project.document.change.subscribe((data)=>{
this.task=data.tasks.find((task)=>task.nr===+taskNr);
this.projectMilestones=data.milestones||[];
});
}
www.EBooksWorld.ir
Finally,we'llcreateareactivesubscriptiontotheLiveDocumentprojectsthatwillextractthetaskthatweareconcernedaboutandstoreitintothecomponentstaskmember.Inthisway,weensurethatourcomponentwillalwaysreceivethelatesttaskdatawhentheprojectisupdatedoutsideofthecurrenttaskdetailsview.
Ifourcomponentgetsdestroyed,weneedtomakesurethatweunsubscribefromtheRxJSObservablethatisprovidedbytheLiveDocumentproject.Let'simplementthengOnDestroylifecyclehookforthispurpose:
ngOnDestroy(){
this.projectChangeSubscription.unsubscribe();
}
Alright,let'snowtakealookatthetemplateofourcomponent,andseehowwe'lldealwiththetaskdatatoprovideaninterfacetoeditthedetails.We'llcreateaproject-task-details.htmlfileinournewcomponentfolder:
<h3class="task-details__title">
TaskDetailsoftask#{{task?.nr}}
</h3>
<divclass="task-details__content">
<divclass="task-details__label">Title</div>
<ngc-editor[content]="task?.title"
[showControls]="true"
(editSaved)="onTitleSaved($event)"></ngc-editor>
<divclass="task-details__label">Description</div>
<ngc-editor[content]="task?.description"
[showControls]="true"
[enableTags]="true"
(editSaved)="onDescriptionSaved($event)">
</ngc-editor>
</div>
ReusingtheEditorcomponentthatwecreatedinChapter4,NoComments,Please!,ofthisbook,wecanrelyonsimpleUIcompositiontomakethetitleanddescriptionofourtaskseditable.
Aswestoredthetaskdataintothetaskmembervariableonourcomponent,wecanreferencethetitleanddescriptionfieldstocreateabindingtothecontentinputpropertyofoureditorcomponents.
Whilethetitleshouldonlyconsistofplaintext,wecansupportthetaggingfunctionalitythatwecreatedinChapter7,ComponentsforUserExperience,onthedescriptionfieldofthetask.Forthis,wesimplysettheenableTagsinputpropertyofthedescriptionEditorcomponenttotrue.
TheEditorcomponenthasaneditSavedoutputpropertythatwillemittheupdatedcontentonceausersaveshisedits.Now,allweneedtomakesureofisthatwecreateabindingtoourcomponentthatwillpersistthesechanges.Let'screatetheonTitleSavedand
www.EBooksWorld.ir
onDescriptionSavedmethodsonourComponentclasstohandletheseevents:
onTitleSaved(title){
this.task.title=title;
this.project.document.persist();
}
onDescriptionSaved(description){
this.task.description=description;
this.project.document.persist();
}
ThetaskmemberisjustareferencetothegiventaskintheLiveDocumentprojectoftheProjectcomponent.Thissimplifiesthewaywepersistthedatathatwaschangedonthetask.Afterupdatingthegivenpropertyonthetask,wesimplycallthepersistmethodontheLiveDocumentprojectstostoreourchangesinthedatastore.
Sofar,sogood.WecreatedataskdetailscomponentthatmakesiteasytoeditthetitleanddescriptionoftasksusingourEditorUIcomponent.TheonlythinglefttoenableourcomponentistocreateachildrouteontheProjectcomponent.Let'sopenourProjectcomponentclassinlib/project/project.jstomakethenecessarymodifications:
…
import{ProjectTaskDetails}from'./project-task-details/project-task-
details';
…
@Component({
selector:'ngc-project',
…
})
@Routes([
newRoute({path:'task/:nr',component:ProjectTaskDetails}),
…
])
exportclassProject{
…
}
WeaddedanewchildrouteonourProjectcomponent,whichisresponsiblefortheinstantiationofourProjectTaskDetailscomponent.Byincludinga:nrparameterintherouteconfiguration,wecanpasstheconcernedtasknumberintotheProjectTaskDetailscomponent.
Ournewly-createdchildrouteisnowaccessibleintherouterandwecanaccessthetaskdetailviewusingthe/projects/project-1/task/1exampleURL.
InordertomakeourTaskDetailsroutenavigable,weneedtoaddanavigationlinktoourTaskcomponentsothatuserscannavigatetoitintheprojectstasklist.
Forthisrathersimpletask,theonlythingthatweneedtodoisusetheRouterLinkdirective
www.EBooksWorld.ir
andcreateanewlinkintheTasktemplate,lib/task-list/task/task.html:
…
<divclass="task__l-box-b">
…
<a[routerLink]="['../task',task?.nr]"
class="buttonbutton--small">Details</a>
</div>
…
WeusearelativerouterURLherebecausewe'realreadyonthe/project/tasksroute.Asourtask/:nrrouteispartoftheprojectrouter,weneedtonavigateonelevelbacktoaccessthetaskroute:
Newlycreatedtaskdetailviewwitheditabletitleanddescription
www.EBooksWorld.ir
EnablingtagsfortasksSofar,thetag-managementsystemthatwecreatedinChapter7,ComponentsforUserExperience,onlysupportsprojecttags.Aswenowcreatedadetailviewtotasks,itwouldbenicetoalsosupporttasktagsdirectlyinourtaggingsystem.Ourtaggingsystemisquiteflexible,andwecanimplementnewtagswithverylittleeffort.Onahigherlevel,weneedtomakethefollowingchangestoenabletasktagsinoursystem:
Editthegenerate-tag.jsmoduleinordertosupportthegenerationoftasktagsfromtaskandprojectdataEdittheTagsServiceinordertoinitializetasktagsusingthegenerate-tag.jsmoduleandcache
Let'sfirstmodifythelib/tags/generate-tag.jsfiletoenabletasktaggeneration:
…
exportconstTAG_TYPE_TASK='task';
exportfunctiongenerateTag(subject){
if(subject.type===TAG_TYPE_PROJECT){
…
}elseif(subject.type===TAG_TYPE_TASK){
//Ifwe'redealingwithatask,wegeneratetheaccordingtag
//object
returnnewTag(
`#${subject.project._id}-task-${subject.task.nr}`,
`${limitWithEllipsis(subject.task.title,20)}(${subject.task.done?
'done':'open'})`,
`#/projects/${subject.project._id}/task/${subject.task.nr}`,
TAG_TYPE_TASK
);
}
}
Asweneedtohaveareferencetoprojectdataaswellastotheindividualtaskofthisproject,weexpectthesubjectparametertolooklikethefollowingobject:
{task:…,project:…,type:TAG_TYPE_TASK}
Fromthissubjectobject,wecanthencreateanewTagobject.ForthetextTagfield,weuseaconstructthatincludestheprojectIDaswellasthetasknumber.Likethis,wecanuniquelyidentifythetaskusingasimpletextrepresentation.
Forthelinkfield,weconstructaURLfromtheprojectaswellasthetasknumber.ThisstringwillresolvetoaURLrequiredtoactivatetheTaskDetailsroute,whichweconfiguredintheprevioussection.
OurgenerateTagfunctionisnowreadytocreatetasktags.Now,theonlythinglefttoenabletasktagsinoursystemisthemodificationrequiredintheTagsServiceclass.Let'sopenthe
www.EBooksWorld.ir
lib/tags/tags-service.jsfileandapplyourchanges:
…
import{generateTag,TAG_TYPE_TASK}from'./generate-tag';
…
@Injectable()
exportclassTagsService{
…
//Thismethodisusedinternallytoinitializeallavailable
//tags
initializeTags(){
…
//Let'salsocreatetasktags
this.projects.forEach((project)=>{
this.tags=this.tags.concat(project.tasks.map((task)=>{
return{
type:TAG_TYPE_TASK,
project,
task
};
}).map(generateTag));
});
…
}
…
}
IntheinitializeTagsmethodofourTagsServiceclass,wenowaddtaskTagobjectsforallavailabletasksinprojects.First,wemapeachprojecttasktotherequiredsubjectobjectbythegenerateTagfunction.Then,wecansimplymaptheresultingarrayusingthegenerateTagfunctiondirectly.TheresultisanarrayofgeneratedtaskTagobjectsthatwethenconcatenateintothetagslistoftheTagsServiceclass.
Thiswasn'ttoocomplicated,right?Thisrelativelysimplechangeresultsinahugeimprovementforourusers.Theycannowreferenceindividualtaskseverywhereinoursystemwhereweenabledtags:
www.EBooksWorld.ir
TheEditorcomponentdisplayingnewly-addedtasktags
www.EBooksWorld.ir
ManagingeffortsInthissection,wewillcreatesomecomponentsthathelpuskeeptrackofefforts.Primarily,wewillusethistomanageeffortsontasks,butthiscouldbeappliedtoanypartofourapplicationwhereweneedtokeeptrackoftime.
Effortsinourcontextalwaysconsistoftwocomponents:
Estimatedduration:ThisisthedurationthatisinitiallyestimatedforthetaskEffectiveduration:Thisisthedurationoftimethatisspentonagiventask
Fortimedurations,weassumesometimeunitsandrulesthatwillsimplifytheprocessingoftimeandaligntosomeworkingstandards.Thegoalhereisnottoprovidearazorsharptimemanagementbutsomethingthatisaccurateenoughtobringvalue.Forthispurpose,wedefinethefollowingworkingtimeunits:
Minute:Oneminuteisaregular60secondsHour:Onehouralwaysrepresents60minutesDay:OnedayrepresentsaregularworkdayofeighthoursWeek:Oneweekisequivalenttofiveworkingdays(5*8hours)
www.EBooksWorld.ir
ThetimedurationinputWecannowstarttowriteacomplexuserinterfacecomponent,whereuserscanenterindividualtimeunitsindifferentinputelement.However,Ibelieveit'smuchmoreconvenienttotreattimedurationinputwithano-UIapproach.Therefore,insteadofbuildingacomplexuserinterface,wecansimplyagreeonatextualshortformtowritedurations,andlettheuserwritesomething,suchas1.5dor5h30m,inordertoprovideinput.Stickingtotheconventionthatwepreviouslyestablished,wecanbuildasimpleparserthatcanhandlethissortofinput.
Thisapproachhasseveraladvantages.Besidesthat,thisisoneofthemosteffectivewaystoentertimedurations,andit'salsoeasyforustoimplement.WecansimplyreuseourEditorcomponenttogathertextinputfromtheuser.Then,weuseaconversionprocesstoparsetheenteredtimeduration.
Let'sspinupanewmodulethathelpsusdealwiththeseconversions.Wecreateanewmoduleinthelib/utilities/time-utilities.jsfile.
First,weneedtohaveaconstantthatdefinesalltheunitsweneedfortheconversionprocess:
exportconstUNITS=[{
short:'w',
milliseconds:5*8*60*60*1000
},{
short:'d',
milliseconds:8*60*60*1000
},{
short:'h',
milliseconds:60*60*1000
},{
short:'m',
milliseconds:60*1000
}];
Thisisalltheunitsthatweneedtodealwithfornow.Youcanseethemillisecondsbeingcalculatedatinterpretationtime.Wecanalsowritethemillisecondsasconstants,butthisprovidesuswithsometransparencyonhowwegettothesevaluesandwecanspearonsomecomments.
Let'slookatourparsingfunction,whichwecanusetoparsetextinputintotimedurations:
exportfunctionparseDuration(formattedDuration){
constpattern=/[\d\.]+\s*[wdhm]/g;
lettimeSpan=0;
letresult;
while(result=pattern.exec(formattedDuration)){
constchunk=result[0].replace(/\s/g,'');
letamount=Number(chunk.slice(0,-1));
letunitShortName=chunk.slice(-1);
timeSpan+=amount*UNITS.find(
(unit)=>unit.short===unitShortName
www.EBooksWorld.ir
).milliseconds;
}
return+timeSpan||null;
}
Let'sanalyzetheprecedingcodebrieflytoexplainwhatwedohere:
1. First,wedefinearegularexpressionthathelpsusdissectthetextrepresentationofaduration.Thispatternwillextractchunksfromthetextinputthatareimportanttocalculatethedurationbehindthetextrepresentation.Thesechunksalwaysconsistofanumber,followedbyeitherw,d,h,orm.Therefore,thetext10w3d2h30mwillbesplitintothechunks10w,3d,2h,and30m.
2. WeinitializeatimeSpanvariablewith0,sowecanaddallthemillisecondsfromdiscoveredchunkstogetherandlaterreturnthissum.
3. Foreachofthepreviously-extractedchunks,wenowextractthenumbercomponentintoavariablecalledamount,andtheunit(w,d,h,orm)intoavariablecalledunitShortName.
4. Now,wecanlookupthedataintheUNITSconstantfortheunitofthechunkthatwewillprocess,multiplytheamountofmillisecondsoftheunitbytheamountweextractfromthechunk,andthenaddthatresulttoourtimeSpanvariable.
Wellthisisquiteaneatfunctionwebuilthere.Itacceptsaformattedtimedurationstringandconvertsitintomilliseconds.Thisisalreadyhalfofwhatweneedtodealwithtextualrepresentationoftimedurations.ThesecondpieceistheoppositeofwhatwehavewiththeparseDurationfunctiontoconvertadurationinmillisecondsintoaformatteddurationstring:
exportfunctionformatDuration(timeSpan){
returnUNITS.reduce((str,unit)=>{
constamount=timeSpan/unit.milliseconds;
if(amount>=1){
constfullUnits=Math.floor(amount);
constformatted=`${str}${fullUnits}${unit.short}`;
timeSpan-=fullUnits*unit.milliseconds;
returnformatted;
}else{
returnstr;
}
},'').trim();
}
Let'salsoexplainbrieflywhattheformatDurationfunctiondoes:
WeusetheArray.prototype.reducefunctiontoformatastringthatcontainsalltimeunitsandtheiramount.WeiterateoverallavailabletimeunitsintheUNITSconstantstartingwiththelargestunitforweeks.WethendividethetimeSpanvariable,whichisinmilliseconds,bythemillisecondsoftheunitwhichgivesustheamountofthegivenunit.Iftheamountisgreaterthanorequalto1,wecanaddtheunitwiththegivenamountandunitshortnametoourformattedstring.Aswecouldbeleftwithsomefractionsafterthecommaintheamount,whichwewill
www.EBooksWorld.ir
needtoencodeinsmallerunits,wesubtracttheflooredversionofouramountfromthetimeSpanbeforewereturntothereducefunctionagain.Thisprocessisrepeatedforeveryunit,whereeachunitwillonlyprovideformattedoutputiftheamountisgreaterthanorequalto1.
Thisisallweneedtoconvertbackandforthbetweenformattedtimedurationandtimedurationrepresentedinmilliseconds.
We'lldoonemorethingbeforewecreatetheactualcomponenttoentertimedurations.WewillcreateasimplepipethatbasicallyjustwrapsourformatTimefunction.Forthis,wewillcreateanewlib/pipes/format-duration.jsfile:
import{Pipe,Inject}from'@angular/core';
import{formatDuration}from'../utilities/time-utilities';
@Pipe({
name:'formatDuration'
})
exportclassFormatDurationPipe{
transform(value){
if(value==null||typeofvalue!=='number'){
returnvalue;
}
returnformatDuration(value);
}
}
UsingtheformatTimefunctionofourtime-utilitiesmodule,wenowhavetheabilitytoformatdurationsinmilliseconddirectlyfrominourtemplates.
www.EBooksWorld.ir
ComponentstomanageeffortsOkay,thisisenoughtimemathforthemoment.Let'snowusetheelementsthatwecreatedtoshapesomecomponentsthatwillhelpusgatheruserinput.
Inthissection,wewillcreatetwocomponentstomanageefforts:
Duration:TheDurationcomponentisasimpleUIcomponent,enablinguserinputoftimedurationsusingtheformattedtimestringswedealtwithintheprevioustopics.ItusesanEditorcomponenttoenableuserinputandmakesuseoftheFormatTimePipepipeaswellastheparseDurationutilityfunction.Efforts:TheEffortscomponentisjustacompositionoftwoDurationcomponentsthatrepresenttheestimatedeffortandtheeffectiveeffortspentonagiventask.Followingastrictruleofcomposition,thiscomponentisimportantforussothatwedon'trepeatourselvesandinsteadcomposealargercomponent.
Let'sstartwiththeDurationcomponentclass,andcreateanewlib/ui/duration/duration.jsfile:
…
import{FormatDurationPipe}from'../../pipes/format-duration';
import{Editor}from'../../ui/editor/editor';
import{parseDuration}from'../../utilities/time-utilities';
@Component({
selector:'ngc-duration',
…
directives:[Editor],
pipes:[FormatDurationPipe]
})
exportclassDuration{
@Input()duration;
@Output()durationChange=newEventEmitter();
onEditSaved(formattedDuration){
this.durationChange.next(formattedDuration?
parseDuration(formattedDuration):null);
}
}
There'snothingfancyaboutthiscomponentreallybecausewecreatedthebulkofthelogicalreadyandwesimplycomposeahighercomponenttogether.
Asthedurationinput,weexpectatimedurationinmilliseconds,whilethedurationChangeoutputpropertywillemiteventswhentheuserprovidessomeinput.
TheonEditSavedmethodservesinthebindingtotheEditorcomponentinourcomponent.WhenevertheusersaveshiseditsontheEditorcomponent,we'lltakethisinput,converttheformattedtimedurationintomillisecondsusingtheparseDurationfunction,andre-emittheconvertedvalueusingthedurationChangeoutputproperty.
www.EBooksWorld.ir
Let'slookatthetemplateofourcomponentinthelib/ui/duration/duration.htmlfile:
<ngc-editor[content]="duration|formatDuration"
[showControls]="true"
(editSaved)="onEditSaved($event)"></ngc-editor>
Surprisedwithhowsimpleourtemplateis?Well,thisisexactlywhatweshouldachievewithhighercomponentsonceweestablishagoodfoundationofbasecomponents.Well-organizedcompositionradicallysimplifiesourcode.TheonlythingthatwedealwithhereisourgoodoldEditorcomponent.
WebindthedurationinputpropertyofourDurationcomponenttothecontentinputpropertyoftheEditorcomponent.Aswe'dliketopasstheformattedtimedurationandnotthedurationinmilliseconds,weusetheFormatDurationPipepipetoconvertinthebindingexpression.
IftheEditorcomponentnotifiesusaboutasavededit,wecalltheonEditSavedmethodonourDurationcomponent,whichwillparsetheentereddurationandre-emittheresultingvalue.
Asweinitiallydefinedalleffortstoconsistofanestimatedandaneffectiveduration,wewouldnowliketocreateanothercomponentthatcombinesthesetwodurations.
Let'screateanewEffortscomponentbystartingwithanewtemplateonthelib/efforts/efforts.htmlpath:
<divclass="efforts__label">Estimated:</div>
<ngc-duration[duration]="estimated"
(durationChange)="onEstimatedChange($event)">
</ngc-duration>
<divclass="efforts__label">Effective:</div>
<ngc-duration[duration]="effective"
(durationChange)="onEffectiveChange($event)">
</ngc-duration>
<buttonclass="buttonbutton--small"
(click)="addEffectiveHours(1)">+1h</button>
<buttonclass="buttonbutton--small"
(click)="addEffectiveHours(4)">+4h</button>
<buttonclass="buttonbutton--small"
(click)="addEffectiveHours(8)">+1d</button>
First,weaddtwoDurationcomponentslabelled,wherethefirstoneisusedtogatherinputfortheestimatedtimeandthelateroneforeffectivetime.
Inadditiontothis,weprovidethreesmallbuttonstoincreasetheeffectivedurationbyasimpleclick.Inthisway,theusercanquicklyaddoneorfourhours(halfaworkingday)oracompleteworkingday(whichwedefinedaseighthours).
LookingattheComponentclass,thereshouldbenosurprises.Let'sopenthe
www.EBooksWorld.ir
lib/efforts/efforts.jscomponentclassfile:
…
import{Duration}from'../ui/duration/duration';
import{UNITS}from'../utilities/time-utilities';
@Component({
selector:'ngc-efforts',
…
directives:[Duration]
})
exportclassEfforts{
@Input()estimated;
@Input()effective;
@Output()effortsChange=newEventEmitter();
onEstimatedChange(estimated){
this.effortsChange.next({
estimated,
effective:this.effective
});
}
onEffectiveChange(effective){
this.effortsChange.next({
effective,
estimated:this.estimated
});
}
addEffectiveHours(hours){
this.effortsChange.next({
effective:(this.effective||0)+
hours*UNITS.find((unit)=>unit.short==='h'),
estimated:this.estimated
});
}
}
Thecomponentprovidestwoseparateinputsforestimatedandeffectivetimedurationinmilliseconds.Ifyoutakealookatthecomponenttemplateagain,theseinputpropertiesaredirectlyboundtotheinputpropertiesoftheDurationcomponents.
TheonEstimatedChangeandonEffectiveChangemethodsareusedtocreatebindingstothedurationChangeoutputpropertiesoftheDurationcomponents.AllwedohereisemitanaggregateddataobjectthatcontainstheeffectiveandestimatedtimeinmillisecondsusingtheeffortsChangeoutputproperty.
IntheaddEffectiveHoursmethod,wesimplyemitaneffortsChangeeventandupdatetheeffectivepropertybythecalculatedamountofmilliseconds.WeuseourUNITSconstantfromthetime-utilitiesmoduleinordertogettheamountofmillisecondsforanhour.
Thisisallthatweneedinordertoprovideauserinputtomanageeffortsonourtasks.To
www.EBooksWorld.ir
completethistopic,wewilladdournewly-createdEffortscomponenttotheProjectTaskDetailcomponentinordertomanageeffortsontasks.
Let'sfirstlookatthecodechangesintheComponentclasslocatedinlib/project/project-task-detail/project-task-detail.js:
…
import{Efforts}from'../../efforts/efforts';
@Component({
selector:'ngc-project-task-details',
…
directives:[Editor,Efforts]
})
exportclassProjectTaskDetails{
…
onEffortsChange(efforts){
if(!efforts.estimated&&!efforts.effective){
this.task.efforts=null;
}else{
this.task.efforts=efforts;
}
this.project.document.persist();
}
…
}
BesidesprovidingtheEffortscomponenttothedirectiveslistofourProjectTaskDetailcomponent,weaddedanewonEffortsChangemethodthatdealswiththeoutputprovidedbytheEffortscomponent.
Ifbothestimatedandeffectiveeffortisn'tset,orsetto0,we'llsetthetaskeffortstonull.Otherwise,weusetheoutputdataoftheEffortscomponentandassignitasournewtaskefforts.
Afterchangingthetaskefforts,wepersisttheLiveDocumentoftheprojectinthesamewaythatwedoforthetitleandthedescriptionupdatesalready.
Let'scheckoutthechangesinthetemplateofourcomponentlocatedinlib/project/project-task-detail/project-task-detail.html:
…
<divclass="task-details__content">
…
<divclass="task-details__label">Efforts</div>
<ngc-efforts[estimated]="task?.efforts?.estimated"
[effective]="task?.efforts?.effective"
(effortsChange)="onEffortsChange($event)">
</ngc-efforts>
</div>
WearebindingtheestimatedandeffectiveinputpropertiesoftheEffortscomponenttothe
www.EBooksWorld.ir
taskdataintheProjectTaskDetailcomponent.FortheeffortsChangeoutputpropertywe'reusinganexpressionthatisinvokingouronEffortsChangemethodthatwe'vejustcreated:
OurnewEffortscomponentthatconsistsoftwodurationinputcomponents
www.EBooksWorld.ir
ThevisualeffortstimelineAlthoughthecomponentsthatwecreatedsofartomanageeffortsprovideagoodwaytoeditanddisplayeffortandtimedurations,wecanstillimprovethiswithsomevisualindication.
Inthissection,wewillcreateavisualeffortstimelineusingSVG.Thistimelineshoulddisplaythefollowinginformation:
ThetotalestimateddurationasagraybackgroundbarThetotaleffectivedurationasagreenbarthatoverlaysonthetotalestimateddurationbarAyellowbarthatshowsanyovertime(iftheeffectivedurationisgreaterthantheestimatedduration)
Thefollowingtwofiguresillustratethedifferentvisualstatesofoureffortstimelinecomponent:
Thevisualstateiftheestimateddurationisgreaterthantheeffectiveduration
Thevisualstateiftheeffectivedurationexceedstheestimatedduration(theovertimeisdisplayedasablackbar)
Let'sstartfleshingoutourcomponentbycreatinganewEffortsTimelineComponentclassonthelib/efforts/efforts-timeline/efforts-timeline.jspath:
www.EBooksWorld.ir
…
@Component({
selector:'ngc-efforts-timeline',
…
})
exportclassEffortsTimeline{
@Input()estimated;
@Input()effective;
@Input()height;
ngOnChanges(changes){
this.done=0;
this.overtime=0;
if(!this.estimated&&this.effective||
(this.estimated&&this.estimated===this.effective)){
//Ifthere'sonlyeffectivetimeoriftheestimatedtime
//isequaltotheeffectivetimeweare100%done
this.done=100;
}elseif(this.estimated<this.effective){
//Ifwehavemoreeffectivetimethanestimatedweneedto
//calculateovertimeanddoneinpercentage
this.done=this.estimated/this.effective*100;
this.overtime=100-this.done;
}else{
//Theregularcasewherewehavelesseffectivetimethan
//estimated
this.done=this.effective/this.estimated*100;
}
}
}
Ourcomponenthasthreeinputproperties:
estimated:Thisistheestimatedtimedurationinmillisecondseffective:Thisistheeffectivetimedurationinmillisecondsheight:Thisisthedesiredheightoftheeffortstimelineinpixels
IntheOnChangeslifecyclehook,wesettwocomponentmemberfields,whicharebasedontheestimatedandeffectivetime:
done:Thiscontainsthewidthofthegreenbarinpercentagethatdisplaystheeffectivedurationwithoutovertimethatexceedstheestimateddurationovertime:Thiscontainsthewidthoftheyellowbarinpercentagethatdisplaysanyovertime,whichisanytimedurationthatexceedstheestimatedduration
Let'slookatthetemplateoftheEffortsTimelinecomponentandseehowwecannowusethedoneandovertimememberfieldstodrawourtimeline.
Wewillcreateanewlib/efforts/efforts-timeline/efforts-timeline.htmlfile:
<svgwidth="100%"[attr.height]="height">
<rect[attr.height]="height"
www.EBooksWorld.ir
x="0"y="0"width="100%"
class="efforts-timeline__remaining"></rect>
<rect*ngIf="done"x="0"y="0"
[attr.width]="done+'%'"[attr.height]="height"
class="efforts-timeline__done"></rect>
<rect*ngIf="overtime"[attr.x]="done+'%'"y="0"
[attr.width]="overtime+'%'"[attr.height]="height"
class="efforts-timeline__overtime"></rect>
</svg>
OurtemplateisSVG-based,anditcontainsthreerectanglesforeachofthebarsthatwewanttodisplay.Thebackgroundbarthatwillbevisibleifthereisremainingeffortwillalwaysbedisplayed.
Abovetheremainingbar,weconditionallydisplaythedoneandtheovertimebarusingthecalculatedwidthsfromourcomponentclass.
Now,wecangoaheadandincludetheEffortsTimelineclassinourEffortscomponent.Thiswayouruserswillhavevisualfeedbackwhentheyedittheestimatedoreffectiveduration,anditprovidesthemasenseofoverview.
Let'slookintothetemplateoftheEffortscomponenttoseehowweintegratethetimeline:
…
<ngc-efforts-timelineheight="10"
[estimated]="estimated"
[effective]="effective">
</ngc-efforts-timeline>
AswehavetheestimatedandeffectivedurationtimesreadilyavailableinourEffortscomponent,wecansimplycreateabindingtotheEffortsTimelinecomponentinputproperties:
www.EBooksWorld.ir
TheEffortscomponentdisplayingournewly-createdeffortstimelinecomponent(theovertimeofsixhoursisvisualizedwiththeyellowbar)
www.EBooksWorld.ir
RecapitulatingoneffortsmanagementInthissection,we'llcreatecomponentsthatallowuserstomanageeffortseasilyandaddasimplebutpowerfultimetrackingtoourtasks.We'vedonethefollowingtoachievethis:
WeimplementedsomeutilityfunctionstodealwiththetimemathinordertoconverttimedurationsinmillisecondsintoformattedtimedurationsandviceversaWecreatedapipetoformattimedurationsinmillisecondsusingourutilityfunctionsWecreatedaDurationUIcomponent,whichwrapsanEditorcomponentandusesourtimeutilitiestoprovideano-UIkindofinputelementtoenterdurationsWecreatedanEffortscomponentthatactsasacompositionoftwoDurationcomponentsforestimatedandeffectivetimeandprovidesadditionalbuttonstoaddeffectivespenttimequicklyWeintegratedtheEffortscomponentintotheProjectTaskDetailcomponentinordertomanageeffortsontasksWecreatedavisualEffortsTimelinecomponentusingSVG,whichdisplaystheoverallprogressonatask
www.EBooksWorld.ir
SettingmilestonesTrackingtimeisimportant.Idon'tknowhowyoufeelabouttime,butIreallysuckatorganizingmytime.AlthoughalotofpeopleaskmehowImanagetodosomanythings,IbelieveI'mactuallyverybadatmanaginghowIgetthesethingsdone.IfIwereabetterorganizer,Icouldgetthingsdonewithmuchlessenergyinvolved.
Onethingthatalwayshelpsmeorganizemyselfistobreakthingsdownintosmallerworkpackages.Usersthatorganizethemselveswithourtaskmanagementapplicationcanalreadydothisbycreatingtasksinprojects.Whileaprojectistheoverallgoal,wecancreatesmallertaskstoachievethisgoal.However,sometimeswetendtolosesightoftheoverallgoalwhenwe'reonlyfocusedontasks.
Milestonesareaperfectgluebetweenprojectsandtasks.Theymakesurethatwebundletaskstogetherintolargerpackages.Thiswillhelpusalotinorganizingourtasks,andwecanlookatmilestonesoftheprojecttoseetheoverallprojecthealth.However,wecanstillfocusontaskswhenweworkinthecontextofamilestone.
Inthissection,wewillcreatethenecessarycomponentsinordertoaddbasicmilestonefunctionalitytoourapplication.
Toimplementmilestonefunctionalityinourapplication,wewillsticktothefollowingdesigndecisions:
Milestonesshouldbestoredontheprojectlevel,andtaskscancontainanoptionalreferencetoaprojectmilestone.Tokeepthingssimple,theonlyinteractionpointwithmilestonesshouldbeontasklevel.Therefore,creationofmilestoneswillbedoneontasklevel,althoughthecreatedmilestoneswillbestoredonprojectlevel.Milestonescurrentlyonlyconsistofaname.Therearealotmoretomilestonesthatwecanpotentiallybuildintooursystem,suchasdeadlines,dependencies,andothernicethings.However,wewillsticktothebareminimum,whichisamilestonename.
www.EBooksWorld.ir
CreatinganautocompletecomponentInordertokeepthemanagementofmilestonessimple,wewillcreateanewuserinterfacecomponenttodealwiththedesignconcernsthatwelisted.Ournewautocompletecomponentwillnotonlydisplaypossiblevaluestoselectfrom,butitwillalsoallowustocreatenewitems.WecanthensimplyusethiscomponentonourProjectTaskDetailcomponentinordertomanagemilestones.
Let'slookattheComponentclassofournewautocompletecomponentthatwewillcreateinthelib/ui/auto-complete/auto-complete.jsfile:
…
import{Editor}from'../editor/editor';
@Component({
selector:'ngc-auto-complete',
…
directives:[Editor]
})
exportclassAutoComplete{
@Input()items;
@Input()selectedItem;
@Output()selectedItemChange=newEventEmitter();
@Output()itemCreated=newEventEmitter();
…
}
Onceagain,ourEditorcomponentcanbereusedtocreatethishighercomponent.We'reluckythatwecreatedsuchanicecomponent,asthissavedusalotoftimethroughoutthisproject.
Let'slookattheinputandoutputpropertiesoftheAutoCompletecomponentinmoredetail:
items:Thisiswhereweexpectanarrayofstrings.Thiswillbethelistofitemsausercanchoosefromwhentypingintotheeditor.selectedItem:Thisiswhenwemaketheselecteditemaninputpropertytoactuallymakethiscomponentpure,andwecanrelyontheparentcomponenttosetthispropertyright.selectedItemChange:Thisoutputpropertywillemitaneventiftheselecteditemwaschanged.Aswecreateapurecomponenthere,wesomehowneedtopropagatetheeventofanitemthatwasselectedintheautocompletelist.itemCreated:Thisoutputpropertywillemitaneventifanewitemwasaddedtotheautocompletelist.Updatingthelistofitemsandchangingthecomponentitemsinputpropertywillstillbetheresponsibilityoftheparentcomponent.
Let'saddmorecodetoourcomponent.WeuseanEditorcomponentasmaininputsource.Whileouruserswilltypeintotheeditor,wefiltertheavailableitemsusingthetextinputoftheeditor.Let'screateafilterItemsforthispurpose:
filterItems(filter){
www.EBooksWorld.ir
this.filter=filter||'';
this.filteredItems=this.items
.filter(
(item)=>item
.toLowerCase()
.indexOf(this.filter.toLowerCase().trim())!==-1)
.slice(0,10);
this.exactMatch=this.items.includes(this.filter);
}
ThefilterItemsmethodhasasingleparameter,whichisthetextthatwewanttouseinordertosearchforrelevantitemsinourlist.
Let'slookatthecontentofthemethodinmoredetail:
Forlateruseinourtemplate,wewillsetasidethefilterquerythatwasusedthelasttimethismethodwascalledInthefilteredItemsmembervariable,wewillstoreafilteredversionoftheitemlistbysearchingfortextoccurrencesofthefilterstringAsalaststep,wealsostoretheinformationifthesearchqueryresultedinanexactmatchofaniteminourlist
Now,weneedtomakesurethatiftheitemsorselectedIteminputpropertieschange,wealsoexecuteourfiltermethodagain.Forthis,wesimplyimplementthengOnChangeslifecyclehook:
ngOnChanges(changes){
if(this.items&&this.selectedItem){
this.filterItems(this.selectedItem);
}
}
Let'snowseehowwedealwiththeeventsprovidedbytheEditorcomponent:
onEditModeChange(editMode){
if(editMode){
this.showCallout=true;
this.previousSelectedItem=this.selectedItem;
}else{
this.showCallout=false;
}
}
Iftheeditorchangestoeditmode,wewanttosavethepreviouslyselecteditem.We'llneedthisiftheuserdecidestocancelhiseditsandswitchbacktothepreviousitem.Ofcourse,thisisalsothepointwhereweneedtodisplaytheautocompletelisttotheuser.
Ontheotherhand,iftheeditmodeisswitchedbacktoreadmode,wewanttohidetheautocompletelistagain:
onEditableInput(content){
this.filterItems(content);
www.EBooksWorld.ir
}
TheeditableInputeventistriggeredbyoureditoroneveryeditorinputchange.Theeventprovidesuswiththetextcontentthatwasenteredbytheuser.Ifsuchaneventoccurs,weneedtoexecuteourfilterfunctionagainwiththeupdatedfilterquery:
onEditSaved(content){
if(content===''){
this.selectedItemChange.next(null);
}elseif(content!==this.selectedItem&&
!this.items.includes(content)){
this.itemCreated.next(content);
}
}
WhentheeditSavedeventistriggeredbyoureditor,weneedtodecidewhetherweshoulddoeitherofthefollowing:
EmitaneventusingtheselectedItemChangeoutputpropertyifthesavedcontentisanemptystringtosignaltheremovalofaselecteditemtotheparentcomponentEmitaneventusingtheitemCreatedoutputpropertyifvalidcontentisgivenandourlistdoesnotincludeanitemwiththatnametosignalanitemcreation:
onEditCanceled(){
this.selectedItemChange.next(this.previousSelectedItem);
}
OntheeditCanceledeventoftheEditorcomponent,wewanttoswitchbacktothepreviousselecteditem.Forthis,wecansimplyemitaneventusingtheselectedItemChangeoutputpropertyandthepreviousSelectedItemmemberthatweputasideaftertheeditorwasswitchedintoeditmode.
Theseareallthebindingfunctionsthatwewillusetowireupoureditorandinordertoattachtheautocompletefunctionalitytoit.
Therearetwomorerathersimplemethodsthatwewillcreatebeforewetakealookatthetemplateofourautocompletecomponent:
selectItem(item){
this.selectedItemChange.next(item);
}
createItem(item){
this.itemCreated.next(item);
}
Wewillusethesetwofortheclickactionsintheautocompletecalloutfromourtemplate.Let'stakealookatthetemplatesothatyoucanseeallthecodethatwejustcreatedinaction:
<ngc-editor[content]="selectedItem"
[showControls]="true"
www.EBooksWorld.ir
(editModeChange)="onEditModeChange($event)"
(editableInput)="onEditableInput($event)"
(editSaved)="onEditSaved($event)"
(editCanceled)="onEditCanceled($event)"></ngc-editor>
First,theEditorcomponentisplacedandallnecessarybindingstothehandlermethodsthatwecreatedinourComponentclassareattached.
Now,wewillcreatetheautocompletelistthatwillbedisplayedasacallouttotheuserrightnexttotheeditorinputarea:
<ul*ngIf="showCallout"class="auto-complete__callout">
<li*ngFor="letitemoffilteredItems"
(click)="selectItem(item)"
class="auto-complete__item"
[class.auto-complete__item--selected]="item===selectedItem">{{item}}</li>
<li*ngIf="filter&&!exactMatch"
(click)="createItem(filter)"
class="auto-complete__itemauto-complete__item--create">Create"{{filter}}"
</li>
</ul>
WerelyontheshowCalloutmembersetbytheonEditModeChangemethodofourComponentclasstosignalifweshoulddisplaytheautocompletelistornot.
WetheniterateoverallfiltereditemsusingtheNgFordirectiveandrenderthetextcontentofeachitem.Ifoneoftheitemsgetsclickedon,wewillcallourselectItemmethodwiththeconcerneditemastheparametervalue.
Asthelastlistelement,aftertherepeatedlistitems,weconditionallydisplayanadditionallistelementinordertocreateanonexistingmilestone.Weonlydisplaythisbuttonifthere'savalidfilteralreadyandifthere'snoexactmatchofthefiltertoanexistingmilestone:
www.EBooksWorld.ir
Ourmilestonecomponentplaysnicelytogetherwiththeeditorcomponentusingacleancomposition
Nowthatwearealldonewithourautocompletecomponent,theonlythinglefttodoinordertomanageprojectmilestonesistomakeuseofitintheProjectTaskDetailscomponent.
Let'sopentheComponentclasslocatedinlib/project/project-task-details/project-task-details.jsandapplythenecessarymodifications:
…
import{AutoComplete}from'../../ui/auto-complete/auto-complete';
@Component({
selector:'ngc-project-task-details',
…
directives:[…,AutoComplete]
})
exportclassProjectTaskDetails{
constructor(@Inject(forwardRef(()=>Project))project,{
…
this.projectChangeSubscription=
this.project.document.change.subscribe((data)=>{
…
this.projectMilestones=data.milestones||[];
});
}
…
onMilestoneSelected(milestone){
this.task.milestone=milestone;
this.project.document.persist();
}
onMilestoneCreated(milestone){
this.project.document.data.milestones=
this.project.document.data.milestones||[];
this.project.document.data.milestones.push(milestone);
this.task.milestone=milestone;
this.project.document.persist();
}
…
}
Inthesubscriptiontoprojectchanges,wenowalsoextractanypreexistingprojectmilestonesandstoretheminaprojectMilestonesmembervariable.Thismakesiteasiertoreferenceinthetemplate.
TheonMilestoneSelectedmethodwillbeboundtotheselectItemChangeoutputpropertyoftheAutoCompletecomponent.WeusetheemittedvalueoftheAutoCompletecomponenttosetourtasksmilestoneandpersisttheLiveDocumentprojectusingitspersistmethod.
www.EBooksWorld.ir
TheonMilestoneCreatedmethodwillbeboundtotheitemCreatedoutputpropertyoftheAutoCompletecomponent.Onsuchanevent,weaddthecreatedmilestonetotheprojectsmilestonelistaswellasassignthecurrenttasktothecreatedmilestone.AfterupdatingtheLiveDocumentdata,weusethepersistmethodtosaveallchanges.
Let'slookintolib/project/project-task-details/project-task-details.htmltoseethenecessarychangesinourtemplate:
…
<divclass="task-details__content">
…
<ngc-auto-complete[items]="projectMilestones"
[selectedItem]="task?.milestone"
(selectedItemChange)="onMilestoneSelected($event)"
(itemCreated)="onMilestoneCreated($event)">
</ngc-auto-complete>
</div>
Besidestheoutputpropertybindingsthatyou'realreadyawareof,wealsocreatetwoinputbindingsfortheitemsandselectedIteminputpropertiesoftheAutoCompletecomponent.
Thisisalreadyit.WecreatedanewUIcomponentthatprovidesautocompletionandusedthatcomponenttoimplementmilestonemanagementonourtasks.
Isn'titnicehoweasyitsuddenlyseemstoimplementnewfunctionalitywhenusingcomponentswithproperencapsulation?Thegreatthingaboutcomponent-orienteddevelopmentisthatyourdevelopmenttimefornewfunctionalitydecreasedwiththeamountofreusablecomponentsthatyoualreadycreated.
www.EBooksWorld.ir
SummaryInthischapter,weimplementedsomecomponentsthathelpouruserskeeptrackoftime.Theycannowlogeffortsontasksandmanagemilestonesonprojects.Wecreatedanewtaskdetailviewthatcanbeaccessedusinganavigationlinkonourtasklist.
Oncemore,weexperiencedthepowerofcompositionusingcomponents,andreusingexistingcomponents,wewereabletoeasilyimplementhighercomponentsthatprovidemorecomplexfunctionality.
Inthenextchapter,wewilllookathowtousethechartinglibraryChartistandcreatesomewrappercomponentsthatallowustobuildreusablecharts.Wewillbuildadashboardforourtaskmanagementsystem,wherewewillseeourchartcomponentsinaction.
www.EBooksWorld.ir
Chapter9.SpaceshipDashboardWhenIwasachild,Ilovedtoplayspaceshippilot.Ipiledupoldcartonboxesanddecoratedtheinteriortolooklikeaspaceshipcockpit.Withamarker,Idrewaspaceshipdashboardontheinsideoftheboxes,andIrememberplayinginthereforhours.
Thethingthat'sspecialaboutthedesignofcockpitsandspaceshipdashboardsisthattheyneedtoprovideanoverviewandcontroloverthewholespaceshiponverylimitedspace.Ithinkthesamealsoappliestoapplicationdashboards.Adashboardshouldprovidetheuserwithanoverviewandasensefortheoverallstatusofwhat'sgoingon.
Inthischapter,wewillcreatesuchadashboardforourtaskmanagementapplication.WewillmakeuseoftheopensourcechartinglibraryChartisttocreategoodlookingresponsivechartsandprovideanoverviewoveropentasksandprojectstatus:
Apreviewofthetaskschartthatwewillbuildduringthecourseofthischapter
www.EBooksWorld.ir
Onahigherlevel,wewillcreatethefollowingcomponentsinthischapter:
Projectsummary:Thisistheprojectsummarythatwillprovideaquickinsightintotheoverallprojectstatus.Byaggregatingalleffortsofcontainingtasks,wecanprovideaniceoveralleffortsstatus,forwhichwehavecreatedthecomponentsinthepreviouschapter.Projectactivitychart:Withoutanylabelsorscales,thisbarchartwilljustgiveaquicksensefortheactivityonprojectsinthelast24hours.Projecttaskschart:Thischartprovidesanoverviewofthetaskprogressonprojects.Usingalinechart,wewilldisplaythecountofopentasksoveracertaintimeperiod.UsingourTogglecomponentthatwecreatedinChapter2,Ready,Set,Go!,ofthisbook,we'llprovideaneasywayfortheusertoswitchthedisplayedtimeframeonthechart.
www.EBooksWorld.ir
IntroductiontoChartistWewillcreatesomecomponentsinthischapterthatwillrendercharts,andweshouldlookforsomehelpinrenderingthem.Ofcourse,wecanfollowasimilarapproachaswedidinChapter6,KeepingUpwithActivities,whenwedrewouractivitytimeline.However,whenitcomestomorecomplexdatavisualization,it'sbettertorelyonalibrarytodotheheavylifting.
Itshouldn'tbeasurprisethatwe'lluseChartisttofillthisgapbecauseI'vespentalmosttwoyearswritingit.AstheauthorofChartist,Ifeelveryluckythatwe'vefoundaperfectspotinthisbooktomakeuseofit.
I'dliketotaketheopportunitytointroduceyoutoChartistbrieflybeforewediveintotheimplementationofthecomponentsforourdashboard.
TheclaimofChartistissimpleresponsivecharts,andthisisluckilystillthecaseafterthreeyearsofexistence.Icantellyouthatprobablythehardestjobofmaintainingthislibrarywastoprotectitfromfeaturebloating.Therearesomanygreatmovements,technologies,andideasintheopensourcecommunityandtoresistandalwaysstayfocusedontheinitialclaimwasn'teasy.
Letmeshowyouaverybasicexampleofhowyoucancreateasimplelinechartonceyou'veincludedtheChartistscriptsonyourwebsite:
constchart=newChartist.Line('#chart',{
labels:['Mon','Tue','Wed','Thu','Fri'],
series:[
[10,7,2,8,5]
]
});
ThecorrespondingHTMLmarkupthatisrequiredforthisexamplelooksassimpleasthefollowing:
<body>
<divid="chart"class="ct-golden-section"></div>
</body>
ThefollowingfigureshowsyoutheresultingchartthatisrenderedbyChartist:
www.EBooksWorld.ir
AsimplelinechartrenderedwithChartist
Ibelievethatbysayingthatwe'vebeenstickingtoourclaimtostaysimple,we'venotpromisedtoomuch.
Let'slookatthesecondcoreconcernofChartist,whichistobeperfectlyresponsive.Well,let'sstartwithoneofmymostappreciatedprinciplesinwebdevelopment,whichistheseparationofconcernsinthefrontend.Chartisttriestosticktothisseparationwhereverpossible,whichmeansthatitusesCSSforitsappearance,SVGforthebasicgraphicalstructure,andJavaScriptforanybehavior.Simplybyfollowingthisprinciple,we'vealreadyenabledalotofresponsiveness.WecanuseCSSmediaqueriestoapplydifferentstylestoourchartsondifferentmedia.
WhileCSSisgreatforvisualstyles,thereareplentyofelementsintheprocessofrenderingcharts,whichcan'tbecontrolledsimplybystyling.Afterall,thisisthereasonwhyweuseaJavaScriptlibrarytorendercharts.
So,howcanwecontrolhowChartistrendersourchartsondifferentmediaifwehaven'tgotcontroloverthisinCSS?Well,Chartistprovidessomethingcalledresponsiveconfigurationoverrides.UsingthebrowsersmatchMediaAPI,Chartistisabletoprovideaconfigurationmechanismthatallowsyoutospecifyoptionsthatyouwantoverriddenoncertainmedia.
Let'slookintoasimpleexampleofhowwecaneasilyimplementresponsivebehaviorusingamobile-firstapproach:
constchart=newChartist.Line('#chart',{
labels:['Mon','Tue','Wed','Thu','Fri'],
series:[
[10,7,2,8,5]
www.EBooksWorld.ir
]
},{
showPoint:true,
showLine:true
},[
['screenand(min-width:400px)',{
showPoint:false
}],
['screenand(min-width:800px)',{
lineSmooth:false
}]
]);
Here,thesecondparametertotheChartist.Lineconstructorsetstheinitialoptions;wecanprovideoverridingoptionsannotatedwithmediaqueriesinanarrayasthethirdparameteroftheconstructor.Inthisexample,we'lloverridetheshowPointoptionforanymedialargerthan400pxinwidth.Medialargerthan800pxinwidthwillreceiveboththeshowPointoverrideaswellasthelineSmoothoverride.
Notonlycanwespecifyrealmediaqueriestotriggersettingchanges,butwecanalsouseanoverridingmechanismthatisverysimilartoCSS.Thisway,wecanimplementvariousapproaches,suchasrangedorexclusivemediaqueries,mobile-first,ordesktop-first.ThisresponsiveoptionsmechanismcanbeusedforalloptionsavailableinChartist.
www.EBooksWorld.ir
Displayingthepreviouschartonthreedifferentmedialefttoright,startingfromamediawithlessthan400px(A),lessthan800px(B),andmorethan800px(C).
Asyoucansee,implementingcomplexresponsivebehaviorisabreezewithChartist.Althoughourtaskmanagementapplicationwasnevermeanttobearesponsivewebapplication,wecanstillbenefitfromthisfeatureinordertooptimizeourcontent.
IfI'vetickledyourfantasywithChartist,Irecommendthatyoucheckouttheproject'swebsiteathttp://gionkunz.github.io/chartist-js.Onthewebsite,youcanalsovisittheliveexamplepageathttp://gionkunz.github.io/chartist-js/examples.html,whereyoucanhacksomechartsdirectlyinthebrowser.
www.EBooksWorld.ir
ProjectsdashboardInthischapter,we'llcreateaprojectsdashboard,whichwillconsistofthefollowingcomponents:
Taskschart:Thisiswherewe'llprovideavisualoverviewonopentasksovertime.Allprojectswillberepresentedinalinechartthatdisplaystheprogressofopentasks.We'llalsoprovidesomeuserinteractionsothattheusercanchoosebetweendifferenttimeframes.Activitychart:Thiscomponentvisualizesactivitiesinabarchartoveratimeframeof24hours.Thiswillhelpourusersquicklyidentifyoverallandpeakprojectactivities.Projectsummary:Thisiswherewe'lldisplayasummaryofeachprojectwhereweoutlinethemostimportantfacts.Ourprojectsummarycomponentwillalsoincludeanactivitychartcomponentthatvisualizesprojectactivity.Projectsdashboard:Thiscomponentisjustacompositionoftheprevioustwocomponents.Thisisourmaincomponentinthedashboard.Itrepresentsourdashboardpageandisdirectlyexposedtotherouter.
www.EBooksWorld.ir
CreatingtheprojectsdashboardcomponentFirst,we'llcreateourmaindashboardcomponent.TheProjectsDashboardcomponenthasonlytworesponsibilities:
Obtainingprojectdata,whichisusedtocreatethedashboardComposingthemaindashboardlayoutbyincludingourdashboardsubcomponents
Let'sjumprightinandcreateanewcomponentclassonthepath,lib/projects-dashboard/projects-dashboard.js:
import{Component,ViewEncapsulation,Inject}from'@angular/core';
importtemplatefrom'./projects-dashboard.html!text';
import{ProjectService}from'../project/project-service/project-service';
@Component({
selector:'ngc-projects-dashboard',
host:{class:'projects-dashboard'},
template,
encapsulation:ViewEncapsulation.None
})
exportclassProjectsDashboard{
constructor(@Inject(ProjectService)projectService){
this.projects=projectService.change;
}
}
Inourdashboardcomponent,we'llusethechangeobservableofProjectServicedirectly.Thisisdifferenttotheusualwaythatwedealwithobservables.Usually,we'dsubscribetotheobservableinourcomponentandthenupdateourcomponentwheneverdatastreamsthrough.However,inourprojectsdashboard,we'redirectlystoringthechangeobservableofProjectServiceonourcomponent.
Now,wecanuseoneofAngular'sasynccorepipesinordertosubscribetotheobservabledirectlyinourview.
Exposingobservablesdirectlyintotheviewandusingtheasyncpipetosubscribetotheobservablecomeswithamainadvantage.
Wedon'tneedtodealwithsubscribingandunsubscribinginourcomponent,astheasyncpipewilldothatforusdirectlyintheview.
Whenanewvalueisemittedwithintheobservable,theasyncpipewillcausetheunderlyingbindingtobeupdated.Also,iftheviewgetsdestroyedbecauseofanyreason,theasyncpipewillautomaticallyunsubscribefromtheobservable.
Tip
BychainingRxJSoperatorstogether,wecanbringanobservablestreamintotherequired
www.EBooksWorld.ir
shapewithoutperforminganysubscription.Usingtheasyncpipe,wecanthenleaveituptotheviewtosubscribeandunsubscribefromthetransformedobservablestream.Thisencouragesustowritepureandstatelesscomponents,andwhenusedcorrectly,thisisagreatpractice.
Let'stakealookattheviewofourcomponentcreatedintheprojects-dashboard.htmlfileinthesamedirectoryastheComponentclass:
<divclass="projects-dashboard__l-header">
<h2class="projects-dashboard__title">Dashboard</h2>
</div>
<divclass="projects-dashboard__l-main">
<h3class="projects-dashboard__sub-title">Projects</h3>
<ulclass="projects-dashboard__list">
<li*ngFor="letprojectofprojects|async">
<div>{{project.title}}</div>
<div>{{project.description}}</div>
</li>
</ul>
</div>
YoucanseefromthetemplatethatweusetheasyncpipetosubscribetotheprojectsobservableofourComponentclass.Theasyncpipewillinitiallyreturnnull,butonanychangeintheobservable,thiswillreturntheresolvedvaluefromthesubscription.Thismeansthatwedon'tneedtoworryaboutsubscribingtoourprojectlistobservable.Wecansimplymakeuseoftheasyncpipetosubscribeandresolvedirectlyinourview.
Forthemoment,weonlydisplayedtheprojecttitleanddescription,butinthenextsection,wewillcreateanewprojectsummarycomponentthatwilldealwithsomemorecomplexrendering.
www.EBooksWorld.ir
ProjectsummarycomponentInthissection,we'llcreateaproject-summarycomponentthatwillprovidesomeoverviewinformationforprojects.Besidesthetitleanddescription,thiswillalsoincludeanoverviewoverthetotaleffortsontheprojecttasks.
Let'sfirstbuildthecomponentandmakethenecessarypreparationssothatwecandisplaythetotaleffortoftheunderlyingtasksoftheproject.
We'llstartwiththeComponentclassonthelib/projects-dashboard/project-summary/project-summary.jspath:
...
import{FormatEffortsPipe}from'../../pipes/format-efforts';
import{EffortsTimeline}from'../../efforts/efforts-timeline/efforts-
timeline';
importtemplatefrom'./project-summary.html!text';
@Component({
selector:'ngc-project-summary',
host:{class:'project-summary'},
template,
directives:[EffortsTimeline],
pipes:[FormatEffortsPipe],
encapsulation:ViewEncapsulation.None
})
exportclassProjectSummary{
@Input()project;
ngOnChanges(changes){
if(this.project){
this.totalEfforts=this.project.tasks.reduce(
(totalEfforts,task)=>{
if(task.efforts){
totalEfforts.estimated+=task.efforts.estimated||0;
totalEfforts.effective+=task.efforts.effective||0;
}
returntotalEfforts;
},{
estimated:0,
effective:0
});
}
}
}
Asyou'veprobablyalreadyguessed,wereusedtheEffortsTimelinecomponentthatwecreatedinthepreviouschapter.Asourprojectsummarywillalsoincludeaneffortstimeline,basedonthesamesemanticsasourtotaleffort,there'snoneedtocreateanewcomponentforthis.
www.EBooksWorld.ir
Whatweneedtodo,though,istoaccumulatealltaskeffortsintoanoveralleffort.UsingtheArray.prototype.reducefunction,wecanaccumulatealltaskeffortsrelativelyeasy.
Theresultingobjectfromthereducecallneedstokeepupwiththeformatthatisexpectedofaneffortsobject.Asaninitialvalue,we'llprovideaneffortsobjectwithanestimatedandeffectivetimeofzero.Then,thereducecallbackwilladdanytaskeffortvaluesthatarefoundintheproject.
Let'slookintothetemplatetoseehowwe'regoingtousethistotaleffortsdatatodisplayourEffortsTimelinecomponent:
<divclass="project-summary__title">{{project?.title}}</div>
<divclass="project-summary__description">
{{project?.description}}
</div>
<divclass="project-summary__label">TotalEfforts</div>
<ngc-efforts-timeline[estimated]="totalEfforts?.estimated"
[effective]="totalEfforts?.effective"
height="10"></ngc-efforts-timeline>
<p>{{totalEfforts|formatEfforts}}</p>
Afterdisplayingthetitleanddescriptionoftheproject,weincludedtheEffortsTimelinecomponent,whichwebindtothetotalEffortsmemberthatwejustconstructed.Thistimelinewillnowdisplaythetotalaggregatedamountofeffortsloggedonthetasks.
Inadditiontothetimeline,wealsorenderedaformattedeffortstext,suchastheonethatwealreadyrenderedintheEffortscomponentofthepreviouschapter.Forthis,weusedtheFormatEffortsPipepipe.
Now,what'sstilllefttodoistointegrateourProjectSummarycomponentintotheProjectsDashboardcomponent.
Let'slookatthetemplatemodificationintheprojects-dashboard.htmlcomponenttemplate:
...
<li*ngFor="letprojectofprojects|async">
<ngc-project-summary
[project]="project"
[routerLink]="['/projects',project._id]">
</ngc-project-summary>
</li>
...
Youcanseethatwebindtheprojectlocalviewvariable,whichwascreatedbytheNgFordirectiveinconjunctionwiththeasyncpipe,totheprojectinputpropertyofourProjectSummarycomponent.
WealsousedtheRouterLinkdirectivetoestablishthenavigationontotheProjectDetailsviewiftheuserclicksononeofthesummarytiles.
www.EBooksWorld.ir
ThemodificationsintheProjectsDashboardcomponentclassarenegligible:
...
import{ROUTER_DIRECTIVES}from'@angular/router';
import{ProjectSummary}from'./project-summary/project-summary';
...
@Component({
selector:'ngc-projects-dashboard',
directives:[ProjectSummary,ROUTER_DIRECTIVES],
...
})
exportclassProjectsDashboard{
...
}
TheonlymodificationthatweappliedtotheComponentclasswastoaddtheProjectSummarycomponentandtheROUTER_DIRECTIVESconstanttothedirectiveslistofthecomponent.TheROUTER_DIRECTIVESconstantincludestheRouterOutletandRouterLinkdirectives,andweusethelatterinourtemplate.
Aprojectsdashboarddisplayingtwoprojectsummarycomponentswiththeaggregatedtotaleffort
Okay,sofarsogood.WecreatedtwonewcomponentsandreusedourEffortsTimelinecomponenttocreateanaggregatedviewonthetasksefforts.Inthenextsection,wewillenrichourProjectSummarycomponentwithaniceChartistchart.
www.EBooksWorld.ir
CreatingyourfirstchartInthissection,wewillcreateourfirstchartusingChartisttoprovideaprojectactivityoverviewoverthepast24hours.Thisbarchartwillonlyprovidesomevisualcluesabouttheprojectactivity,andourgoalisnottomakeitprovidedetailedinformation.Forthisreason,wewillconfigureittohideanylabels,scales,andgridlines.Theonlyvisiblepartshouldbethebarsofthebarchart.
Beforewestartcreatingtheactivitychartitself,weneedtolookathowweneedtotransformandprepareourdataforthecharts.
Let'slookatwhatdatawealreadyhaveinoursystem.Asfarastheactivitiesgo,theyallhaveatimestamponthemstoredinthetimefield.However,forourchart,wewantsomethingelsedisplayed.Whatwe'relookingforisachartthatdisplaysonebarforeachhourofthepast24hours.Eachbarshouldrepresentthecountofactivitiesinthattimeframe.
Thefollowingillustrationshowsoursourcedata,whichisbasicallyatimestreamofactivityevents.Onthelowerarrow,weseethedatathatweneedtoendupwithforourchart:
Anillustrationdisplayingactivitiesasatimestreamwherethedotsrepresentactivities.Thelowerarrowisshowingarasterizedcountbyhourforthelast24hours.
Let'simplementafunctionthatdoesthetransformationoutlinedinthisimage.We'lladdthisfunctiontoourtime-utilitiesmoduleonthelib/utilities/time-utilities.jspath:
functionrasterize(timeData,timeFrame,quantity,now,fill=0){
//Floortoagiventimeframe
now=Math.floor(now/timeFrame)*timeFrame;
returntimeData.reduce((out,timeData)=>{
//Calculatingthedesignatedindexintherasterizedoutput
constindex=Math.ceil((now-timeData.time)/timeFrame);
//Iftheindexislargerorequaltothedesignedrasterized
//arraylength,wecanskipthevalue
if(index<quantity){
www.EBooksWorld.ir
out[index]=(out[index]||0)+timeData.weight;
}
returnout;
},Array.from({length:quantity}).fill(fill)).reverse();
}
Let'slookattheinputparametersofournewly-createdfunction:
timeData:Thisparameterisexpectedtobeanarrayofobjectsthatcontainsatimepropertythatissettothetimestampoftheeventthatshouldbecounted.Theobjectsshouldalsocontainaweightproperty,whichisusedtocount.Usingthisproperty,wecancountoneeventastwoorevencountminusvaluestodecreasethecountinaraster.timeFrame:Thisparameterspecifiesthetimespanofeachrasterinmilliseconds.Ifwewanttohave24rasterizedframes,eachconsistingofonehourthisparameterneedstobesetto3,600,000(1h=60min=3,600s=3,600,000ms).quantity:Thisparametersetstheamountofrasterizedframesthatshouldbepresentintheoutputarray.Inthecaseof24framesofonehour,thisparametershouldbesetto24.now:Thisiswhenourfunctionisrasterizingtime,startingatagivenpointintimebackwards.Thenowparametersetsthispointintime.fill:Thisishowwecanspecifyhowwe'dlikeourrasterizedoutputarraytobeinitialized.Inthecaseofouractivitycounts,wewantthistobesetto0.
Thefunctionthatwejustcreatedisnecessarytocreatetheactivitychart.Thetransformationhelpsusprepareprojectactivitiesfortheinputdataofthechart.
It'stimetocreateourfirstchartcomponent!Let'sstartwithanewtemplatecreatedonthelib/projects-dashboard/project-summary/activity-chart/activity-chart.htmlpath:
<div#chartContainer></div>
AsweleavealltherenderinguptoChartist,thisisactuallyalreadyallthatweneed.Chartistneedsanelementasacontainertocreatethechartin.WesetachartContainerlocalviewreferencesothatwecanreferenceitfromourcomponent,andthenpassittoChartist.
Let'smoveonwiththechartcreation,andfleshouttheactivitychartcomponentbycreatingtheComponentclassinactivity-chart.jsinthesamedirectoryasthetemplate:
...
importChartistfrom'chartist';
import{rasterize,UNITS}from'../../../utilities/time-utilities';
@Component({
selector:'ngc-activity-chart',
...
})
exportclassActivityChart{
@Input()activities;
@ViewChild('chartContainer')chartContainer;
ngOnChanges(){
www.EBooksWorld.ir
this.createOrUpdateChart();
}
ngAfterViewInit(){
this.createOrUpdateChart();
}
...
}
Note
Chartistisavailableforalmostallpackagemanagers,anditalsocomesbundledintheUMDmoduleformat(UniversalModuleFormat),which,infact,isawrappertoenableAMD(AsynchronousModuleDefinition),CommonJSmoduleformat,andglobalexport.
UsingJSPM,wecansimplyinstallChartistbyexecutingthefollowingcommandonthecommandline:
jspminstallchartist
AfterinstallingChartist,wecandirectlyimportitusingES6modulesyntax.
Wealsoimporttherasterizefunctionthatwecreatedsothatwecanuseitlatertoconvertouractivitiesintotheexpectedinputformatforourchart.
Aswerelyonaviewchildasacontainerelementtocreateourchart,weneedtowaitfortheAfterViewInitlifecyclehookinordertoconstructthechart.Atthesametime,weneedtorerenderthechartiftheinputactivitieschange.UsingtheOnChangeslifecyclehook,wecanreactoninputchangesandupdateourchart.
Let'snowlookatthecreateOrUpdateChartfunction,whichdoesexactlywhatitsnamealreadyimplies:
createOrUpdateChart(){
if(!this.activities||!this.chartContainer){
return;
}
consttimeData=this.activities.map((activity)=>{
return{
time:activity.time,
weight:1
};
});
constseries=[
rasterize(
timeData,
UNITS.find((unit)=>unit.short==='h').milliseconds,
24,
+newDate())
];
www.EBooksWorld.ir
if(this.chart){
this.chart.update({series});
}else{
this.chart=newChartist.Bar(this.chartContainer.nativeElement,{
series
},{
width:'100%',
height:60,
axisY:{
onlyInteger:true,
showGrid:false,
showLabel:false,
offset:0
},
axisX:{
showGrid:false,
showLabel:false,
offset:0
},
chartPadding:0
});
}
}
Let'slookintothecodeinmoredetailandwalkthroughitstepbystep:
AswegetcalledbothfromtheAfterViewInitandOnChangeslifecycle,weneedtomakesurethatboththechartContainerandactivitiesinputsarereadybeforewecontinue.Now,it'stimetoconverttheactivitydatathatwereceiveasinputintotherasterizedformthatisrequiredforthechartthatwe'dliketocreate.WeuseArray.prototype.maptotransformouractivitiesintothetimeDataobjectsthatarerequiredbytherasterizefunction.Wealsopassthenecessaryparameterssothatthefunctionwillrasterizeinto24frames,eachconsistingofonehour.Ifthechartmemberisalreadysettoachartthatwaspreviouslycreated,wecanusetheupdatefunctionontheChartistchartobjecttoonlyupdatewiththenewdata.Ifthere'snochartobjectalready,weneedtocreateanewchart.AsafirstparametertotheChartist.Barconstructor,we'llpasstheDOMelementreferenceofourcontainerviewchild.Chartistwillcreateourchartinthiscontainerelement.Thesecondargumentisourdata,whichwefillwiththeseriesthatwasjustgenerated.Inthechartoptions,we'llseteverythingtoachieveaveryplain-lookingchartwithoutanydetailedinformation.
Thisisgreat!WecreatedourfirstchartcomponentusingChartist!Now,wecangobacktoourProjectSummarycomponentandintegratetheactivitycharttheretoprovideanactivityoverview:
...
import{ActivityService}from'../../activities/activity-service/activity-
service';
import{ActivityChart}from'./activity-chart/activity-chart';
@Component({
www.EBooksWorld.ir
selector:'ngc-project-summary',
...
directives:[EffortsTimeline,ActivityChart],
...
})
exportclassProjectSummary{
...
constructor(@Inject(ActivityService)activityService){
this.activityService=activityService;
}
ngOnChanges(){
if(this.project){
...
this.activities=this.activityService.change
.map((activities)=>activities.filter((activity)=>activity.subject
===this.project._id));
}
}
}
ThefirstchangehereistoincludetheActivityServicesothatwecanextracttherequiredprojectactivitiestopassthemtotheActivityChartcomponent.WealsoneedtoimporttheActivityChartcomponentanddeclareitasadirectiveonthecomponent.
Asourcomponentreliesontheprojecttobeprovidedasinput,whichissubjecttochange,weneedtoimplementthelogictoextractactivitiesintheOnChangeslifecyclehookofthecomponent.
Beforewepassontheobservableactivitiesstream,weneedtofiltertheactivitiesthatcomethroughthestreamsothatweonlygetactivitiesthatarerelevanttothecurrentproject.Again,wewillusetheasyncpipeinordertosubscribetotheactivitiessothatthere'snoneedtouseasubscribeformwithinthecomponent.TheactivitiespropertywillbedirectlysettoafilteredObservable.
Let'slookatthechangesintheviewoftheProjectSummarycomponentinordertoenableourchart:
...
<divclass="project-summary__label">Activitylast24hours</div>
<ngc-activity-chart[activities]="activities|async">
</ngc-activity-chart>
WeaddourActivityChartcomponentatthebottomofthealreadyexistingtemplate.Wealsocreatethenecessarybindingtopassouractivitiesintothecomponent.Usingtheasyncpipe,wecanresolvetheobservablestreamandpassthefilteredactivitieslistintothechartcomponent.
Finally,ourProjectSummarycomponentlooksgreatandimmediatelyprovidesaprojectinsightbydisplayingtheaggregatedeffortstimelineandaniceactivitychart.Inthenext
www.EBooksWorld.ir
section,we'lldiveabitdeeperintothechartingcapabilitiesofChartist,andwewillalsoprovidesomeinteractivityusingAngular.
www.EBooksWorld.ir
VisualizingopentasksInthissection,wewillcreateachartcomponentusingChartist,whichwilldisplaytheopentaskprogressofprojectsovertime.Todothis,we'llusealinechartwithaspecificinterpolationthatprovidesquantizedstepsratherthanlineswithdirectlyconnectedpoints.
Wearealsoprovidingsomeinteractivityinthattheuserwillbeabletoswitchthedisplayedtimeframeusingatogglebutton.ThisallowsustoreusetheTogglecomponentthatwecreatedinChapter2,Ready,Set,Go!,ofthisbook.
Let'sfirstlookatthedatathatwehaveinoursystemandhowwecantransformitintothedataneededbyChartist.
Wecanrelyontwodataattributesofourtasksinordertodrawthemontoatimeline.Thecreatedattributeissettothetimestampatthemomentwhenthetaskwascreated.Ifataskismarkedasdone,thedoneattributeissettothetimestampatthattime.
Aswe'reonlyinterestedintheamountofopentasksatanygiventime,wecansafelypresumeamodelwhereweputalltasksontoasingletimelineandwhereweareonlyconcernedaboutthecreatedanddonetimestampsasevents.Let'slookatthefollowingillustrationtogetabetterunderstandingoftheproblem:
Anillustrationthatshowshowwecanrepresentalltasktimelinesonasingletimelineusingthecreatedanddoneevents.Thecreatedeventscountasa+1,whilethedoneeventscountas
-1.
Thelowerarrowisarepresentationofalltasksofthecreatedanddoneeventsonatimeline.Wecannowusethisinformationasinputtoourrasterizefunctioninordertogetthedatathatweneedforourchart.AsthetimeDataobjectsthatareusedasinputfortherasterizationalsosupportaweightproperty,wecanusethistorepresentthecreated(+1)ordone(-1)events.
www.EBooksWorld.ir
Weneedtomakeaslightmodificationtoourrasterizefunction.Sofar,therasterizefunctiononlycountseventstogetherinframes.However,fortheopentaskcounts,welookintoanaccumulationovertime.Ifthetaskcountchanges,weneedtokeepthevalueuntilitchangesagain.Intransformationofactivitiesintheprevioustopic,wedidn'tusethissamelogic.There,weonlycountedeventsinsideframes,buttherewasnoaccumulation.
Let'slookatthefollowingillustrationtoseethedifferenceascomparedtotherasterizationthatweappliedwhileprocessingactivities:
Anillustrationthatshowshowwecanaccumulatetheopentaskscountovertime
WecancounteachweightpropertyofthetimeDataobjects(events)togetherovertime.Onlyifthere'sachangeoftheaccumulatedvalue,wewillwritethecurrentaccumulatedvalueintotherasterizedoutputarray.
Let'sopenourtime-utilitiesmoduleandapplythechangestotherasterizefunction:
exportfunctionrasterize(timeData,timeFrame,quantity,
now=+newDate(),fill=0,
accumulate=false){
//Floortoagiventimeframe
now=Math.floor(now/timeFrame)*timeFrame;
//Accumulationvalueusedforaccumulationmodetokeeptrack
//ofcurrentvalue
letaccumulatedValue=0;
//Inaccumulationmodeweneedtobesurethatthetimedata
//isordered
if(accumulate){
timeData=timeData.slice().sort(
(a,b)=>a.time<b.time?-1:a.time>b.time?1:0);
}
returntimeData.reduce((rasterized,timeData)=>{
//Increasetheaccumulatedvalue,incaseweneedit
accumulatedValue+=timeData.weight;
//Calculatingthedesignatedindexintherasterizedoutput
//array
www.EBooksWorld.ir
constindex=Math.ceil((now-timeData.time)/timeFrame);
//Iftheindexislargerorequaltothedesignedrasterized
//arraylength,wecanskipthevalue
if(index<quantity){
rasterized[index]=accumulate?
accumulatedValue:
(rasterized[index]||0)+timeData.weight;
}
returnrasterized;
},Array.from({length:quantity}).fill(fill)).reverse();
}
Let'swalkthroughthechangesthatweappliedtotherasterizefunctiontoallowaccumulationofframes:
Firstofall,weaddedanewparametertoourfunctionwiththenameaccumulate.WeusedtheES6defaultparameterstosettheparametertofalseifnovaluewaspassedintothefunctionwhencalled.WenowdefineanewaccumulatedValuevariable,whichweinitializewith0.Thisvariablewillbeusedtokeeptrackofthesumofallweightvaluesovertime.Thenextbitofcodeisveryimportant.Ifwewanttoaccumulatethesumofallweightvaluesovertime,weneedtomakesurethatthesevaluescomeinsequence.Inordertoensurethis,wesortthetimeDatalistbyitsitemstimeattribute.Inthereducecallback,weincreasetheaccumulatedValuevariablebytheweightvalueofthecurrenttimeDataobject.IfthetimeDataobjectfallsintoarasterizedframe,wedonotincreasethisframe'scountlikewedidbefore.Inaccumulationmode,wesettheframescounttothecurrentvalueinaccumulatedValue.Thiswillresultinallchangedaccumulatedvaluesbeingreflectedintherasterizedoutputarray.
Thisisallthepreparationthatweneedtoprocessthedateinordertorenderouropentaskschart.Let'smoveonandcreatetheComponentclassofournewchartcomponent.
www.EBooksWorld.ir
CreatinganopentaskschartInthefollowingcomponent,wewillutilizetherefactoredrasterizefunctionoftheprevioustopic.Usingthenewaccumulatefunction,wecannowtrackopentaskcountsovertime.
Let'sstartwiththeComponentclassinanewlib/projects-dashboard/tasks-chart/tasks-chart.jsfiletoimplementourComponentclass:
...
importChartistfrom'chartist';
importMomentfrom'moment';
import{rasterize}from'../../utilities/time-utilities';
@Component({
selector:'ngc-tasks-chart',
...
})
exportclassTasksChart{
@Input()projects;
@ViewChild('chartContainer')chartContainer;
ngOnChanges(){
this.createOrUpdateChart();
}
ngAfterViewInit(){
this.createOrUpdateChart();
}
...
}
Sofar,thislooksexactlylikeourfirstchartcomponentwherewevisualizedprojectactivities.WealsoimportedChartistaswewilluseittorenderourchartinthecreateOrUpdateChartfunctionthatwe'llcreateshortly.Thechartthatwewillcreatewillcontainmuchmoredetailedinformation.Wewillrenderbothaxislabelsandsomescales.Inordertoformatourlabelsthatbasicallycontaintimestamps,weusetheMoment.jslibraryonceagain.
Wealsousetheprojectsinputdataandtransformitwiththeamendedrasterizeutilityfunctioninordertoprepareallthedataforourlinechart.
Let'smoveonandfleshoutthecreateOrUpdateChartmethodofourcomponent:
createOrUpdateChart(){
if(!this.projects||!this.chartContainer){
return;
}
//Createaseriesarraythatcontainsonedataseriesforeach
//project
constseries=this.projects.map((project)=>{
www.EBooksWorld.ir
//FirstweneedtoreducesalltasksintoonetimeDatalist
consttimeData=project.tasks.reduce((timeData,task)=>{
//ThecreatedtimeofthetaskgeneratesatimeDatawith
//weight1
timeData.push({
time:task.created,
weight:1
});
//Ifthistaskisdone,we'realsogeneratingatimeData
//objectwithweight-1
if(task.done){
timeData.push({
time:task.done,
weight:-1
});
}
returntimeData;
},[]);
//Usingtherasterizefunctioninaccumulationmode,wecan
//createtherequireddataarraythatrepresentsourseries
//data
returnrasterize(timeData,600000,144,+newDate(),
null,true);
});
constnow=+newDate();
//Creatinglabelsforallthetimeframeswe'redisplaying
constlabels=Array.from({
length:144
}).map((e,index)=>now-index*600000).reverse();
if(this.chart){
//Ifwealreadyhaveavalidchartobject,wecansimply
//updatetheseriesdataandlabels
this.chart.update({
series,
labels
});
}else{
//CreatinganewlinechartusingthechartContainerelement
//ascontainer
this.chart=newChartist.Line(this.chartContainer.nativeElement,{
series,
labels
},{
width:'100%',
height:300,
//Usingstepinterpolation,wecancausethechartto
//renderinstepsinsteadofdirectlyconnectedpoints
lineSmooth:Chartist.Interpolation.step({
//Thefillholessettingontheinterpolationwillcause
//nullvaluestobeskippedandmakesourlineto
//connecttothenextvalidvalue
fillHoles:true
}),
www.EBooksWorld.ir
axisY:{
onlyInteger:true,
low:0,
offset:70,
//We'reusingthelabelinterpolationfunctionfor
//formattingouropentaskscount
labelInterpolationFnc:(value)=>`${value}tasks`
},
axisX:{
//We'reonlydisplayingtwox-axislabelsandgridlines
labelInterpolationFnc:(value,index,array)=>index%(144/4)===0?
Moment(value).calendar():null
}
});
}
}
Okay,that'squiteabitofcodehere.Let'swalkthroughitstepbysteptogainabetterunderstandingofwhat'sgoingon:
1. First,weneedtocreateourtransformedseriesdatabymappingtheprojectslist.Theseriesarrayshouldincludeonedataarrayforeachproject.Eachdataarraywillcontaintheopenprojecttasksovertime.
2. AstherasterizefunctionexpectsalistoftimeDataobjects,wefirstneedtotransformtheprojectstasklistintothisformat.Byreducingthetasklist,wecreateasinglelistofthetimeDataobjects.ThereducefunctioncallbackwillgenerateonetimeDataobjectwithaweightof1foreachtask.Additionally,itwillgenerateatimeDataobjectforeachtaskmarkedasonewiththeweightvalue-1.ThiswillresultinthedesiredrimeDataarray,whichwecanusetoaccumulateandrasterize.
3. AfterpreparingthetimeDatalist,wecancalltherasterizefunctioninordertocreatealistofopentasksoveracertainamountoftimeframes.Weusea10minutetimeframe(600000ms)andrasterizethiswith144frames.Thismakesatotalof24hours.
4. Besidestheseriesdata,wewillalsoneedlabelsforourchart.Wecreateanewarrayandinitializethiswith144timestamps,allofwhicharesettothestartofthe144rasterizedframesthatwedisplayonthechart.
5. Now,wehavetheseriesdataandthelabelsready,andallthat'slefttodoistorenderourchart.
6. UsingthelineSmoothconfiguration,wecanspecifyaspecialkindofinterpolationforourlinechart.Thestepinterpolationwillnotconnecteachpointinourlinechartdirectly,butratheritwillmoveindiscretestepstomovefrompointtopoint.Thisisexactlywhatwe'relookingfortorendertheopentaskcountsovertime.
7. SettingthefillHolesoptiontotrueinthestepinterpolationisveryimportant.Usingthissetting,wecanactuallytellChartistthatitshouldcloseanygapswithinthedata(actuallynullvalues)andconnectthelinetothenextvalidvalue.Withoutthissetting,we'dseegapsonthechartbetweenthetaskcountchangesinourdataarrays.
8. OnelastimportantthinginourcodeisthelabelInterpolationFncoptionthatwesetonthexaxisconfiguration.Thisfunctioncannotonlybeusedtoformatalabelorinterpolateanyexpressionthatmaycomealongwiththelabel,butitalsoallowsusto
www.EBooksWorld.ir
returnnullinstead.ReturningnullfromthisfunctionwillcauseChartisttoskipthegivenlabelandthecorrespondinggridline.Thisisveryusefulifwe'dliketoskipcertainlabelsbytheirvalueorbytheindexofthelabel.Inourcode,weensurethatweonlyrenderfourlabelsofall144generatedlabels.
Let'stakealookattherathersimpletemplateofourcomponentinthetasks-chart.htmlfileinthesamefolderasourComponentclassfile:
<div#chartContainerclass="tasks-chart__container"></div>
ThesameaswiththeActivityChartcomponent,weonlycreateasimplechartcontainerelement,whichwealreadyreferenceinourComponentclass.
ThisisbasicallyallthatweneededtodoinordertocreateanopentaskschartusingChartist.However,there'sstillsomeroomforimprovementhere:
OpentasksvisualizedwithourtaskschartcomponentusingChartist'sstepinterpolation
www.EBooksWorld.ir
CreatingachartlegendCurrently,there'snowaytotellexactlywhichofthelinesrepresentswhatproject.Weseeonecoloredlineforeachproject,butwecan'tassociatethesecolors.Whatweneedisasimplelegendthathelpsouruserstoassociatelinechartcolorstoprojects.
Let'slookattherequiredcodechangestoimplementlegendsonourchart.IntheComponentclassofourTasksChartcomponent,weneedtoperformthefollowingmodifications:
...
exportclassTasksChart{
...
ngOnChanges(){
if(this.projects){
//Onchangesoftheprojectsinput,weneedtoupdatethe
//legend
this.legend=this.projects.map((project,index)=>{
return{
name:project.title,
class:`tasks-chart__series--series-${index+1}`
};
});
}
this.createOrUpdateChart();
}
...
}
IntheOnChangeslifecyclehook,wemaptheprojectsinputtoalistofobjectsthatcontainanameandclassproperty,whichwillsupportusinrenderingasimplelegend.Thetemplate`tasks-chart__series--series-${index+1}`stringwillgeneratethenecessaryclassnametorendertherightcolorintoourlegend.
Usingthislegendinformation,wecannowgoaheadandimplementthenecessarytemplatechangestorenderthelegendinourchartcomponent:
<ulclass="tasks-chart__series-list">
<li*ngFor="letseriesoflegend"
class="tasks-chart__series{{series.class}}">
{{series.name}}
</li>
</ul>
<div#chartContainerclass="tasks-chart__container"></div>
Well,thatwasapieceofcake,right?However,theresultspeaksforitself.Wecreatedanicelegendforthechartinjustacoupleofminutes:
www.EBooksWorld.ir
Opentaskschartwithouraddedlegend
www.EBooksWorld.ir
MakingtaskschartinteractiveCurrently,wehardcodedthetimeframeofouropentaskcharttobe144frames,eachof10minutes,makingatotalof24hoursdisplayedtotheuser.However,maybeouruserswouldwanttochangethisview.
Inthistopic,wewillcreateasimpleinputcontrolusingourTogglecomponent,whichallowsouruserstochangethetimeframesettingsofthechart.
Wewillprovidethefollowingviewsasoptions:
Day:Thisviewwillrasterize144frames,eachconsistingof10minutes,whichmakesatotalof24hoursWeek:Thisviewwillrasterize168frames,eachconsistingofonehour,whichmakesatotalofsevendaysYear:Thisviewwillrasterize360frames,eachrepresentingafullday
Let'sstartwiththeimplementationofourtimeframeswitchbymodifyingtheComponentclasscodeoftheTasksChartcomponent:
...
import{Toggle}from'../../ui/toggle/toggle';
@Component({
...
directives:[Toggle]
})
exportclassTasksChart{
...
constructor(){
//Definetheavailabletimeframeswithinthechartprovided
//totheuserforselection
this.timeFrames=[{
name:'day',
timeFrame:600000,
amount:144
},{
name:'week',
timeFrame:3600000,
amount:168
},{
name:'year',
timeFrame:86400000,
amount:360
}];
//Fromtheavailabletimeframes,we'regeneratingalistof
//namesforlaterusewithintheTogglecomponent
this.timeFrameNames
=this.timeFrames.map((timeFrame)=>timeFrame.name);
//Thecurrentlyselectedtimeframeissettothefirst
//availableone
this.selectedTimeFrame=this.timeFrames[0];
www.EBooksWorld.ir
}
...
createOrUpdateChart(){
...
constseries=this.projects.map((project)=>{
...
returnrasterize(timeData,
this.selectedTimeFrame.timeFrame,
this.selectedTimeFrame.amount,
+newDate(),null,true);
});
constnow=+newDate();
constlabels=Array.from({
length:this.selectedTimeFrame.amount
}).map((e,index)=>now-index*
this.selectedTimeFrame.timeFrame).reverse();
...
}
...
//CalledfromtheTogglecomponentifanewtimeframewas
//selected
onSelectedTimeFrameChange(timeFrameName){
//Settheselectedtimeframetotheavailabletimeframewith
//thenameselectedintheTogglecomponent
this.selectedTimeFrame=
this.timeFrames.find((timeFrame)=>
timeFrame.name===timeFrameName);
this.createOrUpdateChart();
}
}
Let'sgothroughthesechangesbriefly:
1. Firstofall,weaddedaconstructortoourComponentclassinwhichweinitializedthreenewmembers.ThetimeFramesmemberissettoanarrayoftimeframedescriptionobjects.Theycontainthename,timeFrame,andamountproperties,whicharelaterusedforthecalculations.ThetimeFrameNamesmembercontainsalistoftimeframenames,whichisdirectlyderivedfromthetimeFrameslist.Finally,wehaveaselectedTimeFramemember,whichsimplypointstothefirstavailabletimeframeobject.
2. InthecreateOrUpdateChartfunction,wenolongerrelyonhardcodedvaluesforthetaskcountrasterization,butwerefertothedataintheselectedTimeFrameobject.BychangingthisobjectreferenceandcallingthecreateOrUpdateChartfunctionagain,wecannowswitchtheviewontheunderlyingdatadynamically.
3. Finally,weaddedanewonSelectedTimeFrameChangemethod,whichactsasabindingtotheTogglecomponent,andthiswillbecalledwhenevertheuserselectsadifferenttimeframe.
Let'slookatthenecessarytemplatechangestoenableswitchingoftimeframes:
<ngc-toggle
[buttonList]="timeFrameNames"
www.EBooksWorld.ir
[selectedButton]="selectedTimeFrame.name"
(selectedButtonChange)="onSelectedTimeFrameChange($event)">
</ngc-toggle>
...
<div#chartContainerclass="tasks-chart__container"></div>
FromthebindingstotheTogglecomponent,youcanalreadytellthatwerelyonthetimeFrameNamesmemberonourcomponenttorepresentallselectabletimeframes.WealsobindtotheselectedButtoninputpropertyoftheTogglecomponentusingtheselectedTimeFrame.nameproperty.OnchangesoftheselectedbuttonintheTogglecomponent,wecalltheonSelectedTimeFrameChangefunction,wherethetimeframeisswitchedandthechartisupdated.
Thisisallthatweneedtoenableswitchingthetimeframeonourchart.Theusercannowchoosebetweentheyear,week,anddayviews.
OurTasksChartcomponentisnowreadytobeintegratedintoourdashboard.WecanachievethiswithsomesmallchangestothetemplateofourProjectsDashboardcomponent:
...
<divclass="projects-dashboard__l-main">
<h3class="projects-dashboard__sub-title">TasksOverview</h3>
<divclass="projects-dashboard__tasks">
<ngc-tasks-chart[projects]="projects|async">
</ngc-tasks-chart>
</div>
...
</div>
Thisisbasicallyallthatweneedtomake,andafterthischange,ourdashboardcontainsournicechartdisplayingopentaskcountsovertime.
InthebindingoftheTasksChartprojectsinputproperty,weusetheasyncpipeonceagaintoresolvetheobservablestreamofprojectsdirectlyintheview.
www.EBooksWorld.ir
SummaryInthischapter,welearnedaboutChartistandhowtouseitinconjunctionwithAngulartocreategoodlookingandfunctionalcharts.Wecanleveragethepowerofbothworlds,andcreatereusablechartcomponentsthatarenicelyencapsulated.
Justlikeinmostrealcases,wealwayshavealotofdata,buttheonethatweneedinaparticularcase.Welearnedhowwecantransformexistingdataintoaformthatisoptimizedforvisualrepresentation.
Inthenextchapter,wewilllookatbuildingapluginsysteminourapplication.Thiswillallowustodevelopportablefunctionalitythatispackagedintoplugins.Ourpluginsystemwillloadnewpluginsdynamically,andwewilluseittodevelopasimpleagileestimationplugin.
www.EBooksWorld.ir
Chapter10.MakingThingsPluggableI'mahugefanofpluginarchitectures.Besidestheirtremendouslypositiveeffectonyourapplicationandscopemanagement,theyarealsoalotoffuntodevelop.I'drecommendintegratingapluginarchitectureintheirlibraryorapplicationtoanyonewhoasksme.Agoodpluginarchitectureallowsyoutowriteaconciseapplicationcoreandprovideadditionalfunctionalityviaplugins.
Designingyourwholeapplicationinawaythatitallowsyoutobuildapluginarchitecturehasagreateffectontheextensibilityofyoursystem.Thisisbecauseyou'remakingyourapplicationopenforextensibilitybutclosingitformodification.
Whileauthoringmyopensourceprojects,Ialsoexperiencedthatapluginarchitecturehelpsyoumanagethescopeofyourproject.Sometimes,arequestedfeatureisreallyniceandhelpful,butitwillstillbloatthelibrarycore.Insteadofbloatingyourwholeapplicationorlibrarywithsuchfeatures,youcansimplywriteaplugintogetthejobdone.
Inthischapter,wewillcreateourownpluginarchitecturethatwillhelpusextendthefeaturesofourapplicationwithoutbloatingitscore.We'llfirstbuildthepluginAPIinthecoreofourapplicationandthenusetheAPItoimplementanicelittleagileplugin,whichhelpsustoestimatetasksusingstorypoints.
We'llcoverthefollowingtopicsinthischapter:
Designingapluginarchitecture,basedontheAngularecosystemImplementingadecorator-basedpluginAPIUsingComponentResolverandViewContainerReftoinstantiateplugincomponentsintopredefinedslotsinourapplicationImplementingaplugin-loadingmechanismusingSystemJSUsingareactiveapproachinourpluginarchitecturetoenableplugandplaystylepluginsImplementinganagileplugintorecordstorypointsusingthenewpluginAPI
www.EBooksWorld.ir
PluginarchitectureAtahigherlevel,apluginarchitectureshouldfulfilatleastthefollowingrequirements:
Extensibility:Themainideabehindpluginsistoallowtheextensionofthecorefunctionalityusingisolatedbundlesofcode.Agreatpluginarchitectureallowsyoutoextendthecoreseamlesslyandwithoutnoticeableperformancelosses.Portability:Pluginsshouldbeisolatedenoughsothattheycanbepluggedintothesystemduringruntime.Thereshouldn'tbeanecessitytorebuildasystemtoenableplugins.Ideally,pluginscanevenbeloadedatanytimeduringruntime.Theycanbedeactivatedandactivatedandshouldnotcausethesystemtorunintoaninconsistentstate.Composability:Apluginsystemshouldallowtheuseofmanypluginsinparallelandallowanextensionofthesystembycompositingmultiplepluginstogether.Ideally,thesystemalsoincludesdependencymanagement,pluginversionmanagement,andpluginintercommunication.
Therearealotofdifferentapproachesonhowtoimplementapluginarchitecture.Althoughtheseapproachescanvaryalot,there'salmostalwaysamechanisminplacethatprovidesunifiedextensionpoints.Withoutthis,itwillbehardtoextendasystemuniformly.
I'veworkedwithsomepluginarchitecturesinthepast,andbesidesusingexistingpluginmechanisms,I'vealsoenjoyeddesigningsomeofthemmyself.Thefollowinglistshouldprovideanideaaboutsomeoftheapproachesthatyoucanusewhendesigningapluginsystem:
DSL:Usingdomain-specificlanguagesisonewaytoimplementapluggablearchitecture.Afteryou'veimplementedthecoreofyourapplication,youcandevelopanAPIorevenascriptinglanguagethatallowsyoutodevelopfurtherfeaturesusingthisDSL.AlotofvideogameenginesandCGapplicationsrelyonthisapproach.Althoughthisapproachisveryflexible,itcanalsoleadtoperformanceissuesquickly,andit'spronetointroducingcomplexity.Mostly,theprerequisitestoimplementsuchanarchitecturearetoexposeverylow-levelcoreoperations(suchasaddingUIelements,configuringprocessflows,andsoon)intotheDSL,whichdoesnotprovideclearboundariesandextensionpointsbutisextremelyflexible.SomeexamplesofDSL-basedpluginsystemsaremostofAdobe'sCGapplications,3DStudioMax,andMaya,butalsogameengines,suchasUnrealEngineortheRealVirtualityEnginefromBohemiaInteractiveStudio.Thecoreisthepluginsystem:Anotherapproachistobuildsuchasophisticatedpluginarchitecturethatitfulfilsalltheoutlinedrequirementsinthepreviouslisting(extensibility,portability,andcomposability)andevensomemoresophisticatedrequirementsontop.Thecoreofyourapplicationisonelargepluginsystem.Then,youstarttoimplementeverythingasaplugin.Eventhecoreconcernsofyourapplicationwillbeimplementedasplugins.AperfectexampleofthisapproachistheEclipseIDEwithitsEquinoxcore.Theproblemwiththisapproachisthatyou'relikelytorunintoperformanceproblemsasyourapplicationgrows.Aseverythingisaplugin,
www.EBooksWorld.ir
optimizationisquitetricky,andplugincompatibilitycanmaketheapplicationunstable.Event-basedextensionpoints:Also,agreatwaytoprovideextensibilityofasystemisbyopeningupthepipelineofyoursystemtoinputfromoutside.Imaginethatforeveryimportantstepinyourapplication,younotifytheoutsideworldaboutthestepandallowinterceptionbeforetheapplicationcontinueswithprocessing.Inthismanner,apluginwilljustbeanadapterthatlistensforthesepipelineeventsofyourapplicationandthenmodifiesthebehaviorasrequired.Apluginitselfcanalsoemitevents,whichthencanbeprocessedbyotherpluginsagain.Thisarchitectureisreallyflexible,asitallowsyoutochangethebehaviorofyourcorefunctionalitywithoutintroducingtoomuchcomplexity.It'salsofairlyeasytoimplementthisapproachevenafteryou'vefinishedyourcorewithoutanythoughtsaboutapluginsystem.I'vebeenfollowingthisapproachinmyopensourceprojectChartistand,sofar,I'vehadverygoodresultswithit.Plugininterfaces:Anapplicationcanexposeasetofinterfacesthatdefinecertainextensionpoints.ThisapproachisheavilyusedintheJavaframeworkwhereit'sknownasServiceProviderInterface(SPI).Providersimplementacertaincontract,whichallowsthecoresystemtorelyonaninterfaceratherthananimplementation.Theseproviderscanthenbecycledbackintothesystemwheretheyaremadeavailabletotheframeworkandotherproviders.Althoughthisisprobablythesafestwaytoprovideextensibilityintermsofuniformness,it'salsothemostrigidone.Apluginwillneverbeallowedtodoanythingelsethatwasspecifiedinthecontractoftheinterfaces.
Youcanseethatallfourapproachesvaryalot.Fromthetop-most,whichprovidesextremeflexibilityatthecostofcomplexityandstability,tothebottom-most,whichisveryrobustbutalsorigid.
Theapproachthatyouchoosewhenimplementingapluginsystemheavilydependsontherequirementsforyourapplication.Ifyoudonotplanonbuildinganapplicationthatcomesbundledinvariousflavorsandwheremultipleversionsforcompletelydifferentconcernsshouldexist,theapproachestothebottomoftheprecedinglistingareprobablymorelikelytheonesthatyoushouldfollow.
www.EBooksWorld.ir
PluggableUIcomponentsThesystemthatwe'regoingtobuildinthischapterborrowsalotofmechanismsthatarealreadypresentintheAngularframework.Inordertoimplementextensibilityusingplugins,werelyonthefollowingcoreconcepts:
WeusedirectivestoindicateextensionpointsintheUI,whichwecallpluginslots.ThesepluginslotdirectiveswillberesponsibleforthedynamicinstantiationofplugincomponentsandwillinsertthemintotheapplicationUIatthegivenposition.Pluginsexposecomponentsusingaconceptthatwecallpluginplacements.Pluginplacementsdeclarewhatcomponentsofapluginshouldbeplacedintowhichpluginslotsintheapplication.Wealsousepluginplacementstodecidetheorderinwhichcomponentsfromdifferentpluginsshouldbeinsertedintothepluginslots.Forthis,we'lluseapropertycalledpriority.WeusethedependencyinjectionofAngulartoprovidetheinstantiatedplugininformationintotheplugincomponents.Astheplugincomponentswillbeplacedinaspotwherethere'salreadyaninjectorpresent,theywillbeabletoinjectsurroundingcomponentsanddependenciesinordertoconnecttotheapplication.
Let'slookatthefollowingillustrationtopicturethearchitectureofourpluginsystembeforewestartimplementingit:
Thepluginarchitecturethatwe'llimplementinthischapterusingsomebasicUMLandcardinalityannotations
Let'slookatthedifferententitiesinthisdiagramandquicklyexplainwhattheydo:
PluginConfig:ThisES7decoratoristhekeyelementwhenimplementingaplugin.By
www.EBooksWorld.ir
annotatingapluginclassusingthisdecorator,wecanstoremeta-informationabouttheplugin,whichwillbeusedlaterbyourpluginsystem.Themetadataincludesthepluginname,adescription,andtheplacementinformation.PluginData:Thisisanaggregationclassthatisusedbythepluginsystemtocoupletheinformationaboutaninstantiatedpluginwiththeplacementinformation(whereplugincomponentsshouldbeinstantiated).Thisentityisexposedindependencyinjectiononceaplugincomponentiscreated.Anyplugincomponentcanmakeuseofthisentitytogatherinformationabouttheinstantiationortogainaccesstotheplugininstance.PluginService:Thisistheserviceusedtoglueourpluginsystemtogether.It'smainlyusedtoloadplugins,removeplugins,orusedbythePluginSlotdirectivetogatherplugincomponentstogetherthatarerelevantforcreationinthepluginslot.PluginSlot:ThisdirectiveisusedtomarkUIextensionpointsinourapplication.Whereverwe'dliketomakeitpossibleforpluginstohookintoourapplicationuserinterface,we'llplacethisdirective.Pluginslotsneedtobenamed,andpluginsuseplacementinformationtoreferenceslotsbytheirname.Thiswayaplugincanprovidedifferentcomponentsfordifferentslotsinourapplication.PluginComponent:TheseareregularAngularcomponentsthatcomebundledwithapluginimplementation.AplugincanprovidemultiplecomponentsconfiguredonthepluginusingaPluginPlacementobject.PluginPlacement:Thisisusedinthepluginconfigurationwhereaplugincanhavemultipleplacementconfigurations.Eachplacemententityconsistofareferencetoacomponent,thenameoftheslotwherethecomponentshouldbeinstantiated,andaprioritynumberthathelpsthepluginsystemtoorderplugincomponentscorrectlywhenmultiplecomponentsgetinstantiatedinthesameslot.Plugin:Thisistheactualpluginclasswhenimplementingaplugin.TheclasscontainsthepluginconfigurationannotatedusingthePluginConfigdecorator.ThepluginclassisinstantiatedonceintheapplicationandisalsosharedacrosstheplugincomponentsusingthedependencyinjectionofAngular.Therefore,thisclassisalsoagoodplacetosharedatabetweenplugincomponents.
Now,wehaveanoverviewofwhatwe'regoingtobuildonahigherlevel.Ourpluginsystemisveryrudimentary,butitwillsupportthingssuchashotloadingplugins(plugandplaystyle)andothernicefeatures.Inthenexttopic,we'llstartbyimplementingthepluginAPIcorecomponents.
www.EBooksWorld.ir
ImplementingthepluginAPILet'sstartwiththelesscomplexentitiesofourpluginAPI.Wecreateanewlib/plugin/plugin.jsfiletocreatethePluginConfigdecoratorandthePluginPlacementclass,whichstorestheinformationwhereplugincomponentsshouldbeplaced.WealsocreatethePluginDataclassinthisfile,whichisusedtoinjectpluginruntimeinformationintoplugincomponents:
exportfunctionPluginConfig(config){
return(type)=>{
type._pluginConfig=config;
};
}
ThePluginConfigdecoratorcontainstheverysimplelogicofacceptingaconfigurationparameter,whichwillthenbestoredontheannotatedclass(theconstructorfunction)onthe_pluginConfigproperty.Ifyouneedarefresheronhowdecoratorswork,it'smaybeagoodtimetoreadthedecoratortopicinChapter1,Component-BasedUserInterfaces,again:
exportclassPluginPlacement{
constructor(options){
this.slot=options.slot;
this.priority=options.priority;
this.component=options.component;
}
}
ThePluginPlacementclassrepresentstheconfigurationobjecttoexposeplugincomponentsintodifferentpluginslotsintheapplicationUI:
exportclassPluginData{
constructor(plugin,placement){
this.plugin=plugin;
this.placement=placement;
}
}
ThePluginDataclassrepresentsthepluginruntimeinformationthatwascreatedduringinstantiationofthepluginaswellasonePluginPlacementobject.ThisclasswillbeusedbythePluginServicetoconveyinformationaboutplugincomponentstothepluginslotsintheapplication.
Thesethreeclassesarethemaininteractionpointswhenimplementingaplugin.
Let'slookatasimpleexampleplugin,togetapictureofhowwecanusethePluginConfigdecoratorandthePluginPlacementclasstoconfigureaplugin:
@PluginConfig({
name:'my-example-plugin',
description:'Asimpleexampleplugin',
www.EBooksWorld.ir
placements:[
newPluginPlacement({
slot:'plugin-slot-1',
priority:1,
component:PluginComponent1
}),
newPluginPlacement({
slot:'plugin-slot-2',
priority:1,
component:PluginComponent2
})
]
})
exportdefaultclassExamplePlugin{}
UsingthePluginConfigdecorator,implementinganewpluginisabreeze.Wedecidethename,description,andwherewe'dliketoplaceplugincomponentsintheapplicationatdesigntime.
OurpluginsystemusesnamedPluginSlotdirectivestoindicateextensionpointsinourapplicationcomponenttree.InthePluginPlacementobjects,wereferencetheAngularcomponentsbuiltintothepluginandindicateinwhichslottheyshouldbeplacedbyreferencingthepluginslotname.Thepriorityoftheplacementwilltellthepluginslothowtoordertheplugincomponentwhencreated.Thisgetsimportantwhencomponentsofdifferentpluginsgetcreatedinthesamepluginslot.
Okay,let'sdiverightintothecoreofourpluginarchitecturebyimplementingthepluginservice.We'llcreateanewlib/plugin/plugin-service.jsfileandcreateanewPluginServiceclass:
import{Injectable}from'@angular/core';
import{ReplaySubject}from'rxjs/Rx';
@Injectable()
exportclassPluginService{
...
}
Aswewillcreateaninjectableservice,we'llannotateourPluginServiceclassusingthe@Injectableannotation.WeusetheRxJSReplaySubjecttypeinordertoemiteventsonanychangesoftheactivatedplugins.
Let'slookattheconstructorofourservice:
constructor(){
this.plugins=[];
//Changeobservableifthelistofactivepluginchanges
this.change=newReplaySubject(1);
this.loadPlugins();
}
www.EBooksWorld.ir
First,weinitializeanewemptypluginsarray.Thiswillbethelistofactiveplugins,whichcontainsruntimeplugindatasuchastheURLwherethepluginwasloadedfrom,theplugintype(constructoroftheclass),ashortcuttotheconfigurationstoredontheplugin(createdbythePluginConfigdecorator)andfinally,theinstanceofthepluginclassitself.
WealsoaddachangememberthatweinitializewithanewRxJSReplaySubject.We'llusethissubjectinordertoemitthelistofactivepluginsonceitchanges.Thisallowsustobuildourpluginsysteminareactivewayandenableplugandplaystyleplugins.
Asalastactionintheconstructor,wecalltheloadPluginsmethodoftheservice.Thiswillperformtheinitialloadingwiththeregisteredplugins:
loadPlugins(){
System.import('/plugins.js').then((pluginsModule)=>{
pluginsModule.default.forEach((pluginUrl)=>
this.loadPlugin(pluginUrl)
);
});
}
TheloadPluginsmethodasynchronouslyloadsafilewiththenameplugins.jsfromtherootpathofourapplicationusingSystemJS.Theplugins.jsfileisexpectedtodefaultexportanarray,whichcontainspreconfiguredpathstopluginsthatshouldbeloadedwiththeapplicationstartup.Thisallowsustoconfigurethepluginsthatwe'realreadyawareofandwhichshouldbepresentbydefault.Usingaseparateandasynchronouslyloadedfileforthisconfigurationgivesusabetterseparationfromthemainapplication.Wecanrunthesameapplicationcodebutusingadifferentplugins.jsfileandcontrolwhatpluginsshouldbepresentbydefault.
TheloadPluginsmethodthenloadseachpluginusingtheURLpresentintheplugins.jsfilebycallingtheloadPluginmethod:
loadPlugin(url){
returnSystem.import(url).then((pluginModule)=>{
constPlugin=pluginModule.default;
constpluginData={
url,
type:Plugin,
//Readingthemetadatapreviouslystoredbythe@Plugin
//decorator
config:Plugin._pluginConfig,
//Createstheplugininstance
instance:newPlugin()
};
this.plugins=this.plugins.concat([pluginData]);
this.change.next(this.plugins);
});
}
TheloadPluginmethodisresponsiblefortheloadingandinstantiationofindividualplugin
www.EBooksWorld.ir
modules.ItwilltaketheURLofapluginmoduleasparameterandusesSystem.importtodynamicallyloadthepluginmodule.ThebenefitswegetfromusingSystem.importforthisjobisthatwecanloadboth,alreadyexistingmodulesinthebundledapplicationaswellasremoteURL'susingHTTPrequests.Thismakesourpluginsystemveryportable,andwecanevenloadmodulesduringruntimefromadifferentserver,fromNPMorevenGitHub.Ofcourse,SystemJSalsosupportsdifferentmoduleformats,suchasES6modulesorCommonJSmodules,aswellasdifferenttranspilersifthemodulesarenotalreadytranspiled.
Afterthepluginmoduleissuccessfullyloaded,webundleallinformationabouttheloadedplugintogetherintoapluginDataobject.WecanthenaddthisinformationtoourpluginsarrayandemitaneweventonourReplaySubjecttonotifyinterestedpartiesaboutthechange.
Finally,we'llneedamethodtogatherthePluginPlacementdatafromallourpluginsandfilterthembyaslotname.Thisgetsimportantwhenourpluginslotsneedtoknowwhichcomponentstheyshouldinstantiate.Pluginscanexposeanynumberofcomponentsintoanynumberofapplicationpluginslots.ThisfunctionwillbeusedbyourpluginslotswhentheyneedtoknowwhichoftheexposedAngularcomponentsarerelevanttothem:
getPluginData(slot){
returnthis.plugins.reduce((components,pluginData)=>{
returncomponents.concat(
pluginData.config.placements
.filter((placement)=>placement.slot===slot)
.map((placement)=>newPluginData(pluginData,placement))
);
},[]);
ThisisalreadyitforthePluginServiceclasssofar,andwecreatedthecoreofourpluginsystem.Inthenextchapter,wewilldealwiththepluginslotsandlookathowwecaninstantiateplugincomponentsdynamically.
www.EBooksWorld.ir
InstantiatingplugincomponentsNow,it'stimetolookatthesecondmajorpieceofourpluginarchitecture,whichisthePluginSlotdirectivethatisresponsiblefortheinstantiationofplugincomponentsintherightspots.
Beforewegettoimplementthedirectivethough,let'slookathowwecaninstantiateacomponentdynamicallyinAngular.WealreadycoveredinstantiatingviewsthatcancontaincomponentsinChapter7,ComponentsforUserExperience.Intheinfinitescrolldirective,weusedtheViewContainerReftoinstantiatetemplateelements.However,wehaveadifferentusecasehere.We'dliketoinstantiateasinglecomponentintoanexistingview.
TheViewContainerRefobjectalsoprovidesuswithasolutiontothisproblem.Let'slookataverybasicexampleonhowtousetheViewContainerRefobjecttoinstantiateacomponent.Inthefollowingexample,wemakeuseoffournewconcepts:
Using@ViewChildwithreadoptionssetto{read:ViewContainerRef}toqueryforviewcontainerinsteadoftheelementUsingtheComponentResolverinstancetoobtainthefactoryofthecomponent,whichwewanttoinstantiatedynamicallyUsingReflectiveInjectortocreateanewchildinjectorthatisusedforourinstantiatedcomponentUsingViewContainerRef.createComponenttoinstantiateacomponentandattachittotheunderlyingviewoftheviewcontainer.
ThefollowingcodeexampleshowshowwecandynamicallycreateacomponentusingtheViewContainerRefinstance.
import{Component,Inject,ViewChild,ViewContainerRef,ComponentResolver}from
'@angular/core';
@Component({
selector:'hello-world',
template:'HelloWorld'
})
exportclassHelloWorld{}
@Component({
selector:'app'
template:'<h1#headingRef>App</h1>'
})
exportclassApp{
@ViewChild('headingRef',{read:ViewContainerRef})viewContainer;
constructor(@Inject(ComponentResolver)resolver){
this.resolver=resolver;
}
ngAfterViewInit(){
this.resolver
www.EBooksWorld.ir
.resolveComponent(HelloWorld)
.then((componentFactory)=>{
this.viewContainer.createComponent(componentFactory);
});
}
}
InjectedintotheconstructoroftheAppcomponent,wecanlateruseComponentResolvertoresolvetheHelloWorldcomponent.Weusethe@ViewChilddecoratortoqueryfortheheadingelementintheAppcomponent.Usually,thiswouldgiveustheElementRefobjectthatisassociatedwiththeviewelement.However,asweneedtheviewcontainerassociatedwiththeelement,wecanusethe{read:ViewContainerRef}optionstoobtaintheViewContainerRefobjectinstead.
IntheAfterViewInitlifecyclehook,wefirstcalltheresolveComponentmethodontheComponentResolverinstance.Thiscallreturnsapromise,whichresolvestoanobjectoftheComponentFactorytype.Angularusescomponentfactoriesinternallyinordertocreatecomponents.
Afterthepromisehasbeenresolved,wecannowusethecreateComponentmethodontheviewcontainerofourheadingelementtocreateourHelloWorldcomponent.
Let'slookatthecreateComponentmethodoftheViewContainerRefobjectinmoredetail:
Method Description
ViewContainerRef.createComponent
ThismethodwillcreateacomponentthatisbasedonthecomponentfactoryprovidedinthecomponentFactoryparameter.Thecompiledcomponentwillthenbeattachedtotheviewcontainerataspecificpositionprovidedbytheindexparameter.
Thefollowingaretheparameters:
componentFactory:Thisisthecomponentfactory,whichwillbeusedtocreateanewcomponent.Index:Thisistheoptionalparametertospecifythepositionintheviewcontaineratwhichthecreatedcomponentshouldbeinserted.Ifthisparameterisnotspecified,thecomponentwillbeinsertedatthelastpositionintheviewcontainer.Injector:Thisisanoptionalparameterthatallowsyoutospecifyacustominjectorforthecreatedcomponent.Thisallowsyoutoprovideadditionaldependenciesforthecreated
www.EBooksWorld.ir
component.projectableNodes:Thisisanoptionalparametertospecifynodesforcontentprojection.
Thismethodreturnsapromisethatisresolvedwhentheinstantiatedcomponentiscompiled.ThePromiseresolvestoaComponentRefobject,whichcanalsobeusedtodestroythecomponentagainlateron.
Tip
Bydefault,acomponentcreatedwiththeViewContainerRef.createComponentmethodwillinherittheinjectorfromtheparentcomponent,whichmakesthisprocesscontextaware.However,theinjectorparameterofthecreateComponentmethodisespeciallyusefulwhenyouwanttoprovideadditionaldependenciesintothecomponentthatarenotpresentonanyparentinjector.
Let'sgobacktoourPluginSlotdirectivethatisresponsiblefortheinstantiationofrelevantplugincomponents.
First,let'sthinkaboutthehigh-levelrequirementsofourPluginSlotdirectivebeforewediveintothecode:
Thepluginslotshouldcontainanameinputpropertysothatthisnamecanbereferencedfrompluginsthatwanttoprovidecomponentsfortheslot.ThedirectiveneedstoreactonchangesofthePluginServiceandre-evaluatewhatplugincomponentsneedtobeplaced.Intheinitializationofthepluginslot,weneedtoobtainalistofthePluginDataobjectsthatarerelevanttothisparticularslot.WeshouldconsultthegetPluginDatamethodofPluginServiceinordertogetthislist.UsingtheobtainedlistoftherelevantPluginDataobjects,we'llbeabletoinstantiatecomponentsthatareassociatedwiththeplacementinformationusingtheViewContainerRefobjectofourdirective.
Let'screateourPluginSlotdirectiveonthelib/plugin/plugin-slot.jspath:
import{Directive,Input,Inject,provide,ViewContainerRef,
ComponentResolver,ReflectiveInjector}from'@angular/core';
import{PluginData}from'./plugin';
import{PluginService}from'./plugin-service';
@Directive({
selector:'ngc-plugin-slot'
})
exportclassPluginSlot{
@Input()name;
...
}
www.EBooksWorld.ir
Thenameinputinourdirectiveisveryimportantforourpluginmechanism.Byprovidinganametothedirective,wecandefinenamedextensionpointsinourUIandlaterusethisnameinthePluginPlacementdataofthepluginconfigurations:
constructor(@Inject(ViewContainerRef)viewContainerRef,
@Inject(ComponentResolver)componentResolver,
@Inject(PluginService)pluginService){
this.viewContainerRef=viewContainerRef;
this.componentResolver=componentResolver;
this.pluginService=pluginService;
this.componentRefs=[];
//Subscribingtochangesonthepluginserviceandre-
//initializeslotifneeded
this.pluginChangeSubscription=
this.pluginService
.change.subscribe(()=>this.initialize());
}
Intheconstructor,wefirstinjecttheViewContainerRefobject,whichisareferencetotheviewcontainerofthedirective.Aswewanttousetheviewcontainerofthedirectivedirectly,there'snoneedtouse@ViewChildhere.Ifwewanttheviewcontainerofthecurrentdirective,wecansimplyuseinjection.We'llusethisreferencewhilewe'reinstantiatingcomponentsusingtheViewContainerRef.createComponentmethod.
Inordertoresolvecomponentsandtheirfactory,weinjecttheComponentResolverinstance.
ThePluginServiceisinjectedfortworeasons.First,we'dliketosubscribetoanychangesonthelistofactiveplugins,andsecondly,weuseittoobtainrelevantPluginDataobjectsforthisslot.
WeusethecomponentRefsmembertokeeptrackofalreadyinstantiatedplugincomponents.Thiswillhelpusdestroythemlateronwhenaplugingetsdeactivated.
Finally,wecreateanewsubscriptiontoPluginServiceandstorethesubscriptionintothepluginChangeSubscriptionmemberfield.Onanychangesoftheactivatedpluginlist,weexecutetheinitializemethodonourcomponent:
initialize(){
if(this.componentRefs.length>0){
this.componentRefs.forEach(
(componentRef)=>componentRef.destroy());
this.componentRefs=[];
}
constpluginData=
this.pluginService.getPluginData(this.name);
pluginData.sort(
(a,b)=>a.placement.priority<b.placement.priority?
1:a.placement.priority>b.placement.priority?-1:0);
www.EBooksWorld.ir
returnPromise.all(
pluginData.map((pluginData)=>
this.instantiatePluginComponent(pluginData))
);
}
Let'slookatthefourpartsoftheinitializemethodindetail:
First,wecheckwhetherthispluginslotalreadycontainsinstantiatedplugincomponentsinthecomponentRefsmember.Ifthisisthecase,weusethedetachmethodoftheComponentRefobjectstoremoveallexistinginstances.Afterthis,weinitializethecomponentRefsmemberwithanemptyarray.WeusethegetPluginDatamethodofPluginServicetoobtainalistofthePluginDataobjectsthatarerelevantforthisparticularslot.Wepassthenameofthisslottothemethod,sothePluginServicewillalreadyprovideuswithafilteredlistofplugincomponentsthatareinterestedtobeplacedinourslot.Astherecouldbemanypluginsqueueingforplacementinourslot,weareusingtheprioritypropertyofthePluginPlacementobjectstosortthelistofthePluginDataobjects.Thiswillensurethatplugincomponentswithhigherprioritywillbeplacedbeforetheoneswithalowerpriority.Thisisaniceextrafeaturethatwillcomeinhandywhenwedealwithalotofpluginsfightingforspace.ThelastcodepartinourinitializemethodcallstheinstantiatePluginComponentmethodforeachPluginDataobjectinourlist.
Now,let'screatetheinstantiatePluginComponentmethod,whichiscalledasalaststepintheinitializemethod:
instantiatePluginComponent(pluginData){
returnthis.componentResolver
.resolveComponent(pluginData.placement.component)
.then((componentFactory)=>{
//Gettheinjectorofthepluginslotparentcomponent
constcontextInjector=this.viewContainerRef.parentInjector;
//PreparingadditionalPluginDataproviderforthecreated
//plugincomponent
constproviders=[
provide(PluginData,{
useValue:pluginData
})
];
//We'recreatinganewchildinjectorandprovidethe
//PluginDataprovider
constchildInjector=ReflectiveInjector
.resolveAndCreate(providers,contextInjector);
//Nowwecancreateanewcomponentusingthepluginslotview
//containerandtheresolvedcomponentfactory
constcomponentRef=this.viewContainerRef
.createComponent(componentFactory,
this.viewContainerRef.length,
childInjector);
this.componentRefs.push(componentRef);
});
www.EBooksWorld.ir
}
Thismethodisresponsiblefortheinstantiationofanindividualplugincomponent.Now,wecanusetheknowledgethatwegainedinthistopicabouttheViewContainerRef.createComponentmethodandtheComponentResolverobjecttoinstantiatecomponentsdynamically.
Inadditiontotheinheritedprovidersfromthecomponentwherethispluginslotisplaced,we'dliketoprovidePluginDatatotheinjectoroftheinstantiatedplugincomponent.UsingAngular'sprovidefunction,wecanspecifypluginDatatoresolveforanyinjectiononthePluginDatatype.
TheReflectiveInjectorclassprovidesuswithsomestaticmethodsthatareusedtocreateinjectors.WecanusetheparentInjectormemberonourviewcontainertoobtaintheinjectorthatispresentinthepluginslotcontext.Then,weusethestaticresolveAndCreatemethodontheReflectiveInjectorclassinordertocreateanewchildinjector.
InthefirstparameteroftheresolveAndCreatemethod,wecanprovidealistofproviders.Thoseproviderswillberesolvedandmadeavailableinournewchildinjector.ThesecondparameteroftheresolveAndCreatemethodacceptstheparentinjectorofthenewly-createdchildinjector.
Finally,weusethecreateComponentmethodoftheViewContainerRefobjecttoinstantiatetheplugincomponent.AsasecondparametertothecreateComponentmethodcall,weneedtopassthepositionintheviewcontainer.Here,wemakeuseofthelengthpropertyofourviewcontainerinordertoplaceitattheveryend.Inthethirdparameter,weoverridethedefaultinjectorofthecomponentwithourcustomchildinjector.Onsuccess,weaddthecreatedComponentRefobjecttoourlistofinstantiatedcomponents.
www.EBooksWorld.ir
FinalizingourpluginarchitectureCongratulations,you'vejustbuiltyourownpluginarchitectureusingAngular!WecreatedapluginAPIthatcanbeusedtocreatenewpluginsusingthePluginConfigdecorator.PluginServicemanagesthewholepluginloadingandprovidesthePluginDataobjectstotheslotsinourapplicationusingcustominjectors.ThePluginSlotdirectivecanbeusedinthetaskmanagementapplicationtomarkextensionpointsintheuserinterface.UsingtheinheritingnatureofthedependencyinjectioninAngular,plugincomponentswillbeabletoaccesswhatevertheyrequirefromtheirenvironment.
Inthenextsection,wewillcreateourfirstpluginusingthepluginarchitecturethatwejustcreated.
www.EBooksWorld.ir
BuildinganAgilepluginIntheprevioussection,wecreatedasimplebuteffectivepluginarchitecture,andwewillnowusethispluginAPItobuildourfirstplugininthetaskmanagementapplication.
Beforewegetintotheplugindetails,weshouldfirstagreeonwheretomakeourapplicationextensible.OurpluginsystemisbasedonthePluginSlotdirectives,whichshouldbeplacedsomewhereinourcomponenttreesothatpluginscanexposecomponentstotheseslots.Fornow,wedecidetomaketwospotsinourapplicationextensible:
TaskInfo:Inthelistoftasksdisplayedinaproject,wecurrentlyrenderTaskcomponents.Besidesthetitleofthetask,theTaskcomponentdisplaysadditionalinformationsuchasthetasknumber,thedateofcreation,andmilestones,aswellaseffortsinformationwhereapplicable.ThisadditionalinformationisrenderedontheTaskcomponentusingtheTaskInfossubcomponent.Thisisagoodspottoprovideextensibilityforpluginssothattheycanaddadditionaltaskinformation,whichwillbedisplayedonthetasklistoverview.TaskDetail:AnothergreatspottoprovideextensibilityistheProjectTaskDetailscomponent.Thisiswherewecaneditthedetailsofatask,whichmakesitagreatcomponenttoopenupforextensionbyplugins.
BesidesaddingthePluginSlotdirectivetothedirectiveslistoftheTaskInfoscomponent,wemodifythetemplatelocatedatlib/task-list/task/task-infos/task-infos.html:
...
<ngc-task-infotitle="Efforts"
[info]="task.efforts|formatEfforts">
</ngc-task-info>
<ngc-plugin-slotname="task-info"></ngc-plugin-slot>
AfterincludingthePluginSlotdirectiveandbysettingthenameinputpropertytotask-info,weprovideanextensionpointforpluginswheretheycanprovideadditionalcomponents.
Let'sapplythesamechangestotheProjectTaskDetailscomponenttemplateinlib/project/project-task-details/project-task-details.html:
...
<divclass="task-details__content">
...
<ngc-plugin-slotname="task-detail"></ngc-plugin-slot>
</div>
Rightbeforetheendofthetaskdetailscontentelement,weincludeanotherpluginslotwiththenametask-detail.Byprovidingcomponentsforthisslot,pluginscanhookintotheeditviewoftasks.
Okay,soourextensionpointsaresetupforpluginstoprovideadditionalcomponentsona
www.EBooksWorld.ir
tasklevel.YoucanseethatpreparingthesespotsusingthePluginSlotdirectiveisreallyapieceofcake.
Now,wecanlookintotheimplementationofourAgileplugin,whichwillmakeuseoftheextensionpointsthatwejustexposed.
Theagilepluginthatwewillcreatewillprovidefunctionalitytologstorypointsontasks.StorypointsarecommonlyusedinAgileprojectmanagement.Theyshouldprovideasenseforcomplexity,andtheyarerelativetoaso-calledreferencestory.IfyouwanttoknowmoreaboutAgileprojectmanagementandhowtoestimateusingstorypoints,Ireallyrecommendthebook,AgileEstimatingandPlanningbyMikeCohn.
Let'sstartwithourpluginclassandthenecessaryconfiguration.Wecreatethepluginoutsideourregularlibfolder,justtoindicatetheportablenatureofplugins.
WecreateanewAgilePluginclassontheplugins/agile/agile.jspath:
import{PluginConfig,PluginPlacement}from'../../lib/plugin/plugin';
@PluginConfig({
name:'agile',
description:'Agiledevelopmentplugintomanagestorypointsontasks',
placements:[]
})
exportdefaultclassAgilePlugin{
constructor(){
this.storyPoints=[0.5,1,2,3,5,8,13,21];
}
}
Thepluginclassformsthecentralentrypointofourplugin.WeusethePluginConfigdecorator,whichwecreatedaspartofourpluginAPI.Besidesthenameanddescription,wealsoneedtoconfigureanyplacementswherewemapplugincomponentstoapplicationpluginslots.However,aswehaven'tgotanyplugincomponentyettoexpose,ourlistremainsemptyforthemoment.
It'salsoimportanttonotethatapluginmodulealwaysneedstodefaultexportthepluginclass.Thisisjusthowwe'veimplementedtheplugin-loadingmechanismsinourPluginServiceclass.
LookingbackatthesetwolinesintheloadPluginmethodofPluginServiceshowsyouthatwerelyonthedefaultexportofpluginmodules:
returnSystem.import(url).then((pluginModule)=>{
constPlugin=pluginModule.default;
...
Whenthepluginmoduleissuccessfullyloaded,weobtainthedefaultexportbyreferencingthedefaultpropertyonthemodule.
www.EBooksWorld.ir
Sofar,wecreatedourpluginentrymodule.Thisactsasapluginconfigurationcontainer,anditisnotrelatedtoAngularinanyway.Usingtheplacementsconfiguration,wecanthenexposeourpluginAngularcomponentsoncewe'vecreatedthem.
www.EBooksWorld.ir
AgiletaskinfocomponentLet'smoveontothefirstAgileplugincomponentthatwewanttoexpose.First,wecreatethecomponent,whichwillbeexposedintotheslotwiththenametask-info.Belowthetasktitleonthetasklist,ourAgileinformationcomponentshoulddisplaythestoredstorypoints.
WecreateanewComponentclassontheplugins/agile/agile-task-info/agile-task-info.jspath:
...
import{Task}from'../../../lib/task-list/task/task';
@Component({
selector:'ngc-agile-task-info',
encapsulation:ViewEncapsulation.None,
template,
host:{
class:'task-infos__info'
}
})
exportclassAgileTaskInfo{
constructor(@Inject(Task)taskComponent){
this.task=taskComponent.task;
}
}
Youcanseethatweimplementedaregularcomponenthere.There'snothingspecialaboutthiscomponentatall.
WeimporttheTaskcomponenttogetthetypeinformationtoinjectitinourconstructor.AsthepluginslotisplacedinsideoftheTaskInfoscomponent,which,infact,isalwaysachildofaTaskcomponent,thisisasafeinjection.
Intheconstructor,wethentaketheinjectedTaskcomponentandextractthetaskdataintoalocaltaskmember.
Wealsoborrowthetask-infos__infoclassoftheTaskInfoscomponentinordertogetthesamelooklikeothertaskinformationthatisalreadypresentonthetask.
Let'stakealookatthetemplateoftheAgileTaskInfocomponentthatislocatedinthesamepathintheagile-task-info.htmlfile:
<div*ngIf="task.storyPoints||task.storyPoints===0">
<strong>StoryPoints:</strong>{{task.storyPoints}}
</div>
Followingthesamemark-upthatweusedintheTaskInfocomponent,wedisplaythestoryPointstasksifpresent.
Alright,nowwecanexposetheplugincomponentinthepluginconfigurationusinga
www.EBooksWorld.ir
PluginPlacementobject.Let'smakethenecessarymodificationtoouragile.jsmodulefile:
...
import{AgileTaskInfo}from'./agile-task-info/agile-task-info';
@PluginConfig({
name:'agile',
description:'Agiledevelopmentplugintomanagestorypointsontasks',
placements:[
newPluginPlacement({slot:'task-info',priority:1,
component:AgileTaskInfo})
]
})
exportdefaultclassAgilePlugin{
...
}
Now,weincludeanewPluginPlacementobjectinourpluginconfiguration,whichmapsourAgileTaskInfocomponenttobeexposedintotheapplicationpluginslotwiththenametask-info:
TaskinfodisplayingadditionalinformationthatisprovidedbyourAgileplugin
Thiswouldalreadybeenoughfortheplugintowork.However,aswedon'thaveanydatafilledasstoryPointsonourtasks,thispluginwouldn'treallyshowusanythingatthemoment.
www.EBooksWorld.ir
AgiletaskdetailscomponentLet'screateanotherplugincomponent,whichcanbeusedtoenterstorypoints.Forthis,wewillcreateanewAgileTaskDetailcomponentontheplugins/agile/agile-task-detail/agile-task-detail.jspath:
...
import{Project}from'../../../lib/project/project';
import{ProjectTaskDetails}from'../../../lib/project/project-task-
details/project-task-details';
import{Editor}from'../../../lib/ui/editor/editor';
@Component({
selector:'ngc-agile-task-detail',
encapsulation:ViewEncapsulation.None,
template,
host:{class:'agile-task-detail'},
directives:[Editor]
})
exportclassAgileTaskDetail{
constructor(@Inject(Project)project,
@Inject(ProjectTaskDetails)projectTaskDetails){
this.project=project;
this.projectTaskDetails=projectTaskDetails;
this.plugin=placementData.plugin.instance;
}
onStoryPointsSaved(storyPoints){
this.projectTaskDetails.task.storyPoints=+storyPoints||0;
this.project.document.persist();
}
}
There'snothingreallyfancywiththiscomponenteither.Ourtargetslotisthetask-detailpluginslot,whichisplacedinsidetheProjectTaskDetailscomponent.Therefore,it'ssafeforboththeProjectTaskDetailsandProjectcomponentstobeinjectedintoourplugincomponent.TheProjectTaskDetailscomponentisusedtoobtainthetaskdataincontext.WeuseLiveDocumentthatisstoredontheProjectcomponenttopersistanychangesthatwemaketothetaskdataoftheproject.
WereuseanEditorcomponenttoobtainuserinputandstoretheinputdataintheonStoryPointsSavedcall-back.ThisisthesamemechanismweknowfromotherareaswhereweusetheEditorcomponent.Whenthestorypointsgetedited,wefirstupdatethetaskdatamodelthatisstoredintheProjectTaskDetailscomponent.Afterthis,wecanusetheLiveDocumentpersistmethodtosavethechanges.
Let'slookatthetemplateofourAgileTaskDetailcomponentintheplugins/agile/agile-task-detail/agile-task-detail.htmlfile:
<divclass="task-details__label">StoryPoints</div>
<ngc-editor[content]="projectTaskDetails.task?.storyPoints"
www.EBooksWorld.ir
[showControls]="true"
(editSaved)="onStoryPointsSaved($event)"></ngc-editor>
WecreateadirectbindingfromthecontentinputpropertyofoureditortothestoryPointspropertyofthetaskdata.
Whenaneditissaved,wecalltheonStoryPointsSavedcallbackwiththeupdatedvalue:
TaskdetailsdisplayingthenewAgilestorypointsthatareexposedbyourAgileplugin
Beforeweexposeournewly-createdplugincomponentusinganewPluginPlacementobjectonthepluginconfiguration,we'llenhancethecomponentonemoretime.Thiswouldbeniceifweprovidetwobuttonsonthecomponentthatallowstheusertoincreaseordecreasethestorypointstothenextcommonstorypointvalueinrange.AswealreadystoredthelistofcommonstorypointsontheAgilepluginclass,let'sseehowwecanmakeuseofthis:
...
import{PluginData}from'../../../lib/plugin/plugin';
@Component({
selector:'ngc-agile-task-detail',
...
})
exportclassAgileTaskDetail{
constructor(...,@Inject(PluginData)pluginData){
...
this.plugin=pluginData.plugin.instance;
}
...
increaseStoryPoints(){
constcurrent=this.projectTaskDetails.task.storyPoints||0;
conststoryPoints=this.plugin.storyPoints.slice().sort((a,b)=>a>b?1
:a<b?-1:0);
www.EBooksWorld.ir
this.projectTaskDetails.task.storyPoints=
storyPoints.find((storyPoints)=>storyPoints>current)||current;
this.project.document.persist();
}
decreaseStoryPoints(){
constcurrent=this.projectTaskDetails.task.storyPoints||0;
conststoryPoints=this.plugin.storyPoints.slice().sort((a,b)=>a<b?1
:a>b?-1:0);
this.projectTaskDetails.task.storyPoints=
storyPoints.find((storyPoints)=>storyPoints<current)||current;
this.project.document.persist();
}
}
WhilewepreviouslyinjectedtheProjectandProjectTaskDetailscomponentsthatareprovidedbythecomponentlevelinjectors,wenowmakeuseoftheprovidersthatweaddedduringinstantiationinourPluginSlotdirective.Here,weprovidedPluginData,whichwecannowusetogetareferencebacktotheplugincomponent.
ThenexthigherorlowerstorypointvalueisfoundbyincreaseStoryPointsanddecreaseStoryPoints.ThisisdonebysearchingthelistofcommonstorypointsthatarestoredonourAgilePluginclass.UsingthepluginclassinstancethatispresentontheinjectedPluginData,wecaneasilyaccessthislist.Afterstoringthemodifiedstorypoints,wethenusetheLiveDocumentinstanceoftheprojectcomponenttopersisttheadjustedstorypoints.
InthetemplateofourAgileTaskDetailcomponent,wesimplyaddtwobuttonsthatallowtheusertoincreaseordecreasethestorypointsthatarebasedonournewly-createdmethods:
...
<button(click)="decreaseStoryPoints()"
class="buttonbutton--small">-</button>
<button(click)="increaseStoryPoints()"
class="buttonbutton--small">+</button>
Okay,let'snowaddtheAgileTaskDetailcomponenttothepluginconfigurationusinganewPluginPlacementobject,whichreferencesthetask-detailpluginslot:
...
import{AgileTaskDetail}from'./agile-task-detail/agile-task-detail';
@PluginConfig({
...
placements:[
newPluginPlacement({slot:'task-info',priority:1,
component:AgileTaskInfo}),
newPluginPlacement({slot:'task-detail',priority:1,
component:AgileTaskDetail})
]
})
exportdefaultclassAgilePlugin{
...
}
www.EBooksWorld.ir
Isn'tthisgreat?Youcreatedafully-portablepluginthatenablesthemanagementofAgilestorypointsontasks.
Thetaskdetailsviewwithstorypointsandadditionalincrease/decreasebuttons
TheonlythingthatisleftistoaddtheplugintothelistofpluginsthatshouldbeloadedinitiallybythePluginServicedirective.Forthis,we'llcreateaplugins.jsfileontherootofourapplicationandaddthefollowingcontent:
exportdefault[
'/plugins/agile/agile.js'
];
Now,ifwelaunchourapplication,thepluginwillbeloadedbythePluginServiceandPluginSlotdirectiveswillinstantiatetheAgileplugincomponentswhereappropriate.
www.EBooksWorld.ir
RecapitulatingonourfirstpluginWelldone!Yousuccessfullyimplementedyourfirstplugin!Inthissection,weusedtheAPIofourpluginarchitecturetocreateaplugintomanageAgilestorypoints.WeusedthePluginPlacementclasstomapourplugincomponentstodifferentslotsintheUIofourapplication.WealsomadeuseofthePluginDataobjectthatisprovidedtoeachinstantiatedcomponentinthepluginslotinordertoaccesstheplugininstance.
Theadvantageofimplementingfunctionalitylikethisinsideapluginshouldbeobvious.Weaddedanadditionalfeaturetoourapplicationwithoutbuildingupadditionaldependencies.OurAgilefeatureiscompletelyportable.Third-partydeveloperscanwriteindependentplugins,andtheycanbeloadedbyoursystem.Thisisabigadvantage,andithelpsuskeepourcoreslimwhileprovidinggreatextensibility.
www.EBooksWorld.ir
ManagingpluginsWealreadybuiltthecoreofthepluginarchitectureandafirstpluginthatrunsinthissystem.Wecanusetheplugins.jsfileontherootofourapplicationtoregisterplugins.Thesystemisactuallyfullyfunctionalalready.However,itwouldbenicetoprovideawaytomanageourpluginsduringruntime.
Inthissection,wewillbuildanewroutablecomponent,whichwilllistallactivepluginsinthesystem.Afterwe'vedonethis,we'llalsoaddsomeelementswhichallowuserstounloadactivepluginsaswellasloadnewpluginsduringruntime.Duetothereactivenatureofourpluginsystem,thebrowserdoesnotneedtoberefreshedinorderfornewly-loadedpluginstobecomeactive.Themomentapluginisloaded,itwillimmediatelybemadeavailabletorelevantpluginslots.
Let'sstartwithanewManagePluginscomponentclassonthelib/manage-plugins/manage-plugins.jspath:
...
import{PluginService}from'../plugin/plugin-service';
@Component({
selector:'ngc-manage-plugins',
...
})
exportclassManagePlugins{
constructor(@Inject(PluginService)pluginService){
this.plugins=pluginService.change;
}
}
OurManagePluginscomponentisquitesimple.WeinjectthePluginServiceintotheconstructorofthecomponentandsetthememberfieldpluginstopointtothechangeobservableofPluginService.Aswe'llalwaysgetthelatestpluginlistemittedbythisobservable,wecanthensimplyusetheasyncpipeintheviewtosubscribetotheobservable.
Let'slookatthetemplateofournewcomponentinlib/manage-plugins/manage-plugins.html:
<divclass="manage-plugins__l-header">
<h2class="manage-plugins__title">ManagePlugins</h2>
</div>
<divclass="manage-plugins__l-main">
<h3class="manage-plugins__sub-title">ActivePlugins</h3>
<divclass="manage-plugins__section">
<tableclass="manage-plugins__table">
<thead>
<tr>
<th>Name</th>
<th>Url</th>
<th>Description</th>
www.EBooksWorld.ir
<th>Placements</th>
</tr>
</thead>
<tbody>
<tr*ngFor="letpluginofplugins|async">
<td>{{plugin.config.name}}</td>
<td>{{plugin.url}}</td>
<td>{{plugin.config.description}}</td>
<td>
<div*ngFor="letplacementofplugin.config.placements"
class="manage-plugins__placement">
{{placement.slot}}
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
WeuseanHTMLtabletodisplaythelistofactiveplugins.Onthetablebodyrows,weusetheNgFordirectivetoiterateoverthelistofactiveplugins,whichwesubscribetousingtheasyncpipe.
Inthepluginobjects,wegoteverythingworthdisplayingalreadypresent.ByiteratingoverthePluginPlacementobjectsthatarestoredontheconfigpropertyontheplugindata,wecanevendisplaytheslotnameswhereourpluginsprovidecomponents.
Now,theonlythinglefttodotoenableournewcomponentistomakeitroutableandtoaddittothenavigationofourapplication.Let'smakethenecessarymodificationinthelib/app.jsmoduleforthis:
...
import{ManagePlugins}from'./manage-plugins/manage-plugins';
@Component({
selector:'ngc-app',
...
})
@Routes([
...
newRoute({path:'plugins',component:ManagePlugins})
])
exportclassApp{
...
}
Weaddedanewroute,solet'saddittoournavigationinlib/app.html:
<divclass="app">
<divclass="app__l-side">
<ngc-navigation[openTasksCount]="openTaskCount">
...
www.EBooksWorld.ir
<ngc-navigation-sectiontitle="Admin">
<ngc-navigation-itemtitle="ManagePlugins"
[link]="['/plugins']">
</ngc-navigation-item>
</ngc-navigation-section>
</ngc-navigation>
</div>
<divclass="app__l-main">
<router-outlet></router-outlet>
</div>
</div>
InanewAdminnavigationsection,weaddanewnavigation-itemthatlinkstothenewly-created"plugins"route:
OurnewManagePluginscomponentdisplayingatableofactivepluginsandtheirexposedplacements
www.EBooksWorld.ir
LoadingnewpluginsatruntimeWealreadyhaveeverythinginplacetoprovideapagetoseeallactiveplugins.However,wesaidthatitwouldbenicetobeabletomanagethislist.Ausershouldbeabletoremoveactivepluginsaswellasmanuallyloadadditionalplugins.
Let'saddthesecapabilitiestoourManagePluginscomponent.Beforewecandothis,we'llneedanadditionalmethodonourPluginServiceclass,whichisthepartresponsiblefortheloadingofplugins.Sofar,wedidn'tconsiderthefunctionalitytoremoveactiveplugins.Let'sopenPluginServiceinlib/plugin/plugin-service.jstoaddthisfunctionality:
...
@Injectable()
exportclassPluginService{
...
removePlugin(name){
constplugin=this.plugins.find(
(plugin)=>plugin.name===name);
if(plugin){
constplugins=this.plugins.slice();
plugins.splice(plugins.indexOf(plugin),1);
this.plugins=plugins;
this.change.next(this.plugins);
}
}
}
Well,thiswaseasy!WeprovideanewremovePluginmethod,whichtakesapluginnameasparameter.Wethenlookuptheplugininthepluginsarray,andifapluginwasfoundwiththisname,weremoveitfromthelist.Additionally,afterwe'veremovedtheplugin,weemitachangeeventwiththeupdatedlist.Asallpluginslotsintheapplicationaresubscribedtothischangeobservable,theywillupdateandreinitializerelevantplugincomponentsautomatically.
Let'snowapplythenecessarychangestoourManagePluginscomponentclassinordertonotonlyremovepluginsbutalsotoloadadditionalplugins:
...
@Component({
selector:'ngc-manage-plugins',
...
})
exportclassManagePlugins{
constructor(@Inject(PluginService)pluginService){
...
this.pluginService=pluginService;
}
removePlugin(name){
this.pluginService.removePlugin(name);
}
www.EBooksWorld.ir
loadPlugin(loadUrlInput){
this.pluginService.loadPlugin(loadUrlInput.value);
loadUrlInput.value='';
}
}
Now,wealsostorethePluginServiceonourcomponent.IntheremovePluginandtheloadPluginfunctions,wedelegatetothePluginServicetotakethenecessaryactions.
TheloadPluginmethodwillreceiveanElementRefobjectthatpointstotheinputfield,wheretheuserenterstheURLfromwhichweloadanewplugin.WecanpassthevalueoftheinputfieldtotheloadPluginmethodofPluginService,whichdealswiththerest.Wealsosettheinputfieldvaluetoanemptystringoncewe'vesubmittedthiscall.
Let'sopenthetemplateatlib/manage-plugins/manage-plugins.htmltoapplytherequiredchangesintheviewofourcomponent:
...
<divclass="manage-plugins__l-main">
<h3class="manage-plugins__sub-title">ActivePlugins</h3>
<divclass="manage-plugins__section">
...
<td>
<button(click)="removePlugin(plugin.name)"
class="buttonbutton--small">remove</button>
</td>
...
</div>
<h3class="manage-plugins__sub-title">LoadPlugin</h3>
<divclass="manage-plugins__section">
<divclass="manage-plugins__load-elements">
<input#loadUrlReftype="text"
placeholder="EnterpluginURL"
class="manage-plugins__load-url">
<button(click)="loadPlugin(loadUrlRef)"
class="button">Load</button>
</div>
</div>
</div>
Weaddedanadditionalbuttonforeverylistedplugininthetable,whichcontainsabindingexpressionthatcallstheremovePluginmethodwiththecurrentlyiteratedpluginname.
Wealsoaddedanewsectionafterthelistedpluginstoloadnewplugins.Inthissection,weuseaninputfieldtoenterthepluginURLaswellasabuttontoexecutetheloading.UsingaloadUrlReflocalviewreference,wecanpassareferencetotheinputDOMelementtotheloadPluginmethodonourcomponent:
www.EBooksWorld.ir
AcompletedManagePluginscomponentwiththeabilitytoremoveandloadpluginmodulesatruntime
Now,wehaveeverythinginplacetomanageourplugins.PluginsinitiallyloadedfromURLspresentintherootplugins.jsfilecannowbeunloadedusingtheREMOVEbuttoninthepluginslisting.NewpluginscanbeloadedandactivatedbyenteringtheURLofaplugin,whichcouldbealocalURL,bundledandmappedmodule,orevenaremoteURLonadifferentserver.
www.EBooksWorld.ir
SummaryInthischapter,welookedatdifferentapproachesonhowtoimplementapluginarchitecture.WethencreatedourowndesignforapluginarchitecturethatleveragessomeAngularmechanismsandworksontheconceptofUIextensionpointsthatwecallslots.
WeimplementedapluginAPIthatprovidesgreatdeveloperexperiencebyleveragingES7decoratorstomaketheconfigurationofnewpluginsapieceofcake.WeimplementedthecoreofourpluginsystemusingaservicetoloadandunloadpluginsthatarebasedontheSystemJSmoduleloader.ThisallowedustomakeuseoftheadvancedloadingpossibilitiesthatareprovidedbySystemJS.Pluginscanbetranspiledinrealtime,canbelocatedonalocalURL,remoteURL,orevenbebundledintothemainapplication.
Weimplementedourfirstplugin,whichprovidessomecomponentstomanageAgilestorypointsonourtasks.Thepluginwascreatedoutsideourregularprojectlibfolder,whichshouldunderlinetheportablenatureofourpluginsystem.
Finally,wecreatedanewroutablecomponenttomanagepluginsatruntime.Duetothereactivenatureofourpluginsystem,pluginscanbeloadedandunloadedduringapplicationruntimewithoutanyunwantedside-effects.
Whenyou'replayingwiththesourcecodeofthischapter,Ihighlyrecommendthatyouplaywiththeloadingmechanismofourpluginarchitecture.Theflexibilitythatweachievedwithverylittleeffortisfantastic.YoucanunloadtheAgilepluginandloaditagainbyprovidingtheURLtothepluginmainmodule.Youcaneventrytoplacethewholepluginsfolderontoaremoteserverandloadthepluginfromthere.JustmakesurethatyouconsiderthenecessaryCross-OriginResourceSharing(CORS)headers.
ThewholecodeforthischaptercanbefoundintheZIPfileofthebookresourcesthatyoucandownloadfromPacktPublishing.YoucanrefertotheDownloadingtheexamplecodesectioninthePrefaceofthebook.
Inthenextandlastchapterofthisbook,we'lllookathowwecantestthecomponentsthatwe'vecreatedsofar.So,staytunedforthisoverduetopic!
www.EBooksWorld.ir
Chapter11.PuttingThingstotheTestWritingtestsiscrucialforthemaintainabilityofyourcode.It'saknownfactthathavingagoodrangeofteststhatcovermostofyourfunctionalityisequallyimportantasthefunctionalityitself.
Thefirstthingthatcomestomindwhenthinkingabouttestsisprobablycodequalityassurance.Youtestthecodethatyouwrite,sothisisdefinitelytrue.However,therearemanyotherimportantaspectsofwritingtests:
Resistancetounexpectedchange:Yourtestsdefinewhatyourcodeissupposedtodo.Theytestwhetheryourcodeconformstoyourspecifications.Thishasseveralbenefits,wherethemostobviousisprobablyaresistancetounexpectedchangeinthefuture.Ifyoumodifythecodeinthefuture,you'lllesslikelybreakyourexistingcodebecauseyourtestswillvalidatewhethertheexistingfunctionalitystillworksasspecified.Documentation:Yourtestsdefinewhatyourcodeshoulddo.Atthesametime,theydisplaytheAPIcallsthatarerequiredtousetheconcernedfunctionality.Thisistheperfectdocumentationforanydeveloper.WheneverIwanttounderstandhowalibraryreallyworks,thetestsarethefirstthingthatIlookat.Avoidingunnecessarycode:Thepracticeofwritingtestsforcesyoutolimityourcodetofulfiltherequirementsofyourspecificationandnothingmore.Anycodeinyourapplicationthatisnotreachedinyourautomatedtestscanbeconsidereddeadcode.Ifyousticktoamercilessrefactoringapproach,you'dthenremovesuchunusedcodeASAP.
Sofar,wehaven'tconsideredtestinginourbookatall,andgivenitsimportance,youmaywonderwhyIcomeupwiththisnowinthelastchapter.Inarealproject,we'ddefinitelycreatetestsmuchearlierifnotatfirst.However,Ihopeyouunderstandthatinthisbook,wepostponedthisratherimportanttopicuntiltheend.Ireallylovetesting,butaswe'remainlyfocusedonthecomponentarchitectureofAngular,placingthischapterattheendseemedmorelogical.
Inthischapter,we'lllookintohowtoperformproperunittestingonyourcomponents.We'llfocusonunittesting;automatedend-to-endtestingisbeyondthescopeofthisbook.Still,we'lllookintohowtotestuserinteractiononcomponents,althoughnotonthelevelitwouldbedoneinend-to-endtesting.
Inthischapter,wewilldelveintothefollowingtopics:
AnintroductiontotheJasminetestingframeworkWritingsimpleJavaScripttestsforcomponentsCreatingatests.htmlfile,whichservesasanin-browsertestrunnerCreatingJasminespiesandobservingcomponentoutputpropertiesLearningaboutAngulartestingutilities,suchasinject,async,TestComponentBuilder,DebugElement,andmore
www.EBooksWorld.ir
MockingcomponentsMockingexistingservicesCreatingtestsforourAutoCompleteUIcomponentCreatingtestsforourpluginsystem
www.EBooksWorld.ir
AnintroductiontoJasmineJasmineisaverysimpletestingframework,whichcomeswithanAPIthatallowsyoutowriteBehavior-drivenDevelopment(BDD)styletests.BDDisanagilesoftwaredevelopmentprocessofdefiningspecificationsinawrittenformat.
InBDD,wedefinethatanagileuserstoryconsistsofmultiplescenarios.Thesescenarioscloselyrelatetoorevenreplacetheacceptancecriteriaofastory.Theydefinerequirementsonahigherlevel,andtheyaremostlywrittennarrative.Eachscenariothenconsistsofthreeparts:
Given:Thispartisusedtodescribetheinitialstateofthescenario.Thetestcodeiswhereweperformallthesetupthatisneededtoexecutethetestscenario.When:Thispartreflectsthechangesthatweperformtothesystemundertest.Usually,thispartconsistsofsomeAPIcallsandactionsthatreflectthebehaviorofauserofthesystem.Then:Thispartspecifieswhatthesystemshouldlooklikeafterthegivenstateandthechangesappliedinthewhenpart.Inourcode,thisisthepartthatisusuallyattheendofourtestsfunction,whereweuseassertionlibrariestoverifythestateofthesystem.JasminecomeswithanAPIthatmakesitveryeasytowritetestswhichstructureaccordingtotheBDDstyle.Let'slookataverysimpleexampleofhowweuseJasminetowriteatestforashoppingcartsystem:
describe('Buyingitemsintheshop',()=>{
it('shouldincreasethebasketcount',()=>{
//Given
constshop=newShop();
//When
shop.buy('Toothpaste');
shop.buy('Shampoo');
//Then
expect(shop.basket.length).toBe(2);
expect(shop.basket).toContain('Toothpaste');
expect(shop.basket).toContain('Shampoo');
});
});
Jasmineprovidesuswithadescribefunction,whichallowsustogroupcertainscenariosonthesamesubject.Inthisexample,weusedthedescribefunctiontoregisteranewtestsuitefortestsaboutbuyingitemsinashop.
Usingtheitfunction,wecanregisterindividualscenarios,whichwe'dliketogettested.Inthedescribecallbackfunction,wecanregisterasmanyscenariosusingtheitfunctionaswelike.
InsidethecallbackfunctionoftheJasmineitfunction,wecanstartwritingourtest.WeuseaBDDstyletostructurethecodeinsideourtest.
www.EBooksWorld.ir
Youdon'tnecessarilyneedtorunJasmineinthebrowser,butifyoudothis,you'llgetanicesummaryreportofalltestsandtheirstate:
Jasmineprovidesanicevisualreportoverallyourtestspecifications,whichalsoallowsyoutorerunindividualtestsandprovidesyouwithmoreoptions
Jasminecomesinthreepartsthatarerelevanttous:
Jasminecore:ThiscontainsthetestdefinitionAPIs,theassertionlibrary,andalltheothercorepartsofthetestingframeworkJasmineHTML:ThisistheHTMLreporter,whichwillwritealltestsresultstothebrowserdocumentandevenprovideoptionstorerunindividualtestsJasmineboot:ThisisthefilethatbootstrapstheJasmineframeworkforthebrowserandperformsanysetupthatisneededwiththeHTMLreporter
Inourproject,wewilluseJasmineandtheprecedingpartsdirectlyfromaCDN,sowedon'tneedtoinstallanythingtogetstarted.Wecreateanewtests.htmlfile,whichwillserveasarunnerforourtests.Inconjunctionwithlive-server,wecanalwayshavethispageopeninourbrowser.Thiswaywe'llgetimmediatefeedbackonourtestswhiledeveloping.
Tip
JasminealsoplaysnicewithtestrunnerssuchasKarmatorunyourtests.Karmaisapopulartestrunner,whichallowsyoutorunyourtestsinparallelusingtheKarmaCLIorintegrateitinyourbuildpipeline.Thisalsoallowsyoutoruntestsindifferentbrowsers.Inthischapter,wewillusetheJasmineHTMLandJasmineboottorunourtestsdirectlyinthebrowser.Thisallowsustoskiptherathercomplexsetupthatwe'dneedtoundertakeifweusedKarmaasourtestrunner.
Let'slookatthecodeofthetests.htmlfilethatwecreateintherootfolderofourapplication,rightnexttotheindex.htmlfile,whichisalreadypresent:
...
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.0/es6-
www.EBooksWorld.ir
shim.min.js"></script>
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-
beta.17/angular2-polyfills.js"></script>
<scriptsrc="jspm_packages/system.js"></script>
<scriptsrc="config.js"></script>
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js">
</script>
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-
html.js"></script>
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js">
</script>
BesidesloadingtheusualsuspectsforourAngularapplication(ES6shim,Angularpolyfills,andSystemJS),wenowalsoloadthethreemaincomponentsofJasmine.
Bydefault,Jasmineexecutesallregisteredtestsonthewindow'sloadevent.However,aswewillloadourtestsusingSystemJS,weneedtodeferthebootstrapofJasmineuntilSystemJShascompletelyloadedourtests:
<script>
window._jasmineOnLoad=window.onload;
window.onload=null;
returnSystem.import('./all.spec')
.then(window._jasmineOnLoad)
.catch(console.error.bind(console));
...
</script>
WefirstputasidethefunctionthatwasregisteredbyJasminebootonwindow.onload.Westorethefunctioninatemporary_jasmineOnLoadglobalvariable.
Now,weuseSystemJStoimportourentrypointmoduleforourtests,whichwillbestoredintheall.spec.jsfile.SystemJSreturnsaPromisethatwillberesolvedifthetestmodulehasbeenloadedandexecutedsuccessfully.WecanusethethenfunctionofthereturnedPromisetoexecutetheJasminebootfunctionstoredinwindow._jasmineOnLoad.Inthisway,wemakesurethatJasmineisbootedafterallourtestshavebeenregistered.
www.EBooksWorld.ir
WritingourfirsttestNowthatweareallsetwiththeJasminesetup,wecanstartwritingourfirsttest.Inthissection,wewillcreateafirsttestfortheAutoCompletecomponentthatwecreatedinChapter8,TimeWillTell,ofthisbook.
AsAngularcomponentsarejustclasses,wecanalreadytestalotofthefunctionalitybyinstantiatingtheComponentclassandtestingitsmethods.Teststhatcanbeperformedlikethisshouldalwaysbeconsideredfirst.ThesetestscanrunwithoutAngularbootstrappingthecomponent.
TheAutoCompletecomponentfiltersdisplayedresultsbasedontheavailableitemsandafiltercriteria.Inthefollowingtest,we'llverifythatthefiltermethodonthecomponentworksasexpected.
Tip
Inthisbook,wefollowthepracticetostoretestfilesbyappendinga.spec.jsfiletothenameofthefilethathastobetested.We'llalsostorethesetestfilesinthesamefolderofthesubject.Thismakesitmucheasiertokeepthecontext.
We'llcreateanewauto-complete.spec.jsfileinthefolderoftheAutoCompletecomponentatlib/ui/auto-complete:
import{describe,expect,it,}from'@angular/core/testing';
import{AutoComplete}from'./auto-complete';
describe('AutoComplete',()=>{
it('shouldfilteritemscorrectly',()=>{
//Given
constautoComplete=newAutoComplete();
autoComplete.items=['One','two','three'];
//When
autoComplete.filterItems('o');
//Then
expect(autoComplete.filteredItems).toEqual(['One','two']);
});
});
AsweloadedJasminepriortoexecutingourtest,wecouldrelyontheglobaldescribe,it,andexpectfunctionsthatareexposedbyJasmine.However,AngularprovidesuswithsomenicewrappersoftheJasminefunctions,whichwecanimportfromthemodulelocatedin@angular/core/testing.
Asyoucansee,wedon'treallyneedtoactivatetheAutoCompletecomponentinordertotestsomeofitsfunctionality.Bysimplytestingthecomponentclass,wecanalreadyexecutesomeofourexecutablespecifications.
www.EBooksWorld.ir
WefollowaBDDapproachtostructureourtest,andintheGivensection,weinstantiateanewAutoCompletecomponentclass,andthenweinitializetheitemslistwithsometestitems.Eveniftheitemsfieldisactuallyacomponentinput,wecansimplydisregardthisfactinordertotestthefilteringfunctionality.
IntheWhensectionofourtest,weactuallycallthefilterItemsmethodofthecomponentclassandtestwhetheritdoesfiltertheitemsaccordingtothespecification.
IntheThensection,weusetheexpectfunctionofJasmineinordertoasserttheexpectedstateaftertheWhensection.Asthecomponentshouldfilterallitemswithpartialandcase-insensitivematchesofthefiltercriteria,theexpectedvalueinfilteredItemsshouldbeanarraywiththeOneandtwoitems.
WeusetheassertiontoEqualfunctioninordertoperformadeepequalcheck.IfweusethetoBematcher,we'dcomparethereferencesofthetwoarrays,whichwillresultinanegativematch.
Thisisitforourfirsttest.What'slefttodostillistocreateourmaintestmodulethatisloadedinthetests.htmlfile.
Wecreatedthemainentrypointforallourtestsinaall.spec.jsfileontherootpathofourapplication.Thisfilewillthenincludeallspecificationfilesthatwecreateinourapplication:
import'./lib/ui/auto-complete/auto-complete.spec';
Thisiscurrentlyallthatweneedtomakeourtestrun.Wesimplyimportthetestfilethatwejustcreated.Now,tests.htmlwilluseSystemJStoloadourall.spec.jsfile,andhere,wethenloadtheauto-complete-spec.jsfile.
Wecannowstartlive-serverintherootpathofourapplicationandnavigatetohttp://127.0.0.1:8080/tests.htmlinourbrowser.Aslive-serverwillreloadourbrowseronchanges,wecanstartaddingnewtestswhileweconstantlygetupdatesonourteststateinthebrowser.
www.EBooksWorld.ir
SpyingoncomponentoutputsAcommonpracticeintestingistousespyfunctioncallsduringtheexecutionoftestsandthenevaluatethesecalls,checkingwhetherallfunctionshavebeencalledcorrectly.
Jasmineprovidesuswithsomenicehelpersinordertousespyfunctioncalls.WecanusethespyOnfunctionofJasmineinordertoreplacetheoriginalfunctionwithaspyfunction.Thespyfunctionwillrecordanycalls,andwecanlateronevaluatehowmanytimesitwascalledandwithwhatparameters.
Let'slookatasimpleexampleofhowtousethespyOnfunction:
classCalculator{
multiply(a,b){
returna*b;
}
pythagorean(a,b){
returnMath.sqrt(this.multiply(a,a)+this.multiply(b,b));
}
}
WewilltestasimpleCalculatorclassthathastwomethods.Themultiplymethodsimplymultipliestwonumbersandreturnstheresult.Thepythagoreanmethodcalculatesthehypotenuseofaright-angledtrianglewithtwosides,aandb.
YoumightremembertheformulaforthePythagoreantheoremfromyourearlyschooldays:
a²+b²=c²
Wewillusethisformulatoproducecfromaandbbygettingthesquarerootoftheresultofa*a+b*b.Forthemultiplications,we'lluseourmultiplymethodinsteadofusingarithmeticoperatorsdirectly.
Now,we'dwanttotestourcalculatorpythagoreanmethod,andasitusesthemultiplymethodtomultiplyaandb,wecanspyonthismethodtoverifyourtestresultindepth:
describe('Calculatorpythagoreanfunction',()=>{
it('shouldcallmultiplyfunctioncorrectly',()=>{
//Given
constcalc=newCalculator();
spyOn(calc,'multiply').and.callThrough();
//When
constresult=calc.pythagorean(6,8);
//Then
expect(result).toBe(10);
expect(calc.mul).toHaveBeenCalled();
expect(calc.mul.calls.count()).toBe(2);
expect(calc.mul.calls.argsFor(0)).toEqual([6,6]);
expect(calc.mul.calls.argsFor(1)).toEqual([8,8]);
www.EBooksWorld.ir
});
});
ThespyOnfunctionofJasminetakesanobjectasfirstparameterandthefunctionnameontheobjectwhichwe'dliketospyon.
ThiswilleffectivelyreplacetheoriginalmultiplyfunctiononourclassinstancewithanewspyfunctionofJasmine.Bydefault,spyfunctionswillonlyrecordfunctioncalls,andtheywon'tdelegatethecallfurthertotheoriginalfunction.Wecanusethe.and.callThrough()functiontospecifythatwe'dlikeJasminetocalltheoriginalfunction.Thiswayourspyfunctionwillactasaproxyandrecordanycallsatthesametime.
IntheThensectionofourtest,wecantheninspectthespyfunction.UsingthetoHaveBeenCalledmatcher,wecancheckwhetherthespyfunctionwascalledafterall.
Usingthecallspropertyofthespyfunction,wecaninspectinmoredetailandverifythecallcountaswellastheargumentsthatindividualcallsreceived.
UsingtheknowledgethatwegainedaboutJasminespies,wecannowapplythattoourcomponenttests.AsweknowthatalloutputpropertiesofcomponentscontainanEventEmitter,wecanactuallyspyonthemtocheckwhetherourcomponentsendsoutput.
Insidecomponents,wecallthenextmethodonEventEmitterinordertosendoutputtoparentcomponentbindings.Asthisisanasynchronousoperationandwe'dalsoliketotestourcomponentswithoutneedingtoinvolveparentcomponents,wecansimplyspyonthenextmethodofouroutputproperties.
InthenexttwotestsforourAutoCompletecomponent,we'dliketoverifythefunctionalitywhenwesaveaneditintheEditorchildcomponent.Let'squicklyrecaponthisbehavior:
Onsavededits,wegettheonEditSavedmethodontheAutoCompletecomponentthatiscalledIfthesavedvalueisanemptystring,theAutoCompletecomponentshouldemitaselectedItemChangeeventwithanullvalueIfthesavedvalueisnoemptystringandthevalueisnotpresentintheitemsoftheAutoCompletecomponent,anitemCreatedeventshouldbeemitted
Let'screatethetestsforthepreviousexpectedbehaviortothealreadyexistinglib/ui/auto-complete/auto-complete.spec.jstestfile:
...
it('shouldemitselectedItemChangeeventwithnullonemptycontentbeing
saved',()=>{
//Given
constautoComplete=newAutoComplete();
autoComplete.items=['one','two','three'];
autoComplete.selectedItem='three';
spyOn(autoComplete.selectedItemChange,'next');
www.EBooksWorld.ir
spyOn(autoComplete.itemCreated,'next');
//When
autoComplete.onEditSaved('');
//Then
expect(autoComplete.selectedItemChange.next).toHaveBeenCalledWith(null);
expect(autoComplete.itemCreated.next).not.toHaveBeenCalled();
});
WecreatetwoJasminespieshere.ThefirstonespiesontheselectedItemChangeoutputproperty,whilethesecondonespiesontheitemCreatedoutputproperty.
Aftersimulation,theeditorwassavedwithanemptystring.WecanstartverifyingourspiesintheThensectionofourtest.
ThenextfunctionoftheselectedItemChangeevent,EventEmitter,shouldhavebeencalledwithanullvalue,whilenextofitemCreatedshouldn'thavebeencalledatall.Wecanusethenotpropertyonthereturnedexpectationobjecttoinvertthematcher.
Let'saddasecondtestforthebehaviorwhenaneditorwassavedwithavaluethatdoesnotyetexistintheAutoCompletecomponent:
it('shouldemitanitemCreatedeventoncontentbeingsavedwhichdoesnot
matchanexistingitem',()=>{
//Given
constautoComplete=newAutoComplete();
autoComplete.items=['one','two','three'];
autoComplete.selectedItem='three';
spyOn(autoComplete.selectedItemChange,'next');
spyOn(autoComplete.itemCreated,'next');
//When
autoComplete.onEditSaved('four');
//Then
expect(autoComplete.selectedItemChange.next).not.toHaveBeenCalled();
expect(autoComplete.itemCreated.next).toHaveBeenCalledWith('four');
});
Thistime,wesimulateasavededitwithavalue,whichisn'tanemptystringanddoesnotexistintheautocompleteitemsalready.
IntheThensectionofourcode,weevaluatethespiesandexpectthattheitemCreated.nextfunctionwascalledwithafourstring.
UsingJasminespies,wemanagedtotestourcomponentoutputsuccessfullywithouttheneedtobootstrapAngular.WeperformedthesetestssolelyonthecomponentclassandbycreatingspiesontheEventEmitterthatispresentonalloutputproperties.
www.EBooksWorld.ir
UtilitiestotestcomponentsSofar,wetestedourcomponentswithplainvanillaJavaScript.Thefactthatcomponentsareinjustregularclassesmakethispossible.However,thiscanonlybedoneforverysimpleuse-cases.Assoonaswe'dliketotestcomponentsforthingsthatinvolvetemplatecompilation,userinteractiononcomponents,changedetection,ordependencyinjection,we'llneedtogetalittlehelpfromAngulartoperformourtests.
Angularcomeswithawholebunchoftestingtoolsthathelpusouthere.Infact,theplatform-agnosticwaythatAngularisbuiltallowsustoexchangetheregularviewadapterwithadebugviewadapter.Thisenablesustorendercomponentsinsuchawaythatallowsustoinspectthemingreatdetail.
ToenablethedebuggingcapabilitiesofAngularwhilerenderingcomponents,weneedtomodifyourmainentrypointforourtestsfirst.
Let'sopenupall.spec.jstomakethenecessarymodifications:
import{setBaseTestProviders}from'@angular/core/testing';
import{TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS}from'@angular/platform-browser-
dynamic/testing';
setBaseTestProviders(TEST_BROWSER_PLATFORM_PROVIDERS,
TEST_BROWSER_APPLICATION_PROVIDERS);
import'./lib/ui/auto-complete/auto-complete.spec';
import'./lib/plugin/plugin.spec';
UsingthesetBaseTestProvidersfunctionofthe@angular/core/testingmodule,wecanactuallyinitializeatestplatforminjector,whichwillthenbeusedinthecontextofourAngulartesting.Thisfunctiontakestwoargumentswherethefirstoneisanarrayofplatformproviders,andthesecondoneisanarrayofapplicationproviders.
Fromthe@angular/platform-browser-dynamic/testingmodule,wecanimporttwoconstantsthatcontainanalreadypreparedlistforbothplatformandapplication-leveldependencies.Herearesomeoftheproviderspresentintheseconstants:
Platform-levelproviders:TheseconsistmostlyofplatforminitializationproviderstodebugApplication-levelproviders:Theseconsistofthefollowing:
DebugDomRootRenderer:ThisoverridesthedefaultDomRendererinthebrowserandenablesdebuggingofelementsusingDebugElementandprobingMockDirectiveResolver:ThisoverridesthedefaultDirectiveResolverandallowsoverridingofdirectivemetadatafortestingpurposesMockViewResolver:ThisoverridesthedefaultViewResolverandallowsoverridingofcomponentviewspecificmetadata
www.EBooksWorld.ir
UsingthesetBaseTestProvidersfunctionandtheimportedconstantswiththedebuggingproviders,wecannowinitializeourtestenvironment.Aftercallingthisfunctionandpassingourproviders,Angularissetupfortesting.
www.EBooksWorld.ir
InjectingintestsInjectingAngulardependenciesintestsismadeeasybytwohelperfunctionsthatwecanuse.Theinjectandasyncfunctionsareavailablethroughthe@angular/core/testingpackage,andtheyhelpusinjectdependenciesinourtests.
Let'slookatthissimpleexamplewhereweinjectthedocumentelementusingtheinjectwrapperfunction.Thistestisirrelevantforourapplication,butitillustrateshowwecannowmakeuseofinjectioninourtests:
import{describe,expect,it,inject}from'@angular/core/testing';
import{DOCUMENT}from'@angular/platform-browser';
describe('Applicationinitializedwithtestproviders',()=>{
it('shouldinjectdocument',inject([DOCUMENT],(document)=>{
expect(document).toBe(window.document);
}));
});
Wecansimplyuseinjecttowrapourtestfunction.Theinjectfunctionacceptsanarrayasthefirstparameterthatshouldincludealistofinjectables.Thesecondparameterisouractualtestfunction,whichwillnowreceivetheinjecteddocument.
Theasyncfunctionontheotherhandhelpsuswithadifferentconcerntoo.Whatifourtestsactuallyinvolveasynchronousoperations?Well,astandardasynchronousJasminetestwouldlooklikethefollowing:
describe('Asynctest',()=>{
it('shouldbecompletedbycallingdone',(done)=>{
setTimeout(()=>{
expect(true).toBe(true);
done();
},2000);
});
});
Jasmineprovidesuswithanicewaytospecifyasynchronoustests.Wecansimplyusethefirstparameterofourtestfunctions,whichresolvestoacallbackfunction.Bycallingthiscallbackfunction,inourcasewenameditdone,wetellJasminethatourasynchronousoperationsaredone,andwewouldliketofinishthetest.
Usingcallbackstoindicatewhetherourasynchronoustestisfinishedisavalidoption.However,thiscanmakeourtestquitecomplicatedifmanyasynchronousoperationsareinvolved.It'ssometimesevenimpossibletomonitoralltheasynchronousoperationsthatarehappeningunderthehood,whichalsomakesitimpossibleforustodeterminetheendofourtest.
Thisiswheretheasynchelperfunctioncomesintoplay.AngularusesalibrarycalledZone.jstomonitoranyasynchronousoperationinthebrowser.Simplyput,Zone.jshooksintoany
www.EBooksWorld.ir
asynchronousoperationandmonitorswheretheyareinitiatedaswellaswhentheyarefinished.Withthisinformation,Angularknowsexactlyhowmanypendingasynchronousoperationsthereare.
Ifwe'reusingtheasynchelper,wetellAngulartoautomaticallyfinishourtestwhenallasynchronousoperationsinourtestaredone.ThehelperusesZone.jstocreateanewzoneanddeterminewhetherallthemicrotasksexecutedwithinthiszonearefinished.
Let'slookathowwecancombineinjectionwithanasynchronousoperationinourtest:
import{describe,expect,it,inject,async}from'@angular/core/testing';
import{DOCUMENT}from'@angular/platform-browser';
describe('Applicationinitializedwithtestproviders',()=>{
it('shouldinjectdocument',async(inject([DOCUMENT],(document)=>{
setTimeout(()=>{
expect(document).toBe(window.document);
},2000);
}))
);
});
Bycombininginjectwithasync(wrapping),wenowhaveanasynchronousoperationinourtest.Theasynchelperwillmakeourtestwaituntilallasynchronousoperationsarecompleted.Wedon'tneedtorelyonacallback,andwehavetheguaranteethateveninternalasynchronousoperationswillcompletebeforeourtestfinishes.
Tip
Zone.jsisdesignedtoworkwithallasynchronousoperationsinthebrowser.ItpatchesallcoreDOMAPIsandmakessurethateveryoperationgoesthroughazone.AngularalsoreliesonZone.jsinordertoinitiatechangedetection.
www.EBooksWorld.ir
TestcomponentbuilderAngularcomeswithanotherveryimportanttestingutilitytotestcomponentsanddirectives.Sofar,weonlytestedthecomponentclassofourcomponents.However,assoonasweneedtotestcomponentsandtheirbehaviorinourapplication,thisinvolvesafewmorethings:
Testingtheviewofcomponents:It'ssometimesrequiredthatwetesttherenderedviewofcomponents.Withallthebindingsinourview,dynamicinstantiationusingtemplatedirectivesandcontentinsertion,it'srequiredthatwecanhaveawaytotestallthisbehavior.Testingchangedetection:Assoonasweupdateourmodelinourcomponentclass,wewanttotesttheupdatesthatareperformedviachangedetection.Thisinvolvesthewholechangedetectionbehaviorofourcomponents.Userinteraction:Ourcomponenttemplatesprobablycontainasetofeventbindings,whichtriggersomebehavioronuserinteraction.We'dalsoneedawaytotestthestateaftersomeuserinteraction.Overridingandmocking:Inatestingscenario,it'ssometimesrequiredtomockcertainareasinourcomponentsinordertocreateaproperisolationforourtest.Inunittesting,weshouldbeconcernedonlyaboutthespecificbehaviorthatwewanttotest.
TheTestComponentBuilder,whichisavailablethroughthe@angular/compiler/testingpackage,helpsusexactlywiththepreviousconcerns.It'sourmaintooltotestcomponents.
TestComponentBuilderisprovidedtothetestapplicationinjector,whichweinitializedinourall.spec.jsmoduleusingthesetBaseTestProvidersfunction.Thereasonforthisisthatthebuilderitselfalsoreliesonalotofplatformandapplicationdependenciestocreatecomponents.Asallourdependenciesnowcomefromthetestinjectorandmostofthemareoverriddentoenableinspection,thismakesperfectsense.
Let'slookataverysimpleexampleofhowwecanuseTestComponentBuildertotesttheviewrenderingofadummycomponent:
@Component({
selector:'dummy-component',
template:'dummy'
})
classDummyComponent{}
describe('CreatingacomponentwithTestComponentBuilder',()=>{
it('shouldrenderitsviewcorrectly',async(inject([TestComponentBuilder],
(tbc)=>{
tbc.createAsync(DummyComponent).then((fixture)=>{
//When
fixture.detectChanges();
//Then
expect(fixture.nativeElement.textContent).toBe('dummy');
});
}))
);
www.EBooksWorld.ir
});
AsTestComponentBuilderisexposedinthetestinjector,weneedtousedependencyinjectiontogetholdoftheinstance.Weusetheinjecthelperforthispurpose.Ascreatingacomponentisanasynchronousoperation,wealsoneedtomakeourtestwaitforcompletionusingtheasynchelper.
Inourtestfunction,wecallthecreateAsyncmethodofTestComponentBuilderandpassareferencetoDummyComponent,whichwewanttocreate.ThismethodreturnsaPromise,whichwillresolveoncethecomponentissuccessfullycompiled.
Inthethencallbackofthereturnedpromise,we'llreceiveaspecialfixtureobjectoftheComponentFixturetype.WecanthencallthedetectChangesmethodonthisfixtureobject,whichwillexecutechangedetectiononthecreatedcomponent.Afterthisinitialchangedetection,theviewofourdummycomponentisupdated.WecannowusethenativeElementpropertyofthefixtureinordertoaccesstherootDOMelementofthecreatedcomponent.
Let'slookattheComponentFixturetypeandtheavailablefieldsinmoredetail:
Member Description
detectChanges()
Thisexecuteschangedetectionontherootcomponentthatwascreatedinthecontextofthefixture.ThetemplatebindingswillnotbeevaluatedautomaticallyaftercreatingacomponentusingTestComponentBuilder.It'sourownresponsibilitytotriggerchangedetection.Evenafterwechangethestateofourcomponents,we'dneedtotriggerchangedetectionagain.
destroy()
Thismethoddestroystheunderlyingcomponentandperformsanycleanupthatisrequired.ThiscanbeusedtotesttheOnDestroycomponent'slifecycle.
componentInstanceThispropertypointstothecomponentclassinstance,andthisisourmaininteractionpointifwewanttointeractwiththecomponent.
nativeElement
ThisisareferencetothenativeDOMelementattherootofthecreatedcomponent.ThispropertycanbeusedtoinspecttherenderedDOMofourcomponentdirectly.
elementRef
ThisistheElementRefwrapperaroundtherootelementofthecreatedcomponent.
www.EBooksWorld.ir
debugElement
ThispropertypointstoaninstanceofDebugElementthatwascreatedbyDebugDomRootRendererinthecomponentviewrenderingpipeline.Thedebugelementprovidesuswithsomeniceutilitiestoinspecttherenderedelementtreeandtestinguserinteraction.We'lltakeacloserlookatthislaterinanothersection.
We'venowlookedataverysimpledummycomponentandhowtotestitusingTestComponentBuilderinconjunctionwiththeinjectandasynchelperfunctions.
Thisisgreat,butitdoesn'treallyreflectthecomplexitythatwefacewhenweneedtotestrealcomponents.Realcomponentshavealotmoredependenciesthanourdummycomponent.Werelyonchilddirectivesandprobablyoninjectedservicestoobtaindata.
Ofcourse,theTestComponentBuilderalsoprovidesuswiththetoolsthatweneedinordertotestmorecomplexcomponentsandkeepthenecessaryisolationinaunittest.
Let'sfirstlookatanexamplewherewe'dliketotestaParentComponentcomponent,whichusesaChildComponentcomponenttorenderalistofnumbers.Aswe'donlyliketotestParentComponent,we'renotinterestedinhowChildComponentrendersthislist.WewanttoremovethebehaviorofthechildcomponentfromourtestbyprovidingamockcomponentforChildComponentduringourtest,whichallowsustoeasilyverifythatthedataisreceivedbythechildcomponent:
@Component({
selector:'child',
template:'<ul><li*ngFor="letnofnumbers">Item:{{n}}</li></ul>'
})
classChildComponent{
@Input()numbers;
}
@Component({
selector:'parent',
template:'<child[numbers]="numbers"></child>',
directives:[ChildComponent]
})
classParentComponent{
numbers=[1,2,3];
}
Thisisourstartingpoint.Wehavetwocomponents,wherewe'llonlybeinterestedintestingtheparentcomponent.However,thechildcomponentisrequiredbytheparentcomponent,anditimpliesaveryspecificwaytorenderthenumbersthatarepassedbytheparent.Wewouldonlyliketotestwhetherournumberswerepassedsuccessfullytothechildcomponent.Wedon'twanttoinvolvetherenderinglogicofthechildcomponentinourtest.Thisisveryimportantbecausechangingonlythechildcomponentcouldthenbreakourparentcomponent
www.EBooksWorld.ir
test,whichwewanttoavoid.
Thethingwewanttoachievenowistocreateamockofourchildcomponentinthecontextofourtest:
@Component({
selector:'child',
template:'{{numbers.toString()}}'
})
classMockChildComponent{
@Input()numbers;
}
InourMockChildComponentclass,it'simportantthatweusethesameselectorpropertyastherealcomponent.Otherwise,themockingwillnotwork.Inthetemplate,weuseaverysimpleoutputofthenumbersinput,whichenablesaneasyinspection.
It'salsoimportantthatweprovidethesameinputpropertiesastheoriginalcomponent.Otherwise,wewon'timitatetherealcomponentcorrectly.
Now,wecangoaheadandperformourtest.UsinganadditionalmethodofTestComponentBuilder,weareabletooverridetherealChildComponentwithourmockcomponent:
describe('ParentComponent',()=>{
it('shouldpassdatatochildcorrectly',async(inject([TestComponentBuilder],
(tbc)=>{
tbc
.overrideDirective(ParentComponent,ChildComponent,MockChildComponent)
.createAsync(ParentComponent).then((fixture)=>{
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('1,2,3');
});
}))
);
});
UsingtheoverrideDirectivemethodonTestBuilderComponent,wecanmodifytheparentcomponent'sdirectivesmetadatabeforewecreateit.Inthisway,we'reabletoexchangetherealchildcomponentwithourMockChildComponentclass.
Asaresult,wedecoupleParentComponentfromChildComponentinthecontextofourtest.Weneedthislevelofseparationinordertocreateaproperisolationofourunittest.Asourmockchildcomponentsimplyrendersthestringrepresentationofthepassedarray,wecaneasilytestthetextcontentofourfixture.
Tip
Thedefinitionofaunittestistotestasingleunitandisolatetheunitfromanydependencies.Ifwewanttosticktothisparadigm,we'dneedtocreateamockforeverydependent
www.EBooksWorld.ir
component.Thiscaneasilygetusintoasituationwhereweneedtomaintainmorecomplexityonlyforthesakeofourtests.Thekeyhereliesinfindingtherightbalance.Youshouldmockdependenciesthathaveagreatimpactonoursubjectandignoredependenciesthathavelowimpactonthefunctionalitywe'dliketotest.
Let'slookatadifferentusecasewherewehaveacomponentthatinjectsaserviceinordertoobtaindata.Aswealsowanttotestonlyourcomponentandnottheserviceitrelieson,wesomehowneedtosneakinamockserviceinsteadoftherealserviceintoourcomponent.TestComponentBuilderalsoprovidesamethodtomodifytheprovidersmetadataofdirectives,whichcomesinveryhandyforthiscase.
First,wedeclareourbasecomponentandaservicethatitrelieson.Inthisexample,theNumbersComponentclassinjectstheNumbersServiceclass,anditobtainsanarraywithnumbersfromit:
@Injectable()
classNumbersService{
numbers=[1,2,3,4,5,6];
}
@Component({
selector:'numbers-component',
template:'{{numbers.toString()}}',
providers:[NumbersService]
})
classNumbersComponent{
constructor(@Inject(NumbersService)numbersService){
this.numbers=numbersService.numbers;
}
}
Now,weneedtocreateamockservicethatprovidesthedatarequiredinourtestandisolatesourcomponentfromtheoriginalservice:
@Injectable()
classMockNumbersServiceextendsNumbersService{
numbers=[1,2,3];
}
Inthissimplifiedexample,wejustprovideadifferentsetofnumbers.However,inarealmockingcase,wecanexcludealotofstepsthatareunnecessaryandcouldpotentiallycreatesideeffects.Usingamockservicealsoensuresthatourtest,whichisfocusedontheNumbersComponentclass,willnotbreakbecauseofachangeintheNumbersServiceclass.
Byextendingtherealservice,wecanleveragesomeofthebehaviorofouroriginalservicewhileoverridingcertainfunctionalityinourmock.Youneedtobecarefulwiththisapproachthough,aswerelyontheoriginalservicebydoingthis.Ifyou'dliketocreateafullyisolatedtest,youshouldprobablyoverrideallmethodsandproperties.Oryoucancreateacompletelyindependentmockservice,whichprovidesthesamemethodsandpropertiesthatareusedin
www.EBooksWorld.ir
yourtest.
Tip
WhenusingTypeScript,youshoulduseinterfacesforthispurposewherebothyourrealserviceaswellasyourmockserviceimplementthesameinterface.
Let'snowlookatthetestcaseandhowwecanuseTestComponentBuildertoprovideourmockserviceinsteadoftherealone:
describe('NumbersComponent',()=>{
it('shouldrendernumberscorrectly',async(inject([TestComponentBuilder],
(tbc)=>{
tbc
.overrideProviders(NumbersComponent,[
provide(NumbersService,{
useClass:MockNumbersService
})
])
.createAsync(NumbersComponent).then((fixture)=>{
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('1,2,3');
});
}))
);
});
UsingtheoverrideProvidersmethodonTestComponentBuilder,wecanprovideadditionalproviderstothecomponentundertest.Thisallowsustooverrideexistingprovidersthatarealreadypresentonthecomponent.Usingtheprovidefunctionofthe@angular/coremodule,wecancreateaproviderwhichprovidesonrequestsforNumberServicebutalsoresolvestoaMockNumberService.
TestComponentBuilderallowsustoperformtestsinaverysimple,isolated,andflexiblefashion.Itplaysamajorrolewhenwritingunittestsforcomponents.Ifyou'dliketoreadmoreabouttheavailablemethodsonTestComponentBuilder,youcanvisittheofficialdocumentationwebsiteathttps://angular.io/docs/ts/latest/api/core/testing/TestComponentBuilder-class.html.
Now,it'stimetousewhatwelearnedaboutTestComponentBuilderserviceandstarttotestourapplicationcomponentsinaction!
www.EBooksWorld.ir
TestingcomponentsinactionIntheprevioustopic,welearnedabouttheTestComponentBuilderserviceandhowtouseittocreatecomponentsinourtestingenvironment.Welearnedabouttheinjectandasynchelpersaswellashowtomockcomponentsandservices.
Let'snowusethisknowledgetoworkonourtestsfortheAutoCompletecomponent.Let'saddanothertesttotheauto-complete.spec.jsfileonthelib/ui/auto-completepath.
AstheAutoCompletecomponentreliesontherathercomplexEditorcomponent,it'sprobablyagoodideatomockourEditorcomponentbeforewestartwritingatest:
@Component({
selector:'ngc-editor',
template:'{{content}}'
})
exportclassMockEditor{
@Input()content;
}
Thismightlookabittenuous,butthisisactuallyallthatweneedforourcurrenttestsontheAutoCompletecomponent.TheEditorcomponentshouldjustacceptacontentinput,whichisthemaininteractionbetweenthetwocomponents.InthetemplateofourMockEditorcomponent,wejustrenderthecontentinputproperty.Thisway,wecaneasilyverifytheresultofusingtheAutoCompletecomponent.
Let'susethismockeditortowriteournexttest:
it('shouldinitializeeditorwithselecteditem',
async(inject([TestComponentBuilder],(tcb)=>{
tcb
.overrideDirective(AutoComplete,Editor,MockEditor)
.createAsync(AutoComplete).then((fixture)=>{
//Given
fixture.componentInstance.items=['one','two','three'];
fixture.componentInstance.selectedItem='two';
//When
fixture.detectChanges();
//Then
expect(fixture.nativeElement.textContent.trim())
.toBe('two');
});
})));
Inourtests,we'dliketotestwhethertheAutoCompletecomponentinitializesEditor(respectively,ourMockEditorcomponent)withtherightcontent.WetestwhetherselectedItemofourAutoCompletecomponentsuccessfullyreflectsintotheeditor.
WeuseTestComponentBuilder,whichcreatescomponentsasynchronously.Usingtheasync
www.EBooksWorld.ir
helperfunction,wetellJasminetowaitforallasynchronousoperationstocompleteforthistest.
UsingtheComponentFixturethatisprovidedbyTestComponentBuilder,wecanstarttointeractwiththecreatedcomponent.UsingthecomponentInstancememberofthecomponentfixture,wecansettherequiredinputpropertiesofourAutoCompletecomponent.
Aswe'reresponsibleforthetriggeringofchangedetectionmanuallyinourtests,weusethedetectChangesmethodonourfixturetoupdatethecomponentview,basedonthenewstate.Thiswillinitiatethechangedetectionlifecycleonourcomponentandperformthenecessaryviewupdates.
AftertheviewupdatesbothofourAutoCompletecomponentandtheunderlyingMockEditorcomponent,wecanrunourassertionstovalidatetheupdatedDOMbygettingthetextcontentofthenativeElementpropertyonourfixture.
Forthisparticulartest,we'refinewiththisapproach.However,inotherscenarioswherewehavemoreDOMelementsinvolved,itwouldn'tbesufficienttoassertontherootcomponent'stextContentpropertydirectly.Thiswouldprobablyincludealotofnoise,whichwe'renotinterestedinforourassertion.Weshouldalwaystrytonarrowourassertiontothefewestdetailspossible.
AswehaveaccesstothenativeDOMelementonourfixture,wecansimplyusetheDOMAPItoselectchildelementsinordertonarrowourassertion:
expect(fixture.nativeElement.querySelector('ngc-
editor').textContent.trim()).toBe('two');
ThiswouldsuccessfullyselecttheDOMelementofourmockeditor,andwecanonlycheckthetextcontentinsidetheeditor.
Althoughthiswouldbeafeasibleapproach,Angularprovidesuswithamuchbetterapproachtoachievethisgoal.
ProvidedbyComponentFixture,wehaveaccesstotheDebugElementtreethatiscreatedbyDebugDomRootRendererinthecontextofourtest.DebugElementallowsusadvancedinspectionoftheelementtreethatwascreatedbyAngularwhenrenderingourcomponents.ItalsocontainsanadvancedqueryingAPI,whichallowsustosearchforcertainelementsinthetree.
Let'srewriteourtesttousetheadvancedcapabilitiesprovidedbyDebugElement:
...
import{By}from'@angular/platform-browser';
...
it('shouldinitializeeditorwithselecteditem',
async(inject([TestComponentBuilder],(tcb)=>{
tcb
.overrideDirective(AutoComplete,Editor,MockEditor)
www.EBooksWorld.ir
.createAsync(AutoComplete).then((fixture)=>{
...
expect(fixture.debugElement.query(By.directive(MockEditor)).nativeElement.text
Content.trim()).toBe('two');
});
})));
ThequeryandqueryAllmethodsthatareavailableoneveryDebugElementobjectallowustoquerytheAngularviewtreelikewewouldqueryaDOMtreeusingquerySelectorandquerySelectorAll.Thedifferencehereisthatwecanuseapredicatehelpertoqueryformatchingelements.UsingtheByhelperclass,wecancreatethesepredicates,whichwillthenbeusedinordertoquerytheDebugElementtree.
TherearecurrentlythreedifferentpredicatesavailableusingtheByhelper:
Member Description
By.all()Thisisthepredicate,whichwillresultinqueryingforallthechildDebugElementobjectofthecurrentDebugElementobject
By.css(selector)Thisisthepredicate,whichwillresultinqueryingforDebugElementusingthespecifiedCSSselector
By.directive(type)Thisisthepredicate,whichwillresultinqueryingforDebugElementthatcontainthespecifieddirective
Goingbacktoourtest,wecannowusethequerymethodonthefixturedebugelementinordertoqueryforoureditor.Aswe'veexchangedtherealEditorcomponentwithourMockEditorcomponent,weneedtoqueryforthelatter.WeuseaBy.directive(MockEditor)predicate,whichwillsuccessfullyqueryfortheDebugElementobjectthatrepresentsthehostelementofourMockEditorcomponent.
ThequerymethodoftheDebugElementobjectwillalwaysreturnanewDebugElementobjectofthefirstfoundelementiftherewasamatch.Itwillreturnnullifthequeriedelementwasnotfound.
ThequeryAllmethodoftheDebugElementwillreturnanarrayofmanyDebugElementwhichcontainsallelementsthatmatchthepredicate.Iftherewerenomatchingelements,thismethodwillreturnanemptyarray.
www.EBooksWorld.ir
TestingcomponentinteractionAlthoughUIinteractiontestingisprobablypartofend-to-endtesting,we'lllookathowtotestbasicuserinteractiononyourcomponentsinthistopic.
Inthistopic,we'lltesttheautocompletecomponentbehavioriftheuserclicksonaniteminthecalloutwindowthatshowsallavailableitems.
Let'saddthistesttothealreadyexistingauto-complete.spec.jsmodule:
it('shouldemitselectedItemChangeonclickincallout',
async(inject([TestComponentBuilder],(tcb)=>{
tcb
.overrideDirective(AutoComplete,Editor,MockEditor)
.createAsync(AutoComplete).then((fixture)=>{
spyOn(fixture.componentInstance.selectedItemChange,'next');
fixture.componentInstance.items=['one','two','three'];
fixture.componentInstance.selectedItem='one';
fixture.componentInstance.onEditModeChange(true);
fixture.componentInstance.onEditableInput('');
fixture.detectChanges();
fixture.debugElement
.queryAll(By.css('.auto-complete__item'))
.find((item)=>item.nativeElement.textContent.trim()==='two')
.triggerEventHandler('click');
expect(fixture.componentInstance.selectedItemChange.next).toHaveBeenCalledWith
('two');
});
})));
First,wewanttosetupaJasminespyontheselectedItemChangeEventEmitternextfunctionforourtest.Thisway,wecanchecklaterwhetherourAutoCompletecomponentsuccessfullyemittedtheeventwhentheuserselectsanitemfromthecallout.
IntheGivensectionofourtestcode,wealsocalltheonEditModeChangedandonEditableInputmethodsontheAutoCompletecomponentinstance.Withthesecalls,wesimulatetheeditorthatwasused,andthere'scurrentlynocontentintheeditor.Thiswillresultinthedesiredfiltering,whichwillpresentallavailableitemsinthecalloutforselection.
IntheWhensectionofourcode,wefirstneedtotriggerchangedetectiononthefixture.Thisresultsinthecalloutwithallavailableauto-completeitemsbeingrenderedintheAutoCompletecomponent.
Now,wecansimulatetheclickeventononeofourautocompleteitemstofishingtheactionsinthistest.
First,we'llselectallDebugElementobjectthatmatchtheCSSclassofourautocompleteitemsinthecallout.Thiswillprovideuswithanarraycontainingalltheelements,wherewecan
www.EBooksWorld.ir
nowusetheArray.prototype.findmethodtoselectonespecificitembasedonthecontainedtext.
OntheDebugElementresultingfromourquery,wenowcallthetriggerEventHandlermethodtosimulateaclickevent.Thiswillactuallynottriggerarealclickevent,butratheritwillexecutethehandlerattachedtothebindingintheviewdirectly.
Aftersimulatingaclickontheautocompleteitemwiththetextcontentoftwo,wecannowinspectourspyontheselectedItemChange.nextfunction.Accordingtothebehaviorinourcomponent,thisshouldhavebeencalledwiththeselecteditemvalue.
TestinguserinteractiononcomponentsismadeveryeasyusingtheDebugElement.WealsodecoupleourtestsfromtheunderlyingDOMeventsbytakingtheshortcutenabledbythetriggerEventHandlermethod.
Tip
ThetriggerEventHandlermethodoperatesonthevirtualelementtreeofAngular,ratherthantheactualDOMtree.Duetothis,wecanalsousethismethodtotriggereventhandlersthatareattachedtocomponentoutputproperties.
www.EBooksWorld.ir
TestingourpluginsystemIntheprevioussections,wecreatedtestsfortheAutoCompletecomponent,whichisarathersimpleUIcomponent.However,welearnedaboutallthetechniquesthatarerequiredtoperformtestingonmorecomplexcomponentsorevensystemsofcomponents.
Now,we'lllookintotestingthepluginsystemthatwascreatedinChapter10,MakingThingsPluggable.
It'sprobablyagoodtimetorecaponthepluginsystemarchitectureoverviewbeforeworkingonthistopic.Asalwayswithtesting,it'scrucialtounderstandexactlywhat'shappeninginthesystemundertest.
Let'screateanewplugin.spec.jsfileinthelib/pluginpath.
Beforeweimplementourfirsttestfunctionforthissubject,wewillneedtocreatesomedummycomponentsandpluginstotestoursystemwith.Let'screatetheseatthetopofourtestingmodule:
@Component({
selector:'dummy-plugin-component-1',
template:'dummy1'
})
exportclassDummyPluginComponent1{}
@Component({
selector:'dummy-plugin-component-2',
template:'dummy2'
})
exportclassDummyPluginComponent2{}
@Component({
selector:'dummy-application',
template:'dummy-slot:<ngc-plugin-slotname="dummy-slot"></ngc-plugin-slot>',
directives:[PluginSlot]
})
exportclassDummyApplication{}
Nothingspecialhere.Wedeclaretwodummycomponentswithastatictemplatethatwillserveusinperformingourplugintests.Additionally,wecreatedadummyapplicationcomponent,whichwillbeourmaintestingcomponent.Inthefollowingtests,wewillmakeuseofadummycomponenttotestourPluginSlotdirective,asopposedtotestingacomponentdirectly.
Next,we'llneedtomockourPluginServiceinjectable,whichisdesignedtoloadpluginsasynchronouslyfromURLs.Inourmock,we'dwanttooverridethisfunctionality.InsteadofloadingpluginsfromURLs,wewanttoloadsomepredefinedtestplugins:
@Injectable()
www.EBooksWorld.ir
exportclassMockPluginServiceextendsPluginService{
constructor(){
super();
this.change={
subscribe(){}
};
}
loadPlugins(){}
}
WeoverridetheloadPluginsmethodtoavoidanypluginsbeingloadedduringtheconstructionoftheservice.WealsooverridetheRxJSsubjectpresentonthechangepropertyinordertopreventanyreactivebehaviorofourpluginsystembecausethiswouldonlydisturbourtests.
Let'sdiverightintoourfirsttestwherewewanttotestaverybasicpluginwithoneplugincomponent,tobeinstantiatedcorrectlybythePluginSlotdirective.First,wesetupourteststructureusingthedescribeanditfunctions:
describe('PluginSlot',()=>{
beforeEachProviders(()=>[
provide(PluginService,{
useClass:MockPluginService
})
]);
it('shouldcreatedummycomponentintodesignated
slot',async(inject([TestComponentBuilder,PluginService],(tcb,pluginService)
=>{
tcb.createAsync(DummyApplication).then((fixture)=>{
...
});
}));
TheonlydifferenceheretowhatwealreadyknewisthatweuseanewbeforeEachProvidersfunctionfromthe@angular/core/testingmodule.Thisfunctionallowsustosetupsomedefaultprovidersthatareusedinourtests.AsallourpluginsystemtestswillrelyonthepresenceofPluginService,weusethisfunctiontosetupthemockproviderresolvingtoourMockPluginServiceclass.
InsteadofusingbeforeEachProviders,wecouldalsousetheoverrideProvidersmethodintheTestComponentBuildertoprovideadditionalinjectables.However,thiswilllimittheusetotheinsideofourcomponents.Ifwewanttointeractwiththeservicefromourtestfunction,weneedtousethebeforeEachProvidershelper.
Usingtheinjecthelper,weinjectTestComponentBuilderandPluginService,whichweprovidedusingthebeforeEachProvidershelper.
Let'snowimplementthemissingtestbodyinsideofthePromisecallbackafterexecuting
www.EBooksWorld.ir
createAsync.
Asafirststep,wedefineanewdummyplugin,whichusesthePluginConfigdecoratorfromthepreviouschapter.WecreateaPluginPlacementinthepluginmetadata,whichincludesamappingofDummyPluginComponent1intotheslotwiththenamedummy-slot.IfyoutakealookattheDummyApplicationcomponentthatweuseinthistestagain,youcanseethatitcontainsaPluginSlotdirectivewiththenameattributesettodummy-slot:
@PluginConfig({
name:'dummy-plugin',
description:'DummyPlugin',
placements:[
newPluginPlacement({slot:'dummy-slot',priority:1,component:
DummyPluginComponent1})
]
})
classDummyPlugin{}
ThispluginshouldnowcausetheDummyPluginComponent1componenttoberenderedinthepluginslotofourDummyApplicationclass.
Asanextstep,weaddtheDummyPluginclasstothepluginslistofourMockPluginServicemockservice:
pluginService.plugins=[{
type:DummyPlugin,
config:DummyPlugin._pluginConfig,
instance:newDummyPlugin()
}];
Theobjectthatwe'readdingtothepluginsarrayoftheMockPluginServicesimplysimulatesapluginthatwouldnormallybeloadedinPluginService.
Next,weputasideareferencetothePluginSlotdirective,whichisplacedinourDummyApplicationcomponent.Forthis,wecanusethequerymethodontheDebugElementrootofourfixture.Weuseapredicate,whichallowsustoquerybythedirectivetypeofourPluginSlotcomponent:
constpluginSlot=fixture.debugElement
.query(By.directive(PluginSlot))
.injector
.get(PluginSlot);
Weneedthereferencetothedirectiveinstanceofthepluginslotinordertoinitializetheslotpriortoourtestassertion.Thisisanimportantstepbecausewecan'trelyontheobservablesubjectinourMockPluginServiceclasstoinitializeourPluginSlotdirective.Weexplicitlydisabledthereactivefeaturesofourpluginsysteminordertoperformpropertesting.Therefore,weneedtomanuallyinitializeourpluginslotbeforewecanperformanyassertion.
www.EBooksWorld.ir
Afterexecutingthequerywiththedirectivepredicate(searchingforanelementwhichcontainsthePluginSlotdirective),we'llreceiveDebugElementofourpluginslotelement.Inordertogetthedirectiveinstance,weusetheelementinjectorpresentoneachDebugElementobject.
TheinitializemethodonthePluginSlotcomponentinstancewillcreateallrelevantplugincomponents.Luckily,thiswillalsoreturnaPromisetous,whichwillberesolvedonceallcomponentshavebeencreatedintheviewofourApplicationDummycomponent:
pluginSlot.initialize().then(()=>{
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('dummy-slot:dummy1');
});
InthecallbackofthePromisereturnedbytheinitializemethodofthePluginSlotinstance,wecanfinallyassertonthetextcontentoftherootelementofourDummyApplicationcomponent.
AstheDummyPluginComponent1classhasasimplestatictemplatethatcontainsthetextdummy1,weshouldseeacompletetextcontentofdummy-slot:dummy1inourapplicationview.
Thisisitforourfirstplugintest.Now,wewilllookatasecondtest,whichwe'llusetoverifyanotherfeatureofourpluginsystem.Ourpluginsystemshouldalsobeabletorendertwocomponentsofthesamepluginintotwoseparatepluginslots.However,inthetemplateofourDummyApplicationcomponent,wecurrentlyonlyhaveonepluginslotwiththenamedummy-slot.
InordertomodifythetemplateofourDummyApplicationcomponentjustforaparticulartest,wecanusetheoverrideTemplatemethodonTestBuilderComponent:
it('shouldcreatetwodummycomponentsofsamepluginintodifferent
slots',async(inject([TestComponentBuilder,PluginService],(tcb,pluginService)
=>{
consttemplate='dummy-slot1:<ngc-plugin-slotname="dummy-slot1">
</ngc-plugin-slot>dummy-slot2:<ngc-plugin-slotname="dummy-slot2"></ngc-
plugin-slot>';
tcb.overrideTemplate(DummyApplication,template)
.createAsync(DummyApplication).then((fixture)=>{
...
});
}))
);
Inourtestfunction,wecreateanewtemplateforourDummyApplicationcomponent.We'readdingtwopluginslotstothetemplatewiththeirnameattributessettodummy-slot1anddummy-slot2.
Now,wecanusetheoverrideTemplatemethodonTestComponentBuildertooverridethe
www.EBooksWorld.ir
DummyApplicationcomponenttemplatebeforewecreateit.Thisprovidesuswiththenecessaryflexibilitytoreusemockanddummycomponentsfordifferenttests.
Let'stakealookatthecodethatcomesinthecreateAsyncpromisecallback:
@PluginConfig({
name:'dummy-plugin',
description:'DummyPlugin',
placements:[
newPluginPlacement({slot:'dummy-slot',priority:1,component:
DummyPluginComponent1}),
newPluginPlacement({slot:'dummy-slot',priority:2,component:
DummyPluginComponent2})
]
})
classDummyPlugin{}
First,wecreateanewDummyPluginpluginclassandusethePluginConfigdecoratortoconfigureit.Intheplacementmetadata,weconfigurethemappingssothatwemaptwocomponentsintodifferentpluginslots.ThefirstcomponentismappedtothepluginslotwiththenameDummySlot1,whilethesecondonewillgototheslotwiththenameDummySlot2.We'veoverriddenourDummyApplicationtemplatetoincludebothofthesepluginslots.
WenowaddourDummyPluginclasstotheMockPluginServiceclassandsimulatethepluginbeingloaded:
pluginService.plugins=[{
type:DummyPlugin,
config:DummyPlugin._pluginConfig,
instance:newDummyPlugin()
}];
ThefollowingcodequeriesforDebugElementsinthefixtureusingthequeryAllmethod.WeuseapredicatethatqueriesforallelementscontainingthePluginSlotdirective.WithanadditionalcalltoArray.prototype.map,wetransformthearrayinordertogetbackthecomponentinstancesofthediscoveredPluginSlotcomponentsdirectly:
constpluginSlots=fixture.debugElement
.queryAll(By.directive(PluginSlot))
.map((debugElement)=>debugElement.injector.get(PluginSlot));
Now,it'stimetocompleteourtest.UsingthePromise.allfunction,we'reabletostreamlineanarrayofPromisesintoasinglePromise,whichwillresolveonceallunderlyingPromisesareresolved.WecanthenmapourpluginSlotsarraybyexecutingtheinitializemethodoneachofthePluginSlotcomponents.ThiswillreturnanarrayofPromisestous,whichwillresolvewhenallcomponentswithinthepluginslotsarecreated:
Promise.all(
pluginSlots.map((pluginSlot)=>pluginSlot.initialize())
).then(()=>{
fixture.detectChanges();
www.EBooksWorld.ir
expect(fixture.nativeElement.textContent).toBe('dummy-slot1:dummy1dummy-
slot2:dumm
y2');
});
InthethencallbackoftheconsolidatedpromiseusingthePromise.allfunction,wecanfinallyperformourassertion.WiththeoverriddentemplateofourDummyApplicationcomponentandtheoutputofourtwoplugincomponentsinthetwoseparatedpluginslots,weshouldgetatextcontentofdummy-slot1:dummy1dummy-slot2:dummy2.
Thisisthelasttestthatwelookatinthischapter.However,therearemoretestsinthecodethatcomeswiththisbook.Justcheckoutthecoderepositoryandgetyourhandsonthosetestsyourself.
www.EBooksWorld.ir
SummaryInthischapter,welearnedhowtowriteconciseunittestsforourcomponents.WefollowedaBDDstyleapproachofwritingtests,andwealsocoveredthebasicsoftheJavaScripttestingframework,Jasmine.
WelearnedaboutthedebuggingtoolsthatareavailableinAngularandhowtosetupaninjectorenvironmentfortesting.UsingTestComponentBuilder,wewereabletoperformtestsinaveryflexiblebutpreciseway.WealsolearnedabouttheviewtreeofmultipleDebugElementthatarecreatedalongwithTestComponentBuilderrunninginthedebugenvironment.Thisallowedustoperformcleverinspectionandapplypracticalqueriestotherenderedviewsinordertoassertexpectedresults.
Weusedtheinjectandasynchelperstoinjectdependenciesand,atthesametime,runasynchronoustests.Webuiltbothmockanddummycomponentsinordertoisolateourtestsfromtherestofourapplication.
www.EBooksWorld.ir
AppendixA.TaskManagementApplicationSourceCodeThesourcecodeofthetaskmanagementapplicationbuiltinthisbookisavailablefromthePacktdownloadservers.Thelinksforeachchapter'sfinalcodearelistedinthisappendix.Thisappendixalsoprovidesinstructionsonhowtousethedownloadedcodeandthestepsrequiredinordertorunthecode.Furthermore,ithelpsyoutroubleshootsomeofthecommonproblemsthatyoucanexperiencewhenworkingwiththetaskmanagementapplicationandsomehintsonhowtoresolvethem.
www.EBooksWorld.ir
DownloadThefollowinglistprovidesdownloadlinksforeachchapterofthebook.Thedownloadlinksreferencedownloadablearchivefiles,whichneedtobeunpackedonyourlocalharddrive:
Chapter Link
Chapter2,Ready,Set,Go!
https://github.com/PacktPublishing/Mastering-Angular-2-Components/tree/master/angular-2-components-chapter-2
Chapter3,ComposingwithComponents
https://github.com/PacktPublishing/Mastering-Angular-2-Components/tree/master/angular-2-components-chapter-3
Chapter4,NoComments,Please!
https://github.com/PacktPublishing/Mastering-Angular-2-Components/tree/master/angular-2-components-chapter-4
Chapter5,Component-BasedRouting
https://github.com/PacktPublishing/Mastering-Angular-2-Components/tree/master/angular-2-components-chapter-5
Chapter6,KeepingUpwithActivities
https://github.com/PacktPublishing/Mastering-Angular-2-Components/tree/master/angular-2-components-chapter-6
Chapter7,ComponentsforUserExperience
https://github.com/PacktPublishing/Mastering-Angular-2-Components/tree/master/angular-2-components-chapter-7
Chapter8,TimeWillTell https://github.com/PacktPublishing/Mastering-Angular-2-Components/tree/master/angular-2-components-chapter-8
Chapter9,SpaceshipDashboard
https://github.com/PacktPublishing/Mastering-Angular-2-Components/tree/master/angular-2-components-chapter-9
Chapter10,MakingThingsPluggable
https://github.com/PacktPublishing/Mastering-Angular-2-Components/tree/master/angular-2-components-chapter-10
Chapter11,PuttingThingstotheTest
https://github.com/PacktPublishing/Mastering-Angular-2-Components/tree/master/angular-2-components-chapter-11
www.EBooksWorld.ir
Thecompletesourcecodeisalsoavailableathttp://www.packtpub.com.
www.EBooksWorld.ir
PrerequisitesThetaskmanagementapplicationisbuiltusingNode.jstechnologies;therefore,itrequiresthatyouhaveNode.jsinstalledonyourmachinebeforeyoucanrunanyofthecode.
YoucandownloadandinstallNode.jsfromthewebsite,http://nodejs.org.InordertobuildtheSasssourcefilesandstartaserverwithlive-reload,twoglobalnodemodulesarerequired:
npminstall-ggulplive-server
www.EBooksWorld.ir
UsageAfterdownloadingthesourcecodeofanindividualchapter,you'llneedtoinstallNPMaswellasJSPMdependencies.Byrunningthefollowingtwolinesofcodeonyourconsole,you'llmakesurealldependenciesareinstalled.Makesurethatyourunthesecommandsfrominsidethedownloadedandextractedcodefolder:
npminstall
jspminstall
Afteryou'veinstalledtherequireddependencies,youcangoaheadandstarttheapplication:
npmstart
TheNPMstartscriptwillinvokegulpinordertocompileanySassfilesaswellasliveservertostartastaticserverwithlive-reload.PleasereadtheprerequisitestopiconhowtoinstalltheseglobalNode.jsmodules.
www.EBooksWorld.ir
TroubleshootingWe'vecarefullyconsideredvariousenvironmentswherethetaskmanagementsourcecodewillbeexecuted.However,there'salwaysachancethatyou'llexperiencesomeissueswhenworkingwiththesourcecode.Thistopicwillprovideyouwithsolutionsforcommonproblemswhenworkingwiththetaskmanagementapplication.
www.EBooksWorld.ir
CleaningIndexDBtoresetdataThetaskmanagementapplicationusesadatastorethatispersistedinyourbrowser.Ifyouusetheapplicationextensivelyacrossmultiplechapters,chancesarehighthatyourdataiscausingsomeapplicationinstability.
Ifyou'dliketocleanthelocaldatabaseusedintheapplication,youcanrunthefollowinglineofcodeonthedebuggerconsoleofyourbrowser.It'simportantthatyourunthecodesnippetinthedebuggerofyourbrowserwiththetaskmanagementapplicationopen.Ifyourbrowserpointstoadifferentorigin,theapplicationdatabasecan'tbedeleted.Afterdeletingthedatabase,theapplicationcanbereloaded,anditwillrecreatethedatabasewiththeinitialsampledata:
indexedDB.deleteDatabase('_pouch_angular-2-components');
www.EBooksWorld.ir
EnablingwebcomponentsinFirefoxChapter6,KeepingUpwithActivities,reliesonthepresenceofShadowDOMinthebrowser.ChromesupportsShadowDOMnativelyafterversion35.InFirefox,youcanenableShadowDOMbyvisitingtheabout:configpageandturningonthedom.webcomponents.enabledflag.
IE,Edge,andSafaridon'tsupportthisstandardatall;however,wecanteachthemhowtodealwithShadowDOMbyincludingapolyfillnamedwebcomponents.js.Youcanfindmoreinformationonthispolyfillathttps://github.com/webcomponents/webcomponentsjs.
www.EBooksWorld.ir
IndexA
2alityURL/Classes
activityslidercomponentbuilding/Buildinganinteractiveactivityslidercomponentprojectionoftime/Projectionoftimeactivityindicators,rendering/Renderingactivityindicatorsimplementing/Bringingittolife,Recap
activitytimelinebuilding/Buildingtheactivitytimeline
Agilepluginbuilding/BuildinganAgilepluginAgiletaskinfocomponent/AgiletaskinfocomponentAgiletaskdetailscomponent/Agiletaskdetailscomponent
Angular2componentarchitecture/Angular'scomponentarchitecture
applicationrunning/Runningtheapplication,Recap
asterisksyntaxandtemplates/Theasterisksyntaxandtemplates
autocompletecomponent/Creatinganautocompletecomponent
www.EBooksWorld.ir
BBehavior-drivenDevelopment(BDD)styletests/AnintroductiontoJasmineBootstrapping
about/Bootstrapping
www.EBooksWorld.ir
CChartist
about/IntroductiontoChartistchartlegend
creating/Creatingachartlegendclasses,ECMAScript6
about/Classescommentingsystem
building/Buildingacommentingsystemcommentcomponent,building/Buildingthecommentcomponent,Buildingthecommentscomponent
componentarchitectureabout/Angular'scomponentarchitectureinAngular2/Everythingisacomponent
componentbuildertesting/Testcomponentbuilder
componentoutputsspyingon/Spyingoncomponentoutputs
componentstesting,utilitiesfor/Utilitiestotestcomponentsproviders/Utilitiestotestcomponentsinaction,testing/Testingcomponentsinactioninteraction,testing/Testingcomponentinteraction
components,userinterfacesencapsulation/Encapsulationcomposability/Composabilitycode,structuring/Components,inventedbynaturemetrics/MyUIframeworkwishliststandards/Timefornewstandardstemplateelements/TemplateelementsshadowDOM/ShadowDOM
composabilityabout/Composability
compositioncontentprojectionused/Compositionusingcontentprojectionbyrouting/Compositionbyrouting
containerabout/Components,inventedbynature
contentprojected,mixingwithgeneratedcontent/Mixingprojectedwithgeneratedcontent
contentprojectionused,forcomposition/Compositionusingcontentprojection
customUIelements
www.EBooksWorld.ir
about/CustomUIelements,Recapcheckbox/CustomUIelementstogglebuttons/CustomUIelements
www.EBooksWorld.ir
Ddata
about/Data–Faketorealdecorators,ECMAScript6
about/Decoratorsdownloadlinks/Downloaddraganddrop
about/Draganddropdraggabledirective,implementing/Implementingthedraggabledirectivedroptargetdirective,implementing/Implementingadroptargetdirectiveintasklistcomponent,implementing/Integratingdraganddropintasklistcomponentrecapitulate/Recapitulateondraganddrop
draggabledirectiveimplementing/Implementingthedraggabledirective
droptargetdirectiveimplementing/Implementingadroptargetdirective
DSL/Pluginarchitecture
www.EBooksWorld.ir
EECMAScript6
about/JavaScriptofthefuturefeatures/IspeakJavaScript,translate,please!classes/Classesmodules/Modulestemplatestrings/TemplatestringsversusTypeScript/ECMAScriptorTypeScript?decorators/Decorators
editorabout/Oneeditortorulethemallcomponent,creating/Creatinganeditorcomponent,Recap
editorcomponenttaginput,integrating/Integratingtaginputwithintheeditorcomponent
effortsmanaging/Managingeffortsestimatedduration/Managingeffortseffectiveduration/Managingeffortstimedurationinput/Thetimedurationinputmanaging,componentsfor/Componentstomanageeffortsdurationcomponent/Componentstomanageeffortscomponent/Componentstomanageeffortsvisualeffortstimeline/Thevisualeffortstimelinerecapitulatingon/Recapitulatingoneffortsmanagement
embeddedviewsadding/Addingandremovingembeddedviewsremoving/Addingandremovingembeddedviews
encapsulationabout/Encapsulation,Therightlevelofencapsulation,Recap
event-basedextensionpoints/Pluginarchitecture
www.EBooksWorld.ir
GGangofFour(GoF)
about/Decorators
www.EBooksWorld.ir
Hhelloworldcomponent
writing/Yourfirstcomponentwriting,JavaScriptused/JavaScriptofthefuture
HTMLURL/BuildingSVGcomponents
www.EBooksWorld.ir
Iimmediatelyinvokedfunctionexpression(IIFE)
about/Modulesimmutability
about/Immutabilitybenefits/Immutability
IndexDBcleaning,torestdata/CleaningIndexDBtoresetdata
infinitescrolldirectivecreating/Creatinganinfinitescrolldirectivefinishing/Finishingourinfinitescrolldirective
inputgeneratingoutput/Inputgeneratesoutput,Recap
www.EBooksWorld.ir
JJasmine
about/AnintroductiontoJasminecore/AnintroductiontoJasmineHTML/AnintroductiontoJasmineboot/AnintroductiontoJasminefirsttest,writing/Writingourfirsttest
JavaScriptusing/JavaScriptofthefutureECMAScript6/JavaScriptofthefuture
JSPMabout/SystemJSandJSPM,Startingfromscratchapplication,creating/GettingstartedwithJSPM
www.EBooksWorld.ir
Llifecyclehooks
URL/CustomUIelementslocalviewvariables/Inputgeneratesoutputloggingactivities
service,creatingfor/Creatingaserviceforloggingactivitiesabout/Loggingactivities
www.EBooksWorld.ir
MMathML
URL/BuildingSVGcomponentsmilestones
setting/Settingmilestonesautocompletecomponent,creating/Creatinganautocompletecomponent
modules,ECMAScript6about/Modules
MozillaDeveloperURL/StylingSVG
MozillaDeveloperNetworkURL,fordocumentation/Modules
www.EBooksWorld.ir
NNamespacesCrashCoursearticle
URL/BuildingSVGcomponentsnavigation
refactoring/Refactoringnavigation,SummaryNode.js
about/Node.jsandNPMURL/Node.jsandNPM
nodepackagemanager(NPM)about/Node.jsandNPM
NodeVersionManager(NVM)URL/GettingstartedwithJSPM
NPMstartscript/Usage
www.EBooksWorld.ir
Oobject-orientedprogramming(OOP)
about/Components–Theorgansofuserinterfacesobservabledatastructures
reactiveprogrammingwith/Reactiveprogrammingwithobservabledatastructuresopentasks
visualizing/Visualizingopentaskschart,creating/Creatinganopentaskschartchartlegend,creating/Creatingachartlegendtaskschart,makinginteractive/Makingtaskschartinteractive
www.EBooksWorld.ir
PpluggableUIcomponents
about/PluggableUIcomponentspluginarchitecture,finalizing/Finalizingourpluginarchitecture
plugin,architectureabout/Pluginarchitectureextensibility/Pluginarchitectureportability/Pluginarchitecturecomposability/Pluginarchitecture
pluginAPIimplementing/ImplementingthepluginAPIplugincomponents,instantiating/Instantiatingplugincomponents
plugininterfaces/Pluginarchitectureplugins
managing/Managingpluginsnewplugins,loadingatruntime/Loadingnewpluginsatruntime
pluginsystemtesting/Testingourpluginsystem
PrecisionGraphicsMarkupLanguage(PGML)/LeveragingthepowerofSVGprerequisites/Prerequisitesprojectsdashboard
about/Projectsdashboardtaskschart/Projectsdashboardactivitychart/Projectsdashboardprojectsummary/Projectsdashboardcomponent,creating/Creatingtheprojectsdashboardcomponentsummarycomponent/Projectsummarycomponentfirstchart,creating/Creatingyourfirstchart
purecomponentsabout/Purecomponents
www.EBooksWorld.ir
Rreactiveprogramming
withobservabledatastructures/Reactiveprogrammingwithobservabledatastructures
routeabout/Backtotheroutesroutabletabs/Routabletabs
routerabout/AnintroductiontotheAngularrouterconfiguring/AnintroductiontotheAngularrouteroutlets/AnintroductiontotheAngularrouterlink/AnintroductiontotheAngularrouterversustemplatecomposition/Routerversustemplatecomposition
routetreeabout/Understandingtheroutetree
routingcompositionby/Compositionbyrouting
www.EBooksWorld.ir
Sservice
creating,forloggingactivities/Creatingaserviceforloggingactivitiesloggingactivities/Loggingactivities
ServiceProviderInterface(SPI)/PluginarchitectureshadowDOM
about/ShadowDOMSVG
about/LeveragingthepowerofSVGstyling/StylingSVGgraphicalstructure/StylingSVGvisualappearance/StylingSVGcomponents,building/BuildingSVGcomponentsURL/BuildingSVGcomponents
SynchronizedMultimediaIntegrationLanguage(SMIL)/BuildingSVGcomponentsSystemJS
about/SystemJSandJSPM
www.EBooksWorld.ir
Ttabbedinterfacecomponent
creating/Creatingatabbedinterfacecomponent,Recaptaginput
supporting/Supportingtaginputmanager,creating/Creatingataginputmanagertagsselectcomponent,creating/Creatingatagsselectcomponentintegrating,withineditorcomponent/Integratingtaginputwithintheeditorcomponenttaggingsystem,finishingup/Finishingupourtaggingsystem
tagmanagementabout/Tagmanagementtagdataentity/Tagdataentitytags,generating/Generatingtagstagsservice,creating/Creatingatagsservicetags,rendering/Renderingtagstaskservice,integrating/Integratingthetaskservicetagsservice,finishing/Completionofthetagsservice
tagsenabling,fortasks/Enablingtagsfortasks
tagsselectcomponentcreating/Creatingatagsselectcomponent
tagsservicecreating/Creatingatagsservicefinishing/Completionofthetagsservice
taskdetailsabout/Taskdetails
tasklistcreating/Creatingatasklist,Recappurifying/Purifyingourtasklist,Recap
tasksmanaging/Managingtasksvision/Visionfiltering/Filteringtaskstags,enablingfor/Enablingtagsfortasks
templatecompositionversusrouter/Routerversustemplatecomposition
templatedirectivechange,detecting/Detectingchangewithinourtemplatedirective
templateelementsabout/Templateelements
templatestrings,ECMAScript6about/Templatestrings
www.EBooksWorld.ir
testsinjectingin/Injectingintests
timedurationinputabout/Thetimedurationinput
toolsabout/ToolsNode.js/Node.jsandNPMnodepackagemanager(NPM)/Node.jsandNPMSystemJS/SystemJSandJSPMJSPM/SystemJSandJSPM
troubleshooting/TroubleshootingTypeScript
versusECMAScript6/ECMAScriptorTypeScript?
www.EBooksWorld.ir
Uuserinterfaces
about/Thinkingoforganismscomponents/Components–Theorgansofuserinterfaces
www.EBooksWorld.ir
VVectorMarkupLanguage(VML)/LeveragingthepowerofSVGvisualeffortstimeline/Thevisualeffortstimeline
www.EBooksWorld.ir
Wwebcomponents
enabling,inFirefox/EnablingwebcomponentsinFirefoxwebcomponents.js
URL/Buildinganinteractiveactivityslidercomponent
www.EBooksWorld.ir