Skip to main content

Document

Browser environment, specs

ภาษา JavaScript ถูกสร้างมาเพื่อใช้งานกับ web browsers.

ตั้งแต่นั้นเป็นต้นมา ภาษานี้ได้พัฒนาเป็นภาษาที่มีการใช้งานและแพลตฟอร์มมากมาย.

แพลตฟอร์มที่กล่าวถึง อาจเป็น Browser, web-server, host หรือตู้กาแฟอัจฉริยะ ก็สามารถใช้ JavaScript ได้. โดยแพลตฟอร์มแต่ละอัน ก็มีฟังก์ชั่นการใช้งานที่แตกต่างกันไป และ จะถูกกำหนดด้วย ข้อกำหนดของ JavaScript หรือเรียกว่า host environment

โดย host environment ก็จะจัดเตรียมฟังก์ชั่น ที่นอกเหนือจากภาษาของมัน เพื่อใช้สำหรับแพลตฟอร์มต่าง ๆ

เช่น Web browsers ก็จะมีฟังก์ชั่นที่ใช้ควบคุมหน้าเว็บ, Node.js ก็จะมีฟังก์ชั่นสำหรับ Server และอื่น ๆ

และนี่คือมุมมองจากด้านบน เวลาเรามอง JavaScript run ใน Web-browser

image.png

JavaScript จะมี Object ที่เป็นพื้นฐานของทุกอย่าง เรียกว่า window มีหน้าที่หลักอยู่ 2 อย่างคือ

  1. เป็น Global Object ของ JavaScript
  2. จะแสดงถึง "browser window" และจะมี method เพื่อควบคุมมัน

ตัวอย่างเช่น เราสามารถใช้ฟังก์ชั่น เหมือนการเรียกใช้ method ผ่าน window ได้:

function sayHi() { 
alert("Hello");
}

// global functions are methods of the global object:
window.sayHi();

และเราสามารถใช้ window เพื่อแสดงความสูงของ Web-browser ได้

alert(window.innerHeight); // inner window height

และยังมี method อื่น ๆ อีก ที่เราจะมาสอนกันทีหลัง


DOM (Document Object Model)

Document Object Model หรือ DOM แสดงถึง Content ของหน้าทั้งหมด ออกมาในรูปแบบของ Object ที่สามารถแก้ไขได้

ตัวอย่างเช่น:

// change the background color to red 
document.body.style.background = "red";
// change it back after 1 second
setTimeout(() => document.body.style.background = "", 1000);

เราใช้ document.body.style ในการตกแต่งพื้นหลัง แต่ยังมี method หรือ property อีกมาก ที่เราจะค่อยพูดถึงกันในตอนต่อ ๆ ไป


BOM (Browser Object Model)

Browser Object Model (BOM) แสดงถึง Object เพิ่มเติมที่เบราว์เซอร์ (host environment) มอบให้สำหรับการทำงานกับทุกสิ่ง ยกเว้นกับ Document

ตัวอย่างเช่น:

  1. Object Navigator จะให้ข้อมูลพื้นหลังของ browser และ ระบบปฏิบัติการ (operating system) โดยมีหลาย Properties, แต่มี 2 ตัวที่ใช้กันมาก คือ navigator.userAgent - เกี่ยวกับ browser ที่ใช้อยู่ และ navigator.platform - บอกเกี่ยวกับ แพลตฟอร์ม (Windows/Linux/Mac etc.)
  2. Object Location อนุญาตให้เราเข้าถึง URL ปัจจุบัน และ redirect ไปที่ URL ใหม่ได้

วิธีที่เราสามารถใช้ Object location ได้:

alert(location.href); // shows current URL
if (confirm("Go to Wikipedia?")) {
location.href = "https://wikipedia.org"; // redirect the browser to another URL
}

ฟังก์ชั่น alert/confirm/prompt ก็เป็นส่วนหนึ่งของ BOM เหมือนกัน


Summary

พื้นฐานที่เรามี:

DOM specification

อธิบายเกี่ยวกับ โครงสร้างของ Document, การควบคุม และ event https://dom.spec.whatwg.org.

CSSOM specification

อธิบายเกี่ยวกับ stylesheets และ style rules, การควบคุมการตกแต่ง https://www.w3.org/TR/cssom-1/

HTML specification

อธิบายเกี่ยวกับ ภาษา HTML (tags etc) และ BOM (broswser object model) - ฟังก์ชั่นของ broswer

setTimeout, alert, location https://html.spec.whatwg.org. เราสามารถดู properties และ methods

ของ BOM ได้ที่นี่


Walking the DOM

DOM อนุญาตให้เราทำทุกสิ่งกับ element และ Contents ได้,

แต่สิ่งแรกที่เราต้องทำคือ เข้าถึงไปที่ Object DOM

ทุกการ Operations บน DOM เริ่มจาก Object document , ซึ่งเป็นจุดหลักในการเข้าถึงไปที่ DOM

จาก document เราสามารถเข้าถึงได้ทุก Node

นี่คือรูปของ link ที่เชื่อมกันระหว่าง Node:

image.png

ด้านบน: DocumentElement และ Body

Node บนสุด สามารถใช้ได้ผ่าน Object document :

<html> = document.documentElement

บนสุดของ Node document คือ document.documentElement นั่นคือ DOM node ของ แท็ก <html>

<body> = document.body

DOM Node อีกตัวหนึ่ง ที่ถูกใช้บ่อยคือ <body> เข้าถึงโดย– document.body.

<head> = document.head

แท็ก<head> เข้าถึงโดย document.head.


Children: childNodes, firstChild, lastChild

มีสองคำที่เราจะใช้ต่อจากนี้

  • Child nodes (or children) – element ที่เป็นลูกโดยตรง กล่าวอีกแบบหนึ่งพวกมันซ้อนกันอยู่ ตัวอย่างเช่น <head> และ <body> เป็นลูกขององค์ประกอบ <html>
  • Descendants – element ทั้งหมดที่ซ้อนอยู่ในองค์ประกอบที่กำหนด รวมถึง children, children ของมันและอื่น ๆ

ตัวอย่างเช่น แท็ก <body> มี children <div> และ<ul> (และข้อความ):

<html>
<body>
<div>Begin</div>

<ul>
<li>
<b>Information</b>
</li>
</ul>
</body>
</html>

และ descendants ของ <body> ไม่เพียงแต่เป็น Children โดยตรงของ <div>, <ul> แต่ยังรวมถึง element ที่ซ้อนกันลึกกว่าด้วย, เช่น <li> (child ของ  <ul>) และ<b> (child ของ <li>)

คอลเลกชัน childNodes แสดงรายการ Node ย่อยทั้งหมด รวมถึง Node ข้อความ ตัวอย่างด้านล่างแสดง children ของ document.body

<html>
<body>
<div>Begin</div>

<ul>
<li>Information</li>
</ul>

<div>End</div>

<script>
for (let i = 0; i < document.body.childNodes.length; i++) {
alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ..., SCRIPT
}
</script>
...more stuff...
</body>
</html>

Properties firstChild and lastChild 

ให้เราเข้าถึง children ของตัวแรกและตัวท้ายได้ พวกมันคือ shorthands. ถ้า Node นั้นมีค่าจริง ดังนั้นมันจะเข้าถึงได้ตลอด

elem.childNodes[0] === elem.firstChild
elem.childNodes[elem.childNodes.length - 1] === elem.lastChild

ฟังก์ชั่นพิเศษ elem.hasChildNodes() เพื่อตรวจสอบว่ามี child nodes อยู่หรือไม่


DOM collections

อย่างที่เราเห็น childNodes เป็นเหมือนกัน Array แต่จริง ๆ แล้วมันไม่ใช่ Array แต่มันเป็น collection เป็น array แบบพิเศษ (iterable object)

มีผลกระทบที่สำคัญสองประการ:

  1. เราสามารถใช้ for..of เพื่อลูบเช็คได้

    for (let node of document.body.childNodes) {
    alert(node); // shows all nodes from the collection
    }

    เพราะว่ามันเป็น iterable

  2. Array methods ไม่สามารถใช้งานได้ เพราะว่ามันไม่ใช่ Array

    alert(document.body.childNodes.filter); // undefined (there's no filter method!)

    เพราะเราสามารถใช้ Array.from เพื่อสร้าง Array ของจริงได้จากคอลเลกชันได้ ถ้าเราต้องการใช้ Array methods

    alert( Array.from(document.body.childNodes).filter ); // function

Siblings and the parent

Siblings คือ Node ที่มี children เดียวกัน

ตัวอย่างเช่น, <head> และ <body> เป็น siblings:

<html>
<head>...</head><body>...</body>
</html>
  • <body> ให้พูดคือ “ถัดไป” หรือ “ขวา” sibling ของ <head>,
  • <head> ให้พูดคือ “ก่อน” or “ซ้าย” sibling ของ<body>.

sibling ถัดไปจะอยู่ใน property nextSibling และ sibling ก่อนหน้า จะอยู่ใน previousSibling

และ parent จะเรียกเข้าถึงได้โดยใช้ parentNode

ตัวอย่างเช่น:

// parent of <body> is <html>
alert( document.body.parentNode === document.documentElement ); // true

// after <head> goes <body>
alert( document.head.nextSibling ); // HTMLBodyElement

// before <body> goes <head>
alert( document.body.previousSibling ); // HTMLHeadElement

More links: tables

จนถึงตอนนี้เราได้อธิบายคุณสมบัติ navigation ขั้นพื้นฐานแล้ว

elements DOM บางประเภทอาจให้ properties เพิ่มเติมตามประเภทเพื่อความสะดวก

Tables เป็นตัวอย่างที่ดีในเรื่องนี้ :

element <Table> ซัพพอร์ต properties พวกนี้

  • table.rows – collection ของ elements  <tr> ใน table.
  • table.caption/tHead/tFoot – อ้างอิงถึง elements <caption>, <thead>, <tfoot>.
  • table.tBodies – collection ของ element <tbody

<tr>:

  • tr.cells – collection ของ <td> และ <th>  ภายใน element <tr>.
  • tr.sectionRowIndex – ตำแหน่งของ (index) <tr> ภายใน <thead>/<tbody>/<tfoot>.
  • tr.rowIndex – จำนวนของ <tr> ทั้งหมดใน table (รวม table rows ทั้งหมด).

<td> และ<th>:

  • td.cellIndex – จำนวนทั้งหมดของ<td> และ <th> ภายใน  <tr>.

ตัวอย่างการใช้งาน:

