Hi 🤓 Cảm ơn bạn đã ghé thăm blog này, nếu những bài viết trên blog giúp ích cho bạn. Bạn có thể giúp blog hiển thị quảng cáo bằng cách tạm ngừng ad blocker 😫 và để giúp blog duy trì hoạt động nếu bạn muốn.
Cảm ơn bạn!

Giới thiệu

👉 Hiện nay có rất nhiều các plugin giúp tạo Masonry Layout một cách nhanh chóng lấy ví dụ như Boxify - jquery.

👉 Nếu như các bạn không thích dùng plugin hoặc là một người đang học Javascript thì mình sẽ hướng dẫn cho các bạn cách để tạo bố cục này chỉ bằng Javascript nhé. 👌

Thực hành

Đầu tiên chúng ta sẽ làm HTML trước nhé.

👉 Các bạn thêm bao nhiêu hình ảnh tùy ý nhé. ở đây mình chỉ lấy ví dụ một vài hình như ở bên dưới

<div class="main">
  <div class="header-content">
    <h1>🚀👉 Masonry Layout Tutorial 🌈</h1>
    <h2>By 👉 Homiedev Blog 👩🏻‍💻</h2>
  </div>

  <div class="masonry-layout">
    <div class="masonry-item">
      <img
           src="images/7000a4587f964817ae2767724ff48323.jpg"
           alt=""
           />
    </div>
    <div class="masonry-item">
      <img
           src="images/9a6a26f2a3935d6fe61f991582ddf71e.jpg"
           alt=""
           />
    </div>
    ...
  </div>
</div>

Tiếp theo là style cho bố cục nhé 👉

* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}
h1 {
    font-family: Georgia, 'Times New Roman', Times, serif;
    color: rgb(36, 73, 153);
}
h2 {font-family: monospace;}
body {
    max-width: 1200px;
    margin: 0 auto;
}
.header-content {
    margin-top: 50px;
    text-align: center;
}
.masonry-layout {
    display: flex;
    margin-top: 30px;
}
.masonry-item img {width: 100%;}

🔥🔥 Nào bây giờ ta sẽ tới phần Javascript nhé, mình sẽ giải thích cho các bạn hiểu.

Mình tạm gọi các phần tử con trong masonry layout là masonry item cho các bạn dễ hình dung nhé 😄

Đầu tiên chúng ta sẽ tạo một function tên là make_masonry_layout. Các parameter của function là:

⚡ parentDiv : tên parent class hoặc parent id của các masonry item.

⚡ column : số lượng cột chứa các masonry item, mình sẽ mặc định là 3 cột.

⚡ gap : khoảng cách giữa các cột hay khoảng cách giữa các masonry item

function make_masonry_layout(parentDiv, column = 3, gap = 30) {}

Tiếp theo ta sẽ lấy tất cả các masonry item trong masonry và parent div của nó.

🖐 Vì mình dùng method document.querySelector() nên các bạn sẽ truyền cho parentDiv là một string cùng với '.' nếu là get bằng class hoặc '#' nếu là get bằng id nhé.

👉 Ví dụ: Bạn get bằng class thì parentDiv truyền vào sẽ là '.masonry-layout'

const parent = document.querySelector(parentDiv);
const masonryItems = document.querySelectorAll('.masonry-item');

Tiếp tục nhé, mình sẽ cho parentDiv = '' để remove toàn bộ các masonry item trong nó.

Các bạn yên tâm vì mình đã lưu các masonry item vào trong biến masonryItems rồi.

parent.innerHTML = '';

Sau đó, dựa vào parameter column đã pass vào ở trên, ta sẽ thực hiện tìm width của mỗi cột. Mình đã thêm 1 vài class vào để tiện theo dõi và dễ style hơn. cuối cùng là thêm các cột đó vào parent thôi 😄

for (let i = 0; i < column; i++) {
  const newColumn = document.createElement('div');
  newColumn.classList.add('masonry-column', `column-${i + 1}`);
  newColumn.style.width = `calc(${100 / column}% - ${gap}px)`;
  newColumn.style.margin = `0 ${gap / 2}px`;

  parent.appendChild(newColumn);
}

Như đoạn code ở trên thì mình tìm width của column bằng cách lấy width = 100% chia cho số cột, sau đó bỏ đi phần khoảng cách giữa các column là gap.

👉 ví dụ: có 4 cột và khoảng cách giữa các cột là 30px thì mình lấy (100%/4 - 30px) sẽ ra width của một cột.

Khoảng cách gap giữa các cột sẽ được tính bằng margin-leftmargin-right.

👉 Vậy nên khoảng cách giữa các cột sẽ là newColumn.style.margin = `0 ${gap / 2}px`;

Cuối cùng ta chỉ cần thêm các masonry item lần lượt vào các column ta tạo ở trên.

Mình làm như sau:

Đầu tiên mình đã có số column, mình sẽ tìm chiều cao nhỏ nhất trong số các column đó và thêm masonry item vào, và cứ tiếp tục như vậy mình sẽ thêm đầy đủ các phần tử vào các column.

Để thuận tiện cho việc tìm chiều cao của column (ban đầu height column có giá trị 0) mình sẽ thêm 3 masonry item (giả sử có 3 column) đầu tiên vào lần lượt các cột.

