DOM-manipulation i ren JavaScript. DOM-tekniker: Föräldrar, barn och grannar infogar innehåll i DOM

Vanligtvis använder utvecklare jQuery när de behöver göra något med DOM. Men nästan alla DOM-manipulationer kan göras i ren JavaScript med hjälp av dess DOM API.

Låt oss titta på detta API mer detaljerat:

I slutändan kommer du att skriva ditt eget enkla DOM-bibliotek som kan användas i alla projekt.

DOM-frågor

DOM-frågor utförs med metoden .querySelector(), som tar en godtycklig CSS-väljare som argument.

Const myElement = document.querySelector("#foo > div.bar")

Det kommer att returnera det första matchande elementet. Du kan göra tvärtom - kontrollera om elementet matchar väljaren:

MyElement.matches("div.bar") === sant

Om du vill få alla element som matchar en väljare, använd följande konstruktion:

Const myElements = document.querySelectorAll(".bar")

Om du vet vilket föräldraelement du vill referera till kan du helt enkelt söka igenom dess underordnade element istället för att söka igenom hela koden:

Const myChildElemet = myElement.querySelector("input") // Istället för: // document.querySelector("#foo > div.bar input")

Frågan uppstår: varför då använda andra, mindre bekväma metoder som .getElementsByTagName() ? Det finns ett litet problem - utdata från .querySelector() uppdateras inte, och när vi lägger till ett nytt element (se ), kommer det inte att ändras.

Const elements1 = document.querySelectorAll("div") const elements2 = document.getElementsByTagName("div") const newElement = document.createElement("div") document.body.appendChild(newElement) elements1.length === elements2.length // falskt

Även querySelectorAll() samlar allt i en lista, vilket gör det inte särskilt effektivt.

Hur arbetar man med listor?

Utöver det har .querySelectorAll() två små varningar. Du kan inte bara anropa metoder på resultaten och förvänta dig att de ska tillämpas på var och en (som du kanske är van vid att göra med jQuery). I vilket fall som helst måste du iterera igenom alla element i en slinga. För det andra är det returnerade objektet en lista med element, inte en array. Därför kommer arraymetoder inte att fungera. Naturligtvis finns det metoder för listor, något som .forEach() , men tyvärr är de inte lämpliga för alla fall. Så det är bättre att konvertera listan till en array:

// Använda Array.from() Array.from(myElements).forEach(doSomethingWithEachElement) // Eller arrayprototyp (pre-ES6) Array.prototype.forEach.call(myElements, doSomethingWithEachElement) // Enklare: .forEach.call( myElements , doSomethingWithEachElement)

Varje element har några egenskaper som refererar till en "familj".

MyElement.children myElement.firstElementChild myElement.lastElementChild myElement.previousElementSibling myElement.nextElementSibling

Eftersom elementgränssnittet ärvs från nodgränssnittet finns följande egenskaper också:

MyElement.childNodes myElement.firstChild myElement.lastChild myElement.previousSibling myElement.nextSibling myElement.parentNode myElement.parentElement

De första egenskaperna hänvisar till elementet, och de sista (med undantag för .parentElement) kan vara listor med element av vilken typ som helst. Följaktligen kan du kontrollera elementtypen:

MyElement.firstChild.nodeType === 3 // detta element kommer att vara en textnod

Lägga till klasser och attribut

Lägg till ny klass väldigt enkelt:

MyElement.classList.add("foo") myElement.classList.remove("bar") myElement.classList.toggle("baz")

Att lägga till en egenskap till ett element är detsamma som för alla objekt:

// Hämta attributvärdet const value = myElement.value // Ställ in attributet som en elementegenskap myElement.value = "foo" // Для установки нескольких свойств используйте.Object.assign() Object.assign(myElement, { value: "foo", id: "bar" }) // Удаление атрибута myElement.value = null !}

Du kan använda metoderna .getAttibute(), .setAttribute() och .removeAttribute() . De kommer omedelbart att ändra elementets HTML-attribut (i motsats till DOM-egenskaper), vilket kommer att orsaka en omrendering av webbläsaren (du kan se alla ändringar genom att undersöka elementet med webbläsarens utvecklarverktyg). Sådana omritningar kräver inte bara mer resurser än att ställa in DOM-egenskaper, utan kan också leda till oväntade fel.