<table id="table">
<tr>
<td>one</td><td>two</td>
</tr>
<tr>
<td>three</td><td>four</td>
</tr>
</table>

<script>
// get td with "two" (first row, second column)
let td = table.rows[0].cells[1];
td.style.backgroundColor = "red"; // highlight it
</script>

Summary

ด้วย Node DOM เราสามารถไปที่ neighbors ใกล้เคียงได้โดยใช้ navigation properties

มี 2 ส่วนหลัก ๆ:

  • สำหรับ Node ทั้งหมด: parentNode, childNodes, firstChild, lastChild, previousSibling, nextSibling.
  • สำหรับ element Node: parentElement, children, firstElementChild, lastElementChild, previousElementSibling, nextElementSibling.

Searching: getElement, querySelector**

DOM navigation properties ใช้งานได้ดีก็ต่อเมื่อ element อยู่ใกล้ ๆ กัน แต่ถ้าไม่เราจะสามารถเข้าถึง element ตัวนั้นได้อย่างไร


document.getElementById or just id​

ถ้า element นั้น มี id attribute เราสามารถใช้ method document.getElementById(id) ไม่สำคัญว่ามันจะอยู่ที่ตรงไหน

ตัวอย่างเช่น:


<div id="elem">
<div id="elem-content">Element</div>
</div>

<script>
// get the element
let elem = document.getElementById('elem');

// make its background red
elem.style.background = 'red';
</script>

นอกจากนี้ยังมีตัวแปร global ที่ตั้งชื่อตาม id ที่อ้างอิงถึง element

<div id="elem">
<div id="elem-content">Element</div>
</div>

<script>
// elem is a reference to DOM-element with id="elem"
elem.style.background = 'red';

// id="elem-content" has a hyphen inside, so it can't be a variable name
// ...but we can access it using square brackets: window['elem-content']
</script>

แต่ถ้าเราประกาศตัวแปรไว้ด้วยชื่อเดียวกัน มันจะเอาตัวที่ประกาศไว้

<div id="elem"></div>

<script>
let elem = 5; // now elem is 5, not a reference to <div id="elem">

alert(elem); // 5
</script>

querySelectorAll​

method, elem.querySelectorAll(css) จะรีเทิร์น element ทั้งหมดภายใน elem ที่ macth กับ CSS selector

ตัวอย่าง เรากำลังมองหา <li> ทั้งหมดที่เป็น last children:

<ul>
<li>The</li>
<li>test</li>
</ul>
<ul>
<li>has</li>
<li>passed</li>
</ul>
<script>
let elements = document.querySelectorAll('ul > li:last-child');

for (let elem of elements) {
alert(elem.innerHTML); // "test", "passed"
}
</script>

querySelector​

elem.querySelector(css) คือค่า element ตัวแรกที่ตรงตามกับ CSS selector

อีกนัยหนึ่ง คือ ผลลัพธ์จะเหมือนกับ elem.querySelectorAll(css)[0], แต่ method นี้จะหา element ทั้งหมดออกมา และเลือกแค่อันเดียว ในขณะที่ elem.querySelector(css) จะแค่ตัวเดียว ดังนั้นการใช้ elem.querySelector(css) จะเร็วกว่า


matches​

method ก่อนหน้านี้ใช้หา DOM แต่ elem.matches(css) ไม่ได้หาอะไรเลย แต่ว่าใช้เช็คว่า element นั้น มี CSS-selector ตรงกันหรือไม่ และจะรีเทิร์นค่า true หรือ false มา

ตัวอย่างเช่น:

<a href="http://example.com/file.zip">...</a>
<a href="http://ya.ru">...</a>

<script>
// can be any collection instead of document.body.children
for (let elem of document.body.children) {
if (elem.matches('a[href$="zip"]')) {
alert("The archive reference: " + elem.href );
}
}
</script>

closest​

Ancestors ของ element คือ: parent, parent ของ parent, และ ต่อไป

method elem.closest(css) จะมองหา Ancestors ที่ใกล้ที่สุด ที่ตรงกับ CSS-selector. elemก็รวมอยู่ในการ search ด้วย

method closest จะขึ้นไปเรื่อย ๆ และเช็ค parents แต่ละตัว ถ้ามันตรงกับ selector, ancestor จะ return ออกมา

ตัวอย่างเช่น:

<h1>Contents</h1>

<div class="contents">
<ul class="book">
<li class="chapter">Chapter 1</li>
<li class="chapter">Chapter 2</li>
</ul>
</div>

<script>
let chapter = document.querySelector('.chapter'); // LI

alert(chapter.closest('.book')); // UL
alert(chapter.closest('.contents')); // DIV

alert(chapter.closest('h1')); // null (because h1 is not an ancestor)
</script>

getElementsBy*​

ยังมี methods อื่น ๆ อีก ที่หา nodes โดยการใช้ tag, class, etc.

เช่น:

  • elem.getElementsByTagName(tag) มองหา element ที่มี tag ที่ให้ไว้ และรีเทิร์น collection ที่ให้ไว้, tag parameter สามารถใช้ star * ได้ เพื่อเลือกแท็กอะไรก็ได้
  • elem.getElementsByClassName(className) จะรีเทิร์น element ที่ให้ตาม CSS class
  • document.getElementsByName(name) รีเทิร์น element ที่มี attribute name เดียวกับที่ให้ไป
// get all divs in the document
let divs = document.getElementsByTagName('div');

เรามาหา tag input ภายใน table กันเถอะ

<table id="table">
<tr>
<td>Your age:</td>

<td>
<label>
<input type="radio" name="age" value="young" checked> less than 18
</label>
<label>
<input type="radio" name="age" value="mature"> from 18 to 50
</label>
<label>
<input type="radio" name="age" value="senior"> more than 60
</label>
</td>
</tr>
</table>

<script>
let inputs = table.getElementsByTagName('input');

for (let input of inputs) {
alert( input.value + ': ' + input.checked );
}
</script>

การหา .article element:

<form name="my-form">
<div class="article">Article</div>
<div class="long article">Long article</div>
</form>

<script>
// find by name attribute
let form = document.getElementsByName('my-form')[0];

// find by class inside the form
let articles = form.getElementsByClassName('article');
alert(articles.length); // 2, found two elements with class "article"
</script>

Live collections​

ทุก method ที่เป็น "getElementBy*" จะรีเทิร์น live collection โดย collection ดังกล่าว reflect ถึงสถานะปัจจุบันของ document และ "auto-update" เสมอเมื่อมีการเปลี่ยนแปลง

ในตัวอย่างข้างล่าง จะมี 2 script

  1. สคริปต์แรก จะการอ้างอิงไปถึง collection <div> เมื่อเพิ่ม <div> ค่า length ไม่เปลี่ยน
  2. สคริปต์ที่สอง เมื่อรันแล้วเจอ <div> อีกตัว ค่า length จะเปลี่ยน
<div>First div</div>

<script>
let divs = document.getElementsByTagName('div');
alert(divs.length); // 1
</script>

<div>Second div</div>

<script>
alert(divs.length); // 2
</script>

ในทางตรงกันข้าม querySelectorAll  จะคืนค่า static collection เหมือนกัน Array ที่ไม่เปลี่ยนแปลง

<div>First div</div>

<script>
let divs = document.querySelectorAll('div');
alert(divs.length); // 1
</script>

<div>Second div</div>

<script>
alert(divs.length); // 1
</script>

Summary

มี 6 method หลักที่ใช้ค้นหา node ใน DOM

Method


querySelector

querySelectorAll

getElementById

getElementsByName

getElementsByTagName

getElementsByClassName

Searches by…


CSS-selector

CSS-selector

id

name

tag or '*'

class

Can call on an element?


✅

✅

❌

❌

✅

✅

Live?


❌

❌

❌

✅

✅

✅

เรามากล่าวถึง method สุดท้ายกันใช้สำหรับเช็คความสัมพันธ์ child-parent

  • elemA.contains(elemB) จะรีเทิร์น true ถ้า  elemB อยู่ใน element elemA หรือเมื่อ  elemA==elemB.

Node properties: type, tag and contents

มาดู Node DOM ในเชิงลึกมากขึ้นกัน ในบทนี้เราจะมาดูกันว่ามันคืออะไรและเรียนรู้ properties ที่ใช้บ่อยที่สุด


DOM node classes​

Node DOM ที่แตกต่างกันอาจมี properties ที่แตกต่างกัน ตัวอย่างเช่น Node element ที่สอดคล้องกับแท็ก

<a> มี properties ที่เกี่ยวข้องกับ link เป็นต้น, และอันที่สอดคล้องกับ <input> มี properties ที่เกี่ยวข้องกับ input เป็นต้น

Node DOM แต่ละตัวก็จะมี built-in class อยู่ภายใน

รากของลำดับชั้นทั้งหมดคือ  EventTarget ที่สามารถเข้าถึงได้จาก Node

รูปสำหรับคำอธิบาย:

image.png

Class หลัก ๆ:

  • EventTarget - เป็น root “abstract” class ของทุกอย่าง

    Object ของ Class นี้ไม่ได้ถูกสร้าง มันทำหน้าที่เป็นฐาน ดังนั้น Node DOM ทั้งหมดจึงรองรับสิ่งที่เรียกว่า "Events" เราจะศึกษามันในภายหลัง

  • Node - ก็เป็น “abstract” class เหมือนกัน ทำหน้าที่เป็นฐานสำหรับ Node DOM

    มันมีฟังก์ชันการทำงานหลักของมันเช่น parentNode, nextSibling, childNodes

    Object ของ Class Node ไม่ได้ถูกสร้างขึ้น แต่มี Class อื่นที่สืบทอดมาจากมัน

  • Document, สำหรับเหตุผลทางประวัติศาสตร์ มันจะเข้าถึงโดยใช้ HTMLDocument

    document ที่เป็น global Object ก็อยู่ใน Class นี้เหมือนกัน, มันทำหน้าที่ เข้าถึง DOM

  • CharacterData – เป็น “abstract” Class, ถูกสืบทอดโดย:

    • Text – class นี้ แสดงถึง text ที่อยู่ข้างใน เช่น  Hello in <p>Hello</p>.
    • Comment – class นี้ใช้กับ comment. พวกมันจะไม่ถูกแสดง, แต่ comment แต่ละอัน จะมาเป็นสมาชิกของ DOM
  • Element – เป็นฐานของ DOM elements มันมีหน้าที่ navigate ไปถึง element อื่น ๆ เช่น nextElementSibling, children และ searching method เช่นgetElementsByTagName, querySelector

  • สุดท้าย HTMLElement เป็นพื้นฐานของ element ทั้งหมด มันสืบทอดมาจาก element HTML