const columns = parent.querySelectorAll('.masonry-column');
for (let i = 0; i < column; i++) {
  columns[i].appendChild(masonryItems[i]);
}

Tiếp theo mình sẽ tìm chiều cao nhỏ nhất trong số 3 column ở trên và thêm các phần tử vào.

Mình sẽ khởi tạo các giá trị sau:

heightColumn = columns[0].offsetHeight 👉 tương ứng là giá trị ban đầu của giá trị chiều cao column (Mình dùng để tìm chiều cao nhỏ nhất column thông qua offsetHeight).

columnMinHeight = columns[0]; để lưu phần tử column nhỏ nhất dùng để thêm masonry item.

for (let i = column; i < masonryItems.length; i++) {
  let heightColumn = columns[0].offsetHeight;

  let columnMinHeight = columns[0];

  // find min height of column and store it
  for (let index = 1; index < column; index++) {
    if (columns[index].offsetHeight < heightColumn) {
      heightColumn = columns[index].offsetHeight;
      columnMinHeight = columns[index];
    }
  }

  columnMinHeight.appendChild(masonryItems[i]);
}

Chúng ta cần thêm khoảng cách bên dưới cho các masonry item.

masonryItems.forEach((element) => {
  element.style.marginBottom = `${gap}px`;
});

Kết quả chúng ta sẽ được như hình dưới 👇

masonry1 Tuy nhiên nếu ta để ý hình số 1 và 3 đã bị kéo nhỏ lại vì margin-left và margin-right, vậy nên ta sẽ cho class parent margin số âm để kéo ra lại,

parent.style.marginLeft = `${-gap / 2}px`;
parent.style.marginRight = `${-gap / 2}px`;

Khi dùng javascript (offsetHeight,clientHeight,scrollHeight) để tìm chiều cao của 1 phần tử ta sẽ gặp một vấn đề là chiều cao tìm được không thực sự như mong muốn. Để fix lỗi này mình có tăng độ trễ bằng phương thức setTimeout.

👉 Đây là toàn bộ phần code javascript của chúng ta:

function make_masonry_layout(parentDiv, column = 3, gap = 30) {
  const parent = document.querySelector(parentDiv);
  const masonryItems = document.querySelectorAll('.masonry-item');

  parent.innerHTML = ''; // make parent empty
  parent.style.marginLeft = `${-gap / 2}px`;
  parent.style.marginRight = `${-gap / 2}px`;

  for (let i = 0; i < column; i++) {
    const newColumn = document.createElement('div'); // create one column
    newColumn.classList.add('masonry-column', `column-${i + 1}`);// add class
    // set spacing of column with gap
    newColumn.style.width = `calc(${100 / column}% - ${gap}px)`;
    newColumn.style.margin = `0 ${gap / 2}px`;
    parent.appendChild(newColumn); // add column into parent div
  }

  const columns = parent.querySelectorAll('.masonry-column');
  masonryItems.forEach((element) => {
    element.style.marginBottom = `${gap}px`;
  });

  setTimeout(() => {
    for (let i = 0; i < column; i++) {
      columns[i].appendChild(masonryItems[i]);
    }
    for (let i = column; i < masonryItems.length; i++) {
      let heightColumn = columns[0].offsetHeight;

      let columnMinHeight = columns[0];

      // find min height of column and store it
      for (let index = 1; index < column; index++) {
        if (columns[index].offsetHeight < heightColumn) {
          heightColumn = columns[index].offsetHeight;
          columnMinHeight = columns[index];
        }
      }

      columnMinHeight.appendChild(masonryItems[i]);
    }
  }, 20);
}

Về phần responsive, mình sẽ thêm các media query và tạo sự kiện mỗi khi thay đổi độ rộng màn hình layout tương ứng sẽ kích hoạt. Tìm hiểu thêm về matchMedia tại đây.

const media = [
  window.matchMedia('(min-width:1200px)'),
  window.matchMedia('(min-width:992px)'),
  window.matchMedia('(min-width:576px)'),
];

responsive(); // call listener at first time

for (let i = 0; i < media.length; i++) {
  media[i].addEventListener('change', responsive); // attach listener function to listen when state changes
}

function responsive() {
  if (media[0].matches) {
    // console.log(media[0]);
    make_masonry_layout('.masonry-layout');
  } else if (media[1].matches) {
    // console.log(media[1]);
    make_masonry_layout('.masonry-layout', 2);
  } else {
    // console.log(media[2]);
    make_masonry_layout('.masonry-layout', 1);
  }
}

Kết luận

🤷‍♀️ Như vậy là chúng ta đã tạo được Masonry Layout rồi 🎉🎉. Hy vọng sau bài viết này các bạn có thể hiểu và thực hành nó. Để khi gặp các Layout phức tạp khác mà phải xử lí bằng Javascript thì các bạn vẫn có thể làm được nhé. 😁👌

👉 Nếu có thắc mắc gì các bạn có thể liên hệ với mình nha 🖐🙌.

Có thể bạn thích ⚡
homiedev
About Me

Hi, I'm @devnav. Một người thích chia sẻ kiến thức, đặc biệt là về Frontend 🚀. Trang web này được tạo ra nhằm giúp các bạn học Frontend hiệu quả hơn 🎉😄.

Chúc các bạn tìm được kiến thức hữu ích trong blog này 😁😁.