Vanligtvis används de för element som inte har motsvarande DOM-egenskaper, till exempel colspan . Eller om deras användning verkligen är nödvändig, till exempel för HTML-egenskaper vid ärvning (se).

Lägga till CSS-stilar

De läggs till på samma sätt som andra egenskaper:

MyElement.style.marginLeft = "2em"

Vissa specifika egenskaper kan ställas in med .style , men om du vill få värdena efter några beräkningar är det bättre att använda window.getComputedStyle() . Den här metoden tar emot ett element och returnerar en CSSStyleDeclaration som innehåller stilarna för både själva elementet och dess överordnade:

Window.getComputedStyle(myElement).getPropertyValue("marginal-left")

Ändra DOM

Du kan flytta element:

// Lägg till element1 som det sista underordnade av element2 element1.appendChild(element2) // Infoga element2 som underordnat element till element1 före element3 element1.insertBefore(element2, element3)

Om du inte vill flytta den, men behöver infoga en kopia, använd:

// Skapa en klon const myElementClone = myElement.cloneNode() myParentElement.appendChild(myElementClone)

Metoden .cloneNode() tar ett booleskt värde som ett argument, och om det är sant, klonas även underordnade element.

Naturligtvis kan du skapa nya element:

Const myNewElement = document.createElement("div") const myNewTextNode = document.createTextNode("viss text")

Och sätt sedan in dem som visas ovan. Du kan inte ta bort ett element direkt, men du kan göra det genom det överordnade elementet:

MyParentElement.removeChild(myElement)

Du kan även kontakta indirekt:

MyElement.parentNode.removeChild(myElement)

Metoder för element

Varje element har egenskaper som .innerHTML och .textContent , de innehåller HTML-kod och följaktligen själva texten. Följande exempel ändrar innehållet i ett element:

// Ändra HTML-koden myElement.innerHTML = ` Nytt innehåll ( el.addEventListener("change", function (event) ( console.log(event.target.value) )) ))

Förhindra standardåtgärder

För att göra detta, använd metoden .preventDefault(), som blockerar standardåtgärder. Till exempel kommer det att blockera inlämning av formulär om auktorisering på klientsidan inte lyckades:

MyForm.addEventListener("submit", funktion (event) ( const name = this.querySelector("#name") if (name.value === "Donald Duck") { alert("You gotta be kidding!") event.preventDefault() } }) !}

Metoden .stopPropagation() kommer att hjälpa om du har en specifik händelsehanterare tilldelad till ett underordnat element och en andra händelsehanterare tilldelad till föräldern för samma händelse.

Som nämnts tidigare tar metoden .addEventListener() ett valfritt tredje argument i form av ett konfigurationsobjekt. Detta objekt måste innehålla någon av följande booleska egenskaper (alla inställda på false som standard):

  • capture: händelsen kommer att kopplas till detta element före något annat element nedan i DOM;
  • en gång: en händelse kan endast tilldelas en gång;
  • passiv: event.preventDefault() kommer att ignoreras (undantag vid fel).

Den vanligaste egenskapen är .capture , och den är så vanlig att det finns en genväg för den: istället för att skicka den i ett konfigurationsobjekt, skicka bara dess värde här:

MyElement.addEventListener(typ, lyssnare, sant)

Hanterare tas bort med metoden .removeEventListener() som tar två argument: händelsetypen och en referens till hanteraren som ska tas bort. Till exempel kan once-egenskapen implementeras så här:

MyElement.addEventListener("change", function listener (event) ( console.log(event.type + " triggades på " + this) this.removeEventListener("change", listener) ))

Arv

Låt oss säga att du har ett element och du vill lägga till en händelsehanterare för alla dess barn. Sedan måste du gå igenom dem med metoden myForm.querySelectorAll("input") som visas ovan. Du kan dock helt enkelt lägga till element i formuläret och kontrollera deras innehåll med hjälp av event.target .

MyForm.addEventListener("ändring", funktion (händelse) ( const target = event.target if (target.matches("input")) ( console.log(target.value) ) ))