มีแท็กอื่นๆ อีกมากมายที่มี class เป็นของตัวเองซึ่งอาจมี properties และ method เฉพาะ ในขณะที่ element บางอย่าง เช่น <span>, <section>, <article> ไม่มี properties เฉพาะใดๆ

ตัวอย่างเช่น, ลองมาพิจารณา DOM object สำหรับ <input> element.

เฉลย มันอยู่กับ class HTMLInputElement .

วิธีที่จะดูชื่อของ DOM node class เราสามารถเรียกใช้ Object ที่มี Constructor property ที่อ้างอิงไปถึง class contructor และ ชื่อของมันคือ constructor.name

alert( document.body.constructor.name ); // HTMLBodyElement

หรือเราสามารถใช้ toString 

alert( document.body ); // [object HTMLBodyElement]

หรือเราสามารถใช้ instanceof  เพื่อเช็คการสืบทอด

alert( document.body instanceof HTMLBodyElement ); // true
alert( document.body instanceof HTMLElement ); // true
alert( document.body instanceof Element ); // true
alert( document.body instanceof Node ); // true
alert( document.body instanceof EventTarget ); // true

นอกจากนี้เรายังสามารถดู Output ของ element โดยการใช้ console.dir(elem) ใน Browser.

โดย Output ของมันจะมี HTMLElement.prototype, Element.prototype และอื่น ๆ


The “nodeType” property​

nodeType property เป็นอีก 1 วิธีที่ ล้าสมัย เพื่อที่จะดึงค่าประเภทของ DOM node

มันเป็นค่าตัวเลข

  • elem.nodeType == 1 สำหรับ element node,
  • elem.nodeType == 3 สำหรับ text node,
  • elem.nodeType == 9 สำหรับ document Object,
  • มีค่าอื่น ๆ อีกใน the specification.

ตัวอย่างเช่น:

<body>
<script>
let elem = document.body;

// let's examine: what type of node is in elem?
alert(elem.nodeType); // 1 => element

// and its first child is...
alert(elem.firstChild.nodeType); // 3 => text

// for the document object, the type is 9
alert( document.nodeType ); // 9
</script>
</body>

ใน script สมัยใหม่ เราสามารถใช้ instanceof และ class-based อื่น ๆ ในการหา Node type

เราสามารถทำได้แค่อ่าน Node type และไม่สามารถเปลี่ยนมันได้


Tag: nodeName and tagName​

DOM node ที่เราให้ไป เราสามารถอ่าน Tag name ผ่าน nodeName หรือ tagName property ได้

ตัวอย่างเช่น:

alert( document.body.nodeName ); // BODY
alert( document.body.tagName ); // BODY

แล้วมีข้อแตกต่างระหว่าง tagName และ nodeName หรือไม่? แน่นอนว่ามี ข้อแตกต่างคือ การใช้กับชนิดของ Node

  • tagName ใช้สำหรับแค่ Element node
  • nodeName ใช้ได้กับทุก Node
    • สำหรับ element จะใช้เหมือกับ tagName
    • สำหรับ node type อื่น ๆ (text/comment etc.) จะมี string ที่เป็นประเภทของ Node

กล่าวง่าย ๆ คือ tagName สามารถใช้ได้แค่กับ element แต่ nodeName ใช้กับกับ Node ทุกประเภท

ตัวอย่าง:

<body><!-- comment -->

<script>
// for comment
alert( document.body.firstChild.tagName ); // undefined (not an element)
alert( document.body.firstChild.nodeName ); // #comment

// for document
alert( document.tagName ); // undefined (not an element)
alert( document.nodeName ); // #document
</script>
</body>

innerHTML: the contents​

innerHTML property อนุญาตให้เราเข้าถึง HTML ข้างใน element ได้ โดยออกมาเป็น String

และเราสามารถดัดแปลงแก้ไขมันได้ด้วย ดังนั้นมันจึงเป็นทางที่ค่อนข้างมีประสิทธิภาพในการเปลี่ยน content ของ page

ตัวอย่าง:

<body>
<p>A paragraph</p>
<div>A div</div>

<script>
alert( document.body.innerHTML ); // read the current contents
document.body.innerHTML = 'The new BODY!'; // replace it
</script>

</body>

เราสามารถ ใส่ invalid HTML ได้ browser จะทำการแก้ไขให้

<body>

<script>
document.body.innerHTML = '<b>test'; // forgot to close the tag
alert( document.body.innerHTML ); // <b>test</b> (fixed)
</script>

</body>

Beware: “innerHTML+=” does a full overwrite​

เราสามารถเพิ่ม HTML เข้าไปใน elementได้ โดยใช้ elem.innerHTML+="more html".


chatDiv.innerHTML += "<div>Hello<img src='smile.gif'/> !</div>";
chatDiv.innerHTML += "How goes?";

แต่เราควรระวังในการใช้มันอย่างมาก เพราะว่า สิ่งที่เกิดขึ้นไม่ใช่การเพิ่มเข้าไปแต่เป็นการ overwrite มันทั้งหมด

ในทางเทคนิค สองบรรทัดนี้ทำงานเหมือนกัน

elem.innerHTML += "...";
// is a shorter way to write:
elem.innerHTML = elem.innerHTML + "..."

ในทางกลับกัน innerHTML += ทำแบบนี้

  1. content อันเก่าถูกลบออก
  2. innerHTML อันใหม่ ถูกเขียนลงไปแทน (เป็นการต่อข้อมูลกันระหว่าง อันเก่าและอันใหม่)

outerHTML: full HTML of the element​

outerHTML property เก็บ HTML ทั้งหมดของ element ไว้ เหมือนกับ innerHTML แต่มันจะรวมตัวเองไปด้วย

<div id="elem">Hello <b>World</b></div>

<script>
alert(elem.outerHTML); // <div id="elem">Hello <b>World</b></div>
</script>

Beware: ไม่เหมือนกับinnerHTML, เขียน outerHTML ไม่ได้เปลี่ยน element แต่เป็นการแทนที่ใน DOM

<div>Hello, world!</div>

<script>
let div = document.querySelector('div');

// replace div.outerHTML with <p>...</p>
div.outerHTML = '<p>A new element</p>'; // (*)

// Wow! 'div' is still the same!
alert(div.outerHTML); // <div>Hello, world!</div> (**)
</script>

ดูแปลกจริงๆ ใช่ไหม?

เราแทนที่ <div> ด้วย <p>A new element</p> เห็นได้ว่า ค่าของ <div> เก่าไม่ได้เปลี่ยน

outerHTML  การกำหนดด้านบนไม่ได้แก้ไข element แต่ลบออกจาก DOM และแทรก HTML ใหม่เข้าไป

ดังนั้นสิ่งที่เกิดขึ้นคือ:

  • div ถูกลบออกจาก document
  • HTML อีกชิ้น ถูกแทนที่ <p>A new element</p> ถูกแทรกเข้ามา
  • div ยังคงเป็นค่าเก่าอยู่ และ HTML ใหม่ไม่ได้ถูกบันทึกไปที่ตัวแปรไหนเลย

มันมีโอกาสสูงมากที่จะเกิด Error ขึ้นได้ การแปลง div.outerHTML และทำงานต่อไปกับ div ถ้ามันมี content ใหม่เข้ามา มันได้ สิ่งนี้ถูกต้องสำหรับ innerHTML แต่ไม่ใช่สำหรับ outerHTML


nodeValue/data: text node content​

property innerHTML จะสามารถใช้ได้แค่กับ element node

Node type อื่น ๆ เช่น text node ก็จะมี properties ของมันเอง คือ nodeType และ data สองตัวนี้ เมื่อใช้จริงแล้ว มีหน้าที่คล้าย ๆ กัน มันจะมีความแตกต่างแค่เล็กน้อยเท่านั้น ดังนั้นเราจะใช้ data เพราะว่ามันจะสั้นกว่า

ตัวอย่างของการอ่าน text node และ comment:

<body>
Hello
<!-- Comment -->
<script>
let text = document.body.firstChild;
alert(text.data); // Hello

let comment = text.nextSibling;
alert(comment.data); // Comment
</script>
</body>

สำหรับ text node เราสามารถจิตนาการได้ ถึงการอ่านค่า หรือ แก้ไข ค่าของ text แต่ทำไม comment ล่ะ

developer บางคนฝังข้อมูลหรือ template เข้าไปใน HTML แบบนี้

<!-- if isAdmin -->
<div>Welcome, Admin!</div>
<!-- /if -->

ดังนั้น JavaScript สามารถอ่านค่าจาก data property ได้


textContent: pure text​

textContent เป็นตัวที่ทำให้เราเข้าถึง ข้อความ ข้างใน element และไม่มี <tag> เข้ามารวม

ตัวอย่างเช่น:

<div id="news">
<h1>Headline!</h1>
<p>Martians attack people!</p>
</div>

<script>
// Headline! Martians attack people!
alert(news.textContent);
</script>

อย่างที่เราเห็น มีแค่ ข้อความ เท่านั้นถูกรีเทิร์นออกมา และ <tag> ถูกตัดออกทั้งหมด

ในทางปฏิบัติ การอ่านข้อความ ไม่ได้ใช้มากเท่าไหร่

การใช้ textContent มีประโยชน์มากกว่า เพราะว่ามันทำให้เราเขียนได้ปลอดภัยขึ้น:

  • การใช้ innerHTML เป็นการแทนค่า HTML เข้าไปทั้งหมด
  • การใช้ textContent เป็นการแทนค่า ข้อความ เข้าไป

เปรียบเทียบทั้งสองตัว:

<div id="elem1"></div>
<div id="elem2"></div>

<script>
let name = prompt("What's your name?", "<b>Winnie-the-Pooh!</b>");

elem1.innerHTML = name;
elem2.textContent = name;
</script>
  1. <div> ได้รับ input เข้ามาเป็น HTML ดังนั้นมันจะแปลง <b> ให้เป็น HTML
  2. <div> ได้รับ input เข้ามาเป็น ข้อควาาม ดังนั้น output ที่เราจะเห็น จะออกมาเป็น   <b>Winnie-the-Pooh!</b>.

The “hidden” property​

“hidden” attribute และ DOM property จะเป็นตัวกำหนดว่า element นั้น จะปรากฏหรือไม่

เราสามารถใช้ใน HTML หรือ กำหนดค่ามันใน JavaScript ได้ แบบนี้:

<div>Both divs below are hidden</div>

