Upload
others
View
1
Download
0
Embed Size (px)
Citation preview
LearningVue.js2
TableofContents
LearningVue.js2CreditsAbouttheAuthorAcknowledgmentsAbouttheReviewerwww.PacktPub.com
Whysubscribe?DedicationPreface
WhatthisbookcoversWhatyouneedforthisbookWhothisbookisforConventionsReaderfeedbackCustomersupport
DownloadingtheexamplecodeErrataPiracyQuestions
1.GoingShoppingwithVue.jsBuzzwordsVue.jshistoryThemostimportantthingaboutVue.jsLet'sgoshopping!
ImplementingashoppinglistusingjQueryImplementingashoppinglistusingVue.jsAnalyzingdatabindingusingdevelopertoolsBringinguserinputtothedatawithtwo-waybindingRenderingthelistofitemsusingthev-fordirectiveCheckanduncheckshoppinglistitemsAddingnewshoppinglistitemsusingthev-ondirective
UsingVue.jsinanexistingprojectVue.js2.0!ProjectsusingVue.js
GrammarlyOptimizelyFilterBlendPushSilver
BookroadmapLet'smanagetime!
Togglethetitlebyusingcomputedproperties
Left-padtimevaluesusingcomputedpropertiesKeepstatewithstart,pause,andstopbuttons
ExerciseSummary
2.Fundamentals–InstallingandUsingMVVMarchitecturalpatternDefineProperty,getters,andsettersComparingwithotherframeworks
ReactAngularVue
Vue.jsfundamentalsReusablecomponentsVue.jsdirectivesPluginsinVue.jsExerciseApplicationstateandVuexvue-cliVuepluginsforIDEs
Installing,using,anddebuggingaVue.jsapplicationInstallingVue.js
StandaloneCDNBowerCSP-compliantnpmvue-cliDevbuild
DebuggingyourVueapplicationScaffoldingourapplications
ScaffoldingtheshoppinglistapplicationBootstrapingyourPomodoroapplication
ExerciseSummary
3.Components–UnderstandingandUsingRevisitingcomponentsBenefitsofusingcomponents
DeclaringtemplatesinHTMLHandlingdataandelpropertiesinsideacomponentScopeofthecomponentsComponentsinsideothercomponents
RewritingtheshoppinglistwithsimplecomponentsDefiningtemplatesforallthecomponentsDefiningandregisteringallthecomponents
ExerciseSingle-filecomponents
PluginsforIDEsStyleandscopeHot-reloadingPreprocessors
HTMLpreprocessorsCSSpreprocessorsJavaScriptpreprocessors
Rewritingourshoppinglistapplicationwithsingle-filecomponentsAddItemComponentConfiguringItemComponentandItemsComponent
ExerciseRewritingthePomodoroapplicationwithsingle-filecomponentsReactivebindingofCSStransitionsSummary
4.Reactivity–BindingDatatoYourApplicationRevisitingdatabindingInterpolatingdata
AddingtitleofthePomodorostateExercise
UsingexpressionsandfiltersExpressionsFiltersExercise
RevisitingandapplyingdirectivesTwo-waybindingusingthev-modeldirectiveTwo-waybindingbetweencomponentsBindingattributesusingthev-binddirectiveConditionalrenderingusingv-ifandv-showdirectivesArrayiterationusingthev-fordirective
CreatingShoppingListComponentandmodifyingItemsComponentModifyingApp.vue
Eventlistenersusingthev-ondirectiveShorthandsExercise
KittensSummary
5.Vuex–ManagingStateinYourApplicationParent-childcomponents'communication,events,andbrainteaserWhydoweneedaglobalstatestore?WhatisVuex?Howdoesthestoreworkandwhatissospecialaboutit?Greetingswithstore
StorestateandgettersMutationsActions
InstallingandusingtheVuexstoreinourapplicationsUsingtheVuexstoreintheshoppinglistapplicationUsingVuexstoreinthePomodoroapplication
Bringinglifetostart,pause,andstopbuttonsBindingPomodorominutesandsecondsCreatingthePomodorotimerChangingthekitten
Summary6.Plugins–BuildingYourHousewithYourOwnBricks
ThenatureofVuepluginsUsingthevue-resourcepluginintheshoppinglistapplication
CreatingasimpleserverInstallingvue-resource,creatingresources,anditsmethodsFetchingalltheshoppingliststheapplicationstartsUpdatingserverstatusonchangesCreatinganewshoppinglistDeletingexistingshoppinglistsExercise
CreatingandusingaplugininthePomodoroapplicationCreatingtheNoiseGeneratorpluginUsingtheplugininthePomodoroapplicationCreatingabuttontotogglethesoundExercise
Summary7.Testing–TimetoTestWhatWeHaveDoneSoFar!
Whyunittests?UnittestsforVueapplicationWritingunittestsfortheshoppinglistapplication
Testingactions,getters,andmutationsGoodtestcriteriaCodecoverageFakingserverresponsesandwritingasynchronoustestsTestingcomponents
WritingunittestsforourPomodoroapplicationWhatisend-to-endtesting?Nightwatchfore2eWritinge2etestsforthePomodoroapplicationSummary
8.Deploying–TimetoGoLive!Softwaredeployment
WhatisGitHub?
WhatisTravis?WhatisHeroku?
MovingtheapplicationtotheGitHubrepositorySettingcontinuousintegrationwithTravisDeployingthePomodoroapplication
CheckinglogsPreparingtheapplicationtorunonHeroku
DeployingtheshoppinglistapplicationTryingHerokulocally
9.WhatIsNext?ThejourneysofarVue2.0Revisitingourapplications
ShoppinglistapplicationThePomodoroapplication
Whyisitjustthebeginning?Addingfeaturestoourapplications
ShoppinglistapplicationThePomodoroapplication
BeautifyingourapplicationsLogotypeIdentityanddesignAnimationsandtransitions
ExtendingourapplicationstootherdevicesSummary
10.SolutionstoExercisesExerciseforchapter1Exercisesforchapter2
EnhancingMathPluginCreatingaChromeapplicationofthePomodorotimer
Exercisesforchapter3Exercise1Exercise2
Summary
LearningVue.js2
LearningVue.js2Copyright©2016PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:December2016
Productionreference:1071216
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
Birmingham
B32PB,UK.
ISBN978-1-78646-994-6
www.packtpub.com
Credits
Author
OlgaFilipova
CopyEditor
SameenSiddiqui
Reviewer
Bogdan-AlinBâlc
ProjectCoordinator
SheejalShah
CommissioningEditor
WilsonD'souza
Proofreader
SafisEditing
AcquisitionEditor
ChaitanyaNair
Indexer
TejalDaruwaleSoni
ContentDevelopmentEditor
DivijKotian
ProductionCoordinator
MelwynD'sa
TechnicalEditor
PrajaktaMhatre
AbouttheAuthorOlgaFilipovawasborninUkraine,inKyiv.Shegrewupinafamilyofphysicists,scientists,andprofessors.ShestudiedsystemanalysisattheNationalUniversityofUkraineKyivPolytechnicInstitute.Attheageof20,shemovedtoPortugalwhereshedidherbachelor'sandmaster'sdegreesincomputerscienceintheUniversityofCoimbra.Duringherstudies,sheparticipatedinresearchanddevelopmentofEuropeanprojectsandbecameanassistantteacherofoperatingsystemsandcomputergraphics.Afterobtaininghermaster'sdegree,shestartedtoworkatFeedzai.Atthattime,itwasasmallteamoffourwhodevelopedaproductfromscratch,andnowitisoneofthemostsuccessfulPortuguesestartups.Atsomepoint,hermainresponsibilitybecametodevelopalibrarywritteninJavaScriptwhosepurposewastobringdatafromtheenginetothewebinterface.ThismarkedOlga'smaindirectionintech—webdevelopment.Atthesametime,shecontinuedherteachingpracticeinacourseofprofessionalwebdevelopmentinthelocalprofessionaleducationcenterinCoimbra.
In2013,alongwithherbrotherandherhusband,shestartedaneducationalprojectbasedinUkraine.Thisproject'snameisEdEraandithasgrownfromasmallplatformofonlinecoursesintoabigplayerintheUkrainianeducationalsystem.Currently,EdEraispointinginantheinternationaldirectionandpreparinganawesomeonlinecourseaboutIT.Don'tmissit!
In2014,Olga,withherhusbandanddaughter,movedfromPortugaltoBerlin,whereshestartedworkingatMeetricsasafrontendengineer,andafterayearshebecametheleadofanamazingteamoffrontendsoftwaredevelopers.
OlgaishappilymarriedtoanawesomeguycalledRui,whoisalsoasoftwareengineer.RuistudiedwithOlgaattheUniversityofCoimbraandworkedwithheratFeedzai.OlgahasasmartandbeautifuldaughtercalledTaissa,afluffycatcalledPatusca,andtwofluffiestchinchillascalledBarabashkaandCheburashka.
AcknowledgmentsIamgratefultoPacktPublishingforofferingmethepossibilitytowritethisbook.Youaregreatandsoisyourteam.ThankyouDivij,Chaitanya,Prajakta,andthewholePacktteamforbeingawesomeandsupportingmethroughallthisjourneyinsuchafriendlyandwarmway.
Qualityissomethingthatisdifficulttoachievewhenworkingonsomethingonyourown.Thankyou,Packtteam,you'vebeenawesome.AndabigspecialgratitudegoestoRomania,toBogdan,whothoroughlyreviewedthebook,ranallthecodesnippets,tests,andlint.Bogdan'sattentiontoeventhemosttinydetailsisastonishing.ThebookwasrewrittenafterBogdan'scomments,anditbecamesomuchcleaner.Thankyouverymuch,BogdanandAlex,fortherecommendation.
Time.Support.Love.Whenyouhavethesethreethingsyouarehappyandanychallengeintheworldcanscareyou.Whenyouhavethesethreethingsyouknowthatyouarecapableofeverything.Whenyouhavethesethreethingsyouhavepower.Butyoucanneverhavethesethingsalone.Thatiswhyyoumustbeeternallygratefultothosewhoprovidetime,support,andlovetoyou.
ThatiswhymybigthanksgoestomycompanywhereIamcurrentlyworking—Meetrics.Meetricsprovidedmewithtimetowritethebook.Theytrustedmeandallowedmetouseafractionofmyworkingtimeforwritingthebook.Thankyouverymuch!
Iwanttothanktoallmyfriendsandcolleagueswhosupportedmeduringthisjourney.EverytimeIcometoMeetricsmyteamasksmehowthebookisgoing.Everytimewe'regoingtoPortugalorUkraine,ourfriendsandfamilyask.EverydaymyfriendsfromBerlinaskmehowisitgoing.Thankyou,people,youareawesome!Thankyou,EdErateam,forbeingamazingandpostponingimportanttasksbecauseofmybook.
IwouldlovetoexpressgratitudetomyparentsforeducatingmewithsomuchlovethatIknowthatIamcapableofanything.IknowthatIwillnotfail.Thankyouforallyourloveandsupport.Thankyouforgivingmethisconfidenceinmyself.Iwanttothanktomylovelydaughter,whoseloveandhelpkeepsmegoingandcontinuingwhatI'mdoing,knowingthatallthisisnotfornothing.Iwanttothanktomybrotherforallthefunweshareevenandmostlywhilewe'reworking.
AndIwanttoaddressaspecialthankswithlovetomyhusband.AlongthiswritingjourneyRuihasgivenmetime,support,andlove.RuidideverythingathomesoIcouldhaveallthetimeforwriting.RuifelteveryslightchangeinmymoodandprovidedsupportduringallofthemsoIcouldfeelcomfortableagainandwrite.IfIwouldstayupthewholenightwritingandneededsomeonetobenearby,Ruiwouldstayupthewholenight.Foreverychapterinthebook,Ruiwasthefirstpersontoreviewthem.Thiswasinvaluablefeedback.RuigavemechaptersbackfullofcorrectionsandIfeltsad.Butthenhewouldsaysomethinglike:Ohmygod,Olga,thischapterisamazing!Iunderstoodeverything!Iamlookingforwardforthe
nextchaptertoseewhat'snext!Whensomeonewhoyouloveverymuchtellsyouthis,youjustwanttomoveonandcontinueyouramazingwriting.Thankyouverymuchforthis!
AbouttheReviewerBogdan-AlinBâlcisateamleadwithapassionforfrontendtechnologies.HehasworkedwithJavaScriptforthepast8years,fromtheemergenceofjQueryandAjaxtomodernfull-fledgedMVCframeworks.Whenhe'snotlookingintosomenewJSchallenge,hespendstimewithhisfriends,playinggamesandwatchingsports.
www.PacktPub.comForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusatservice@packtpub.comformoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www.packtpub.com/mapt
Getthemostin-demandsoftwareskillswithMapt.MaptgivesyoufullaccesstoallPacktbooksandvideocourses,aswellasindustry-leadingtoolstohelpyouplanyourpersonaldevelopmentandadvanceyourcareer.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
DedicationIdevotethisbooktomydaughter,Taissa.
PrefaceThisbookisaboutVue.js.WewillstartourjourneytryingtounderstandwhatVue.jsis,howitcomparestootherframeworks,andwhatitallowsustodo.WewilllearndifferentaspectsofVue.jswhilebuildingsmallfunnyapplicationsontopofitandapplyingtheseaspectsinpractice.Intheend,wewilllookbacktoseewhat'vewelearnedandhavealookintothefuturetoseewhatwecanstilllearnanddo.So,youwilllearnthefollowing:
WhatisVue.jsandhowitworksReactivityanddatabindingwithVue.jsReusablecomponentswithVue.jsPluginsforVue.jsTestinganddeployingapplicationswritteninVue.js
AlltheexamplesinthisbookarebuiltontopoftherecentlyreleasedVue2.0version.Thebookalsocontainsreferencestothepreviousversionregardingdeprecatedorchangedaspectsoftheframework.
IamsureyouwillenjoytheprocessofbuildingapplicationsusingVue.jswiththisbook.
WhatthisbookcoversChapter1,GoingShoppingwithVue.js,containsanintroductiontoVue.js,totheterminologyusedthroughthebook,andfirstbasicexamples.
Chapter2,Fundamentals–InstallingandUsing,explainsthebehindthescenesofVue.js,providestheoreticalinsightsintothearchitecturalpattern,touchesnearlyallthemainVue.jsconcepts,andbootstrapstheapplicationsthatwillbedevelopedthroughthebook.
Chapter3,Components–UnderstandingandUsing,goesdeepintocomponentsandexplainshowtorewriteapplicationsusingasimplecomponentsystemandsingle-filecomponents.
Chapter4,Reactivity–BindingDatatoYourApplication,containsadetailedexplanationsoftheusageofdatabindingmechanismsinVue.js.
Chapter5,Vuex–ManagingStateinYourApplication,containsdetailedintroductiontoVuex,astatemanagementsystemforVue.js,andexplainshowtouseitinyourapplicationinordertoachieveanice,maintainablearchitecture.
Chapter6,Plugins–BuildingYourHousewithYourOwnBricks,showshowtousepluginsinVueapplicationsandexplainshowtouseanexistingplugininanapplicationandexplainshowtobuildourownpluginandthenuseit.
Chapter7,Testing–TimetoTestWhatWeHaveDoneSoFar,containsanintroductiontothetestingtechniquesthatcanbeusedinVueapplicationstobringthemtotheneededlevelofquality.Wetackleitbyshowinghowtowriteunittestsandhowtodevelopend-to-endtestsfortheapplicationsinthebook.
Chapter8,Deploying–TimetoGoLive!,showshowtobringyourVueapplicationtotheworld,guaranteeingitsqualitywithcontinuousintegrationtools.ItexplainshowtoconnectaGitHubrepositorytotheTraviscontinuousintegrationsystemandtotheHerokuclouddeploymentplatform.
Chapter9,WhatIsNext,wrapsupeverythingthathasbeendonesofarandleavesthereaderwiththefollowupsteps.
Appendix,SolutionstoExercises,providessolutionstotheexercisesforfirstthreechapters.
WhatyouneedforthisbookTherequirementsforthisbookarethefollowing:
ComputerwithanInternetconnectionTexteditor/IDENode.js
WhothisbookisforThisbookisforwebdevelopersorforpeoplewhowanttobecomewebdevelopers.Whetheryouhavejuststartedtoworkwithwebtechnologiesoryouarealreadyaguruofframeworksandlanguagesinthevastoceanofwebtechnologies,thisbookmightshowyousomethingnewintheworldofreactivewebapplications.IfyouareaVuedeveloperandhaveusedVue1.0,thisbookmightbeausefulguideforyoutomigratetoVue2.0,sincealltheexamplesofthebookarebasedonVue2.0.EvenifyouarealreadyusingVue2.0,thisbookmightbeaniceexerciseofbuildinganapplicationfromscratch,applyingallVueandsoftwareengineeringconceptsandtakingittothedeploymentstage.
Atleastsometechnicalbackgroundisrequired.IfyoucanalreadywritecodeinJavaScript,itisahugeplus.
ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:"Yourpluginmustprovideaninstallmethod."
Ablockofcodeissetasfollows:
exportdefault{
components:{
ShoppingListComponent,
ShoppingListTitleComponent
},
computed:mapGetters({
shoppinglists:'getLists'
})
}
Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:
exportdefault{
components:{
ShoppingListComponent,
ShoppingListTitleComponent
},
computed:mapGetters({
shoppinglists:'getLists'
}),
methods:mapActions(['populateShoppingLists']),
store,
mounted(){
this.populateShoppingLists()
}
}
Anycommand-lineinputoroutputiswrittenasfollows:
cdshopping-list
npminstallvue-resource--save-dev
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:"ChecktheDevelopermodecheckbox."
Note
Warningsorimportantnotesappearinaboxlikethis.
Tip
Tipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook-whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.Tosendusgeneralfeedback,[email protected],andmentionthebook'stitleinthesubjectofyourmessage.Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforthisbookfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Youcandownloadthecodefilesbyfollowingthesesteps:
1. Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.2. HoverthemousepointerontheSUPPORTtabatthetop.3. ClickonCodeDownloads&Errata.4. EnterthenameofthebookintheSearchbox.5. Selectthebookforwhichyou'relookingtodownloadthecodefiles.6. Choosefromthedrop-downmenuwhereyoupurchasedthisbookfrom.7. ClickonCodeDownload.
Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux
ThecodebundleforthebookisalsohostedonGitHubathttps://github.com/PacktPublishing/Learning-Vue.js-2.Wealsohaveothercodebundlesfromourrichcatalogofbooksandvideosavailableathttps://github.com/PacktPublishing/.Checkthemout!
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks-maybeamistakeinthetextorthecode-wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.
Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusatcopyright@packtpub.comwithalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
QuestionsIfyouhaveaproblemwithanyaspectofthisbook,[email protected],andwewilldoourbesttoaddresstheproblem.
Chapter1.GoingShoppingwithVue.js"Vue.jsisaJavaScriptframeworkforbuildingastonishingwebapplications.Vue.jsisaJavaScriptlibraryforcreatingwebinterfaces.Vue.jsisatoolthatleveragestheuseofMVVMarchitecture."
SimplifiedJavaScriptJargonsuggeststhatVue.jsisaJavaScriptlibraryforcreatinguserinterfaces(Views)basedonunderlyingdatamodels(http://jargon.js.org/_glossary/VUEJS.md).
TheofficialVue.jswebsite(https://vuejs.org/)justsomemonthsagostatedthatVue.jswerereactivecomponentsformodernwebinterfaces.
NowitstatesthatVue.jsisaprogressiveJavaScriptframework:
SowhatisVue.jsafterall?Framework?Tool?Library?Shoulditbeusedforbuildingfull-stackwebapplicationsorjustforaddingsomespecialfunctionality?ShouldIswitchfrommyfavoriteframeworktoit?Ifyes,why?CanIuseitalongsideothertoolsinmyproject?Whatadvantagesitmightbring?
Inthischapter,wewilltrytofindtheanswerstoallthesequestions.WewillslightlytouchVue.jsanduseitwithinsomesmallandsimpleexamples.
Morespecifically,wewilldothefollowing:
LearnwhatVue.jsis,itsimportantparts,anditshistoryLearnwhatprojectsuseVue.jsBuildasimpleshoppinglistusingVue.jsandcomparetheimplementationtothejQueryimplementationofthesameapplicationBuildasimplePomodorotimerusingVue.jsEnjoyasmallandsimpleexercise
BuzzwordsTherewillbelotsofbuzzwords,abbreviations,andotherhipstercombinationsoflettersinthisbook.Pleasedonotbeafraidofthem.Icantellyoumorebut,forthemostpartofthingsyouneedtodousingVue.jsoranyotherframework,youdonotneedtoknowthemallbyheart!But,inanycase,letusleavethethesaurusheresothatyoubecomeconfusedwithterminologyatanypointofthebook,youcancomebackhereandhavealook:
Applicationstate:Thisisaglobalcentralizedstateoftheapplication.Thedatainthisstateisinitializedwhentheapplicationisstarted.Thisdatacanbeaccessedbyanyapplication'scomponent;however,itcannotbechangedeasilybythem.Eachitemofthestatehasanattachedmutationthatcanbedispatchedonspecialeventsoccurringinsidetheapplication'scomponents.Bootstrap:ThisisaprojectthatprovidesasetofstylesandJavaScripttoolsfordevelopingaresponsiveandniceapplicationwithouthavingtothinkalotaboutCSS.ContentDistributionNetwork(CDN):Thisisaspecialserverwhoseaimistodeliverdatatotheuserswithhighavailabilityandhighperformance.PeopleandcompanieswhodevelopframeworksliketodistributethemviaCDNsbecausetheyallowthemjusttopointouttheCDN'sURLintheinstallationinstructions.Vue.jsishostedinnpmcdn(https://npmcdn.com/),whichisareliableandglobalnetworkforthethingsthatarepublishedtothenpm.Components:ThesearethepiecesoftheapplicationwiththeirowndataandViewthatcanbereusedthroughtheapplication,actingasabrickfromwhichthehouseisbeingbuilt.CascadingStyleSheets(CSS):ThisisasetofstylestoapplytotheHTMLdocumenttomakeitniceandbeautiful.DeclarativeViews:ThesearetheViewsthatprovideawayofdirectdatabindingbetweenplainJavaScriptdatamodelsandtherepresentation.Directives:ThesearespecialHTMLelementsattributesinVue.jsthatallowdatabindingindifferentways.DocumentObjectModel(DOM):ThisisaconventionforrepresentingnodesinmarkuplanguagessuchasHTML,XML,andXHTML.ThenodesofthedocumentsareorganizedintoaDOMtree.WhensomeonesaysinteractingwithDOM,itisjusttheirfancywayofsayinginteractingwithHTMLelements.npm:ThisisapackagemanagerforJavaScriptandallowssearching,installing,andmanagingJavaScriptpackages.Markdown:Thisisahuman-friendlysyntaxthatallowswebwriterstowritetheirtextwithoutworryingaboutstylesandHTMLtags.Markdownfileshavea.mdextension.ModelViewViewModel(MVVM):ThisisanarchitecturalpatternwhosecentralpointisaViewModelthatactsasabridgebetweentheViewandthedatamodel,allowingthedataflowbetweenthem.ModelViewController(MVC):Thisisanarchitecturalpattern.ItallowsseparatingViewsfromModelsandfromthewaythatinformationflowsfromViewstoModels,andviceversa.
One-waydatabinding:ThisisthetypeofdatabindingwherethechangesinthedatamodelareautomaticallypropagatedtotheViewlayer,butnotviceversa.Rapidprototyping:IntheWeb,thisisatechniqueofeasilyandrapidlybuildingthemockupsoftheuserinterface,includingsomebasicuserinteraction.Reactivity:IntheWeb,thisisactuallytheimmediatepropagationofanychangesofdatatotheViewlayer.Two-waydatabinding:ThisisthetypeofdatabindingwherethechangesinthedatamodelareautomaticallypropagatedtotheViewlayer,andthechangesthathappenintheViewlayerareimmediatelyreflectedinthedatamodel.Userinterface(UI):Thisisasetofvisualcomponentsthatallowtheusertocommunicatewiththeapplication.Vuex:ThisisanarchitectureforVueapplicationsandallowssimplemanagementoftheapplicationstate.
Vue.jshistoryWhen,EvanYou,Vue.jscreator(http://evanyou.me/),wasworkingatGoogleCreativeLabsononeoftheprojects,theyneededtofastprototypearatherbigUIinterface.WritingalotofrepeatedHTMLwasclearlytime-andresource-consuming,andthat'swhyEvanstartedlookingforsomealreadyexistingtoolforthispurpose.Tohissurprise,hediscoveredthattherewasnotool,library,orframeworkthatcouldfitexactlyintothepurposeofrapidprototyping!Atthattime,Angularwaswidelyused,React.jswasjuststarting,andframeworkssuchasBackbone.jswereusedforlarge-scaleapplicationswithMVCarchitecture.ForthekindofprojectthatneededsomethingreallyflexibleandlightweightjustforquickUIprototyping,neitherofthesecomplexframeworkswereadequate.
Whenyourealizethatsomethingcooldoesnotexistandyouareabletocreateit—justdoit!
Note
Vue.jswasbornasatoolforrapidprototyping.Nowitcanbeusedtobuildcomplexscalablereactivewebapplications.
ThatwaswhatEvandid.Thatishowhecametotheideaofcreatingalibrarythatwouldhelpinrapidprototypingbyofferinganeasyandflexiblewayofreactivedatabindingandreusablecomponents.
Likeeverygoodlibrary,Vue.jshasbeengrowingandevolving,thusprovidingmorefeaturesthanitwaspromisingfromthebeginning.Currently,itprovidesaneasywayofattachingandcreatingplugins,writingandusingmixins,andoveralladdingcustombehavior.Vuecanbeusedinsuchaflexiblewayandissononopinionatedoftheapplicationstructuringthatitdefinitelycanbeconsideredasaframeworkcapableofsupportingtheend-to-endbuildingofcomplexwebapplications.
ThemostimportantthingaboutVue.jsVue.jsallowsyoutosimplybindyourdatamodelstotherepresentationlayer.Italsoallowsyoutoeasilyreusecomponentsthroughouttheapplication.
Youdon'tneedtocreatespecialmodelsorcollectionsandtoregistereventsobjectinthere.Youdon'tneedtofollowsomespecialsyntax.Youdon'tneedtoinstallanyofnever-endingdependencies.
YourmodelsareplainJavaScriptobjects.TheyarebeingboundtowhateveryouwantinyourViews(text,inputtext,classes,attributes,andsoon),anditjustworks.
Youcansimplyaddthevue.jsfileintoyourprojectanduseit.Alternatively,youcanusevue-cliwithWebpackandBrowserifyfamily,whichnotonlybootstrapsthewholeprojectbutalsosupportshotreloadingandprovidesdevelopertools.
YoucanseparatetheViewlayerfromstylesandJavaScriptlogicoryoucanputitalltogetherintothesameVuefileandbuildyourcomponents'structureandlogicinthesameplace.ThereispluginsupportforallmodernandcommonlyusedIDEs.
Youcanusewhateverpreprocessorsyouwant,andyoucanuseES2015.Youcanuseitalongsideyourfavoriteframeworkyouhavebeendevelopingin,oryoucanuseititself.Youcanuseitjusttoaddasmallfunctionality,oryoucanusethewholeVueecosystemtobuildcomplexapplications.
Ifyouwanttocheckhowitcomparestootherframeworks,suchasAngularorReact,thenpleasevisithttp://vuejs.org/guide/comparison.html.
IfyouwanttocheckoutalltheamazingthingsaboutVue.js,thenyouaremorethanwelcometovisithttps://github.com/vuejs/awesome-vue.
Let'sgoshopping!Idon'tknowhowbutIcanfeelthatyourweekendiscloseandthatyouarestartingtothinkaboutgoingshoppingtobuytheneededgroceriesforthenextweek.Unlessyouareageniuswhoisabletomaintainthewholelistinyourheadoryouareamodestpersonwhodoesnotneedsomuch,youprobablymakeashoppinglistbeforegoingshopping.Maybeyouevenusesomeappforthat.Now,Iaskyou:whynotuseyourownapp?Howdoyoufeelaboutcreatinganddesigningit?Let'sdothat!Let'screateourownshoppinglistapplication.Let'sstartbycreatingarapidprototypeforit.It'sareallyeasytask—buildaninteractiveprototypefortheshoppinglist.
Itshouldshowthelistandallowustoaddandremovetheitems.Actually,it'sverysimilartoaToDolist.Let'sstartdoingitusingclassicHTML+CSS+JS+jQueryapproach.WewillalsousetheBootstrapframework(http://getbootstrap.com/)tomakethingsalittlebitmorebeautifulwithouthavingtowriteextensiveCSScode.(Yes,becauseourbookisnotaboutCSSandbecausemakingthingswithBootstrapissocrazilyeasy!)
ImplementingashoppinglistusingjQueryProbably,yourcodewillenduplookingassomethinglikethefollowing:
HereistheHTMLcode:
<divclass="container">
<h2>MyShoppingList</h2>
<divclass="input-group">
<inputplaceholder="addshoppinglistitem"
type="text"class="js-new-itemform-control">
<spanclass="input-group-btn">
<button@click="addItem"class="js-addbtnbtn-default"
type="button">Add!</button>
</span>
</div>
<ul>
<li>
<divclass="checkbox">
<label>
<inputclass="js-item"name="list"
type="checkbox">Carrot
</label>
</div>
</li>
<li>
<divclass="checkbox">
<label>
<inputclass="js-item"name="list"type="checkbox">Book
</label>
</div>
</li>
<liclass="removed">
<divclass="checkbox">
<label>
<inputclass="js-item"name="list"type="checkbox"
checked>Giftforaunt'sbirthday
</label>
</div>
</li>
</ul>
</div>
HereistheCSScode:
.container{
width:40%;
margin:20pxauto0pxauto;
}
.removed{
color:gray;
}
.removedlabel{
text-decoration:line-through;
}
ulli{
list-style-type:none;
}
HereistheJavaScript/jQuerycode:
$(document).ready(function(){
/**
*Addbuttonclickhandler
*/
functiononAdd(){
var$ul,li,$li,$label,$div,value;
value=$('.js-new-item').val();
//validateagainstemptyvalues
if(value===''){
return;
}
$ul=$('ul');
$li=$('<li>').appendTo($ul);
$div=$('<div>')
.addClass('checkbox')
.appendTo($li);
$label=$('<label>').appendTo($div);
$('<input>')
.attr('type','checkbox')
.addClass('item')
.attr('name','list')
.click(toggleRemoved)
.appendTo($label);
$label
.append(value);
$('.js-new-item').val('');
}
/**
*Checkboxclickhandler-
*togglesclassremovedonliparentelement
*@paramev
*/
functiontoggleRemoved(ev){
var$el;
$el=$(ev.currentTarget);
$el.closest('li').toggleClass('removed');
}
$('.js-add').click(onAdd);
$('.js-item').click(toggleRemoved);
});
Tip
DownloadingtheexamplecodeDetailedstepstodownloadthecodebundlearementionedinthePrefaceofthisbook.ThecodebundleforthebookisalsohostedonGitHubathttps://github.com/PacktPublishing/Learning-Vue.js-2.Wealsohaveothercodebundlesfromourrichcatalogofbooksandvideosavailableathttps://github.com/PacktPublishing/.Checkthemout!
Ifyouopenthepageinabrowser,youwillprobablyseesomethinglikethefollowing:
ShoppinglistimplementationusingtheHTML+CSS+jQueryapproach
PleasehavealookatJSFiddleathttps://jsfiddle.net/chudaol/u5pcnLw9/2/.
Asyoucansee,itisaverybasicpieceofHTMLcodethatcontainsanunorderedlistofelements,whereeachelementispresentedwithacheckboxandatext—aninputfortheusertextandtheAdd!button.EachtimetheAdd!buttonisclicked,thecontentofthetextinputistransformedintoalistentryandappendedtothelist.Whenthecheckboxofanyitemisclicked,thestateofanentryistoggledfromtotobuy(unchecked)tobought(checked).
Let'salsoaddafeaturethatallowsustochangethetitleofthelist(itmightbecomeusefulifweendupimplementingmultipleshoppinglistsintheapplication).
So,wewillendupwithsomeextramarkupandsomemorejQueryeventlistenersandhandlers:
<divclass="container">
<h2>MyShoppingList</h2>
<!--...-->
<divclass="footer">
<hr/>
<em>Changethetitleofyourshoppinglisthere</em>
<inputclass="js-change-title"type="text"
value="MyShoppingList"/>
</div>
</div>
//Andjavascriptcode:
functiononChangeTitle(){
$('h2').text($('.js-change-title').val());
}
$('.js-change-title').keyup(onChangeTitle);
CheckJSFiddleathttps://jsfiddle.net/chudaol/47u38fvh/3/.
ImplementingashoppinglistusingVue.jsThiswasaverysimpleexample.Let'strytoimplementitstep-by-stepusingVue.js.Thereareplentyofwaysofincludingvue.jsintoyourproject,butinthischapter,wewillincludeitjustbyaddingtheJavaScriptVuefilefromtheCDN:
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js">
</script>
So,let'sstartbyrenderingalistofelements.
CreatetheHTMLfileandaddthefollowingmarkup:
<divid="app"class="container">
<h2>{{title}}</h2>
<ul>
<li>{{items[0]}}</li>
<li>{{items[1]}}</li>
</ul>
</div>
NowaddthefollowingJavaScriptcode:
vardata={
items:['Bananas','Apples'],
title:'MyShoppingList'
};
newVue({
el:'#app',
data:data
});
Openitinthebrowser.Youwillseethatthelistisrendered:
ShoppinglistimplementedusingVue.js
Let'sanalyzethisexample.TheVueapplicationcodestartswiththenewVuekeyword.Howdowebindthepieceofmarkuptotheapplicationdata?WepasstotheVueinstancetheDOMelementthatmustbeboundtoit.AnyothermarkupinthepagewillnotbeaffectedandwillnotrecognizeVue'smagic.
Asyoucansee,ourmarkupiswrappedintothe#appelementandispassedasafirstargumentinthemapofVueoptions.Thedataargumentcontainstheobjectsthatarebeingusedinsidethemarkupusingdoublecurlybrackets({{}}).Youwillprobablyfindthisannotationveryeasytounderstandifyouarefamiliarwithtemplatingpreprocessors(forexample,handlebars;formoreinformation,visithttp://handlebarsjs.com/).
Sowhat?—you'reprobablyexclaiming.Whatareyougoingtoteachme?Howtousetemplatingpreprocessors?Thankyouverymuch,butIwouldbebetteroffhavingsomebeersandwatchingfootball.
Stop,dearreader,don'tgo,justgrabyourbeerandlet'scontinueourexample.You'llseethatit'llbelotsoffun!
AnalyzingdatabindingusingdevelopertoolsLet'sseedatabindinginaction.Openyourbrowser'sdevelopertools,findyourJavaScriptcode,andaddabreakpointatthestartofthescript.NowanalyzehowthedataobjectslookbeforeandaftertheVueapplicationisinitialized.Yousee,itchangedalot.Nowthedataobjectispreparedtothereactivedatabinding:
ThedataobjectbeforeandaftertheVueobjectinitialization
Nowifwechangethetitlepropertyofthedataobjectfromthedevelopertoolsconsole(wecandoitbecauseourdataisaglobalobject),itwillbereflectedautomaticallyinthetitleonthepage:
Databinding:changingobjectpropertiesaffectstheViewimmediately
Bringinguserinputtothedatawithtwo-waybindingSo,inourexample,wewereabletobringthedatafromtheplainJavaScriptdatamodeltothepage.Weprovideditasortofaflightfromtheapplicationcodetothepage.Don'tyouthinkthatitwouldbeniceifwecouldofferatwo-wayflighttoourdata?
Let'sseenowhowwecanachievetwo-waydatabindingandhowwecanchangethevalueofadatapropertyfromthepage.
CopytheHTMLmarkupforthetitle,changetheinputfromthefirstjQueryexample,andaddtheattributev-model="title"totheinputelement.
Tip
HaveyoualreadyheardaboutdirectivesinVue.js?Congratulations,you'vejustusedone!Actually,thev-modelattributeisadirectiveofVue.jsthatprovidestwo-waydatabinding.YoucanreadmoreaboutitattheofficialVuepage:http://vuejs.org/api/#v-model.
Now,theHTMLcodeforourshoppinglistapplicationcodelookslikethefollowing:
<divid="app"class="container">
<h2>{{title}}</h2>
<ul>
<li>{{items[0]}}</li>
<li>{{items[1]}}</li>
</ul>
<divclass="footer">
<hr/>
<em>Changethetitleofyourshoppinglisthere</em>
<inputv-model="title"/>
</div>
</div>
Andthat'sit!
Refreshthepagenowandmodifytheinput.You'llseethetitleautomaticallybeingupdatedasyoutype:
Databinding:changingthetextboundtothemodel'spropertyaffectsthetextboundtothesamepropertyimmediately
So,everythingisnice;however,thisexamplejustgrabsthetwoitemelementsandrendersthemaslistitems.Wewantittorenderthelistofitemsindependentlyofthelistsize.
Renderingthelistofitemsusingthev-fordirectiveSo,weneedsomemechanismtoiteratethroughtheitemsarrayandtorendereachiteminour<ul>element.
Fortunately,Vue.jsprovidesuswithanicedirectiveforiteratingthroughiterativeJavaScriptdatastructures.Itiscalledv-for.Wewilluseitinthelistitem<li>element.Modifythemarkupofthelistsothatitlookslikethefollowing:
<ul>
<liv-for="iteminitems">{{item}}</li>
</ul>
Note
Youwilllearnothernicedirectivessuchasv-if,v-else,v-show,v-on,v-bind,andsooninthisbook,sokeepreading.
Refreshthepageandhavealook.Thepageremainsthesame.Now,trytopushanitemintothearrayofitemsfromthedevelopertoolsconsole.Trytopopthemaswell.Youwillnotbesurprisedtoseethattheitemsarraymanipulationsareimmediatelyreflectedonthepage:
Databinding:changinganarrayaffectslistsbasedonitimmediately
So,nowwehavealistofitemsthatisrenderedonapagewithjustonelineofthemarkup.However,westillneedtheseitemstohaveacheckboxthatallowsustocheckthealreadyboughtitemsoruncheckthemwhenneeded.
CheckanduncheckshoppinglistitemsToachievethisbehavior,let'sslightlymodifyouritemsarraybychangingourstringitemsandtransformingthemintotheobjectswithtwoproperties,textandchecked(toreflectthestate),andlet'smodifythemarkuptoaddacheckboxtoeachitem.
SoourJavaScriptcodeforthedatadeclarationwilllooklikethefollowing:
vardata={
items:[{text:'Bananas',checked:true},
{text:'Apples',checked:false}],
title:'MyShoppingList',
newItem:''
};
Andourlistmarkupwilllooklikethis:
<ul>
<liv-for="iteminitems"v-bind:class="{'removed':
item.checked}">
<divclass="checkbox">
<label>
<inputtype="checkbox"v-model="item.checked">{{
item.text}}
</label>
</div>
</li>
</ul>
Refreshthepageandcheckthatthecheckedpropertyoftheitemscheckbox,andtheremovedclassofeachlistitem,<li>,isboundtothecheckedBooleanstateoftheitems.Playaroundandtrytoclickcheckboxestoseewhathappens.Isn'titnicethatjustwithtwodirectivesweareabletopropagatethestateoftheitemsandchangetheclassofthecorresponding<li>HTMLelement?
Addingnewshoppinglistitemsusingthev-ondirectiveSonowwejustneedasmalladditiontoourcodetobeabletoactuallyaddshoppinglistitems.Toachievethat,wewilladdonemoreobjecttoourdataandcallitnewItem.We'llalsoaddasmallmethodthatpushesnewitemtotheitemsarray.Andwe'llcallthismethodfromthemarkuppageusingthev:ondirectiveusedontheHTMLinputelementthatwillbeusedforthenewitemandonthebuttonusedtoclicktoaddanewitem.
SoourJavaScriptcodewilllooklikethefollowing:
vardata={
items:[{text:'Bananas',checked:true},
{text:'Apples',checked:false}],
title:'MyShoppingList',
newItem:''
};
newVue({
el:'#app',
data:data,
methods:{
addItem:function(){
vartext;
text=this.newItem.trim();
if(text){
this.items.push({
text:text,
checked:false
});
this.newItem='';
}
}
}
});
WeaddedanewpropertytothedataobjectcallednewItem.ThenweaddedanewsectioncalledmethodstoourVueinitializationoptionsobjectandaddedtheaddItemmethodtothissection.Allthedatapropertiesareaccessibleinthemethodssectionviathethiskeyword.Thus,inthismethod,wejustgetthis.newItemandpushitintothethis.itemsarray.Nowwemustbindthecalltothismethodtosomeuseraction.Asithasalreadybeenmentioned,wewillusethev-ondirectiveandapplyittotheenterkeyuponthenewiteminputandtotheAdd!buttonclick.
Soaddthefollowingmarkupbeforeourlistofitems:
<divclass="input-group">
<inputv-model="newItem"v-on:keyup.enter="addItem"
placeholder="addshoppinglistitem"type="text"class="form-
control">
<spanclass="input-group-btn">
<buttonv-on:click="addItem"class="btnbtn-default"
type="button">Add!</button>
</span>
</div>
Note
Thev-ondirectiveattachesaneventlistenertotheelements.Theshortcutisthe@sign.So,insteadofwritingv-on:keyup="addItem",youcanwrite@keyup="addItem".Youcanreadmoreaboutthev-ondirectiveontheofficialdocumentationsiteathttp://vuejs.org/api/#v-on.
Let'sfinalize.Thewholecodenowlookslikethefollowing:
HereistheHTMLcode:
<divid="app"class="container">
<h2>{{title}}</h2>
<divclass="input-group">
<inputv-model="newItem"@keyup.enter="addItem"
placeholder="addshoppinglistitem"type="text"
class="form-control">
<spanclass="input-group-btn">
<button@click="addItem"class="btnbtn-default"
type="button">Add!</button>
</span>
</div>
<ul>
<liv-for="iteminitems":class="{'removed':item.checked
}">
<divclass="checkbox">
<label>
<inputtype="checkbox"v-model="item.checked">{{
item.text}}
</label>
</div>
</li>
</ul>
<divclass="footerhidden">
<hr/>
<em>Changethetitleofyourshoppinglisthere</em>
<inputv-model="title"/>
</div>
</div>
HereistheJavaScriptcode:
vardata={
items:[{text:'Bananas',checked:true},
{text:'Apples',checked:false}],
title:'MyShoppingList',
newItem:''
};
newVue({
el:'#app',
data:data,
methods:{
addItem:function(){
vartext;
text=this.newItem.trim();
if(text){
this.items.push({
text:text,
checked:false
});
this.newItem='';
}
}
}
});
Here'salinktoJSFiddle:https://jsfiddle.net/chudaol/vxfkxjzk/3/.
UsingVue.jsinanexistingprojectIcanfeelnowthatyouhaveseenhoweasyistobindthepropertiesofthemodeltothepresentationlayerandyouarealreadystartingtothinkabouthowitcanbeusedinyourexistingproject.Butthenyouthink:hell,no,Ineedtoinstallsomethings,runnpminstall,changetheproject'sstructure,adddirectives,andchangethecode.
AndhereIcantellyou:no!Noinstalls,nonpms,justgrabthevue.jsfile,insertitintoyourHTMLpage,anduseit.That'sall,nostructurechanges,noarchitecturaldecisions,nodiscussions.Justuseit.IwillshowyouhowweuseditatEdEra(https://www.ed-era.com)toincludeasmall"checkyourself"functionalityattheendofaGitBookchapter.
EdEraisaUkraine-basedonlineeducationalprojectwhoseaimistotransformthewholeeducationalsystemintosomethingmodern,online,interactive,andfun.Actually,Iamaco-founderandthechieftechnicalofficerofthisyoungniceproject,beingresponsibleforthewholetechnicalpartofthething.So,inEdEra,wehavesomeonlinecoursesbuiltontopoftheopenEdXplatform(https://open.edx.org/)andsomeinteractiveeducationalbooksbuiltontopofthegreatGitBookframework(http://www.gitbook.org).Basically,GitBookisaplatformbasedontopoftheNode.jstechnologystack.ItallowssomeonewithbasicknowledgeofthemarkdownlanguageandbasicGitcommandstowritebooksandhostthemintheGitBookservers.EdEra'sbookscanbefoundathttp://ed-era.com/books(beware,theyareallinUkrainian).
Let'sseewhatwehavedoneinourbooksusingVue.js.
Atsomepoint,IdecidedtoincludeasmallquizattheendofthechapteraboutpersonalpronounsinthebookthatteachesEnglish.Thus,I'veincludedthevue.jsJavaScriptfile,editedthecorresponding.mdfile,andincludedthefollowingHTMLcode:
<divid="pronouns">
<p><strong>Checkyourself:)</strong></p>
<textareaclass="textarea"v-model="text"v-
on:keyup="checkText">
{{text}}
</textarea><iv-bind:class="{'correct':correct,
'incorrect':!correct}"></i>
</div>
ThenIaddedacustomJavaScriptfile,whereI'veincludedthefollowingcode:
$(document).ready(function(){
varinitialText,correctText;
initialText='MeissadbecauseheismorecleverthanI.';
correctText='Iamsadbecauseheismorecleverthanme.';
newVue({
el:'#pronouns',
data:{
text:initialText,
correct:false
},
methods:{
checkText:function(){
vartext;
text=this.text.trim();
this.correct=text===correctText;
}
}
});
});
Note
YoucancheckthiscodeatthisGitHubpage:https://github.com/chudaol/ed-era-book-english.Here'sacodeofapagewritteninmarkdownwithinsertedHTML:https://github.com/chudaol/ed-era-book-english/blob/master/2/osobovi_zaimenniki.md.Andhere'saJavaScriptcode:https://github.com/chudaol/ed-era-book-english/blob/master/custom/js/quiz-vue.js.Youcanevenclonetherepositoryandtryitlocallyusinggitbook-cli(https://github.com/GitbookIO/gitbook/blob/master/docs/setup.md).
Let'shavealookatthiscode.Youhaveprobablyalreadydetectedthepartsthatyouhavealreadyseenandeventried:
Thedataobjectcontainstwoproperties:ThestringpropertytextTheBooleanpropertycorrect
ThecheckTextmethodjustgrabsthetextproperty,comparesitwiththecorrecttext,andassignsthevaluetothecorrectvalueThev-ondirectivecallsthecheckTextmethodonkeyupThev-binddirectivebindstheclasscorrecttothecorrectproperty
HereishowthecodelooksinmyIDE:
UsingVueinamarkdown-drivenproject
Andnextiswhatitlookslikeinthebrowser:
Vue.jsinactioninsidetheGitBookpage
Vue.jsinactioninsidetheGitBookpage
Checkitoutathttp://english.ed-era.com/2/osobovi_zaimenniki.html.
Amazing,right?Prettysimple,prettyreactive!
Vue.js2.0!Atthetimeofwriting,Vue.js2.0wasannounced(https://vuejs.org/2016/04/27/announcing-2.0/).Checkthefollowinglinksinthisregard:
http://www.infoworld.com/article/3063615/javascript/vuejs-lead-our-javascript-framework-is-faster-than-react.htmlhttps://www.reddit.com/r/vuejs/comments/4gq2r1/announcing_vuejs_20/
ThesecondversionofVue.jshassomeconsiderabledifferencescomparingtoitspredecessor,startingfromthewaythatdatabindingisbeinghandledandmovingtoitsAPI.ItuseslightweightvirtualDOMimplementationforrendering,supportsserver-siderendering,andisfasterandleaner.
Atthetimeofwriting,Vue2.0wasinanearlyalphastage.Donotworry,though.AlltheexamplesthatwewillcoverinthisbookarebasedonthelateststableversionofVue2.0andarefullycompatiblewiththeboththeversions.
ProjectsusingVue.jsProbably,atthistime,youarewonderingwhatprojectsareouttherethatarebuiltontopofVue.js,oruseitasapartoftheircodebase.Therearealotofniceopensource,experimental,andenterpriseprojectsusingit.Thecompleteandconstantlyupdatedlistoftheseprojectscanbefoundathttps://github.com/vuejs/awesome-vue#projects-using-vuejs.
Let'shavealookatsomeofthem.
GrammarlyGrammarly(https://www.grammarly.com/)isaservicethathelpsyouwriteEnglishcorrectly.Ithasseveralapps,oneofthemisasimpleChromeextensionthatjustchecksanytextinputthatyoufillin.Anotheroneisanonlineeditorthatyoucanusetocheckbigchunksoftext.ThiseditorisbuiltusingVue.js!ThefollowingisascreenshotofthistextbeingeditedintheonlineeditorofGrammarly:
Grammarly:aprojectthatisbuiltontopofVue.js
OptimizelyOptimizely(https://www.optimizely.com/)isaservicethathelpsyoutest,optimize,andpersonalizeyourwebsites.I'veusedthePacktsitetocreateanOptimizelyexperimentandtocheckoutVue.jsinactioninthisresource.Itlookslikethefollowing:
Optimizely:aprojectthatisbuiltontopofVue.js
Hoveringaroundwiththemousegivesusthepossibilityofopeningacontextmenuthatallowsdifferentmanipulationswiththepagedata,includingthesimplestone,textediting.Let'strythisone:
UsingOptimizelyandwatchingVue.jsinaction
Thetextboxisopened.WhenItypeinit,thetextinthetitleisreactivelychanged.WesawandimplementeditusingVue.js:
UsingOptimizelyandwatchingVue.jsinaction
FilterBlendFilterBlend(https://github.com/ilyashubin/FilterBlend)isanopensourceplaygroundfortheCSSbackground-blend-modeandfilterproperties.
Youcanloadyourimagesandcombineblendingwithfilters.
IfyouwanttogiveFilterBlendatry,youcaninstallitlocally:
1. Clonetherepository:
gitclonehttps://github.com/ilyashubin/FilterBlend.git
2. EntertheFilterBlenddirectory:
cdFilterBlend
3. Installthedependencies:
npminstall
4. Runtheproject:
gulp
Openyourbrowseronlocalhost:8000andplayaround.Youcanseethatonceyouchangesomethinginthemenuontheright,itisimmediatelypropagatedtotheimagesontheleftside.AllthisfunctionalityisachievedusingVue.js.CheckthecodeonGitHub.
FilterBlend:aprojectbuiltontopofVue.js
PushSilverPushSilver(https://pushsilver.com)isaniceandsimpleserviceforbusypeopletocreatesimpleinvoices.Itallowscreatinginvoices,sendingandresendingthemtotheclients,andkeepingtrackingofthem.Itwascreatedbyadeveloperdoingfreelanceconsultancyandbeingtiredofhavingtocreateinvoiceseachtimeforeachsmallproject.ThistoolworkswellanditwasbuiltusingVue.js:
PushSilver:invoicemanagingapplicationbuiltontopofVue.js
PushSilver:invoicemanagingapplicationbuiltontopofVue.js
BookroadmapThisbook,likemostpartoftechnicalbooks,isorganizedinsuchawaythatyoudonotneedtoreaditfrombeginningtoend.Youcanchoosethepartsthatinterestyouthemostandskiptherest.
Thisbookisorganizedasfollows:
Ifyouarereadingthis,there'snoneedtospecifywhatisgoingoninthefirstchapter.Chapter2,Fundamentals-InstallingandUsing,isprettytheoreticalandwillexplainwhat'sgoingonbehindthescenesofVue.jsanditsmainparts.So,ifyouarenotintotheoryandwanttoputyourhandsintocoding,youarefreetoskipthispart.Inthispart,wewillalsogothroughtheinstallationandsetupprocess.Fromthethirdtotheeighthchapter,we'llexplorethemainfeaturesofVue.jswhilebuildingtheapplication:
InChapter3,Components-UnderstandingandUsing,wewillintroduceVuecomponentsandapplythisknowledgetoourapplication.InChapter4,Reactivity-BindingDatatoYourApplication,wewilluseallthedatabindingmechanismsprovidedbyVue.InChapter5,Vuex-ManagingStateinYourApplication,wewillintroducetheVuexstatemanagementsystemandexplainhowtouseitinourapplications.InChapter6,Plugins-BuildingYourHousewithYourOwnBricks,wewilllearnhowtocreateandusepluginsforVueapplicationstoenrichtheirfunctionality.InChapter7,Testing-TimetoTestWhatWeHaveDonesoFar!,wewillcoverandexplorecustomdirectivesofVue.jsandcreatesomeinourapplication.InChapter8,Deploying-TimetoGoLive!,wewilllearnhowtotestanddeployJavaScriptapplicationwritteninVue.js.
InChapter9,WhatIsNext?,we'llsummarizewhatwe'velearnedandseewhatwecandonext.
Let'smanagetime!Atthispointoftime,Ialreadyknowthatyouareso,so,soenthusiasticwiththisbookthatyouwanttoreadittotheendwithoutstopping.Butthisisnotright.Weshouldmanageourtimeandgiveussometimetoworkandsometimetorest.Let'screateasmallapplicationthatimplementsaPomodorotechniquetimersothatitcanhelpusinourworkingtimemanagement.
Note
ThePomodorotechniqueisatimemanagementtechniquenamedafterthekitchentomatotimer(infact,PomodoromeanstomatoinItalian).Thistechniqueconsistsofbreakingdowntheworkingtimeintosmallintervalsseparatedbyshortbreaks.ReadmoreaboutthePomodorotechniqueontheofficialsite:http://pomodorotechnique.com/.
Thus,ourgoalisverysimple.Wejusthavetocreateaverysimpletimecounterthatwilldecrementuntilltheendoftheworkingintervalandthenrestartanddecrementtilltheendoftherestingtimeandsoon.
Let'sdothat!
WewillintroducetwoVuedatavariables,minuteandsecond,whichwillbedisplayedonourpage.Themainmethodoneachsecondwilldecrementsecond;itwilldecrementminutewhensecondbecomes0;andwhenbothminuteandsecondvariablescometo0,theapplicationshouldtogglebetweenworkingandrestinginterval:
OurJavaScriptcodewilllooklikethefollowing:
constPOMODORO_STATES={
WORK:'work',
REST:'rest'
};
constWORKING_TIME_LENGTH_IN_MINUTES=25;
constRESTING_TIME_LENGTH_IN_MINUTES=5;
newVue({
el:'#app',
data:{
minute:WORKING_TIME_LENGTH_IN_MINUTES,
second:0,
pomodoroState:POMODORO_STATES.WORK,
timestamp:0
},
methods:{
start:function(){
this._tick();
this.interval=setInterval(this._tick,1000);
},
_tick:function(){
//ifsecondisnot0,justdecrementsecond
if(this.second!==0){
this.second--;
return;
}
//ifsecondis0andminuteisnot0,
//decrementminuteandsetsecondto59
if(this.minute!==0){
this.minute--;
this.second=59;
return;
}
//ifsecondis0andminuteis0,
//toggleworking/restingintervals
this.pomodoroState=this.pomodoroState===
POMODORO_STATES.WORK?POMODORO_STATES.REST:
POMODORO_STATES.WORK;
if(this.pomodoroState===POMODORO_STATES.WORK){
this.minute=WORKING_TIME_LENGTH_IN_MINUTES;
}else{
this.minute=RESTING_TIME_LENGTH_IN_MINUTES;
}
}
}
});
InourHTMLcode,let'screatetwoplaceholdersforminuteandsecond,andastartbuttonforourPomodorotimer:
<divid="app"class="container">
<h2>
<span>Pomodoro</span>
<button@click="start()">
<iclass="glyphiconglyphicon-play"></i>
</button>
</h2>
<divclass="well">
<divclass="pomodoro-timer">
<span>{{minute}}</span>:<span>{{second}}</span>
</div>
</div>
</div>
Again,weareusingBootstrapforthestyling,soourPomodorotimerlookslikethefollowing:
CountdowntimerbuiltwithVue.js
OurPomodoroisnice,butithassomeproblems:
Firstofall,wedon'tknowwhichstateisbeingtoggled.Wedon'tknowifweshouldworkorrest.Let'sintroduceatitlethatwillchangeeachtimethePomodorostateischanged.Anotherproblemisinconsistentdisplayofminutesandsecondsnumbers.Forexample,for24minutesand5seconds,wewouldliketosee24:05andnot24:5.Let'sfixitintroducingcomputedvaluesinourapplicationdataanddisplayingtheminsteadofnormalvalues.Yetanotherproblemisthatourstartbuttoncanbeclickedoverandoveragain,whichcreatesatimereachtimeit'sclicked.Trytoclickitseveraltimesandseehowcrazyyourtimergoes.Let'sfixitbyintroducingstart,pause,andstopbuttons,applyapplicationstatestothem,anddisablebuttonstothestateaccordingly.
TogglethetitlebyusingcomputedpropertiesLet'sstartbyfixingthefirstproblembycreatingcomputedpropertytitleandusingitinourmarkup.
Note
Computedpropertiesarethepropertiesinsidethedataobjectthatallowustoavoidblowingupthetemplatewithsomeextralogic.Youcanfindmoreinformationaboutcomputedpropertiesontheofficialdocumentationsite:http://vuejs.org/guide/computed.html.
AddthecomputedsectionintheVueoptionsobjectandaddthepropertytitlethere:
data:{
//...
},
computed:{
title:function(){
returnthis.pomodoroState===POMODORO_STATES.WORK?'Work!':
'Rest!'
}
},
methods:{
//...
AndnowjustusethefollowingpropertyasitwasanormalVuedatapropertyinyourmarkup:
<h2>
<span>Pomodoro</span>
<!--!>
</h2>
<h3>{{title}}</h3>
<divclass="well">
Andvoilà!NowwehaveatitlethatchangeseachtimethePomodorostateisbeingtoggled:
Automaticchangeofthetitlebasedonthestateofthetimer
Nice,isn'tit?
Left-padtimevaluesusingcomputedpropertiesNowlet'sapplythesamelogicforleftpaddingourminuteandsecondnumbers.Let'saddtwocomputedproperties,minandsec,inourcomputedsectioninthedataoptionsandapplythesimplealgorithmtopadthenumberswith0ontheleft.Ofcourse,wecoulduseafamousleft-padproject(https://github.com/stevemao/left-pad),buttokeepthingssimpleandnottobreakthewholeInternet(http://www.theregister.co.uk/2016/03/23/npm_left_pad_chaos/),let'sapplyasimplelogicofourown:
computed:{
title:function(){
returnthis.pomodoroState===POMODORO_STATES.WORK?'Work!':
'Rest!'
},
min:function(){
if(this.minute<10){
return'0'+this.minute;
}
returnthis.minute;
},
sec:function(){
if(this.second<10){
return'0'+this.second;
}
returnthis.second;
}
}
Andlet'susethesepropertiesinsteadofminuteandsecondinourHTMLcode:
<divclass="pomodoro-timer">
<span>{{min}}</span>:<span>{{sec}}</span>
</div>
Refreshapageandcheckhowbeautifulournumbersarenow:
LeftpaddingusingcomputedpropertiesinVue.js
Keepstatewithstart,pause,andstopbuttonsSo,tofixthethirdproblem,let'sintroducethreeapplicationstates,started,paused,andstopped,andlet'shavethreemethodsthatwouldallowustopermuteoverthesestates.Wealreadyhavethemethodthatstartstheapplication,sowejustaddthelogictheretochangethestatetostarted.Wealsoaddtwoadditionalmethods,pauseandstop,whichwouldpausethetimerandchangetothecorrespondingapplicationstate:
constPOMODORO_STATES={
WORK:'work',
REST:'rest'
};
constSTATES={
STARTED:'started',
STOPPED:'stopped',
PAUSED:'paused'
};
//<...>
newVue({
el:'#app',
data:{
state:STATES.STOPPED,
//<...>
},
//<...>
methods:{
start:function(){
this.state=STATES.STARTED;
this._tick();
this.interval=setInterval(this._tick,1000);
},
pause:function(){
this.state=STATES.PAUSED;
clearInterval(this.interval);
},
stop:function(){
this.state=STATES.STOPPED;
clearInterval(this.interval);
this.pomodoroState=POMODORO_STATES.WORK;
this.minute=WORKING_TIME_LENGTH_IN_MINUTES;
this.second=0;
},
//<...>
}
});
And,let'saddtwobuttonstoourHTMLcodeandaddtheclicklistenersthatcallthecorrespondingmethods:
<button:disabled="state==='started'"
@click="start()">
<iclass="glyphiconglyphicon-play"></i>
</button>
<button:disabled="state!=='started'"
@click="pause()">
<iclass="glyphiconglyphicon-pause"></i>
</button>
<button:disabled="state!=='started'&&state!=='paused'"
@click="stop()">
<iclass="glyphiconglyphicon-stop"></i>
</button>
Nowourapplicationlooksniceandallowsustostart,pause,andstopthetimer:
Togglingstart,stop,andpausebuttonsaccordingtotheapplicationstate
CheckwhatthewholecodelookslikeinJSFiddleathttps://jsfiddle.net/chudaol/b6vmtzq1/1/.
Aftersomuchworkandsomanyofnewtermsandknowledge,youcertainlydeserveakitten!Ialsolovekittens,sohereyouhavearandomkittenfromtheawesomesitehttp://thecatapi.com/:
ExerciseAttheendofthischapter,Iwouldliketoproposeasmallexercise.ThePomodorotimerthatwebuiltearlierinthechaptersis,withoutanydoubt,great,butitstilllackssomenicefeatures.Areallynicethingthatitcouldprovidewouldbeshowingrandomkittensfromhttp://thecatapi.com/duringrestingtime.Canyouimplementthis?Ofcourseyoucan!Butpleasedonotconfuserestingwithworkingtime!Iamalmostsurethatyourprojectmanagerwillnotlikeitmuchifyoustareatkittensinsteadofworking.
ThesolutiontothisexercisecanbefoundinAppendix,SolutionstoExercises.
SummaryIamverygladthatyouhavereachedthispoint,thismeansthatyoualreadyknowwhatVue.jsis,andifsomeoneasksyouwhetheritisatool,alibrary,oraframework,youcertainlywillfindananswer.YoualsoknowhowtostartanapplicationusingVue.jsandyouknowhowtouseVue'sfeaturesinanalreadyexistingproject.YouplayedaroundwithsomereallyniceprojectsthatarewritteninVue.jsandyoustarteddevelopingsomeofyourown!Nowyoudonotjustgoshopping,nowyougoshoppingwithashoppinglistcreatedbyyouusingVue.js!Nowyoudon'tneedtostealyourtomatotimerfromthekitchentouseitasaPomodorotimerwhileworking;youcanuseyourowndigitalPomodorotimermadewithVue.js.And,lastbutnottheleast,nowyoucaninsertrandomkittensinyourJavaScriptapplicationalsousingVue.js.
Inthenextchapter,wewillcoverthebehindthescenesofVue,howandwhydoesitwork,andthearchitecturalpatternsituses.Eachoftheconceptswillbewrappedupwithanexampletodemonstrateit.Thenwewillbereadytoputourhandsdeepintothecodeandtoimproveourapplicationstakingthemtothestateofawesomeness.
Chapter2.Fundamentals–InstallingandUsingInthepreviouschapter,wegainedsomefamiliaritywithVue.js.Wewereabletouseitintwodifferentapplicationsthatwecreatedfromscratch.WelearnedhowtointegrateVue.jsintoanalreadyexistingproject.WewereabletoseeVue'sreactivedatabindinginaction.
Now,youareprobablyaskingyourself:howdoesitwork?WhatdoesitdotoachievethisbehavioroffastUIchangeswhenthedatamodelischanged?Probably,youdecidedtouseVue.jsinyourprojectandarenowwonderingwhetheritfollowssomearchitecturalpatternorparadigmsothatyoushouldadoptitinyourproject.Inthischapter,wewillexplorethekeyconceptsoftheVue.jsframeworktounderstandallitsbehindthescenesfeatures.Alsointhischapter,wewillanalyzeallthepossiblewaysofinstallingVue.js.Wewillalsocreateaskeletonforourapplications,whichwewilldevelopandenhancethroughthenextchapters.Wewillalsolearnwaysofdebuggingandtestingourapplications.
So,inthischapter,wearegoingtolearn:
WhattheMVVMarchitectureparadigmisandhowitappliestoVue.jsWhatdeclarativeViewsareHowVue.jsexploresdefinedproperties,getters,andsettersHowreactivityanddatabindingworksinVue.jsWhatdirtychecking,DOM,andvirtualDOMareThemaindifferencesbetweenVue.js1.0andVue.js2.0WhatreusablecomponentsareHowplugins,directives,customplugins,andcustomdirectivesworkinVue.jsHowtoinstall,start,run,anddebugaVueapplication
MVVMarchitecturalpatternDoyourememberhowwewerecreatingtheVueinstanceinthefirstchapter?WewereinstantiatingitcallingnewVue({...}).Youalsorememberthatintheoptions,wewerepassingtheelementonthepagewherethisVueinstanceshouldbeboundandthedataobjectthatcontainedthepropertieswewantedtobindtoourView.ThedataobjectisourModelandtheDOMelementwheretheVueinstanceisboundisourView:
ClassicView-ModelrepresentationwheretheVueinstancebindsonetoanother
Inthemeantime,ourVueinstanceissomethingthathelpstobindourModeltotheViewandviceversa.OurapplicationthusfollowsModel-View-ViewModel(MVVM)pattern,wheretheVueinstanceisaViewModel:
ThesimplifieddiagramoftheModel-View-ViewModelpattern
OurModelcontainsdataandsomebusinesslogic,andourViewisresponsibleforitsrepresentation.ViewModelhandlesdatabinding,ensuringthatthedatachangedintheModelisimmediatelyaffectingtheViewlayerandviceversa.
OurViewsthusbecomecompletelydatadriven.ViewModelbecomesresponsibleforthecontrolofthedataflow,makingdatabindingfullydeclarativeforus.
DefineProperty,getters,andsettersSo,whathappenswiththedataoncepassedtotheVueinstance?WhatarethesetransformationsthatVueappliestoitsothatitbecomessoautomaticallyboundtotheViewlayer?
Let'sanalyzewhatwouldwedoifwehad,let'ssay,astring,andeverytimeitchangeswewouldliketoapplysometransformationstosomeDOMelement.Howwouldweapplythestring-changinglistenerfunction?Towhatwouldweattachit?ThereisnosuchthingasvarstringVar='hello';stringVar.onChange(doSomething).
Sowewouldprobablywrapthestring'svaluesettingandgettinginsomesortoffunctionthatwoulddosomething,forexample,updatingtheDOMeachtimethestringwasupdated.Howwouldyouimplementit?Whileyou'rethinkingaboutit,I'llprepareaquickdemoofsomethinginteresting.
Openthedevelopertoolsonyourshoppinglistapplication.Let'scodealittlebit.Createanobjvariableandanothertextvariable:
varobj={};
vartext='';
Let'sstoretheDOMelementh2inavariable:
varh2=document.getElementsByTagName('h2')[0];
Ifweassigntexttotheobj.textproperty,howcanweachievethatineverychangeofthispropertytheinnerHTMLofh2wouldchangeaswell?
Let'susetheObject.definePropertymethod(https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty).
Thismethodallowsthecreationofgetterandsetterfunctions,thusspecifyingwhatmusthappenwhenthepropertyisaccessedorchanged:
Object.defineProperty(obj,'text',{
get:function(){
returntext;
},
set:function(newVal){
text=newVal;
h2.innerHTML=text;
}
});
Nowtrytochangetheobj.textpropertyfromtheconsole.Lookatthetitle:
Thesetmethodofobject.definePropertyiscalledeverytimethepropertychanges
ThisexactmechanismwasusedbyVue.js.OncethedataarepassedtotheVueinstance,allitspropertiesgothroughtheObject.definePropertymethod,whichassignsreactivegettersandsetterstothem.Foreachdirectiveexistingonapage,awatcherisadded,whichisbeingnotifiedwithinthesetmethod.Openthevue.jscodeintheconsoleandsearchforthelinethatsaysset:functionreactiveSetter(newVal).Addabreakpointandtrytochangethetitleoftheshoppinglistintheinput.Nowstepoveruntilyoureachthelastcallinthisfunctionthatsaysdep.notify():
Breakpointinsidethesetterfunctionthatcallsthewatchersnotifymethod
Stepintothefunction.Youwillseethatthisfunctionisiteratingthroughthewatchersofthepropertyandupdatesthem.Ifyoustepoverthiscall,youwillseethattheDOMisnotbeingupdated.Thisisbecausetheupdatesperformedonthesameeventlooparebeingputintothequeuethatisbeingflushedperiodically.
FindtherunBatcherQueuefunctionandputabreakpointinsideit.Trytochangethetitleagain.Asyoucansee,thisfunctioniteratesthroughallthewatchersthatarewaitinginsidethequeueandcallstherunmethodoneachofthem.Ifyoustepintothismethod,youwillseethatitcomparesthenewvaluewiththepreviousone:
if(value!==this.value||...
Itthenitcallsacallback'sexecution:
this.cb.call(this.vm,value,oldValue);
Ifyoustepintothiscallbackfunction,youwillseethatintheend,itwillfinallyupdatethe
DOMvalue:
update:functionupdate(value){
this.el[this.attr]=_toString(value);
}
Isn'titsimple?
Note
InthisdebuggingVueversion1.0isused.
SothemechanismbehindtheVue.jsreactivedatabindingisverysimple.Watchersarebeingassignedtoallthedirectivesanddataproperties.Then,duringthesetmethodofObject.defineProperty,theWatchersarenotifiedand,inturn,theyupdatethecorrespondingDOMordata:
ThedataflowfromthedataobjecttotheDOM
DOMelementsthathavedirectiveshaveattachedlistenersthatlistentotheirupdatesandcallthecorrespondingdatapropertysetterthat,inturn,wakesupitsWatchers.
ComparingwithotherframeworksWhenyoutryanewtool,youwanttoknowhowitcompareswithothertoolsorframeworks.YoucanfindadeepanalysisinthisregardontheofficialpageofVue.js:http://vuejs.org/guide/comparison.html.IwilljustpointtosometopicsthatIfindimportantregardingthemostlyusedframeworks.
ReactReactandVuearereallysimilar.TheybothusevirtualDOM,havereusablecomponents,andareaboutreactivedata.Itisworthmentioning,however,thatVueonlyusesvirtualDOMstartingfromitssecondmajorversion.PriortoVue2.0,itusedrealDOM.TheVue2.0releasenotonlybecamemoreperformantthanVue1.0butitalsobecamemoreperformantthanReact(http://vuejs.org/guide/comparison.html#Performance-Profiles).
Themostsignificantdifferenceisprobablythewayyoucreateyourcomponentsinbothframeworks.YoumightalreadyknowthatinReact,everythingisJavaScript.Developingeverything,eventemplates,inJavaScript,canactuallybegood,soprogrammersarealwaysinthesamescopeandrenderingbecomesmoreflexible.
However,forsomedesignerswhowanttodorapidprototypingorfordeveloperswithnot-so-strongprogrammingskills,orforpeoplewhosimplydon'twanttolearnJSX,itmightbecomereallypainfultoworklikethis.InVuecomponents,youcanactuallyalsouseJSX,butyoucanstillfollowacommonwebdevelopmentstructure:writingCSSinsidethe<style>tags,writingHTMLcodeinsidethe<template>tags,andwritingthecomponent'slogicinJavaScriptinsidethe<script>tags.Compare,forexample,thetemplateinsidetherenderfunctioninReactandthetemplatethatyoucanwriteinsidetheVuecomponent.Inthisexample,Iwillshowhowtorenderthelistofitemsoftheshoppinglistthatwehaveseenbefore.SoinReact,youwillendupwiththeJSXcodesimilartothisone:
render(){
return(
<ul>
{items.map(item=>
<liclassName={item.checked&&'removed'}>
<divclassName='checkbox'>
<inputtype='checkbox'checked={item.checked}>
{item.text}
</div>
</li>
)}
</ul>
)
});
UsingVue,youwilljustwritethefollowingHTMLcodeinsidethetemplatetag:
<template>
<ul>
<liv-for="iteminitems":class="{'removed':item.checked}">
<divclass="checkbox">
<label>
<inputtype="checkbox"v-model="item.checked">{{item.text}}
</label>
</div>
</li>
</ul>
</template>
I,personally,liketohavethesethingsseparated,thusIfinditnicethatVueoffersthispossibility.
AnothernicethingaboutVueisthatitallowstoscopestylewithinthecomponentsusingthescopedattributeattachedtothestyletag:
<stylescoped>
</style>
Withinthisstyle,incaseyouusepreprocessors,youstillhaveaccesstoallgloballydefinedvariablesandcancreateorredefinestylesthatwillbeonlyaccessiblebythiscomponent.
It'salsoworthtomentionthelearningcurveforbothframeworks.TobeabletostartdevelopingapplicationsusingReact,youwouldprobablyhavetolearnJSXandES2105syntax,sincemostexamplesinofficialReactdocumentationuseit.WithVue,youcanstartoutoftheblue.Justincludeitinthepage,likeyouwoulddowithjQuery,andyoucanalreadyuseVuemodelsanddatabindingusingprettysimpleandeasytounderstandsyntax,andanyJavaScriptversionyouliketouse.Afterthat,youcanscaleupinyourlearningandinyourapplicationsstyle.
Incaseyouwanttoperformadeeperanalysisofbothframeworks,havealookatthedocumentation,trytoelaboratesimilarexamples,andcheckwhatsuitsyourneedsmore.
AngularThereisahugedifferencebetweenAngular1andAngular2.WeallknowthatthesecondversionofAngulariscompletelydifferentfromitspredecessor.Itoffersmoreperformance,theAPIisdifferent,andtheunderlyingimplementationhasbeenrewritten.
ThesetwoversionsaresodifferentthatinVueofficialdocumentation,youwillfindthecomparisonbetweenboththeAngularversionsasitwasbetweentwodifferentframeworks.However,thelearningcurveandthewayinwhicheachoftheframeworksforcesyoutostructuretheapplicationaretransversalforboththeAngularversions.ItturnsoutthatVueismuchlessopinionatedthanAngular1aswellasAngular2.JustcompareAngular'squickstartguideandVue'shelloworldapplicationsathttps://angular.io/docs/js/latest/quickstart.htmlandhttp://vuejs.org/guide/index.html#Hello-World.
"EvenwithoutTypeScript,Angular'sQuickstartguidestartsoutwithanappthatusesES2015JavaScript,NPMwith18dependencies,4files,andover3,000wordstoexplainitall-justtosayHelloWorld."
--http://vuejs.org/guide/comparison.html#Learning-Curve
IfyoustilluseAngular1,it'sworthtomentionthatthebigdifferencebetweenthisframeworkandVueisthatinthisversionofAngular,eachtimethescopechanged,re-evaluatedallthewatchers,thusperformingdirtychecking,hencereducingtheperformancewhentheamountofwatchersbecameconsiderablyhigh.Hence,inVue,whensomethinginthescopechanges,onlythisproperty'swatcherisbeingre-evaluated.Allothersaresittingidleandwaitingfortheirrespectivecalls.
VueNo,itisnotatypo.ItisalsoworthcomparingVuewithVue.Vuehasalsorecentlylauncheditssecondversion,whichisfasterandcleanerthanitspredecessor.IfyoustilluseVue1.0,itisworthtoupgrade.Ifyoudon'tknowanythingaboutVueversions,itisworthtocheckhowitevolvedandwhatdoesthenewversionallow.ChecktheVueblogpostthatannouncedVue2.0inApril2016athttps://vuejs.org/2016/04/27/announcing-2.0/.
Vue.jsfundamentalsBeforeputtingourhandsintothecodeandstartingtoenhanceourapplicationswithcomponents,plugins,mixins,templates,andotherthings,let'soverviewthemainVuefeatures.Let'sanalyzewhatarereusablecomponentsandhowtheapplicationstatecanbemanaged,andalsotalkaboutplugins,filters,andmixins.Inthissection,wewillhavejustaslightoverviewofthesefeatures.Wewilllearnthemdeeplyinthenextchapters.
ReusablecomponentsNowthatyouknownotonlywhatdatabindinginVue.jsisandhowtouseit,butalsohowitworks,itistimetointroduceanotherpowerfulVue.jsfeature.ComponentscreatedwithVue.jscanbeusedandreusedintheapplicationasbricksyoubuildyourhouseof.Eachcomponenthasitsownscopeofstylesandbindings,beingcompletelyisolatedfromtheothercomponents.
ThecomponentcreationsyntaxisverysimilartotheVueinstancecreationthatwealreadyknow,andyoushouldonlyuseVue.extendinsteadofjustVue:
varCustomComponent=Vue.extend({...})
CustomcomponentsinVue.js
Let's,forexample,trytodivideourshoppinglistcodeintocomponents.Asyouremember,ourshoppinglistconsistsessentiallyofthreeparts:thepartthatcontainstheshoppinglistitem,anotherpartthatcontainstheinputforaddingnewitems,andthethirdpartthatallowschangingthetitleoftheshoppinglist:
Threeessentialpartsoftheshoppinglistapplication
Let'schangethecodeoftheapplicationsothatitusesthreecomponents,oneforeachpart.
Ourcodewaslookinglikethefollowing:
vardata={
items:[{text:'Bananas',checked:true},
{text:'Apples',checked:false}],
title:'MyShoppingList',
newItem:''
};
newVue({
el:'#app',
data:data,
methods:{
addItem:function(){
vartext;
text=this.newItem.trim();
if(text){
this.items.push({
text:text,
checked:false
});
this.newItem='';
}
}
}
});
Nowwewillcreatethreecomponents:ItemsComponent,ChangeTitleComponent,andAddItemComponent.Allofthemwillhavethedatapropertywiththedataobject.TheaddItemmethodwilljumpfromthemainVueinstancetoChangeTitleComponent.AllthenecessaryHTMLwillgofromourindex.htmlfiletoeachofthecomponents.Sointheend,ourmainscriptwilllooklikethefollowing:
vardata={
items:[{text:'Bananas',checked:true},
{text:'Apples',checked:false}],
title:'MyShoppingList',
newItem:''
};
/**
*Declaringcomponents
*/
varItemsComponent=Vue.extend({
data:function(){
returndata;
},
template:'<ul>'+
'<liv-for="iteminitems"
:class="{'removed':item.checked}">'+
'<divclass="checkbox">'+
'<label>'+
'<inputtype="checkbox"
v-model="item.checked">{{item.text}}'+
'</label>'+
'</div>'+
'</li>'+
'</ul>'
});
varChangeTitleComponent=Vue.extend({
data:function(){
returndata;
},
template:'<inputv-model="title"/>'
});
varAddItemComponent=Vue.extend({
data:function(){
returndata;
},
methods:{
addItem:function(){
vartext;
text=this.newItem.trim();
if(text){
this.items.push({
text:text,
checked:false
});
this.newItem="";
}
}
},
template:
'<divclass="input-group">'+
'<inputv-model="newItem"@keyup.enter="addItem"
placeholder="addshoppinglistitem"type="text"
class="form-control">'+
'<spanclass="input-group-btn">'+
'<button@click="addItem"class="btnbtn-default"
type="button">Add!</button>'+
'</span>'+
'</div>'
});
/**
*Registeringcomponents
*/
Vue.component('items-component',ItemsComponent);
Vue.component('change-title-component',ChangeTitleComponent);
Vue.component('add-item-component',AddItemComponent);
/**
*InstantiatingaVueinstance
*/
newVue({
el:'#app',
data:data
});
HowdoweusethesecomponentsinsidetheView?Weshouldjustreplacethecorrespondingmarkupwiththetagoftheregisteredcomponent.Ourmarkuplookedlikethefollowing:
Theshoppinglistapplicationmarkupwithdefinedcomponents
So,thefirsthighlightedareawewillreplacewiththe<add-item-component></add-item-component>tag,thesecondonewiththe<items-component></items-component>tag,andthethirdonewiththe<change-title-component></change-title-component>tag.Thus,ourpreviouslyhugemarkupnowlookslikethefollowing:
<divid="app"class="container">
<h2>{{title}}</h2>
<add-item-component></add-item-component>
<items-component></items-component>
<divclass="footer">
<hr/>
<em>Changethetitleofyourshoppinglisthere</em>
<change-title-component></change-title-component>
</div>
</div>
Wewillgodeeplyintocomponentsinthenextchapterandwilllearnanevennicerwayofstructuringthem.Staytuned!
Vue.jsdirectivesYouhavealreadylearnedinthepreviouschapterwhatdirectivesareandhowtheyareusedtoenhancetheapplication'sbehavior.
You'vealreadyusedsomedirectivesthatallowdatabindingindifferentwaystotheViewlayer(v-model,v-if,v-show,andsoon).Besidesthesedefaultdirectives,Vue.jsallowsyoutocreatecustomdirectives.CustomdirectivesprovideamechanismtoenablecustombehaviorofDOMtodatamapping.
Whenregisteringacustomdirective,youcanprovidethreefunctions:bind,update,andunbind.Insidethebindfunction,youcanattachaneventlistenertotheelementanddowhateverneedstobedonethere.Insidetheupdatefunctionthatreceivesoldandnewvaluesasparameters,youcandefineacustombehaviorofwhatshouldhappenwhendatachanges.Theunbindmethodprovidesallthecleaningoperationsneeded(forexample,detacheventlisteners).
Tip
InVue2.0,directiveshavesignificantlyreducedthescopeofresponsibility—nowtheyareonlyusedtoapplylow-leveldirectDOMmanipulations.Vue'schangingguidesuggeststopreferusingcomponentsovercustomdirectives(https://github.com/vuejs/vue/issues/2873).
Thus,thefullversionofthecustomdirectivewouldlooklikethefollowing:
Vue.directive('my-directive',{
bind:function(){
//dothepreparationworkonelementbinding
},
update:function(newValue,oldValue){
//dosomethingbasedontheupdatedvalue
},
unbind:function(){
//dotheclean-upwork
}
})
Thesimplifiedversion,incaseyoujustneedtodosomethingonthevalueupdate,canonlyhavetheupdatemethodthatcanbepasseddirectlyasthesecondparameterofthedirectivefunction:
Vue.directive('my-directive',function(el,binding){
//dosomethingwithbinding.value
})
Thetheoryisnice,butwithoutasmallexample,itturnsoutboring.Solet'shavealookataverysimpleexample,whichwillshowthesquareofthenumbereachtimeitsvalueisupdated.
Ourcustomdirectivewilllooklikethefollowing:
Vue.directive('square',function(el,binding){
el.innerHTML=Math.pow(binding.value,2);
});
Usethisdirectiveinyourtemplatefileusingthev-prefix:
<divv-square="item"></div>
InstantiatetheVueinstancewithiteminitsdataandtrytochangethevalueofitem.Youwillseethatthevalueinsidethedivelementwillimmediatelydisplaythesquarenumberofthechangedvalue.ThecompletecodeforthiscustomdirectivecanbefoundintheJSFiddleathttps://jsfiddle.net/chudaol/we07oxbd/.
PluginsinVue.jsVue'scorefunctionality,aswehavealreadyanalyzed,providesdeclarativedatabindingandcomponentscomposing.Thiscorebehaviorisenhancedwithpluginsthatprovidearichsetoffunctionality.Thereareseveraltypesofplugins:
Pluginsthataddsomeglobalpropertyormethod(vue-element)Pluginsthataddsomeglobalassets(vue-touch)PluginsthataddVueinstancemethodsattachingthemtoVue'sprototypePluginsthatprovidesomeexternalfunctionalityorAPI(vue-router)
PluginsmustprovideaninstallmethodthathasaccesstotheglobalVueobjectthatcanenhanceandmodifyit.Inordertousethisplugin,Vueprovidestheusemethodthatreceivespluginsinstances(Vue.use(SomePlugin)).
Tip
YoucanalsowriteaVuepluginofyourowntoenablecustombehaviorforyourVueinstance.
Let'susethepreviouscustomdirectivesexampleandcreateaminimalisticpluginthatimplementsmathematicalsquareandsquarerootdirectives.CreateafilenamedVueMathPlugin.jsandaddthefollowingcode:
exportdefault{
install:function(Vue){
Vue.directive('square',function(el,binding){
el.innerHTML=Math.pow(binding.value,2);
});
Vue.directive('sqrt',function(el,binding){
el.innerHTML=Math.sqrt(binding.value);
});
}
};
Nowcreateafilecalledscript.js.Let'saddthemainscripttothisfile.Inthisscript,wewillimportbothVueandVueMathPlugin,andwillcallVue'susemethodinordertotellittousethepluginandcalltheplugin'sinstallmethod.Thenwe'lljustinitiateaVueinstanceaswealwaysdo:
importVuefrom'vue/dist/vue.js';
importVueMathPluginfrom'./VueMathPlugin.js';
Vue.use(VueMathPlugin);
newVue({
el:'#app',
data:{item:49}
});
Nowcreateanindex.htmlfilethatincludesthemain.jsfile(wewillbuilditwithBrowserifyandBabelify).Inthisfile,let'saddaninputusingthev-modeldirectivethatwillbeusedtochangethevalueoftheitem.Createtwospansusingv-squareandv-sqrtdirectivesaswell:
<body>
<divid="app">
<inputv-model="item"/>
<hr>
<div>Square:<spanv-square="item"></span></div>
<div>Root:<spanv-sqrt="item"></span></div>
</div>
<scriptsrc="main.js"></script>
</body>
Createapackage.jsonfiletoincludetheneededdependenciesforbuildingtheprojectandaddascriptforbuildingthemain.jsfile:
{
"name":"vue-custom-plugin",
"scripts":{
"build":"browserifyscript.js-omain.js-t
[babelify--presets[es2015]]"
},
"version":"0.0.1",
"devDependencies":{
"babel-preset-es2015":"^6.9.0",
"babelify":"^7.3.0",
"browserify":"^13.0.1",
"vue":"^2.0.3"
}
}
Nowinstallthedependenciesandbuildtheprojectfromthefollowingcommandline:
npminstall
npmrunbuild
Openindex.htmlinthebrowser.Trytochangethenumberintheinputbox.Bothsquareandsquarerootvalueschangeimmediately:
Thechangesinthedataareappliedimmediatelytothedirectivescreatedasapartofcustomplugin
ExerciseEnhanceMathPluginwithtrigonometricalfunctions(sine,cosine,andtangent).
ApossiblesolutiontothisexercisecanbefoundintheAnnexes.
ApplicationstateandVuexWhenanapplicationreachesaconsiderablesize,itmightbecomenecessaryforustomanagetheglobalapplicationstatesomehow.InspiredfromFlux(https://facebook.github.io/flux/),thereisaVuexmodulethatallowsustomanageandsharetheglobalapplicationstateamongVuecomponents.
Tip
Donotthinkabouttheapplicationstateassomethingcomplexanddifficulttounderstand.Infact,itisnomorethanjustdata.Eachcomponenthasitsowndata,and"applicationstate"isdatathatcanbeeasilysharedbetweenallthecomponents!
HowVuexstoremanagesapplicationsstateupdates
Liketheotherplugins,inordertobeabletouseandtoinstantiatetheVuexstore,youneedtoinstructVuetouseit:
importVuexfrom'vuex';
importVuefrom'vue';
Vue.use(Vuex);
varstore=newVuex.Store({
state:{<...>},
mutations:{<...>}
});
Then,wheninitializingthemaincomponent,assignthestoreinstancetoit:
newVue({
components:components,
store:store
});
Nowthemainapplicationandallitscomponentsareawareaboutthestore,haveaccesstothedatainsideit,andareabletotriggeractionsonitatanytimeoftheapplication'slifecycle.Wewilldigdeeplyintotheapplicationstateinthenextchapters.
vue-cliYes,Vuehasitsowncommand-lineinterface.ItallowsustoinitializeaVueapplicationwithwhateverconfigurationwewant.YoucaninitializeitwithWebpackboilerplate,withBrowserifyboilerplate,orjustwithasimpleboilerplatethatjustcreatesanHTMLfileandprepareseverythingforyoutostartworkingwithVue.js.
Installitwithnpm:
npminstall-gvue-cli
Thedifferentwaysofinitializinganapplicationareasfollows:
vueinitwebpack
vueinitwebpack-simple
vueinitbrowserify
vueinitbrowserify-simple
vueinitsimple
Toseethedifference,let'srunvueinitwiththesimpletemplateandwiththeWebpacktemplate,andlookatthedifferencesinthegeneratedstructure.Followingishowtheoutputdiffersfrombothcommands:
Theoutputfromthecommandsvueinitwebpackandvueinitsimple
Thefollowingishowtheapplicationstructurediffers:
Thedifferenceinstructureinapplicationscaffoldedwithvueinitsimpleandvueinitwebpack
Theindex.htmlfileinthesimpleconfigurationalreadycontainsVue.jsfromtheCDN,soifyoujustneedtodosomethingreallysimplesuchasquickprototyping,usethisone.
ButifyouareabouttostartacomplexSinglePageApplication(SPA)projectthatwillrequiretestingandhotreloadingduringdevelopment,usetheWebpackorBrowserifyconfiguration.
VuepluginsforIDEsTherearepluginsforVuesyntaxhighlightingforsomemajorIDEs.Iwillleaveyouwiththelinkstothefanciestofthem:
IDE LinktotheVueplugin
Sublime https://github.com/vuejs/vue-syntax-highlight
Webstorm https://github.com/postalservice14/vuejs-plugin
Atom https://github.com/hedefalk/atom-vue
VisualStudioCode https://github.com/LiuJi-Jim/vscode-vue
vim https://github.com/posva/vim-vue
Brackets https://github.com/pandao/brackets-vue
Installing,using,anddebuggingaVue.jsapplicationInthissection,wewillanalyzeallthepossiblewaysofinstallingVue.js.Wewillalsocreateaskeletonforourapplicationsthatwewilldevelopandenhancethroughthenextchapters.Wewillalsolearnthewaysofdebuggingandtestingourapplications.
InstallingVue.jsThereareanumberofwaystoinstallVue.js.Startingfromclassic,includingthedownloadedscriptintoHTMLwithinthe<script>tags,usingtoolslikebower,npm,orVue'scommand-lineinterface(vue-cli),tobootstrapthewholeapplication.
Let'shavealookatallthesemethodsandchooseourfavorite.Inalltheseexamples,wewilljustshowaheaderonapagesayingLearningVue.js.
Standalone
Downloadthevue.jsfile.Therearetwoversions,minifiedanddeveloperversion.Thedevelopmentversionisathttps://vuejs.org/js/vue.js.Theminifiedversionisathttps://vuejs.org/js/vue.min.js.
Tip
Ifyouaredeveloping,makesureyouusethedevelopmentnon-minifiedversionofVue.Youwilllovethenicetipsandwarningsontheconsole.
Thenjustincludevue.jsinthe<script>tags,asfollows:
<scriptsrc="vue.js"></script>
Vueisregisteredintheglobalvariable.Youarereadytouseit.
Ourexamplewillthenlookassimpleasthefollowing:
<divid="app">
<h1>{{message}}</h1>
</div>
<scriptsrc="vue.js"></script>
<script>
vardata={
message:'LearningVue.js'
};
newVue({
el:'#app',
data:data
});
</script>
CDN
Vue.jsisavailableinthefollowingCDNs:
jsdelivr:https://cdn.jsdelivr.net/vue/2.0.3/vue.jscdnjs:https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.jsunpkg:https://unpkg.com/[email protected]/dist/vue.js(recommended)
JustputtheURLinsourceinthescripttagandyouarereadytouseVue!
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js">
</script>
Tip
BewaretheCDNversionmightnotbesynchronizedwiththelatestavailableversionofVue.
Thus,theexamplewilllookexactlythesameasinthestandaloneversion,butinsteadofusingdownloadedfileinthe<script>tags,weareusingaCDNURL.
Bower
IfyouarealreadymanagingyourapplicationwithBoweranddon'twanttouseothertools,there'salsoaBowerdistributionofVue.Justcallbowerinstall:
#lateststablerelease
bowerinstallvue
Ourexamplewilllookexactlylikethetwopreviousexamples,butitwillincludethefilefromthebowerfolder:
<scriptsrc="bower_components/vue/dist/vue.js"></script>
CSP-compliant
ContentSecurityPolicy(CSP)isasecuritystandardthatprovidesasetofrulesthatmustbeobeyedbytheapplicationinordertopreventsecurityattacks.Ifyouaredevelopingapplicationsforbrowsers,youarelikelyfamiliarwiththispolicy!
FortheenvironmentsthatrequireCSP-compliantscripts,there'saspecialversionofVue.jsathttps://github.com/vuejs/vue/tree/csp/dist.
Let'sdoourexampleasaChromeapplicationtoseetheCSP-compliantVue.jsinaction!
Startbycreatingafolderforourapplicationexample.ThemostimportantthinginaChromeapplicationisthemanifest.jsonfile,whichdescribesyourapplication.Let'screateit.Itshouldlooklikethefollowing:
{
"manifest_version":2,
"name":"LearningVue.js",
"version":"1.0",
"minimum_chrome_version":"23",
"icons":{
"16":"icon_16.png",
"128":"icon_128.png"
},
"app":{
"background":{
"scripts":["main.js"]
}
}
}
Thenextstepistocreateourmain.jsfile,whichwillbetheentrypointfortheChromeapplication.Thescriptshouldlistenfortheapplicationlaunchingandopenanewwindowwithgivensizes.Let'screateawindowof500x300sizeandopenitwithindex.html:
chrome.app.runtime.onLaunched.addListener(function(){
//Centerthewindowonthescreen.
varscreenWidth=screen.availWidth;
varscreenHeight=screen.availHeight;
varwidth=500;
varheight=300;
chrome.app.window.create("index.html",{
id:"learningVueID",
outerBounds:{
width:width,
height:height,
left:Math.round((screenWidth-width)/2),
top:Math.round((screenHeight-height)/2)
}
});
});
Atthispoint,theChrome-specificapplicationmagicisoverandnowweshalljustcreateourindex.htmlfilethatwilldothesamethingasinthepreviousexamples.Itwillincludethevue.jsfileandourscript,wherewewillinitializeourVueapplication:
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Vue.js-CSP-compliant</title>
</head>
<body>
<divid="app">
<h1>{{message}}</h1>
</div>
<scriptsrc="assets/vue.js"></script>
<scriptsrc="assets/app.js"></script>
</body>
</html>
DownloadtheCSP-compliantversionofVue.jsandaddittotheassetsfolder.
Nowlet'screatetheapp.jsfileandaddthecodethatwealreadywroteaddedseveraltimes:
vardata={
message:"LearningVue.js"
};
newVue({
el:"#app",
data:data
});
Addittotheassetsfolder.
Donotforgettocreatetwoiconsof16and128pixelsandcallthemicon_16.pngandicon_128.png,respectively.
Yourcodeandstructureintheendshouldlookmoreorlesslikethefollowing:
StructureandcodeforthesampleChromeapplicationusingvue.js
Andnowthemostimportantthing.Let'scheckifitworks!Itisvery,verysimple:
1. Gotochrome://extensions/urlinyourChromebrowser.2. ChecktheDevelopermodecheckbox.3. ClickonLoadunpackedextension...andcheckthefolderthatwe'vejustcreated.4. Yourappwillappearinthelist!Nowjustopenanewtab,clickonapps,andcheckthat
yourappisthere.Clickonit!
SampleChromeapplicationusingvue.jsinthelistofChromeapps
Congratulations!YouhavejustcreatedaChromeapplication!
npm
Thenpminstallationmethodisrecommendedforlarge-scaleapplications.Justrunnpminstallvueasfollows:
#lateststablerelease
npminstallvue
#lateststableCSP-compliantrelease
npminstallvue@csp
Thenrequireit:
varVue=require("vue");
Or,forES2015lovers,runthefollowing:
importVuefrom"vue";
OurHTMLwilllookexactlylikeinthepreviousexamples:
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Vue.js-NPMInstallation</title>
</head>
<body>
<divid="app">
<h1>{{message}}</h1>
</div>
<scriptsrc="main.js"></script>
</body>
</html>
Nowlet'screateascript.jsfilethatwilllookalmostexactlythesameasinastandaloneorCDNversion,withtheonlydifferencebeingthatitwillrequirevue.js:
varVue=require('vue/dist/vue.js');
vardata={
message:'LearningVue.js'
};
newVue({
el:'#app',
data:data
});
Let'sinstallVueandBrowserifyinordertobeabletocompileourscript.jsfileintothemain.jsfile:
npminstallvue--save-dev
npminstallbrowserify--save-dev
Inthepackage.jsonfile,addascriptforbuildaswellthatwillexecuteBrowserifyonscript.jstranspilingitintomain.js.Soourpackage.jsonfilewilllooklikethefollowing:
{
"name":"learningVue",
"scripts":{
"build":"browserifyscript.js-omain.js"
},
"version":"0.0.1",
"devDependencies":{
"browserify":"^13.0.1",
"vue":"^2.0.3"
}
}
Nowrunthefollowingcommand:
npmrunbuild
Andopenindex.htmlinthebrowser.
Ihaveafriendthatatthispointwouldsaysomethinglike:really?Somanysteps,installations,
commands,explanations...Justtooutputsomeheader?I'mout!
Ifyouarealsothinkingthis,wait.Yes,thisistrue,nowwe'vedonesomethingreallysimpleinarathercomplexway,butifyoustaywithmeabitlonger,youwillseehowcomplexthingsbecomeeasytoimplementifweusethepropertools.Also,donotforgettocheckyourPomodorotimer,maybeit'stimetotakearest!
vue-cli
Aswehavealreadymentionedinthepreviouschapter,Vueprovidesitsowncommand-lineinterfacethatallowsbootstrappingsingle-pageapplicationsusingwhateverworkflowsyouwant.Itimmediatelyprovideshotreloadingandstructureforatest-drivenenvironment.Afterinstallingvue-cli,justrunvueinit<desiredboilerplate><project-name>andthenjustinstallandrun:
#installvue-cli
$npminstall-gvue-cli
#createanewproject
$vueinitwebpacklearn-vue
#installandrun
$cdlearn-vue
$npminstall
$npmrundev
Nowopenyourbrowseronlocalhost:8080.Youjustusedvue-clitoscaffoldyourapplication.Let'sadaptittoourexample.Openasourcefolder.Inthesrcfolder,youwillfindanApp.vuefile.DoyourememberwetalkedaboutVuecomponentsthatarelikebricksfromwhichyoubuildyourapplication?Doyourememberthatwewerecreatingandregisteringtheminsideourmainscriptfile,andImentionedthatwewilllearntobuildcomponentsinamoreelegantway?Congratulations,youarelookingatthecomponentbuiltinafancyway!
FindthelinethatsaysimportHellofrom'./components/Hello'.Thisisexactlyhowthecomponentsarebeingreusedinsideothercomponents.Havealookatthetemplateatthetopofthecomponentfile.Atsomepoint,itcontainsthe<hello></hello>tag.ThisisexactlywhereinourHTMLfilethehellocomponentwillappear.Havealookatthiscomponent;itisinthesrc/componentsfolder.Asyoucansee,itcontainsatemplatewith{{msg}}andascriptthatexportsdatawithdefinedmsg.Thisisexactlythesameasweweredoinginourpreviousexampleswithoutusingcomponents.Let'sslightlymodifythecodetomakeitthesameasinthepreviousexamples.IntheHello.vuefile,changemsginthedataobject:
<script>
exportdefault{
data(){
return{
msg:"LearningVue.js"
}
}
}
</script>
IntheApp.vuecomponent,removeeverythingfromthetemplateexceptthehellotagsothatthetemplatelookslikethefollowing:
<template>
<divid="app">
<hello></hello>
</div>
</template>
Nowifyoureruntheapplication,youwillseeourexamplewithbeautifulstylesthatwedidn'ttouch:
Vueapplicationbootstrappedusingvue-cli
Tip
BesidesWebpackboilerplatetemplate,youcanusethefollowingconfigurationswithyourvue-cli:
webpack-simple:AsimpleWebpack+vue-loadersetupforquickprototypingbrowserify:Afull-featuredBrowserify+Vueifysetupwithhot-reload,linting,andunittesting
browserify-simple:AsimpleBrowserify+Vueifysetupforquickprototypingsimple:ThesimplestpossibleVuesetupinasingleHTMLfile
Devbuild
Mydearreader,IcanseeyourshiningeyesandIcanreadyourmind.NowthatyouknowhowtoinstallanduseVue.jsandhowitworks,youdefinitelywanttoputyourhandsdeeplyintothecorecodeandcontribute!
Iunderstandyou.Forthis,youneedtousethedevelopmentversionofVue.js,whichyouhavetodownloadfromGitHubandcompileyourself.
Let'sbuildourexamplewiththisdevelopmentversionofVue.Createanewfolder,forexample,dev-build,andcopyallthefilesfromthenpmexampletothisfolder.
Donotforgettocopythenode_modulesfolder.YoushouldcdintoitanddownloadfilesfromGitHubtoit,andthenrunnpminstallandnpmrunbuild:
cd<APP-PATH>/node_modules
rm-rfvue
gitclonehttps://github.com/vuejs/vue.git
cdvue
npminstall
npmrunbuild
Nowbuildourexampleapplication:
cd<APP-PATH>
npmrunbuild
Openindex.htmlinthebrowser;youwillseetheusualLearningVue.jsheader.
Let'snowtrytochangesomethinginvue.jssource!Gotothenode_modules/vue/src/compiler/parserfolderandopenthetext-parser.jsfile.Findthelinethatsaysthefollowing:
constdefaultTagRE=/\{\{((?:.|\n)+?)\}\}/g
Actually,thisregularexpressiondefinesdefaultdelimitersusedintheHTMLtemplates.ThethingsinsidethesedelimitersarerecognizedasaVuedataorasaJavaScriptcode.Let'schangethem!Let'sreplace{and}withdoublepercentagesigns!Goonandeditthefile:
constdefaultTagRE=/\%\%((?:.|\n)+?)\%\%/g
NowrebuildbothVuesourceandourapplicationandrefreshthebrowser.Whatdoyousee?
AfterchangingtheVuesourceandreplacingdelimiters,{{}}delimitersdonotworkanymore!
Themessageinside{{}}isnolongerrecognizedasdatathatwepassedtoVue.Infact,itisbeingrenderedaspartofHTML.
Nowgototheindex.htmlfileandreplaceourcurlybracketsdelimiterswithdoublepercentage,asfollows:
<divid="app">
<h1>%%message%%</h1>
</div>
Rebuildourapplicationandrefreshthebrowser!Whataboutnow?Youseehoweasyitistochangetheframework'scodeandtotryoutyourchanges.I'msureyouhaveplentyofideasabouthowtoimproveoraddsomefunctionalitytoVue.js.Sochangeit,rebuild,test,deploy!Happypullrequests!
DebuggingyourVueapplicationYoucandebugyourVueapplicationthesamewayyoudebuganyotherwebapplication.Useyourdevelopertools(firebug),breakpoints,debuggerstatements,andsoon.IfyouwanttodivedeepinsidetheChromedebuggingtools,checkChrome'sdocumentationathttps://developer.chrome.com/devtools.
VuealsoprovidesVue.jsdevtools,soitgetseasiertodebugVueapplications.YoucandownloadandinstallitfromtheChromewebstoreathttps://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd.
Unfortunately,itdoesn'tworkwithlocallyopenedfiles,sousesomesimpleHTTPserverinordertoserveourexamplesasawebpage(forexample,https://www.npmjs.com/package/http-server).
Afterinstallingit,open,forexample,ourshoppinglistapplication.Opendevelopertools.YouwillseetheVuetabhasautomaticallyappeared:
Vuedevtools
Inourcase,weonlyhaveonecomponent—<Root>.Asyoucanimagine,oncewestartworkingwithcomponentsandhavinglotsofthem,theywillallappearintheleftpartoftheVuedevtoolspalette.Clickonthe<Root>componentandinspectit.You'llseeallthedataattachedtothiscomponent.Ifyoutrytochangesomething,forexample,addashoppinglistitem,checkoruncheckacheckbox,changethetitle,andsoon,allthesechangeswillbeimmediatelypropagatedtothedataintheVuedevtools.Youwillimmediatelyseethechangesontheright-handsideofit.Let'stry,forexample,toaddashoppinglistitem.Onceyoustarttyping,youseeontherighthownewItemchangesaccordingly:
ThechangesintheModelsareimmediatelypropagatedtotheVuedevtoolsdata
WhenwestartaddingmorecomponentsandintroducecomplexitytoourVueapplications,thedebuggingwillcertainlybecomemorefun!
ScaffoldingourapplicationsDoyourememberthetwoapplicationsthatwestartedtoworkoninthefirstchapter,theshoppinglistapplicationandthePomodoroone?Inthissection,wewillscaffoldtheseapplicationsusingthevue-clitoolinorderforthemtobereadytocontainreusablecomponents,betested,andbedeployed.Oncewebootstraptheseapplications,wewillworkonthemuntiltheendofthisbook.Solet'sdoitcarefullyandwithlotsoflove!
ScaffoldingtheshoppinglistapplicationWewillscaffoldtheshoppinglistapplicationusingvue-cliWebpackconfiguration.
Tip
Incaseyouhaveignoredallpreviouspracticalexercisesrelatedtovue-cli,donotforgettoinstallitbeforeproceedingtothenextsteps:npminstall-gvue-cli
Ifyoualreadyhavevue-cliinstalled,gotothedirectorywhereyouwanttobootstraptheapplicationandrunthefollowing:
vueinitwebpackshopping-list
Answeryestoallthequestions(justclickenter)andvoilà!Youhavetheapplicationbootstrapped:
Bootstrapingtheshoppinglistapplicationwithvue-cli
Switchtotheshoppinglistdirectoryandrunnpminstallandnpmrundev.Openyourbrowseratlocalhost:8080.YouwillseetheHelloWorldpageofthenewlycreatedVueapplication:
TheHelloWorldviewofthenewlybootstrappedapplication
Let'scleanthebootstrappedcodesothattheapplicationgetsreadytobepopulatedwithourapplication-specificcode.GototheApp.vuefileandremoveeverything,leavingjustthetagsthatdefinetheapplicationstructure:
<template>withthemain<div>insideThe<script>tagThe<style>tag
So,intheend,yourApp.vuefilelookslikethefollowing:
<template>
<divid="app">
</div>
</template>
<script>
</script>
<style>
</style>
Havealookatthepageopenedinthebrowser.Funny,youhaven'tdoneanything,butthepagenowdoesn'tcontainthedefaultHelloWorld.Thepageisempty!Ithaschangedautomatically!
Tryaddingsomethinginsidethe<template>tags.Lookatthepage;itautomaticallyreloadsonceyouintroducechanges.Thisworksbecauseofthevue-hot-reloadpluginthatdetectschangesinyourVuecomponentsandautomaticallyrebuildstheprojectandreloadsthebrowserpage.TrytowritesomeJavaScriptcodeinsidethe<script>tagsthatdoesn'tcorrespondtolintstandards,forexample,usingnotDefinedVariable:
<script>
notDefinedVariable=5;
</script>
Thepageinthebrowserisnotrefreshed.Lookatyourshellconsole.Itshowsthelinterrorsand"refuses"tobuildyourapplication:
Eachtimetheapplicationischangedthelintrulesarechecked
Thishappens,thankstotheESLintplugin,whichchecksthecodeagainstthelintruleseachtimetheapplicationchanges.
Withthat,wecanbesurethatourcodewillfollowthebestqualitystandards.
Speakingofquality,weshouldalsoprepareourapplicationtobeabletorununittests.Luckilyforus,vue-cliwithWebpackhasalreadydoneitforus.Runnpmrununittorununittestsandnpmrune2etorunend-to-endnightwatchtests.End-to-endtestswillnotruninparallelwithyourrunningapplicationsincebothareusingthesameport.So,ifyouwanttoruntestsduringdevelopment,youshouldchangetheportintheconfig/index.jsconfigurationfileorsimplystoptheapplicationbetweenrunningtests.Afterrunningtests,youwillseetheend-to-endtestsfail.Thisisbecausetheyarecheckingfortheapplication'sspecificelementsthatwehaveremoved.Openthefiletest.jsfromthetest/e2e/specs/directoryandcleanalltheassertionsthatwedon'tneedanymore.Nowitshouldlooklikethefollowing:
module.exports={
'defaulte2etests':function(browser){
browser
.url('http://localhost:8080')
.waitForElementVisible('#app',5000)
.end()
}
}
Rerunthetests.Nowtheyshouldbepassing.Fromnowon,aswewilladdcodetoourapplication,wewilladdunitandend-to-endtests.
BootstrapingyourPomodoroapplicationForthePomodoroapplication,dothesameasfortheshoppinglistapplication.RunvueinitwebpackpomodoroandrepeatallthenecessarystepstoensurethatthestructureisreadytobepopulatedwiththePomodoroapplicationcode!
ExerciseImplementourPomodoroapplicationasaChromeapp!YoujustneedtouseitwithaCSP-compliantversionofVue.jsandaddamanifest.jsonfile.
SummaryInthischapter,wehaveanalyzedthebehind-the-scenesofVue.js.Youlearnedhowdatareactivityisachieved.YousawhowVue.jsleveragesObject.definePropertygettersandsetterstopropagatechangesinthedata.YousawanoverviewofthekeyVue.jsconcepts,suchasreusablecomponents,pluginssystem,andstatemanagementwithVuex.Wehavebootstrappedtheapplicationsthatwewilldevelopduringthenextchapters.
Inthenextchapter,wewillhaveadeeperlookintotheVue'scomponentssystem.Wewillusecomponentsinourapplications.
Chapter3.Components–UnderstandingandUsingInthepreviouschapter,youlearnedhowVue.jsworks.YousawbehindthescenesandevenmadeaslightdebugofthecoreVue.jscode.YoulearnedsomeofVue'skeyconcepts.YoualsolearnedandtrieddifferentwaysofinstallingVue.js.Wehavebootstrappedtheapplications;wewilldevelopandenhancefromthischapteron.Wehavealsoseenhowtodebugandtotestourapplications.
Inthefirstchapter,wetalkedaboutcomponentsandevencreatedsome.Inthischapter,wewillusecomponentsinourapplicationsandseesomeinterestingdirectivesinaction.Thatbeingsaid,inthischapter,wearegoingtodothefollowing:
RevisitthecomponentstopicandreviewwhatcomponentsareCreatecomponentsforourapplicationsLearnwhatsingle-filecomponentsareLearnhowtoachievereactiveCSStransitionswithspecialattributes
RevisitingcomponentsAsyousurelyrememberfromthepreviouschapters,componentsarespecialpartsoftheVueapplicationthathavetheirownscopeofdataandmethods.Componentscanbeusedandreusedthroughouttheapplication.Inthepreviouschapter,youlearnedthatacomponentiscreatedbyusingtheVue.extend({...})methodandregisteredusingtheVue.component()syntax.So,inordertocreateanduseacomponent,wewouldwritethefollowingJavaScriptcode:
//creatingcomponent
varHelloComponent=Vue.extend({
template:'<h1>Hello</h1>'
});
//registeringcomponent
Vue.component('hello-component',HelloComponent);
//initializingtheVueapplication
newVue({
el:'#app'
});
Then,wewillusehello-componentinsidetheHTML:
<divid='app'>
<hello-component></hello-component>
</div>
Tip
BothinitializationandregistrationcanbewrittenasasingleVue.componentinvocationwithcorrespondingoptions:
Vue.component('hello-component',{template:'<h1>Hello</h1>'});
BenefitsofusingcomponentsTherearesomethingsthatweneedtolearnbeforegoingdeepintothecomponentsandrewriteourapplicationsusingthem.Inthissection,wewillcoverthingssuchashandlingdataandelpropertiesinsideacomponent,componenttemplates,scope,andpreprocessors.
DeclaringtemplatesinHTMLInourpreviousexample,wecreatedaVuecomponentwithatemplatewrittenasastring.It'sactuallyeasyandnicebecausewehaveeverythingweneedinsideourcomponent.NowimagineourcomponentwithamorecomplexHTMLstructure.WritingacomplexHTMLstringtemplateiserror-prone,ugly,andagainstbestpractices.
Tip
Bybestpractices,Imeancleanandmaintainablecode.ComplexHTMLwrittenasastringisanythingbutmaintainable.
VueallowsdeclaringtemplatesinsideanHTMLfilewithinaspecial<template>tag!
So,torewriteourexample,wewilldeclareanHTMLtagtemplatewiththecorrespondingmarkupinside:
<templateid="hello">
<h1>Hello</h1>
</template>
Andthen,insideourcomponent,insteadoftheHTMLstring,wewilljustusetheIDofthetemplate:
Vue.component('hello-component',{
template:'#hello'
});
Ourwholecodewilllooklikethefollowing:
<body>
<templateid="hello">
<h1>Hello</h1>
</template>
<divid="app">
<hello-component></hello-component>
</div>
<scriptsrc="vue.js"></script>
<script>
Vue.component('hello-component',{
template:'#hello'
});
newVue({
el:'#app'
});
</script>
</body>
Intheprecedingexample,wehadonlyusedthetemplateattributeforthecomponent.Let'smoveonandseehowthedataandelattributesshouldbetreatedinsideacomponent.
HandlingdataandelpropertiesinsideacomponentAsalreadymentioned,thecomponent'ssyntaxisthesameastheVueinstance'ssyntax,butitmustextendtheVueinsteadofcallingitdirectly.Withthispremise,itseemscorrecttocreateacomponentlikethefollowing:
varHelloComponent=Vue.extend({
el:'#hello',
data:{msg:'Hello'}
});
Butthiswouldleadtoascopeleak.EveryinstanceofHelloComponentwouldsharethesamedataandel.Andthisisnotexactlywhatwewant.ThatiswhyVueexplicitlydemandstodeclarethesepropertiesasfunctions:
varHelloComponent=Vue.component('hello-component',{
el:function(){
return'#hello';
},
data:function(){
return{
msg:'Hello'
}
}
});
Evenifyoumakeamistakeanddeclarethedataortheelpropertiesasanobjectoranelement,Vuewillkindlywarnyou:
Vue'swarningwhenusingdataasanobjectinsteadofafunctioninsideofaVuecomponent
ScopeofthecomponentsAsalreadymentioned,allcomponentshavetheirownscopethatisinaccessiblebyothercomponents.Nevertheless,theglobalapplicationscopeisaccessiblebyalltheregisteredcomponents.Youcanseethecomponents'scopeaslocalandtheapplicationscopeasglobalscopes.It'sthesame.However,usingtheparent'sdatainsideacomponentisnotstraightforward.Youhavetoexplicitlyindicateinsideacomponentwhichparent'sdatapropertiesshouldbeaccessedusingthepropattributeandbindthemtothecomponentinstanceusingthev-bindsyntax.Let'sseehowitworksonourHelloComponentexample.
Let'sstartbydeclaringHelloComponentwithdatathatcontainstheattributemsg:
Vue.component('hello-component',{
data:function(){
return{
msg:'Hello'
}
}
});
Now,let'screateaVueinstancewithsomedatainsideit:
newVue({
el:'#app',
data:{
user:'hero'
}
});
InsideourHTML,let'screateatemplateandapplyittothecomponentusingthetemplate'sID:
//templatedeclaration
<templateid="hello">
<h1>{{msg}}{{user}}</h1>
</template>
//usingtemplateincomponent
Vue.component('hello-component',{
template:'#hello',
data:function(){
return{
msg:'Hello'
}
}
});
Inordertoseethecomponentonthepage,weshouldinvokeitinsidetheHTMLofourappcontainer:
<divid="app">
<hello-component></hello-component>
</div>
Ifyouopenthepageinthebrowser,youwillonlyseeHello;theuserdatapropertyisstillnotboundtothecomponent:
Theparent'sdatapropertyisnotyetboundtoourVuecomponent
InordertobindthedatafromtheparentVueapplication,wehavetodothefollowingtwothings:
IndicatethispropertyinsideofthepropattributeofacomponentBindittothehello-componentinvocation:
//callingparent'sdataattributesinthecomponent
Vue.component('hello-component',{
template:'#hello',
data:function(){
return{
msg:'Hello'
}
},
props:['user']
});
//bindingauserdatapropertytothecomponent
<divid="app">
<hello-componentv-bind:user="user"></hello-component>
</div>
Refreshthepageandyouwillseehowitnowpresentsyouwithagreeting:
Afterthecorrectbindingoftheparent'sdatapropertytothecomponent,everythingworksasexpected.
Tip
Actually,thev-bind:usersyntaxcanbeshortcutjustbyusingthefollowing:
:user<hello-component:user="user"></hello-component>
ComponentsinsideothercomponentsThebeautyofthecomponentsisthattheycanbeusedandreusedinsideothercomponentsasLegobricksandblocks!Let'sbuildanothercomponent;let'scallitgreetings,whichwillbecomposedoftwosub-components:theformaskingfortheuser'snameandourhellocomponent.
Inordertodothis,let'sdeclarethetemplatefortheformandouralreadyfamiliarhellotemplate:
<!--templatefortheform-->
<templateid="form">
<div>
<labelfor="name">What'syourname?</label>
<inputv-model="user"type="text"id="name">
</div>
</template>
//templateforsayinghello
<templateid="hello">
<h1>{{msg}}{{user}}</h1>
</template>
NowwewillregistertwoVuecomponentsbasedonthesetemplates:
//registerformcomponent
Vue.component('form-component',{
template:'#form',
props:['user']
});
//registerhellocomponent
Vue.component('hello-component',{
template:'#hello',
data:function(){
return{
msg:'Hello'
}
},
props:['user']
});
Finally,wewillcreateourgreetingstemplatethatwillusebothformandhellocomponents.Donotforgetthatwehavetobindtheuserpropertyonthecomponentsinvocation:
<templateid="greetings">
<div>
<form-component:user="user"></form-component>
<hello-component:user="user"></hello-component>
</div>
</template>
Atthispoint,wecancreateourgreetingscomponentandusethegreetingstemplateinsideit.
Let'sinitialize,whichdatafunctionwiththenameoftheuserinthiscomponent:
//creategreetingscomponentbasedonthegreetingstemplate
Vue.component('greetings-component',{
template:'#greetings',
data:function(){
return{
user:'hero'
}
}
});
Insideourmainapplicationcontainer,wewillnowinvokethegreetingscomponent:
<divid="app">
<greetings-component></greetings-component>
</div>
DonotforgettoinitializetheVueapplication:
newVue({
el:'#app'
});
Openthepageinthebrowser.Youshouldseesomethinglikethefollowing:
ThepagebuiltfromvariousVuecomponents
Trytochangethenameintheinput.Youareexpectingittochangealsointhegreetingsheaderbecauseweboundittoit.Butstrangely,itdoesn'tchange.Well,thisisactuallythenormalbehavior.Bydefault,allpropsfollowone-waydatabinding.Thismeansthatifthedatachangeswithintheparent'sscope,thesechangesarepropagatedtothechildcomponent,butnotviceversa.Itisdonethiswayinordertopreventchildrencomponentsfromaccidentallymutatingtheparentstate.Itis,however,possibletoforcechildrencomponentstocommunicatewiththeirparentsbyinvokingevents.ChecktheVuedocumentationathttps://vuejs.org/guide/components.html#Custom-Events.
Inourcase,wecanbindausermodeltoourforminputcomponentandemittheinputeventeverytimetheusertypesintheinputbox.Weachieveitbyusingthev-on:inputmodifier,justlikeitisdescribedinthissectionathttps://vuejs.org/guide/components.html#Form-Input-
Components-using-Custom-Events.
Thus,wehavetopassv-model:usertoform-component:
<form-componentv-model="user"></form-component>
Then,form-componentshouldacceptthevaluepropandemittheinputevent:
Vue.component('form-component',{
template:'#form',
props:['value'],
methods:{
onInput:function(event){
this.$emit('input',event.target.value)
}
}
});
Theinputboxinsidetheform-componenttemplateshouldbindthev-on:inputandtheonInputmethodtothev-on:inputmodifier:
<inputv-bind:value="value"type="text"id="name"v-on:input="onInput">
Tip
Actually,priortoVue2.0,thiskindoftwo-waysynchronizationbetweencomponentsandtheirparentswaspossiblebyexplicitlytellingthepropertybeingboundtosyncusingthe.syncmodifier:<form-component:user.sync="user"></form-component>
Refreshthepage.Nowyoucanchangethenameinsidetheinputanditisimmediatelypropagatedtotheparent'sscope,andthustootherchildrencomponentsthatrelyonthisproperty:
Bindingpropertieswiththe.syncmodifierallowstwo-waydatabindingbetweenparentandchildrencomponents
YoucanfindthecompletecodeforthisexampleintheJSFiddleathttps://jsfiddle.net/chudaol/1mzzo8yn/.
Tip
BeforetheVue2.0release,therewasonemoredata-bindingmodifier,.once.Withthismodifier,thedatawouldbeboundonlyonce,andanyotherchangeswouldnotaffectthestateofcomponents.Comparethefollowing:
<form-component:user="user"></form-component>
<form-component:user.sync="user"></form-component>
<form-component:user.once="user"></form-component>
RewritingtheshoppinglistwithsimplecomponentsNowthatwealreadyknowalotaboutcomponents,let'srewriteourshoppinglistapplicationusingthem.
Tip
Fortherewritingoftheapplication,wewillusethisversionoftheshoppinglistapplicationasabase:https://jsfiddle.net/chudaol/vxfkxjzk/3/.
Wehavealreadydoneitpreviously,whenwestartedtalkingaboutcomponents.Butatthattime,weusedstringtemplatesinsidethecomponents'options.Let'sdoitnowusingtemplatesaswehavejustlearnedtodo.Let'sjusthavealookattheinterfaceandidentifythecomponentsagain:
Ourshoppinglistapplicationwillhavefourcomponents
Thus,Isuggestthatourshoppinglistapplicationconsistsofthefollowingfourcomponents:
AddItemComponent:ThecomponentresponsibleforaddinganewitemtotheshoppinglistItemComponent:Thecomponentresponsiblefortherenderingofthenewitemintheshoppinglist
ItemsComponent:ThecomponentresponsibleforrenderingandmanagingthelistofItemComponent
ChangeTitleComponent:Thecomponentresponsibleforchangingthetitleofthelist
DefiningtemplatesforallthecomponentsLet'screatetemplatesforthesecomponentsassumingthatthecomponentsthemselvesarealreadydefinedandregistered.
Note
CamelCaseVSkebab-caseYouhaveprobablynoticedthatwhilewedeclarevariablesdescribingcomponentsinCamelCase(varHelloComponent=Vue.extend({...})),wenametheminkebab-case:Vue.component('hello-component',{...}).Wedothisbecauseofthecase-insensitiveHTMLattributenature.Thus,ourcomponentsfortheshoppinglistapplicationwillbecalledasfollows:
add-item-component
item-component
items-component
change-title-component
Havealookathowourmarkupwaspreviously(https://jsfiddle.net/chudaol/vxfkxjzk/3/).
Let'srewriteitusingtemplatesandcomponents'names.Inthispart,wewilljustworryaboutthepresentationlayer,leavingthedatabindingandactionshandlingforafutureimplementation.WejustcopyandpastetheHTMLpartoftheapplicationanddistributeitoverourcomponents.Ourfourtemplateswilllooksomethinglikethefollowing:
<!--addnewitemtemplate-->
<templateid="add-item-template">
<divclass="input-group">
<[email protected]="addItem"v-model="newItem"
placeholder="addshoppinglistitem"type="text"
class="form-control">
<spanclass="input-group-btn">
<button@click="addItem"class="btnbtn-default"
type="button">Add!</button>
</span>
</div>
</template>
<!--listitemtemplate-->
<templateid="item-template">
<li:class="{'removed':item.checked}">
<divclass="checkbox">
<label>
<inputtype="checkbox"v-model="item.checked">{{item.text}}
</label>
</div>
</li>
</template>
<!--itemslisttemplate-->
<templateid="items-template">
<ul>
<item-componentv-for="iteminitems":item="item">
</item-component>
</ul>
</template>
<!--changetitletemplate-->
<templateid="change-title-template">
<div>
<em>Changethetitleofyourshoppinglisthere</em>
<inputv-bind:value="value"v-on:input="onInput"/>
</div>
</template>
Thus,ourmaincomponents'markupwillconsistofsomecomponents:
<divid="app"class="container">
<h2>{{title}}</h2>
<add-item-component></add-item-component>
<items-component:items="items"></items-component>
<divclass="footer">
<hr/>
<change-title-componentv-model="title"</change-title-component>
</div>
</div>
Asyoucansee,themajorityofeachtemplateisaplaincopyandpasteofthecorrespondingHTMLcode.
However,therearesomesignificantdifferences.Thelistitemtemplate,forexample,isslightlychanged.Youhavealreadylearnedandusedthev-fordirectivepreviously.Inthepreviousexamples,weusedthisdirectivewithHTMLelementssuchas<li>.NowyouseethatitcanalsobeusedwithVuecustomcomponents.
Youmighthavealsonoticedasmalldifferenceinthechangetitletemplate.NowithasavalueboundtoitandemitstheonInputmethodboundtothev-on:inputmodifier.Asyouhavelearnedintheprevioussection,childrencomponentscannotdirectlyaffectdirectlyaparent'sdata,whichiswhywehavetousetheeventssystem.
DefiningandregisteringallthecomponentsHavealookattheJavaScriptcodeinourpreviousshoppinglistapplication:https://jsfiddle.net/chudaol/c8LjyenL/.Let'saddthecodethatcreatesVuecomponents.WewillusetheIDsofalreadydefinedtemplatesfortheirtemplateattribute.Also,donotforgetaboutthepropsattributetopassthepropertiesfromtheparentapplication.Thus,weaddthefollowingcode:
//additemcomponent
Vue.component('add-item-component',{
template:'#add-item-template',
data:function(){
return{
newItem:''
}
}
});
//itemcomponent
Vue.component('item-component',{
template:'#item-template',
props:['item']
});
//itemscomponent
Vue.component('items-component',{
template:'#items-template',
props:['items']
});
//changetitlecomponent
Vue.component('change-title-component',{
template:'#change-title-template',
props:['value'],
methods:{
onInput:function(event){
this.$emit('input',event.target.value)
}
}
});
Asyoucansee,inpropsofeachcomponent,wehavepasseddifferentdataattributes—onlythosethatspecificallyconcernthecomponent.WehavealsomovedthenewItemattributetothedataattributeofadd-item-component.Inchange-title-component,wehaveaddedtheonInputmethodthatemitstheinputevent,sothetitleintheparentcomponentisaffectedbywhatevertheusertypesintheinputbox.
OpentheHTMLfileinthebrowser.Theinterfaceisexactlythesameasitwasearlier!ThecompletecodeofwhatwehavedoneinthissectioncanbefoundintheJSFiddleathttps://jsfiddle.net/chudaol/xkhum2ck/1/.
ExerciseAlthoughourapplicationlooksexactlyasitwaslookingearlier,itsfunctionalitywaslost.Notonlydoesitnotadditems,butitalsoshowstheuglyerrorinthedevtoolsconsole.
Pleaseusetheeventsemittingsystemtobringtheaddingitemsfunctionalityback.
ApossiblesolutionforthisexercisecanbefoundintheAppendix,SolutionstoExercises.
Single-filecomponentsWeknowfromtheoldbestpracticesthatitisalwaysgoodtoseparateHTMLfromCSSandJavaScriptfiles.SomemodernframeworkssuchasReactarerelaxingandgraduallywipingoutthisrule.Nowadays,youwillnotbeshockedbylookingatthesmallfileorthecomponentthatcontainsitsownmarkup,style,andapplicationcodeinsideit.Actually,forsmallcomponents,weevenfinditmoreconvenienttohavesucharchitecture.Vuealsoallowsdefiningeverythingrelatedtothesamecomponentinthesamefile.Thiskindofcomponentisknownasasingle-filecomponent.
Note
Asingle-fileVuecomponentisafilewitha.vueextension.Theapplicationthatcontainssuchcomponentscanbebuiltusingthewebpackvueconfiguration.Toscaffoldanappwithsuchaconfiguration,theeasiestwayistousevue-cli(https://github.com/vuejs-templates/webpack).
AVuecomponentcanhaveuptothreesectionsinit:
<script>
<template>
<style>
Eachofthesesectionsisresponsibleforexactlywhatyouarethinking.Putintothe<template>tagwhatevertheHTMLtemplateshouldberesponsiblefor,putintothe<script>tagtheJavaScriptcoderesponsiblefortheVuecomponent,methods,data,props,andsoon.The<style>tagshallcontaintheCSSstyleforthegivencomponent.
Doyourememberourhello-component?HavealookatitintheJSFiddleathttps://jsfiddle.net/chudaol/mf82ts9a/2/.
Startbyscaffoldingtheappusingthewebpack-simpleconfigurationwithvue-cli:
npminstall-gvue-clivueinitwebpack-simplehello
TorewriteitasaVuecomponent,wecreateourHelloComponent.vuefileandaddthefollowingcode:
<template>
<h1>{{msg}}</h1>
</template>
<script>
exportdefault{
data(){
return{
msg:'Hello!'
}
}
}
</script>
NotethatwedonotneedtospecifythetemplateinourJavaScriptcomponentdefinition.Beingasingle-filecomponent,itisimplicitthatthetemplatethatshouldbeusedistheonedefinedinthisfile.YoumightalsohavenoticedthatweuseES6styleinhere.Also,donotforgetthatthedataattributeshouldbeafunctionandnotanobject.
Inourmainscript,wehavetocreatetheVueappandinstructittouseHelloComponent:
importVuefrom'vue'
importHelloComponentfrom'./HelloComponent.vue'
newVue({
el:'#app',
components:{HelloComponent}
});
Ourindex.htmlmarkupwillnotchange.Itwillstillinvokehello-component:
<body>
<divid="app">
<hello-component></hello-component>
</div>
<scriptsrc="./dist/build.js"></script>
</body>
Nowwejustneedtoinstallnpmdependencies(ifyoustillhaven'tdoneso)andbuildtheapplication:
npminstall
npmrundev
Onceyoudoit,yourbrowserwillautomaticallyopenthelocalhost:8080page!
Checkthecompletecodeinthechapter3/hellofolder.
Youcanalsotest,modify,retest,andcheckthehellocomponentinthewebpackbinathttp://www.webpackbin.com/N1LbBIsLb.
Tip
WebpackbinisaniceservicetorunandtestapplicationsbuiltwithWebpack.Itisaverynicetooleventhoughit'sstillinbeta.Asit'sstillyoung,itstillhassomeminorissues.Forinstance,ifyoutrytodownloadthepackageoftheentireproject,itwillnotbuild.
PluginsforIDEsVuecreatorsandcontributorsthoughtaboutdevelopersanddevelopedpluginsforalargesetofmodernIDEs.Youcanfindthemathttps://github.com/vuejs/awesome-vue#syntax-highlighting.IfyouarelikemeanduseWebStormIDEbyIntelliJ,followtheseinstructionstoinstalltheVuesupportplugin:
1. GotoPreferences|Plugins.2. ClickonBrowserepositories.3. Typevueinthesearchbox.4. SelectVue.jsandclickontheInstallbutton:
InstallingtheVuepluginforwebstormIDE
StyleandscopeItisprettyobviousthatthetemplateandthescriptofthecomponentbelongonlytoit.However,thesamedoesnotapplytostyle.Try,forexample,toaddastyletagtoourhellocomponentandaddtheCSSruleforthe<h1>tagtohavetheredcolor:
<style>
h1{
color:red;
}
</style>
Now,whenthepageisrefreshed,itisquiteexpectedthatthecoloroftheHello!headerchangestored.Nowtrytoaddthe<h1>tagtothemainindex.htmlfile.Youmightbesurprised,butitwillalsobered:
<divid="app">
<h1>Thisisasinglefilecomponentdemo</h1>
<hello-component></hello-component>
</div>
Allthe<h1>tagshavethestylethatwedefinedinsideacomponent
Tomakethestylebeattachedonlytothescopeofthecomponent,weneedtoindicatetheattributescopedtothe<style>tag:
<stylescoped>
h1{
color:red;
}
</style>
Lookatthepageandyou'llseethatonlytheHello!textisred,theotherh1hasitsdefaultstyle.
Hot-reloadingYoumighthavenoticedthatnowInolongeraskyoutorefreshthepagebuttolookatthepage.Thisisbecausethepageisautomaticallyrefreshedoneachchangewhentheapplicationisbootstrappedusingvue-cliWebpackscaffoldingapproach.Themagichappensthankstothevue-hot-reloadAPIthatwatchestheapplication'sfilesandtellsthebrowsertoautomaticallyreloadeverytimesomethinghaschanged!Yay!
PreprocessorsIfyouareintopreprocessors,youaremorethanwelcometousetheminyour.vuecomponents.Thisispossibleduetovue-loaderthatallowsusingWebpackloaders.
Note
Youcanfindmoreaboutvue-loadersandpreprocessorsinthetutorialathttp://vue-loader.vuejs.org/en/.
HTMLpreprocessors
Inordertobeabletouseapreprocessorinasingle-fileVuecomponents,justaddthelangattributetothe<template>tag!Donotforgettoinstallthecorrespondingnodemodule:
npminstalljade--save-dev
Usingjade,forexample,inourhellocomponent'stemplate,wouldbeaseasyasfollows:
<templatelang="jade">
h1{{msg}}
</template>
CSSpreprocessors
ThesamelogicappliestotheCSSpreprocessors.Let'sseehowtouse,forexample,asasspreprocessor:
<stylelang="sass"scoped>
$red:red;
h1{
color:$red;
}
</style>
Tip
Likeinthepreviousexample,donotforgettoinstallthecorrespondingloaderforthistowork:npminstallsass-loadernode-sass--save-dev
JavaScriptpreprocessors
ItisalsopossibletouseanyJavaScriptpreprocessors.Likeinthetwopreviousexamples,justusethelangattributetospecifythepreprocessortouse.Anddonotforgettoinstallitvianpm!
>npminstallcoffee-loadercoffee-script--save-dev
<scriptlang="coffee">
exports.default=data:->
{msg:'Hello!'}
</script>
Rewritingourshoppinglistapplicationwithsingle-filecomponentsNowthatwealreadyknowsomuchaboutcomponentsandhowtousethem,andalsoknownicetechniquestomakeourcodeeasiertowrite,let'sgetbacktoourshoppinglistandrewriteitassingle-filecomponent'sVueapplication.Tohaveaneasysetup,wecanusevue-cliwithWebpackconfiguration.Actually,we'vealreadydoneitinChapter2,Fundamentals-InstallingandUsing.So,justfindthisapplicationandbepreparedtostartworkingonit.Ifyoucannotfindit,youcaneasilycreateit:
#installvue-cliifyoustillhadn'tinstalledit
$npminstallvue-cli-g
#bootstraptheapplication
$vueinitwebpackshopping-list
$cdshopping-list
$npminstall
$npmrundev
Ensurethatyourindex.htmlfilelookslikethefollowing:
<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>shopping-list</title>
<linkrel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/
3.3.6/css/bootstrap.min.css">
</head>
<body>
<app></app>
</body>
</html>
Andyourmain.jsfileshouldlooklikethefollowing:
importVuefrom'vue'
importAppfrom'./App'
newVue({
el:'app',
components:{App}
})
Wearenowreadytocreateourcomponentsandtopopulateourapplicationwiththem.Ofcourse,yourememberthatourshoppinglisthasessentiallyfourcomponents:
AddItemComponent:Thecomponentresponsibleforaddinganewitemtotheshoppinglist
ItemComponent:ThecomponentresponsiblefortherenderingofthenewitemintheshoppinglistitemslistItemsComponent:ThecomponentresponsiblefortherenderingandmanagingthelistofItemComponent
ChangeTitleComponent:Thecomponentresponsibleforchangingthetitleofthelist
Let'screatealloftheminthecomponentsfolder.Tostartwith,justincludethreeemptysections(<template>,<script>,and<style>)ineachofthemandinvoketheminthecorrectplaceswithinthemainApp.vuecomponent.Pleaseputsomethingintothetemplatethatwillallowustovisiblyidentifythedifferentcomponentsonthepage.So,thecodeofallourfourcomponentswilllooklikethefollowing:
Thecodeforallfourcomponentsoftheshoppinglistapplication
NowopentheApp.vuecomponent.Thisisourmaincomponentthatwillassembleallthecomponentstogether.
Removeeverythingfromthe<template>,<script>,and<style>tags.Wewillnowstarttobuildourapplication.
Firstofall,wemustimportthecomponentsthatwillbeusedbyApp.vue(inthiscase,allofthem).
Tip
Donotforgetthat,asweareusingES2015inthisapplication,wecanuseimport/exportandalltheotherbeautifulES2015things.
Insidethe<script>tag,let'simportthecomponentsandexporttheobjectthatwillcontaintheimportedcomponentsanddatafunctionthatreturnstheshoppinglist'sitems:
<script>
importAddItemComponentfrom'./components/AddItemComponent'
importItemsComponentfrom'./components/ItemsComponent'
importChangeTitleComponentfrom'./components/ChangeTitleComponent'
exportdefault{
components:{
AddItemComponent,
ItemsComponent,
ChangeTitleComponent
},
data(){
return{
items:[{text:'Bananas',checked:true},
{text:'Apples',checked:false}]
}
},
methods:{
addItem(text){
this.items.push({
text:text,
checked:false
})
}
}
}
</script>
Ourtemplatecanbasicallybethesameasthetemplatethatwehavebuiltintheshoppinglistapplicationusingsimplecomponents.Let'sjustremoveeverythingconcerningthemodelsanddatabindingfornow.First,insertthecomponentresponsibleforaddingitems,thenthecomponentcontainingalltheitems,andthen,inthefooter,thecomponentresponsibleforchangingthetitle.
Ourtemplatewillthenlooklikethefollowing:
<template>
<divid="app"class="container">
<h2>{{title}}</h2>
<add-item-component></add-item-component>
<items-component></items-component>
<divclass="footer">
<hr/>
<change-title-component></change-title-component>
</div>
</div>
</template>
Youstillrememberthatthenamesofthecomponents'variablesareCamelCased,andwhentheyareusedinsidethetemplate,theyshouldbeinvokedusingkebab-case,right?Good,let'sseehowitlooksinthebrowser:
Shoppinglistapplicationbuiltofsingle-filecomponents
Doesn'tseemthatbeautiful,right?Let'sfilleachofthecomponentswiththeirtemplates.
Tip
WewillcontinueusingBootstrap'sCSSstyleforthisapplication.Includeitgloballyintheindex.htmlfile:<linkrel="stylesheet"href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
AddItemComponentOpenAddItemComponent.vue.Let'sfillits<template>.Itwilllooklikethefollowing:
<template>
<div>
<divclass="input-group">
<inputtype="text"class="inputform-control"
placeholder="addshoppinglistitem">
<spanclass="input-group-btn">
<buttonclass="btnbtn-default"type="button">Add!</button>
</span>
</div>
</div>
</template>
Ifyoulookatthepageinyourbrowser,youcanalreadyseethatitchangedandbecamemorerecognizableasourshoppinglistapplication.
ConfiguringItemComponentandItemsComponentLet'snowmovetotheItemComponent.WewilljustcopyandpastetheHTMLfromthesimplecomponentexample:
//ItemComponent.vue
<template>
<li:class="{'removed':item.checked}">
<divclass="checkbox">
<label>
<inputtype="checkbox"v-model="item.checked">{{item.text}}
</label>
</div>
</li>
</template>
Let'salsoaddsomescopedstyleforthiscomponent.Thiscomponent'sspecificstyleisthestylethathastodowiththe<li>,<span>,andclass.removed.Let'scopyandpastethemintothiscomponent:
//ItemComponent.vue
<stylescoped>
.removed{
color:gray;
}
.removedspan{
text-decoration:line-through;
}
li{
list-style-type:none;
}
lispan{
margin-left:5px;
}
</style>
NowopenItemsComponents.Asyouremember,itisalistoftheItemComponentelements.Evenifyoudonotremember,Iguessthatthepluralcharacteristicsofthenameofthiscomponentsuggeststhis.InorderforittobeabletousetheItemComponent,itmustimportitandregisterinthecomponentsproperty.So,let'smodifythescriptfirst:
//ItemsComponent.vue
<script>
importItemComponentfrom'./ItemComponent'
exportdefault{
components:{
ItemComponent
}
}
</script>
Nowyoucanuseitem-componentin<template>!Doyoustillrememberhowtoiteratewith
vue.js?Ofcourseyoudo!Thatiswhyyouareopeningthe<template>tagrightnowandwritingthefollowingcode:
//temsComponent.vue
<template>
<div>
<item-componentv-for="iteminitems":item="item">
</item-component>
</div>
</template>
Ifyoucheckthepagenow,you'llbesurprisedtoseethatthingsactuallydonotwork.Thewebconsoleisfulloferrors.Canyoufigureoutwhy?
Doyourememberthatwhenchildrencomponentswanttohaveaccesstotheparent'sdata,theymustdeclare"props"onthecomponentinitialization?Thisisexactlywhatwe'veforgottenaboutonthedeclarationofbothItemsComponentandItemComponent.
Firstofall,withinApp.vue,binditemstotheitems-componentinvocation:
//App.vue
<items-component:items="items"></items-component>
ThenaddthepropsattributetoItemsComponent:
//ItemsComponent.vue
<script>
importItemComponentfrom'./ItemComponent'
exportdefault{
components:{
ItemComponent
},
props:['items']
}
</script>
NowgobacktoItemComponentandaddthepropsproperty:
//temComponent.vue
<script>
exportdefault{
props:['item']
}
</script>
Checkthepagenow.Nowitindeedcontainsthelistofitemsandhasalookandfeelalmostthesameasithadwhenwefirstcreatedit.Checkthefullcodeforthissectioninthechapter3/shopping-listfolder.
ExerciseFinishtheshoppinglistapplicationsothatithasthesamefunctionalityasbefore.
There'snotsomuchleftandI'msureyouwillbedonewithitinlessthanhalfanhour.ThepossiblesolutiontothisexercisecanbefoundintheAppendix,SolutionstoExercises.
RewritingthePomodoroapplicationwithsingle-filecomponentsIhopeyoustillrememberandpossiblyevenusethePomodoroapplicationthatwedevelopedinthefirstchapterofthisbook.
Iwouldliketorevisititnowandtodothesameexercisewedidintheprevioussection—definethecomponentsoftheapplicationandrewriteitusingthesecomponents.
Let'shavealookatourPomodoroapplication.AndnowIamgoingtospoilyou:I'llincludeascreenshotthatalreadycontainsthekittensthatarebeingshownduringtherestingtimeusinghttp://thecatapi.com/api:
ThePomodoroapplicationinitsRest!state
Therearesomeeasilyidentifiablecomponents:
Thecomponentofthecontrols(start,pause,end),let'snameitControlsComponentThecomponentofthetimecountdown,CowntdownComponentThecomponentofthetitleofthecurrentstate(Work!/Rest!),StateTitleComponentThecomponentofthekittensrenderingthatdependsonthestate(workingorresting),KittensComponent(thisismyfavoriteone!)
Now,pleasestopstaringatthekittenandlet'sstartimplementingourPomodoroapplicationusingsingle-filecomponents!Somefirststepstoscaffoldtheapplicationareasfollows:
1. StartbyopeningthescaffoldedPomodoroapplicationfromthepreviouschapterorcreateanewapplicationbasedontheWebpacktemplate.
2. Runnpminstallandnpmrundevintheapplicationfolder.3. Ensurethatyourindex.htmllookslikethefollowing:
<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>pomodoro</title>
</head>
<body>
<app></app>
</body>
</html>
4. Ensurethatyourmain.jsfilelookslikethefollowing:
importVuefrom'vue'
importAppfrom'./App'
/*eslint-disableno-new*/
newVue({
el:'app',
components:{App}
})
5. Openyourbrowsertothepagelocalhost:8080.6. Then,likeinthepreviousexample,gotothecomponentsfolderandcreateallthe
necessary.vuecomponents.7. GotoApp.vue,andimportandregisterallthecreatedcomponents.8. Inthe<template>sectionofeachofthecomponents,putsomethingthatwilluniquely
identifyitsothatwecaneasilyrecognizeitwhencheckingthepage.
Youwillalmostcertainlycometothestructureandtheinitialcode,whichlookssomethinglikethefollowing:
TheveryinitialstateofthePomodoroapplicationimplementedwithsingle-filecomponents
Now,let'sassumethatourcomponentsarereadytouseandlet'splacethemwheretheybelongintotheapplication'slayout,accordingly.
Iwilljustslightlyremindyouhowthewholeapplication'smarkuplookedearlier:
<divid="app"class="container">
<h2>
<span>Pomodoro</span>
//LookslikeourControlsComponent
<button>
<iclass="glyphiconglyphicon-play"></i>
</button>
<button>
<iclass="glyphiconglyphicon-pause"></i>
</button>
<button>
<iclass="glyphiconglyphicon-stop"></i>
</button>
</h2>
//LookslikeourStateTitleComponent
<h3>{{title}}</h3>
//LookslikeourCountdownComponent
<divclass="well">
<divclass="pomodoro-timer">
<span>{{min}}</span>:<span>{{sec}}</span>
</div>
</div>
//LookslikeourKittensComponent
<divclass="well">
<img:src="catImgSrc"/>
</div>
</div>
You'veprobablynoticedthatIremovedsomepartsthatareresponsiblefortheclassbindingsoractionshandlers.Donotworry.RememberScarlettO'HarainGonewiththeWind?Sheusedtosay,
"Ican'tthinkaboutthatrightnow.I'llthinkaboutthattomorrow."
(http://goo.gl/InYm8e).ScarlettO'Harawasawisewoman.BelikeScarlettO'Hara.Fornow,wewillfocusmerelyonthe<template>tagforourApp.vue.Everythingelsewillcomelaterandwewillthinkaboutitthen.NowwecanbasicallycopyandpastethisHTMLsnippetandreplacethesectionsthatweidentify,suchasthecomponentswiththeirkebab-casenames.So,thetemplateinApp.vuewilllooklikethefollowing:
//App.vue
<template>
<divid="app"class="container">
<h2>
<span>Pomodoro</span>
<controls-component></controls-component>
</h2>
<state-title-component></state-title-component>
<countdown-component></countdown-component>
<kittens-component></kittens-component>
</div>
</template>
Abitsmaller,huh?Checkyourbrowserwithyourappopened.NotverybeautifulandforsurehasnothingtodowithourPomodoroapplication,but...itworks!
Pomodoroapplicationbootstrappedasasingle-filecomponentsapplication
Whatshouldwedonow?Copythecorrespondingmarkuptotheircomponent's<template>sections.Pleasedothistinycopyandpastebyyourself,letitbeasmallhomeexercise.However,ifyouwanttocheckyourself,takealookatthechapter3/pomodorofolder.That'sitfornow!Allthedatabindingsandinterestingstuffwillcomeinthenextchapter.Sodonotclosethebook.However,donotforgettotakesomePomodoropauses.
ReactivebindingofCSStransitionsJustbeforethetransitiontothenextchapter,whichwilltalkalotaboutdifferenttypesofdatabinding,Iwouldliketogiveyoujustatinyflavorofsomethinginterestingthatispossibletobind.Iknowthatyoupayalotofattentiontothewords,mydearreader.So,you'vealreadyfoundthewordtransitiontwotimesuntilnow,andyouhaveprobablyguessedthatwecanactuallybindCSStransitionstothedatachanges.
So,imaginethatyouhaveanelementthatshouldonlybeshownifthedataattributeshowistrue.Thisiseasy,right?Youalreadyknowthev-ifdirective:
<divv-if="show">hello</div>
Thus,whenevertheshowattributeischanged,this<div>behavesaccordingly.Imaginethatonhiding/showing,youwouldliketoapplysomeCSStransition.WithVueyoucanusethespecialtransitionwrappercomponenttospecifythetransitiontouseondatachanging:
<transitionname="fade">
<divv-if="show"transition="my">hello</div>
</transition>
Afterthat,youjusthavetodefineCSSrulesforthefade-enter,fade-leave,fade-enter-active,andfade-leave-activeclasses.ChecktheofficialVuedocumentationpageregardingtheseclassesathttps://vuejs.org/v2/guide/transitions.html#Transition-Classes.
Let'sseehowitworksinourkittenscomponentexample.Let'sstartbyaddingthev-ifdirectivetothekittens-componentinsideApp.vue:
<template>
<...>
<kittens-componentv-if="kittens"></kittens-component>
<...>
</template>
Also,weshouldaddthedatafunctioninthe<script>tagofApp.vue(let'salsomakeitglobalsothatwecanmodifyitfromthedevtoolsconsole):
<script>
//...//
window.data={
kittens:true
};
exportdefault{
//.....//
data(){
returnwindow.data
}
}
</script>
Lookatthebrowser:everythingseemsunchanged.Openthedevtoolsconsoleandtypethefollowing:
data.kittens=false
You'llseethatthekittenscomponentwilldisappearfromthepage.Ifyoutypethefollowing,itwillappearagain:
data.kittens=true
Tip
Ihopeyouhaven'tforgottentoincludeBootstrap'sCSSinthemainindex.htmlfile.Withoutit,you'llseenoappearing/disappearingatallbecauseour<div>taghasnoinformationnoranyclassappliedtoit:<linkrel="stylesheet"href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
However,wearetalkingabouttheCSStransitionsandnotaboutsimplyhiding/showingstuff.Nowlet'sapplytheCSSfadetransitiontoourkittenscomponent.Justaddawrappercomponenttransitionwithanameattributefade:
<template>
<...>
<transitionname="fade">
<kittens-componentv-if="kittens"></kittens-component>
</transition>
<...>
</template>
Nowifwedefinenicerulestothecorrectclasses,we'llseeaniceCSStransition.Let'sdoit.AddthefollowingCSSrulesinsidethe<style>tag:
<stylescoped>
.fade-enter-active,.fade-leave-active{
transition:opacity.5s
}
.fade-enter,.fade-leave-active{
opacity:0
}
</style>
Lookatthepageagain.Opentheconsoleandtypedata.kittens=falseanddata.kittens=trueagain.Nowyoucanseeanicefadetransitionhappeningoneachdatachange.Inthenextchapter,wewilltalkmoreabouttransitionsinVue.jsandapplythemtoourapplications.
SummaryInthischapter,youlearnedaboutVuecomponentsandhowtousethem.Yousawhowtocreateandregisterthemusingaclassicapproach(applicationthatusesHTML,CSS,andJavaScript)andyoualsosawhoweasyitistocreateandmanipulatethemusingasingle-filecomponentsapproach.Thingstoretain:
WhilevariablesarecreatedusingCamelCasedformat,inordertobeabletousecomponentsinsidetemplates,youmustapplythecorrespondingkebab-casedformat,forexample,MyBeautifulComponent->my-beautiful-componentAttributesdataandelinsidethecomponentmustbefunctionsandnotobjects:{data:function(){}}
Ifyouwantthestyleofthecomponentnottoleaktotheglobalscope,addascopedattributetoit:<stylescoped></style>
Wehavealsorewrittenourapplicationsusingsingle-filecomponentsandtouchedonthedatabindingtotheCSStransitionsslightly.
Inthenextchapter,wewilldivedeeplyintoallthetypesofdatabinding,includingCSSandJavaScripttransitions.Wewillbringourapplicationsbacktolifeusingdatabinding.Lastbutnotleast,wewillseemorecats!
Chapter4.Reactivity–BindingDatatoYourApplicationInthepreviouschapter,youlearnedoneofthemostimportantconceptsofVue.js:components.Yousawhowtocreatecomponents,howtoregister,howtoinvoke,andhowtouseandreusethem.Youalsolearnedtheconceptofsingle-filecomponentsandevenusedthemintheshoppinglistandPomodoroapplications.
Inthischapter,wewillgodeeperintotheconceptofdatabinding.Wehavealreadytalkedaboutitearlier,soyouarealreadyfamiliarwithit.Wewillbinddatainallpossiblewaysinourcomponents.
Summingitup,inthischapter,wearegoingto:
RevisitthedatabindingsyntaxApplydatabindinginourapplicationsIterateoverthearrayofelementsandrendereachelementusingthesametemplatewithdifferentdataRevisitandapplytheshorthandsofdataandeventsbindinginourapplications
RevisitingdatabindingWehavebeentalkingaboutdatabindingandreactivitystartingfromtheveryfirstchapter.So,youalreadyknowthatdatabindingisamechanismofpropagatingchangesfromthedatatothevisiblelayerandviceversa.Inthischapter,wewillcarefullyrevisitallthedifferentwaysofdatabindingandapplytheminourapplications.
InterpolatingdataLet'simaginethefollowingpieceofHTMLcode:
<divid="hello"></div>
Also,imaginethefollowingJavaScriptobject:
vardata={
msg:'Hello'
};
Howcanwerenderthevaluesofdataentriesonthepage?HowcanweaccessthemsothatwecanusetheminsideourHTML?Actually,wehavebeendoingthisalotwithVue.jsduringthelasttwochapters.Thereisnoprobleminunderstandinganddoingitagainandagain.
"Repetitioestmaterstudiorum"
Ifyouarealreadyaprofessionalofdatainterpolation,justskipthissectionandproceedtotheexpressionsandfilters.
So,whatshouldwedotopopulatethe<div>withthevalueofmsg?Ifwegotheold-fashionedjQueryway,wewouldprobablydosomethinglikethefollowing:
$("#hello").text(data.msg);
Butthen,duringruntime,ifyouchangethevalueofmsgandifyouwantthischangetobepropagatedtotheDOM,youmustdoitmanually.Bysimplychangingthedata.msgvalue,nothingwillhappen.
Forexample,let'swritethefollowingcode:
vardata={
msg:'Hello'
};
$('#hello').text(data.msg);
data.msg='Bye';
Thenthetextthatwillappearinthe<div>will,ofcourse,beHello.CheckthisJSFiddleathttps://jsfiddle.net/chudaol/uevnd0e4/.
WithVue,thesimplestinterpolationisdonewith{{}}(handlebarsannotation).Inourexample,wewouldwritethefollowingHTMLcode:
<divid="hello">{{msg}}</div>
Thecontentofthe<div>thusbecomesboundtothemsgdata.Eachtimemsgchanges,thecontentofthedivchangesautomaticallyfollowingitscontent.Havealookatthejsfiddle
exampleathttps://jsfiddle.net/chudaol/xuvqotmq/1/.data.msgisalsochangedaftertheVueinstantiation.Thevaluethatappearsonthescreenisthenewone!
Itisstillone-waybindinginterpolation.IfwechangethevalueintheDOM,nothingwillhappentodata.Still,ifweonlyneedthevaluesofthedatatoappearintheDOMandtobechangedaccordingly,itisaperfectandvalidapproach.
Atthismoment,itshouldbereallyclearthatifwewanttousethevaluesofthedataobjectinsidethetemplate,weshouldsurroundthemwith{{}}.
Let'saddthemissinginterpolationstoourPomodoroapplication.Pleasecheckthecurrentsituationinthechapter4/pomodorofolder.Ifyourunnpmrundevandhavealookattheopenedpage,youwillseethatthepagelookslikethefollowing:
MissinginterpolationsinourPomodoroapplication
Fromtheveryfirstglanceatthepage,weareabletoidentifywhatismissingthere.
Thepageismissingthetimer,thekittens,thetitleofthePomodorostate(theonethatdisplaysWork!orRest!),andthelogicthatshowsorhidesthekittens'placeholderaccordingtothePomodorostate.Let'sstartbyaddingthetitleofthePomodorostateandtheminutesandsecondsofthePomodorotimer.
AddingtitleofthePomodorostateFirstofall,weshoulddecidewhatcomponentthiselementshouldbelongto.Havealookatourfourcomponents.ItismorethanobviousthatitshouldbelongtoStateTitleComponent.Ifyoulookatthefollowingcode,youwillseethatitactuallyalreadyinterpolatesthetitleinitstemplate:
//StateTitleComponent.vue
<template>
<h3>{{title}}</h3>
</template>
<stylescoped>
</style>
<script>
</script>
Good!Inthepreviouschapter,we'vealreadydonemostofthework.Nowwejusthavetoaddthedatathatmustbeinterpolated.Inthe<script>tagofthiscomponent,let'saddthedataobjectwiththetitleattributeinside.Fornow,let'shardcodeittooneofthepossiblevaluesandthendecidehowtochangeit.Whatdoyouprefer?Work!orRest!?IthinkIknowtheanswer,solet'saddthefollowingcodetoourscripttag:
//StateTitleComponent.vue
<script>
exportdefault{
data(){
return{
title:'LearningVue.js!'
}
}
}
</script>
Let'sleaveitlikethisfornow.Wewillcomebacktothislaterinthemethodsandeventhandlingsection.
ExerciseInthesamewayinwhichweaddedthetitleofthePomodorostate,pleaseaddtheminutesandsecondstimercounterstotheCountDownComponent.Theycanbehardcodedfornow.
UsingexpressionsandfiltersInthepreviousexample,wehaveusedsimplepropertykeysinsidethe{{}}interpolations.Actually,Vuesupportsalotmoreinsidethesenicecurlybrackets.Let'sseewhatitispossibletodothere.
ExpressionsItmightsoundunexpected,butVuesupportsfullJavaScriptexpressionsinsidethedatabindingbrackets!Let'sgotoanyofthePomodoroapplicationcomponentsandaddanyJavaScriptexpressiontothetemplate.Youcandosomeexperimentsinthechapter4/pomodoro2folder.
Try,forexample,toopentheStateTitleComponent.vuefile.Let'saddsomeJavaScriptexpressioninterpolationtoitstemplate,forexample:
{{Math.pow(5,2)}}
Actually,youjustneedtouncommentthefollowinglines:
//StateTitleComponent.vue
<!--<p>-->
<!--{{Math.pow(5,2)}}-->
<!--</p>-->
Youwillseenumber25onthepage.Nice,isn'tit?Let'sreplacesomeofourdatabindingsinthePomodoroapplicationwithaJavaScriptexpression.Forexample,intheCountdownComponentcomponent'stemplate,twodirectives,eachforminandsec,canbereplacedbyoneexpression.Currentlyitlooksasfollows:
//CountdownComponent.vue
<template>
<divclass="well">
<divclass="pomodoro-timer">
<span>{{min}}</span>:<span>{{sec}}</span>
</div>
</div>
</template>
Wecanreplaceitwiththefollowingcode:
//CountdownComponent.vue
<template>
<divclass="well">
<divclass="pomodoro-timer">
<span>{{min+':'+sec}}</span>
</div>
</div>
</template>
Whereelsecanweaddsomeexpressions?Let'shavealookatStateTitleComponent.Atthismoment,weusethehardcodedtitle.Weknow,however,thatsomehowitshoulddependonthePomodorostate.Ifitisintheworkingstate,itshoulddisplayWork!,otherwiseitshoulddisplayRest!.Let'screatethisattributeandcallitisworking,andlet'sassignittothemainApp.vuecomponentbecauseitseemstobelongtotheglobalapplicationstate.ThenwewillreuseitinsidetheStateTitleComponentcomponent'spropsattribute.Thus,openApp.vue,and
addtheBooleanpropertyisworkingandsetittotrue:
//App.vue
<...>
window.data={
kittens:true,
isworking:true
};
exportdefault{
<...>
data(){
returnwindow.data
}
}
Let'snowreusethispropertyinStateTitleComponent,addtwostringpropertiesforeachofthepossibletitles,and,finally,addtheexpressioninthetemplatethatwillconditionallyrenderonetitleoranotheraccordinglytothecurrentstate.Thus,thescriptofthecomponentwilllooklikethefollowing:
//StateTitleComponent.vue
<script>
exportdefault{
data(){
return{
workingtitle:'Work!',
restingtitle:'Rest!'
}
},
props:['isworking']
}
</script>
Nowwecanconditionallyrenderonetitleoranotherbasedontheisworkingproperty.Thus,thetemplateofStateTitleComponentwilllooklikethefollowing:
<template>
<div>
<h3>
{{isworking?workingtitle:restingtitle}}
</h3>
</div>
</template>
Lookattherefreshedpage.Strangely,itshowsRest!asthetitle.HowdidthishappeniftheisworkingpropertyissettotrueinApp.vue?WesimplyforgottobindthispropertyonthecomponentinvocationintheApp.vuetemplate!OpentheApp.vuecomponentandaddthefollowingcodeonthestate-title-componentinvocation:
<state-title-componentv-bind:isworking="isworking"></state-title-component>
Now,ifyoulookatthepage,thecorrecttitleappearsasWork!Ifyouopenthedevtools
consoleandtypedata.isworking=false,youwillseethetitlechanging.
Iftheisworkingattributeisfalse,thetitleisRest!,asshowninthefollowingscreenshot:
Iftheisworkingattributeistrue,thetitleisWork!,asshowninthefollowingscreenshot:
FiltersBesidesexpressionsinsidethecurlyinterpolationbrackets,itisalsopossibletousefiltersthatareappliedtotheresultoftheexpression.Filtersarejustfunctions.Theyarecreatedbyusandappliedbyusingthepipesymbol:|.Ifyoucreateafilterthatmakeslettersuppercaseandcallituppercase,inordertoapplyit,justuseitafterthepipesymbolinsidethemustacheinterpolation:
<h3>{{title|lowercase}}</h3>
Youcanchainasmanyfiltersasyouwant,forexample,ifyouhavefilterA,B,C,youcandosomethinglike{{key|A|B|C}}.FiltersarecreatedusingVue.filtersyntax.Let'screateourlowercasefilter:
//main.js
Vue.filter('lowercase',(key)=>{
returnkey.toLowerCase()
})
Let'sapplyittothePomodorotitleinthemainApp.vuecomponent.Inordertobeabletousethefilter,weshouldpassthe'Pomodoro'stringinsidethehandlebarsinterpolationnotation.WeshouldpassitasaJavaScriptstringexpressionandapplyafilterusingthepipesymbol:
<template>
<...>
<h2>
<span>{{'Pomodoro'|lowercase}}</span>
<controls-component></controls-component>
</h2>
<...>
</template>
Checkthepage;thePomodorotitlewillactuallyappearwritteninthelowercasesyntax.
Let'srevisitourCountdownTimercomponentandhavealookatthetimer.Fornow,thereareonlyhardcodedvalues,right?Butwhentheapplicationisfullyfunctional,thevalueswillcomefromsomecomputation.Therangeofvalueswillbefrom0to60.Itisokayifthetimershows20:40,butitisnotokayforfewerthantenvalues.Forexample,whenitisonly1minuteand5seconds,itwillbe1:5,whichisnotgood.Weareexpectingtoseesomethinglike01:05.So,weneedtheleftpadfilter!Let'screateit.
Gotothemain.jsfileandaddaleftpadfilteraftertheuppercasefilterdefinition:
//main.js
Vue.filter('leftpad',(value)=>{
if(value>=10){
returnvalue
}
return'0'+value
})
OpentheCountdownComponentcomponentandlet'sagainsplitminandsectothedifferentinterpolationbracketsandaddfilterstoeachofthem:
//CountdownComponent.vue
<template>
<divclass="well">
<divclass="pomodoro-timer">
<span>{{min|leftpad}}:{{sec|leftpad}}</span>
</div>
</div>
</template>
Replaceminandsecindatawith1and5,respectively,andhavealook.Thenumbersappearwithapreceding"0"!
ExerciseCreatetwofilters,uppercaseandaddspace,andapplythemtothetitlePomodoro:
TheuppercasefiltermustdoexactlywhatitsaysitdoesTheaddspacefiltermustaddaspaceontherightofthegivenstringvalue
DonotforgetthatPomodoroisnotakey,soinsidetheinterpolationbrackets,itshouldbetreatedasastring!Thetitlebeforeandafterthisexercisewouldlooksomethinglikethefollowing:
ThetitleofthePomodoroapplicationbeforeandafterapplyingfiltersuppercaseandaddspace
Checkyourself:havealookatthechapter4/pomodoro3folder.
RevisitingandapplyingdirectivesIntheprevioussection,wesawhowtointerpolatetheapplication'sdataandhowtobindittothevisuallayer.Thoughthesyntaxisprettypowerfulandoffersahighpossibilityofdatamodification(usingfiltersandexpressions),ithassomelimitations.Try,forexample,toimplementthefollowingusing{{}}notation:
UsetheinterpolateddataintheuserinputandapplythechangestothecorrespondingdatawhentheusertypesintheinputBindaspecificelement'sattribute(forexample,src)tothedataRendersomeelementconditionallyIteratethroughanarrayandrendersomecomponentwiththeelementsofthearrayCreateeventlistenersontheelements
Let'stryatleastthefirstone.Open,forexample,theshoppinglistapplication(it'sinthechapter4/shopping-listfolder).CreateaninputelementintheApp.vuetemplateandsetitsvalueto{{title}}:
<template>
<divid="app"class="container">
<h2>{{title}}</h2>
<inputtype="text"value="{{title}}">
<add-item-component></add-item-component>
<...>
</div>
</template>
Ohno!Errors,errorseverywhere.Interpolationinsideattributeshasbeenremoved,itsays.DoesitmeanthatpriortoVue2.0youcouldeasilyusetheinterpolationinsideattributes?Yes,andno.Youwouldnotgetanerrorifyou'duseinterpolationsinsideattributes,butchangingthetitleinsidetheinputwouldresultinnothing.InVue2.0,aswellasinthepriorversions,toachievethiskindofbehavior,wemustusedirectives.
Note
Directivesarespecialattributesoftheelementsthathaveav-prefix.Whyv-?BecauseVue!Directivesprovideatinysyntaxthatprovidesarichersetofpossibilitiesthansimpletextinterpolation.Theyhavethepowertoreactivelyapplysomespecialbehaviortothevisuallayeroneachdatachange.
Two-waybindingusingthev-modeldirectiveTwo-waybindingisatypeofbindingwherenotonlydatachangesarepropagatedtotheDOMlayer,butalsothechangesthatoccurtothebounddataintheDOMarepropagatedtothedata.TobindthedatainsuchawaytotheDOM,wecanusethev-modeldirective.
Iamsureyoustillrememberfromthefirstchapterthatthev-modeldirectiveisusedasfollows:
<inputtype="text"v-model="title">
Inthisway,thevalueofthetitlewillappearintheinput,andifyoutypesomethinginthisinput,thecorrespondingchangewillbeimmediatelyappliedtothedataandreflectedinallinterpolatedvaluesonthepage.
Justreplacethehandlebarsnotationwithv-modelandopenthepage.
Trytotypesomethingintheinput.Youwillseehowthetitleisimmediatelychanged!
Justremember,thisdirectivecanonlybeusedwiththefollowingelements:
<input>
<select>
<textarea>
Tryallofthemandthendeletethiscode.Ourmainpurposeistobeabletochangethetitleusingthechangetitlecomponent.
Two-waybindingbetweencomponentsRememberfromthepreviouschapterthattwo-waybindingbetweencomponentscannotbeeasilyachievedusingthev-modeldirective.Duetoarchitecturalreasons,Vuejustpreventschildrenfromeasilychangingtheparents'scope.
That'swhyweusedtheeventssysteminthepreviouschaptertobeabletochangethetitleoftheshoppinglistfromthechildcomponent.
Wewilldoitagaininthischapter.Justwaitcoupleofparagraphsuntilwereachthesectiononv-ondirectives.
Bindingattributesusingthev-binddirectiveThev-binddirectiveallowsustobindanelement'sattributeoracomponentpropertytoanexpression.Inordertoapplyittothespecificattribute,weuseacolondelimiter:
v-bind:attribute
Forexample:
v-bind:src="src"
v-bind:class="className"
Anyexpressioncanbewritteninsidethe"".Thedatapropertiescanbeusedaswell,justlikeinthepreviousexamples.Let'saddthekittenimagetoKittenComponentinourPomodoroapplicationusingthecatapiasthesource.OpenourPomodoroapplicationfromthechapter4/pomodoro3folder.
OpenKittenComponent,addcatimgsrctothecomponent'sdata,andbindittotheimagetemplateusingv-bindsyntaxwiththesrcattribute:
<template>
<divclass="well">
<imgv-bind:src="catImgSrc"/>
</div>
</template>
<stylescoped>
</style>
<script>
exportdefault{
data(){
return{
catimgsrc:"http://thecatapi.com/api/images/get?size=med"
}
}
}
</script>
Openthepage.Enjoythekitten!
PomodoroKittenComponentwithappliedsourceattribute
Conditionalrenderingusingv-ifandv-showdirectivesIfyouhavepaidenoughattentionintheearliersections,andifIaskyoutoconditionallyrendersomething,youmightbeactuallyabletodoitusingJavaScriptexpressionsinsidetheinterpolationbrackets{{}}.
However,trytoconditionallyrendersomeelementorthewholecomponent.Itmightnotbeassimpleasapplyinganexpressioninsidethebrackets.
Thev-ifdirectiveallowstoconditionallyrenderthewholeelement,whichmightalsobeacomponentelementdependingonsomecondition.Theconditioncanbeanyexpressionanditcanusethedatapropertiesaswell.Forexample,wecandothefollowing:
<divv-if="1<5">hello</div>
Or:
<divv-if="Math.random()*10<6">hello</div>
Oreven:
<divv-if="newDate().getHours()>=16">BeerTime!</div>
Orusingthecomponent'sdata:
<template>
<div>
<h1v-if="!isadmin">BeerTime!</h1>
</div>
</template>
<script>
exportdefault{
data(){
return{
isadmin:false
}
}
}
</script>
Thev-showattributedoesthesamejob.Theonlydifferenceisthatv-ifwillorwillnotrendertheelementtotheconditionaccordingly,whereasthev-showattributewillalwaysrendertheelement,justapplyingdisplay:noneCSSpropertywhentheresultoftheconditionisfalse.Let'sseethedifference.Openthebeer-timeprojectinthechapter4/beer-timefolder.Runnpminstallandnpmrundev.OpentheApp.vuecomponentandplaywithtrue/falsevalues,andtrytoreplacev-ifwithv-show.Opendevtoolsandchecktheelementstab.
Let'sfirstcheckhowitlookswhenweswitchbetweentrueandfalseintheisadminpropertyvalueusingv-if.
Whentheconditionismet,everythingappearsasexpected;theelementisrenderedandappearsonthepage:
Conditionalrenderingusingthev-ifdirective.Conditionismet.
Whentheconditionisnotmet,theelementisnotrendered:
Conditionalrenderingusingthev-ifdirective.Conditionisnotmet.
Notethatwhentheconditionisnotfulfilled,thecorrespondingelementisnotrenderedatall!
Let'splaywiththeconditionresultvalueusingthev-showdirective.Whentheconditionismet,itappearsinexactlythesamewayasitwasinthepreviouscaseusingv-if:
Conditionalrenderingusingthev-showdirective.Conditionismet.
Nowlet'scheckwhatwillhappenwiththeelementusingthev-showdirectivewhentherightconditionisnotmet:
Conditionalrenderingusingthev-showdirective.Conditionisnotmet.
Inthiscase,everythingisthesamewhentheconditionismet,butwhentheconditionisnotfulfilled,theelementisrenderedaswellwiththedisplay:noneCSSproperty.
Howdoyoudecidewhichoneisbettertouse?Onthefirstrender,iftheconditionisnotmet,thev-ifdirectivewillnotrendertheelementatall,hencereducingthecomputationcostsontheinitialrendering.But,ifthepropertychangesfrequentlyduringruntime,thecostofrendering/removinganelementishigherthanjusttoapplythedisplay:noneproperty.Thus,usev-showwithfrequentlychangingpropertiesandv-ififtheconditionwillnotchangetoomuchduringruntime.
Let'scomebacktoourPomodoroapplication.KittensComponentshouldbeconditionallyrenderedwhenPomodoroisnotinitsworkingstate.So,openyourPomodoroapplicationcodeinthechapter4/pomodoro4folder.
Whatdoyouthinkshouldbeused?v-iforv-show?Let'sanalyze.Independentlyfromwhatweuse,shouldthiselementbevisibleontheinitialrender?Theanswerisno,becauseontheinitialrender,theuserstartsherworkingdayandstartsthePomodorotimer.Itmightbebettertousev-iftonothavethecostofinitialrenderingwhenthereisnoneed.But,let'sanalyzeanotherfactor—thefrequencyoftogglingthestatethatwillmakethekittenscomponentvisible/invisible.ThiswillhappenateachPomodorointerval,right?After15-20minutesofworkandthenafter5minutesofrestinterval,whichis,actually,notsofrequentandwillnotaffectthecostofrenderingthatmuch.Inthiscase,inmyopinion,itdoesn'tmatterwhichoneyouuse.Let'susev-show.OpentheApp.vuefileandapplythev-showdirectivetothe
kittens-componentinvocation:
<template>
<divid="app"class="container">
<...>
<transitionname="fade">
<kittens-componentv-show="!isworking"></kittens-component>
</transition>
</div>
</template>
Openthepageandtrytotogglethevalueofdata.isworkinginthedevtoolsconsole.Youwillseehowthekittenscontainerappearsanddisappears.
Arrayiterationusingthev-fordirectiveYouprobablyrememberthatarrayiterationisdoneusingthev-fordirectivewiththefollowingsyntax:
<divv-foriteminitems>
item
</div>
Orwithcomponents:
<componentv-foriteminitemsv-bind:componentitem="item"></component>
Foreachiteminthearray,thiswillrenderacomponentandbindthecomponent'sitempropertytothevalueoftheitem.Ofcourse,yourememberthatinsidethe""ofthebindingsyntaxyoucanusewhateverJavaScriptexpressionyouwant.So,justbecreative!
Tip
Donotforgetthatthepropertyweuseinthebindingsyntax(componentitem)shouldbepresentinthecomponent'sdata!
Havealook,forexample,atourshoppinglistapplication(Thechapter4/shopping-listfolder).Italreadyusesthev-forsyntaxinItemsComponenttorenderthelistofitems:
<template>
<ul>
<item-componentv-for="iteminitems":item="item"></item-component>
</ul>
</template>
ItemComponent,inturn,hastheitempropertydeclaredusingprops:
<script>
exportdefault{
props:['item']
}
</script>
Now,let'sdosomethinginterestingwithourshoppinglistapplication.Untilnowweweredealingonlywithoneshoppinglist.Imaginethatyouwanttohaveadifferentshoppinglistfordifferentkindofshopping.Forexample,youmighthavearegularshoppinglistforthenormalgroceriesshoppingday.Youmighthaveadifferentshoppinglistfortheholidays.Youmightalsowanttohaveadifferentshoppinglistwhenyoubuyanewhouse.Let'susethepowerofthereusabilityoftheVuecomponentsandtransformourshoppinglistapplicationintothelistofshoppinglists!WewilldisplaythemusingBootstrap'stabpanel;formoreinformation,refertohttp://getbootstrap.com/javascript/#tabs.
OpenyourshoppinglistapplicationintheIDE(thechapter4/shopping-listfolder).
Firstofall,weshouldaddBootstrap'sJavaScriptfileandjQuery,becausebootstrapreliesonitfordoingitsamazingmagic.Goonandjustaddthemmanuallytotheindex.htmlfile:
<body>
<...>
<scriptsrc="https://code.jquery.com/jquery-3.1.0.js"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js">
</script>
<...>
</body>
Now,let'sestablishastep-by-stepoverviewofwhatweshoulddoinordertotransformourapplicationintothelistofshoppinglists:
1. Firstofall,wemustcreateanewcomponent.Let'scallitShoppingListComponentandmovethecontentofourcurrentApp.vuetothere.
2. OurnewShoppingListComponentshouldcontainthepropsattributewithtitleanditemsthatitwillreceivefromApp.vue.
3. ItemsComponentshouldreceiveitemsfromthepropsattributeratherthanhavingithardcoded.
4. InAppcomponent'sdata,let'sdeclareandhardcode(fornow)anarrayofshoppinglists,eachoftheitemsshouldhaveatitle,anarrayofitems,andanID.
5. App.vueshouldimportShoppingListComponent,andinthetemplate,iterateovertheshoppinglistsarray,andforeachofthem,buildthehtml/jadestructureofthetabspanelforeachoftheshoppinglists.
Okay,then,let'sstart!
CreatingShoppingListComponentandmodifyingItemsComponent
Insidethecomponentsfolder,createanewShoppingListComponent.vue.CopyandpastetheApp.vuefile'scontentintothisnewfile.Donotforgettodeclarepropsthatwillcontaintitleanditemsandbinditemstotheitems-componentinvocationinsidethetemplate.Yourfinalcodeforthiscomponentshouldlooksomethinglikethefollowing:
//ShoppingListComponent.vue
<template>
<div>
<h2>{{title}}</h2>
<add-item-component></add-item-component>
<items-componentv-bind:items="items"></items-component>
<divclass="footer">
<hr/>
<change-title-component></change-title-component>
</div>
</div>
</template>
<script>
importAddItemComponentfrom'./AddItemComponent'
importItemsComponentfrom'./ItemsComponent'
importChangeTitleComponentfrom'./ChangeTitleComponent'
exportdefault{
components:{
AddItemComponent,
ItemsComponent,
ChangeTitleComponent
}
props:['title','items']
}
</script>
<stylescoped>
.footer{
font-size:0.7em;
margin-top:20vh;
}
</style>
Notethatweremovedthestylingforthecontainerandthecontainer'sclassfromtheparentdiv.ThispartofthecodeshouldstayinApp.vuebecauseitdefinestheglobalapplication'scontainerstyling.Donotforgetaboutthepropsattributeandbindingpropstoitems-component!
OpenItemsComponent.vueandensurethatitcontainsthepropsattributewithitems:
<script>
<...>
exportdefault{
props:['items'],
<...>
}
</script>
ModifyingApp.vue
NowgotoApp.vue.Removeallthecodeinsidethe<script>and<template>tags.Inthescripttag,importShoppingListComponentandinvokeitinsidethecomponentsproperty:
//App.vue
<script>
importShoppingListComponentfrom'./components/ShoppingListComponent'
exportdefault{
components:{
ShoppingListComponent
}
}
</script>
Addadataattributeandcreateashoppinglistsarraythere.Addarbitrarydataforthisarray.Eachoftheobjectsofthearrayshouldhaveid,title,anditemsattributes.items,asyou
remember,mustcontainthecheckedandtextproperties.Forexample,yourdatapropertymightlooklikethefollowing:
//App.vue
<script>
importShoppingListComponentfrom'./components/ShoppingListComponent'
exportdefault{
components:{
ShoppingListComponent
},
data(){
return{
shoppinglists:[
{
id:'groceries',
title:'Groceries',
items:[{text:'Bananas',checked:true},
{text:'Apples',checked:false}]
},
{
id:'clothes',
title:'Clothes',
items:[{text:'blackdress',checked:false},
{text:'allstars',checked:false}]
}
]
}
}
}
</script>
Bemorecreativethanme:addmorelists,moreitems,somethingniceandinteresting!
Let'snowcreateastructureforcomposingthebootstraptabpanelbasedoniterationovertheshoppinglist!Let'sstartbydefiningabasicstructureneededfortabstowork.Let'saddallthenecessaryclassesandjadestructurepretendingthatwehaveonlyoneelement.Let'salsowriteinCapsLockalltheunknownsthatwillbereusedfromourshoppinglistarray:
//App.vue
<template>
<divid="app"class="container">
<ulclass="navnav-tabs"role="tablist">
<lirole="presentation">
<ahref="ID"aria-controls="ID"role="tab"data-toggle="tab">TITLE</a>
</li>
</ul>
<divclass="tab-content">
<divclass="tab-pane"role="tabpanel"id="ID">
SHOPPINGLISTCOMPONENT
</div>
</div>
</div>
</template>
Therearetwoelementswhereweneedtoiterateovertheshoppinglistsarray—the<li>tagthatcontainsan<a>attributeandthetab-panediv.Inthefirstcase,wemustbindtheIDofeachshoppinglisttothehrefandaria-controlsattributeandinterpolatethetitle.Inthesecondcase,weneedtobindtheidattributetotheidpropertyandrendertheshoppinglistitemandbindtheitemsarrayandtitletoit.Easy!Let'sgo.Startbyaddingthev-fordirectivetoeachoftheelements(tothe<li>andtothetab-panedivelement):
//App.vue
<template>
<divid="app"class="container">
<ulclass="navnav-tabs"role="tablist">
<liv-for="listinshoppinglists"role="presentation">
<ahref="ID"aria-controls="ID"role="tab"data-
toggle="tab">TITLE</a>
</li>
</ul>
<divclass="tab-content">
<divv-for="listinshoppinglists"class="tab-pane"
role="tabpanel"
id="ID">
SHOPPINGLISTCOMPONENT
</div>
</div>
</div>
</template>
NowreplacethepartsinCapsLockwiththeproperbindings.Rememberthattothebindattribute,weusethev-bind:<corresponding_attribute>="expression"syntax.
Forthehrefattributeoftheanchorelement,wehavetodefineanexpressionthatappendstheIDselector#toid:v-bind:href="'#'+list.id".Thearia-controlsattributeshouldbeboundtothevalueoftheID.titlecanbeboundusingthesimple{{}}notationinterpolation.
Forshopping-list-component,wemustbindtitleanditemstothecorrespondingvaluesofthelistitem.DoyourememberthatwedefinedthetitleanditemspropertiesinsidethepropsoftheShoppingListComponent?Thebindings,thus,shouldlooklikev-bind:title=list.titleandv-bind:items=list.items.
Soaftertheproperbindingattribution,thetemplatewilllooklikethefollowing:
//App.vue
<template>
<divid="app"class="container">
<ulclass="navnav-tabs"role="tablist">
<liv-for="listinshoppinglists"role="presentation">
<av-bind:href="'#'+list.id"v-bind:aria-controls="list.id"
role="tab"data-toggle="tab">{{list.title}}</a>
</li>
</ul>
<divclass="tab-content">
<divv-for="listinshoppinglists"class="tab-pane"role="tabpanel"
v-bind:id="list.id">
<shopping-list-componentv-bind:
v-bind:items="list.items"></shopping-list-component>
</div>
</div>
</div>
</template>
We'realmostdone!Ifyouopenthepagenow,youwillseebothofthetitlesofthetabsappearingonthepage:
Tabtitlesasseenonthescreenafterthemodification
Ifyoustartclickingonthetabstitles,thecorrespondingtabpaneswillopen.Butthisisnotwhatwewereexpectingtosee,right?Whatwewereexpectingisforthefirsttabtobevisible(active)bydefault.Forthistohappen,weshouldaddtheactiveclasstothefirstliandtothefirsttab-panediv.Buthowcanwedoitifthecodeisthesameforallthetabsaslongasweareiteratingthroughthearray?
Fortunately,forus,Vueallowsustoprovidenotonlytheiterationiteminsidethev-forloop,butalsoindex,andthenreusethisindexvariableinsidetheexpressionsusedinthetemplates.Thus,wecanuseittoconditionallyrendertheactiveclassiftheindexis"0".Usingtheindexvariableinsidethev-forloopisaseasyasthefollowing:
v-for="(list,index)inshoppinglists"
Thesyntaxforclassbindingisthesameasforeverythingelse(classisalsoanattribute):
v-bind:class="active"
DoyourememberthatwecanwriteanyJavaScriptexpressioninsidethequotes?Inthiscase,wewanttowriteaconditionthatevaluatesthevalueofindex,andincaseitis"0",thevalueofclassisactive:
v-bind:class="index===0?'active':''"
Addtheindexvariabletothev-formodifiersandtheclassbindingstotheliandtothetab-paneelement,sothatthefinaltemplatecodelookslikefollowing:
<template>
<divid="app"class="container">
<ulclass="navnav-tabs"role="tablist">
<liv-bind:class="index===0?'active':
''"v-for="(list,index)inshoppinglists"role="presentation">
<av-bind:href="'#'+list.id"v-bind:aria-controls="list.id"
role="tab"data-toggle="tab">{{list.title}}</a>
</li>
</ul>
<divclass="tab-content">
<divv-bind:class="index===0?'active':''"
v-for="(list,index)inshoppinglists"class="tab-pane"
role="tabpanel"v-bind:id="list.id">
<shopping-list-componentv-bind:
v-bind:items="list.items"></shopping-list-component>
</div>
</div>
</div>
</template>
Lookatthepage.Nowyoushouldseenicetabsthatdisplaythecontentbydefault:
Thelookandfeeloftheshoppinglistapplicationafterthecorrectclassbinding
Thefinalshoppinglistapplicationcodeafterthesemodificationscanbefoundinthechapter4/shopping-list2folder.
Eventlistenersusingthev-ondirectiveItisveryeasytolistentotheeventsandcallcallbacksusingVue.js.Eventlisteningisalsodoneusingaspecialdirectivewithspecificmodifiersforeachoftheeventtypes.Thedirectiveisv-on.Themodifiersareappliedafterthecolon:
v-on:click="myMethod"
Ok,yousay,andwheredoIdeclarethismethod?Youwillprobablynotbelieveme,butallthecomponent'smethodsaredeclaredinsidethemethodsproperty!So,todeclarethemethodcalledmyMethod,youshoulddothefollowing:
<script>
exportdefault{
methods:{
myMethod(){
//dosomethingnice
}
}
}
</script>
Allthedataandpropsattributesareaccessibleinsidethemethodsusingthethiskeyword.
Let'saddamethodtoaddanewitemtotheitemsarray.Wehaveactuallydoneitalreadyinthepreviouschapter,whenwelearnedhowtopassdatabetweenparentandchildrencomponentsusingtheeventsemittingsystem.Wewilljustrecapthisparthere.
InordertobeabletoaddnewitemswithinAddItemComponenttotheshoppinglistthatbelongstoShoppingListComponent,weshoulddothefollowing:
EnsurethatAddItemComponenthasadatapropertycallednewItem.CreateanaddItemmethodinsidetheAddItemComponentthatpushesthenewItemandemitstheeventadd.ApplyaneventlistenertotheAdd!buttonusingthev-on:clickdirective.ThiseventlistenershouldcallthedefinedaddItemmethod.CreateanaddItemmethodinsidetheShoppingListComponentthatwillreceivethetextasaparameterandpushittotheitemsarray.Bindthev-ondirectivewithacustomaddmodifiertotheadd-item-componentinvocationinsidetheShoppingListComponent.ThislistenerwillcalltheaddItemmethoddefinedinthiscomponent.
Let'sgothen!Usetheshoppinglistapplicationfromthechapter4/shopping-list2folderandplaywithit.
StartbyopeningAddItemComponentandaddthemissingv-ondirectivetotheAdd!buttonandtheaddItemmethod:
//AddItemComponent.vue
<template>
<divclass="input-group">
<inputtype="text"v-model="newItem"
placeholder="addshoppinglistitem"class="form-control">
<spanclass="input-group-btn">
<buttonv-on:click="addItem"class="btnbtn-default"
type="button">Add!</button>
</span>
</div>
</template>
<script>
exportdefault{
data(){
return{
newItem:''
}
},
methods:{
addItem(){
vartext
text=this.newItem.trim()
if(text){
this.$emit('add',this.newItem)
this.newItem=''
}
}
}
}
</script>
SwitchtoShoppingListComponentandbindthev-on:adddirectivetotheinvocationofadd-item-componentinsidethetemplatetag:
//ShoppingListComponent.vue
<template>
<div>
<h2>{{title}}</h2>
<add-item-componentv-on:add="addItem"></add-item-component>
<items-componentv-bind:items="items"></items-component>
<divclass="footer">
<hr/>
<change-title-component></change-title-component>
</div>
</div>
</template>
NowcreatetheaddItemmethodinsidetheShoppingListComponent.Itshouldreceivethetextandjustpushitintothethis.itemsarray:
//ShoppingListComponent.vue
<script>
importAddItemComponentfrom'./AddItemComponent'
importItemsComponentfrom'./ItemsComponent'
importChangeTitleComponentfrom'./ChangeTitleComponent'
exportdefault{
components:{
AddItemComponent,
ItemsComponent,
ChangeTitleComponent
},
props:['title','items'],
methods:{
addItem(text){
this.items.push({
text:text,
checked:false
})
}
}
}
</script>
Openthepageandtrytoaddtheitemstothelistbytypingintheinputboxandclickingthebuttonafterward.Itworks!
Now,Iwouldliketoaskyoutoswitchyourrolefromtheapplication'sdevelopertoitsuser.Typethenewitemintheinputbox.Whatistheobvioususeractionaftertheitemhasbeenintroduced?Aren'tyoutryingtohittheEnterbutton?Ibetyouare!Whennothingishappening,itisalittlebitfrustrating,isn'tit?Don'tworry,myfriend,wejusthavetoaddonemoreeventlistenertotheinputboxandcallthesamemethodaswedidwiththeAdd!button.
Soundseasy,right?Whateventisfiredwhenwe'rehittingtheEnterbutton?Right,itisthekeyupevent.So,wejusthavetousethev-ondirectivewiththekeyupmethodafterthedelimitercolon:v-on:keyup.Theproblemisthatthiseventisfiredwhenanykeyboardbuttonishit,whichmeansthatwhilewe'retypingthenewshoppinglistitem,eachtimethenewletterisbeingintroduced,themethodwillbecalled.Thisisnotwhatwewant.Ofcourse,wecouldaddaconditioninsideouraddItemmethodthatwouldcheckfortheevent.codeattributeand,onlyincaseit's13(whichcorrespondstotheEnterkey),wewouldcalltherestofthemethod.Fortunately,forus,Vueprovidesamechanismtoprovidekeystrokemodifierstothismethodthatallowsustoonlycallamethodifacertainkeycodewashit.Itshouldbeimplementedusingthedot(.)modifier.Inourcase,itisasfollows:
v-on:keyup.enter
Let'saddittoourinputbox.GotoAddItemComponentandaddthev-on:keyup.enterdirectivetotheinputasfollows:
<template>
<divclass="input-group">
<inputtype="text"v-on:keyup.enter="addItem"v-model="newItem"
placeholder="addshoppinglistitem"class="form-control">
<spanclass="input-group-btn">
<buttonv-on:click="addItem"class="btnbtn-default"
type="button">Add!</button>
</span>
</div>
</template>
OpenthepageandtrytoaddtheitemtotheshoppinglistusingtheEnterbutton.Itworks!
Let'sdothesamefortitlechanging.Theonlydifferenceisthattheaddingitems,weusedacustomaddeventandherewewillusethenativeinputevent.Wehavealreadydoneit.Wejusthavetoperformthefollowingsteps:
1. Bindthemodeltitleusingthev-modeldirectivetochange-title-componentinthetemplateoftheShoppingListComponent.
2. ExportvalueinthepropsattributeoftheChangeTitleComponent.3. CreateanonInputmethodinsidetheChangeTitleComponentthatwillemitthenative
inputmethodwiththevalueoftheeventtarget.4. BindvaluetoinputinsidetheChangeTitleComponentcomponent'stemplateandthev-
ondirectivewiththeonInputmodifier.
Thus,thechange-title-componentinvocationinsidetheShoppingListComponenttemplatewilllooklikethefollowing:
//ShoppingListComponent.vue
<change-title-componentv-model="title"></change-title-component>
ChangeTitleComponentwilllooklikethefollowing:
//ChangeTitleComponent.vue
<template>
<div>
<em>Changethetitleofyourshoppinglisthere</em>
<inputv-bind:value="value"v-on:input="onInput"/>
</div>
</template>
<script>
exportdefault{
props:['value'],
methods:{
onInput(event){
this.$emit('input',event.target.value)
}
}
}
</script>
Thefinalcodeforthispartcanbefoundinthechapter4/shopping-list3folder.
ShorthandsOfcourse,itisnottimeconsumingtowritethev-bindorv-ondirectiveinthecodeeachtime.Developerstendtothinkthateachtimewedecreasetheamountofcode,wewin.Vue.jsallowsustowin!Justrememberthattheshorthandforthev-binddirectiveisacolon(:)andtheshorthandforthev-ondirectiveisthe@symbol.Thismeansthatthefollowingcodedoesthesamething:
v-bind:items="items":items="items"
v-bind:class='$index===0?"active":""'
:class='$index===0?"active":""'
v-on:keyup.enter="addItem"@keyup.enter="addItem"
ExerciseRewriteallthev-bindandv-ondirectivesintheshoppinglistapplicationusingtheshortcutswejustlearned.
Checkyourselfbylookingatthechapter4/shopping-list4folder.
KittensInthischapter,wehaven'ttouchedalotonourPomodoroapplicationwithitsnicekittens.Ipromiseyouthatwe'lldoalotofitinthenextchapter.Inthemeantime,Ihopethatthiskittenwillmakeyouhappy:
Kittenasking"What'snext?"
SummaryInthischapter,wehadanextensiveoverviewofallthepossiblewaysofbindingdatatoourpresentationlayer.Youlearnedhowtosimplyinterpolatedatausinghandlebarsbrackets({{}}).YoualsolearnedhowtouseJavaScriptexpressionsandfiltersinsuchaninterpolation.Youlearnedandapplieddirectivessuchasv-bind,v-model,v-for,v-if,andv-show.
Wemodifiedourapplicationssothattheyusericherandmoreefficientdata-bindingsyntax.
Inthenextchapter,wewilltalkaboutVuex,thestatemanagementarchitectureinspiredbyFluxandReduxbutwithsimplifiedconcepts.
Wewillcreateglobalapplicationstatemanagementstoresforbothofourapplicationsandexploretheirpotentialbyworkingwithit.
Chapter5.Vuex–ManagingStateinYourApplicationInthepreviouschapter,youlearnedoneofthemostimportantconceptsofVue.js:databinding.Youlearnedandappliedalotofwaysofbindingdatatoourapplication.Youalsolearnedhowtousedirectives,howtolistentoevents,andhowtocreateandinvokemethods.Inthischapter,youwillseehowtomanagethedatathatrepresentsaglobalapplicationstate.WewilltalkaboutVuex,aspecialarchitectureforcentralizedstatesinVueapplications.Youwilllearnhowtocreateaglobaldatastoreandhowtoretrieveandchangeitinsidethecomponents.Wewilldefinewhatdataislocalandwhatshouldbeglobalinourapplications,andwewillusetheVuexstoretoworkwithaglobalstateinthem.
Summingitup,inthischapter,wearegoingto:
UnderstandthedifferencebetweenlocalandglobalapplicationstatesUnderstandwhatVuexisandhowitworksLearnhowtousedatafromtheglobalstoreLearnaboutstoregetters,mutations,andactionsInstallandusetheVuexstoreintheshoppinglistandPomodoroapplications
Parent-childcomponents'communication,events,andbrainteaserRememberourshoppinglistapplication?DoyourememberourChangeTitleComponentandhowweensuredthattypinginitsinputboxwouldaffectthetitleoftheshoppinglistthatbelongstotheparentcomponent?Yourememberthateachcomponenthasitsownscope,andthescopeoftheparentcomponentcannotbeaffectedbychildrencomponents.Thus,inordertobeabletopropagatethechangesfrominsidethechildrencomponentstotheparentcomponents,weusedevents.Puttingitverysimply,youcancallthe$emitmethodfromthechildcomponentwiththenameoftheeventbeingdispatchedandlistentothiseventwithinthev-ondirectiveontheparentcomponent.
Ifitisanativeevent,suchasinput,it'sevenmoresimple.Justbindtheneededattributetothechildcomponentasav-modelandthencallthe$emitmethodwiththenameoftheevent(forexample,input)fromthechildcomponent.
Actually,thisisexactlywhatwehavedonewithChangeTitleComponent.
Openthecodeinsidethechapter5/shopping-listfolderandcheckifI'mright.(Youmightalsowanttorunnpminstallandnpmrundevifyouwanttochecktheapplication'sbehaviorinyourbrowser.)
Weboundthetitleusingthev-modeldirectivetoChangeTitleComponentinsidetheShoppingListComponenttemplate:
//ShoppingListComponent.vue
<template>
<div>
<...>
<divclass="footer">
<hr/>
<change-title-componentv-model="title"></change-title-component>
</div>
</div>
</template>
Afterthat,wedeclarethevalueofthetitlemodelinsidethepropsattributeoftheChangeTitleComponentandemittheinputeventontheinputaction:
<template>
<div>
<em>Changethetitleofyourshoppinglisthere</em>
<input:value="value"@input="onInput"/>
</div>
</template>
<script>
exportdefault{
props:['value'],
methods:{
onInput(event){
this.$emit('input',event.target.value)
}
}
}
</script>
Seemsprettystraightforward,right?
Ifwetrytochangethetitleintheinputbox,thetitleofourshoppinglistchangesaccordingly:
Afterestablishingevents-basedcommunicationbetweentheparentandchildcomponents,weareabletochangethetitle
Lookslikewewereactuallyabletoachieveourpurpose.However,ifyouopenyourdevtools,youwillseeanuglyerror:
[Vuewarn]:Avoidmutatingapropdirectlysincethevaluewillbeoverwritten
whenevertheparentcomponentrerenders.Instead,useadataorcomputed
propertybasedontheprop'svalue.Propbeingmutated:"title"
Ouch!Vueisactuallyright,wearemutatingthedatathatiscontainedinsidetheShoppingListcomponent'spropsattribute.Thisattributecomesfromthemainparentcomponent,App.vue,whichis,inturn,theparentofourShoppingListComponent.Andwealreadyknowthatwecannotmutatetheparent'sdatafromthechildcomponent.IfthetitlebelongeddirectlytotheShoppingListComponent,wewereallgood,butinthiscase,wearedefinitelydoingsomethingwrong.
Also,ifyouarepayingenoughattention,youprobablynoticedthatthere'sonemoreplacethatcontainsthesamepieceofdatathatdoesn'tchangedespiteoureffort.Lookatthetab'stitle.ItcontinuestodisplaythewordGroceries.Butwewantittochangeaswell!
Smallsidenote:I'veaddedanewcomponent,ShoppingListTitleComponent.Itrepresentsthetab'stitle.Doyouremembercomputedproperties?Notethatthiscomponentcontainsonethatjustprepends#totheIDimportedthroughthepropsattributetogenerateananchor:
<template>
<a:href="href":aria-controls="id"role="tab"data-toggle="tab">
{{title}}</a>
</template>
<script>
exportdefault{
props:['id','title'],
computed:{
href(){
return'#'+this.id
}
}
}
</script>
Theanchorthatdisplaysthetab'stitlecontainsanhrefbindingdirectivethatreliesonthiscomputedproperty.
So,backtothetitlechanging.WhatcanwedotochangethetitleofthiscomponentwhenthetitleinsidetheChangeTitleComponentchanges?IfwecouldpropagatetheeventtothemainApp.vuecomponent,wecouldactuallysolvebothproblems.Wheneverthedataintheparentcomponentchanges,itaffectsallthechildrencomponents.
So,weneedtosomehowmaketheeventflowfromChangeTitleComponentuntilthemainAppcomponent.Soundsdifficult,butactually,wejustneedtoregisterourcustomeventinbothChangeTitleComponentanditsparentandemitituntilitreachestheAppcomponent.TheAppcomponentshouldhandlethiseventbyapplyingthechangetothecorrespondingtitle.InorderforApp.vuetoknowexactlywhichshoppinglistisbeingchanged,itschildShoppingListComponentshouldalsopasstheIDoftheshoppinglistthatitrepresents.Forthistohappen,App.vueshouldpasstheidpropertytothecomponent,andtheshoppinglistcomponentshouldregisteritinitspropsattribute.
So,wewilldothefollowing:
1. BindtheidpropertytoShoppingListComponentonitscreationinsidetheAppcomponent'stemplate.
2. Bindpropertytitleinsteadofv-modeltothechange-title-componentfromwithintheShoppingListcomponent.
3. Attachthecustomevent(let'scallitchangeTitle)toinputinsidetheChangeTitleComponent.
4. TellShoppingListComponenttolistentothecustomchangeTitleeventcomingfromthe
change-title-componentusingthev-ondirectiveandhandleitbyemittinganotherevent(itcanalsobecalledchangeTitle)thatshouldbecaughtbytheAppcomponent.
5. AttachlistenertothechangeTitleeventtotheshopping-list-componentinsideApp.vueandhandleitbyactuallychangingthetitleofthecorrespondingshoppinglist.
Let'sstartbymodifyingtheApp.vuefile'stemplateandbindingtheshoppinglist'sIDtoshopping-list-component:
//App.vue
<template>
<divid="app"class="container">
<...>
<shopping-list-component:id="list.id":
:items="list.items"></shopping-list-component>
<...>
</div>
</template>
NowregistertheidattributeinsidetheShoppingListComponentcomponent'sprops:
//ShoppingListComponent.vue
<script>
<...>
exportdefault{
<...>
props:['id','title','items'],
<...>
}
</script>
Bindthetitledatapropertyinsteadofthev-modeldirectivetothechange-title-component:
//ShoppingListComponent.vue
<template>
<...>
<change-title-component:></change-title-component>
<...>
</template>
//ChangeTitleComponent.vue
<template>
<div>
<em>Changethetitleofyourshoppinglisthere</em>
<input:value="title"@input="onInput"/>
</div>
</template>
<script>
exportdefault{
props:['value','title'],
<...>
}
</script>
EmitacustomeventinsteadofinputfromtheChangeTitleComponentandlistentothiseventinitsparentcomponent:
//ChangeTitleComponent.vue
<script>
exportdefault{
<...>
methods:{
onInput(event){
this.$emit('changeTitle',event.target.value)
}
}
}
</script>
//ShoppingListComponent.vue
<template>
<...>
<change-title-component:
v-on:changeTitle="onChangeTitle"></change-title-component>
<...>
</template>
CreatetheonChangeTitlemethodinShoppingListComponentthatwillemititsownchangeTitleevent.ListentothiseventintheApp.vuecomponentusingthev-ondirective.NotethattheonChangeTitlemethodoftheshoppinglistcomponentshouldsenditsIDinorderforApp.vuetoknowwhichshoppinglist'stitleisbeingchanged.So,theonChangeTitlemethodanditshandlingwilllookasfollows:
//ShoppingListComponent.vue
<script>
<...>
exportdefault{
<...>
methods:{
<...>
onChangeTitle(text){
this.$emit('changeTitle',this.id,text)
}
}
}
</script>
//App.vue
<template>
<...>
<shopping-list-component:id="list.id":
:items="list.items"v-on:changeTitle="onChangeTitle">
</shopping-list-component>
<...>
</template>
Finally,let'screateachangeTitlemethodinsideApp.vuethatwillfindashoppinglistinthe
shoppinglistsarraybyitsIDandchangeitstitle:
<script>
<...>
import_from'underscore'
exportdefault{
<...>
methods:{
onChangeTitle(id,text){
_.findWhere(this.shoppinglists,{id:id}).title=text
}
}
}
</script>
Notethatwehaveusedtheunderscoreclass'sfindWheremethod(http://underscorejs.org/#findWhere)tomakeourtaskoffindingtheshoppinglistbyitsIDeasier.
And...wearedone!Checkthefinalcodeforthisteaserinthechapter5/shopping-list2folder.Checkthepageinthebrowser.Trytochangethetitleintheinputbox.Youwillseethatitchangeseverywhere!
Admitthatthiswasquitechallenging.Trytorepeatallthestepsbyyourself.Inthemeantime,letmeberandomandtellyoutwowords:globalandlocal.Thinkaboutit.
Whydoweneedaglobalstatestore?Asadeveloper,youarealreadyfamiliarwithglobalandlocalconcepts.Thereareglobalvariablesthatareaccessiblebyeachsectionoftheapplication,butmethodsalsohavetheirown(local)scopeandtheirscopeisnotaccessiblebyothermethods.
Acomponent-basedsystemalsohasitslocalandglobalstates.Eachcomponenthasitslocaldata,buttheapplicationhasaglobalapplicationstatethatcanbeaccessedbyanycomponentoftheapplication.Thechallengethatwehavemetinthepreviousparagraphwouldbeeasilysolvedifwehadsomekindofaglobalvariablesstorecontainingthetitlesoftheshoppinglistsandeachcomponentcouldaccessandmodifythem.Fortunatelyforus,Vue'screatorthoughtaboutusandcreatedVuexarchitecture.Thisarchitectureallowsustocreateaglobalapplicationstore—theplacewheretheglobalapplicationstatecanbestoredandmanaged!
WhatisVuex?Aspreviouslymentioned,Vuexisanapplicationarchitectureforcentralizedstatemanagement.ItwasinspiredbyFluxandRedux,butitisalittlebiteasiertounderstandandtouse:
Vuexarchitecture;theimageistakenfromtheVuexGitHubpageathttps://github.com/vuejs/vuex
Lookinthemirror(donotforgettosmiletoyourself).Youseeaniceprettyhuman.However,there'sawholecomplexsysteminsideit.Whatdoyoudowhenyoufeelcold?Andhowdoyoufeelwhenit'shot?Howdoesitfeeltobehungry?Andveryhungry?Andhowdoesitfeeltotouchafluffycat?Thehumancanbeinvarioustypesofstates(happy,hungry,smiley,angry,andsoon).Thehumanalsohasalotofcomponents,suchashands,arms,legs,
stomach,face,andsoon.Canyouimaginehowwoulditbeif,let'ssay,ahandwereabletodirectlyinfluenceyourstomach,makingyoufeelhungry,withoutyourawareness?
Thewayweworkisverysimilartothecentralizedstatemanagementsystem.Ourbraincontainsaninitialstateofthings(happy,nothungry,satisfied,andsoon).Italsoprovidesthemechanismthatallowspullingthestringsinitthatcanaffectthestate.Forexample,makeasmile,feelsatisfied,clapyourhands,andsoon.Ourhands,stomach,mouth,andothercomponentscannotdirectlyaffectthestate.Buttheycantellourbraintodispatchcertainchanges,andthesechanges,inturn,willaffectthestate.
Forexample,whenyouarehungry,youeat.Yourstomachatsomecertainpointtellsthebrainthatitisfull.Theactiondispatchesamutationofthestateofbeinghungrytobesatisfied.Yourcomponentmouthisboundtothisstateanditmakesitexpressthesmile.Thus,thecomponentsareboundtotheread-onlybrainstateandcandispatchbrainactionsthatwillalterthestate.Thecomponentsarenotawareofeachotherandcannotmodifyeachother'sstatedirectlyinanyway.Theyalsocanalsonotaffectdirectlythebrain'sinitialstate.Theycanonlycalltheactions.Actionsbelongtothebrain,andintheircallbacks,thestatecanbemodified.Thus,ourbrainisasinglesourceoftruth.
Tip
Singlesourceoftruthininformationsystemsisawayofdesigningthearchitectureoftheapplicationinsuchawaythateverydataelementisonlystoredonce.Thisdataisread-onlytopreventtheapplication'scomponentsfromcorruptingthestatethatisaccessedbyothercomponents.TheVuexstoreisdesignedinsuchawaythatitisnotpossibletochangeitsstatefromanycomponent.
Howdoesthestoreworkandwhatissospecialaboutit?TheVuexstorecontainsessentiallytwothings:stateandmutations.Stateisanobjectthatrepresentstheinitialstateoftheapplicationdata.Mutationsisalsoanobjectcontainingactionfunctionsthataffectthestate.VuexstoreisjustaplainJavaScriptfilethatexportsthesetwoobjectsandtellsVuetouseVuex(Vue.use(Vuex)).Thenitcanbeimportedintoanyothercomponent.IfyouimportitinthemainApp.vuefileandregisterthestoreontheVueapplicationinitialization,itispassedtothewholechildrenchainandcanbeaccessedthroughthethis.$storevariable.So,veryroughly,inaverysimplifiedway,wewouldcreateastore,importitinthemainapp,anduseitinacomponentinthefollowingway:
//CREATESTORE
//initializestate
conststate={
msg:'Hello!'
}
//initializemutations
constmutations={
changeMessage(state,msg){
state.msg=msg
}
}
//createstorewithdefinedstateandmutations
exportdefaultnewVuex.Store({
state:state
mutations:mutations
})
//CREATEVUEAPP
<script>
importstorefrom'./vuex/store'
exportdefault{
components:{
SomeComponent
},
store:store
}
</script>
//INSIDESomeComponent
<script>
exportdefault{
computed:{
msg(){
returnthis.$store.state.msg;
}
},
methods:{
changeMessage(){
this.$store.commit('changeMessage',newMsg);
}
}
}
</script>
Theverylogicalquestionmightarise:whycreateaVuexstoreinsteadofjusthavingasharedJavaScriptfilethatimportssomestate?Youcan,ofcourse,dothat,butthenyoumustmakesurethatnoneofthecomponentscanmutatethestatedirectly.Beingabletochangethestoreattributesdirectlywould,ofcourse,bealoteasier,butthenitmightleadtoerrorsandinconsistencies.Vuexprovideacleanwayofimplicitlyprotectingthestore'sstateofdirectaccess.And,it'sreactive.Puttingallthisinstatements:
TheVuexstoreisreactive.Oncecomponentsretrieveastatefromit,theywillreactivelyupdatetheirviewseverytimethestatechanges.Componentsarenotabletodirectlymutatethestore'sstate.Instead,theyhavetodispatchmutationsdeclaredbythestore,whichallowseasytrackingofchanges.OurVuexstorethusbecomesasinglesourceoftruth.
Let'screateasimplegreetingsexampletoseeVuexinaction.
GreetingswithstoreWewillcreateaverysimpleVueapplicationwithtwocomponents:oneofthemwillcontainthegreetingsmessageandtheotheronewillcontaininputthatwillallowustochangethismessage.Ourstorewillcontaintheinitialstatethatwillrepresenttheinitialgreetingandthemutationthatwillbeabletochangethemessage.Let'sstartbycreatingaVueapplication.Wewillusevue-cliandthewebpack-simpletemplate:
vueinitwebpack-simplesimple-store
Installthedependenciesandruntheapplicationasfollows:
cdsimple-storenpminstallnpmrundev
Theapplicationisstarted!Openthebrowserinlocalhost:8080.Actually,thegreetingisalreadythere.Let'snowaddthenecessarycomponents:
ShowGreetingsComponentwilljustdisplaythegreetingsmessageChangeGreetingsComponentwilldisplaytheinputfieldthatwillallowtochangethemessage
Inthesrcfolder,createacomponentssubfolder.StartbyaddingShowGreetingsComponent.vuetothisfolder.
Itwilllookassimpleasthefollowing:
<template>
<h1>{{msg}}</h1>
</template>
<script>
exportdefault{
props:['msg']
}
</script>
Afterthat,addChangeGreetingsComponent.vuetothisfolder.Ithastocontaintheinputwiththev-model='msg'directive:
<template>
<inputv-model='msg'>
</template>
<script>
exportdefault{
props:['msg']
}
</script>
NowopentheApp.vuefile,importthecomponents,andreplacethemarkupwiththesetwocomponents.Donotforgettobindmsgtobothofthem.So,yourApp.vueafterthemodificationswilllooklikethefollowing:
<template>
<div>
<show-greetings-component:msg='msg'></show-greetings-component>
<change-greetings-component:msg='msg'></change-greetings-component>
<div>
</template>
<script>
importShowGreetingsComponentfrom'./components/ShowGreetingsComponent.vue'
importChangeGreetingsComponentfrom'./components/ChangeGreetingsComponent.vue'
exportdefault{
components:{ShowGreetingsComponent,ChangeGreetingsComponent},
data(){
return{
msg:'HelloVue!'
}
}
}
</script>
Openthebrowser.Youwillseetheinputboxwithourgreeting;however,typinginitwillnotchangethemessageinthetitle.Wewerealreadyexpectingthatbecauseweknowthatcomponentscannotdirectlyaffecteachother'sstate.Let'snowintroducethestore!Firstofall,wemustinstallvuex:
npminstallvuex--save
Createafoldernamedvuexinthesrcfolder.CreateaJavaScriptfilenamedstore.js.Thiswillbeourstatemanagemententry.Firstofall,importbothVueandVuexandtellVuethatwewanttouseVuexinthisapplication:
//store.js
importVuefrom'vue'
importVuexfrom'vuex'
Vue.use(Vuex)
Nowcreatetwoconstants,stateandmutations.Statewillcontainthemessagemsgwhilemutationswillexportthemethodthatwillallowustomodifymsg:
conststate={
msg:'HelloVue!'
}
constmutations={
changeMessage(state,msg){
state.msg=msg
}
}
NowinitializetheVuexstorewiththealreadycreatedstateandmutations:
exportdefaultnewVuex.Store({
state:state,
mutations:mutations
})
Tip
AsweareusingES6,thenotation{state:state,mutations:mutations}canbereplacedwith,simply,{state,mutations}
Ourwholestore'scodewillthuslooklikethefollowing:
//store.js
importVuefrom'vue'
importVuexfrom'vuex'
Vue.use(Vuex)
conststate={
msg:'HelloVue!'
}
constmutations={
changeMessage(state,msg){
state.msg=msg
}
}
exportdefaultnewVuex.Store({
state,
mutations
})
WecannowimportthestoreinourApp.vue.Bydoingthis,wetellallthecomponentsthattheycanusetheglobalstore,andasaresult,wecanremovedatafromApp.vue.Also,wedonotneedtobinddatatothecomponentsanymore:
//App.vue
<template>
<div>
<show-greetings-component></show-greetings-component>
<change-greetings-component></change-greetings-component>
</div>
</template>
<script>
importShowGreetingsComponentfrom'./components/ShowGreetingsComponent.vue'
importChangeGreetingsComponentfrom'./components/ChangeGreetingsComponent.vue'
importstorefrom'./vuex/store'
exportdefault{
components:{ShowGreetingsComponent,ChangeGreetingsComponent},
store
}
</script>
Nowlet'sgobacktoourcomponentsandreusethedatafromthestore.Inordertobeabletoreusereactivedatafromthestore'sstate,weshouldusecomputedproperties.Vueissosmartthatitwilldoalltheworkforustoreactivelyupdatethesepropertieswheneverthestate
changes.Andno,wedonotneedtoimportthestoreinsidethecomponents.Wehaveaccesstoitjustbyusingthethis.$storevariable.So,ourShowGreetingsComponentwilllooklikethefollowing:
//ShowGreetingsComponent.vue
<template>
<h1>{{msg}}</h1>
</template>
<style>
</style>
<script>
exportdefault{
computed:{
msg(){
returnthis.$store.state.msg
}
}
}
</script>
Followthesamelogictoreusethestore'smsgintheChangeGreetingsComponent.Nowwejusthavetodispatchthemutationoneachkeyupevent.Forthistohappen,wejustneedtocreateamethodthatwillcommitthecorrespondingstore'smutationandthatwewillcallfromtheinput'skeyuplistener:
//ChangeGreetingsComponent.vue
<template>
<inputv-model='msg'@keyup='changeMsg'>
</template>
<script>
exportdefault{
computed:{
msg(){
returnthis.$store.state.msg
}
},
methods:{
changeMsg(ev){
this.$store.commit('changeMessage',ev.target.value)
}
}
}
</script>
Openthepage.Trytochangethetitle.Etvoilà!Itworks!
UsingtheVuexstoretocallthemutationsandchangethestore'sstatepropagatingitthroughthecomponents
Wedon'tneedtobindthev-modeldirectiveanymorebecauseallthechangeshappenduetothecallingstore'smutationmethod.Thus,themsgpropertycanbeboundasthevalue'sattributetotheinputbox:
<template>
<input:value='msg'@keyup='changeMsg'>
</template>
Checkthecodeforthissectioninthechapter5/simple-storefolder.Inthisexample,wehaveusedaverysimplifiedversionofthestore.However,complexSingle-PageApplications(SPAs)requireamorecomplexandmodularstructure.Wecanandshouldextractthestore'sgettersandactionsthatdispatchmutationstoseparatedfiles.Wecanalsogroupthesefilesaccordingtothecorrespondingdata'sresponsibilities.Inthenextsections,wewillseehowwecanachievesuchamodularstructurebyusinggettersandactions.
StorestateandgettersItis,ofcourse,goodthatwecanreusethethis.$store.statekeywordinsidethecomponents.Butimaginethefollowingscenarios:
Inalarge-scaleappwheredifferentcomponentsaccessthestateofthestoreusing$this.store.state.somevalue,wedecidetochangethenameofsomevalue.Thismeansthatwehavetochangethenameofthevariableinsideeachandeverycomponentthatusesit!Wewanttouseacomputedvalueofstate.Forexample,let'ssaywewanttohaveacounter.Itsinitialstateis"0".Eachtimeweuseit,wewanttoincrementit.Thismeansthateachcomponenthastocontainafunctionthatreusesthestore'svalueandincrementsit,whichmeanshavingrepeatedcodeineachcomponent,whichisnotgoodatall!
Sorryforthenot-so-goodscenarios,guys!Fortunately,thereisanicewaynottofallintoanyofthem.Imaginethecentralizedgetterthataccessesthestore'sstateandprovidesagetterfunctiontoeachofthestate'sitems.Ifneeded,thisgettercanapplysomecomputationtothestate'sitem.Andifweneedtochangethenameofsomeoftheattributes,weonlychangeitinoneplace,inthisgetter.It'sratheragoodpracticeoraconventionthananarchitecturalmandatorysystem,butIstronglyrecommendtouseitevenifyouhaveonlyacoupleofstateitems.
Let'screatesuchagetterforoursimplegreetingsapplication.Justcreateagetters.jsfileinsidethevuexfolderandexportagetMessagefunctionthatwillreturnstate.msg:
//getters.js
exportdefault{
getMessage(state){
returnstate.msg
}
}
ThenitshouldbeimportedbythestoreandexportedinthenewVuexobject,sothestoreknowswhatitsgettersare:
//store.js
importVuefrom'vue'
importVuexfrom'vuex'
importgettersfrom'./getters'
Vue.use(Vuex)
conststate={
msg:'HelloVue!'
}
constmutations={
changeMessage(state,msg){
state.msg=msg
}
}
exportdefaultnewVuex.Store({
state,mutations,getters
})
Andthen,inourcomponents,weusegettersinsteadofdirectlyaccessingthestore'sstate.Justreplaceyourcomputedpropertyinboththecomponentswiththefollowing:
computed:{
msg(){
returnthis.$store.getters.getMessage
}
},
Openthepage;everythingworkslikeacharm!
Stillthethis.$store.gettersnotationcontainssomanyletterstowrite.We,programmersarelazy,right?Vueisniceenoughtoprovideuswithaneasywaytosupportourlaziness.ItprovidesamapGettershelperthatdoesexactlyasitsnamesuggests—providesallthestore'sgetterstoourcomponents.Justimportitanduseitinyourcomputedpropertiesasfollows:
//ShowGreetingsComponent.vue
<template>
<h1>{{getMessage}}</h1>
</template>
<script>
import{mapGetters}from'vuex'
exportdefault{
computed:mapGetters(['getMessage'])
}
</script>
//ChangeGreetingsComponent.vue
<template>
<input:value='getMessage'@keyup='changeMsg'>
</template>
<script>
import{mapGetters}from'vuex'
exportdefault{
computed:mapGetters(['getMessage']),
methods:{
changeMsg(ev){
this.$store.commit('changeMessage',ev.target.value)
}
}
}
</script>
Notethatwe'vechangedthepropertyusedinsidethetemplatetohavethesamenameasthegetter'smethodname.However,itisalsopossibletomapthecorrespondinggettermethod's
nametothepropertynamethatwewanttouseinourcomponent:
//ShowGreetingsComponent.vue
<template>
<h1>{{msg}}</h1>
</template>
<style>
</style>
<script>
import{mapGetters}from'vuex'
exportdefault{
computed:mapGetters({
msg:'getMessage'
})
}
</script>
//ChangeGreetingsComponent.vue
<template>
<input:value='msg'@keyup='changeMsg'>
</template>
<script>
import{mapGetters}from'vuex'
exportdefault{
computed:mapGetters({
msg:'getMessage'
}),
methods:{
changeMsg(ev){
this.$store.commit('changeMessage',ev.target.value)
}
}
}
</script>
So,wewereabletoextractthegetterforthemsgpropertytothecentralizedstore'sgettersfile.
Now,ifyoudecidetoaddsomecomputationtothemsgproperty,youonlyneedtodoitinoneplace.Justinoneplace!
Rickwasalwayschangingthecodeinallthecomponentsandjustdiscoveredthatitispossibletoonlychangeitinoneplace
Forexample,ifwewanttoreusetheuppercasedmessageinallthecomponents,wecanapplytheuppercasefunctioninsidethegetterasfollows:
//getters.js
exportdefault{
getMessage(state){
return(state.msg).toUpperCase()
}
}
Fromnowon,eachcomponentthatusesthegettertoretrievethestatewillhaveanuppercasedmessage:
TheShowTitleComponentuppercasedmessage.ThetoUpperCasefunctionisappliedinsidethegetters
Notealsohowsmoothlythemessageisbeingchangedtouppercaseinsidetheinputboxwhileyou'retypinginit!Checkthefinalcodeforthissectioninsidethechapter5/simple-store2folder.
Ifwedecidetochangethenameofthestate'sattribute,wewillonlychangeitinsidethegettersfunction.Forexample,ifwewanttochangethenameofmsgtomessage,wewilldoitinsideourstore:
conststate={
message:'HelloVue!'
}
constmutations={
changeMessage(state,msg){
state.message=msg
}
}
Andthen,wewillalsochangeitinsidethecorrespondinggetterfunction:
exportdefault{
getMessage(state){
return(state.message).toUpperCase()
}
}
That'sit!Therestoftheapplicationisleftcompletelyuntouched.Thisisthepowerofsucharchitecture.Insomeverycomplexapplications,wecanhavemorethanonegettersfilesthatexportstatefordifferentkindoftheapplication'sproperties.Modularityisapowerthatdrivesthemaintainability;useit!
MutationsFromthepreviousexample,itshouldbeclearthatmutationsarenomorethansimpleeventhandlerfunctionsthataredefinedbyname.Mutationhandlerfunctionsreceiveastateasafirstargument.Otherargumentscanbeusedtopassdifferentparameterstothehandlerfunction:
constmutations={
changeMessage(state,msg){
state.message=msg
},
incrementCounter(state){
state.counter++;
}
}
Aparticularityofmutationsisthattheycannotbecalleddirectly.Tobeabletodispatchamutation,weshouldcallamethodcalledcommitwithanameofthecorrespondingmutationandparameters:
store.commit('changeMessage','newMessage')
store.commit('incrementCounter')
Tip
PriortoVue2.0,amethodtodispatchmutationwascalled"dispatch".Soyouwouldcallitasfollows:store.dispatch('changeMessage','newMessage')
Youcancreateasmanymutationsasyouwish.Theycanperformdifferentoperationsonsame-stateitems.Youcangoevenfurtheranddeclaremutationnamesasconstantsinaseparatedfile.Inthisway,youcaneasilyimportthemandusetheminsteadofstrings.So,forourexample,wewouldcreateafileinsidethevuexdirectoryandcallitmutation_types.js,andexportalltheconstantnamesthere:
//mutation_types.js
exportconstINCREMENT_COUNTER='INCREMENT_COUNTER'
exportconstCHANGE_MSG='CHANGE_MSG'
Then,inourstore,wewillimporttheseconstantsandreusethem:
//store.js
<...>
import{CHANGE_MSG,INCREMENT_COUNTER}from'./mutation_types'
<...>
constmutations={
[CHANGE_MSG](state,msg){
state.message=msg
},
[INCREMENT_COUNTER](state){
state.counter++
}
}
Insidethecomponentsthatdispatchmutations,wewillimportthecorrespondingmutationtypeanddispatchitusingthevariablename:
this.$store.commit(CHANGE_MSG,ev.target.value)
Thiskindofstructuremakesalotofsenseinbigapplications.Again,youcangroupyourmutationtypesaccordingtothefunctionalitytheyprovidetotheapplicationandimportonlythosemutationsinthecomponentsthatareneededforthespecificcomponent.Thisis,again,aboutbestpractices,modularity,andmaintainability.
ActionsWhenwedispatchamutation,webasicallyperformanaction.SayingthatwecommitaCHANGE_MSGmutationisthesameassayingthatweperformanactionofchangingthemessage.Forthesakeofbeautyandtotalextraction,likeweextractthestorestate'sitemsintogettersandmutationsnamesconstantstothemutation_typeswecanalsoextractthemutationstotheactions.
Note
Thus,actionisnomorethanjustafunctionthatdispatchesamutation!
functionchangeMessage(msg){store.commit(CHANGE_MSG,msg)}
Let'screateasimpleactionsfileforourchange,messageexample.Butbeforethat,let'screateonemoreitemforthestore'sinitialstate,counter,andinitializeitwitha"0"value.So,ourstorewilllooklikethefollowing:
//store.js
importVuefrom'vue'
importVuexfrom'vuex'
import{CHANGE_MSG,INCREMENT_COUNTER}from'./mutation_types'
Vue.use(Vuex)
conststate={
message:'HelloVue!',
counter:0
}
constmutations={
[CHANGE_MSG](state,msg){
state.message=msg
},
[INCREMENT_COUNTER](state){
state.counter++;
}
}
exportdefaultnewVuex.Store({
state,
mutations
})
Let'salsoaddacountergettertothegettersfile,soourgetters.jsfilelookslikethefollowing:
//getters.js
exportdefault{
getMessage(state){
return(state.message).toUpperCase()
},
getCounter(state)
{
return(state.counter)
}
}
And,finally,let'susethecounter'sgetterinsideShowGreetingsComponenttoshowtheamountoftimesthemessagemsgwaschanged:
<template>
<div>
<h1>{{msg}}</h1>
<div>themessagewaschanged{{counter}}times</div>
</div>
</template>
<script>
import{mapGetters}from'vuex'
exportdefault{
computed:mapGetters({
msg:'getMessage',
counter:'getCounter'
})
}
</script>
Let'snowcreateactionsforboththemutations,forthecounterandforthechangemessage.Insideavuexfolder,createanactions.jsfileandexporttheactionsfunctions:
//actions.js
import{CHANGE_MSG,INCREMENT_COUNTER}from'./mutation_types'
exportconstchangeMessage=(store,msg)=>{
store.commit(CHANGE_MSG,msg)
}
exportconstincrementCounter=(store)=>{
store.commit(INCREMENT_COUNTER)
}
WecanandshoulduseES2015argumentsdestructuringandmakeourcodemoreelegant.Let'salsoexportalltheactionsinasingleexportdefaultstatement:
//actions.js
import{CHANGE_MSG,INCREMENT_COUNTER}from'./mutation_types'
exportdefault{
changeMessage({commit},msg){
commit(CHANGE_MSG,msg)
},
incrementCounter({commit}){
commit(INCREMENT_COUNTER)
}
}
Okay,nowwehaveniceandbeautifulactions.Let'susetheminourChangeGreetingsComponent!Tobeabletouseactionsinsidecomponents,weshouldfirstimportthemtoourstoreandthenexportinthenewVuexobject.Thenactionscanbedispatchedusingthethis.$store.dispatchmethodinsidethecomponents:
//ChangeGreetingsComponent.vue
<template>
<input:value="msg"@keyup="changeMsg">
</template>
<script>
import{mapGetters}from'vuex'
exportdefault{
computed:mapGetters({
msg:'getMessage'
}),
methods:{
changeMsg(ev){
this.$store.dispatch('changeMessage',ev.target.value)
}
}
}
</script>
Sowhat'sactuallythedifference?Wecontinuetowritethis.$storecode,theonlydifferenceisthatinsteadofcallingthecommitmethodwecalldispatch.DoyourememberhowwediscoveredmapGettershelper?Wasn'titnice?ActuallyVuealsoprovidesamapActionshelperthatallowsustoavoidwritingtheextensivethis.$store.dispatchsomethingmethod.JustimportmapActionsinthesamewayasweimportmapGettersanduseitinsidethecomponent'smethodsproperty:
//ChangeGreetingsComponent.vue
<template>
<input:value="msg"@keyup="changeMessage">
</template>
<script>
import{mapGetters}from'vuex'
import{mapActions}from'vuex'
exportdefault{
computed:mapGetters({
msg:'getMessage'
}),
methods:mapActions(['changeMessage','incrementCounter'])
}
</script>
Notethatwechangedthehandler'sfunctionforthekeyupevent,sowedon'thavetomaptheevents'namestothecorrespondingactions.However,justlikeinthecaseofmapGetters,wecanalsomapcustomevents'namestothecorrespondingactionsnames.
WeshouldalsochangethechangeMessageinvocationbecausewedon'textractanyevent's
targetvalueinsidetheactionsnow;thus,weshoulddoitinsidetheinvocation:
//ChangeGreetingsComponent.vue
<template>
<input:value="msg"@keyup="changeMessage($event.target.value)">
</template>
Finally,let'sbindtheincrementCounteractiontotheuser'sinput.Let's,forexample,callthisactionfromourinputtemplateonthekeyup.enterevent:
<template>
<input:value="msg"@keyup="changeMessage"
@keyup.enter="incrementCounter">
</template>
Ifyouopenthepage,andtrytochangethetitleandhittheEnterbutton,youwillseethatthecounterwillbeincrementedeachtimeyouhitEnter:
Usingactionstoincrementthecounteronthepage
So,youseehoweasyitistomodularizeourapplicationbyusingactionsinsteadofdirectlyaccessingthestore.YouexportactionsinyourVuexstore,importthemapActionsinthecomponents,andcallthemfromtheeventhandlerdirectivesinthetemplates.
Doyourememberour"human"exampleinwhichwewerecomparingthepartsofthehumanbodytothecomponentsandthehumanbraintothestoreoftheapplicationstate?Imaginethatyouarerunning.Itisonlyoneactionbuthowmanychangesarebeingdispatchedandhowmanycomponentsarebeingaffectedbythosechanges?Whenyourun,yourheartrateincreases,yousweat,yourarmsmove,andyourfacesmilesbecauseyourealizehowniceitistorun!Whenyoueat,youalsosmilebecauseitisgoodtoeat.Youalsosmilewhenyouseeakitten.So,differentactionscandispatchmorethanonechange,andthesamechangecanbedispatchedbymorethanoneaction.
ThesamehappenswithourVuexstore,anditsmutationsandactions.Withinthesameaction,morethanonemutationcanbedispatched.Forinstance,wecoulddispatchourmutationforchangingamessageandincreasingthecounterwithinthesameaction.Let'screatethisactioninsideouraction.jsfile.Let'scallithandleMessageInputChangesandmakeitreceiveone
argument:event.ItwilldispatchtheCHANGE_MSGmutationwithevent.target.value,andincaseevent.keyCodeisenter,itwilldispatchtheINCREMENT_COUNTERmutation:
//actions.js
handleMessageInputChanges({commit},event){
commit(CHANGE_MSG,event.target.value)
if(event.keyCode===13){
commit(INCREMENT_COUNTER)
}
}
Nowlet'simportthisactioninsideourChangeGreetingsComponentcomponent'smapActionsobjectandlet'suseitcallingitwiththe$eventparameter:
//ChangeGreetingsComponent.vue
<template>
<input:value="msg"@keyup="handleMessageInputChanges($event)"/>
</template>
<script>
import{mapGetters,mapActions}from'vuex'
exportdefault{
computed:mapGetters({
msg:'getMessage'
}),
methods:mapActions(['handleMessageInputChanges'])
}
</script>
Openthepage,andtrytochangethegreetingsmessageandincrementthecounterbyhittingtheEnterbutton.Itworks!
Thefinalcodeforthesimple-storeexamplecanbefoundinthechapter5/simple-store3folder.
InstallingandusingtheVuexstoreinourapplicationsNowthatweknowwhatVuexis,howtocreateastore,dispatchmutations,andhowtousegettersandactions,wecaninstallthestoreinourapplicationsanduseittofinalizetheirdataflowandcommunicationchain.
Youcanfindtheapplicationstoworkoninthefollowingfolders:
Pomodoro:chapter5/pomodoroShoppinglist:chapter5/shopping-list2
Donotforgettorunnpminstallonbothapplications.
Startbyinstallingvuexanddefinethenecessarydirectoryandfilestructureinbothapplications.
Toinstallvuex,justrunthefollowing:
npminstallvuex--save
Afterinstallingvuex,createasubfoldervuexineachoftheapplication'ssrcfolders.Inthisfolder,createfourfiles:store.js,mutation_types.js,actions.js,andgetters.js.
Preparethestore.jsstructure:
//store.js
importVuefrom'vue'
importVuexfrom'vuex'
importgettersfrom'./getters'
importactionsfrom'./actions'
importmutationsfrom'./mutations'
Vue.use(Vuex)
conststate={
}
exportdefaultnewVuex.Store({
state,
mutations,
getters,
actions
})
ImportandusethestoreinthemainApp.vue:
//App.vue
<script>
<...>
importstorefrom'./vuex/store'
exportdefault{
store,
<...>
}
</script>
Wewillnowdefinewhichistheglobalandwhichisthelocalstateineachoftheapplications,definewhatdataandbindingaremissing,dividethedata,andaddallthemissingstuffusingwhatwe'vejustlearned.
UsingtheVuexstoreintheshoppinglistapplicationIhopeyoustillrememberthechallengewewerefacingatthebeginningofthischapter.WewouldliketoestablishcommunicationbetweenthecomponentsinsuchawaythatitwouldbeeasytochangethetitleoftheshoppinglistsfromtheChangeTitleComponentandpropagateittobothShoppingListTitleandShoppingListComponent.Let'sremovethehardcodedarrayofshoppinglistsfromApp.vueandcopyittothestore'sstate:
//store.js
<...>
conststate={
shoppinglists:[
{
id:'groceries',
title:'Groceries',
items:[{text:'Bananas',checked:true},
{text:'Apples',checked:false}]
},
{
id:'clothes',
title:'Clothes',
items:[{text:'blackdress',checked:false},
{text:'all-stars',checked:false}]
}
]
}
<...>
Let'sdefinegettersfortheshoppinglists:
//getters.js
exportdefault{
getLists:state=>state.shoppinglists
}
Now,importmapGettersinApp.vueandmaptheshoppinglistsvaluetothegetListsmethodsothatthe<script>taginsidetheApp.vuecomponentwilllooklikethefollowing:
//App.vue
<script>
importShoppingListComponentfrom'./components/ShoppingListComponent'
importShoppingListTitleComponentfrom
'./components/ShoppingListTitleComponent'
import_from'underscore'
importstorefrom'./vuex/store'
import{mapGetters}from'vuex'
exportdefault{
components:{
ShoppingListComponent,
ShoppingListTitleComponent
},
computed:mapGetters({
shoppinglists:'getLists'
}),
methods:{
onChangeTitle(id,text){
_.findWhere(this.shoppinglists,{id:id}).title=text
}
},
store
}
</script>
Therestisleftuntouched!
Nowlet'sdefineamutationinsideourstorethatwillberesponsibleforchangingthetitle.Itisclearthatitshouldbeafunctionthatreceivesanewtitlestringasaparameter.However,thereissomedifficulty.Wedon'tknowwhichoftheshoppingliststitleshouldbechanged.Ifwecouldpassalist'sIDfromacomponenttothisfunction,wecouldactuallywriteapieceofcodethatwouldfindacorrectlistbyitsID.DidIjustsayifwecould?Ofcourse,wecan!Actually,ourShoppingListComponentalreadyreceivestheIDfromitsparentApp.vue.Let'sjustpassthisIDfromShoppingListComponenttoChangeTitleComponent.Inthisway,wewillbeabletoinvokethenecessaryactionfromthecomponentwherethetitleisactuallychanged,withouthavingtopropagatetheeventthroughtheparents'chain.
So,justbindtheIDtothechange-title-componentinsidetheShoppingListComponentcomponent'stemplate,asfollows:
//ShoppingListComponent.vue
<template>
<...>
<change-title-component::id="id"v-
on:changeTitle="onChangeTitle"></change-title-component>
<...>
</template>
DonotforgettoaddtheidattributetotheChangeTitleComponentcomponent'spropsattribute:
//ChangeTitleComponent.vue
<script>
exportdefault{
props:['title','id'],
<...>
}
</script>
Okay,nowourChangeTitleComponenthasaccesstobothtitleandidoftheshoppinglist.Let'saddthecorrespondingmutationtothestore.
WecanstartbywritingafunctionthatfindsashoppinglistbyitsID.Forthis,Iwillusetheunderscoreclass's_.findWheremethod,justlikewedidintheApp.vuecomponent'schangeTitlemethod.
Importunderscoreinsidemutations.jsandaddthefindByIdfunctionasfollows:
//mutations.js
<...>
functionfindById(state,id){
return_.findWhere(state.shoppinglists,{id:id})
}
<...>
Let'snowaddthemutationandlet'scallit,forexample,CHANGE_TITLE.Thismutationwillreceivethedataobjectasaparametercontainingtitleandid,andassignthevalueofthereceivedtitletothetitleofthefoundshoppinglistitem.Firstofall,let'sdeclareaconstantCHANGE_TITLEinmutation_types.jsandreuseitinsteadofwritingthemutation'snameasastring:
//mutation_types.js
exportconstCHANGE_TITLE='CHANGE_TITLE'
//mutations.js
import_from'underscore'
import*astypesfrom'./mutation_types'
functionfindById(state,id){
return_.findWhere(state.shoppinglists,{id:id})
}
exportdefault{
[types.CHANGE_TITLE](state,data){
findById(state,data.id).title=data.title
}
}
Wearealmostfinished.Let'snowdefineachangeTitleactioninsidetheactions.jsfileandreuseitinourChangeTitleComponent.Opentheactions.jsfileandaddthefollowingcode:
//actions.js
import{CHANGE_TITLE}from'./mutation_types'
exportdefault{
changeTitle:({commit},data)=>{
commit(CHANGE_TITLE,data)
}
}
And,thefinaltouch.OpenChangeTitleComponent.vue,importthemapActionshelper,maptheonInputmethodtothechangeTitleaction,andcallitinsidetemplatewiththeobjectmappingtitletoevent.target.valueandIDtotheidparameter.So,thecodeofChangeTitleComponentwilllooklikethefollowing:
//ChangeTitleComponent.vue
<template>
<div>
<em>Changethetitleofyourshoppinglisthere</em>
<input:value="title"@input="onInput({title:$event.target.value,
id:id})"/>
</div>
</template>
<script>
import{mapActions}from'vuex'
exportdefault{
props:['title','id'],
methods:mapActions({
onInput:'changeTitle'
})
}
</script>
Youcannowremovealltheevents-handlingcodefromtheShoppingListComponentandthemainAppcomponent.
Openthepageandtrytotypeintheinputbox!Thetitlewillchangeinalllocations:
Usingstore,mutations,andactions—allcomponentsupdatetheirstatewithouttheneedofeventshandlingmechanism
Thefinalcodefortheshoppinglistapplicationafterapplyingthestore'sfunctionscanbefoundinthechapter5/shopping-list3folder.
UsingVuexstoreinthePomodoroapplicationFinally,wegotbacktoourPomodoro!Whenwasthelasttimeyoutooka5-minutebreak?Let'sbuildourPomodoroapplicationwiththeVuexarchitectureandthentakearestlookatkittens.Let'sstartwiththebaseinthechapter5/pomodorofolder,whereyoualreadyincludedthebasicstructureoftheVuexstore(ifnot,gotothestartoftheInstallingandusingVuexstoreinourapplicationssection).
Bringinglifetostart,pause,andstopbuttonsLet'sstartbyanalyzingwhatcanactuallybedonewithourPomodorotimer.Lookatthepage.Wehaveonlythreebuttons:start,pause,andstop.Thismeansthatourapplicationcanbeinoneofthesethreestates.Let'sdefineandexporttheminourstore.jsfile:
//store.js
<...>
conststate={
started:false,
paused:false,
stopped:false
}
<...>
Initially,allthesestatesaresettofalse,whichmakessensebecausetheapplicationisnotstarted,it'snotpausedand,ofcourse,itisnotstopped!
Let'snowdefinegettersforthesestates.Openthegetters.jsfileandaddthegetterfunctionsforallthreestates:
//getters.js
exportdefault{
isStarted:state=>state.started,
isPaused:state=>state.paused,
isStopped:state=>state.stopped
}
Whatshouldhappentoourcontrolbuttonsforeachofthedefinedstates:
Thestartbuttonshouldbecomedisabledwhentheapplicationisstarted.However,itshouldbeenabledagainwhentheapplicationispausedsothatwecanusethisbuttontoresumetheapplication.Thepausebuttoncanonlybeenabledwhentheapplicationisstarted(becausewecannotpausesomethingthathasnotbeenstartedyet).However,itshouldbedisablediftheapplicationispaused(becausewecannotpausesomethingthatisalreadypaused).Thestopbuttoncanonlybeenabledwhentheapplicationisstarted.
Let'stranslatethisintocodebyaddingthedisabledclasstoourcontrolbuttonsconditionally,dependingontheapplicationstates.
Tip
Onceweapplythedisabledclass,Bootstrapwilltakecareofthebuttons'behaviorforusbynotonlyapplyingspecialstylingbutalsodeactivatinginteractiveelements.
Inordertobeabletousethealreadydefinedgetters,wemustimportmapGettersintothe<script>tagofthecomponent.Afterthat,wemusttellthecomponentthatwewanttousethembyexportingthemwithinthecomputedpropertyobject:
//ControlsComponent.vue
<script>
import{mapGetters}from'vuex'
exportdefault{
computed:mapGetters(['isStarted','isPaused','isStopped'])
}
</script>
Nowthesegetterscanbeusedinsidethetemplate.So,wewillapplythedisabledclasstothefollowing:
Thestartbuttonwhentheapplicationisstartedandnotpaused(isStarted&&!isPaused)Thepausebuttonwhentheapplicationisnotstartedorpaused(!isStarted||isPaused)Thestopbuttonwhentheapplicationisnotstarted(!isStarted)
Ourtemplatewillnowlooklikethefollowing:
//ControlsComponent.vue
<template>
<span>
<button:disabled='isStarted&&!isPaused'>
<iclass="glyphiconglyphicon-play"></i>
</button>
<button:disabled='!isStarted||isPaused'>
<iclass="glyphiconglyphicon-pause"></i>
</button>
<button:disabled='!isStarted'>
<iclass="glyphiconglyphicon-stop"></i>
</button>
</span>
</template>
Youseenowthatthepauseandstopbuttonslookdifferent!Ifyoumousehoveryourmouseoverthem,thecursorisnotchanged,whichmeansthattheyarereallydisabled!Let'sjustcreateastylefortheiconsthatareinsidethedisabledbuttonstohighlightthedisabledstateevenmore:
//ControlsComponent.vue
<stylescoped>
button:disabledi{
color:gray;
}
</style>
Okay,nowthatwehavebeautifuldisabledbuttons,let'sbringabitoflifeintothem!
Let'sthinkaboutwhatshouldactuallyhappentotheapplicationstateswhenwestart,pause,orstoptheapplication:
Whenwestarttheapplication,thestatestartedshouldbecometrueandbothpausedandstoppedstatesshouldforsurebecomefalse.
Whenwepausetheapplication,thestatepausedistrue,statestoppedisfalse,andstatestartedistruebecauseapausedapplicationcontinuestobestarted.Whenwestoptheapplication,thestatestoppedbecomestrueandboththepausedandstartedstatesbecomefalse.Let'stranslateallthisbehaviorintomutation_types,mutations,andactions!
Openmutation_types.jsandaddthreemutationtypesasfollows:
//mutation_types.js
exportconstSTART='START'
exportconstPAUSE='PAUSE'
exportconstSTOP='STOP'
Nowlet'sdefinemutations!Openthemutations.jsfileandaddthreemutationsforeachofthemutationtypes.So,wehavedecidedthatwhenwe:
Starttheapplication:Thestatestartedistrue,andstatespausedandstoppedarefalse.Pausetheapplication:Thestatestartedistrue,thestatepausedistrue,andstoppedarefalse.Stoptheapplication:Thestatestoppedistrue,andstatesstartedandpausedarefalse.
Nowlet'sputitintothecode.Importmutation_typestomutations.jsandwriteallthreenecessarymutationsasfollows:
//mutations.js
import*astypesfrom'./mutation_types'
exportdefault{
[types.START](state){
state.started=true
state.paused=false
state.stopped=false
},
[types.PAUSE](state){
state.paused=true
state.started=true
state.stopped=false
},
[types.STOP](state){
state.stopped=true
state.paused=false
state.started=false
}
}
Nowlet'sdefineouractions!Gototheactions.jsfile,importmutationtypes,andexportthreefunctions:
//actions.js
import*astypesfrom'./mutation_types'
exportdefault{
start:({commit})=>{
commit(types.START)
},
pause:({commit})=>{
commit(types.PAUSE)
},
stop:({commit})=>{
commit(types.STOP)
}
}
ThefinaltouchtobringourbuttonstolifeistoimporttheseactionsintoControlsComponentandcallthemontheclickeventoneachbutton.Let'sdoit.DoyoustillrememberhowtocalltheactiononsomeeventappliedtotheHTMLelement?Ifwearetalkingabouttheclickevent,itisjustthefollowing:
@click='someAction'
So,inourControlsComponent.vue,weimportthemapActionsobject,mapittothecomponent'smethodsproperty,andapplyittothecorrespondingbutton'sclicks.That'sall!The<script>tagofControlsComponentwillthuslooklikethefollowing:
//ControlsComponent.vue
<script>
import{mapGetters,mapActions}from'vuex'
exportdefault{
computed:mapGetters(['isStarted','isPaused','isStopped']),
methods:mapActions(['start','stop','pause'])
}
</script>
Nowcallthesefunctionsinsidetheeventhandlerdirectiveswithinthetemplatesothatthe<template>tagoftheControlsComponentlookslikethefollowing:
//ControlsComponent.vue
<template>
<span>
<button:disabled='isStarted&&!isPaused'
@click="start">
<iclass="glyphiconglyphicon-play"></i>
</button>
<button:disabled='!isStarted||isPaused'
@click="pause">
<iclass="glyphiconglyphicon-pause"></i>
</button>
<button:disabled='!isStarted'@click="stop">
<iclass="glyphiconglyphicon-stop"></i>
</button>
</span>
</template>
Trytoclickthebuttons.Theydoexactlywhatweneedthemtodo.Nicework!Checkitoutinthechapter5/pomodoro2folder.However,wearenotdoneyet.WestillhavetomakeourPomodorotimerintoanactualtimerandnotjustsomepagethatallowsyoutoclicksomebuttonsandwatchthemchangingtheirstatesfromdisabledtoenabled.
BindingPomodorominutesandsecondsIntheprevioussection,wewereabletodefinethreedifferentstatesofthePomodoroapplication:started,paused,andstopped.However,let'snotforgetaboutwhatthePomodoroapplicationshouldbeusedfor.Itmustcountdownsomegiventimeforworkandthenswitchtothebreakcountdowntimer,andthencomebacktowork,andsoon.
ThisleadsustorealizethatthereisonemoreveryimportantPomodoroapplication'sstate:thebinarystatethattogglesbetweenworkingandrestingperiodsoftime.Thisstatecannotbetoggledbybuttons;itshouldsomehowbemanagedbyourapplication'sinternallogic.
Let'sstartbydefiningtwostateproperties:oneforthecounterthatwillbedecreasedwiththetimeandtheotheronetodistinguishbetweentheworkingandnot-workingstates.Let'sassumethatwhenwestartourPomodoro,westartourworkingday,sotheworkingstateshouldbesettotrueandthecountdowncountershouldbesettotheamountoftimethatwedefineforourworkingPomodoroperiod.Forthesakeofmodularityandmaintainability,let'sdefinetheamountoftimeforworkandforrestinanexternalfile.Let'scallit,forexample,config.js.Createtheconfig.jsfileintheproject'srootdirectoryandaddthefollowingcontent:
//config.js
exportconstWORKING_TIME=20*60
exportconstRESTING_TIME=5*60
BytheseinitializationsImeanthatourPomodoroshouldcountdown20minutesfortheworkingPomodorointervaland5minutesforbreaks.Ofcourse,youarefreetodefineyourownvaluesthataremostsuitableforyou.Let'snowexportconfig.jsinourstoreandreusetheWORKING_TIMEvaluetoinitializeourcounter.Let'salsocreateapropertythattogglesbetweenwork/breakandcallitisWorking.Let'sinitializeittotrue.
So,ournewstatewilllooklikethefollowing:
//store.js
<...>
import{WORKING_TIME}from'../config'
conststate={
started:false,
paused:false,
stopped:false,
isWorking:true,
counter:WORKING_TIME
}
So,wehavethesetwonicenewproperties.Beforestartingtocreatemethods,actions,mutations,andotherthingsthatdecreasethecounterandtoggletheisWorkingproperty,let'sthinkofthevisualelementsthatrelyontheseproperties.
Wedon'thavesomanyelements,soit'seasytodefine.
TheisWorkingstateisaffectingthetitle:weshoulddisplayWork!whenit'stimetoworkandRest!whenit'stimetohavearest.TheisWorkingstateisalsoaffectingthekittenscomponentvisibility:itshouldbedisplayedonlywhenisWorkingisfalse.Thecounterpropertyaffectsminuteandsecond:eachtimeitdecreases,thesecondshouldalsodecreaseitsvalue,andevery60decreases,theminuteshouldalsodecreaseitsvalue.
Let'sdefinegettersfunctionsfortheisWorkingstateandfortheminuteandthesecond.Afterdefiningthesegetters,wecanreusetheminourcomponentsinsteadofusingthehardcodedvalues.Let'sstartbydefiningagetterfortheisWorkingproperty:
//getters.js
exportdefault{
isStarted:state=>state.started,
isPaused:state=>state.paused,
isStopped:state=>state.stopped,
isWorking:state=>state.isWorking
}
Let'sreusethisgetterinthecomponentsthatwereusinghardcodedisworkingdefinedintheApp.vuecomponent.OpenApp.vue,removeallthereferencestotheisworkinghardcodedvariable,importthemapGettersobject,andmaptheisworkingpropertytotheisWorkingmethodinsidethecomputedpropertyasfollows:
//App.vue
<script>
<...>
import{mapGetters}from'vuex'
exportdefault{
<...>
computed:mapGetters({
isworking:'isWorking'
}),
store
}
</script>
RepeatthesamestepsinStateTitleComponent.ImportmapGettersandreplacepropswithmappedcomputedproperty:
//StateTitleComponent.vue
<script>
import{mapGetters}from'vuex'
exportdefault{
data(){
return{
workingtitle:'Work!',
restingtitle:'Rest!'
}
},
computed:mapGetters({
isworking:'isWorking'
})
}
</script>
Therestisleftuntouchedinboththecomponents!Insidethetemplates,theisworkingpropertyisused.Thispropertycontinuestoexist;it'sjustimportedfromthereactiveVuexstoreandnotfromthehardcodeddata!
Nowwemustdefinegettersforminutesandseconds.Thispartistrickierbecauseinthesegetters,wehavetoapplysomecomputationtothecounterstate'sproperty.Thisisnotdifficultatall.Ourcounterrepresentsatotalnumberofseconds.Thismeansthatwecaneasilyextractminutesbydividingthecounterby60androundingtothelowestinteger(Math.floor).Thesecondscanbeextractedbytakingtheremainderofthedivisionby60.Thus,wecanwriteourgettersforminutesandsecondsinthefollowingway:
//getters.js
exportdefault{
<...>
getMinutes:state=>Math.floor(state.counter/60),
getSeconds:state=>state.counter%60
}
That'sit!Let'snowreusethesegettersintheCountdownComponent.ImportmapGettersandmapitscorrespondingmethodstotheminandsecpropertiesinsidethecomputedproperty.Donotforgettoremovethehardcodeddata.OurscripttagoftheCountdownComponent.vuewillthuslooklikethefollowing:
//CountdownComponent.vue
<script>
import{mapGetters}from'vuex'
exportdefault{
computed:mapGetters({
min:'getMinutes',
sec:'getSeconds'
})
}
</script>
Therestisleftcompletelyuntouched!Thetemplatewasreferencingtheminandsecproperties,andtheycontinuetoexist.Thecodeasitwasuntilnowcanbefoundinthechapter5/pomodoro3folder.Lookatthepage;nowthedisplayedminutesandsecondscorrespondtotheamountofworkingtimewe'vedefinedinourconfigurationfile!Ifyouchangeit,itwillchangeaswell:
ChangingtheconfigurationfortheamountofworkingtimewillimmediatelyaffectthePomodoroapplicationview
CreatingthePomodorotimerOkay,noweverythingisreadytoactuallystarttocountdownourworkingtimesowecanfinallyhavesomerest!Let'sdefinetwoauxiliaryfunctions,togglePomodoroandtick.
ThefirstonewilljusttoggletheisWorkingproperty.Itwillalsoredefinethestate'scounter.WhenthestateisWorking,thecountershouldcorrespondtotheworkingtime,andwhenthestateisnotworking,thecountershouldcorrespondtotherestingtime.
Thetickfunctionwilljustdecreasethecounterandcheckifithasreached"0"value,andinthiscase,willtogglethePomodorostate.Therestisalreadybeingtakencareof.So,thetogglePomodorofunctionwilllooklikethefollowing:
//mutations.js
functiontogglePomodoro(state,toggle){
if(_.isBoolean(toggle)===false){
toggle=!state.isWorking
}
state.isWorking=toggle
state.counter=state.isWorking?WORKING_TIME:RESTING_TIME
}
Ah,anddonotforgettoimportWORKING_TIMEandRESTING_TIMEfromourconfig!Also,donotforgettoimportunderscoresinceweareusingitforthe_.isBooleancheck:
//mutations.js
import_from'underscore'
import{WORKING_TIME,RESTING_TIME}from'./config'
Then,thetickfunctionwilljustdecreasethecounterandcheckifithasreachedthe"0"value:
//mutations.js
functiontick(state){
if(state.counter===0){
togglePomodoro(state)
}
state.counter--
}
Fine!It'sstillnotenough.Weneedtosettheintervalthatwouldcallthetickfunctionforeachsecond.Whereshoulditbeset?Well,itismorethanclearthatitshouldbedonewhenwestartourPomodoro,intheSTARTmutation!
ButifwesettheintervalintheSTARTmutationanditcallsthetickfunctioneachsecond,howwillitbestoppedorpausedonhittingthepauseorstopbutton?That'swhythesetIntervalandclearIntervalJavaScriptfunctionsexistandthat'swhywehaveastorewherewecansavetheinitialstatefortheintervalvalue!Let'sstartbydefiningintervalasnullinthestore'sstate:
//store.js
conststate={
<...>
interval:null
}
Now,inourSTARTmutation,let'saddthefollowingcodethatinitializestheinterval:
//mutations.js
exportdefault{
[types.START](state){
state.started=true
state.paused=false
state.stopped=false
state.interval=setInterval(()=>tick(state),1000)
},
<...>
}
Wejustsetanintervalthatwillcallthetickfunctioneachsecond.Inturn,thetickfunctionwilldecreasethecounter.Thevaluesthatrelyonthecounter'svalue—minuteandsecond—willchangeandreactivelypropagatethesechangestotheview.
Ifyouclickonthestartbuttonnow,youwillsetthecountdowninaction!Yay!It'salmostdone.WejustneedtoaddtheclearIntervalfunctiononthepauseandstopmutationmethods.Apartfromthis,onthestopmethod,let'scallthetogglePomodorofunctionwithtrue,whichwillresetthePomodorotimertotheworkingstate:
//mutations.js
exportdefault{
[types.START](state){
state.started=true
state.paused=false
state.stopped=false
state.interval=setInterval(()=>tick(state),1000)
},
[types.PAUSE](state){
state.paused=true
state.started=true
state.stopped=false
clearInterval(state.interval)
},
[types.STOP](state){
state.stopped=true
state.paused=false
state.started=false
togglePomodoro(state,true)
}
}
ChangingthekittenIhopeyouworkedalotandyourrestingtimehasfinallycome!Ifnotandifyoucan'twaitforit,justchangetheWORKING_TIMEvalueintheconfig.jsfileforsomethingconsiderablysmallandwaitforit.IthinkIfinallydeservesomerest,soI'vebeenstaringatthisniceimageforsomeminutesalready:
Iamstaringattheimage,thecatisstaringatme
Wouldn'tyoulikethedisplayedimagetochangesometimes?Ofcourse,youwould!Inordertoachievethis,wemustjustappendsomethingtotheimagesourcesothatitchangeswithtimeanddeliversanon-cachedimagetous.
Tip
Oneofthebestpracticestodelivernon-cachedthingsistoappendthetimestamptotherequestedURL.
Wecould,forexample,haveanotherpropertyinourstore,let'ssay,timestamp,whichwouldbeupdatedwitheachcounterdecreaseanditsvaluewouldbeappendedtotheimage-sourceURL.Let'sdoit!Let'sstartbydefiningatimestamppropertyinourstore'sstateasfollows:
//store.js
conststate={
<...>
timestamp:0
}
Tellthetickfunctiontoupdatethisvalueoneachtick:
//mutations.js
functiontick(state){
<...>
state.timestamp=newDate().getTime()
}
Createthegetterforthisvalueingetters.jsanduseitinsidetheKittensComponentbyaccessingthethis.$store.getters.getTimestampmethodinsidethecomputedproperty:
//getters.js
exportdefault{
<...>
getTimestamp:state=>state.timestamp
}
//KittensComponent.vue
<script>
exportdefault{
computed:{
catimgsrc(){
return'http://thecatapi.com/api/images/get?size=med&ts='
+this.$store.getters.getTimestamp
}
}
}
</script>
Nowit'salittlebittoofast,right?Let'sdefineatimetoshoweachkitten.It'snotdifficultatall.Forexample,ifwedecidetoshoweachkittenfor3seconds,beforechangingthestateofthetimestampinsidethetickfunction,wejusthavetocheckifthecountervalueisdivisibleby3.Let'salsomaketheamountofsecondstoshowthekittenconfigurable.Addthefollowingtoconfig.js:
//config.js
exportconstWORKING_TIME=0.1*60
exportconstRESTING_TIME=5*60
exportconstKITTEN_TIME=5//eachkittenisvisiblefor5seconds
Nowimportittothemutations.jsfileanduseitinthetickfunctiontocheckifit'stimetochangethetimestamp'svalue:
//mutations.js
import{WORKING_TIME,RESTING_TIME,KITTEN_TIME}from'./config'
<...>
functiontick(state){
<...>
if(state.counter%KITTEN_TIME===0){
state.timestamp=newDate().getTime()
}
}
Wearedone!Youcancheckthefinalcodeforthissectioninthechapter5/pomodoro4folder.Yes,I'vesettheworkingtimeto6secondssothatyoucanhaveabreakandenjoysomereallynicekittensfromthecatapi.com.
So,beforereadingthesummaryofthischapterandstartingthenextone,takeabreak!Justlikethiswonderfulspecies:
Wonderfulthinghavingitsbreak.Belikehim.Takeabreak.
SummaryInthischapter,yousawhowtousetheeventshandlingandtriggeringmechanismtopropagatethecomponents'datachangestotheirparents.
Mostimportantly,youusedthepowerofVuexarchitecturetobeabletoestablishthedataflowbetweenthecomponents.Yousawhowthestoreiscreated,anditsmainparts,mutations,andstates.Youlearnedhowtostructuretheapplicationthatusesthestoresothatitbecomesmodularandmaintainable.Youalsolearnedhowtocreatethestore'sgettersandhowtodefineactionsthatdispatchthestorestate'smutations.Weappliedallthelearnedmechanismstoourapplicationsandsawthedataflowinaction.
Atthispoint,weareabletouseanydataexchangingmechanisminVueapplications,startingfromsimplelocaldatabindinginsidethecomponentsandgoingfurthertoglobalstatemanagement.Atthispoint,weknowallthebasestooperatedatainsideourVueapplication.We'realmostdone!
Inthenextchapter,wewillgodeepintothepluginssystemforVueapplications.Youwilllearnhowtouseexistingpluginsandcreateyourownplugintoenrichyourapplicationswithcustombehavior.
Chapter6.Plugins–BuildingYourHousewithYourOwnBricksInthepreviouschapter,youlearnedhowtomanagetheglobalapplicationstoreusingtheVuexarchitecture.Youlearnedalotofnewconceptsandappliedthem.Youalsolearnedhowtocreateastore,howtodefineitsstateandmutations,andhowtouseactionsandgetters.WebroughtourshoppinglistandPomodoroapplicationstolifeusingtheknowledgeacquiredduringthechapter.
Inthischapter,wewillrevisitVueplugins,seehowtheywork,andhowtheymustbecreated.Wewillusesomeexistingpluginsandcreateourown.
Summingitup,inthischapter,wearegoingtodothefollowing:
UnderstandthenatureofVuepluginsUsetheresourcepluginintheshoppinglistsapplicationCreateapluginthatproduceswhite,pink,andbrownnoisesandapplyittoourPomodoroapplication
ThenatureofVuepluginsPluginsinVue.jsareusedforexactlythesamepurposeastheyareusedinanyotherscope:toaddsomenicefunctionalitythat,duetoitsnature,cannotbeachievedwiththecorefunctionalityofthesystem.PluginswrittenforVuecanprovidevariousfunctionalities,startingfromthedefinitionofsomeglobalVuemethodsoreventheinstancemethodsandmovingtowardprovidingsomenewdirectives,filters,ortransitions.
Inordertobeabletouseanexistingplugin,youmustfirstinstallit:
npminstallsome-plugin--save-dev
Andthen,tellVuetouseitinyourapplication:
varVue=require('vue')
varSomePlugin=require('some-plugin')
Vue.use(SomePlugin)
Wecanalsocreateourownplugins.Thisisalsoeasy.Yourpluginmustprovideaninstallmethodwhereyoudefineanyglobalorinstancemethods,orcustomdirectives:
MyPlugin.install=function(Vue,options){
//1.addglobalmethodorproperty
Vue.myGlobalMethod=...
//2.addaglobalasset
Vue.directive('my-directive',{})
//3.addaninstancemethod
Vue.prototype.$myMethod=...
}
Thenitcanbeusedjustlikeanyotherexistingplugin.Inthischapter,wewillusetheexistingresourcepluginforVue(https://github.com/vuejs/vue-resource)andcreateourownpluginthatgenerateswhite,pink,andbrownnoises.
Usingthevue-resourcepluginintheshoppinglistapplicationOpentheshoppinglistapplication(thechapter6/shopping-listfolder)andrunnpminstallandnpmrundev.It'sniceandclean,butitstillusesthehardcodedlistoftheshoppinglists.Itwouldbereallyniceifwewereabletoaddnewshoppinglists,deletethem,andstoretheinformationonupdatedshoppinglistssothatwhenwerestarttheapplication,thedisplayedinformationcorrespondstothelastwesawbeforerestarting.Inordertobeabletodothat,wewillusetheresourceplugin,whichallowsustoeasilycreateRESTresourcesandcallRESTmethodsonthem.Beforestarting,let'ssummarizeeverythingthatweneedtodoinordertoachievethis:
Firstofall,weneedtohaveasimpleserverthatcontainssomestoragefromwherewecanretrieveandwherewecanstoreourshoppinglists.Thisservermustprovidetheneededendpointsforallthisfunctionality.Aftercreatingourserverandallneededendpoints,weshouldinstallandusethevue-resourceplugintocreatearesourceandactionsthatwillcallthemethodsontheprovidedendpoints.Inordertoguaranteethedataintegrity,weshouldcallactionsthatupdateserver'sstateoneachshoppinglistsupdate.Ontheapplicationstart,weshouldfetchshoppinglistsfromtheserverandassignthemtoourstore'sstate.Weshouldalsoprovideamechanismtocreatenewshoppinglistsanddeletetheexistingones.
Doesn'tsoundtoodifficult,right?Let'sstartthen!
CreatingasimpleserverForthesakeofsimplicity,wewilluseaverybasicandeasy-to-useHTTPserverthatstoresdatainsidearegularJSONfile.Itiscalledjson-serveranditishostedathttps://github.com/typicode/json-server.Installitintheshoppinglistapplication'sdirectory:
cdshopping-list
npminstall--save-devjson-server
Createaserverfolderwiththedb.jsonfileinsideitwiththefollowingcontent:
//shopping-list/server/db.json
{
"shoppinglists":[
]
}
Thiswillbeourdatabase.Let'saddthescriptentrytoourpackage.jsonfilesothatwecaneasilystartourserver:
"scripts":{
"dev":"nodebuild/dev-server.js",
"server":"node_modules/json-server/bin/index.js--watch
server/db.json",
<...>
},
Now,tostartaserver,justrunthefollowing:
cdshopping-list
npmrunserver
Openthebrowserpageathttp://localhost:3000/shoppinglists.Youwillseeanemptyarrayasaresult.Thisisbecauseourdatabaseisstillempty.Trytoinsertsomedatausingcurl:
curl-H"Content-Type:application/json"-d'{"title":"new","items":[]}'
http://localhost:3000/shoppinglists
Ifyourefreshthepagenow,youwillseeyournewinsertedvalue.
NowthatwehaveoursimpleRESTserverupandrunning,let'suseitinourshoppinglistapplicationwiththehelpofthevue-resourceplugin!
Installingvue-resource,creatingresources,anditsmethodsBeforegoingdeeperintotheusageofthevue-resourceplugin,checkoutitsdocumentationathttps://github.com/vuejs/vue-resource/blob/master/docs/resource.md.Basically,thedocumentationprovidesaneasywayofcreatingresourcesbasedonthegivenURL(inourcase,itwillbehttp://localhost:3000/shoppinglists).Aftertheresourceiscreated,wecancallget,delete,post,andupdatemethodsonit.
Installitintheproject'sfolder:
cdshopping-list
npminstallvue-resource--save-dev
Nowlet'screatetheentrypointforourAPI.Insideansrcfolderoftheshoppinglistapplication,createasubfolderandcallitapi.Createanindex.jsfileinsideit.Inthisfile,wewillimportthevue-resourcepluginandtellVuetouseit:
//api/index.js
importVuefrom'vue'
importVueResourcefrom'vue-resource'
Vue.use(VueResource)
Nice!NowwearereadytocreateShoppingListsResourceandattachsomemethodstoit.Tocreatearesourceusingthevue-resourceplugin,wejustcallaresourcemethodonVueandpasstheURLtoit:
constShoppingListsResource=Vue.resource('http://localhost:3000/'+
'shoppinglists{/id}')
TheShoppingListsResourceconstantnowexposesallthemethodsneededfortheimplementationofCRUD(Create,Read,Update,andDelete)operations.Itissoeasytousethatwecouldbasicallyexporttheresourceitself.Butlet'sexportnicemethodsforeachoftheCRUDoperations:
exportdefault{
fetchShoppingLists:()=>{
returnShoppingListsResource.get()
},
addNewShoppingList:(data)=>{
returnShoppingListsResource.save(data)
},
updateShoppingList:(data)=>{
returnShoppingListsResource.update({id:data.id},data)
},
deleteShoppingList:(id)=>{
returnShoppingListsResource.remove({id:id})
}
}
Thefullcodefortheapi/index.jsfilecanbeseeninthisgistathttps://gist.github.com/chudaol/d5176b88ba2c5799c0b7b0dd33ac0426.
That'sit!OurAPIisreadytobeusedandtopopulateourreactiveVuedata!
FetchingalltheshoppingliststheapplicationstartsLet'sstartbycreatinganactionthatwillfetchandpopulatestore'sshoppinglistsstate.Afteritscreation,wecancallitonthemainApp.vuereadystate.
Defineaconstantinthemutation_types.jsfileasfollows:
//mutation_types.js
exportconstPOPULATE_SHOPPING_LISTS='POPULATE_SHOPPING_LISTS'
Nowcreateamutation.Thismutationwilljustreceiveanarrayofshoppinglistsandassignittotheshoppinglistsstate:
//mutations.js
exportdefault{
[types.CHANGE_TITLE](state,data){
findById(state,data.id).title=data.title
},
[types.POPULATE_SHOPPING_LISTS](state,lists){
state.shoppinglists=lists
}
}
Okthen!NowwejustneedanactionthatwillusetheAPI'sgetmethodanddispatchthepopulatingmutation.ImporttheAPIintheactions.jsfileandcreateacorrespondingactionmethod:
import{CHANGE_TITLE,POPULATE_SHOPPING_LISTS}from'./mutation_types'
importapifrom'../api'
exportdefault{
changeTitle:({commit},data)=>{
commit(CHANGE_TITLE,data)
},
populateShoppingLists:({commit})=>{
api.fetchShoppingLists().then(response=>{
commit(POPULATE_SHOPPING_LISTS,response.data)
})
}
}
Intheprecedinglinesofcode,weperformaverysimpletask—wecallthefetchShoppingListsAPI'smethodthat,inturn,callsthegetmethodoftheresource.ThismethodperformsanhttpGETcallandreturnsapromisethatisresolvedwhenthedataisbackfromtheserver.
Thisdataisthenusedtodispatchthepopulatingmutationwithit.Thismethodwillassignthisdatatothestore'sstateshoppinglistsproperty.Thispropertyisreactive;doyouremember?Thismeansthatalltheviewsthatrelyontheshoppinglistspropertygetterwillbeupdated.Let'snowusethisactioninthemainApp.vuecomponentonitsmountedstate.CheckmoreaboutmountedstatehookintheofficialVuedocumentationpageat
https://vuejs.org/v2/api/#mounted.
OpentheApp.vuecomponent,importthemapActionsobject,mapthepopulateShoppingListsactioninsidethecomponent'smethodsproperty,andcallitinsidethemountedhandler.So,afterthechanges,thescripttagofApp.vuelookslikethefollowing:
<script>
importShoppingListComponentfrom'./components/ShoppingListComponent'
importShoppingListTitleComponentfrom
'./components/ShoppingListTitleComponent'
importstorefrom'./vuex/store'
import{mapGetters,mapActions}from'vuex'
exportdefault{
components:{
ShoppingListComponent,
ShoppingListTitleComponent
},
computed:mapGetters({
shoppinglists:'getLists'
}),
methods:mapActions(['populateShoppingLists']),
store,
mounted(){
this.populateShoppingLists()
}
}
</script>
Ifyouopenthepagenow,youwillseetheonlyshoppinglistthatwecreatedusingcurl,asshowninthefollowingscreenshot:
Thedisplayedshoppinglistsarebeingservedbyoursimpleserver!
Trytoinsertmoreitemsusingcurlorevendirectlymodifyingthedb.jsonfile.Refreshthepageandlookhowitworkslikeacharm!
UpdatingserverstatusonchangesVerywell,nowwehaveourshoppinglistsbeingservedbyourRESTAPIandeverythingworksandlooksnice.Trytoaddsomeshoppinglistitemsorchangethetitlesoftheshoppinglistsandcheckoruncheckitems.Afteralltheseinteractions,refreshthepage.Whoops,thelistsareempty,nothinghappened.That'sabsolutelycorrect,wehaveanAPImethodforupdatingthegivenshoppinglistbutwedon'tcallitanywhere,soourserverisnotawareoftheappliedchanges.
Let'sstartbydefiningwhatcomponentsdosomethingwithourshoppinglistssothatthesechangesaresenttotheserver.Thefollowingthreethingscanhappentotheshoppinglistsandtheiritems:
ThetitleofthelistcanbechangedinChangeTitleComponentThenewitemcanbeaddedtotheshoppinglistinAddItemComponentTheitemoftheshoppinglistcanbecheckedoruncheckedinItemComponent
Wemustcreateanactionthatmustbetriggeredonallthesechanges.Withinthisaction,weshouldcalltheupdateAPI'smethod.Haveacloselookattheupdatemethodinsidetheapi/index.jsmodule;itmustreceivethewholeshoppinglistobjectasaparameter:
//api/index.js
updateShoppingList:(data)=>{
returnShoppingListsResource.update({id:data.id},data)
}
Let'screateanactionthatreceivesanidasaparameter,retrievestheshoppinglistbyitsID,andcallstheAPI'smethod.Beforedoingthis,createagetListByIdmethodinthegetters.jsfileandimportitintotheactions:
//getters.js
import_from'underscore'
exportdefault{
getLists:state=>state.shoppinglists,
getListById:(state,id)=>{
return_.findWhere(state.shoppinglists,{id:id})
}
}
//actions.js
importgettersfrom'./getters'
Nowwearereadytodefinetheactionforupdatingtheshoppinglist:
//actions.js
<...>
exportdefault{
<...>
updateList:(store,id)=>{
letshoppingList=getters.getListById(store.state,id)
api.updateShoppingList(shoppingList)
}
}
Actually,wecannowdeletethefindByIdmethodfrommutations.jsandjustreusethisonefromgetters.js:
//mutations.js
import*astypesfrom'./mutation_types'
importgettersfrom'./getters'
exportdefault{
[types.CHANGE_TITLE](state,data){
getters.getListById(state,data.id).title=data.title
},
[types.POPULATE_SHOPPING_LISTS](state,lists){
state.shoppinglists=lists
}
}
Well,nowwehavedefinedtheactionthatcallstheupdateListmethodofourAPI.Nowwejusthavetocalltheactiononeachchangethathappensinsidethecomponents!
Let'sstartwithAddItemComponent.WemustdispatchtheupdateListactioninsidetheaddItemmethodusingthethis.$store.dispatchmethodwiththeaction'sname.However,there'sasmallproblem—wemustpassthelistitemIDtotheupdateListmethodandwedonothaveareferencetoitinsidethiscomponent.Butit'sactuallyaneasyfix.JustaddtheIDinsidethecomponent'spropsandbindittothecomponentonitsinvocationinsideShoppingListComponent.SoourAddItemComponentcomponent'sscripttaglookslikethefollowing:
//AddItemComponent.vue
<script>
exportdefault{
props:['id'],
data(){
return{
newItem:''
}
},
methods:{
addItem(){
vartext
text=this.newItem.trim()
if(text){
this.$emit('add',this.newItem)
this.newItem=''
this.$store.dispatch('updateList',this.id)
}
}
}
}
</script>
And,insideShoppingListComponent,ontheadd-item-componentinvocation,bindtheIDtoit:
//ShoppingListComponent.vue
<template>
<...>
<add-item-component:id="id"@add="addItem"></add-item-component>
<...>
</template>
Now,ifyoutrytoadditemstotheshoppinglistsandrefreshthepage,thenewlyaddeditemsappearinthelist!
NowweshoulddothesameforChangeTitleComponent.OpentheChangeTitleComponent.vuefileandcheckthecode.Rightnow,itcallsthechangeTitleactiononinput:
//ChangeTitleComponent.vue
<template>
<div>
<em>Changethetitleofyourshoppinglisthere</em>
<input:value="title"@input="onInput({title:
$event.target.value,id:id})"/>
</div>
</template>
<script>
import{mapActions}from'vuex'
exportdefault{
props:['title','id'],
methods:mapActions({
onInput:'changeTitle'
})
}
</script>
Wecould,ofcourse,importtheupdateListactionandcallitrightaftercallingthechangeTitleaction.Butitmightbeeasiertodoitinsidetheactionitself.Youmayrememberthatinordertodispatchthestore'saction,weshouldcallthedispatchmethodappliedtothestorewiththeaction'snameasaparameter.SowecandoitinsidethechangeTitleaction.Justopentheaction.jsfile,findourchangeTitleaction,andaddthecalltoupdateList:
//actions.js
exportdefault{
changeTitle:(store,data)=>{
store.commit(CHANGE_TITLE,data)
store.dispatch('updateList',data.id)
},
<...>
}
It'sdone!Openthepage,modifythetitlesofthepages,andrefreshthepage.Thetitlesshouldmaintaintheirmodifiedstate!
Thelastchangethatweneedtoguaranteetobepersistedisthechangeintheshoppinglist'sitemscheckedproperty.Let'slookatItemComponentanddecidewhereweshouldcalltheupdateListaction.
Let'sstartbyaddingtheIDinsidethepropsattribute,justlikewedidwithAddItemComponent:
//ItemComponent.vue
<script>
exportdefault{
props:['item','id']
}
</script>
Wemustalsobindtheidpropertytothecomponent'sinvocation,whichisdoneinsideItemsComponent:
//ItemsComponent.vue
<template>
<ul>
<item-componentv-for="iteminitems":item="item":id="id">
</item-component>
</ul>
</template>
<script>
importItemComponentfrom'./ItemComponent'
exportdefault{
components:{
ItemComponent
},
props:['items','id']
}
</script>
Thisalsomeansthatwemustbindtheidpropertytoitem-componentinsideShoppingListComponent:
//ShoppingListComponent.vue
<template>
<...>
<items-component:items="items":id="id"></items-component>
<...>
</template>
WeshouldalsoimportthemapActionsobjectinsideItemComponentandexporttheupdateListmethodinsidethemethodsproperty:
//ItemComponent.vue
<script>
import{mapActions}from'vuex'
exportdefault{
props:['item','id'],
methods:mapActions(['updateList'])
}
</script>
Okaythen,everythingisboundtoeverything;nowwejusthavetofindtherightplaceinsideItemComponenttocalltheupdateListaction.
Andthisturnsouttobenotsuchaseasytask,becauseunlikeintheothercomponentswherewehadeventhandlersdealingwithchangesandcallingthecorrespondingfunctions,herewejusthaveclassandmodelbindingsattachedtothecheckboxelement.Luckilyforus,Vueprovidesawatchoptionthatallowsustoattachlistenerstoanyofthecomponent'sdataandbindthehandlerstothem.Inourcase,wewanttowatchtheitem.checkedpropertyandcalltheaction.So,justaddthewatchattributetothecomponentsoptionsasfollows:
//ItemComponent.vue
<script>
import{mapActions}from'vuex'
exportdefault{
props:['item','id'],
methods:mapActions(['updateList']),
watch:{
'item.checked':function(){
this.updateList(this.id)
}
}
}
</script>
And...wearedone!Trytoadditemstotheshoppinglists,check,uncheck,andcheckthemagain.Refreshthepage.Everythinglookslikeitwasbeforerefreshing!
CreatinganewshoppinglistOkaythen,wearealreadyfetchingtheshoppinglistsfromtheserver;wealsostoreappliedchanges,sowearefine.Butwouldn'titalsobeniceifwecouldcreatetheshoppinglistsusingtheuserinterfaceofourapplicationinsteadofmodifyingthedb.jsonfileorusingcurlpostrequests?Ofcourse,itwouldbenice.And,ofcourse,wecandoitwithfewlinesofcode!
Let'sstartbyaddingtheactionthatcallsthecorrespondingAPImethod,asfollows:
//actions.js
exportdefault{
<...>
createShoppingList:({commit},shoppinglist)=>{
api.addNewShoppingList(shoppinglist)
}
}
Nowwehavetoprovideavisualmechanismforcallingthisaction.Forthat,wecancreateanextratabinthetablistwiththeplusbutton,whichwillcalltheactionwhenitisclicked.WewilldoitinsidetheApp.vuecomponent.WehavealreadyimportedthemapActionsobject.Let'sjustaddthecreateShoppingListmethodtotheexportedmethodsproperty:
//App.vue
<script>
importShoppingListComponentfrom'./components/ShoppingListComponent'
importShoppingListTitleComponentfrom
'./components/ShoppingListTitleComponent'
importstorefrom'./vuex/store'
import{mapGetters,mapActions}from'vuex'
exportdefault{
components:{
ShoppingListComponent,
ShoppingListTitleComponent
},
computed:mapGetters({
shoppinglists:'getLists'
}),
methods:mapActions(['populateShoppingLists',
'createShoppingList']),
store,
mounted(){
this.populateShoppingLists()
}
}
</script>
Atthismoment,ourApp.vuecomponenthasaccesstothecreateShoppingListactionandcancallitonaneventhandler.Thequestionis—withwhatdata?ThecreateShoppingListmethodiswaitingtoreceiveanobjectthatwillthenbesenttotheserver.Let'screateamethodthatwillgenerateanewlistwithahardcodedtitle,andwithinthismethod,calltheactionwiththis
newobject.Butwhereshoulditputthismethod?ThemethodspropertyofthecomponentisalreadyoccupiedbytheinvocationofthemapActionshelper.Well,themapActionsmethodreturnsamapofmethods.Wecansimplyextendthismapwithourlocalmethod:
//App.vue
methods:_.extend({},
mapActions(['populateShoppingLists','createShoppingList']),
{
addShoppingList(){
letlist={
title:'NewShoppingList',
items:[]
}
this.createShoppingList(list)
}
}),
NowwejustneedtoaddabuttonandbindtheaddShoppingListmethodtoitsclickevent.Youcancreateyourownbuttonanywhereonthepage.Mybutton'scodelookslikethefollowing:
App.vue
<template>
<divid="app"class="container">
<ulclass="navnav-tabs"role="tablist">
<li:class="index===0?'active':''"v-for="(list,index)in
shoppinglists"role="presentation">
<shopping-list-title-component:id="list.id"
:title="list.title"></shopping-list-title-component>
</li>
<li>
<ahref="#"@click="addShoppingList">
<iclass="glyphiconglyphicon-plus-sign"></i>
</a>
</li>
</ul>
<divclass="tab-content">
<div:class="index===0?'active':''"v-for="(list,index)in
shoppinglists"class="tab-pane"role="tabpanel":id="list.id">
<shopping-list-component:id="list.id":title="list.title"
:items="list.items"></shopping-list-component>
</div>
</div>
</div>
</template>
Lookatthepage;nowwehaveaniceplusbuttononthelasttab,whichclearlyindicatesthatthereisapossibilityofaddinganewshoppinglist,asshowninthefollowingscreenshot:
Nowwecanaddnewshoppinglistsusingthisniceplusbutton
Trytoclickonthebutton.Whoops,nothinghappens!However,ifwelookattheNetworkpanel,wecanseetherequestwasactuallyperformedandthatsucceeded:
Thecreationrequestwasperformedsuccessfully;however,nothingchangedonthepage
Actually,thismakesperfectsense.Weupdatedtheinformationontheserver,buttheclientsideisnotawareofthesechanges.Ifwecouldpopulateshoppinglistsafterthesuccessfulshoppinglistcreation,itwouldbenice,wouldn'tit?DidIsay"ifwecould"?Ofcoursewecan!Justgobacktoactions.jsandcallthepopulateShoppingListsactiononthepromise'sthencallbackusingthestore.dispatchmethod:
//actions.js
createShoppingList:(store,shoppinglist)=>{
api.addNewShoppingList(shoppinglist).then(()=>{
store.dispatch('populateShoppingLists')
})
}
Now,ifyouclickontheplusbutton,youwillimmediatelyseethenewlycreatedlistappearinginthetabpane,asshowninthefollowingscreenshot:
Newlyaddedshoppinglistafterrepopulatingourlists
Youcannowclickonthenewshoppinglist,changeitsname,additsitems,andcheckanduncheckthem.Whenyourefreshthepage,everythingisjustlikeitwasbeforetherefreshing.Amazingwork!
DeletingexistingshoppinglistsWearealreadyabletocreateandupdateourshoppinglists.Nowwejustneedtobeabletodeletethem.Afterallthethingsthatwehavelearnedinthischapter,thiswillbetheeasiestpart.WeshouldaddtheactionthatwillcallthedeleteShoppingListmethodofourAPI,addtheremovebuttontoeachoftheshoppinglist,andcalltheactiononthebuttonclick.
Let'sstartbyaddingtheaction.Similarly,aswedidwiththecreationofshoppinglists,wewillcallthepopulatemethodrightafterremovingtheshoppinglist,soouractionwilllooklikethefollowing:
//action.js
deleteShoppingList:(store,id)=>{
api.deleteShoppingList(id).then(()=>{
store.dispatch('populateShoppingLists')
})
}
Nowlet'sthinkwhereweshouldaddtheremovebutton.Iwouldliketoseeitneartheshoppinglisttitleinthetabheader.ThisisthecomponentcalledShoppingListTitleComponent.OpenitandimportthemapActionshelper.Exportitinthemethodsproperty.So,thecodeinsidethescripttagofthiscomponentlookslikethefollowing:
//ShoppingListTitleComponent.vue
<script>
import{mapActions}from'vuex'
exportdefault{
props:['id','title'],
computed:{
href(){
return'#'+this.id
}
},
methods:mapActions(['deleteShoppingList'])
}
</script>
Nowlet'saddtheremovebuttonandbindthedeleteShoppingListmethodtoitsclickeventlistener.WeshouldpasstheIDtothismethod.Wecandoitdirectlyinsidethetemplate:
//ShoppingListTitleComponent.vue
<template>
<a:href="href":aria-controls="id"role="tab"data-toggle="tab">
{{title}}
<iclass="glyphiconglyphicon-remove"
@click="deleteShoppingList(id)"></i>
</a>
</template>
Ialsoaddedalittlebitofstylingtotheremoveiconsothatitlooksabitsmallerandabitmoreelegant:
<stylescoped>
i{
font-size:x-small;
padding-left:3px;
cursor:pointer;
}
</style>
That'sit!Openthepageandyou'llseeatinyxbuttonneareachshoppinglisttitle.Tryclickingonitandyouwillimmediatelyseethechanges,asshowninthefollowingscreenshot:
ShoppinglistswiththeremoveXbuttonthatallowsustodeleteunusedshoppinglists
Congratulations!Nowwehaveafullyfunctionalapplicationthatallowsustocreateshoppinglistsforanyoccasion,removethem,andmanagetheitemsoneachofthem!Goodwork!Thefinalcodeforthissectioncanbefoundinthechapter6/shopping-list2folder.
ExerciseOurshoppinglistsareallverysimilartoeachother.Iwouldliketoproposeasmallstylingexerciseinwhichyoushouldattachcoloringtoyourlistsinordertomakethemdifferonefromanother.Itwillrequireyoutoaddonemorefieldforthebackgroundcolorontheshoppinglistcreationandtouseitinsidethecomponenttopaintyourlistswiththegivencolor.
CreatingandusingaplugininthePomodoroapplicationNowthatweknowhowtouseexistingpluginswithourVueapplication,whynotcreateourownplugin?WealreadyhavealittlebitofanimationinourPomodoroapplication,andthescreenchangescompletelywhenthestateischangedfromtheworkingPomodorointervaltotherestinginterval.However,ifwearenotlookingatthetab,wehavenoideaifweshouldworkorrest.ItwouldbenicetoaddsomesoundstoourPomodoro!
Whenthinkingaboutsoundsinatimemanagementapplication,Iwouldliketothinkaboutthesoundthatisniceforworking.Everyoneofushasourownfavoriteplaylistforwork.Ofcourse,itdiffersaccordingtoeachperson'smusicalpreferences.That'swhyIdecidedtoaddsomeneutralsoundtoourapplicationduringtheworkingperiodoftime.Itwasprovenbysomestudiesthatdifferentnoises(white,pink,brown,andsoon)aregoodforthekindofworkwhereahighlevelofconcentrationisrequired.TheWikipediaentryaboutthesestudiescanbefoundathttps://en.wikipedia.org/wiki/Sound_masking.AndsomeQuoraexpertstalkingaboutthiscanbefoundathttp://bit.ly/2cmRVW2.
Inthissection,wewillusetheWebAudioAPI(https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)tocreateapluginforVuethatgenerateswhite,pink,andbrownnoises.WewillprovideamechanismtoinstantiateonenoiseoranotherusingVuedirectivesandwewillalsoprovideglobalVuemethodsthatwillstartandpausethesesounds.Afterthat,wewillusethisplugintoswitchbetweenasilentstatewhilerestingandlookingatcatsandanoisystatewhileworking.Doesitsoundchallengingandinteresting?Ireallyhopeitdoes!Let'sstartthen!
CreatingtheNoiseGeneratorpluginOurpluginwillbestoredinasingleJavaScriptfile.Itwillcontainthreemethods,oneforthegenerationofeachnoiseandprovideaVue.installmethodwherethedirectivesandneededVuemethodswillbedefined.Usethechapter6/pomodorofolderasastartingpoint.StartbycreatingapluginssubfolderinthesrcfolderandaddingtheVueNoiseGeneratorPlugin.jsfilethere.Nowlet'screatethefollowingthreemethods:
generateWhiteNoise
generatePinkNoise
generateBrownNoise
IwillnotreinventthewheelandwilljustcopyandpastethealreadyexistingcodethatIfoundontheInternet.Ofcourse,IwouldliketogivehugecredittothegreatresourcethatIfoundathttp://noisehack.com/generate-noise-web-audio-api/.Thatbeingsaid,ourpluginaftercopyingthecodeandorganizingitinthefunctionsshouldlooklikethefollowing:
//plugins/VueNoiseGenerator.js
import_from'underscore'
//Thankstothisgreattutorial:
//http://noisehack.com/generate-noise-web-audio-api/
varaudioContext,bufferSize,noise
audioContext=new(window.AudioContext||window.webkitAudioContext)()
functiongenerateWhiteNoise(){
varnoiseBuffer,output
bufferSize=2*audioContext.sampleRate
noiseBuffer=audioContext.createBuffer(1,bufferSize,
audioContext.sampleRate)
output=noiseBuffer.getChannelData(0)
_.times(bufferSize,i=>{
output[i]=Math.random()*2-1
})
noise=audioContext.createBufferSource()
noise.buffer=noiseBuffer
noise.loop=true
noise.start(0)
returnnoise
}
functiongeneratePinkNoise(){
bufferSize=4096
noise=(function(){
varb0,b1,b2,b3,b4,b5,b6,node
b0=b1=b2=b3=b4=b5=b6=0.0
node=audioContext.createScriptProcessor(bufferSize,1,1)
node.onaudioprocess=function(e){
varoutput
output=e.outputBuffer.getChannelData(0)
_.times(bufferSize,i=>{
varwhite=Math.random()*2-1
b0=0.99886*b0+white*0.0555179
b1=0.99332*b1+white*0.0750759
b2=0.96900*b2+white*0.1538520
b3=0.86650*b3+white*0.3104856
b4=0.55000*b4+white*0.5329522
b5=-0.7616*b5-white*0.0168980
output[i]=b0+b1+b2+b3+b4+b5+b6+white*0.5362
output[i]*=0.11//(roughly)compensateforgain
b6=white*0.115926
})
}
returnnode
})()
returnnoise
}
functiongenerateBrownNoise(){
bufferSize=4096
noise=(function(){
varlastOut,node
lastOut=0.0
node=audioContext.createScriptProcessor(bufferSize,1,1)
node.onaudioprocess=function(e){
varoutput=e.outputBuffer.getChannelData(0)
_.times(bufferSize,i=>{
varwhite=Math.random()*2-1
output[i]=(lastOut+(0.02*white))/1.02
lastOut=output[i]
output[i]*=3.5//(roughly)compensateforgain
})
}
returnnode
})()
returnnoise
}
YoucantestallthesenoisesintheJSFiddleathttps://jsfiddle.net/chudaol/7tuewm5z/.
Okay,sowehaveallthethreenoisesimplemented.NowwemustexporttheinstallmethodthatwillbecalledbyVue.ThismethodreceivestheVueinstanceandcancreatedirectivesandmethodsonit.Let'screateadirectiveandcallitnoise.Thisdirectivecanhaveoneofthreevalues,white,pink,orbrown,andaccordingtothereceivedvaluewillinstantiatethenoisevariablebycallingthecorrespondingnoisecreationmethod.So,ourdirectivecreationwithinaninstallmethodwilllooklikethefollowing:
//plugins/VueNoiseGeneratorPlugin.js
exportdefault{
install:function(Vue){
Vue.directive('noise',(value)=>{
varnoise
switch(value){
case'white':
noise=generateWhiteNoise()
break
case'pink':
noise=generatePinkNoise()
break
case'brown':
noise=generateBrownNoise()
break
default:
noise=generateWhiteNoise()
}
noise.connect(audioContext.destination)
audioContext.suspend()
})
}
}
Aftertheinstantiation,weconnectthenoisetothealreadyinstantiatedaudioContextandsuspenditbecausewedon'twantittostartproducingthenoiserightonthedirectivebinding.Wewantittobeinstantiatedonsomeevents(forexample,clickingonthestartbutton)andpausedonotherevents(forexample,whensomeoneclicksonthepausebutton).Forthat,let'sprovidemethodsforstarting,pausing,andstoppingouraudioContext.WewillputthesethreemethodsontheglobalVuepropertycallednoise.Wewillcallthesemethodsstart,pause,andstop.Withinthestartmethod,wewanttoresumeaudioContextandsuspenditonboththepauseandstopmethods.So,ourmethodswilllooklikethefollowing:
//plugins/VueNoiseGeneratorPlugin.js
exportdefault{
install:function(Vue){
Vue.directive('noise',(value)=>{
<...>
})
Vue.noise={
start(){
audioContext.resume()
},
pause(){
audioContext.suspend()
},
stop(){
audioContext.suspend()
}
}
}
}
That'sit!Ourpluginiscompletelyreadytobeused.It'snotperfect,ofcourse,becausewe
onlyhaveoneaudioContext,whichisbeinginstantiatedonceandthenpopulatedbyoneofthechosennoises,meaningwewillnotbeabletousethenoisedirectivemorethanonceonthepage,butagain,thisisjustaprototypeandyouaremorethanwelcometoenhanceitandmakeitperfectandpublic!
UsingtheplugininthePomodoroapplicationFinethen,nowwehaveournicenoise-producingplugin,andtheonlythingthatismissingisusingit!Youalreadyknowhowtodoit.Openthemain.jsfile,importVueNoiseGeneratorPlugin,andtellVuetouseit:
importVueNoiseGeneratorPluginfrom
'./plugins/VueNoiseGeneratorPlugin'
Vue.use(VueNoiseGeneratorPlugin)
Fromnowon,wecanattachthenoisedirectiveandusetheVue.noisemethodinanypartofourPomodoroapplication.Let'sbindittoourmaintemplateinsidetheApp.vuecomponent:
//App.vue
<template>
<divid="app"class="container"v-noise="'brown'">
<...>
</div>
</template>
Notethatweusev-noiseinthenameofthedirectiveandnotjustnoise.Wealreadytalkedaboutitwhenwelearnedcustomdirectives.Touseadirective,weshouldalwaysprependthev-prefixtoitsname.Alsonotethatweuseddoublequotesinsidethesinglequotestowrapthebrownstring.Ifwedidn'tdoit,Vuewouldsearchforthedatapropertycalledbrown,becausethat'showtheVueworks.AswecanwriteanyJavaScriptstatementinsidethedirectivebindingassignment,wemustpassthestringwithdoublequotes.Youcangofurtherandcreateadatapropertycallednoiseandassigntoitthevalueyouwant(white,brown,orpink)andreuseitinsidethedirectivebindingsyntax.
Afterthatbeingdone,let'scalltheVue.noise.startmethodinourstartmutation:
//mutations.js
importVuefrom'vue'
<...>
exportdefault{
[types.START](state){
<...>
if(state.isWorking){
Vue.noise.start()
}
},
<...>
Checkthepageandclickonthestartbutton.Youwilllistentoanicebrownnoise.Becareful,however,tonottowakeupyourcoworkersnortoscareyourfamily(orviceversa).Trychangingthevalueofthenoisedirectiveandchooseyourfavoritenoisetoworkwith.
Still,wearenotdone.Wecreatedamechanismsothatthenoiseisstarted,butit'sturningout
tobeanever-endingnoise.Let'scalltheVue.noise.pauseandVue.noise.stopmethodsonthepauseandstopmutations,respectively:
//mutations.js
exportdefault{
<...>
[types.PAUSE](state){
<...>
Vue.noise.pause()
},
[types.STOP](state){
<...>
Vue.noise.stop()
}
}
Lookatthepage.Nowifyouclickonthepauseorstopbutton,thenoiseissuspended!Wearestillnotdoneyet.Rememberthatourpurposewastohavethenoiseonlyduringworkingtimeandnotduringrestingtime.So,let'shavealookatthetooglePomodoromethodinsidemutations.jsandaddamechanismthatstartsorstopsthenoiseaccordingtothePomodoro'scurrentstate:
//mutations.js
functiontogglePomodoro(state,toggle){
if(_.isBoolean(toggle)===false){
toggle=!state.isWorking
}
state.isWorking=toggle
if(state.isWorking){
Vue.noise.start()
}else{
Vue.noise.pause()
}
state.counter=state.isWorking?WORKING_TIME:RESTING_TIME
}
ThecodeofthePomodoroapplicationafterallthesemodificationscanbefoundinthechapter6/pomodoro2folder.Checkhowthenoiseisstartedwhenwestarttheapplication,howit'spausingwhentheworkingPomodoroiscompleted,andhowitrestartedagainwhenweshouldbebacktowork.Checkalsohowthestart,pause,andstopbuttonstriggerthenoiseaswell.Nicework!
CreatingabuttontotogglethesoundIt'sreallynicethatwehavethenoisesoundboundtotheworkingstateofthePomodoroapplication.It'salsonicethatthesoundispausedwhenwepausetheapplication.However,itmightbealsousefultobeabletopausethesoundwithouthavingtopausethewholeapplication.Thinkaboutthosesituationswhenyouwanttoworkincompletesilence,oryoumightwanttoreceiveaSkypecall.Inthesesituations,havinganoiseinbackground,evenifit'sniceandpink,isnotniceatall.Let'saddabuttontoourapplicationtotogglethesound.StartbydeclaringastorepropertycalledsoundEnabledandinitializeitwithtrue.Also,creategetterforthisproperty.Sostore.jsandgetters.jsstartlookinglikethefollowing:
//store.js
<...>
conststate={
<...>
soundEnabled:true
}
//getters.js
exportdefault{
<...>
isSoundEnabled:state=>state.soundEnabled
}
Nowwemustprovideamechanismtotogglethesound.Let'screateamutationmethodforthisandaddanactionthatdispatchesthismutation.StartbydeclaringamutationtypecalledTOGGLE_SOUND:
//mutation_types.js
<...>
exportconstTOGGLE_SOUND='TOGGLE_SOUND'
Nowlet'sopenmutations.jsandaddthemutationmethodthattogglesthesoundEnabledstoreproperty:
//mutations.js
[types.TOGGLE_SOUND](state){
state.soundEnabled=!state.soundEnabled
if(state.soundEnabled){
Vue.noise.start()
}else{
Vue.noise.pause()
}
}
Nowlet'saddtheactionthatdispatchesthismutation:
//actions.js
exportdefault{
<...>
toggleSound:({commit})=>{
commit(types.TOGGLE_SOUND)
}
}
Okaythen,nowwehaveeverythingweneedtocreateatogglesoundbutton!Let'sdoitinourControlsComponent.Startbyaddinganecessarygetterandactiontothemapofmethods:
//ControlsComponent.vue
<script>
import{mapGetters,mapActions}from'vuex'
exportdefault{
computed:mapGetters(['isStarted','isPaused','isStopped',
'isSoundEnabled']),
methods:mapActions(['start','stop','pause','toggleSound'])
}
</script>
Nowwecanaddthebuttontoourtemplate.Isuggestthatitwillbetheiconwiththeglyphiconclassthatwillbealignedtotheright.
Let'sonlyshowthisiconwhentheapplicationisstartedandnotpaused,andonlywhenthePomodorostateisworkingsothatwedon'tmessupthetogglesoundbuttoninastatewhereitisnotsupposedtohavesoundatall.Thismeansthatourv-showdirectiveonthiselementwilllooklikethefollowing:
v-show="isStarted&&!isPaused&&isWorking"
NotethatweareusingtheisWorkingpropertyhere,whichhasnotyetbeenimported.Addittothemapofmethods:
//ControlsComponents.vue
<script>
import{mapGetters,mapActions}from'vuex'
exportdefault{
computed:mapGetters(['isStarted','isPaused','isStopped',
'isWorking','isSoundEnabled']),
methods:mapActions(['start','stop','pause','toggleSound'])
}
</script>
Let'salsousetheglyphicon-volume-offandglyphicon-volume-onclassesonthiselement.Theywillindicatecallingfortheactiontotogglethesound'sstate.Thismeansthattheglyphicon-volume-offclassshouldbeappliedwhenthesoundisenabledandtheglyphicon-volume-onclassshouldbeappliedwhenthesoundisdisabled.Puttingitinthecode,ourclassdirectiveshouldlooklikethefollowing:
:class="{'glyphicon-volume-off':isSoundEnabled,'glyphicon-volume-up':
!isSoundEnabled}"
Lastbutnotleast,weshouldcallthetoggleSoundactionwhenthebuttonisclicked.This
meansthatweshouldalsobindtheclickeventlistenertothiselement,whichwilllooklikethefollowing:
@click='toggleSound'
So,thewholejademarkupcodeforthisbuttonwillbelikethefollowing:
//ControlsComponent.vue
<template>
<span>
<...>
<iclass="toggle-volumeglyphicon"v-show="isStarted&&
!isPaused&&isWorking":class="{'glyphicon-volume-off':
isSoundEnabled,'glyphicon-volume-up':!isSoundEnabled}"
@click="toggleSound"></i>
</span>
</template>
Let'sjustaddabitofstylingtothisbuttonsothatitappearsalignedtotheright:
<stylescoped>
<...>
.toggle-volume{
float:right;
cursor:pointer;
}
</style>
OpenthepageandstartthePomodoroapplication.Nowyoucanseethisnicebuttononthetop-rightcornerthatwillallowyoutoturnthesoundoff,asshowninthefollowingscreenshot:
Nowwecanturnthesoundoffwhileworking!
Ifyouclickonthisbutton,itwilltransformintoanotherbutton,whosepurposeistoturnthesoundonagain,asshowninthefollowingscreenshot:
Andwecanturnitonagain!
Nowconsiderthefollowingscenario:westarttheapplication,turnoffthesound,pausetheapplication,andresumetheapplication.Ourcurrentlogicsuggeststhatthesoundisstartedeachtimetheapplicationisstarted.Wewillbeinaninconsistentstate—theapplicationhasstarted,thesoundisplaying,butthetogglingsoundbuttonissuggestingtoturnthesoundon.That'snotright,isit?Butthishasaneasyfix—justaddonemoreconditiontothestartmutation,notonlyitshouldcheckifisWorkingistrue,butalsothatthesoundisenabled:
//mutations.js
[types.START](state){
<...>
if(state.isWorking&&state.soundEnabled){
Vue.noise.start()
}
},
Nowwearefine.Thecodeafterallthesemodificationscanbefoundinthechapter6/pomodoro3folder.
Checkthecode,runtheapplication,enjoythesound,anddonotforgettohaveabreak!
ExerciseItwouldbeniceifduringourPomodorointervalswecouldalsoenjoysomehappynicemusicwhilelookingatcats.Createapluginthatplaysachosenmp3fileanduseitonthePomodorointervals.
SummaryWhileIwaswritingthelastlinesofcodeforthischapterandcheckingthepage,atonepointIgotstucklookingatthispicture:
Alotofcatslookingatmeandasking:willthischaptergettoitsendatsomepoint?
Ievenpausedtheapplicationtohaveabetterlookatthispicture(yes,whenyoupausethePomodoroapplicationduringrestingtime,thepicturewillpauseaswellbecausethecache-bustertimestampisnotbeingupdatedanymore).Doesn'titseemlikethesecatsareaskingustogetsomerest?Also,theamountofthemisprettyclosetothenumberofthingsthatwe'velearnedinthischapter!
Inthischapter,youlearnedhowthepluginssystemworkwithVue.js.Weusedanexistingresourceplugintoattachtheserver-sidebehaviortoourshoppinglistapplication.Nowwe
cancreate,delete,andupdateourshoppinglists.
Wehavealsocreatedourownplugin!Ourpluginisabletoproducesoundthatcanhelpinconcentratingduringtheworkingperiod.Notonlyhavewecreatedit,butwehavealsouseditinourPomodoroapplication!NowwecanconcentratebetterwhilePomodoroisworkingandtogglethesoundatanytime!
Nowwehavetworeallyniceapplicationsinourhands.Doyouknowwhatisbetterthananiceapplication?
Theonlythingthatisbetterthananiceapplicationisanicelytestedapplication!
Withthatinmind,it'sabouttimewetestedourapplications.Inthenextchapter,wewillcheckandapplysometestingtechniques.WewillwriteunittestsusingKarmatestrunnerandJasmineasanassertionlibrary.Wewillalsowriteend-to-endtestsusingNightwatch.IlovetotestapplicationsandIhopethatyouwillloveitaswell.Let'sgo!
Chapter7.Testing–TimetoTestWhatWeHaveDoneSoFar!Inthepreviouschapter,youlearnedhowtouseandcreateVueplugins.WeusedtheexistingresourcepluginforVueandcreatedourownNoiseGeneratorplugin.
Inthischapter,wewillensurethequalityofboththePomodoroandshoppinglistapplications.Wewilltesttheseapplicationsapplyingdifferenttestingtechniques.First,wewillperformaclassicunittestonVuecomponentsandonVuex-relatedcodesuchasactions,mutations,andgetters.Afterthat,wewilllearnhowtoperformend-to-endtestingusingNightwatch.So,inthischapter,wewilldothefollowing:
Talkabouttheimportanceofunitandend-to-endtestsImplementunittestsforthePomodoroandshoppinglistapplicationsLearnhowtomockserverresponsesinunittestsImplementend-to-endtestsforbothapplicationsusingNightwatch
Whyunittests?Beforewestartwritingunittests,let'strytounderstandwhatwe'retryingtoachievebywritingthem.Whyisunittestingsoimportant?SometimeswhenIwritemytests,theonlythingIcanthinkaboutismycodecoverage;Iwanttoachievealevelof100%.
Codecoverageisaveryimportantmetricandhelpsalottounderstandthecodeflowandwhatneedstobetested.Butitisnotametricofunittestquality.Thisisnotametricofagoodcodequality.Youcanhaveyourcode100%coveredjustbecauseyoucallallyourfunctionsinyourtestingcode,butifyourassertionsarewrong,thecodemightbewrongaswell.Writinggoodunittestsisanartthatrequirestimeandpatience.Butwhenyourunittestsaregoodenoughandwhenyouareconcentratingonmakinggoodassertions,withregardtocornercasesandbranchcoverage,theyprovidethefollowing:
HelpustoidentifyfailuresinalgorithmsandlogicHelpustoimprovethecodequalityMakeuswritecodethatiseasytotestPreventfuturechangesfrombreakingthefunctionalityHelpustohavemorepredictabledeadlinesandestimations
Codethatiseasytocoverwithunittestsisatthesametimecodethatiseasytoread.Codethatiseasytoreadislesserror-proneandmoremaintainable.Maintainabilityisoneofthemainpillarsofanapplication'squality.
Note
Checkmoreaboutunittestinginthepresentationathttps://chudaol.github.io/presentation-unit-testing.
Let'swritesomeunittestsforourapplications.
WewillusetheKarmatestrunner,Mochatestframework,Chaiexpectationslibrary,andSinonformocks.
Formoreinformationaboutthesetools,refertothefollowing:
Karma:http://karma-runner.github.io/Mocha:https://mochajs.orgChaijs:http://chaijs.com/Sinon:http://sinonjs.org/
Ifwehadn'tbootstrappedourapplicationusingvue-cliwebpackscaffolding,wewouldhavetoinstallallthesetoolsvianpm.Butinourcase,wedon'tneedthisinstallation.Checkyourpackage.jsonfileandyoucanseethatallthesethingsarealreadythere:
"devDependencies":{
<...>
"chai":"^3.5.0",
<...>
"karma":"^0.13.15",
"karma-chrome-launcher":"^2.0.0",
"karma-coverage":"^0.5.5",
"karma-mocha":"^0.2.2",
"karma-phantomjs-launcher":"^1.0.0",
"karma-sinon-chai":"^1.2.0",
"mocha":"^2.4.5",
<...>
}
Youcertainlyknowhowsimpleitistowriteunittestsforsimplefunctions.It'salmostlikespeakinghumanlanguage.It(thisfunction)shouldreturnXiftheinputisY.IexpectittobeX.
Soifwehaveamodulethatexports,let'ssay,afunctionthatreturnsthesumoftwoarguments,theunittestforthisfunctionmustcallthefunctionwithdifferentargumentsandexpectsomeoutput.So,let'sassumewehaveafunctionsuchasthefollowing:
functionsum(a,b){
returna+b
}
Thenourunittestmightlooklikethefollowing:
it('shouldfollowcommutativelaw',()=>{
leta=2;
letb=3;
expect(sum(a,b)).to.equal(5);
expect(sum(b,a)).to.equal(5);
})
Weshouldneverbeshywhenwethinkaboutthepossibleinputstofunctionsthatarebeingunittested.Emptyinputs,negativeinputs,stringinputs,everythingcounts!Haveyouseenthisfamoustweet(https://twitter.com/sempf/status/514473420277694465)?
ViraltweetaboutQAEngineer'smindset
Thinkaboutallthepossibleinputsandadequateoutputs.Expressthisinexpectationsandassertions.Runthetests.Seewhatisfailing.Fixyourcode.
UnittestsforVueapplicationFirst,let'scheckonsomeparticularitiesofunittestingourVueapplicationanditscomponents.Inordertobeabletowritetestsforthecomponentinstance,firstofall,itshouldbeinstantiated!Quitelogical,right?Thethingis,howdoweinstantiatetheVuecomponentsothatitsmethodsbecomeaccessibleandeasilytestable?Totestbasicassertionsoftheinitialstateofthecomponent,youmustjustimportthemandasserttheirproperties.Ifyouwanttotestdynamicproperties—thingsthatchangeoncethecomponentisboundtoDOM—youmustdojustthefollowingthreethings:
1. Importacomponent.2. InstantiateitbypassingittotheVuefunction.3. Mountit.
Tip
WhentheinstanceisboundtothephysicalDOM,onceinstantiated,thecompilationisstartedimmediately.Inourcase,wearenotbindingtheinstancetoanyrealphysicalDOMelement,andthuswehavetoexplicitlymakeitcompileitbyinvokingmanuallythemountmethod($mount).
Nowyoucanusethecreatedinstanceandaccessitsmethods.Inpseudo-code,itlookssomethinglikethefollowing
importMyComponentfrom<pathtomycomponent>
varvm=newVue(MyComponent).$mount()
Nowwecanaccessallvminstancemethodsandtestthem.Therestofthethings,suchasdata,props,andsoonwecanjustfake.Thereisnoproblemwithfakingthingsbecauseitoffersusthepossibilityoftryingallsortsofinputeasilyandtestingalltheviableoutputsforeachofthem.
Ifyouwanttohaveamorerealscenariowhiletestingcomponentsthatuseprops,whichcomeboundtothecomponentbyitsparent,oraccesstothevuexstore,andsoon,youcanusetherefattributetobindthecomponenttotheVueinstance.ThisVueinstance,initsturn,instantiatesthestoreanddataandbindsthedataitemstothecomponentinausualway.Afterthat,youaccessthecomponentinstancebyusingthe$refsVueproperty.Thiskindofbindingwilllooklikethefollowing:
importstorefrom<pathtostore>
importMyComponentfrom<pathtomycomponent>
//loadthecomponentwithavueinstance
varvm=newVue({
template:'<div><test:items="items":id="id"ref=testcomponent></test></div>',
components:{
'test':MyComponent
},
data(){
return{
items:[],
id:'myId'
}
},
store
}).$mount();
varmyComponent=vm.$refs.testcomponent;
NowyoucantestallthemethodsofmyComponentwithoutbeingworriedaboutoverridingitsprops,methods,andotherinstance-relatedthings.Thisisagoodpartofthisapproach;however,asyoucansee,itisnottheeasiestsetupandyoushouldthinkabouteverything.Forexample,ifyourcomponentcallssomestore'sactionthatcallssomeAPI'smethods,youshouldbereadytohavetofaketheserverresponses.
Ipersonallyliketokeepthingsassimpleaspossible,fakeallthedatainputs,andconcentrateontestingthefunctions'possibleoutputsandallthepossibleedgecases.Butitisjustmypersonalpointofview,andalso,weshouldtryeverythinginourlives,sointhischapter,wewilltrydifferentapproaches.
WritingunittestsfortheshoppinglistapplicationBeforestartingtheactualwritingofourunittests,let'sestablishsomerules.Foreachofour.jsor.vuefiles,therewillexistacorrespondingtestspecfile,whichwillhavethesamenameanda.spec.jsextension.Thestructureofthesespecswillfollowthisapproach:
ItwilldescribethefilewearetestingItwillhaveadescribemethodforeachofthemethodsthatisbeingtestedItwillhaveanitmethodforeachofthecaseswearedescribing
So,ifwehadamyBeautifulThing.jsfileandspecforit,itmightlooklikethefollowing:
//myBeautifulThing.js
exportmyBeautifulMethod1(){
return'hellobeauty'
}
exportmyBeautifulMethod2(){
return'helloagain'
}
//myBeautifulThing.spec.js
importmyBeautifulThingfrom<pathtomyBeautifulThing>
describe('myBeautifulThing',()=>{
//defineneededvariables
describe('myBeautifulMethod1',()=>{
it('shouldreturnhellobeauty',(){
expect(myBeautifulThing.myBeautifulMethod1()).to.equal('hello
beauty')
})
})
})
Let'sstartbycoveringwithunittestsallthethingsthatareinsidethevuexfolder.
Testingactions,getters,andmutationsForthissection,usethecodeinsidethechapter7/shopping-listfolder.Donotforgettorunthenpminstallcommand.Notethattherearetwonewmutations:ADD_SHOPPING_LISTandDELETE_SHOPPING_LIST.ThesemutationsaddnewshoppinglisttothelistandremovethelistbyitsID.TheyareusedinsidethecreateShoppingListanddeleteShoppingListactionsinsidethepromisefailurehandlers:
//actions.js
createShoppingList:(store,shoppinglist)=>{
api.addNewShoppingList(shoppinglist).then(()=>{
store.dispatch('populateShoppingLists')
},()=>{
store.commit(ADD_SHOPPING_LIST,shoppinglist)
})
},
deleteShoppingList:(store,id)=>{
api.deleteShoppingList(id).then(()=>{
store.dispatch('populateShoppingLists')
},()=>{
store.commit(DELETE_SHOPPING_LIST,id)
})
}
Thus,evenifourbackendserverisdown,westillarenotlosingthisfunctionality.
Ifyoucheckyourproject'sstructure,youwillseethatthereisalreadyanexistingdirectorynamedtest.Insidethisdirectory,therearetwodirectories,unitande2e.Fornow,weshouldgototheunitfolder.Here,youwillseeanotherdirectorycalledspecs.Thisiswhereallourunittestspecificationswillreside.Let'sstartbycreatingadirectorycalledvuexinsidespecs.HereiswhereallourspecsforVuex-relatedJavaScriptfileswilllive.
Let'sstartbytestingthemutations.jsmethod.
Createamutations.spec.jsfile.Inthisfile,weshouldimportmutations.jsandmutationtypessothatwecaneasilyinvokemutations.Havealookatmutationsdeclaredinmutations.js.Allofthemreceivestateandsomeotherparameters.Let'salsocreateafakestateobjectwiththeshoppinglistarrayinsideitsowecanuseitinourtests.
Let'salsoresetitbeforeeachtesttoanemptyarray.
So,afterallthepreparations,thebootstrappedspecformutations.jslookslikethefollowing:
//mutations.spec.js
importmutationsfrom'src/vuex/mutations'
import{ADD_SHOPPING_LIST,DELETE_SHOPPING_LIST,POPULATE_SHOPPING_LISTS,
CHANGE_TITLE}from'src/vuex/mutation_types'
describe('mutations.js',()=>{
varstate
beforeEach(()=>{
state={
shoppinglists:[]
}
})
})
Let'snowaddtestsfortheADD_SHOPPING_LISTmutation.
Checkagainwhatitisdoing:
[types.ADD_SHOPPING_LIST](state,newList){
state.shoppinglists.push(newList)
},
Thismutationjustpushesthereceivedobjecttotheshoppinglistsarray.Prettystraightforwardandeasytotest.
Startbycreatingadescribestatementwiththenameofthefunction:
describe('ADD_SHOPPING_LIST',()=>{
})
Now,insidethisdescribecallback,wecanadditstatementswiththeneededassertions.Let'sthinkwhatshouldhappenwhenweaddanewshoppinglisttotheshoppinglistsarray.Firstofall,thearray'slengthwillincrease,anditwillalsocontainthenewlyaddedshoppinglistobject.Thisisthemostbasicthingtotest.Ouritfunctionwiththeneededassertionswilllooklikethefollowing:
it('shouldadditemtotheshoppinglistarrayandincreaseits
length',()=>{
//calltheadd_shopping_listmutations
mutations[ADD_SHOPPING_LIST](state,{id:'1'})
//checkthatthearraynowequalsarraywithnewobject
expect(state.shoppinglists).to.eql([{id:'1'}])
//checkthatarray'slengthhadincreased
expect(state.shoppinglists).to.have.length(1)
})
Aftercreatingthisfunction,thewholespec'scodeshouldlooklikethefollowing:
//mutations.spec.js
importmutationsfrom'src/vuex/mutations'
import{ADD_SHOPPING_LIST,DELETE_SHOPPING_LIST,POPULATE_SHOPPING_LISTS,
CHANGE_TITLE}from'src/vuex/mutation_types'
describe('mutations.js',()=>{
varstate
beforeEach(()=>{
state={
shoppinglists:[]
}
})
describe('ADD_SHOPPING_LIST',()=>{
it('shouldadditemtotheshoppinglistarrayandincreaseits
length',()=>{
mutations[ADD_SHOPPING_LIST](state,{id:'1'})
expect(state.shoppinglists).to.eql([{id:'1'}])
expect(state.shoppinglists).to.have.length(1)
})
})
})
Let'srunthetests!Opentheconsoleintheproject'sdirectoryandrunthefollowing:
npmrununit
Youshouldseethefollowingoutput:
Theoutputofrunningourtest
RememberthejokeaboutaQAengineer?Wecantesttheadd_shopping_listfunctionforallpossibleinputs.Whatshouldhappen,forexample,ifwecallitwithoutpassinganyobject?Intheory,itshouldnotaddittotheshoppinglistarray,right?Let'stestit.Createanewitstatementandtrytocallthefunctionwithoutthesecondparameter.Assertforanemptylist.
Thistestwilllooksomethinglikethefollowing:
it('shouldnotaddtheitemifitemisempty',()=>{
mutations[ADD_SHOPPING_LIST](state)
expect(state.shoppinglists).to.have.length(0)
})
Runthetestswiththenpmrununitcommand.Oh,snap!Itfailed!Theerrorisasfollows:
expected[undefined]tohavealengthof0butgot1
Why?Havealookatthecorrespondingmutation.Itjustpushesthereceivedparametertothe
arraywithoutanychecks.That'swhyweareabletoaddanygarbage,anyundefined,andanyotherinappropriatevalue!DoyourememberwhenIsaidthatwritinggoodunittestshelpsustocreatelesserror-pronecode?Thisisthecase.Nowwerealizethatweshouldprobablyrunsomechecksbeforepushingthenewitemtothearray.Let'saddthecheckthatthereceiveditemisanobject.OpentheADD_SHOPPING_LISTmutationinthemutations.jsfileandrewriteitasfollows:
//mutations.js
[types.ADD_SHOPPING_LIST](state,newList){
if(_.isObject(newList)){
state.shoppinglists.push(newList)
}
}
Runthetestsnow.Theyareallpassing!
Ofcourse,wecouldbeevenmoreprecise.Wecouldcheckandtestforemptyobjectsandwecouldalsorunsomevalidationsforthisobjecttocontainpropertiessuchasid,items,andtitle.Iwillleaveittoyouasasmallexercise.Trytothinkaboutallpossibleinputsandallpossibleoutputs,writeallthepossibleassertions,andmakethecodetocorrespondtothem.
GoodtestcriteriaAgoodunittestisonethatwouldfailwhenyouchangeyourcode.Imagine,forexample,thatwedecidetoassignadefaulttitletothenewshoppinglistbeforepushingittothearray.So,themutationwouldlooklikethefollowing:
[types.ADD_SHOPPING_LIST](state,newList){
if(_.isObject(newList)){
newList.title='NewShoppingList'
state.shoppinglists.push(newList)
}
}
Ifyourunthetests,theywillfail:
Unittestfailswhenthecodechanges
Andthisisverygood.Whenyourtestsfailafterthechangesinthecode,thepossibleoutcomeisthatyoufixthetestbecausethecodeisperformingtheintendedbehavior,oryoufixyourcode.
CodecoverageIamsurethatyouhavenoticedsometeststatisticsintheconsoleoutputafterrunningthetests.Thesestatisticsdisplaydifferenttypesofcoveragethatourtestsachievedatthetimeofrunning.Rightnow,itlookslikethefollowing:
Codecoverageofmutations.jsafterwritingtwotestsfortheADD_SHOPPING_LISTmutation
DoyourememberwhenIsaidthatgoodcodecoveragedoesn'tmeanthatourtestsandcodeareperfect?Weactuallyhavesomewhatnicestatements,branches,andlinescoverage,butwestilljusttestedonlyonefunctionofonlyonefile,andwehaven'tevencoveredallpossibleinputsofthisfunction.Butnumbersdonotlie.Wehavealmost100%branchescoveragebecausewealmostdonothavebranchesinourcode.
Ifyouwanttoseeamoredetailedreport,justopentheindex.htmlfilefromthetest/unit/coverage/lcov-reportdirectoryinyourbrowser.Itwillgiveyouacompleteandfullpictureofyourcodeandwhatexactlyiscoveredandhow.Currently,itlookslikethefollowing:
Thewholepictureofourcodebasecoverage
Youcandrilldowntothefolders,openthefiles,andcheckhowexactlyourcodeiscovered.Let'scheckmutations.js:
Coveragereportforactions.jsshowexactlywhichcodewascoveredandwhichwasnot
Nowyouseewhatstillhastobetested.Doyouwanttoseehowitreportstheif…elsemissingbranchcoverage?Justskipoursecondtest:
it.skip('shouldnotaddtheitemifitemisempty',()=>{
mutations[ADD_SHOPPING_LIST](state)
expect(state.shoppinglists).to.have.length(0)
})
Runthetestsandrefreshthereportforactions.js.YouwillseeanEiconontheleftoftheifstatement:
TheEiconneartheifstatementindicatesthattheelsebranchwasnotcoveredbytests
Thisindicatesthatwehaven'tcoveredtheelsebranch.Ifyouskipthefirsttestandleavetheonewiththeemptyobject,youwillseetheIiconthatisindicatingthatwehaveskippedtheifbranch:
TheIiconneartheifstatementindicatesthattheifbranchwasnotcoveredbytests
Writetestsfortherestofthemutations.Performatleastthefollowingchecks:
FortheDELETE_SHOPPING_LISTmutation,checkthatthelistwiththeIDwepassisactuallydeletedifitexistedbeforeinthelist,andthatcallingthemutationwiththeIDthatdoesn'texistinthelistwillnotcauseanychangeForthePOPULATE_SHOPPING_LISTSmutation,checkthattheshoppinglistarrayisoverriddenwiththearraywepasswhencallthismutationFortheCHANGE_TITLEmutation,checkthatwhenwepassthenewtitleandtheID,exactlythisobject'stitleischanged
Intheend,yourmutation.spec.jsfilewillprobablylooklikethegistathttps://gist.github.com/chudaol/befd9fc5701ff72dff7fb68ef1c7f06a.
Afterthesetests,thecoverageofmutation.jslooksprettynice,actually:
100%coverageformutations.jsafterwritingunittestsforallthemutations
Intheexactsameway,wecantestourgetters.js.Createagetters.spec.jsfileandfillitwithteststotestourtwogettersfunctions.Intheend,itmightlooklikethegistathttps://gist.github.com/chudaol/e89dd0f77b1563366d5eec16bd6ae4a9.
Theonlyimportantstorecomponentthatismissinginunittestingisactions.js.Butouractions.jsusesextensivelytheAPIthat,inturn,performsHTTPrequests.Itsfunctionsarealsoasynchronous.Canthiskindofthingbeunittestedinthesameflexibleandeasywayaswejusttestedgettersandactions?Yes,itcan!Let'sseehowcanwefakeserverresponsesusingsinon.jsandhowcanwewriteasynchronoustestswithmocha.js.
FakingserverresponsesandwritingasynchronoustestsOpentheactions.jsfileandchecktheveryfirstactionmethod:
//actions.js
populateShoppingLists:({commit})=>{
api.fetchShoppingLists().then(response=>{
commit(POPULATE_SHOPPING_LISTS,response.data)
})
}
Firstofall,let'saddareturnstatementtothisfunctiontomakeitreturnapromise.Wedoittoenableustocallthe.thenmethodoncethepromiseresolvessothatwecantesteverythingthathappensinthemeantime.So,ourfunctionlookslikethefollowing:
//actions.js
populateShoppingLists:({commit})=>{
returnapi.fetchShoppingLists().then(response=>{
commit(POPULATE_SHOPPING_LISTS,response.data)
})
}
Now,checkwhatishappeninghere:
1. Thisfunctionreceivesstorewithitsdispatchmethod.2. ItperformsacalltotheAPI.TheAPI,inturn,callstheresourcegetmethodthatjust
performsanHTTPrequesttoourserver.3. AftertheAPI'sfetchShoppingListspromiseisresolved,ourmethodiscallingthe
store'scommitmethodwithtwoparameters:aPOPULATE_SHOPPING_LISTSstringandthedatathatcameinresponse.
Howcanweunittestthisworkflow?Ifwewereabletocatchtherequestandmocktheresponse,wecouldcheckifthecommitmethod(passedbyus,whichmeansthatitcanalsobemocked)iscalledwiththeresponsethatweprovideinourserver'smock.Soundsconfusing?Notatall!Thestepsarethefollowing:
1. Createamockforthestoreanditscommitmethod.2. Createamockforthehypotheticalserverresponse.3. CreateafakeserverthatwillintercepttheGETrequestandreturnthemockedresponse.4. Checkthecommitmethodiscalledwithourmockedresponseandthe
POPULATE_SHOPPING_LISTSstring.
Itmeansthatourtestcouldlooksomethinglikethefollowing:
it('shouldtestthatcommitiscalledwithcorrectparameters',()=>{
actions.populateShoppingLists({commit}).then(()=>{
expect(commit).to.have.been.calledWith(<...>)
})
})
Theproblemhereisthatourtestsaresynchronous,meaningthecodewillneverreachwhatisinsideour.thencallback.Luckilyforus,mocha.jsprovidessupportforasynchronoustesting.Checkitoutathttps://mochajs.org/#asynchronous-code.Theonlythingyouhavetodoistopassdonecallbacktoit()andcallitwhenthetestiscomplete.Inthisway,ourpseudo-codeforthistestwouldlookthefollowing:
it('shouldtestthatcommitiscalledwithcorrectparameters',
(done)=>{
actions.populateShoppingLists({commit}).then(()=>{
expect(commit).to.have.been.calledWith(<...>)
done()
})
})
Let'scodenow!Createatestspecandcallitactions.spec.js,andwritealltheneededbootstrappingcode:
//actions.spec.js
importactionsfrom'src/vuex/actions'
import{CHANGE_TITLE,POPULATE_SHOPPING_LISTS}from'src/vuex/mutation_types'
describe('actions.js',()=>{
describe('populateShoppingLists',()=>{
//herewewilladdourtestcase
})
})
Nowlet'sfollowoursteps.Firstofall,let'smocktheserverresponse.JustcreatethelistsvariableandinitializeitinthebeforeEachmethod:
//actions.spec.js
describe('actions.js',()=>{
varlists
beforeEach(()=>{
//mockshoppinglists
lists=[{
id:'1',
title:'Groceries'
},{
id:'2',
title:'Clothes'
}]
})
describe('populateShoppingLists',()=>{
})
})
Now,let'smockthestore'scommitmethod:
//actions.spec.js
describe('actions.js',()=>{
varlists,store
beforeEach(()=>{
<...>
//mockstorecommitmethod
store={
commit:(method,data)=>{},
state:{
shoppinglists:lists
}
}
})
<...>
})
Now,wehavetospyonthiscommitmethodinordertobeabletoassertthatitwascalledwiththerequiredparameters.Wewillusethesinon.stubmethodforthis.Checkthedocumentationonsinon.jsonthismatterathttp://sinonjs.org/docs/#stubs.Creatingastubonagivenfunctionisveryeasy.Justcallthesinon.stubmethodandpasstoittheobjectanditsmethodthatwewanttospyon:
sinon.stub(store,'commit')
So,ourbeforeEachfunctionwilllooklikethefollowing:
beforeEach(()=>{
<...>
//mockstorecommitmethod
store={
commit:(method,data)=>{},
state:{
shoppinglists:lists
}
}
sinon.stub(store,'commit')
})
It'sveryimportantthataftereachmethod,werestorethestubsothateachtestingmethodrunsinacleanenvironmentthatisnotaffectedbyothertests.Forthis,createanafterEachmethodandaddthefollowingline:
afterEach(function(){
//restorestub
store.commit.restore()
})
Nowtheonlythingweneedtodoisfakeourserverresponsewithourmockeddata.Let'suseSinon'sfakeServerforthispurpose.Checksinon'sdocumentationathttp://sinonjs.org/docs/#fakeServer.WejustneedtocreatefakeServerandtellittorespondwithourmockedresponsetotheGETrequest:
describe('actions.js',()=>{
varlists,store,server
beforeEach(()=>{
<...>
//mockserver
server=sinon.fakeServer.create()
server.respondWith('GET',/shoppinglists/,xhr=>{
xhr.respond(200,{'Content-Type':'application/json'},
JSON.stringify(lists))
})
})
<...>
})
Afterthesepreparations,eachtestthatwillsomehowperformarequestshouldcalltheserver'srespondmethodinordertoinvoketheserver'sfunctionality.
However,wecansimplifythisbyjusttellingtheservertoauto-respondeachcaughtrequest:
server.autoRespond=true
So,ourcodeformockingtheserverwilllooklikethefollowing:
beforeEach(()=>{
<...>
//mockserver
server=sinon.fakeServer.create()
server.respondWith('GET',/shoppinglists/,xhr=>{
xhr.respond(200,{'Content-Type':'application/json'},
JSON.stringify(lists)
})
server.autoRespond=true
})
Itisveryimportantthataftereachtest,werestoreourfakeserversothatnoothertestisaffectedbyourmocksinthistest.SoaddthefollowinglinetotheafterEachmethod:
afterEach(()=>{
//restorestubsandservermock
store.commit.restore()
server.restore()
})
Nowthatwehavemockedeverythingthatitwaspossibletomock,wecanfinallywriteourtestcase!So,youremember,wecreateanit()statementwithdonecallback,callourpopulateShoppingListsmethod,andcheckthattheresolvedresponseisthesameasourmockedlistobject.Stepintothedescribemethodandjusttranslateintothecodewhatwe'vejustdescribed:
it('shouldcallcommitmethodwithPOPULATE_SHOPPING_LISTandwithmocked
lists',done=>{
actions.populateShoppingLists(store).then(()=>{
expect(store.commit).to.have.been.calledWith(POPULATE_SHOPPING_LISTS,
lists)
done()
}).catch(done)
})
Ourwholetestspecnowlookslikethegistathttps://gist.github.com/chudaol/addb6657095406234bc6f659970f3eb8.
Runthetestswithnpmrununit.Itworks!
Nowwejusthavetomocktheserver'sresponsesforthePUT,POST,andDELETEmethods.Thesemethodsdonotreturnanydata;however,inordertobeabletotesttheresponses,let'sreturnfakedsuccessmessages,andineachtest,checkthatthereturneddatacorrespondstotheseresponses.Addthefollowingvariablesontopofthespec:
varserver,store,lists,successPut,successPost,successDelete
successDelete={'delete':true}
successPost={'post':true}
successPut={'put':true}
Andaddthefollowingfakeresponsesmethodstoourserver:
server.respondWith('POST',/shoppinglists/,xhr=>{
xhr.respond(200,{'Content-Type':'application/json'},
JSON.stringify(successPost))
})
server.respondWith('PUT',/shoppinglists/,xhr=>{
xhr.respond(200,{'Content-Type':'application/json'},
JSON.stringify(successPut))
})
server.respondWith('DELETE',/shoppinglists/,xhr=>{
xhr.respond(200,{'Content-Type':'application/json'},
JSON.stringify(successDelete))
})
Let'sseehowit'llwork,forexample,forthechangeTitlemethod.Inthistest,wewanttotestthatthecommitmethodwillbecalledwiththegivenIDandtitle.Ourtest,thereforewilllooklikethefollowing:
describe('changeTitle',()=>{
it('shouldcallcommitmethodwithCHANGE_TITLEstring',(done)=>{
lettitle='newtitle'
actions.changeTitle(store,{title:title,id:'1'}).then(()=>{
expect(store.commit).to.have.been.calledWith(CHANGE_TITLE,
{title:title,id:'1'})
done()
}).catch(done)
})
})
Forthistoworkproperly,weshouldalsomockthestore'sdispatchmethodsinceit'sbeing
usedinsidethechangeTitleaction.Justaddthedispatchpropertytoourstore'smockandreturnaresolvedpromise:
//mockstorecommitanddispatchmethods
store={
commit:(method,data)=>{},
dispatch:()=>{
returnPromise.resolve()
},
state:{
shoppinglists:lists
}
}
Checkthefinalcodeforunittestsatthismomentathttps://gist.github.com/chudaol/1405dff6a46b84c284b0eae731974050.
Finishthetestingforactions.jsbyaddingunittestsfortheupdateList,createShoppingList,anddeleteShoppingListmethods.Checkthewholecodeforunittestsuntilnowinthechapter7/shopping-list2folder.
TestingcomponentsNowthatallourVuex-relatedfunctionsareunittested,itistimetoapplyspecificVuecomponentstestingtechniquestotestcomponentsofourshoppinglistapplication.
YourememberfromthefirstsectionofthischapterthatinordertopreparetheVueinstancetobeunittested,wemustimport,initiate(passingittonewVueinstance),andmountit.Let'sdoit!Createacomponentsfolderinsidethetest/unit/specsdirectory.Let'sstartbytestingtheAddItemComponentcomponent.CreateanAddItemComponent.spec.jsfileandimportVueandAddItemComponent:
//AddItemComponent.spec.js
importVuefrom'vue'
importAddItemComponentfrom'src/components/AddItemComponent'
describe('AddItemComponent.vue',()=>{
})
ThevariableAddItemComponentcanbeusedtoaccessdirectlyallthecomponent'sinitialdata.Sowecanassert,forexample,thatthecomponentdataisinitializedwithanewItempropertythatequalstoemptystring:
describe('initialization',()=>{
it('shouldinitializethecomponentwithemptystringnewItem',()=>{
expect(AddItemComponent.data()).to.eql({
newItem:''
})
})
})
Let'snowcheckwhichmethodsofthiscomponentwecancoverwithunittests.
Thiscomponenthasonlyonemethod,whichisaddItemmethod.Let'scheckwhatthismethoddoes:
//AddItemComponent.vue
addItem(){
vartext
text=this.newItem.trim()
if(text){
this.$emit('add',this.newItem)
this.newItem=''
this.$store.dispatch('updateList',this.id)
}
}
Thismethodaccesstothestore,so,wehavetouseanotherstrategyofinitializingthecomponentratherthanjustdirectlyusingtheimportedvalue.Inthiscase,weshouldinitializeVuemaincomponentwithAddItemComponentasachild,passallthenecessaryattributestoit,
andaccessitusingthe$refsattribute.So,thecomponent'sinitializationinsidethetestmethodwilllooklikethefollowing:
varvm,addItemComponent;
vm=newVue({
template:'<add-item-component:items="items":id="id"
ref="additemcomponent">'+
'</add-item-component>',
components:{
AddItemComponent
},
data(){
return{
items:[],
id:'niceId'
}
},
store
}).$mount();
addItemComponent=vm.$refs.additemcomponent
Backtothemethod'sfunctionality.So,theaddItemmethodgrabstheinstance'snewItemproperty,trimsit,checksifit'snotfalsyand,ifnot,emitsthecustomeventadd,resetsthenewItemproperty,anddispatchestheupdateListactiononstore.Wecantestthismethodbyassigningdifferentvaluescomponent.newItem,component.idandcheckingiftheoutputcorrespondstowhatweareexpectingofit.
Tip
Positivetestingmeanstestingasystembygivingitvaliddata.Negativetestingmeanstestingasystembygivingitinvaliddata.
Inourpositivetest,weshouldinitializethecomponent.newItempropertywithavalidstring.Aftercallingthemethod,weshouldensurevariousthings:
The$emitmethodofthecomponenthasbeencalledwithaddandthetextweassignedtothenewItempropertycomponent.newItemwasresettotheemptystringThestore'sdispatchmethodhasbeencalledwiththeidpropertyofthecomponent
Let'sgo!Let'sstartbyaddingthedescribemethodfortheaddItemfunction:
describe('addItem',()=>{
})
Nowwecanaddtheit()methodwherewewillassignavaluetocomponent.newItem,calltheaddItemmethod,andcheckeverythingweneedtocheck:
//AddItemComponent.spec.js
it('shouldcall$emitmethod',()=>{
letnewItem='LearningVueJS'
//stub$emitmethod
sinon.stub(component,'$emit')
//stubstore'sdispatchmethod
sinon.stub(store,'dispatch')
//setanewitem
component.newItem=newItem
component.addItem()
//newItemshouldbereset
expect(component.newItem).to.eql('')
//$emitshouldbecalledwithcustomevent'add'andanewItemvalue
expect(component.$emit).to.have.been.calledWith('add',newItem)
//dispatchshouldbecalledwithupdateListandtheidofthelist
expect(store.dispatch).to.have.been.calledWith('updateList',
'niceId')
store.dispatch.restore()
component.$emit.restore()
})
Runthetestsandcheckthattheyarepassingandeverythingisokay.CheckthefinalcodeforAddItemComponentinthechapter7/shopping-list3folder.
Trytowriteunittestsfortherestofthecomponentsoftheshoppinglistapplication.Remembertowriteunitteststocoveryourcodesothatitbreaksifyouchangeit.
WritingunittestsforourPomodoroapplicationOk!Let'smovetoourPomodoroapplication!Bytheway,whenwasthelasttimeyoutookabreak?Probably,itistimetoopentheapplicationinyourbrowser,waitafewminutesofthePomodoroworkingperiodtimer,andcheckforsomekittens.
Ijustdiditanditmademefeelreallyniceandcute:
I'mnotyourclothes...pleasehavesomerest
Let'sstartwithmutations.Openthecodeinthechapter7/pomodorofolder.Openthemutations.jsfileandcheckwhatishappeningoutthere.Therearefourmutationshappening:START,STOP,PAUSE,andTOGGLE_SOUND.Guesswhichonewewillstartwith.Yes,youareright,wewillstartwiththestartmethod.Createavuexsubfolderinsidethetest/unit/specs
folderandaddthemutations.spec.jsfile.Let'sbootstrapittobereadyfortests:
//mutations.spec.js
importVuefrom'vue'
importmutationsfrom'src/vuex/mutations'
import*astypesfrom'src/vuex/mutation_types'
describe('mutations',()=>{
varstate
beforeEach(()=>{
state={}
//let'smockVuenoiseplugin
//tobeabletolistenonitsmethods
Vue.noise={
start:()=>{},
stop:()=>{},
pause:()=>{}
}
sinon.spy(Vue.noise,'start')
sinon.spy(Vue.noise,'pause')
sinon.spy(Vue.noise,'stop')
})
afterEach(()=>{
Vue.noise.start.restore()
Vue.noise.pause.restore()
Vue.noise.stop.restore()
})
describe('START',()=>{
})
})
NotethatImockedallthemethodsofthenoisegeneratorplugin.Thisisbecauseinthisspec,wedon'tneedtotesttheplugin'sfunctionality(infact,wemustdoitinthescopeofthepluginitselfbeforepublishingit).Forthescopeofthistest,weshouldtestthattheplugin'smethodsarecalledwhentheyneedtobecalled.
Inordertobeabletotestthestartmethod,let'sthinkwhatshouldhappen.Afterthestartbuttonisclicked,weknowthattheapplication'sstarted,paused,andstoppedstatesmustgainsomespecificvalues(actually,true,false,andfalse,respectively).Wealsoknowtheapplication'sintervalshouldbestarted.WealsoknowthatifthePomodoro'sstateisworkingandifthesoundisenabled,thestartmethodofthenoisegeneratorpluginshouldbecalled.Infact,thisiswhatourmethodisactuallydoing:
[types.START](state){
state.started=true
state.paused=false
state.stopped=false
state.interval=setInterval(()=>tick(state),1000)
if(state.isWorking&&state.soundEnabled){
Vue.noise.start()
}
},
Butevenifitdidn'tdoallthesethingsandwehavewrittenthetesttotestit,wewouldimmediatelyunderstandthatsomethingismissinginourcodeandfixit.Let'sthenwriteourtest.Let'sstartbydefiningtheit()methodthatteststhatallthepropertieswerecorrectlyset.Inordertobesurethattheyarenotalreadysetbeforecallingthemethod,let'salsoassertthatallthesepropertiesarenotdefinedatthestartofthetest:
it('shouldsetallthestatepropertiescorrectlyafterstart',()=>{
//ensurethatallthepropertiesareundefined
//beforecallingthestartmethod
expect(state.started).to.be.undefined
expect(state.stopped).to.be.undefined
expect(state.paused).to.be.undefined
expect(state.interval).to.be.undefined
//callthestartmethod
mutations[types.START](state)
//checkthatallthepropertieswerecorrectlyset
expect(state.started).to.be.true
expect(state.paused).to.be.false
expect(state.stopped).to.be.false
expect(state.interval).not.to.be.undefined
})
Let'snowcheckontheVue.noise.startmethod.Weknowthatitshouldonlybecalledifstate.isWorkingistrueandstate.soundEnabledistrue.Let'swriteapositivetest.Inthistest,wewouldinitializebothBooleanstatestotrueandcheckthatthenoise.startmethodiscalled:
it('shouldcallVue.noise.startmethodifbothstate.isWorkingand
state.soundEnabledaretrue',()=>{
state.isWorking=true
state.soundEnabled=true
mutations[types.START](state)
expect(Vue.noise.start).to.have.been.called
})
Let'saddtwonegativetestsforeachofthestates,withisWorkingandsoundEnabledbeingfalse:
it('shouldnotcallVue.noise.startmethodifstate.isWorkingisnottrue',()
=>{
state.isWorking=false
state.soundEnabled=true
mutations[types.START](state)
expect(Vue.noise.start).to.not.have.been.called
})
it('shouldnotcallVue.noise.startmethodifstate.soundEnabledisnottrue',()
=>{
state.isWorking=true
state.soundEnabled=false
mutations[types.START](state)
expect(Vue.noise.start).to.not.have.been.called
})
Ourstartmutationisnicelytested!Checkthefinalstateofthecodeinthechapter7/pomodoro2folder.Isuggestthatyounowwritetherestoftheunittestsnotonlyforthemutations,butalsoforallthestore-relatedfunctionsthatresideingettersandactions.Afterthat,applythetechniquestotestVuecomponentsthatwejustlearnedandtestsomeofthecomponentsofourPomodoroapplication.
Atthispoint,wearedonewithunittesting!
Whatisend-to-endtesting?End-to-end(e2e)testingisatechniqueinwhichthewholeflowoftheapplicationisbeingtested.Inthiskindoftesting,neithermocksnorstubsareused,andtherealsystemisbeingunderthetest.Performinge2etestingallowsustotestalltheaspectsoftheapplication—APIs,frontend,backend,databases,serverload,assuringthusthequalityofthesystemintegration.
Inthecaseofwebapplications,thesetestsareperformedviaUItesting.Eachtestdescribesallthestepsfromopeningthebrowseruntilclosingit.Allthestepsneededtoperforminordertoachievesomesystem'sfunctionalitymustbedescribed.Infact,thisisthesameasyouclickinganddoingsomeoperationsonyourapplication'spage,butisautomatedandfast.Inthissection,wewillseewhataSeleniumwebdriveris,andwhatNightwatchis,andhowtheycanbeusedtocreatee2etestsforourapplications.
Nightwatchfore2eIfyouhavealreadyworkedwithtestautomationorifyouhaveworkedwithsomeonewhohasworkedwithtestautomation,forsure,youhavealreadyheardthemagicwordSelenium—Seleniumopensthebrowser,clicks,writes,doeseverythinglikeahuman,inaparallel,nicelydistributed,multiplatform,andcross-browserway.Infact,SeleniumisjustaJARfilethatcontainsanAPItoperformdifferentoperationsonabrowser(click,type,scroll,andsoon).
Note
CheckoutSelenium'sdocumentationathttp://www.seleniumhq.org/.
WhenthisJARfileisexecuted,itconnectstothespecifiedbrowser,openstheAPI,andwaitsforthecommandstobeperformedonthebrowser.ThecommandssenttotheSeleniumservercanbeperformedintonsofdifferentwaysandlanguages.
Therearealotofexistingimplementationsandframeworksthatallowyoutocallseleniumcommandswithcouplelinesofcode:
YoucanusethenativeSelenium'sframeworkforJava(http://seleniumhq.github.io/selenium/docs/api/java/)YoucanusetheFirefoxpluginforbrowsers(https://addons.mozilla.org/en-us/firefox/addon/selenium-ide/)YoucanuseSelenide,whichisyetanotherimplementationforJavabutaloteasiertousethanSelenium'sframework(http://selenide.org/)IfyouareanAngularJSdeveloper,youcanuseProtractor,whichisaverynicee2etestframeworkforAngularJSapplicationsthatalsousestheSeleniumwebdriver(http://www.protractortest.org/)
Inourcase,wewilluseNightwatch,whichisaniceandveryeasy-to-usetestingframeworktocallSelenium'scommandsusingJavaScript.
CheckNightwatch'sdocumentationathttp://nightwatchjs.org/.
Vueapplications,whenbootstrappedusingthevue-cliwebpackmethod,alreadycontainssupportforwritingNightwatchtestsrightawaywithouttheneedtoinstallanything.Basically,eachtestspecwilllooksomewhatlikethefollowing:
module.exports={
'e2etest':function(browser){
browser
.url('http://localhost:8080')
.waitForElementVisible('#app',5000)
.assert.elementPresent('.logo')
.assert.containsText('h1','HelloWorld!')
.assert.elementCount('p',3)
.end()
}
}
Thesyntaxisniceandeasytounderstand.EachofthehighlightedmethodsisaNightwatchcommandthatbehindthescenesistransformedintotheSeleniumcommandandinvokedassuch.CheckthefulllistoftheNightwatchcommandsintheofficialdocumentationpageathttp://nightwatchjs.org/api#commands.
Writinge2etestsforthePomodoroapplicationSo,nowthatweknowallthetheorybehindtheUItesting,wecancreateourfirstend-to-endtestforourPomodoroapplication.Let'sdefinethestepsthatwewillperformandthethingsthatweshouldtest.So,firstofall,weshouldopenthebrowser.Then,weshouldprobablycheckthatourcontainer(thathasthe#appID)isonthepage.
Wecanalsotrytocheckthatthepauseandstopbuttonsaredisabledandthatthesoundtogglebuttondoesnotexistonthepage.
Thenwecanclickonthestartbuttonandcheckthatthesoundtogglebuttonhasappeared,thestartbuttonhasbecomedisabled,andthepauseandstopbuttonshavebecomeenabled.Thereisaninnumerousnumberofpossibilitiesoffurtherclickingandchecking,butlet'sperformatleastthedescribedsteps.Let'sjustwritethemintheformofbulletpoints:
1. Openthebrowserathttp://localhost:8080.2. Checkthatthe#appelementisonthepage.3. Checkthatthe.toggle-volumeiconisnotvisible.4. Checkthatthe'[title=pause]'and'[title=stop]'buttonsaredisabledandthe
'[title=start]'buttonisenabled.5. Clickonthe'[title=start]'button.6. Checkthatthe'[title=pause]'and'[title=stop]'buttonsarenowenabledand
the'[title=start]'buttonisdisabled.7. Checkthatthe.toggle-volumeiconisnowvisible.
Let'sdoit!Justopenthetest.jsfileinsidethetests/e2e/specsfolder,deleteitscontent,andaddthefollowingcode:
module.exports={
'defaulte2etests':(browser)=>{
//openthebrowserandcheckthat#appisonthepage
browser.url('http://localhost:8080')
.waitForElementVisible('#app',5000);
//checkthattoggle-volumeiconisnotvisible
browser.expect.element('.toggle-volume')
.to.not.be.visible
//checkthatpausebuttonisdisabled
browser.expect.element('[title=pause]')
.to.have.attribute('disabled')
//checkthatstopbuttonisdisabled
browser.expect.element('[title=stop]')
.to.have.attribute('disabled')
//checkthatstartbuttonisnotdisabled
browser.expect.element('[title=start]')
.to.not.have.attribute('disabled')
//clickonstartbutton,checkthattogglevolume
//buttonisvisible
browser.click('[title=start]')
.waitForElementVisible('.toggle-volume',5000)
//checkthatpausebuttonisnotdisabled
browser.expect.element('[title=pause]')
.to.not.have.attribute('disabled')
//checkthatstopbuttonisnotdisabled
browser.expect.element('[title=stop]')
.to.not.have.attribute('disabled')
//checkthatstopbuttonisdisabled
browser.expect.element('[title=start]')
.to.have.attribute('disabled')
browser.end()
}
}
Doyouseehowsuperhuman-friendlythislanguageis?Let'snowperformachecktoseewhether,aftertheperiodofworkingtime,thekittenelementappearsonthescreen.Inordertomakethetestshorterandnotwaitforalongtimefortesttopass,let'sestablishtheworkingperiodas6seconds.Changethisvalueinourconfig.jsfile:
//config.js
exportconstWORKING_TIME=0.1*60
Theelementthatcontainsthecatimageshasa'div.well.kittens'selector,sowewillcheckwhetheritisvisible.Let'salsocheckinthistestthatafterthekittenelementappears,thesourceoftheimagecontainsthe'thecatapi'string.Thistestwillbeassimpleasthefollowing:
'waitforkittentest':(browser)=>{
browser.url('http://localhost:8080')
.waitForElementVisible('#app',5000)
//initiallythekittenelementisnotvisible
browser.expect.element('.well.kittens')
.to.not.be.visible
//clickonthestartbuttonandwaitfor7sfor
//kittenelementtoappear
browser.click('[title=start]')
.waitForElementVisible('.well.kittens',7000)
//checkthattheimagecontainsthesrcelement
//thatmatchesthecatapistring
browser.expect.element('.well.kittensimg')
.to.have.attribute('src')
.which.matches(/thecatapi/);
browser.end()
}
Runthetests.Inordertodothat,invokethee2enpmcommand:
npmrune2e
Youwillseehowthebrowseropensandperformsalltheoperationsbyitself.
It'sakindofmagic!
Allourtestshavepassedandallexpectationsarefulfilled;checkouttheconsole:
Alltestsarepassing!
Congratulations!You'vejustlearnedhowtouseNightwatchtowritee2etests.Checkthecodeinthechapter7/pomodoro3folder.WritemoretestcasesforourPomodoroapplication.Donotforgetaboutourshoppinglistapplication,whichmighthaveevenmorescenariosforUItests.WritethemallandcheckhowSeleniumdoestheworkforyou.Ifyoudecidetoenhancethecode,notonlyisyourcodequalityprotectedbyunittests,butitalsonowhasregressiontestingappliedtoit.Eachtimeyouchangethecode,runbothtypesoftestsjustwithonecommand:
npmtest
Nowyoucertainlydeservesomerest.Takeacupofcoffeeortea,openyourbrowseronthePomodoroapplicationpage,waitfor6seconds,andappreciateourlittlefluffyfriends:
Actually,thisisnotakittenfromthecatapi.ThisismycatPatuscaswishingyoualltohaveagoodresttime!
SummaryInthischapter,we'vetestedbothofourapplications.WehavewrittenunittestsforVuexmethodsandVuecomponents.WehaveusedsimpleunittestsandasynchronousunittestsandwegotfamiliarwithSinonmockingtechniquessuchasspyingonmethodsandfakingserverresponses.WealsolearnedhowtocreateUItestsusingNightwatch.Ourapplicationsarenowtestedandpreparedtobedeployedtoproduction!Wewilldiscoverhowtodeploytheminthenextchapter,whichwillbedevotedtodeployingapplicationsusingtheHerokucloudapplicationplatform.
Chapter8.Deploying–TimetoGoLive!Inthepreviouschapter,youlearnedhowtotestyourVueapplications.Wetestedthemapplyingdifferenttestingtechniques.Inthebeginning,wehaveperformedclassicunittestingonVuecomponentsandonVuex-relatedmodules,suchasactions,mutations,andgetters.Afterthat,welearnedhowtoapplyend-to-endtestingtechniquesusingNightwatch.
Inthischapter,wewillmakeourapplicationsgolivebydeployingthemtoaserverandmakingthemavailabletotheworld.Wewillalsoguaranteecontinuousintegrationandcontinuousdeploymentofourapplications.Thismeansthateverytimewecommitchangesperformedontheapplications,theywillautomaticallybetestedanddeployed.
Withthisinmind,inthischapter,wearegoingtodothefollowing:
SetupacontinuousintegrationprocessusingTravisSetupacontinuousdeploymentusingHeroku
SoftwaredeploymentBeforestartingtodeployourapplications,let'sfirsttrytodefinewhatitactuallymeans:
"Softwaredeploymentisalloftheactivitiesthatmakeasoftwaresystemavailableforuse."–Wikipedia:https://en.wikipedia.org/wiki/Software_deployment
Thisdefinitionmeansthatafterweperformallthenecessaryactivities,oursoftwarewillbeaccessibletothepublic.Inourcase,aswearedeployingwebapplications,itmeansthattherewillbeapublicURL,andanypersonwillbeabletotypethisURLontheirbrowserandaccesstheapplication.Howcanthisbeachieved?ThesimplestwayistoprovideyourownIPaddresstoyourfriendsandruntheapplication.Thus,peopleinsideyourprivatenetworkwillbeabletoaccesstheapplicationontheirbrowser.So,run,forexample,thePomodoroapplication:
>cd<pathtopomodoro>
>npmrundev
AndthencheckyourIP:
ifconfig
CheckingtheIPaddresswiththeifconfigcommand
Andthensharetheaddresswithyourfriendsonthesameprivatenetwork.Inmycase,itwouldbehttp://192.168.1.6:8080.
However,onlyyourfriendswhoareinsideyournetworkwillbeabletoaccesstheapplication,andthere'sobviouslynotthatmuchfuninit.
Youcanusesomesoftwarethatwillcreateapubliclyaccessibleaddressandthustransformyourcomputerintoahostingprovider,forexample,ngrok(https://ngrok.com/).Runtheapplicationandthenrunthefollowingcommand:
ngrokhttp8080
Thiswillcreateanaddressthatwillbeaccessiblefromanywhere,justlikearegularwebsite:
Usingngroktoprovideatunneltoyourlocalhost
Inmycase,itwouldbehttp://5dcb8d46.ngrok.io.IcansharethisaddressonmysocialnetworksandeverybodywillbeabletoaccessitandtrythePomodoroapplication!Butstop...Icanleavemylaptoponforthewholenight,butIcan'tleaveitonforever.OnceIswitchitoff,thenetworkconnectionislostandthereisnoaccesstomyapplicationanymore.Also,evenifIcouldleaveitonforever,Idon'tlikethiswebsiteaddress.It'sabunchoflettersandnumbers,andIwantittobesomethingmeaningful.
Therearemorerobustways.Icanbuy,forexample,avirtualinstanceonAWS(AmazonWebServices),copymyapplicationtothisinstance,buyadomainatadomainprovidersuchasGoDaddy,associatethisdomaintotheboughtinstance'sIP,andruntheapplicationthereanditwillbeaccessible,maintained,backedup,andtakencareofbytheAmaz(on)ingservice.Amazing,but...expensiveashell.Let'sthinkofthissolutionwhenourapplicationsreachthecorrespondingsizeandpaybacklevel.
Fornow,forthischapter,wewantourdeploymentsolutiontobecheap(wherecheapmeans
free),robust,andsimple.ThatiswhywewilldeployourapplicationtoHeroku,acloud-applicationplatform.Inordertodothat,wewillfirsthostourapplicationonGitHub.Doyourememberthatdeploymentissomethingthatmakesourapplicationsreadytouse?Iconsideranapplicationtobereadytousewhenit'stestedandwhentestsarenotfailing.ThatiswhywewillalsouseTravistoguaranteethequalityofourapplicationsbeforetheiractualdeployment.So,ournecessaryactivitiestodeploytheapplicationwillbethefollowing:
1. CreateGitHubrepositoriesfortheapplicationsandmovetheapplicationsintotherepositories.
2. SetupcontinuousintegrationwithTravis.3. ConnectapplicationstoHeroku,andsetupandconfiguretheminorderforHerokuto
runthemandtoexposethemtotheworld.
Inthenextthreesubsections,IwillgiveasmallintroductiontoGitHub,Travis,andHeroku.
WhatisGitHub?GitHubisahostingproviderforGit-basedprojects.
Itcanbeusedatasmall,personalscaleforindividualprivateandpublicprojects.Itcanalsobeusedforbigcorporateprojectsandalldevelopment-relatedactivities,suchascodereviews,continuousintegration,andsoon.
EveryonewholivesintheworldofopensourcesoftwareknowsGitHub.IfyouarereadingthisbookaboutVue,whichishostedonGitHub(https://github.com/vuejs/),Iamsurethatyouareskippingthissubsection,soprobablyIcanwritesomestupidjokesaboutyouhereandyouwillnevernoticethem!Justkidding!
WhatisTravis?TravisisatoolforGitHubthatallowsustoconnectGitHubprojectstoitandensuretheirquality.Itrunstestsinyourprojectsandtellsyouthatbuildhaspassed,orwarnsyouthatbuildhasfailed.CheckmoreaboutTravisandhowtouseitathttps://travis-ci.org/.
WhatisHeroku?Herokuisacloudplatformfordeployingyourapps.Itisextremelyeasytouse.Youjustcreateanapplication,giveitanicemeaningfulname,connectittoyourGitHubproject,andvoilà!Eachtimeyoupushtoagivenbranch(forexample,tothemasterbranch),Herokuwilljustrunascriptprovidedbyyouasanentrypointscriptofyourappandredeployit.
Itishighlyconfigurableandalsoprovidesacommand-lineinterfacesothatyoucanaccessallyourapplicationsfromyourlocalcommandlinewithouthavingtocheckyourHerokudashboardwebsite.Let'sthenstartandlearneverythingbydoingitourselves.
MovingtheapplicationtotheGitHubrepositoryLet'sstartbycreatingtheGitHubrepositoriesforourapplications.
Pleaseusethecodefromthechapter8/pomodoroandchapter8/shopping-listdirectories.
Ifyoustilldon'thaveanaccountatGitHub,createit.NowlogintoyourGitHubaccountandcreatetworepositories,PomodoroandShoppingList:
CreatearepositoryatGitHub
OnceyouhittheCreaterepositorybutton,apagewithdifferentinstructionsappears.We
areparticularlyinterestedinthesecondparagraph,whichsays...orcreateanewrepositoryonthecommandline.Copyit,pasteittothecommandlinewhileinthePomodoroapplicationdirectory,removethefirstline(becausewealreadyhavetheREADMEfile)andmodifythethirdlinetoaddeverythinginsidethedirectory,andhittheEnterbutton:
gitinit
gitadd
gitcommit-m"firstcommit"
gitremoteaddoriginhttps://github.com/chudaol/Pomodoro.git
gitpush-uoriginmaster
RefreshyourGitHubprojectpage,andyouwillseethatallthecodeisthere!Inmycase,itisathttps://github.com/chudaol/Pomodoro.
Dothesamefortheshoppinglistapplication.IjustdiditandhereIam:https://github.com/chudaol/ShoppingList.
Ifyoudon'twanttocreateyourownrepositories,youcanjustforkmine.Opensourceisopen!
SettingcontinuousintegrationwithTravisInordertobeabletosetupcontinuousintegrationwithTravis,firstofallyouhavetoconnectyourTravisaccountwithyourGitHubaccount.Openhttps://travis-ci.org/andclickontheSigninwithGitHubbutton:
ClickontheSigninwithGitHubbutton
NowyoucanaddrepositoriesthatwillbetrackedwithTravis.Clickontheplussign(+):
ClickontheplussigntoaddyourGitHubproject
Afteryouclickontheplusbutton,thewholelistofyourGitHubprojectappears.Choosetheprojectsyouwanttotrack:
ChoosetheprojectsyouwanttotrackwithTravis
NowthatwehaveourprojectsconnectedtotheTravisbuildsystemthatlistenstoeverycommitandpushtothemasterbranch,weneedtotellitsomehowwhatithastodoonceitdetectschanges.AlltheconfigurationforTravisshouldbestoredinthe.travis.ymlfile.Addthe.travis.ymlfiletoboththeprojects.Wehaveatleasttotellwhichnodeversionshouldbeused.ChecktheNodeversionofyoursystem(thisistheonethatyouarecompletelysurethatworkswithourprojects).Justrunthefollowingcommand:
node--version
Inmycase,itisv5.11.0.SoIwilladdittothe.travis.ymlfile:
//.travis.yml
language:node_js
node_js:
-"5.11.0"
Ifyoucommitandpushnow,youwillseethatTravisautomaticallystartsrunningtests.Bydefault,itcallsthenpmtestcommandontheproject.Waitforafewminutesandobservetheresult.Unfortunately,itwillfailwhileperformingend-to-end(Selenium)tests.Whydoesthis
happen?
Bydefault,virtualimagesoftheTravisbuildingandtestingenvironmentdonothavetheChromebrowserinstalled.AndourSeleniumtestsaretryingtorunontheChromebrowser.Butfortunatelyforus,Travisprovidesamechanismofperformingsomecommandsbeforebuilding.Itshouldbedoneinthebefore_scriptsectionoftheYMLfile.Let'sinvokethenecessarycommandstoinstallChromeandexporttheCHROME_BINvariable.Addthefollowingtoyour.travis.ymlfiles:
before_script:
-exportCHROME_BIN=/usr/bin/google-chrome
-sudoapt-getupdate
-sudoapt-getinstall-ylibappindicator1fonts-liberation
-wgethttps://dl.google.com/linux/direct/google-chrome-
stable_current_amd64.deb
-sudodpkg-igoogle-chrome*.deb
Asyoucansee,inordertoperformtheinstallationandsystemupdate,wemustinvokecommandswithsudo.Bydefault,Travisdoesnotletyouexecutesudocommandsinordertopreventaccidentaldamagebynon-trustworthyscripts.ButyoucantellTravisexplicitlythatyourscriptusessudo,whichmeansthatyouareawareofwhatareyoudoing.Justaddthefollowinglinestoyour.travis.ymlfiles:
sudo:required
dist:trusty
Nowyourwhole.travis.ymlfileshouldlooklikethefollowing:
//.travis.yml
language:node_js
sudo:required
dist:trusty
node_js:
-"5.11.0"
before_script:
-exportCHROME_BIN=/usr/bin/google-chrome
-sudoapt-getupdate
-sudoapt-getinstall-ylibappindicator1fonts-liberation
-wgethttps://dl.google.com/linux/direct/google-chrome-
stable_current_amd64.deb
-sudodpkg-igoogle-chrome*.deb
TrytocommititandcheckyourTravisdashboard.
Ohno!Itfailsagain.Thistime,itseemstobetimeoutissue:
EvenafterinstallingChrome,testssilentlyfailduetothetimeout
Whydidithappen?Let'srecallwhatactuallyhappenswhenwerunourend-to-endtests.Eachtestopensthebrowserandthenperformsclicks,inputs,andotherthingstotestourUI.ThekeywordofthelastsentenceisUI.IfweneedtotestaUI,weneedagraphicaluserinterface(GUI).Travisvirtualimagesdonothavegraphicaldisplays.Thus,thereisnowaythattheycanopenthebrowseranddisplayourUIsinit.Fortunatelyforus,thereisanicethingcalledXvfb-Xvirtualframebuffer.
Xvfbisadisplayserverthatimplementstheprotocolusedbythephysicaldisplays.Allneededgraphicaloperationsareperformedinmemory;thus,thereisnoneedofhavingphysicaldisplays.Therefore,wecanrunanXvfbserverthatwillprovideavirtualgraphicalenvironmenttoourtests.AndifyoucarefullyreadtheTravisdocumentation,youwillfindthatthisisexactlywhatitsuggestsasawayofrunningteststhatrequireGUI:https://docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI.Soopenthe.travis.ymlfilesandaddthefollowingtothebefore_scriptsection:
-exportDISPLAY=:99.0
-sh-e/etc/init.d/xvfbstart
ThewholeYMLfilenowlookslikethefollowing:
//.travis.yml
language:node_js
sudo:required
dist:trusty
node_js:
-"5.11.0"
before_script:
-exportCHROME_BIN=/usr/bin/google-chrome
-sudoapt-getupdate
-sudoapt-getinstall-ylibappindicator1fonts-liberation
-wgethttps://dl.google.com/linux/direct/google-chrome-
stable_current_amd64.deb
-sudodpkg-igoogle-chrome*.deb
-exportDISPLAY=:99.0
-sh-e/etc/init.d/xvfbstart
CommititandcheckyourTravisdashboard.ThePomodoroapplicationwasbuiltsuccessfully!
ThePomodoroapplicationbuiltwithsuccess!
However,theshoppinglistapplication'sbuildhasfailed.NotethatTravisevenchangesthetab'stitlecolorforeachofthebuildstates:
Travischangestheicononthetab'stitleaccordingtothebuildstate
Sowhatishappeningwiththeshoppinglistapplicationbuild?There'sastepintheend-to-endtestthatchecksfortheGroceriestitlebeingpresentonthepage.Thethingisthatthistitlecomesfromourbackendserverthatshouldberunwiththenpmrunservercommand.DoyourememberweimplementeditinChapter6,Plugins–BuildingYourHousewithYourOwnBricks,usingthevue-resourceplugin?Thismeansthatbeforebuildingtheapplication,weneedtotellTravistorunoursmallserver.Justaddthefollowinglinetothe.travis.ymlfileoftheshoppinglistapplication:
-nohupnpmrunserver&
CommityourchangesandcheckTravisdashboard.Thebuildpassed!Everythingisgreenandwearehappy(atleastIam,andIhopethatsuccessfulbuildmakesyouhappyaswell).Nowitwouldbeniceifwecouldtelltheworldthatourbuildsarepassing.WecandoitbyaddingtheTravisbuttontoourREADME.mdfiles.Thiswillallowustoimmediatelyseethebuildstatusontheproject'sGitHubpage.
ClickonthebuildpassingbuttonontheTravispageofyourapplication,checktheMarkdownoptionfromtheseconddrop-downlist,andcopythegeneratedtexttotheREADME.mdfile:
Clickonthebuildpassingbutton,selectoptionMarkdownfromtheseconddrop-down,andcopythetexttotheREADME.mdfile
LookhowniceitlooksintheREADMEfileontheGitHubpageofourproject:
TheTravisbuttonlooksreallyfancyintheREADMEfileoftheprojectonitsGitHubpage
Nowthatourapplicationsarebeingcheckedoneachcommitandthereforewehaveforsureguaranteedtheirquality,wecanfinallydeploythemtothepubliclyaccessibleplace.
Beforestartingtheprocessofdeployment,pleasecreateanaccountatHeroku(https://signup.heroku.com/dc)andinstallHerokuToolbelt(https://devcenter.heroku.com/articles/getting-started-with-nodejs#set-up).
Nowwearereadytodeployourprojects.
DeployingthePomodoroapplicationLet'sstartbyaddingnewapplicationtoourHerokuaccount.ClickontheCreateNewAppbuttonontheHerokudashboard.Youcancreateyourownnameorleavethenameinputfieldblank,andHerokuwillcreateanameforyou.IwillcallmyapplicationcatodorobecauseitisPomodorothathascats!
CreatinganewappwithHeroku
ClickontheCreateAppbuttonandyouwillproceedtothepage,choosingadeploymentpipelineforyourapplication.ChoosetheGitHubmethod,andthenfromtheproposeddrop-downofyourGitHubprojects,choosetheprojectthatwewanttodeploy:
ChoosetheGitHubmethodofdeploymentandselectthecorrespondingprojectfromyourGitHubprojects
AfterclickingontheConnectbutton,twothingsthatyoumightprobablywanttocheckaretheAutomaticdeploysfrommasterareenabledandWaitforCItopassbeforedeployoptions:
ChecktheWaitforCItopassbeforedeploycheckboxandclickontheEnableAutomaticDeploysbutton
EverythingisreadytoperformafirstdeploymentandyoucanevenclickontheDeployBranchbutton,andHerokuwilltrytoperformabuild,butthen,ifyoutrytoopentheapplicationinthebrowser,itwillnotwork.Ifyouwonderwhy,youshouldalwayslookattherunninglogwhileperformingsuchoperations.
CheckinglogsIhopeyouhavesuccessfullyinstalledtheHerokuCLI(orHerokutoolbelt),sonowyoucanrunherokucommandsinyourcommandline.Let'scheckthelogs.Runtheherokulogscommandinyourshell:
herokulogs--appcatodoro--tail
YouwillseeacontinuouslyrunninglogwhileHerokutriestoperformabuild.AndtheerrorisnpmERR!missingscript:start.Wedon'thaveastartscriptinourpackage.jsonfile.
Thisisentirelytrue.Inordertocreateastartscript,let'sfirsttrytounderstandhowtobuildandrunaVueapplicationforproduction.TheREADMEfiletellsusthatweneedtorunthenpmrunbuildcommand.Let'srunitlocallyandcheckwhathappens:
Theoutputofthenpmrunbuildcommand
Soweknowthattheresultofthebuildcommandgoestothedistfolder.Andwealsoknowthatwehavetoservetheindex.htmlfilefromthisfolderusinganHTTPserver.Wealsoknowthatwehavetocreateastartscriptinthescriptssectionofthepackage.jsonfile,soHerokuknowshowtorunourapplication.
PreparingtheapplicationtorunonHerokuWewereabletogatheralotofinformationbycheckingthelogfile.Let'salsosummarizehereHeroku'spipelineforrunningtheapplicationbeforeproceedingtothestepsfordeployingtheapplication.
So,Herokudoesthefollowing:
Runsthenpminstallscripttoinstallalltheneededdependencies(itchecksthedependenciesinthedependenciessectionofthepackage.jsonfile)Runsthenpmstartscriptfromthepackage.jsonandservestheresultofitontheknownwebaddress
So,giventhisinformationandtheinformationwegatheredfromthelogsandrunningthenpmbuildscript,weneedtodothefollowing:
TellHerokutoinstallalltheneededdependencies;forthat,weneedtomoveprojectdependenciesfromthedevDependenciessectiontothedependenciessectioninthepackage.jsonfilesothatHerokuinstallsthemallTellHerokutorunabuildscriptafterperformingnpminstall;forthat,weneedtocreateapostinstallscriptinthepackage.jsonfilewherewewillcallthenpmrunbuildcommand.Createaserver.jsfilethatservestheindex.htmlfilefromthedistfolderProvideawayforHerokutoruntheserver.jsscript;forthis,weneedtocreateastartscriptinthepackage.jsonfilethatrunstheserver.jsscript
Startbymovingallthedependencies,excepttheonesthathavetodowithtesting,fromthedevDependenciessectiontothedependenciessectionofourpackage.jsonfile:
"dependencies":{
"autoprefixer":"^6.4.0",
"babel-core":"^6.0.0",
"babel-eslint":"^7.0.0",
"babel-loader":"^6.0.0",
"babel-plugin-transform-runtime":"^6.0.0",
"babel-polyfill":"^6.16.0",
"babel-preset-es2015":"^6.0.0",
"babel-preset-stage-2":"^6.0.0",
"babel-register":"^6.0.0",
"chalk":"^1.1.3",
"connect-history-api-fallback":"^1.1.0",
"cross-spawn":"^4.0.2",
"css-loader":"^0.25.0",
"es6-promise":"^4.0.5",
"eslint":"^3.7.1",
"eslint-config-standard":"^6.1.0",
"eslint-friendly-formatter":"^2.0.5",
"eslint-loader":"^1.5.0",
"eslint-plugin-html":"^1.3.0",
"eslint-plugin-promise":"^2.0.1",
"eslint-plugin-standard":"^2.0.1",
"eventsource-polyfill":"^0.9.6",
"express":"^4.13.3",
"extract-text-webpack-plugin":"^1.0.1",
"file-loader":"^0.9.0",
"function-bind":"^1.0.2",
"html-webpack-plugin":"^2.8.1",
"http-proxy-middleware":"^0.17.2",
"inject-loader":"^2.0.1",
"isparta-loader":"^2.0.0",
"json-loader":"^0.5.4",
"lolex":"^1.4.0",
"opn":"^4.0.2",
"ora":"^0.3.0",
"semver":"^5.3.0",
"shelljs":"^0.7.4",
"url-loader":"^0.5.7",
"vue":"^2.0.1",
"vuex":"^2.0.0",
"vue-loader":"^9.4.0",
"vue-style-loader":"^1.0.0",
"webpack":"^1.13.2",
"webpack-dev-middleware":"^1.8.3",
"webpack-hot-middleware":"^2.12.2",
"webpack-merge":"^0.14.1"
},
"devDependencies":{
"chai":"^3.5.0",
"chromedriver":"^2.21.2",
"karma":"^1.3.0",
"karma-coverage":"^1.1.1",
"karma-mocha":"^1.2.0",
"karma-phantomjs-launcher":"^1.0.0",
"karma-sinon-chai":"^1.2.0",
"karma-sourcemap-loader":"^0.3.7",
"karma-spec-reporter":"0.0.26",
"karma-webpack":"^1.7.0",
"mocha":"^3.1.0",
"nightwatch":"^0.9.8",
"phantomjs-prebuilt":"^2.1.3",
"selenium-server":"2.53.1",
"sinon":"^1.17.3",
"sinon-chai":"^2.8.0"
}
Nowlet'screateapostinstallscriptinwhichwewilltellHerokutorunthenpmrunbuildscript.Inthescriptssection,addthepostinstallscript:
"scripts":{
<...>
"postinstall":"npmrunbuild"
},
Nowlet'screateaserver.jsfileinwhichwewillservetheindex.htmlfilefromthedistdirectory.Createaserver.jsfileintheproject'sfolderandaddthefollowingcontent:
//server.js
varexpress=require('express');
varserveStatic=require('serve-static');
varapp=express();
app.use(serveStatic(__dirname+'/dist'));
varport=process.env.PORT||5000;
app.listen(port);
console.log('serverstarted'+port);
Okay,nowwejustneedtocreateastartscriptinthescriptssectionofourpackage.jsonfileandwearedone!Ourstartscriptshouldjustrunnodeserver.js,solet'sdoit:
"scripts":{
<...>
"postinstall":"npmrunbuild",
"start":"nodeserver.js"
},
Commityourchanges,gototheHerokudashboard,andclickontheDeployBranchbutton.Donotforgettocheckrunninglogs!
Andyippee!Thebuildwassuccessful!Afterasuccessfulbuild,youareinvitedtoclicktheViewbutton;don'tbeshy,clickonitandyouwillseeyourapplicationinaction!
ThePomodoroapplicationissuccessfullydeployedtoHeroku
NowyoucanuseyourPomodoroapplicationeverywhere.NowyoucanaskyourfriendstouseitaswellbysimplyprovidingthemtheHerokulink.
Well,congratulations!You'vejustdeployedyourVueapplication,anditcanbeusedbyeveryone.Howniceisit?
DeployingtheshoppinglistapplicationInordertodeployourshoppinglistapplication,weneedtoperformexactlythesamestepsaswehavedonewiththePomodoroapplication.
CreateanewapplicationonyourHerokudashboardandconnectittoyourGitHubshoppinglistproject.Afterthat,copytheserver.jsfilefromthePomodoroapplication,dealwiththedependenciesinthepackage.jsonfile,andcreatepostinstallandstartscripts.
However,westillhaveonestepleft.DonotforgetaboutourbackendserverthatservestheRESTAPIfortheshoppinglists.Weneedtorunitaswell.
Orevenbetter,whydoweneedtoruntwoserversifwecanhavejustoneserverthatdoeseverything?WecanintegrateourJSONserverwithourexpressserverbyprovidingittheroutingpathtoservetheshoppinglistendpoint,let'ssayapi.Opentheserver.jsfile,importthejsonServerdependencythere,andtelltheexpressapptouseit.So,yourserver.jsfilewilllooklikethefollowing:
//server.js
varexpress=require('express');
varjsonServer=require('json-server');
varserveStatic=require('serve-static');
varapp=express();
app.use(serveStatic(__dirname+'/dist'));
app.use('/api',jsonServer.router('server/db.json'));
varport=process.env.PORT||5000;
app.listen(port);
console.log('serverstarted'+port);
Withtheprecedingline,wetellourexpressapptousejsonServerandservethedb.jsonfileoverthe/api/endpoint.
Weshouldalsochangetheendpoint'saddressinourVueresource.Openindex.jsintheAPIfolderandreplacelocalhost:3000withanapiprefix:
constShoppingListsResource=Vue.resource('api/'+'shoppinglists{/id}')
WeshouldalsoaddJSONserversupporttodev-server.js;otherwise,wewillnotbeabletoruntheapplicationindevelopmentmode.So,openthebuild/dev-server.jsfile,importjsonServer,andtelltheexpressapptouseit:
//dev-server.js
varpath=require('path')
varexpress=require('express')
varjsonServer=require('json-server')
<...>
//compilationerrordisplay
app.use(hotMiddleware)
//usejsonserver
app.use('/api',jsonServer.router('server/db.json'));
<...>
Trytoruntheapplicationindevmode(npmrundev).Everythingworksfine.
Youcannowalsoremovetheserverrunningcommand(-nohupnpmrunserver&)fromthetravis.ymlfile.Youcanalsoremovetheserverscriptfrompackage.json.
Runtestslocallyandcheckthattheyarenotfailing.
Wearealmostdone.Let'stryourapplicationlocally.
TryingHerokulocallySometimesitgetsalotoftry-failiterationstogetthingswork.Wetrysomething,commit,push,trytodeploy,seewhetheritworks.Werealizethatwehaveforgottenaboutsomething,commit,push,trytodeploy,seetheerrorlog.Doitagainandagain.Itmightbereallytime-consumingbecausethingsoverthenetworktaketime!Fortunatelyforus,theHerokuCLIprovidesawaytoruntheapplicationlocallyasitwasalreadydeployedtotheHerokuserver.Youjustneedtoruntheherokulocalwebcommandrightafterbuildingtheapplication:
npmrunbuild
herokulocalweb
Tryit.
Openhttp://localhost:5000inyourbrowser.Yes,itworks!
RunningtheapplicationlocallywiththeHerokulocalwebcommand.Itworks!
Let'snowcommitandpushthechanges.
NowyoucanwaitforthesuccessfulTravisbuildandautomaticdeploybyHerokuafterthat,oryoucanjustopenyourHerokudashboardandclickontheDeployBranchbutton.Waitabit.And...itworks!Hereistheresultoftwodeploymentsweperformedtoday:
Pomodoroapplication:https://catodoro.herokuapp.com/Shoppinglistapplication:https://shopping-list-vue.herokuapp.com/
TherespectiveGitHubrepositoriescanbefoundathttps://github.com/chudaol/Pomodoroandhttps://github.com/chudaol/ShoppingList.
Fork,play,test,deploy.Atthismoment,youhavealltheinstrumentsneededtoenhance,improve,andshowtheseapplicationstothewholeworld.Thankyouforbeingwithmethroughthisexcitingjourney!
Chapter9.WhatIsNext?Inthepreviouschapter,wemadeourapplicationsgolivebydeployingthemtoaserverandmakingthemavailabletotheworld.Wehavealsoguaranteedcontinuousintegrationandcontinuousdeploymentofourapplications.Thismeansthateverytimewecommitchangesperformedontheapplications,theywillautomaticallybetestedanddeployed.
Itseemsthatourjourneyinthisbookhasfinished.But,infact,ithasjuststarted.Afterallwehavediscoveredandlearned,thereisstillsomuchtodo!Inthischapter,wewillwrapupeverythingwehavelearnedsofarandseewhatwestillhavetolearnandwhatnicethingswestillcandotoreachthelevelofawesomenessofourapplications.So,inthischapter,wewilldothefollowing:
WrapupeverythingwehavelearnedsofarMakealistoffollowupthings
ThejourneysofarWehavebeenonabigjourneysofar,andit'stimetosumupwhatwehavedoneandwhatwehavelearned.
InChapter1,GoingShoppingwithVue.js,wehadourfirstdatewithVue.js.WetalkedaboutwhatVue.jsis,howitwascreated,andwhatitdoesandsawsomebasicexamples.
InChapter2,Fundamentals-InstallingandUsing,wewentdeepintobehindthescenesofVue.js.WelearnedaboutMVVMarchitecturalpattern,wesawhowdoesVue.jswork,andwetoucheddifferentaspectsofVue.jssuchascomponents,directives,plugins,andapplicationstate.WelearneddifferentwaysofinstallingVue.js,startingfromusingasimplestandalonecompiledscript,passingbyusingtheCDNversion,NPMversion,andgoingtowardusingthedevelopmentversionofVue.jsbeingabletonotonlyuseitbutalsocontributetoitscodebase.WelearnedhowtodebugandhowtoscaffoldVue.jsapplicationusingVue-cli.WehaveevencreatedareallysimpleChromeapplicationusingCSP-compliantversionofVue.
InChapter3,Components-UnderstandingandUsing,weputourhandsdeepinsidethecomponent'ssystem.WelearnedhowtodefineVuecomponents,howcomponent'sscopeworks,andhowdocomponentsrelatetoeachother,andwestartedusingsingle-filecomponentsintheapplicationsthatwehavebootstrappedbefore.
InChapter4,Reactivity-BindingDatatoYourApplication,wewentdeepintodatabindingandreactivitywithVue.js.Welearnedhowtousedirectives,expressions,andfilters.Webroughtdatabindingtotheapplicationsdevelopedintheinitialchaptersandmadetheminteractive,thankstothereactivityfashionofVue.js.
InChapter5,Vuex-ManagingStateinYourApplication,welearnedhowtomanageglobalstateinVueapplicationsusingtheVuexstoresystem.Welearnedhowtousestate,actions,getters,andmutationsinordertocreateamodularandniceapplicationstructurewherethecomponentscaneasilycommunicatewitheachother.Weappliedthisnewknowledgeinourapplicationsthatwedevelopedsofarinthepreviouschapters.
InChapter6,Plugins-BuildingYourHousewithYourOwnBricks,welearnedhowVuepluginscooperatewithVueapplications.Weusedanexistingplugin,vue-resource,whichhelpedustosavetheapplication'sstatebetweenbrowserrefreshes.WealsocreatedourownpluginforVueapplicationsthatproduceswhite,brown,andpinknoises.Atthispoint,wehadfullyfunctionalapplicationswithaquitenicesetofworkingfeatures.
InChapter7,Testing-TimetoTestWhatWeHaveDoneSoFar!,welearnedhowtotestourVueapplications.Welearnedhowtowriteunittestsandhowtocreateandrunend-to-endtestswithSeleniumdriver.Welearnedwhatcodecoverageisandhowtofakeserverresponsesinunittests.Wecoveredalmost100%ofourcodewithunittestsandwesawtheSeleniumdriver
inactionrunningourend-to-endtests.
InChapter8,Deploying-TimetoGoLive!,wefinallyexposedourapplicationstothewholeworld.WedeployedthemtotheHerokucloudsystemandnowtheycanbeaccessedfromeverywherewheretheInternetexists.Morethanthat,wemadeourdeploymentprocesscompletelyautomated.Eachtimewepushcodechangestothemasterbranch,theapplicationisdeployed!Evenmorethanthat.Theyarenotonlydeployedoneachpush,butalsoautomaticallytestedwiththeTraviscontinuousintegrationsystem.
Thus,inthisbook,wehaven'tjustlearnedanewframework.Weappliedourknowledgetodeveloptwosimple,yetniceapplicationsfromscratch.WeappliedthemostimportantVue'sconceptstomakeourapplicationsreactive,fast,maintainable,andtestable.However,thisisnottheend.Duringthewritingofthisbook,Vue2.0hasbeenlaunched.Itbringssomenewpossibilitiesandsomenewthingstolearnanduse.
Vue2.0Vue2.0launchedontheSeptember30,2016.CheckoutthispostofEvanYouathttps://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8#.ifpgtjlek.
Acrossthisbook,weusedthenewestversion;however,ItriedtoreferencethewayofdoingthingsinthefirstgenerationofVuewheneveritwasnecessary.Actually,theAPIisalmostthesame;therearesomeslightchanges,somedeprecatedattributes,butthewholeinterfaceprovidedtothefinaluserremainsalmostuntouched.
Nevertheless,itwasalmostrewrittenfromscratch!Ofcourse,therearesomepartsofcodethatwerealmost100%reused,butoverall,itwasamajorrefactorandsomeoftheconceptswerecompletelychanged.Forexample,therenderinglayerwascompletelyrewritten.If,earlier,therenderingenginewasusingtherealDOM,nowitusesalightweightvirtualDOMstructure(https://github.com/snabbdom/snabbdom).Itsperformancebeatseverything!Checkoutthebenchmarkfigureinthefollowing:
Performancebenchmark(thelowerisbetter)takenfromhttps://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8#.fjxegtv98
Thereisanotherinterestingpointinthisnewversion.IfyouhavealreadyusedthefirstgenerationofVue,andreadaboutitandlistenedtopodcasts,youprobablyknowthatoneof
themajordifferencesbetween,let'ssay,VueandReactwasReactNative(theframeworkthatallowsustobuildnativeappsbasedonReact).EvanYouwasalwaysclaimingthatVuewasjustatinylayerforwebinterfaces.Now,wehavetheemergingWeex,aframeworkthatrendersVue-inspiredcomponentsintonativeapps(https://github.com/alibaba/weex).AccordingtoEvanYou,verysoon,"Vue-inspired"willbecome"Vue-powered"!Justwaitforit.Juststaytuned.IwouldliketorecommendthisamazingFullStackRadiopodcast,whereEvanYoutalksaboutthenewversionofVue:http://www.fullstackradio.com/50.
Vuehasevolvedalotsinceitshumblebeginningasasideproject.Todayitiscommunityfunded,widelyadoptedintherealworld,andboastsoneofthestrongestgrowthtrendsamongallJavaScriptlibrariesaccordingtostats.js.org.Webelieve2.0isgoingtopushitevenfurther.It'sthebiggestupdatetoVuesinceitsinception,andweareexcitedtoseewhatyoubuildwithit.-EvanYou,https://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8#.fjxegtv98)
Withthisinmind,ifyouarecomingfromtheVue1.0generation,itwillnotbehardforyoutoupgradeyourapplications.Checkthemigrationguide,http://vuejs.org/guide/migration.html,installthemigrationhelper,https://github.com/vuejs/vue-migration-helper,applyallneededchanges,andseehowyourapplicationsperformafterthat.
RevisitingourapplicationsLet'scheckagainwhathavewedonesofar.WehavedevelopedtwoapplicationsusingVue.js.Let'srevisitthem.
ShoppinglistapplicationTheshoppinglistapplicationthatwehavedevelopedinthisbook'schaptersisawebapplicationthatallowsthefollowing:
CreatedifferentshoppinglistsAddnewitemstotheshoppinglistsandcheckthemoncetheyareboughtRenameshoppinglistsandremovethem
OurshoppinglistapplicationresidesontheHerokucloudplatform:https://shopping-list-vue.herokuapp.com/.
ItscodeishostedonGitHub:https://github.com/chudaol/ShoppingList.
ItiscontinuouslyintegratedwithTravis:https://travis-ci.org/chudaol/ShoppingList
Itsinterfaceissimpleandeasytounderstand:
TheinterfaceoftheshoppinglistapplicationdevelopedusingVue.js
Itisstillfarfromsomethingthatyouwoulduseeverytimeyougoshopping,isn'tit?
ThePomodoroapplicationThePomodoroapplicationthatwehavedevelopedinthisbookisawebapplicationthatimplementsaPomodorotimerwithwhitenoiseduringtheworkingPomodorosandnicepicturesofcatsduringtheintervaltime.Itallowsthefollowing:
Start,pause,andstoptheapplicationListentowhitenoisewhileworking,noisethathelpsconcentratingMuteandunmutethewhitenoisesoundStareatthekittensduringsparetime
OurPomodoroapplicationisalsohostedontheHerokucloudplatform:https://catodoro.herokuapp.com/.
ItscodeisalsohostedatGitHub:https://github.com/chudaol/Pomodoro.
AnditisalsobuiltandtestedoneachpushusingtheTraviscontinuousintegrationplatform:https://travis-ci.org/chudaol/Pomodoro.
Itsinterfaceiscleanandeasytouse.Hereiswhatitshowsforthe20-minuteworkingPomodorointerval:
ThePomodoroapplicationduringtheworkingPomodoro
Andhere'swhatappearswhenthetimefora5-minutebreakcomes:
ThePomodoroapplicationduringitsintervaltime
Itisactuallyprettyusable,butalsostillfarfromperfect.
Whyisitjustthebeginning?Intheprevioussection,wesummarizedwhattheapplicationsthatwehavedevelopedthroughoutthebookaredoing.Wehavealsoagreed(Ihope)thattheyarestillfarfromperfect.Thingsthatarefarfromperfectarethingsthatwewanttoimproveandthereforetheygiveuschallengesandpurpose.Thereisactuallystillalotofworktobedone.Ourapplicationsarenice,buttheylackfeatures,style,identity,UXpatterns,extensiontootherplatforms,andsoon.Let'scheckwhatwecanstilldo.
AddingfeaturestoourapplicationsOurapplicationsalreadyhavesomeprettynicefeatures,buttheycanhaveevenmore.Theycanbemoreconfigurable.Theycanbemoreflexible.TheycanbemoreUI/UXfriendly.Let'slookateachofthemandwritealistoffeaturesthatcanbeadded.Itwillbeyourhomework.
Shoppinglistapplication
Openourshoppinglistapplicationinthebrowserandlookatit.Youcanaddyourlistsanditemstothem.Youcandeleteitemsandlists.Buteverypersonwhoopenstheapplicationinthebrowserwillbeabletodothesame.Itmeansthatwehavetoprovideawayofeverypersonhavingtheirownshoppinglistapplication,whichisonlypossiblewithanauthenticationmechanism.
TherearealsosomeUXissues.Whyshouldwechangethenameoftheshoppinglistusingtheinputfieldinthefooterifwecanchangeit,let'ssay,inline?Well,actually,shoppinglist'snameeditingintheinputfieldwasthefirstthingweimplementedwhenwewerelearninghowtoachievedatabindinginVueapplications.So,itmadesenseatthetime,butnowitcanandshouldbeimproved.
Anotherthinghastodowithdeleteditems.Thereisnowayofclearingthemup.Ifwehavealonglistofitems,evenwhenwedeletethem,theystayforeverunlessweremovethewholeshoppinglist.Thereshouldbeawayofclearingupcheckeditemsonthelist.
Anothercosmeticchangethatwecanapplyhastodowithstyling.Differentlistsmighthaveadifferentbackgroundcolors,differentfontcolors,andprobablyevendifferentfontstylesandsizes.Withthat,here'sthelistofimprovementsfortheshoppinglistapplication:
ImplementanauthenticationmechanismImplementinlinenameeditingImplementclearingupcheckeditemsImplementamechanismofconfiguringdifferentshoppinglists'styling,suchasbackgroundcolor,textcolor,fontsize,andstyle
Youcanalsoimplementcategoriesfortheitemsandiconsforeachofthecategories.Asaninspiration,youcanhavealookattheSplitwiseapplicationathttps://www.splitwise.com/.Whenyoustartaddingitems,theiconoftheitemisgeneric.Onceyoutypeinsomethingmeaningful,theiconchanges,asshowninthefollowingscreenshot:
ThescreenshotoftheSplitwiseapplicationfortheinspirationfortheiconcategories:itadaptstowhatyoutypeintheinputfield
Trytoimplementthiskindofcategorizationforourshoppinglistapplication.Itwouldbeareallyniceandpowerfulbonus!
ThePomodoroapplication
OpenourPomodoroapplicationinyourbrowserandtryusingit.It'snice,withoutanydoubt.Itissimpleandeasytouse.However,someextraconfigurationmightbringsomeextrapowertoit.Forexample,whyshouldIworkfor20minutes?MaybeIwouldliketohave15-minuteperiodsofworkingPomodoros.OrmaybeIwanttohavebiggerworkingPomodoros,let'ssay25or30minutes.Itshoulddefinitelybeconfigurable.
Let'sthoroughlycheckthedescriptionofthePomodorotechniqueinWikipediatoseeifwearemissingsomething:https://en.wikipedia.org/wiki/Pomodoro_Technique.
I'mprettysureweare.Checkthispointattheunderlyingprinciples:
"Afterfourpomodoros,takealongerbreak(15-30minutes),resetyourcheckmarkcounttozero,thengotostep1."
--https://en.wikipedia.org/wiki/Pomodoro_Technique
Aha!SomethingshouldhappenafterfourPomodoros.Biggerinterval,moretimestaringatcats(ordoingwhateveryouwanttodo).Hmm,probablyitwouldbenicetobeabletoconfigurethisperiodoftimeaswell!
There'sanotherimportantthing.Asanyhumanbeing,afterworkinghard,Iwouldliketosee
someprogress.Wouldn'titbeniceifourPomodoroapplicationcoulddisplaysomestatisticsabouttheamountoftimewewereabletoconcentrateonourselvesandtodoourwork?Forthis,wecouldcollectsomestatisticsanddisplaytheminourPomodorotimer.
Also,wouldn'titbenicetostorethesestatisticstobeabletovisualizethemduringsomeperiodoftime,let'ssay,oneweek,onemonth,oneyear?Thisleadsustotheneedtoimplementastoragemechanism.Thisstoreshouldstorestatisticsforeachuser,soagain,anauthenticationmechanismisneededaswell.
Let'sthinkaboutournicewhite,brown,andpinknoises.Currently,wejustplaythebrownnoisethatishardcodedinourApp.vue:
<template>
<divid="app"class="container"v-noise="'brown'">
</div>
</template>
Shouldn'twebeabletoswitchbetweennoisesandchooseourfavoriteone?Hence,wehaveidentifiedonemoreitemtoaddtotheapplication'sconfiguration.That'senoughfornow;let'sputallthisinthelist:
ImplementtheauthenticationmechanismImplementastoragemechanism—itshouldcollectthestatisticsaboutworkingtimesandstoretheminsomepersistencelayerImplementthestatisticsdisplayingmechanism—itshouldgrabthestoredstatisticaldataanddisplayitinaniceandcleanway(forexample,charts)AddaconfigurationmechanismtothePomodoroapplication.Thisconfigurationshouldallowthefollowing:
ConfigurethePomodoroworkingperiodoftimeConfiguretherestingintervalsoftimesConfigureabigrestingtimeafteraconfigurableamountofworkingPomodoros(4bydefault)Configurethepreferrednoisetoplayduringtheworkingintervals
Asyoucansee,youstillhavesomeworktodo.It'sagoodthing,youhavealreadyaworkingPomodorotimerapplicationtousewhileyouareworkingonitsimprovements!
BeautifyingourapplicationsBothapplicationsarecurrentlyprettygray.OnlythePomodorotimerapplicationbecomesalittlebitmorecolorfulwhenacatappearsonthescreen.Itwouldbenicetoaddsomedesigntothem.Makethemunique,givethemtheiridentity;youworkedsohardonthem,theyclearlydeservesomeniceclothes.Let'sthinkaboutwhatwecandowithstyling.
Logotype
Startwiththelogotype.Agoodlogodefinesyourproductandmakesitunique.IcanhelpyouwiththePomodoroapplication'slogo,atleastwiththeideaforit.IhaveaverygoodfriendcalledCarinawhodesignedatomatoformeandIhavejusttriedmybesttoaddalittlekittentoit.Checkitout.Youcanuseitasisorusejustasanideatodevelopyourown.Eventheskyisnotthelimitforyourimagination,really!
TheideaforalogotypeforthePomodoroapplication
Thinkaboutanicelogofortheshoppinglistapp.Whatcanitbe?Abagforthegroceries?Acheckbox?Justinitials—SL?Again,nolimits.Ihopetoseeyournicelogosinthe
repositoriesforks.Can'twaitforit!
Identityanddesign
Ourapplicationsdefinitelyneedsomeuniquedesign.UsesomeUXtechniquestodevelopaniceidentityguideforthem.Thinkaboutcolors,fonts,andhowtheelementsshouldbecomposedonthepagesothattheyprovideauniqueuser-friendlyexperiencetoourusers.
Animationsandtransitions
Animationsandtransitionsarepowerfulmechanismsthatbringsomelifetoanapplication.However,theycannotbeabused.Thinkaboutwhereandhowtheymakesense.Forexample,hoveringontheshoppingliststitlescouldendupinsomehighlighting,shoppinglistitemscandosometinybouncingwhentheyarechecked,theprocessofchangingthetitleoftheshoppinglistcouldalsobehighlightedinsomeway,andsoon.ThePomodoroapplicationcanchangeitsbackgroundcoloroneachofthestate'stransitions.Itcanalsobeawareofthetimeofthedayandcolorthebackgroundaccordingly.Thenumberofopportunitiesisendless.Useyourcreativity,useVue'spowertoachieveyourideas.
ExtendingourapplicationstootherdevicesBothofourapplicationsarewebapplications.WhileitmightbeokayforthePomodoroapplicationifweworkthewholedayonthecomputerandusetheWeb,itmightbealittlebituncomfortablefortheshoppinglistapplication.Youdon'tbringyourlaptopwhenyougoshopping.Ofcourse,youcanfilltheshoppinglistwithitemsathomeandthenopenthemobilebrowserinthesupermarket,butitmightbeslowandnotsonicetouse.UseWeex(https://github.com/alibaba/weex)tobringourwebapplicationstothemobiledevices.BoththeapplicationscanalsobeextendedtobeusedasaGoogleChromeapp,justaswelearnedinChapter2,Fundamentals-InstallingandUsing.Extendyourworktoeachandeverydeviceyoucan.Iamlookingforwardtocheckingyourwork.
SummaryThisisthelastchapterofthisbook.Honestly,Ifeelalittlebitsadaboutit.Ihadareallyfuntimewithyou.IknowthatIdon'tknowyou,butIfeellikeIdo.ItalktoyouandIfeelthatsometimesyoutalktome.Everythingthatwasdevelopedsofar,Icannotsayatallthatitwasdevelopedbyme;Ifeelthatwehavebeenworkingtogetheronitallthistime.
Itisaveryfunnyfeeling,actually,becauseIamatthesametimeinthepresentandinthefuturewhenyouarereadingthisbook(forme,it'sthefuture).Andyouarenowinyourpresentandatthesametimetalkingtomeinthepast.Ilovethewaythatbooksandtechnologiesestablishconnectionsnotonlybetweenpeoplebutalsobetweendifferenttimeintervals.Thisisamazing.
IreallyhopethatyoubecameafanofVue.jsinthesamewayIamafanofit.
Ireallyhopethatyouwillenhanceatleastoneoftheapplicationswehavedevelopedsofarandshowittome.Iwillbereallygladtohelpifyouneedmyhelp.Donothesitatetodropmeamessageatchudaol@gmail.com.
Thankyouforbeingwithmeallthistime,andIhopetomeetyousooninthenextbook!
Chapter10.SolutionstoExercises
Exerciseforchapter1Intheendofthefirstchapter,therewasthefollowingexercise:
Note
ThePomodorotimerthatwehavebuiltinthepreviouschaptersis,withoutanydoubt,great,butitstilllackssomenicefeatures.Areallynicethingthatitcouldprovidewouldbeshowingrandomkittensfromhttp://thecatapi.com/duringrestingtime.Canyouimplementthis?Ofcourseyoucan!Butpleasedonotconfuserestingwithworkingtime!Iamalmostsurethatyourprojectmanagerwillnotlikemuchifyoustareatthekittensinsteadofworking:)
Let'ssolveit.
CheckthecodeforPomodoroathttps://jsfiddle.net/chudaol/b6vmtzq1/.
Checkhttp://thecatapi.com/website.
Let'sstartbyaddingthewellBootstrapelementwithanimagewhosesourcepointstothecatAPI:
<divid="app"class="container">
<...>
<divclass="well">
<img:src="’http://thecatapi.com/api/images/get?
type=gif&size=med’"/>
<div>
</div>
Ifyouopenthepageyouwillseethattheimageisalwaysvisible.Thisisnotwhatwewant,wewant,ittoonlybevisiblewhenweareinourPomodororestinginterval.Youalreadyknowhowtodoit.Thereareseveralwaysofachievingthis;let'susetheclassbindingmethodandbindaclassthat'shiddenwhenthestateisworking:
<divclass="well":class="{'hidden':pomodoroState==='work'}">
<img:src="'http://thecatapi.com/api/
images/get?type=gif&size=med'"/>
</div>
Now,ifyouopenthepageyouwillseethattheimageonlyappearswhentheworkingPomodoroisfinished.
However,theproblemisthatforallthetimethatwerest,theimageisthesame.Itwouldbegreatifwecouldupdateitevery,let'ssay,10seconds.
Let'suseacachebustermechanismforthispurpose.IfweattachsomepropertytoourURLandchangeiteach10seconds,theURLwillchangeandthereforewewillobtainanotherrandomcat.Let'saddatimestampvariabletoourVueapplicationandchangeitinsidethe
_tickfunction:
<...>
newVue({
el:"#app",
data:{
<...>
timestamp:0
},
<...>
methods:{
<...>
_tick:function(){
//updatetimestampthatisusedinimagesrc
if(this.second%10===0){
this.timestamp=newDate().getTime();
}
<...>
}
}
});
Afterthetimestampiscreatedandupdated,wecanuseitinourimagesourceURL:
<divclass="well":class="{'hidden':pomodoroState==='work'}">
<img:src="'http://thecatapi.com/api/images/get?
type=gif&size=med&ts='+timestamp"/>
</div>
That'sall!CheckthewholecodeinthisJSFiddleathttps://jsfiddle.net/chudaol/4hnbt0pd/2/.
Exercisesforchapter2
EnhancingMathPluginEnhanceourMathPluginwithtrigonometricalfunctions(sine,cosine,andtangent).
Actually,itisjustaboutaddingthemissingdirectivesandusingtheMathobject'sfunctionsinit.OpenVueMathPlugin.jsandaddthefollowing:
//VueMathPlugin.js
exportdefault{
install:function(Vue){
Vue.directive('square',function(el,binding){
el.innerHTML=Math.pow(binding.value,2);
});
Vue.directive('sqrt',function(el,binding){
el.innerHTML=Math.sqrt(binding.value);
});
Vue.directive('sin',function(el,binding){
el.innerHTML=Math.sin(binding.value);
});
Vue.directive('cos',function(el,binding){
el.innerHTML=Math.cos(binding.value);
});
Vue.directive('tan',function(el,binding){
el.innerHTML=Math.tan(binding.value);
});
}
};
YoucancheckhowthisdirectiveworksintheHTMLfile:
//index.html
<divid="app">
<inputv-model="item"/>
<hr>
<div><strong>Square:</strong><spanv-square="item"></span></div>
<div><strong>Root:</strong><spanv-sqrt="item"></span></div>
<div><strong>Sine:</strong><spanv-sin="item"></span></div>
<div><strong>Cosine:</strong><spanv-cos="item"></span></div>
<div><strong>Tangent:</strong><spanv-tan="item"></span></div>
</div>
That'sit!
CreatingaChromeapplicationofthePomodorotimerPleasecombineasolutionofbootstrappingtheapplicationusingaSCP-compliantversionofVue.jsandthesimplePomodoroapplicationthatwecreatedinChapter1,GoingShoppingwithVue.js.Checkthecodeinthechrome-app-pomodorofolder.
Exercisesforchapter3
Exercise1Whenwewererewritingtheshoppinglistapplicationusingsimplecomponents,welosttheapplication'sfunctionality.Theexercisesuggestsusinganeventsemittingsysteminordertobringthefunctionalityback.
Thecodeweendedupwithinthissectionwaslookingsimilartowhatisinthechapter3/vue-shopping-list-simple-componentsfolder.
Whydoesn'titwork?Checkthedevtoolserrorconsole.Itstatesthefollowing:
[Vuewarn]:Propertyormethod"addItem"isnotdefinedontheinstancebut
referencedduringrender.Makesuretodeclarereactivedatapropertiesinthe
dataoption.
(foundincomponent<add-item-component>)
Aha!Thishappensbecauseinsideadd-item-templatewearecallingtheaddItemmethodwhichdoesnotbelongtothiscomponent.Thismethodbelongstotheparentcomponent,andofcourse,thechildcomponentdoesnothaveaccesstoit.Whatshouldwedo?Let'semitevents!Wealreadyknowhowtodoit.So,wedon'thavetodotoomuch.Actually,wehavetodothreesmallthings:
AttachtheaddItemmethodtoadd-item-componentinwhichwewillemitaneventandpassthiscomponent'snewItempropertytoit.Modify/simplifytheaddItemmethodoftheparentcomponent.Itshouldnowjustreceiveatextandaddittoitsitemsproperty.Attachthev-onmodifierwiththenameoftheeventtothecomponent'sinvocationinsidethemainmarkupthatwillcalltheaddItemmethodeachtimetheeventisemitted.
Let'sstartbyaddingtheaddItemmethodtoadd-item-component.ItiscalledeachtimetheaddbuttonorEnterishit.ThismethodshouldcheckthenewItemproperty,andifitcontainsatext,shouldemitanevent.Let'scallthiseventadd.Thus,theJavaScriptcodeofourcomponentwillnowlookthefollows:
//additemcomponent
Vue.component('add-item-component',{
template:'#add-item-template',
data:function(){
return{
newItem:''
}
},
methods:{
addItem:function(){
vartext;
text=this.newItem.trim();
if(text){
this.$emit('add',this.newItem);
this.newItem='';
}
}
}
});
Whentheaddeventisemitted,somehowtheaddItemmethodofthemaincomponentshouldbeinvoked.Let'sbindtheaddeventtoaddItembyattachingthev-on:addmodifiertotheadd-item-componentinvocation:
<add-item-componentv-on:add="addItem":items="items">
</add-item-component>
Okaythen.Asyoucansee,thismethoddoesalmostthesamethattheaddItemmethodofthemaincomponentwasdoingbefore.Itjustdoesn'tpushnewItemtotheitemsarray.Let'smodifytheaddItemmethodofthemaincomponentsoitjustreceivesalreadyprocessedtextandpushesitintothearrayofitems:
newVue({
el:'#app',
data:data,
methods:{
addItem:function(text){
this.items.push({
text:text,
checked:false
});
}
}
});
We'redone!Thefullsolutionofthisexercisecanbefoundintheappendix/chapter3/vue-shopping-list-simple-componentsfolder.
Exercise2InthesectioncalledRewritingtheshoppinglistapplicationwithsingle-filecomponentsinChapter3,Components–UnderstandingandUsing,wedidquiteanicejobofchangingtheshoppinglistapplicationusingsingle-filecomponents,buttherearestillfewthingsleft.Wehavetwomissingfunctionalities:addingitemstotheitemslist,andchangingthetitle.
Inordertoachievethefirstfunctionality,wehavetoemitaneventfromAddItemComponentandattachthev-onmodifiertotheadd-item-componentinvocationwiththemainApp.vuecomponent,exactlylikewehavedoneinthecaseofdealingwithsimplecomponents.Youcanbasicallyjustcopyandpastethecode.
Thesamegoesforthechangingtitlefunctionality.Weshouldalsoemitaninputevent,justlikewedidinthesimplecomponentsexample.
DonotforgettoaddthestyletotheApp.vuecomponenttomakeitlookjustasitwasbefore.
Checkthefullcodeintheappendix/chapter3/shopping-list-single-file-componentsfolder.
SummaryInthischapter,youlearnedhowtomakeourapplicationsavailableforeveryone.YoualsolearnedhowtodeploythemusingHerokuintegrationwiththeGitHubrepository.Youalsolearnedhowtodoitautomaticallyoneachcommitandpush.WealsousedTravisforautomaticbuildsoneachdeployment.Nowourapplicationsarebeingfullytestedandautomaticallyredeployedeachtimewecommitachange.Congratulations!
Youprobablythinkthatthisistheendofthejourney.No,itisnot.Thisisjustthebeginning.Inthenextchapter,wewillseewhatyoucanlearnandwhatnicethingsyoucandonextwithboththePomodoroandshoppinglistapplications.Staywithme!