034.4 Bài 1
Chứng chỉ: |
Web Development Essentials |
---|---|
Phiên bản: |
1.0 |
Chủ đề: |
034 Lập trình JavaScript |
Mục tiêu: |
034.4 Thao tác JavaScript với Nội dung và Thiết kế của Trang Web |
Bài học: |
1 trên 1 |
Giới thiệu
HTML, CSS và JavaScript là ba công nghệ riêng biệt kết hợp với nhau trên trang Web. Để tạo ra các trang web có tính động và tương tác, lập trình viên JavaScript phải kết hợp các thành phần từ HTML và CSS trong thời gian chạy - một nhiệm vụ được hỗ trợ rất nhiều thông qua Mô hình Đối tượng Tài liệu (DOM).
Tương tác với DOM
DOM là một cấu trúc dữ liệu hoạt động như một giao diện lập trình cho tài liệu mà trong đó, mọi khía cạnh của tài liệu sẽ được biểu diễn dưới dạng một nút trong DOM và mọi thay đổi được thực hiện đối với DOM sẽ ngay lập tức được thể hiện trên tài liệu. Để hiển thị cách sử dụng DOM trong JavaScript, hãy lưu mã HTML sau vào tệp có tên example.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>HTML Manipulation with JavaScript</title>
</head>
<body>
<div class="content" id="content_first">
<p>The dynamic content goes here</p>
</div><!-- #content_first -->
<div class="content" id="content_second" hidden>
<p>Second section</p>
</div><!-- #content_second -->
</body>
</html>
DOM sẽ chỉ khả dụng sau khi HTML được tải; vì vậy, hãy viết mã JavaScript sau vào cuối phần thân trang (trước thẻ kết thúc </body>
):
<script>
let body = document.getElementsByTagName("body")[0];
console.log(body.innerHTML);
</script>
Đối tượng document
là phần tử DOM cao nhất, tất cả các phần tử khác đều sẽ phân nhánh từ nó. Phương thức getElementsByTagName()
sẽ liệt kê tất cả các phần tử giảm dần từ document
có tên thẻ đã cho. Mặc dù thẻ body
chỉ được sử dụng một lần trong tài liệu nhưng phương thức getElementsByTagName()
sẽ luôn trả về một tập hợp giống như mảng gồm các phần tử được tìm thấy; do đó, ta sẽ sử dụng chỉ mục [0]
để trả về phần tử đầu tiên (và duy nhất) được tìm thấy.
Nội dung HTML
Như đã trình bày trong ví dụ trước, phần tử DOM được trả về bởi document.getElementsByTagName("body")[0]
đã được gán cho biến body
. Sau đó, biến body
có thể được sử dụng để thao tác với phần tử nội dung của trang bởi nó kế thừa tất cả các phương thức và thuộc tính DOM từ phần tử đó. Chẳng hạn như thuộc tính innerHTML
sẽ chứa toàn bộ mã đánh dấu HTML được viết bên trong phần tử tương ứng; vì vậy, nó có thể được sử dụng để đọc mã đánh dấu bên trong. Lệnh gọi console.log(body.innerHTML)
sẽ in nội dung bên trong <body></body>
ra bảng điều khiển web. Biến cũng có thể được sử dụng để thay thế nội dung đó như trong body.innerHTML = "<p>Content erased</p>"
.
Thay vì thay đổi toàn bộ phần đánh dấu HTML, nếu ta giữ nguyên cấu trúc tài liệu và chỉ tương tác với các thành phần của nó thì sẽ thực tế hơn. Sau khi trình duyệt hiển thị tài liệu, tất cả các phần tử đều có thể được truy cập bằng các phương thức DOM. Ví dụ: ta có thể liệt kê và truy cập tất cả các phần tử HTML bằng cách sử dụng chuỗi đặc biệt *
trong phương thức getElementsByTagName()
của đối tượng document
:
let elements = document.getElementsByTagName("*");
for ( element of elements )
{
if ( element.id == "content_first" )
{
element.innerHTML = "<p>New content</p>";
}
}
Mã này sẽ đặt tất cả các phần tử được tìm thấy trong document
vào biến elements
. Biến elements
là một đối tượng giống như mảng; vì vậy, chúng ta có thể lặp qua từng mục của nó bằng một vòng lặp for
. Nếu trang HTML nơi mã này chạy có một phần tử với thuộc tính id
được đặt thành content_first
(xem trang HTML mẫu được hiển thị ở đầu bài học), câu lệnh if
sẽ khớp với phần tử đó và nội dung đánh dấu của nó sẽ được đổi thành <p>New content</p>
. Hãy lưu ý rằng các thuộc tính của một phần tử HTML trong DOM có thể truy cập được bằng cách sử dụng ký hiệu dấu chấm của các đặc tính đối tượng JavaScript; do đó, element.id
là để nói đến thuộc tính id
của phần tử hiện tại của vòng lặp for
. Phương thức getAttribute()
cũng có thể được sử dụng như trong element.getAttribute("id")
.
Không cần thiết phải lặp qua tất cả các phần tử nếu ta chỉ muốn kiểm tra một tập hợp con của chúng. Ví dụ: phương thức document.getElementsByClassName()
sẽ giới hạn các phần tử khớp với những phần tử có một hạng cụ thể:
let elements = document.getElementsByClassName("content");
for ( element of elements )
{
if ( element.id == "content_first" )
{
element.innerHTML = "<p>New content</p>";
}
}
Tuy nhiên, việc lặp qua nhiều thành phần tài liệu bằng vòng lặp không phải là chiến lược tốt nhất khi ta cần thay đổi một thành phần cụ thể trong trang.
Chọn các Yếu tố cụ thể
JavaScript cung cấp các phương pháp được tối ưu hóa để chọn chính xác phần tử mà ta muốn thao tác. Vòng lặp trước có thể được thay thế hoàn toàn bằng phương thức document.getElementById()
:
let element = document.getElementById("content_first");
element.innerHTML = "<p>New content</p>";
Mỗi thuộc tính id
trong tài liệu đều phải là duy nhất; vì vậy, phương thức document.getElementById()
sẽ chỉ trả về một đối tượng DOM duy nhất. Ngay cả việc khai báo biến element
cũng có thể được bỏ qua vì JavaScript cho phép chúng ta xâu chuỗi các phương thức một cách trực tiếp:
document.getElementById("content_first").innerHTML = "<p>New content</p>";
Phương thức getElementById()
là phương pháp thích hợp hơn để định vị các phần tử trong DOM vì hiệu suất của nó tốt hơn nhiều so với các phương thức lặp khi làm việc với các tài liệu phức tạp. Tuy nhiên, không phải tất cả các phần tử đều có một ID rõ ràng, và phương thức này sẽ trả về giá trị null nếu không có phần tử nào khớp với ID được cung cấp (điều này cũng ngăn việc sử dụng các thuộc tính hoặc hàm xâu chuỗi như innerHTML
trong ví dụ trên). Ngoài ra, sẽ thực tế hơn nếu ta chỉ gán các thuộc tính ID cho các thành phần chính của trang và sau đó sử dụng bộ chọn CSS để định vị các phần tử con của chúng.
Bộ chọn được giới thiệu trong bài học trước về CSS chính là các mẫu khớp với các phần tử trong DOM. Phương thức querySelector()
sẽ trả về phần tử khớp đầu tiên trong cây DOM, trong khi querySelectorAll()
sẽ trả về tất cả các phần tử khớp với bộ chọn đã chỉ định.
Trong ví dụ trước, phương thức getElementById()
sẽ truy xuất phần tử mang ID content_first
. Phương thức querySelector()
cũng có thể thực hiện tác vụ giống như vậy:
document.querySelector("#content_first").innerHTML = "<p>New content</p>";
Do phương thức querySelector()
sử dụng cú pháp bộ chọn nên ID được cung cấp phải bắt đầu bằng một dấu thăng (#). Nếu không tìm thấy phần tử phù hợp, phương thức querySelector()
sẽ trả về null.
Trong ví dụ trước, toàn bộ nội dung của div content_first
đã được thay thế bằng chuỗi văn bản được cung cấp. Chuỗi này có mã HTML trong đó nên đây không được coi là phương pháp hay nhất. Chúng ta cần cẩn thận khi thêm đánh dấu HTML cố định vào mã JavaScript bởi các yếu tố theo dõi có thể trở nên khó khăn trong việc thay đổi cấu trúc tài liệu tổng thể.
Bộ chọn sẽ không bị giới hạn đối với ID của phần tử. Phần tử p
bên trong có thể được xử lý trực tiếp:
document.querySelector("#content_first p").innerHTML = "New content";
Bộ chọn #content_first p
sẽ chỉ khớp với phần tử p
đầu tiên bên trong div #content_first
. Nếu chúng ta muốn thao tác với phần tử đầu tiên thì nó sẽ hoạt động bình thường. Tuy nhiên, chúng ta có thể sẽ muốn thay đổi đoạn thứ hai:
<div class="content" id="content_first">
<p>Don't change this paragraph.</p>
<p>The dynamic content goes here.</p>
</div><!-- #content_first -->
Trong trường hợp này, chúng ta có thể sử dụng hạng giả :nth-child(2)
để khớp với phần tử p
thứ hai:
document.querySelector("#content_first p:nth-child(2)").innerHTML = "New content";
Số 2
trong p:nth-child(2)
cho biết đoạn thứ hai đã khớp với bộ chọn. Hãy xem bài học bộ chọn CSS để biết thêm về bộ chọn và cách sử dụng chúng.
Làm việc với Thuộc tính
Khả năng tương tác với DOM của JavaScript không chỉ giới hạn ở việc thao tác với nội dung. Trên thực tế, JavaScript trong trình duyệt được sử dụng phổ biến nhất với mục đích sửa đổi các thuộc tính của các phần tử HTML hiện có.
Giả sử trang ví dụ HTML ban đầu của chúng ta hiện có ba phần nội dung:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>HTML Manipulation with JavaScript</title>
</head>
<body>
<div class="content" id="content_first" hidden>
<p>First section.</p>
</div><!-- #content_first -->
<div class="content" id="content_second" hidden>
<p>Second section.</p>
</div><!-- #content_second -->
<div class="content" id="content_third" hidden>
<p>Third section.</p>
</div><!-- #content_third -->
</body>
</html>
Có thể ta sẽ chỉ muốn hiển thị một trong số chúng tại một thời điểm; do đó mà ta có thuộc tính hidden
trong tất cả các thẻ div
. Điều này rất hữu ích, ví dụ như trong việc chỉ hiển thị một hình ảnh từ bộ sưu tập hình ảnh. Để hiển thị một trong số chúng khi tải trang, hãy thêm mã JavaScript sau vào trang:
// Which content to show
let content_visible;
switch ( Math.floor(Math.random() * 3) )
{
case 0:
content_visible = "#content_first";
break;
case 1:
content_visible = "#content_second";
break;
case 2:
content_visible = "#content_third";
break;
}
document.querySelector(content_visible).removeAttribute("hidden");
Biểu thức được đánh giá bởi câu lệnh switch
sẽ trả về số 0, 1 hoặc 2 một cách ngẫu nhiên. Sau đó, bộ chọn ID tương ứng sẽ được gán cho biến content_visible
được sử dụng bởi phương thức querySelector(content_visible)
. Lệnh gọi removeAttribute("hidden")
được xâu chuỗi sẽ loại bỏ thuộc tính hidden
khỏi phần tử.
Cách tiếp cận ngược lại cũng có thể được áp dụng: tất cả các phần có thể hiển thị ban đầu (không có thuộc tính hidden
) và chương trình JavaScript sau đó có thể gán thuộc tính hidden
cho mọi phần ngoại trừ phần trong content_visible
. Để làm như vậy, ta phải lặp qua tất cả các phần tử div nội dung khác với phần tử đã chọn. Điều này có thể được thực hiện bằng cách sử dụng phương thức querySelectorAll()
:
// Which content to show
let content_visible;
switch ( Math.floor(Math.random() * 3) )
{
case 0:
content_visible = "#content_first";
break;
case 1:
content_visible = "#content_second";
break;
case 2:
content_visible = "#content_third";
break;
}
// Hide all content divs, except content_visible
for ( element of document.querySelectorAll(".content:not("+content_visible+")") )
{
// Hidden is a boolean attribute, so any value will enable it
element.setAttribute("hidden", "");
}
Nếu biến content_visible
được đặt thành #content_first
, bộ chọn sẽ là .content:not(#content_first)
và nó sẽ đọc tất cả các phần tử có hạng content
ngoại trừ những phần tử có ID content_first
. Phương thức setAttribute()
sẽ thêm hoặc thay đổi các thuộc tính của phần tử HTML. Tham số đầu tiên của nó là tên của thuộc tính và tham số thứ hai là giá trị của thuộc tính.
Tuy nhiên, cách thích hợp để thay đổi giao diện của các phần tử là sử dụng CSS. Trong trường hợp này, chúng ta có thể đặt thuộc tính CSS display
thành hidden
và sau đó thay đổi nó thành block
bằng JavaScript:
<style>
div.content { display: none }
</style>
<div class="content" id="content_first">
<p>First section.</p>
</div><!-- #content_first -->
<div class="content" id="content_second">
<p>Second section.</p>
</div><!-- #content_second -->
<div class="content" id="content_third">
<p>Third section.</p>
</div><!-- #content_third -->
<script>
// Which content to show
let content_visible;
switch ( Math.floor(Math.random() * 3) )
{
case 0:
content_visible = "#content_first";
break;
case 1:
content_visible = "#content_second";
break;
case 2:
content_visible = "#content_third";
break;
}
document.querySelector(content_visible).style.display = "block";
</script>
Các phương pháp hay tương tự áp dụng cho việc kết hợp các thẻ HTML với JavaScript cũng sẽ áp dụng cho CSS. Do đó, ta không nên viết các thuộc tính CSS trực tiếp bằng mã JavaScript. Thay vào đó, các quy tắc CSS nên được viết riêng khỏi JavaScript. Cách thích hợp để thay thế thiết kế trực quan là chọn một hạng CSS được xác định trước cho phần tử.
Làm việc với các Hạng
Các phần tử có thể có nhiều hơn một hạng liên kết giúp cho việc thêm hoặc bớt trong khi viết các kiểu dáng khi cần thiết trở nên dễ dàng hơn. Sẽ rất khó để thay đổi trực tiếp nhiều thuộc tính CSS trong JavaScript; vì vậy, ta có thể tạo một hạng CSS mới với các thuộc tính đó rồi thêm hạng vào phần tử. Các phần tử DOM có thuộc tính classList
có thể được sử dụng để xem và thao tác các hạng được gán cho phần tử tương ứng.
Ví dụ: thay vì thay đổi mức độ hiển thị của phần tử, chúng ta có thể tạo một hạng CSS bổ sung để làm nổi bật div content
:
div.content {
border: 1px solid black;
opacity: 0.25;
}
div.content.highlight {
border: 1px solid red;
opacity: 1;
}
Biểu định kiểu này sẽ thêm một đường viền đen mỏng và nửa trong suốt cho tất cả các phần tử có hạng content
. Chỉ những phần tử cũng có hạng highlight
mới hoàn toàn trong suốt và có đường viền mỏng màu đỏ. Sau đó, thay vì thay đổi trực tiếp các thuộc tính CSS như đã làm trước đây, ta có thể sử dụng phương thức classList.add("highlight")
trong phần tử đã chọn:
// Which content to highlight
let content_highlight;
switch ( Math.floor(Math.random() * 3) )
{
case 0:
content_highlight = "#content_first";
break;
case 1:
content_highlight = "#content_second";
break;
case 2:
content_highlight = "#content_third";
break;
}
// Highlight the selected div
document.querySelector(content_highlight).classList.add("highlight");
Tất cả các kỹ thuật và ví dụ mà chúng ta đã thấy cho đến nay đều được thực hiện ở cuối quá trình tải trang, nhưng chúng sẽ không bị giới hạn ở giai đoạn này. Trên thực tế, điều khiến JavaScript trở nên hữu ích đối với các nhà phát triển Web là khả năng phản ứng với các sự kiện trên trang mà chúng ta sẽ xem tiếp theo đây.
Trình Xử lý Sự kiện
Tất cả các thành phần trang để hiển thị đều dễ bị ảnh hưởng bởi các sự kiện tương tác, chẳng hạn như nhấp chuột hoặc chuyển động của chính con chuột. Chúng ta có thể liên kết các hành động tùy chỉnh với những sự kiện này, giúp mở rộng đáng kể những gì một tài liệu HTML có thể thực hiện.
Có lẽ phần tử HTML rõ ràng nhất được hưởng lợi từ một hành động được liên kết là phần tử button
. Để biết nó hoạt động như thế nào, hãy thêm ba nút phía trên phần tử div
đầu tiên của trang ví dụ:
<p>
<button>First</button>
<button>Second</button>
<button>Third</button>
</p>
<div class="content" id="content_first">
<p>First section.</p>
</div><!-- #content_first -->
<div class="content" id="content_second">
<p>Second section.</p>
</div><!-- #content_second -->
<div class="content" id="content_third">
<p>Third section.</p>
</div><!-- #content_third -->
Các nút sẽ không tự làm bất cứ việc gì. Nhưng giả sử ta muốn đánh dấu div
tương ứng với nút được nhấn, chúng ta có thể sử dụng thuộc tính onClick
để liên kết một hành động với từng nút:
<p>
<button onClick="document.getElementById('content_first').classList.toggle('highlight')">First</button>
<button onClick="document.getElementById('content_second').classList.toggle('highlight')">Second</button>
<button onClick="document.getElementById('content_third').classList.toggle('highlight')">Third</button>
</p>
Phương thức classList.toggle()
sẽ thêm hạng đã chỉ định vào phần tử nếu nó chưa tồn tại và sẽ loại bỏ hạng đó nếu đã có. Nếu chạy ví dụ này, bạn sẽ thấy được rằng ta có thể làm nổi bật nhiều div
cùng một lúc. Để chỉ đánh dấu div
tương ứng với nút được nhấn, ta cần phải xóa hạng highlight
khỏi các thành phần div
khác. Tuy nhiên, nếu hành động tùy chỉnh quá dài hoặc liên quan đến nhiều dòng mã, việc viết một hàm ngoài thẻ phần tử sẽ thực tế hơn:
function highlight(id)
{
// Remove the "highlight" class from all content elements
for ( element of document.querySelectorAll(".content") )
{
element.classList.remove('highlight');
}
// Add the "highlight" class to the corresponding element
document.getElementById(id).classList.add('highlight');
}
Giống như các ví dụ trước, hàm này có thể được đặt bên trong thẻ <script>
hoặc trong tệp JavaScript bên ngoài được liên kết với tài liệu. Trước tiên, hàm highlight
sẽ xóa hạng highlight
khỏi tất cả các phần tử div
được liên kết với hạng content
, sau đó thêm hạng highlight
vào phần tử đã chọn. Sau đó, mỗi nút sẽ gọi hàm này từ thuộc tính onClick
của nó và sử dụng ID tương ứng làm đối số của hàm:
<p>
<button onClick="highlight('content_first')">First</button>
<button onClick="highlight('content_second')">Second</button>
<button onClick="highlight('content_third')">Third</button>
</p>
Ngoài thuộc tính onClick
, chúng ta có thể sử dụng thuộc tính onMouseOver
(được kích hoạt khi thiết bị trỏ được sử dụng để di chuyển con trỏ lên phần tử), thuộc tính onMouseOut
(được kích hoạt khi thiết bị trỏ không còn được chứa bên trong phần tử), v.v. Ngoài ra, các trình xử lý sự kiện cũng không bị hạn chế đối với các nút; vì vậy, bạn có thể chỉ định các hành động tùy chỉnh cho các trình xử lý sự kiện này đối với tất cả các phần tử HTML hiển thị.
Bài tập Hướng dẫn
-
Bằng cách sử dụng phương thức
document.getElementById()
, bạn có thể chèn cụm từ “Dynamic content” vào nội dung bên trong của phần tử có ID làmessage
như thế nào? -
Sự khác biệt giữa việc tham chiếu một phần tử bằng ID của nó bằng cách sử dụng phương thức
document.querySelector()
và thực hiện như vậy thông qua phương thứcdocument.getElementById()
là gì? -
Mục đích của phương thức
classList.remove()
là gì? -
Kết quả của việc sử dụng phương thức
myelement.classList.toggle("active")
là gì nếumyelement
không có hạngactive
được gán cho nó?
Bài tập Mở rộng
-
Đối số nào của phương thức
document.querySelectorAll()
sẽ làm cho nó bắt chước phương thứcdocument.getElementsByTagName("input")
? -
Làm cách nào để có thể sử dụng thuộc tính
classList
để liệt kê tất cả các hạng được liên kết với một phần tử đã cho?
Tóm tắt
Bài học này đã trình bày cách sử dụng JavaScript để thay đổi nội dung HTML và các thuộc tính CSS của chúng bằng cách sử dụng DOM (Mô hình Đối tượng Tài lliệu). Những thay đổi này có thể được kích hoạt bởi các sự kiện của người dùng và sẽ rất hữu ích trong việc tạo giao diện động. Bài học đã đi qua các khái niệm và quy trình sau:
-
Cách kiểm tra cấu trúc của tài liệu bằng các phương thức như
document.getElementById()
,document.getElementsByClassName()
,document.getElementsByTagName()
,document.querySelector()
vàdocument.querySelectorAll()
. -
Cách thay đổi nội dung tài liệu bằng thuộc tính
innerHTML
. -
Cách thêm và sửa đổi các thuộc tính của thành phần trang bằng các phương thức
setAttribute()
vàremoveAttribute()
. -
Cách thích hợp để thao tác với các hạng của phần tử bằng cách sử dụng thuộc tính
classList
và mối quan hệ của nó với các thiết kế CSS. -
Cách liên kết các hàm với sự kiện của chuột trong các phần tử cụ thể.
Đáp án Bài tập Hướng dẫn
-
Bằng cách sử dụng phương thức
document.getElementById()
, bạn có thể chèn cụm từ “Dynamic content” vào nội dung bên trong của phần tử có ID làmessage
như thế nào?Nó có thể được thực hiện với đặc tính
innerHTML
:document.getElementById("message").innerHTML = "Dynamic content"
-
Sự khác biệt giữa việc tham chiếu một phần tử bằng ID của nó bằng cách sử dụng phương thức
document.querySelector()
và thực hiện như vậy thông qua phương thứcdocument.getElementById()
là gì?ID phải đi kèm với ký tự dấu thăng trong các hàm sử dụng bộ chọn, chẳng hạn như
document.querySelector()
. -
Mục đích của phương thức
classList.remove()
là gì?Nó loại bỏ hạng (có tên được cung cấp làm đối số của hàm) khỏi thuộc tính
class
của phần tử tương ứng. -
Kết quả của việc sử dụng phương thức
myelement.classList.toggle("active")
là gì nếumyelement
không có hạngactive
được gán cho nó?Phương thức này sẽ gán hạng
active
chomyelement
.
Đáp án Bài tập Mở rộng
-
Đối số nào của phương thức
document.querySelectorAll()
sẽ làm cho nó bắt chước phương thứcdocument.getElementsByTagName("input")
?Việc sử dụng
document.querySelectorAll("input")
sẽ khớp với tất cả các phần tửinput
trong trang, giống nhưdocument.getElementsByTagName("input")
. -
Làm cách nào để có thể sử dụng thuộc tính
classList
để liệt kê tất cả các hạng được liên kết với một phần tử đã cho?Thuộc tính
classList
là một đối tượng giống như mảng; do đó, vòng lặpfor
có thể được sử dụng để lặp qua tất cả các hạng mà nó chứa.