<div hidden>With the attribute "hidden"</div>

<div id="elem">JavaScript assigned the property "hidden"</div>

<script>
elem.hidden = true;
</script>

ในทางเทคนิต hidden ทำหน้าที่เหมือนกับ style = "display: none;" แต่มันนสั้นกว่าเวลาเราเขียน

นี่คือ element ที่กระพริบได้:

<div id="elem">A blinking element</div>

<script>
setInterval(() => elem.hidden = !elem.hidden, 1000);
</script>

More properties​

DOM elements ก็มี properties เพิ่มเติมเหมือนกัน ในทางปฏิบัติแล้ว มันขึ้นอยู่กับ Class:

  • value คือค่าของ <input>, <select> และ <textarea> (HTMLInputElement, HTMLSelectElement…)
  • href ใช้กับ แท็ก <a href="..." > (HTMLAnchorElement)

ตัวอย่างเช่น:


Summary​

DOM node แต่ละตัว ก็จะมี Class ของมันเอง

DOM node properties หลัก ๆ:

nodeType เราใช้เพื่อดูว่า Node นั้น เป็นประเภทอะไร เช่น ค่า 1 เป็น element Node, 3 สำหรับ text Node

nodeName/tagName สำหรับ element ใช้ tagName สำหรับตัวที่ไม่ใช่ element ใช้ nodeName เพื่ออธิบาย ว่ามันคืออะไร

innerHTML ใช้อ่าน content ของ element สามารถดัดแปลงแก้ไขได้

outerHTML การใช้ outerHTML ในการแทนค่า จะแทนค่าทั้งหมดลงไปเลย

nodeValue/data การใช้ดู content ของ text หรือ comment ซึ่งจะให้ data มากกว่า

textContent ใช้อ่านข้อความข้างใน element จะไม่มี tag เข้ามา, การใช้มัน เป็นการใส่ข้อความเข้าไปใน element, special characters และ tag จะถูกทำให้เป็นเหมือนข้อความ

hidden เมื่อเรากำหนดให้เป็น true จะทำหน้าที่เหหมือนกับ display:none

DOM nodes แต่ละตัว มี properties ต่างกัน ขึ้นอยู่กับว่าอยู่ Class ไหน เช่น <input> element จะ support value ,type ในขณะที่ <a> element จะ support href และอื่น ๆ


Attributes and properties

เมื่อ Browser โหลดหน้าเพจ มันอ่าน HTML และ generates DOM objects ออกมา.

สำหรับ element node นั้น attribute ของ HTML ส่วนใหญ่ จะมาเป็น property ของ DOM Object

ตัวอย่างเช่น <body id="page"> ตัวของ DOM object จะมี body.id = “page”

แต่การจับคู่ของ attribute-property ไม่ใช่การจับคู่แบบ 1 ต่อ 1 ในบทนี้เราจะมาให้ความสนใจกับ หลักการทำงานของมัน และดูว่าเราจะทำงานกับมันได้อย่างไรบ้าง ตอนไหนมันจะเหมือนกัน ตอนไหนมันจะต่างกัน


DOM properties​

เราเห็น Built-in DOM properties หลาย ๆ ตัวไปแล้ว แต่ว่า มันไม่มีอะไรมากำหนดว่าให้ใช้ได้แค่ Built-in เรายังสามารถเพิ่ม attribute ของเราเองได้

DOM nodes เป็นเหมือน Object ของ JavaScript ธรรมดาเลย. เราสามารถเปลี่ยนแปลงแก้ไขมันได้

ตัวอย่างเช่น เราจะสร้าง properties ใหม่ใน document.body

 document.body.myData = {
name: 'Caesar',
title: 'Imperator'
};

alert(document.body.myData.title); // Imperator

เราสามารถสร้าง method ได้เหมือนกัน

document.body.sayTagName = function() {
alert(this.tagName);
};

document.body.sayTagName(); // BODY (the value of "this" in the method is document.body)

เราสามารถ แก้ไข Built-in prototypes เช่น Element.prototype และเพิ่ม method เข้าไปใน element ได้

Element.prototype.sayHi = function() {
alert(`Hello, I'm ${this.tagName}`);
};

document.documentElement.sayHi(); // Hello, I'm HTML
document.body.sayHi(); // Hello, I'm BODY

ดังนั้น DOM properties และ method จะทำตัวเหมือนกับ JavaScript Object ธรรมดาเลย

  • มันสาารถมีค่าใดก็ได้
  • พวกมันเป็น case-sensitive (เช่น ต้องเขียน elem.nodeType, ไม่ใช่ elem.NoDeTyPe)

HTML attributes​

ใน HTML, บาง tag อาจมี attribute อยู่ เมื่อ Browser วิเคราะห์ HTML เพื่อที่จะสร้าง DOM object สำหรับ tag มันจะสร้าง standard attribute และ สร้าง DOM properties ให้กับมัน

ตัวอย่างเช่น

<body id="test" something="non-standard">
<script>
alert(document.body.id); // test
// non-standard attribute does not yield a property
alert(document.body.something); // undefined
</script>
</body>

โปรดจำไว้ว่า standard attribute สำหรับ element หนึ่ง อาจจะไม่มีค่าในอีก element หนึ่ง เช่น "type" เป็น standard สำหรับ <input> แต่ไม่มีใน <body>

<body id="body" type="...">
<input id="input" type="text">
<script>
alert(input.type); // text
alert(body.type); // undefined: DOM property not created, because it's non-standard
</script>
</body>

ดังนั้น ถ้า attribute นั้น ไม่ใช่ standard attribute แล้ว มันจะไม่ถูกสร้างใน DOM-property สำหรับมัน.

มันมีทางที่จะเข้าถึง attribute ได้หรือไม่. แน่นอน attribute ทุกตัววสามารถเข้าถึงได้ โดยการใช้ method พวกนี้

  • elem.hasAttribute(name) – ใช้ตรวจสอบว่ามี attribute ที่ตรงกับ name หรือไม่.
  • elem.getAttribute(name) – ใช้ดึงค่าจาก attribute ตามชื่อที่กำหนด.
  • elem.setAttribute(name, value) – กำหนดค่าให้กับ attribute ที่กำหนด.
  • elem.removeAttribute(name) – ลบ attribute.

method พวกนี้ทำหน้าที่เหมือนกับ สิ่งที่เราเขียนลงไปใน HTML เลย

ตัวอย่างในการอ่านข้อมูลที่เป็น non-standard properties:

<body something="non-standard">
<script>
alert(document.body.getAttribute('something')); // non-standard
</script>
</body>

HTML attributes มี features ตามนี้:

  • ชื่อของพวกมัน เป็น case-insensitive (id เหมือนกับ ID).
  • ค่าของพวกมันจะเป็น String เสมอ

ตัวอย่างการทำงานกับ attribute

<body>
<div id="elem" about="Elephant"></div>

<script>
alert( elem.getAttribute('About') ); // (1) 'Elephant', reading

elem.setAttribute('Test', 123); // (2), writing

alert( elem.outerHTML ); // (3), see if the attribute is in HTML (yes)

for (let attr of elem.attributes) { // (4) list all
alert( `${attr.name} = ${attr.value}` );
}
</script>
</body>

Please note:

  • getAttribute('About') - ตัวแรกเป็นตัวพิมพ์ใหญ่ แต่ใน HTML เป็น lowwercase ทั้งหมด แต่มันไม่สำคัญ เพราะ attribute name เป็น case-insensitive
  • เราสามารถกำหนดอะไรก็ได้ลงไปใน attribute แต่ว่าค่าของมันจะเป็น String ดังนั้นเราจะได้ "123"
  • attribute ทั้งหมด เราจะสามารถดูได้ใน outerHTML
  • collection attributes เป็น iterable และ attribute ของมันทั้งหมด จะมาในรูปแบบของ Object โดยมี name และ value เป็น properties

Property-attribute synchronization​

เมื่อ standard attribute เปลี่ยน มันจะ update โดยอัตโนมัติ ไปที่ property

ในตัวอย่างข้างล่าง id ถูกทำให้เป็น attribute และอย่างที่เห็น property ก็ถูกเปลี่ยนด้วยเช่นกัน

<input>

<script>
let input = document.querySelector('input');

// attribute => property
input.setAttribute('id', 'id');
alert(input.id); // id (updated)

// property => attribute
input.id = 'newId';
alert(input.getAttribute('id')); // newId (updated)
</script>

แต่ว่า มีข้อยกเว้นกับ input.value มันจะเชื่อมแค่ attribute → property เท่านั้น ดังนั้นการเปลี่ยนค่า ที่ property จะไม่เปลี่ยนค่าของมัน

<input>

<script>
let input = document.querySelector('input');

// attribute => property
input.setAttribute('value', 'text');
alert(input.value); // text

// NOT property => attribute
input.value = 'newValue';
alert(input.getAttribute('value')); // text (not updated!)
</script>

ในตัวอย่างด้านบน:

  • เปลี่ยนค่าของ attribute value มันไป update ค่าของ property.
  • เปลี่ยนที่ property มันไม่ส่งผลไปที่ attribute

DOM properties are typed​

DOM properties ไม่ได้เป็น String เสมอไป ตัวอย่างเช่น input.check property (ใช้กับ Checkboxes) เป็น boolean

<input id="input" type="checkbox" checked> checkbox

<script>
alert(input.getAttribute('checked')); // the attribute value is: empty string
alert(input.checked); // the property value is: true
</script>

อีกตัวอย่าง. style attribute เป็น string แต่ style properties เป็น Object

<div id="div" style="color:red;font-size:120%">Hello</div>

<script>
// string
alert(div.getAttribute('style')); // color:red;font-size:120%

// object
alert(div.style); // [object CSSStyleDeclaration]
alert(div.style.color); // red
</script>

แต่ยังไงก็ตาม ส่วนใหญ่ก็เป็น string อยู่ดี

แม้ว่า DOM property เป็น string แต่ก็อาจเก็บค่าแตกต่างจาก attribute ตัวอย่างเช่น href  DOM property จะเป็น URL แบบเต็ม ถึงแม้ attribute จะเก็บแค่ #hash ตัวอย่างเช่น:


<a id="a" href="#hello">link</a>
<script>
// attribute
alert(a.getAttribute('href')); // #hello

// property
alert(a.href); // full URL in the form http://site.com/page#hello
</script>

Non-standard attributes, dataset​

เมื่อเราเขียน HTML แล้ว เราใช้ standard attribute มากมาย แต่ non-standard attribute ล่ะ