Och en annan fördel med denna metod är att hanteraren automatiskt kopplas till nya underordnade element.

Animation

Det enklaste sättet att lägga till animering är att använda CSS med övergångsegenskapen. Men för större flexibilitet (till exempel för spel) är JavaScript bättre lämpat.

Att anropa metoden window.setTimeout() tills animeringen slutar är inte en bra idé, eftersom din applikation kan låsa sig, särskilt på Mobil enheter. Det är bättre att använda window.requestAnimationFrame() för att spara alla ändringar till nästa omritning. Det tar en funktion som ett argument, som i sin tur får en tidsstämpel:

Const start = window.performance.now() const duration = 2000 window.requestAnimationFrame(funktion fadeIn (nu)) ( const progress = now - start myElement.style.opacity = progress / varaktighet if (progress< duration) { window.requestAnimationFrame(fadeIn) } }

På så sätt uppnås mycket smidig animering. I sin artikel diskuterar Mark Brown detta ämne.

Att skriva vårt eget bibliotek

Det faktum att man i DOM måste iterera över element hela tiden för att utföra några operationer på element kan verka ganska tråkigt jämfört med jQuerys $(".foo").css((färg: "röd")) syntax. Men varför inte skriva några av dina egna metoder för att göra denna uppgift enklare?

Const $ = funktion $ (väljare, sammanhang = dokument) ( const elements = Array.from(context.querySelectorAll(selector)) return ( elements, html (newHtml) ( this.elements.forEach(element => ( element.innerHTML = newHtml )) returnera detta ), css (newCss) ( this.elements.forEach(element => ( Object.assign(element.style, newCss) )) returnera detta ), på (händelse, hanterare, optioner) ( this.elements .forEach(element => ( element.addEventListener(event, handler, options) )) returnera detta ) ) )

Pseudokod

