DOM Manipulation - Go Make Things
Transcript of DOM Manipulation - Go Make Things
DOMManipulationByChrisFerdinandiGoMakeThings,LLCv4.0.0
Copyright2021ChrisFerdinandiandGoMakeThings,LLC.AllRightsReserved.
TableofContents
1. Intro
AquickwordaboutbrowsercompatibilityUsingthecodeinthisguide
2. Selectors
document.querySelectorAll()document.querySelector()Element.matches()Type-specificselectormethods
3. Loops
forfor…offor…inSkippingandendingloopsArray.forEach()andNodeList.forEach()
4. Classes
Element.classListElement.className
5. Styles
InlineStylesComputedStyles
6. Attributes&Properties
Element.getAttribute(),Element.setAttribute(),Element.removeAttribute(),andElement.hasAttribute()Properties
Attributesvs.Properties
7. EventListeners
EventTarget.addEventListener()MultipleTargetsCapturingeventsthatdon’tbubbleMultipleEvents
8. Puttingitalltogether
GettingSetupListeningforclicksDeterminingwhethertoshoworhidepasswordsShowingandhidingpasswordsStylingthebutton
9. AbouttheAuthor
Intro
Inthisguide,you’lllearn:
HowtogetelementsintheDOM.Howtoloopthrougharrays,objects,NodeLists,andotheriterableitems.Howtoget,set,andremoveclasses.Howtomanipulate,remove,andupdatestyles.Howtoget,set,andremoveattributes.HowtolistenforeventsintheDOM.Techniquesforimprovingeventlistenerperformance.
Aquickwordaboutbrowsercompatibility
ThisguidefocusesonmethodsandAPIsthataresupportedinallmodernbrowsers.ThatmeansthelatestversionsofEdge,Chrome,Firefox,Opera,Safari,andthelatestmobilebrowsersforAndroidandiOS.
Usingthecodeinthisguide
Unlessotherwisenoted,allofthecodeinthisbookisfreetouseundertheMITlicense.Youcanviewofcopyofthelicenseathttps://gomakethings.com/mit.
Let’sgetstarted!
Selectors
HowtogetelementsintheDOM.
document.querySelectorAll()
Findallmatchingelementsonapage.YoucanuseanyvalidCSSselector.
document.querySelector()
Findthefirstmatchingelementonapage.
//Getallbuttonelementsletbuttons=document.querySelectorAll('button');
//Getallelementswiththe.bg-redclassletelemsRed=document.querySelectorAll('.bg-red');
//Getallelementswiththe[data-snack]attributeletelemsSnacks=document.querySelectorAll('[data-snack]');
Ifanelementisn’tfound,querySelector()returnsnull.If
youtrytodosomethingwiththenonexistentelement,anerrorwillgetthrown.Youshouldcheckthatamatchingelementwasfoundbeforeusingit.
Element.matches()
//Thefirstbuttonletbutton=document.querySelector('button');
//Thefirstelementwiththe.bg-redclass
letred=document.querySelector('.bg-red');
//Thefirstelementwithadataattributeofsnackequaltocarrotsletcarrots=document.querySelector('[data-snack="carrots"]');
//Anelementthatdoesn'texistletnone=document.querySelector('.bg-orange');
//Verifyelementexistsbeforedoinganythingwithit
if(none){
//Dosomething...
}
Checkifanelementwouldbeselectedbyaparticularselectororsetofselectors.Returnstrueiftheelementisamatch,and
falsewhenit’snot.
Type-specificselectormethods
Thereareotherselectormethodsthattargetelementsbyspecifictype.
Thedocument.getElementById()methodgetselementsby
theirID,predatesIE6.Thedocument.getElementsByName()methodreturnsa
NodeListofelementswithmatching[name]attributes.Italso
hasdeepbackwardscompatibility.
Ifyouwantedtogetallelementsofacertaintype,youcouldusedocument.getElementsByTagName(),whichworks
backtoIE6.Andthenewkidontheblock,
//Checkifthefirst.bg-redelementhasthe[data-snackattribute]
letred=document.querySelector('.bg-red');
if(red.matches('[data-snack]')){
console.log('Yummysnack!');
}else{
console.log('Nosnacks');
}
document.getElementsByClassName(),getsallelements
thatmatchaspecificclass.ItworksinIE9andup.
Idon’trecommendusinganyofthem.
I’mlazy.Idon’tliketothinkaboutwhichselectoristherightonetouse.Thedocument.querySelector()and
document.querySelectorAll()methodsdoeverything
thoseothermethodsdoandmore.
ThetoughestdecisionIhavetomakeiswhetherIneedallmatchingelementsorjustthefirstone.
Loops
Howtoloopthrougharrays,objects,andarray-likeobjects.
for
Loopthrougharrays,NodeLists,andotherarray-likeobjects.
Inthefirstpartoftheloop,beforethefirstsemicolon,wesetacountervariable(typicallyi,butitcanbeanything)
to0.
Thesecondpart,betweenthetwosemicolons,isthetestwecheckagainstaftereachiterationoftheloop.Inthiscase,wewanttomakesurethecountervalueislessthanthetotalnumberofitemsinourarray.Wedothisbycheckingthe.lengthofourarray.
Finally,afterthesecondsemicolon,wespecifywhattorun
letsandwiches=['turkey','tuna','ham','pb&j'];
//logs0,"tuna",1,"ham",2,"turkey",3,"pb&j"for(leti=0;i<sandwiches.length;i++){
console.log(i);//index
console.log(sandwiches[i]);//value
}
aftereachloop.Inthiscase,we’readding1tothevalueof
iwithi++.
Wecanthenuseitograbthecurrentitemintheloopfromour
array.
for…of
Loopoveriterableobjects.Thatincludesstrings,arrays,andotherarray-likeobjectssuchasNodeLists,HTMLCollections,andHTMLFormControlsCollection,butnotplainobjects({}).
Inafor...ofloop,youdefineavariabletorepresentthe
currentitemoftheiterablethatyou’reloopingthrough.Inside
theblock(thestuffbetweencurlybrackets),youcanusethatvariabletoreferencethecurrentitem.
for…in
letsandwiches=['turkey','tuna','ham','pb&j'];
//logs"tuna","ham","turkey","pb&j"
for(letsandwichofsandwiches){
console.log(sandwich);
}
Loopoverplainobjects({}).
Thefirstpart,key,isavariablethatgetsassignedtotheobject
keyoneachloop.Thesecondpart(intheexamplebelow,lunch),istheobjecttoloopover.
Inafor...inloop,youdefineavariabletorepresentthekey
intheobjectthatyou’reloopingthrough.Insidetheblock(the
stuffbetweencurlybrackets),youcanusethatvariabletogetthekeynameandthevalueofthatkey.
letlunch={
sandwich:'ham',
snack:'chips',
drink:'soda',
desert:'cookie',
guests:3,
alcohol:false,
};
//logs"sandwich","ham","snack","chips","drink","soda","desert","cookie","guests",3,"alcohol",false
for(letkeyinlunch){
console.log(key);//key
console.log(lunch[key]);//value
}
Skippingandendingloops
Youcanskiptothenextiteminaloopusingcontinue,orend
theloopaltogetherwithbreak.Theseworkwithfor,
for...of,andfor...inloops.
/**
*Skippingaloop
*/letsandwiches=['turkey','tuna','ham','pb&j'];
//logs"turkey","tuna","turkey","pb&j"
for(letsandwichofsandwiches){
//Skiptothenextitemintheloop
if(sandwich==='ham')continue;
console.log(sandwich);
}
/**
*Breakingaloop
*/
letlunch={
sandwich:'ham',
snack:'chips',
drink:'soda',
desert:'cookie',
Array.forEach()andNodeList.forEach()
TheArray.forEach()andNodeList.forEach()methods
provideasimplerwaytoiterateoverarraysandNodeListswhilestillhavingaccesstotheindex.
YoupassacallbackfunctionintotheforEach()method.The
callbackitselfacceptsthreearguments:thecurrentitemintheloop,theindexofthecurrentitemintheloop,andthearrayitself.Allthreeareoptional,andyoucannamethemanythingyouwant.
desert:'cookie',
guests:3,
alcohol:false,
};
//logs"sandwich","ham","snack","chips"
for(letkeyinlunch){
if(key==='drink')break;
console.log(lunch[key]);
}
Unlikewithfor,for...of,andfor...inloops,youcan’t
endaforEach()callbackfunctionbeforeit’sloopedthrough
allitems.Youcanreturntoendthecurrentloop(likeyou
wouldwithcontinue),butthere’snowaytobreaktheloop.
Becauseofthat,Igenerallypreferusingafor...ofloop
unlessIexplicitlyneedtheindex.
letsandwiches=['turkey','tuna','ham','pb&j'];
//logs0,"tuna",1,"ham",2,"turkey",3,"pb&j"sandwiches.forEach(function(sandwich,index){
console.log(index);//index
console.log(sandwich);//value
});
//Skip"ham"
//logs0,"tuna",2,"turkey",3,"pb&j"sandwiches.forEach(function(sandwich,index){
if(sandwich==='ham')return;
console.log(index);//index
console.log(sandwich);//value
});
Classes
Howtoadd,remove,toggle,andcheckforclassesonanelement.
Element.classList
TheElement.classListAPIprovidesasimplewaytoadd,
remove,toggle,andcheckforclassesonanelement.
Usetheadd()methodtoaddaclass,theremove()methodto
removeaclass,thetoggle()methodtotoggleaclassonor
off,andthecontains()methodtocheckifaclassexists.
Element.className
Getalloftheclassesonanelementasastring,addaclassorclasses,orcompletelyreplaceorremoveallclasses.
letelem=document.querySelector('#sandwich');
//Addthe.turkeyclass
elem.classList.add('turkey');
//Removethe.tunaclass
elem.classList.remove('tuna');
//Togglethe.tomatoclassonoroff//(Addtheclassifit'snotalreadyontheelement,removeitifitis.)
elem.classList.toggle('tomato');
//Checkifanelementhasthe.mayoclass
if(elem.classList.contains('mayo')){
console.log('addmayo!');
}
letelem=document.querySelector('div');
//Getalloftheclassesonanelement
letelemClasses=elem.className;
//Addaclasstoanelement
elem.className+='vanilla-js';
//Completelyreplaceallclassesonanelement
elem.className='new-class';
Styles
Howtogetandsetstyles(asin,CSS)foranelement.
VanillaJavaScriptusescamelcasedversionsoftheattributesyouwoulduseinCSS.TheMozillaDeveloperNetworkprovidesacomprehensivelistofavailableattributesandtheirJavaScriptcounterparts.
InlineStyles
GetandsetinlinestylesforanelementwiththeElement.styleproperty.
TheElement.stylepropertyisaread-onlyobject.Youcan
getandsetindividualstylepropertiesonitusingcamelCasestylenamesaspropertiesontheElement.styleobject.
<pid="sandwich"style="background-color:green;color:white;">
Sandwich
</p>
Youcanalsogetandsetastringrepresentationoftheentireinlinestylepropertyontheelementitselfwiththe
Element.style.cssTextproperty.
letsandwich=document.querySelector('#sandwich');
//Getastyle//Ifthisstyleisnotsetasaninlinestyledirectlyontheelement,itreturnsanemptystringletbgColor=sandwich.style.backgroundColor;//thiswillreturn"green"letfontWeight=sandwich.style.fontWeight;//thiswillreturn""
//Setthebackground-colorstyleproperty
sandwich.style.backgroundColor='purple';
ComputedStyles
Thewindow.getComputedStyle()methodgetstheactual
computedstyleofanelement.Thisfactorsinbrowserdefaultstylesheetsaswellasexternalstylesbeingusedonthepage.
//Getthestylesonanelement//returns"background-color:green;color:white;"
letstyles=sandwich.style.cssText;
//Completelyreplacetheinlinestylesonanelementsandwich.style.cssText='font-size:2em;font-weight:bold;';
//Addadditionalstyles
sandwich.style.cssText+='color:purple;';
letsandwich=document.querySelector('#sandwich');letbgColor=window.getComputedStyle(sandwich).backgroundColor;
Attributes&Properties
Howtoget,set,andremoveattributesforanelement.
Element.getAttribute(),Element.setAttribute(),Element.removeAttribute(),andElement.hasAttribute()
Get,set,remove,andcheckfortheexistenceofattributes(includingdataattributes)onanelement.
Ifanattributedoesnotexistonanelement,theElement.getAttribute()methodreturnsnull.
Properties
HTMLelementshavedozensofpropertiesthatyoucanaccessdirectly.
Someofthemarereadonly,meaningyoucangettheirvaluebutnotsetit.Otherscanbeusedtobothreadandsetvalues.YoucanfindafulllistontheMozillaDeveloperNetwork.
letelem=document.querySelector('#lunch');
//Getthevalueofthe[data-sandwich]attributeletsandwich=elem.getAttribute('data-sandwich');
//Setavalueforthe[data-sandwich]attributeelem.setAttribute('data-sandwich','turkey');
//Removethe[data-chips]attribute
elem.removeAttribute('data-chips');
//Checkifanelementhasthe`[data-drink]`attribute
if(elem.hasAttribute('data-drink')){
console.log('Addadrink!');
}
Attributesvs.Properties
InJavaScript,anelementhasattributesandproperties.Thetermsareoftenusedinterchangeably,butthey’reactuallytwoseparatethings.
AnattributeistheinitialstatewhenrenderedintheDOM.Apropertyisthecurrentstate.
Inmostcases,attributesandpropertiesarekeptin-syncautomatically.Forexample,whenyouusesetAttribute()
toupdateanIDattribute,theidpropertyisupdatedaswell.
letelem=document.querySelector('#main');
//GettheIDoftheelement
//returns"main"
letid=elem.id;
//SettheIDoftheelement
elem.id='secondary';
//GettheparentNodeoftheelement
//Thispropertyisread-only
letparent=elem.parentNode;
<p>Hello</p>
However,user-changeableformproperties—noteably,value,
checked,andselected—arenotautomaticallysynced.
letp=document.querySelector('p');
//UpdatetheID
p.setAttribute('id','first-paragraph');
//Thesebothreturn"first-paragraph"
letid1=p.getAttribute('id');
letid2=p.id;
<labelfor="greeting">Greeting</label>
<inputtype="text"id="greeting">
Ifyoutrytoupdatethevaluepropertydirectly,thatwill
updatetheUI.
Thisallowsyoutochoosedifferentapproachesdependingonwhetheryouwanttooverwriteuserupdatesornot.
Ifyouwanttoupdateafield,butonlyiftheuserhasn’tmadeanychanges,useElement.setAttribute().Ifyouwantto
overwriteanythingthey’vedone,usethevalueproperty.
letgreeting=document.querySelector('#greeting');
//Updatethevaluegreeting.setAttribute('value','Hellothere!');
//Ifyouhaven'tmadeanyupdatestothefield,thesebothreturn"Hellothere!"//IfyouHAVEupdatedthefield,val1returnswhateverwastypedinthefieldinstead
letval1=greeting.value;
letval2=greeting.getAttribute('value');
greeting.value='Hellothere!';
EventListeners
Howtolistenforbrowsereventsandruncallbackfunctionswhentheyhappen.
EventTarget.addEventListener()
Listenforeventsonanelement.YoucanfindafulllistofavailableeventsontheMozillaDeveloperNetwork.
RuntheEventTarget.addEventListener()methodon
theelementyouwanttolistenforeventson.Itacceptstwoarguments:theeventtolistenfor,andacallbackfunctiontorunwhentheeventhappens.
Youcanpasstheeventintothecallbackfunctionasan
argument.Theevent.targetpropertyistheelementthat
triggeredtheevent.Theeventobjecthasotherpropertiesas
well,manyofthemspecifictothetypeofeventthatoccurred.
MultipleTargets
TheEventTarget.addEventListener()methodonlybe
attachedtoanindividualelement.Youcan’tattachittoanarrayornodelistofmatchingelementslikeyoumightinjQueryorotherframeworks.
letbtn=document.querySelector('#click-me');
btn.addEventListener('click',function(event){
console.log(event);//Theeventdetailsconsole.log(event.target);//Theclickedelement
});
//Thiswon'twork!letbtns=document.querySelectorAll('.click-me');
btns.addEventListener('click',function(event){
console.log(event);//Theeventdetailsconsole.log(event.target);//Theclickedelement
});
Forperformancereasons,youalsoshouldnotloopovereachelementandattachanevenlistenertoit.
Fortunately,there’sareallyeasyandperformantwaytogetajQuery-likeexperience:eventdelegationoreventbubbling.
Insteadoflisteningforaneventonspecificelements,youattachyourlistenertoaparentelementthatyourelementsarecontainedwithin,suchasthewindowordocument.Events
thathappensonelementsinsideitbubbleup.Wecanthenchecktoseeiftheitemthattriggeredtheeventhasamatchingselector.
/**
*Thisworks,butit'sbadforperformance
*DON'TDOIT!
*/letbtns=document.querySelectorAll('.click-me');
for(letbtnofbtns){btn.addEventListener('click',function(event){console.log(event);//Theeventdetailsconsole.log(event.target);//Theclickedelement
});
}
Yes,itisactuallybetterforperformancetolistentoallclicksonthedocumentthanhaveabunchofindividualeventlisteners.
Asasidebenefit,youcandynamicallyloadmatchingelementstotheDOMaftertheeventlistenerisalreadysetupanditwillstillwork.
Capturingeventsthatdon’tbubble
Certainevents,likefocus,don’tbubble.Inordertouseevent
delegationwitheventsthatdon’tbubble,youcansetanoptionalthirdargumentontheEventTarget.addEventListener()method,called
useCapture,totrue.
//Listenforclicksontheentirewindowdocument.addEventListener('click',function(event){
//Iftheclickedelementhasthe`.click-me`class,it'samatch!
if(event.target.matches('.click-me')){
//Dosomething...
}
});
YoucandetermineifuseCaptureshouldbesettotrueor
falsebylookingattheeventdetailspageontheMozilla
DeveloperNetwork(likethisoneforthefocusevent).
IfBubblesinthechartatthetopofthepageis“No,”youneedtosetuseCapturetotruetouseeventdelegation.
MultipleEvents
InvanillaJavaScript,eacheventtyperequiresit’sowneventlistener.Unfortunately,youcan’tpassinmultipleeventstoasinglelistenerlikeyoumightinjQueryandotherframeworks.
//Listenforallfocuseventsinthedocumentdocument.addEventListener('focus',function(event){//Runfunctionswheneveranelementinthedocumentcomesintofocus
},true);
Instead,createanamedfunctionandpassthatintoyoureventlistener.Thisletsyouavoidwritingthesamecodeoverandoveragain,andkeepsyourcodemoreDRY.
Fornamedcallbackfunctions,donotincludetheparentheses(())onthefunction.
Theeventobjectisautomaticallypassedinasanargument.
Youcandeterminewhichtypeofeventtriggeredthecallbackfunctionwiththeevent.typeproperty.
/**
*Thiswon'twork!
*/window.addEventListener('click,scroll',function(event){
console.log(event);//Theeventdetailsconsole.log(event.target);//Theclickedelement
});
//Setupourfunctiontorunonvariousevents
functionlogTheEvent(event){console.log('Thefollowingeventhappened:'+event.type);
}
//Addoureventlistenersdocument.addEventListener('click',logTheEvent);window.addEventListener('scroll',logTheEvent);
Puttingitalltogether
Tomakethisalltangible,let’sworkonaprojecttogether.We’llbuildascriptthatlet’suserstogglethevisibilityofpasswordfieldsinaform.
GettingSetup
Thetemplatehassomestartingmarkup:aformwithtwopasswordfieldsandsomebuttons.
Intheform,there’sabuttonwiththe[data-password]
attributethatwillbeusedtotogglethepasswordfieldvisibility.Thatbuttonhastwoadditionalattributes.
The[type="button"]attributepreventsthebuttonfrom
submittingtheformwhenclicked.
I’vealsoaddedsomedefaultCSSsothatwecanfocusontheJavaScript.
Alright,let’sgetstarted.
Listeningforclicks
Thefirstthingwewanttodoisdetectclicksonour[data-
password]button.Let’susethe
document.querySelector()methodtogetbuttonandsave
ittothetogglevariable.
<form>
<labelfor="current-pw">CurrentPassword</label>
<inputtype="password"id="current-pw">
<labelfor="new-pw">NewPassword</label>
<inputtype="password"id="new-pw">
<p><buttontype="button"data-password>ShowPasswords</button></p>
<p><button>ChangePassword</button></p>
</form>
Next,we’llusetheaddEventListener()methodtolistenfor
clickeventsonit,anddothingswhenthebuttonisclicked.
Determiningwhethertoshoworhidepasswords
Whenthebuttonisclicked,weneedtodetermineifweshouldshoworhidepasswords.Onesimplewaytodothatistocheckifthebuttoniscurrentlyselectedornot.
The[aria-pressed]attributeisusedtotellscreenreaders
(softwarethatpeoplewithvisualimpairmentsusetointeractwithwebpages)ifastate-basedbuttonlikethisoneispressed
//Getthepasswordtogglelettoggle=document.querySelector('[data-password]');
//Getthepasswordtogglelettoggle=document.querySelector('[data-password]');
//Listenforclicksonthetogglebuttontoggle.addEventListener('click',function(event){
//Dostuff...
});
ornot.It’sexactlywhatweneed!
The[aria-pressed]attributehasavalueoftruewhenthe
buttonisselected,andfalsewhenit’snot.Let’sstartby
addingittoourbutton.
Insideoureventlistener’scallbackfunction,wecancheckthevalueofthe[aria-pressed]attributetodetermineifthe
buttoniscurrentlyactiveornot.
We’llusetheevent.targettogetthebuttonthattriggered
theclickevent.Wecoulduseourtogglevariable,butIwant
toshowthedifferentoptionsyouhave.
We’llusethegetAttribute()methodtogetthevalueof
[aria-pressed].
<buttontype="button"data-passwordaria-pressed="false">ShowPasswords</button>
//Listenforclicksonthetogglebuttontoggle.addEventListener('click',function(event){
//Getthevalueofthe[aria-pressed]attributeletpressed=event.target.getAttribute('aria-pressed');
});
Next,we’llusethestrictequalsoperator(===)tocheckif
pressedhasavalueoffalse.
Ifitdoes,weneedtoshowthepasswordfieldsandupdatethevalueof[aria-pressed]totrue.Ifnot,weneedtohidethe
fieldsandchangethevaluetofalse.
WecanusethesetAttribute()methodtosetthe[aria-
pressed]attribute.
Showingandhidingpasswords
//Listenforclicksonthetogglebuttontoggle.addEventListener('click',function(event){
//Getthevalueofthe[aria-pressed]attributeletpressed=event.target.getAttribute('aria-pressed');
//Ifbuttonisn'tpressedyet,pressitandshowfields//Otherwise,unpressitandhidethefields
if(pressed==='false'){event.target.setAttribute('aria-pressed','true');
//Showthefields...
}else{event.target.setAttribute('aria-pressed','false');
//Hidethefields...
}
});
Now,we’rereadytoactuallyshowandhideourpasswordfields.Let’susethedocument.querySelectorAll()methodto
getallfieldswiththe[type="password"]attribute.
We’lluseafor...oflooptoloopthrougheachofour
fields,andthetypepropertytoupdatethefieldtypeas
needed.
Ifthepasswordshouldbevisible,we’llchangethetypeto
text.Ifitshouldbehidden,we’llchangeitbacktopassword.
//Getthepasswordtoggleandpasswordfieldslettoggle=document.querySelector('[data-password]');letfields=document.querySelectorAll('[type="password"]');
//Listenforclicksonthetogglebuttontoggle.addEventListener('click',function(event){
//Getthevalueofthe[aria-pressed]attributeletpressed=event.target.getAttribute('aria-pressed');
//Ifbuttonisn'tpressedyet,pressitandshowfields//Otherwise,unpressitandhidethefields
if(pressed==='false'){event.target.setAttribute('aria-pressed','true');
for(letfieldoffields){
field.type='text';
}
}else{event.target.setAttribute('aria-pressed','false');
for(letfieldoffields){
field.type='password';
}
}
});
Now,thepasswordswillshoworhidebasedonthebuttonstate.
There’sjustonelastthingtodo:stylethebuttonsouserscanvisuallytellifit’sselectedornot.
Stylingthebutton
OnereallycoolthingaboutattributesisthattheycanbeusedtostyleelementsjustlikeclassesandIDs.
Sincethe[aria-pressed]attributealreadyholds
informationaboutwhetherornotthebuttonisselected,itmakessensetouseittostylethebuttonvisuallyaswell.Let’sgiveabluebackgroundwithwhitetextwhenit’sactive.
Congratulations!YoujustcreatedashowpasswordscriptusingavarietyofDOMmanipulationtechniques.
/**
*ActiveButtonStyle
*/
[aria-pressed="true"]{
background-color:#0088cc;
color:#ffffff;
}
AbouttheAuthor
Hi,I’mChrisFerdinandi.Ibelievethere’sasimpler,moreresilientwaytomakethingsfortheweb.
I’mtheauthoroftheVanillaJSPocketGuideseries,creatoroftheVanillaJSAcademytrainingprogram,andhostoftheVanillaJSPodcast.Mydevelopertipsnewsletterisreadbythousandsofdeveloperseachweekday.
Ilovepirates,puppies,andPixarmovies,andlivenearhorsefarmsinruralMassachusetts.
Youcanfindme:
OnmywebsiteatGoMakeThings.com.Byemailatchris@gomakethings.com.OnTwitterat@ChrisFerdinandi.