เรามาดูกันว่า มันสำคัญหรือไม่ และใช้เพื่ออะไร

บางที non-standard attributes ใช้เพื่อส่ง custom data จาก HTML ไปที่ JavaScript หรือ “mark” HTML element สำหรับ JavaScipt

<!-- mark the div to show "name" here -->
<div show-info="name"></div>
<!-- and age here -->
<div show-info="age"></div>

<script>
// the code finds an element with the mark and shows what's requested
let user = {
name: "Pete",
age: 25
};

for(let div of document.querySelectorAll('[show-info]')) {
// insert the corresponding info into the field
let field = div.getAttribute('show-info');
div.innerHTML = user[field]; // first Pete into "name", then 25 into "age"
}
</script>

และพวกมันยังสามารถเพื่อตกแต่ง element ได้อีกด้วย

<style>
/* styles rely on the custom attribute "order-state" */
.order[order-state="new"] {
color: green;
}

.order[order-state="pending"] {
color: blue;
}

.order[order-state="canceled"] {
color: red;
}
</style>

<div class="order" order-state="new">
A new order.
</div>

<div class="order" order-state="pending">
A pending order.
</div>

<div class="order" order-state="canceled">
A canceled order.
</div>

ทำไมเราถึงควรใช้ attribute แทนการใช้ class เช่น .order-state-new, .order-state-pending, .order-state-canceled ?

เพราะว่า attribute สามารถจัดการได้ง่ายกว่า, state สามารถเปลี่ยนได้ง่าย

// a bit simpler than removing old/adding a new class
div.setAttribute('order-state', 'canceled');

attribute ทั้งหมดที่เริ่มด้วย “data-” สงวนไว้ให้กับ programmer ใช้. พวกมันสามารถใช้ได้ที่ dataset. property

<body data-about="Elephants">
<script>
alert(document.body.dataset.about); // Elephants
</script>

attribute ที่ใช้หลายคำเช่น data-order-state จะมาเป็น camel-case: dataset.orderState. ตัวอย่างเช่น:


<style>
.order[data-order-state="new"] {
color: green;
}

.order[data-order-state="pending"] {
color: blue;
}

.order[data-order-state="canceled"] {
color: red;
}
</style>

<div id="order" class="order" data-order-state="new">
A new order.
</div>

<script>
// read
alert(order.dataset.orderState); // new

// modify
order.dataset.orderState = "pending"; // (*)
</script>

การใช้ data- attributes เป็นวิธีที่ปลอดภัย สำหรับการส่งค่าไปที่ JavaScript

และการใช้ data- เราไม่ได้ทำได้แค่อ่านมัน เรายังสามารถ custom data-attributes


Summary

  • Attributes – คือสิ่งที่เขียนใน HTML
  • Properties – คือสิ่งที่อยู่ใน DOM object

type

Name

Properties


สามารถเป็นได้ทุก type

name เป็น case-sensitive

Attributes


เป็น String

name ไม่เป็น case-sensitive

Method ที่ใช้ทำงานร่วมกับ attribute คือ:

  • elem.hasAttribute(name) – ใช้ตรวจสอบว่ามี attribute ที่ตรงกับ name หรือไม่.
  • elem.getAttribute(name) – ใช้ดึงค่าจาก attribute ตามชื่อที่กำหนด.
  • elem.setAttribute(name, value) – กำหนดค่าให้กับ attribute ที่กำหนด.
  • elem.removeAttribute(name) – ลบ attribute.
  • elem.attributes คืนค่า attribute ทั้งหมดที่มี.

ในหลายสถานการณ์ เรานิยมใช้ DOM properties มากกว่า เราควรใช้ attribute แค่ตอนเมื่อ DOM properties ไม่เหมาะกับสถานการณ์นั้น เราต้องการใช้ attribute จริง ๆ ก็ต่อเมื่อ

  • เราต้องการใช้ non-standard attribute
  • เราต้องการอ่านค่าที่เราเขียนใน HTML ตัวอย่างเช่น href  ที่เราพูดถึงไป

Modifying the document

DOM modification เป็นกุญแจสำคัญที่ทำให้สามารถสร้าง page ที่อัปเดตได้ตลอดเวลา

Example: show a message​

มาสาธิตโดยใช้ตัวอย่างกัน เราจะเพิ่มข้อความบนหน้าเว็บที่ดูดีกว่าการใช้ alert

ตัวอย่างเช่น:

<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>

<div class="alert">
<strong>Hi there!</strong> You've read an important message.
</div>

image.png

นั่นคือการใช้ HTML. ตอนนี้เราสร้าง <div> ตัวเดิม โดยใช้ JavaScript กันเถอะ


Creating an element​

ในการสร้าง DOM node เราจะใช้ 2 method ด้วยกัน

document.createElement(tag) สร้าง element โดยการให้ชื่อแท็ก

let div = document.createElement('div');

document.createTextNode(text) สร้าง text node โดยการใส่ข้อความที่ต้องการ

let textNode = document.createTextNode('Here I am');

ส่วนใหญ่เราต้องการสร้าง element node เช่น <div> เพื่อแสดงข้อความ


Creating the message​

สร้าง <div> ที่มีข้อความ ใช้ขั้นตอน 3 วิธี:

// 1. Create <div> element
let div = document.createElement('div');

// 2. Set its class to "alert"
div.className = "alert";

// 3. Fill it with the content
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

ตอนนี้เราสร้าง Node element แล้ว แต่ว่าเรายังเก็บไว้ในตัวแปร div อยู่ และยังไม่ได้นำไปแสดง แปลว่าเรายังไม่เห็นมัน


Insertion methods​

วิธีทำให้ div ขึ้นไปแสดงผลนั้น เราต้องแทรกเข้าไปซักที่ใน document ตัวอย่างเช่น ใส่เข้าไปใน <body> ที่จะอ้างอิงไปถึง document.body และเราสามารถใช้ method append เพิ่มแทรก div เข้าไปได้ document.body.append(div) :

<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>

<script>
let div = document.createElement('div');
div.className = "alert";
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

document.body.append(div);
</script>

เราเรียกใช้ append บน document.body แต่เราก็สามารถเรียกใช้ append ได้บน element อื่นเหมือนกัน

เช่น เราต้องการเพิ่มบางอย่างเข้าไปใน <div> ก็เขียนแค่ว่า div.append(anotherElement)

และยังมี method อีกหลายตัวที่ใช้แทรก element เข้าไปได้อีก

  • node.append(...nodes or strings) – เพิ่มเข้าไปข้างในและอยู่ตรงท้ายของ node,
  • node.prepend(...nodes or strings) – เพิ่มเข้าไปข้างในและอยู่ตรงข้างหน้าของ node,
  • node.before(...nodes or strings) –- เพิ่มไปก่อน  node,
  • node.after(...nodes or strings) –- เพิ่มไปหลัง node,
  • node.replaceWith(...nodes or strings) –- แทนที่ node กับ Node ที่ให้มา

เรามาดูวิธีใช้กัน

<ol id="ol">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>

<script>
ol.before('before'); // insert string "before" before <ol>
ol.after('after'); // insert string "after" after <ol>

let liFirst = document.createElement('li');
liFirst.innerHTML = 'prepend';
ol.prepend(liFirst); // insert liFirst at the beginning of <ol>

let liLast = document.createElement('li');
liLast.innerHTML = 'append';
ol.append(liLast); // insert liLast at the end of <ol>
</script>

image.png

ภาพหลักการทำงานของ method

image.png

ดังนั้น สุดท้ายแล้วจะให้ผลลัพธ์ใน HTML แบบนี้

before
<ol id="ol">
<li>prepend</li>
<li>0</li>
<li>1</li>
<li>2</li>
<li>append</li>
</ol>
after

อย่างที่พูดไป เราสามารถแทรก Node ได้มากกว่า 1 ตัวได้ภายในการเรียกใช้ method ครั้งเดียว

ตัวอย่างเช่น:

<div id="div"></div>
<script>
div.before('<p>Hello</p>', document.createElement('hr'));
</script>

โปรดจำไว้ว่า มันแทรกข้อความ ไม่ใช่ HTML และด้วย proper escaping ของตัวอักษรดังนั้น โค้ดที่ออกมา หน้าตาจะเป็นแบบนี้

&lt;p&gt;Hello&lt;/p&gt;
<hr>
<div id="div"></div>

ในอีกความหมายคือ มันจะดีกว่า ถ้าเราใช้แทรกข้อความ เหมือนที่ elem.textContent ทำ

แต่ถ้าเราต้องการที่จะแทรก HTML เข้าไปล่ะ เราสามารถทำอย่างไรได้บ้าง


insertAdjacentHTML/Text/Element​

สำหรับสิ่งนั้น เราสามารถใช้ method อื่น ๆ ได้ elem.insertAdjacentHTML(where, html)

parameter ตัวแรกเป็นคำเฉพาะที่ถูกล็อกมาไว้แล้ว ว่าต้องใช้แค่คำพวกนี้

  • "beforebegin" – แทรก html ไปข้างหน้าของ elem,
  • "afterbegin" – แทรก html เข้าไปใน elem, ตรงจุดแรก
  • "beforeend" – แทรก html เข้าไปในelem, ตรงจุดสุดท้าย
  • "afterend" – แทรก html ไปด้านท้ายของ elem.

parameter ตัวที่สอง คือ HTML ที่เป็น string ที่ถูกแทรกเข้าไปในรูปแบบของ HTML

ตัวอย่างเช่น:

<div id="div"></div>
<script>
div.insertAdjacentHTML('beforebegin', '<p>Hello</p>');
div.insertAdjacentHTML('afterend', '<p>Bye</p>');
</script>

โค้ด HTML หน้าตาจะเป็นแบบนี้

<p>Hello</p>
<div id="div"></div>
<p>Bye</p>

รูปภาพของการทำงาน

image.png

สังเกตได้ว่า รูปภาพนี้ คล้ายกับรูปภาพข้างบน แตกต่างกันที่ว่า มันแทรก HTML เข้าไป

และมันยังมี method ที่คล้ายกันอีก 2 ตัว

  • elem.insertAdjacentText(where, text) ใช้ syntax เหมือนกัน แต่ text จะแทรกเป็น String เข้าไป
  • elem.insertAdjacentElement(where, elem) – ใช้ syntax เหมือนกัน แต่ จะแทรกเป็น element เข้าไป

จริง ๆ แล้วพวกมันมีอยู่เพื่อทำให้ syntax "เหมือนกัน" เป็นหลัก ในทางปฏิบัติแล้ว insertAdjacentHTML จะถูกใช้มากกว่า เพราะว่าสำหรับ text และ element แล้ว เรามี method append/prepend/before/after