$(".rightArrow").click(function() (rightArrowParents = this.dom(); //.dom(); är pseudofunktionen ... den ska visa hela varningen(rightArrowParents); ));

Larmmeddelandet kommer att vara:

body div.lol a.rightArrow

Hur kan jag få detta med javascript/jquery?

Att använda jQuery så här (följt av en lösning som inte använder jQuery förutom händelsen, mycket färre funktionsanrop om det är viktigt):

Exempel i realtid:

$(".rightArrow").click(function() ( var rightArrowParents = ; $(this).parents().addBack().not("html").each(function() ( var entry = this.tagName .toLowerCase(); if (this.className) (post += "." + this.className.replace(/ /g, "."); ) rightArrowParents.push(entry); )); alert(rightArrowParents.join (" ")); return false; )); Klicka här

(I live-exemplen uppdaterade jag class-attributet på div till att vara lol multi för att demonstrera hantering av flera klasser.)

Här är en lösning för exakt matchning av element.

Det är viktigt att förstå att väljaren (är inte verklig) som kromverktyg visar inte identifierar elementet i DOM unikt. ( till exempel kommer den inte att skilja mellan en lista med på varandra följande span-element. Ingen positionerings-/indexeringsinformation)

$.fn.fullSelector = function () ( var path = this.parents().addBack(); var quickCss = path.get().map(function (item) ( var self = $(item), id = item .id ? "#" + item.id: "", clss = item.classList.length ? item.classList.toString().split(" ").map(function (c) (retur "." + c; )).join("") : "", name = item.nodeName.toLowerCase(), index = self.siblings(name).length ? ":nth-child(" + (self.index() + 1) + ")" : ""; if (namn === "html" || namn === "kropp") ( returnera namn; ) returnera namn + index + id + clss; )).join(" > ") ; returnera quickCss; );

Och du kan använda den så här:

Console.log($("någon väljare").fullVäljare());

Jag flyttade en bit från T.J. Trångt till liten jQuery plugin. Jag använde jQuery-versionen, även om han har rätt i att det är helt onödigt overhead, men jag använder det bara för felsökningsändamål så jag bryr mig inte.

Användande:

Kapslad span Enkel span Pre

// resultat (array): ["body", "div.sampleClass"] $("span").getDomPath(false) // resultat (sträng): body > div.sampleClass $("span").getDomPath( ) // resultat (array): ["body", "div#test"] $("pre").getDomPath(false) // resultat (sträng): body > div#test $("pre").getDomPath ()

Var obj = $("#show-editor-button"), sökväg = ""; while (typeof obj.prop("tagName") != "odefinierad")( if (obj.attr("klass"))( sökväg = "."+obj.attr("klass").replace(/\s /g , "."") + sökväg; ) if (obj.attr("id"))( sökväg = "#"+obj.attr("id") + sökväg; ) sökväg = " " +obj.prop( "tagName").toLowerCase() + sökväg; obj = obj.parent(); ) console.log(sökväg);

hej den här funktionen löser felet relaterat till att det aktuella elementet inte visas i sökvägen

Kolla in det nu

$j(".wrapper").click(function(event) ( selectedElement=$j(event.target); var rightArrowParents = ; $j(event.target).parents().not("html,body") .each(function() ( var entry = this.tagName.toLowerCase(); if (this.className) (entry += "." + this.className.replace(/ /g, "."); )else if (this.id)( entry += "#" + this.id; ) entry=replaceAll(entry,.","."); rightArrowParents.push(entry); )); rightArrowParents.reverse(); // if(event.target.nodeName.toLowerCase()=="a" || event.target.nodeName.toLowerCase()=="h1")( var entry = event.target.nodeName.toLowerCase(); if (händelse .target.className) ( entry += "." + event.target.className.replace(/ /g, "."); )else if(event.target.id)( entry += "#" + event. target.id; ) rightArrowParents.push(entry); // )

Där $j = jQuery Variable

även lösa problemet med .. i klassnamn

här är ersättningsfunktionen:

Funktion escapeRegExp(str) ( return str.replace(/([.*+?^=!:$()()|\[\]\/\\])/g, "\\$1"); ) funktion replaceAll(str, find, replace) ( return str.replace(new RegExp(escapeRegExp(find), "g"), replace); )

Här är en inbyggd JS-version som returnerar jQuery-sökvägen. Jag lägger även till ID för elementen om de finns. Detta ger dig möjlighet att ta den kortaste vägen om du ser id:t i arrayen.

Var sökväg = getDomPath(element); console.log(path.join(" > "));

Body > section:eq(0) > div:eq(3) > section#content > section#firehose > div#firehoselist > article#firehose-46813651 > header > h2 > span#title-46813651

Här är funktionen.

Funktion getDomPath(el) ( var stack = ; while (el.parentNode != null) ( console.log(el.nodeName); var sibCount = 0; var sibIndex = 0; for (var i = 0; i< el.parentNode.childNodes.length; i++) { var sib = el.parentNode.childNodes[i]; if (sib.nodeName == el.nodeName) { if (sib === el) { sibIndex = sibCount; } sibCount++; } } if (el.hasAttribute("id") && el.id != "") { stack.unshift(el.nodeName.toLowerCase() + "#" + el.id); } else if (sibCount >1) ( stack.unshift(el.nodeName.toLowerCase() + ":eq(" + sibIndex + ")"); ) else ( stack.unshift(el.nodeName.toLowerCase()); ) el = el.parentNode ; ) returnera stack.slice(1); // tar bort html-elementet)

Komplexa och tunga webbapplikationer har blivit vanliga i dessa dagar. Korswebbläsare och lättanvända bibliotek som jQuery med sin rika funktionalitet kan till stor del hjälpa till att manipulera DOM i farten. Därför är det inte förvånande att många utvecklare använder sådana bibliotek oftare än att arbeta med den inbyggda DOM API, som det fanns många problem med. Även om webbläsarskillnader fortfarande är ett problem, är DOM i bättre form nu än för 5-6 år sedan när jQuery blev populärt.

I den här artikeln kommer jag att demonstrera HTML-manipuleringsfunktionerna i DOM, med fokus på relationer mellan föräldrar, barn och grannar. Avslutningsvis kommer jag att ge information om webbläsarstöd för dessa funktioner, men kom ihåg att ett bibliotek som jQuery fortfarande är ett bra alternativ på grund av förekomsten av buggar och inkonsekvenser i implementeringen av inbyggd funktionalitet.

Räknar barnnoder

För demonstration kommer jag att använda följande HTML-uppmärkning, kommer vi att ändra det flera gånger i artikeln:

  • Exempel ett
  • Exempel två
  • Exempel tre
  • Exempel fyra
  • Exempel fem
  • Exempel sex

Var myList = document.getElementById("minList"); console.log(myList.children.length); // 6 console.log(myList.childElementCount); // 6

Som du kan se är resultaten desamma, även om teknikerna som används är olika. I det första fallet använder jag barnens egendom. Detta är en skrivskyddad egenskap, den returnerar en samling HTML-element, placerad inuti det begärda elementet; För att räkna deras antal använder jag egenskapen length för den här samlingen.

I det andra exemplet använder jag metoden childElementCount, som jag tycker är ett snyggare och potentiellt mer underhållbart sätt (diskutera detta mer senare, jag tror inte att du kommer att ha problem med att förstå vad den gör).

Jag skulle kunna försöka använda childNodes.length (istället för children.length), men titta på resultatet:

Var myList = document.getElementById("minList"); console.log(myList.childNodes.length); // 13

Den returnerar 13 eftersom childNodes är en samling av alla noder, inklusive mellanslag - tänk på detta om du bryr dig om skillnaden mellan underordnade noder och underordnade elementnoder.

Kontrollera förekomsten av barnnoder

För att kontrollera om ett element har underordnade noder kan jag använda metoden hasChildNodes() . Metoden returnerar ett booleskt värde som indikerar deras närvaro eller frånvaro:

Var myList = document.getElementById("minList"); console.log(myList.hasChildNodes()); // Sann

Jag vet att min lista har underordnade noder, men jag kan ändra HTML så att det inte finns några; Markeringen ser nu ut så här:

Och här är resultatet av att köra hasChildNodes() igen:

Console.log(myList.hasChildNodes()); // Sann

Metoden returnerar fortfarande sant. Även om listan inte innehåller några element, innehåller den ett mellanslag, vilket är en giltig nodtyp. Den här metoden tar hänsyn till alla noder, inte bara elementnoder. För att hasChildNodes() ska returnera false måste vi ändra uppmärkningen igen:

Och nu visas det förväntade resultatet i konsolen:

Console.log(myList.hasChildNodes()); // falskt

Naturligtvis, om jag vet att jag kan stöta på blanksteg, ska jag först kontrollera om det finns underordnade noder och sedan använda egenskapen nodeType för att avgöra om det finns några elementnoder bland dem.

Lägga till och ta bort barnelement

Det finns tekniker du kan använda för att lägga till och ta bort element från DOM. Den mest kända av dem är baserad på en kombination av metoderna createElement() och appendChild().

Var myEl = document.createElement("div"); document.body.appendChild(myEl);

I det här fallet skapar jag med metoden createElement() och lägger sedan till den i kroppen . Det är väldigt enkelt och du har förmodligen använt den här tekniken förut.

Men istället för att sätta in specifikt element som skapas, Jag kan också använda appendChild() och bara flytta det befintliga elementet. Låt oss säga att vi har följande uppmärkning:

  • Exempel ett
  • Exempel två
  • Exempel tre
  • Exempel fyra
  • Exempel fem
  • Exempel sex

Exempeltext

Jag kan ändra platsen för listan med följande kod:

Var myList = document.getElementById("minList"), container = document.getElementById("c"); container.appendChild(minlista);

Den slutliga DOM kommer att se ut så här:

Exempeltext

  • Exempel ett
  • Exempel två
  • Exempel tre
  • Exempel fyra
  • Exempel fem
  • Exempel sex

Lägg märke till att hela listan har tagits bort från sin plats (ovanför stycket) och sedan infogats efter den innan den avslutande texten . Även om metoden appendChild() vanligtvis används för att lägga till element skapade med createElement() , kan den också användas för att flytta befintliga element.

Jag kan också helt ta bort ett underordnat element från DOM med removeChild() . Så här tar du bort vår lista från föregående exempel:

Var myList = document.getElementById("minList"), container = document.getElementById("c"); container.removeChild(minlista);

Elementet har nu tagits bort. Metoden removeChild() returnerar det borttagna elementet så att jag kan spara det om jag skulle behöva det senare.

Var myOldChild = document.body.removeChild(myList); document.body.appendChild(myOldChild);

Det finns också en ChildNode.remove()-metod som relativt nyligen lades till i specifikationen:

Var myList = document.getElementById("minList"); myList.remove();

Den här metoden returnerar inte fjärrobjektet och fungerar inte i IE (endast Edge). Och båda metoderna tar bort textnoder på samma sätt som elementnoder.

Byte av underordnade element

Jag kan ersätta ett befintligt underordnat element med ett nytt, oavsett om det nya elementet finns eller om jag skapat det från början. Här är markeringen:

Exempeltext

Var myPar = document.getElementById("par"), myDiv = document.createElement("div"); myDiv.className = "exempel"; myDiv.appendChild(document.createTextNode("Ny elementtext")); document.body.replaceChild(myDiv, myPar);

Ny elementtext

Som du kan se tar metoden replaceChild() två argument: det nya elementet och det gamla elementet det ersätter.

Jag kan också använda den här metoden för att flytta ett befintligt element. Ta en titt på följande HTML:

Exempeltext 1

Exempeltext 2

Exempeltext 3

Jag kan ersätta det tredje stycket med det första stycket med följande kod:

Var myPar1 = document.getElementById("par1"), myPar3 = document.getElementById("par3"); document.body.replaceChild(myPar1, myPar3);

Nu ser den genererade DOM ut så här:

Exempeltext 2

Exempeltext 1

Att välja ut specifika barn

Det finns flera olika sätt välja ett specifikt element. Som visats tidigare kan jag börja med att använda barnsamlingen eller egenskapen childNodes. Men låt oss titta på andra alternativ:

Egenskaperna firstElementChild och lastElementChild gör precis vad deras namn antyder att de gör: välj de första och sista underordnade elementen. Låt oss återgå till vår uppmärkning:

  • Exempel ett
  • Exempel två
  • Exempel tre
  • Exempel fyra
  • Exempel fem
  • Exempel sex

Jag kan välja de första och sista elementen med dessa egenskaper:

Var myList = document.getElementById("minList"); console.log(myList.firstElementChild.innerHTML); // "Exempel ett" console.log(myList.lastElementChild.innerHTML); // "Exempel sex"

Jag kan också använda egenskaperna previousElementSibling och nextElementSibling om jag vill välja andra underordnade element än det första eller sista. Detta görs genom att kombinera egenskaperna firstElementChild och lastElementChild:

Var myList = document.getElementById("minList"); console.log(myList.firstElementChild.nextElementSibling.innerHTML); // "Exempel två" console.log(myList.lastElementChild.previousElementSibling.innerHTML); // "Exempel fem"

Det finns också liknande egenskaper firstChild , lastChild , previousSibling och nextSibling , men de tar hänsyn till alla typer av noder, inte bara element. Generellt sett är egenskaper som endast tar hänsyn till elementnoder mer användbara än de som väljer alla noder.

Infogar innehåll i DOM

Jag har redan tittat på sätt att infoga element i DOM. Låt oss gå vidare till liknande ämne och ta en titt på de nya alternativen för att infoga innehåll.

För det första finns det en enkel insertBefore()-metod, ungefär som replaceChild(), den tar två argument och fungerar med både nya och befintliga. Här är markeringen:

  • Exempel ett
  • Exempel två
  • Exempel tre
  • Exempel fyra
  • Exempel fem
  • Exempel sex

Exempel stycke

Lägg märke till stycket jag ska ta bort först och sedan infoga det före listan, allt i ett svep:

Var myList = document.getElementById("minList"), container = document.getElementBy("c"), myPar = document.getElementById("par"); container.insertBefore(myPar, myList);

I den resulterande HTML-koden kommer stycket att visas före listan och detta är ett annat sätt att linda elementet.

Exempel stycke

  • Exempel ett
  • Exempel två
  • Exempel tre
  • Exempel fyra
  • Exempel fem
  • Exempel sex

Precis som replaceChild() tar insertBefore() två argument: elementet som ska läggas till och elementet innan vi vill infoga det.

Denna metod är enkel. Låt oss nu prova en mer kraftfull insättningsmetod: metoden insertAdjacentHTML() .