ให้ใช้ และสั้นกว่าเมื่อเราใช้

นี่เป็นอีกทางเลือกหนึ่งในการแสดงข้อความ:

<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>

<script>
document.body.insertAdjacentHTML("afterbegin", `<div class="alert">
<strong>Hi there!</strong> You've read an important message.
</div>`);
</script>

Node removal

ในการลบ Node เราสามารถใช้ method node.remove() ได้

เรามาลองทำให้ข้อความของเรา หายไปหลังจาก 1 วินาทีกัน

<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>

<script>
let div = document.createElement('div');
div.className = "alert";
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

document.body.append(div);
setTimeout(() => div.remove(), 1000);
</script>

Note: ถ้าเราต้องการที่จะย้าย element ไปที่อีกที่หนึ่ง เราไม่จำเป็นต้องลบมันออกจากอันเก่า

insertion method ทั้งหมดจะทำการลบไปให้อัติโนมัติ

<div id="first">First</div>
<div id="second">Second</div>
<script>
// no need to call remove
second.after(first); // take #second and after it insert #first
</script>

Cloning nodes: cloneNode

เราจะเพิ่มข้อความที่เหมือนกันได้อย่างไร

เราสามารถสร้างฟังก์ชั่นและใส่โค้ดเข้าไป แต่เรายังมีทางเลือกอื่นที่จะ Clone div และแก้ไขดัดแปลงมัน(ถ้าต้องการ)

ในกรณีที่เรามี element ที่ใหญ่ นี่อาจเป็นวิธีที่เร็วกว่าและง่ายกว่า

  • การเรียกใช้ elem.cloneNode(true) จะสร้าง element ที่หน้าตาเหมือนกันกับ elem – ที่มีทั้ง attributes และ element ย่อย.
  • ถ้าเรียกใช้ elem.cloneNode(false), มันจะ clone element มาโดยไม่มี child element

ตัวอย่างการ clone:

<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>

<div class="alert" id="div">
<strong>Hi there!</strong> You've read an important message.
</div>

<script>
let div2 = div.cloneNode(true); // clone the message
div2.querySelector('strong').innerHTML = 'Bye there!'; // change the clone

div.after(div2); // show the clone after the existing div
</script>

DocumentFragment

DocumentFragment เป็น DOM node แบบพิเศษ ทำหน้าที่หุ้มและส่ง list ของ Node ออกไป

เราสามารถ append ใส่ node ต่าง ๆ เข้าไปได้ แต่เมื่อเรา insert เข้าไปซักที่ content ของมันจะถูก insert เข้าไปแทน

ตัวอย่างเช่น getListContent ทำหน้าที่ generates <li> ออกมา และใส่เข้าไปที่ <ul>

<ul id="ul"></ul>

<script>
function getListContent() {
let fragment = new DocumentFragment();

for(let i=1; i<=3; i++) {
let li = document.createElement('li');
li.append(i);
fragment.append(li);
}

return fragment;
}

ul.append(getListContent()); // (*)
</script>

ผลลัพธ์โครงสร้างคือ

<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

DocumentFragment ไม่ค่อยได้ใช้งาน, ทำไมเราต้องใช้ Node ชนิดพิเศษ ในเมื่อเราสามารถ รีเทิร์น Array ของ Node ออกไปได้:

<ul id="ul"></ul>

<script>
function getListContent() {
let result = [];

for(let i=1; i<=3; i++) {
let li = document.createElement('li');
li.append(i);
result.push(li);
}

return result;
}

ul.append(...getListContent()); // append + "..." operator = friends!
</script>

A word about “document.write”​

อีกหนึ่งวิธีที่ค่อนข้างเก่า ในการเพิ่มบางอย่างเข้าไปใน web-page โดยการใช้ document.write

<p>Somewhere in the page...</p>
<script>
document.write('<b>Hello from JS</b>');
</script>
<p>The end</p>

การใช้ document.write จะสามารถใช้ได้แค่ตอนที่ page กำลัง load อยู่เท่านั้น เช่น:

<p>After one second the contents of this page will be replaced...</p>
<script>
// document.write after 1 second
// that's after the page loaded, so it erases the existing content
setTimeout(() => document.write('<b>...By this.</b>'), 1000);
</script>

Summary

  • Methods ที่ใช้สร้าง Node:

    • document.createElement(tag) – สร้าง Node จากแท็กที่ให้,
    • document.createTextNode(value) – สร้าง Node ข้อความ,
    • elem.cloneNode(deep) – clones element, ถ้า deep==true มันจะ clone child element มาด้วย.
  • แทรกและลบ:

    • node.append(...nodes or strings) – แทรกเข้าไปที่ node, ที่ข้างหลัง,
    • node.prepend(...nodes or strings) – แทรกเข้าไปที่ node, ที่ข้างหน้า,
    • node.before(...nodes or strings) –- แทรกไปที่ข้างหน้า node,
    • node.after(...nodes or strings) –- แทรกไปที่ข้างหลัง node,
    • node.replaceWith(...nodes or strings) –- แทนที่node.
    • node.remove() –- ลบ node.

    ข้อความถูกแทรกเข้าไปในรูปของ “text”.

  • html, elem.insertAdjacentHTML(where, html) ตำแหน่งของมันขึ้นอยู่กับ where:

    • "beforebegin" – แทรก html ก่อน elem,
    • "afterbegin" – แทรก html เข้าไปที่ elem, ตรงข้างหน้า
    • "beforeend" – แทรก html เข้าไปที่elem, ตรงข้างหลัง
    • "afterend" – แทรก html หลัง elem.

Styles and classes

ก่อนที่เราจะไปต่อกับ JavaScript เรามาดูวิธีการทำงานกับ styles and classes นี่คือกฏที่สำคัญ

มันมี 2 วิธีที่เราจะ style ตัว element

  1. สร้าง class ใน CSS และเพิ่ม: <div class="...">
  2. เขียน properties ที่ directly ไปที่  style: <div style="...">.

JavaScript สามารถ modify properties classes และ style ได้

ตัวอย่างเช่น เราสามารถคำนวณ style ได้ ถ้าเราต้องการที่จะคำนวณตำแหน่งของ element จาก JavaScript

let top = /* complex calculations */;
let left = /* complex calculations */;

elem.style.left = left; // e.g '123px', calculated at run-time
elem.style.top = top; // e.g '456px'

className and classList​

การเปลี่ยน Class ถูกใช้บ่อยในการเขียน Script

ในสมัยก่อน มีข้อจำกัดอยู่ใน JavaScript เพราะในตอนนั้น "class" เป็น reserved word และไม่สามารถเป็น property ของ Object ได้ แต่ในตอนนี้ข้อจำกัดนั้นไม่มีแล้ว และ "class" property ยังสำคัญมากอีกด้วย

เช่น elem.class

ดังนั้น property "className" ถูกนำเสนอมา: โดย elem.className มีความเกี่ยวข้องกับ attribute "class"

ตัวอย่าง:

<body class="main page">
<script>
alert(document.body.className); // main page
</script>
</body>

ถ้าเรากำหนด elem.className มันจะแทน class ทั้งหมด บางทีเราต้องการแบบนั้น แต่บางทีเราต้องการจะ เพิ่มและลบแค่ class เดียว

ดังนั้นเราจึงมี property อีกตัวสำหรับเหตุการนั้น elem.classList

elem.classList เป็น Object ชนิดพิเศษ และมี method add/remove/toggle ใน classList เดียว

ตัวอย่างเช่น

<body class="main page">
<script>
// add a class
document.body.classList.add('article');

alert(document.body.className); // main page article
</script>
</body>

Method ของ classList

  • elem.classList.add/remove("class") – เพิ่ม/ลบ Class
  • elem.classList.toggle("class") – เพิ่ม Class ถ้ามันไม่มี กลับกันถ้ามันไม่มี มันจะเพิ่ม Class เข้าไป
  • elem.classList.contains("class") – ตรวจสอบ Class ที่ให้ไป true/false.

classList เป็น iterable ดังนั้นเราจึงสามารถ loop มัน โดยใช้ for...of แบบนี้

<body class="main page">
<script>
for (let name of document.body.classList) {
alert(name); // main, and then page
}
</script>
</body>

Element style​

property elem.style เป็น Object ที่เกียวกับการเขียน style attribute เช่น กำหนด elem.style.width="100px" ทำงานเหมือนกัน attribute style

สำหรับ property ที่เป็นหลายคำ จะใช้เป็น camelCase

background-color  => elem.style.backgroundColor
z-index => elem.style.zIndex
border-left-width => elem.style.borderLeftWidth

ตัวอย่างเช่น

document.body.style.backgroundColor = prompt('background color?', 'green');

Resetting the style property​

บางครั้งเราต้องการที่จะกำหนดค่าของ property และลบมันออก

ตัวอย่างเช่น ถ้าเราจะซ่อน element เราสามารถกำหนดelem.style.display = "none". และหลังจากนั้น เราอาจต้องการที่จะลบ style.display แทนที่เราจะใช้ delete elem.style.display เราควรกำหนดค่ามันให้เป็น String เปล่าจะดีกว่า elem.style.display = ""

// if we run this code, the <body> will blink
document.body.style.display = "none"; // hide

setTimeout(() => document.body.style.display = "", 1000); // back to normal

เรามี method พิเศษ สำหรับการลบ property elem.style.removeProperty('style property') ดังนั้น เราสามารถลบ property ได้ แบบนี้

document.body.style.background = 'red'; //set background to red

setTimeout(() => document.body.style.removeProperty('background'), 1000); // remove background after 1 second

Mind the units​

อย่าลืมเติมหน่วยของ CSS

ตัวอย่างเช่น เราไม่ควรกำหนด elem.style.top เป็น 10 แต่ควรจะเป็น 10px แทน ในทางตรงกันข้าม มันอาจจะไม่ทำงาน

<body>
<script>
// doesn't work!
document.body.style.margin = 20;
alert(document.body.style.margin); // '' (empty string, the assignment is ignored)

// now add the CSS unit (px) - and it works
document.body.style.margin = '20px';
alert(document.body.style.margin); // 20px

alert(document.body.style.marginTop); // 20px
alert(document.body.style.marginLeft); // 20px
</script>
</body>

Computed styles: getComputedStyle​

การกำหนด style เป็นเรื่องที่ค่อนข้างง่าย แต่เราจะอ่านค่ามันได้อย่าไร

ตัวอย่างเช่น เราต้องการที่จะรู้ขนาดของ size, margin หรือ สีของ element เราสามารถ จะทำมันได้อย่างไร

style property ทำงานแค่กับ style attribute

ตัวอย่างเช่น proprety style  ไม่เห็น margin ที่ถูกกำหนดอยู่ใน tag <style>

<head>
<style> body { color: red; margin: 5px } </style>
</head>
<body>

The red text
<script>
alert(document.body.style.color); // empty
alert(document.body.style.marginTop); // empty
</script>
</body>

แต่ถ้าเราอยากบอกว่า เราต้องการเพิ่ม margin ไป 20px ล่ะ, โดยเราต้องการค่าปัจจุบันของมัน

นี่คือ method สำหรับการดึงค่ามา

getComputedStyle(element, [pseudo])

element

element ที่ต้องการจะอ่านค่า

pseudo

pseudo-element ถ้าเราต้องการจะใช้, ตัวอย่างเช่น ::before. การที่เว้นว่างไว้ หมายถึง element ตัวของมันเอง

ตัวอย่างเช่น:

<head>
<style> body { color: red; margin: 5px } </style>
</head>
<body>

<script>
let computedStyle = getComputedStyle(document.body);

// now we can read the margin and the color from it

alert( computedStyle.marginTop ); // 5px
alert( computedStyle.color ); // rgb(255, 0, 0)
</script>

</body>

Summary

ในการจัดการกับ Class เราสามารถใช้ 2 DOM properties นี้ได้

  • className – ค่าที่เป็น string, ใช้งานได้ดี เมื่อจัดการกับ ชื่อ class ทั้งหมด
  • classList – object นี้มี method add/remove/toggle/contains, ใช่เมื่อต้องการจัดการกับ class แค่บางส่วน.

ในการจัดการกับ style

  • style property เป็น Object ที่เป็น camelCased ใช้เพื่ออ่านหรือเขียน ใช้ได้เหมือนกับ attribute style ใน HTML
  • style.cssText property ใช้จัดการกับ "style" ในขนาดใหญ่ และใส่เป็น string

ในการอ่านค่า resolved (ค่าที่ถูกคำนวณมาแล้ว)

  • getComputedStyle(elem, [pseudo]) คืนค่า style ที่เป็น Object ออกมา สามารถอ่านได้อย่างเดียว

Element size and scrolling

มี properties หลายตัวที่ทำให้เราอ่าน ความกว้าง ความยาวของ element ได้

บ่อยครั้งเราต้องการใช้เมื่อเราต้องการที่จะ เคลื่อนที่ หรือ จัด position element ใน JavaScript


Sample element​

เราจะยกตัวอย่างการใช้ property ในการจัด element ที่จะทำต่อไปนี้

<div id="example">
...Text...
</div>
<style>
#example {
width: 300px;
height: 200px;
border: 25px solid #E8C48F;
padding: 20px;
overflow: auto;
}
</style>

elememt ตัวนี้ จะมี border, padding และ scrolling แต่ไม่มี margin เพราะว่ามันไม่ได้เป็นส่วนของ element และมันไม่มี property สำหรับมัน

และ element จะหน้าตาเป็นแบบนี้

image.png


Geometry​

นี่คือภาพรวมของ geometry properties:

image.png

ค่าของ properties พวกนี้เป็นตัวเลขที่อยู่ในหน่วยของ pixels ดังนั้น ค่าเหล่านี้เป็นค่าของ pixel


offsetParent, offsetLeft/Top​

properties พวกนี้ไม่ค่อยจะได้ใช้

offsetParent คือ nearest ancestor ที่ broswser ใช้สำหรับการคำนวณพิกัดของ element ขณะที่กำลังเรนเดอร์อยู่

nearest ancestor มีดังนั้น

  1. การใช้ CSS กำหนด (position คือ absolute, relative, fixed หรือ sticky), หรือ
  2. <td>, <th>, หรือ <table>, หรือ
  3. <body>.

Properties offsetLeft/offsetTop  เป็นตัวกำหนด พิกัด x/y ที่ยึดตาม offsetParent โดยยึดตามมุมซ้ายบ

ในตัวอย่างข้างล่าง <div> มี <main> เป็น offsetParent และoffsetLeft/offsetTop เลื่อนจาก มุมซ้ายบน ไป 180px

<main style="position: relative" id="main">
<article>
<div id="example" style="position: absolute; left: 180px; top: 180px">...</div>
</article>
</main>
<script>
alert(example.offsetParent.id); // main
alert(example.offsetLeft); // 180 (note: a number, not a string "180px")
alert(example.offsetTop); // 180
</script>

image.png

มีหลายครั้ง ที่ offsetParent เป็น null เหตุเกิดจาก:

  1. element ไม่ถูก show (display:none หรือ ไม่ได้อยู่ใน document).
  2. offsetParentเป็น <body> และ <html>.
  3. และ element ที่เป็น position:fixed.

offsetWidth/Height​

ไปต่อกันที่ตัว element กันเถอะ

properties สองตัวนี้เป็นตัวที่เป็นสิ่งที่เข้าใจง่าย มันเป็นตัวกำหนด ความยาว/ความกว้าง ภายในของ element พูดง่าย ๆ คือ เป็นขนาดที่รวมกับ borders ด้วย

image.png

สำหรับ element ที่เรายกตัวอย่าง:

  • offsetWidth = 390 ความกว้างภายนอก คำนวณได้โดย, ความกว้างภายใน (300px) รวมกับ paddings (2 * 20px) และ borders (2 * 25px).
  • offsetHeight = 290 – ความสูงภายนอก

clientTop/Left​

ใน element เรามี borders

ในการวัดพวกมัน มีอยู่ 2 properties clientTop and clientLeft.

ตัวอย่างเช่น

  • clientLeft = 25 – ความกว้าง border ซ้าย
  • clientTop = 25 – ความกว้าง border บน

image.png

แต่เพื่อความแม่นยำ properties พวกนี้ไม่ใช่การกำหนด borders ของมันโดยตรง แต่เหมือนเป็นการกำหนดพิกัดของมันจากมุมภายนอก

หมายความว่าอย่างไร?

เมื่อ scrollBar มาอยู่ข้างซ้าย ดังนั้น clientLeft จะรวม scrollBar ไปด้วย

เช่น clientLeft จะไม่เป็น 25 แต่จะรวมกัน scrollBar ด้วย 25 + 16 = 41

image.png


clientWidth/Height​

properties นี้จะกำหนดขนาดข้างใน border ซึ่งรวมกับ content width และ padding แต่ไม่รวมกับ scrollBar

image.png

สังเกตได้ว่า clientWidth = 324px

เรามาพิจารณา clientHeight กันก่อน

ในเมื่อแนวตั้ง ไม่มี scrollBar ดังนั้นผลรวมของ clientHeight จะรวมกับ CSS-height 200px รวมกับ padding บนและล่าง เป็น 2 * 20px รวมกันเป็น 240px

ถ้ามันไม่มี padding clientWidth/Height จะรวมกับ scrollBar ด้วย

image.png


scrollWidth/Height​

properties นี้จะเป็นเหมือน clientWidth/clientHeight แต่มันจะรวมส่วนที่หายไปด้วย (ส่วนที่เกินหน้าจอออกไป)

image.png

รูปด้านบน

  • scrollHeight = 723 – เป็นความสูงทั้งหมด ที่รวมกับส่วนที่ล้นหน้าจอออกไปด้วย.
  • scrollWidth = 324 – เป็นความกว้างทั้งหมด ถ้าเราไม่มี scrollBar ในแนวนอน ดังนั้น ค่ามันจะเท่ากับclientWidth.

เราสามารถใช้ properties นี้เพื่อขยายความสูง element ออกไปได้

// expand the element to the full content height
element.style.height = `${element.scrollHeight}px`;

ความสูงที่กำหนดจาก CSS:

image.png

ความสูงที่กำหนดจาก JavaScript:

image.png


scrollLeft/scrollTop​

properties scrollLeft/scrollTop คือความกว้าง/สูง ของส่วนที่ล้นจอออกไปด้านบน/ซ้าย

image.png

พูดง่าย ๆ คือ scrollTop คือ เราเลื่อนมันไปเท่าไหร่


Summary

element มี geometry properties ตามนี้

  • offsetParent – คือตัวที่เป็น nearest ancestor td, th, table, body.
  • offsetLeft/offsetTop – พิกัด จะยึดตามมุมซ้ายบนของ offsetParent.
  • offsetWidth/offsetHeight – ความกว้างความสูงภายนอก รวมกับ border
  • clientLeft/clientTop – ระยะห่างจาก มุมซ้ายบนของภายนอก ไปจนถึงมุมซ้ายบนของภายใน (content + padding).
  • clientWidth/clientHeight – ความสูง/กว้างของ content รวมกับ padding แต่ไม่รวมกับ scrollBar
  • scrollWidth/scrollHeight – ความสูง/กว้างของ content, เหมือนกับclientWidth/clientHeight, แต่รวมกับความสูง/กว้าง ของ element ที่พ้นจอออกไป.
  • scrollLeft/scrollTop – ความสูง/กว้างของ ส่วนที่ถูก scroll ขึ้นไปด้านบน เริ่มจากซ้านบนของ element

properties ทั้งหมดเป็น read-only ยกเว้น scrollLeft/scrollTop ที่ทำให้ browser scroll element ได้


Window sizes and scrolling

เราจะหาความกว้างความสูง ของ หน้าต่าง Browser ได้อย่างไร, เราจะหาความสูงและกว้างของ document ได้อย่างไร แล้วของส่วนที่ถูกเลื่อนไปล่ะ? เราจะเลื่อนหน้า page โดยใช้ JavaScript ได้อย่างไร

สำหรับข้อมูลประเภทนั้น เราสามารถใช้ document.documentElement ที่เกี่ยวข้องกับแท็ก <html> แต่มันยังมี method เพิ่มเติมอยู่อีก


Width/height of the window​

ในการที่จะดึงค่าความสูงและกว้างของ Browser ได้ เราสามารถใช้ clientWidht/clientHeight ของ document.documentElement:

image.png

ตัวอย่างเช่น ถ้าเราจะแสดงค่าความสูงของหน้าต่าง Browser

alert(document.documentElement.clientHeight)

Width/height of the document​

ในทางทฤษฏีแล้ว รากของ document คือ document.documentElement และมันเป็นตัวที่คลุม content ทั้งหมดไว้ เราสามาถวัดขนาดของมันได้โดยใช้ document.documentElement.scrollWidth/scrollHeight. เพื่อให้ได้ความสูงของ document ทั้งหมด เราควรใช้ค่าที่สูงสุดของ properties พวกนี้

let scrollHeight = Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight
);

alert('Full document height, with scrolled out part: ' + scrollHeight);

ไม่ใช่วิธีที่ดีนัก แต่เป็นปัญหาที่มีมาทั้งแต่นานแล้ว


Get the current scroll​

DOM element มีสถานะ current scroll ของมันใน scrollLeft/scrollTop  properties

สำหรับ document แล้ว document.documentElement.scrollLeft/scrollTop สามารถใช้ได้ใน ยกเว้นใน browser เวอร์ชั่นเก่า เช่น Safari (bug 5991) ที่เราควรใช้ document.body แทนที่จะเป็น document.documentElement.

แต่โชคดีหน่อย ที่เราไม่จำเป็นต้องเขียนข้อความยาว ๆ พวกนี้ เพราะเราสามมารถใช้ได้ ใน properties พิเศษ window.pageXOffset/pageYOffset

alert('Current scroll from the top: ' + window.pageYOffset);
alert('Current scroll from the left: ' + window.pageXOffset);

พวกนี้เป็น properties read-only


Scrolling: scrollTo, scrollBy, scrollIntoView​

สำหรับ element ธรรมดาแล้วเราสามารถใช้ scrollTop/scrollLeft ในการเลื่อน element ได้

เราสามารถทำกับ page ได้เหมือนกัน document.documentElement.scrollTop/scrollLeft ยกเว้นกับ Safari ที่เราต้องใช้ document.body.scrollTop/Left แทน

แต่มีอีกทางหนึ่ง ที่เราจะทำได้เหมือนกัน โดยเราจะใช้ properties พิเศษ  window.scrollBy(x,y) และ window.scrollTo(pageX,pageY).

  • method scrollBy(x,y) จะเลื่อนไปตามตำแหน่งที่เรากำหนด. ตัวอย่างเช่น, scrollBy(0,10) จะเลื่อน page ลงไป 10px
  • method scrollTo(pageX,pageY) เลื่อน page ไปอย่างอิสระ, ซึ่งจะยึดมุมซ้ายบนของ document. เหมือนกับการใช้scrollLeft/scrollTop.

method พวกนี้ใช้งานได้กับทุก Browser


scrollIntoView​

เพื่อความสมบูรณ์แบบ เรามาดูอีก method หนึ่งกัน elem.scrollIntoView(top). ถ้าเราเรียกใช้ elem.scrollIntoView(top) จะทำงาน scroll elem ให้เห็น

  • ถ้า top=true (เป็น default), ดังนั้น page จะเลื่อน  elem ให้ขึ้นปรากฏที่ด้านบนของหน้าต่าง browser. ส่วนขอบของด้านบน จะชิดกับด้านบนของหน้าต่าง
  • ถ้า top=false, ดังนั้น page จะเลื่อน elem ให้ขึ้นปรากฏที่ด้านล่างของหน้าต่าง browser.

Forbid the scrolling​

บางทีเราต้องการที่จะทำให้ document ไม่สามารถเลื่อนได้ ตัวอย่างเช่น เรามีข้อความเด้งขึ้นมาหน้า page และเราต้องการใช้ user ทำการโต้ตอบกับข้อความนั้น เราจึงทำให้ document ไม่สามารถเลื่อนได้

วิธีที่ทำให้ document ไม่สามารถเลื่อนได้ เราสามารถใช้ document.body.style.overflow = "hidden" ได้ เพื่อทำให้ page ไม่สามารถที่จะเลื่อนได้

  • document.body.style.overflow = ‘hidden’ ใช้เพื่อทำให้ไม่สามารถเลื่อนได้
  • document.body.style.overflow = ‘’ ใช้เพื่อทำให้เลื่อนได้อีกรอบ

Summary

Geometry:

  • ความกว้างความสูง ของ document ที่สามารถมองเห็นได้ document.documentElement.clientWidth/clientHeight

  • ความกว้างความสูง ของ document ทั้งหมด

    let scrollHeight = Math.max(
    document.body.scrollHeight, document.documentElement.scrollHeight,
    document.body.offsetHeight, document.documentElement.offsetHeight,
    document.body.clientHeight, document.documentElement.clientHeight
    );

Scrolling:

  • ในการอ่าน current scroll window.pageYOffset/pageXOffset.
  • ในการเปลี่ยน current scroll
    • window.scrollTo(pageX,pageY) – เราสามารถกำหนดได้แบบ absolute,
    • window.scrollBy(x,y) – จะเลื่อนลง/ขึ้นตามตำแหน่งปัจจุบัน,
    • elem.scrollIntoView(top) – เลื่อนเพื่อให้ elem มองเห็นได้ (จัดตำแหน่ง top/bottom ของหน้าต่าง Browser).

Coordinates

ในการเคลื่อนย้าย element เราควรคุ้นเคยกับระบบพิกัด

method ส่วนใหญ่ใน JavaScript สามารถทำงานได้กับ 2 ระบบพิกัด

  1. ยึดตามหน้าต่าง Browser - คล้ายกับ position:fixed คำนวนจากมุมซ้ายบนของหน้าต่าง
    • เราจะแสดงพิกัดเหล่านี้เป็นค่า clientX/clientY
  2. ยึกตาม document - คล้ายกับ position:absolute ใน document root, คำนวนจากมุมซ้ายบนของ document
    • เราจะแสดงพิกัดเหล่านี้เป็น pageX/pageY

เมื่อเลื่อน page ไปจนบนสุด ระยะจากบนซ้ายของ window จะมีค่าเท่ากับ ระยะจากบนซ้านของ document หมายความว่าค่าระยะจะเท่ากัน แต่หลังจากเราเลื่อน page ลงไปแล้ว ค่าที่ยึดยึดตามหน้าต่าง Browser (clientX/clientY) จะเปลี่ยน แต่ค่าที่ยึดตาม document (pageX/pageY) จะไม่เปลี่ยน

รูปภาพต่อไปนี้เป็นค่าระยะของ clientX/clientY และ pageX/pageY ของก่อนเลื่อนและหลังเลื่อน

image.png

เมื่อเราเลื่อน:

  • pageY – ค่าระยะที่ยึดกับ document จะมีค่าเท่าเดิม มันนับค่าซึ่งยึดจากด้านบนของ document
  • clientY – พิกัดที่ยึดตามหน้าต่าง Browser จะมีค่าที่เปลี่ยนแปลงไป เพราะว่ามันเข้าไปใกล้ส่วนด้านบนของ หน้าต่าง Browser

Element coordinates: getBoundingClientRect​

method elem.getBoundingClientRect() จะคืนค่าพิกัดของสำหรับสี่เหลี่ยมธรรมดา ที่ครอบคลุมไว้ elem ออกมาเป็น Object ของ Built-in Class DOMRect .

properties หลักของ DOMRect :

  • x/y – พิกัด x/y ของสี่เหลี่ยม ยึดตามหน้าต่าง Browser,
  • width/height – ความกว้างความสูงของสี่เหลี่ยม (ค่าสามารถติดลบได้).

properties เพิ่มเติม:

  • top/bottom – ค่าพิกัดแกน Y ของขอบสี่เหลี่ยม
  • left/right – ค่าพิกัดแกน X ของขอบสี่เหลี่ยม

รูปภาพของ elem.getBoundingClientRect() output:

image.png

อย่างที่เห็น, x/y และ width/height ถูกอธิบายด้วย. โดยสูตรการคำนวณคือ:

  • left = x
  • top = y
  • right = x + width
  • bottom = y + height

จำไว้ว่า

  • ค่าพิกัดสามารถเป็นทศนิยมได้, เช่น 10.5. นั่นปกติ, เพราะว่า Browser ใช้เศษส่วนในการคำนวณ.
  • ค่าพิกัดสามารถเป็นค่าติดลบได้. ตัวอย่างเช่น เราเลื่อน elem ขึ้นไปอยู่เหนือหน้าต่าง Browser และเมื่อเราใช้ elem.getBoundingClientRect().top ค่าจะเป็นค่าติดลบ

elementFromPoint(x, y)​

ในการเรียกใช้ document.elementFromPoint(x, y) จะคืนค่า ที่ใกล้ที่สุดตามพิกัดที่กำหนดให้

let elem = document.elementFromPoint(x, y);

ตัวอย่างโค้ดข้างล่าง คือ จะเปลี่ยนสี element ที่อยู่ตรงกลางจอ

let centerX = document.documentElement.clientWidth / 2;
let centerY = document.documentElement.clientHeight / 2;

let elem = document.elementFromPoint(centerX, centerY);

elem.style.background = "red";
alert(elem.tagName);

ยึดตามหน้าต่าง Browser, element จะเปลี่ยนไปตามที่เราเลื่อน


Using for “fixed” positioning​

หลาย ๆ ครั้งเราต้องการพิกัดที่ต้องการเพื่อทำการจัดตำแหน่งบางอย่าง

ในการโชว์ซักอย่างใกล้ element ที่เราต้องการเราสามารถใช้ getBoundingClientRect ในการดึงค่าพิกัด และใช้ CSS ในการจัดตำแหน่งได้

ตัวอย่างเช่น function createMessageUnder(elem, html) ใช้เพื่อโชว์ข้อความข้างล่าง element

let elem = document.getElementById("coords-show-mark");

function createMessageUnder(elem, html) {
// create message element
let message = document.createElement('div');
// better to use a css class for the style here
message.style.cssText = "position:fixed; color: red";

// assign coordinates, don't forget "px"!
let coords = elem.getBoundingClientRect();

message.style.left = coords.left + "px";
message.style.top = coords.bottom + "px";

message.innerHTML = html;

return message;
}

// Usage:
// add it for 5 seconds in the document
let message = createMessageUnder(elem, 'Hello, world!');
document.body.append(message);
setTimeout(() => message.remove(), 5000);

Summary

ทุกจดใน page มีพิกัดของมัน

  1. พิกัดที่ยึดกับหน้าต่าง Browser – elem.getBoundingClientRect().
  2. พิกัดที่ยึดกับหน้าต่าง document – elem.getBoundingClientRect() รวมกับ current page scroll

ระบบพิกัดทั้งสองอย่าง มีทั้งข้อดีและข้อเสีย; มีหลายครั้งที่เราต้องการจะใช้อย่างอื่น, ซึ่งสามารถใช้ CSS กำหนดได้ position absolute and fixed


END OF DOCUMENT