Sử dụng page fragments (các phần trang) cho EzyPlatform theme
Back To BlogsHiểu được điều này EzyPlatform đã tạo ra plugin EzyArticle để cho phép bạn phân mảnh hoá giao diện, từ đó người dùng có tạo được giao diện thông qua admin và ghi đè giao diện mặc định.
Mục tiêu
Thông qua bài này bạn sẽ có thể:
- Đăng ký các page fragment thông qua lớp config
- Đưa page fragment vào view
- Sử dụng page fragment
Đăng ký các phần trang (page fragment)
Hãy chúng ta sẽ tiếp tục phát triển theme personal, bây giờ chúng ta có thể tạo lớp PersonalPageFragmentConfig tại module personal-sdk với mã nguồn như sau:
package org.youngmonkeys.personal.config; import com.tvd12.ezyfox.bean.EzyBeanConfig; import com.tvd12.ezyfox.bean.annotation.EzyAutoBind; import lombok.Setter; import org.youngmonkeys.ezyarticle.sdk.manager.PageFragmentManager; @Setter public class PersonalPageFragmentConfig implements EzyBeanConfig { @EzyAutoBind private PageFragmentManager pageFragmentManager; @Override public void config() { pageFragmentManager.registerFragmentNames( "common", "styles", "scripts", "header", "footer" ); pageFragmentManager.registerFragmentNames( "home", "container" ); pageFragmentManager.registerFragmentNames( "blog_details", "container" ); } }
Ở đây chúng ta đã đăng ký các phần trang cho:
common: Đây là nhóm các phần trang dùng chung cho mọi trang, cụ thể ở đây chúng ta có các phần trangstylescung cấp các mã nguồn style dùng chung, phần trangscriptscung cấp mã nguồn script dùng chung, phần trangheaderchứa mã nguồn html cho phần đầu trang dùng chung vàfooterchứ mã nguồn html cho phần chân trang dùng chung.home: Đây là nhóm phần trang dành cho trang chủ, ở đây chúng ta sẽ chỉ có 1 phần trang duy nhất làcontentchứa toàn bộ html của trang chủ.blog_details: Đây là nhóm phần trang dành cho trang chi tiết blog, ở đây chúng ta cũng sẽ chỉ có 1 phần trang duy nhất làcontentchứa toàn bộ html của chi tiết bài blog.
Tiếp theo, chúng ta sẽ tạo lớp WebPersonalPageFragmentConfig tại module personal-web-pluginvới mã nguồn như sau:
package org.youngmonkeys.personal.web.config; import com.tvd12.ezyfox.bean.annotation.EzyConfigurationAfter; import org.youngmonkeys.personal.config.PersonalPageFragmentConfig; @EzyConfigurationAfter public class WebPersonalPageFragmentConfig extends PersonalPageFragmentConfig {}
Mục tiêu của nó là để đăng ký các mảnh trang với đối tượng quản lý mảnh trang ở web (WebPageFragmentManager).
Tiếp theo chúng ta sẽ tạo lớp AdminPersonalPageFragmentConfig tại module personal-admin-plugin với mã nguồn như sau:
package org.youngmonkeys.personal.admin.config; import com.tvd12.ezyfox.bean.annotation.EzyConfigurationAfter; import org.youngmonkeys.personal.config.PersonalPageFragmentConfig; @EzyConfigurationAfter public class AdminPersonalPageFragmentConfig extends PersonalPageFragmentConfig {}
Mục tiêu của nó là để đăng ký các mảnh trang với đối tượng quản lý mảnh trang ở admin (AdminPageFragmentManager).
Lúc này khi truy cập vào các phần trang ở admin bạn sẽ thấy danh sách các phần trang đã được đăng ký hiển thị trong danh sách các phần trang kiểu thế này:
Đưa các phần trang vào view
Để có thể đưa phần trang dùng chung vào view, trước hết chúng ta sẽ cần cập nhật lớp WebPersonalViewDecorator với mã nguồn như sau:
@EzySingleton @AllArgsConstructor public class WebPersonalViewDecorator extends WebViewDecorator { private final WebPageFragmentManager pageFragmentManager; // các mã nguồn khác private final WebLanguageControllerService languageControllerService; @SuppressWarnings("MethodLength") @Override public void decorate(HttpServletRequest request, View view) { super.decorate(request, view); // các mã nguồn khác String languageCode = languageControllerService .getLanguageCodeOrDefault(request); view.setVariable( "commonFragments", pageFragmentManager.getPageFragmentMap( "common", languageCode ) ); } // các mã nguồn khác }
Ở đây, do đã đăng ký các phần trang dùng chung ở lớp config rồi nên chúng ta sẽ cần chỉ định common cho hàm getPageFragmentMap và nó sẽ lấy toàn bộ các phần trang mà chúng ta đã đăng ký.
Bây giờ chúng ta có thể cập nhật template page.html như sau:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:ezy="http://www.ezyplatform.com/thymeleaf/layout"> <head th:remove="tag"> <th:block th:if="${commonFragments.get('styles') == null || commonFragments.get('styles').content == 'styles'}"> <link rel="stylesheet" type="text/css" ezy:vhref="/css/main.css" /> </th:block> <th:block th:if="${commonFragments.get('styles') != null && commonFragments.get('styles').content != 'styles'}"> <ezy:block ezy:utext="${commonFragments.get('styles').content}" /> </th:block> <th:block th:if="${pageFragments != null && pageFragments.get('container') != null && !#strings.isEmpty(pageFragments.get('container').additionalHead)}"> <ezy:block ezy:utext="${pageFragments.get('container').additionalHead}" /> </th:block> <th:block th:if="${commonFragments.get('footer') != null}"> <ezy:block ezy:utext="${commonFragments.get('footer').additionalHead}" /> </th:block> </head> <body th:remove="tag"> <div class="wrapper light-theme" id="theme-wrapper"> <th:block th:if="${commonFragments.get('header') != null && commonFragments.get('header').content != 'header'}"> <ezy:block ezy:utext="${commonFragments.get('header').content}" /> </th:block> <th:block th:if="${commonFragments.get('header') == null || commonFragments.get('header').content == 'header'}"> <header class="header"> <div th:replace="~{fragments/header :: header}"></div> </header> </th:block> <th:block th:if="${pageFragments != null && pageFragments.get('container') != null && pageFragments.get('container').content != 'container'}"> <ezy:block ezy:utext="${pageFragments.get('container').content}" /> </th:block> <th:block th:if="${pageFragments == null || pageFragments.get('container') == null || pageFragments.get('container').content == 'container'}"> <main> <div layout:fragment="content"></div> </main> </th:block> <th:block th:if="${commonFragments.get('footer') != null && commonFragments.get('footer').content != 'footer'}"> <th:block th:utext="${commonFragments.get('footer').content}" /> </th:block> <th:block th:if="${commonFragments.get('footer') == null || commonFragments.get('footer').content == 'footer'}"> <footer th:replace="~{fragments/footer :: footer}"></footer> </th:block> </div> <div id="loadingScreen" class="screen-loading d-none"> <i class="fas fa-3x fa-sync fa-spin text-secondary"></i> </div> <th:block layout:fragment="modals"></th:block> <!-- REQUIRED SCRIPTS --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <th:block th:if="${commonFragments.get('scripts') == null || commonFragments.get('scripts').content == 'scripts'}"> <script type="module" src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"></script> <script nomodule src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"></script> <script ezy:vsrc="/js/main.js" type="text/javascript"></script> </th:block> <th:block th:if="${commonFragments.get('scripts') != null && commonFragments.get('scripts').content != 'scripts'}"> <ezy:block ezy:utext="${commonFragments.get('scripts').content}" /> </th:block> <!-- OPTIONAL SCRIPTS --> <script layout:fragment="import-scripts" th:remove="tag"></script> <script layout:fragment="pre-main-scripts" type="text/javascript"></script> <script layout:fragment="scripts" type="text/javascript"></script> <script layout:fragment="post-scripts" type="text/javascript"></script> <script> $( document ).ready(function() { ezyweb.formatDateStringElements(); ezyweb.formatDateTimeStringElements(); ezyweb.formatDateTimeMinuteStringElements(); ezyweb.formatStatusTextElements(); ezyweb.formatNumberWithCommasElements(); if (ezyweb.lang) { ezyweb.appendLangParameterToLinks(ezyweb.lang); } }); </script> <th:block th:if="${commonFragments.get('header') != null && !#strings.isEmpty(commonFragments.get('header').additionalFoot)}"> <ezy:block ezy:utext="${commonFragments.get('header').additionalFoot}" /> </th:block> <th:block th:if="${commonFragments.get('header') == null || #strings.isEmpty(commonFragments.get('header').additionalFoot)}"> <script th:replace="~{fragments/header :: scripts}" type="text/javascript"></script> </th:block> <th:block th:if="${commonFragments.get('footer') != null && !#strings.isEmpty(commonFragments.get('footer').additionalFoot)}"> <ezy:block ezy:utext="${commonFragments.get('footer').additionalFoot}" /> </th:block> </body> </html>
Ở đây rất nhiều if else, nguyên nhân là do chúng ta cần kiểm tra xem phần trang đã được cập nhật thông qua admin hay chưa, thông qua việc kiểm tra, ví dụ commonFragments.get('styles') == nullđể kiểm tra phần trang styles đã được khai báo hay chưa, và commonFragments.get('styles').content == 'styles' để kiểm tra xem giá trị của phần trang vẫn đang là mặc định hay đã bị thay đổi. Nếu có phần trang và phần trang đã bị thay đổi, chúng ta sẽ lấy phần trang từ admin còn không thì chúng ta sẽ lấy theo mặc định là mã nguồn template chúng ta đã cung cấp bằng sẵn.
Sử dung page fragment
Bây giờ đến phần thú vị, chúng ta hãy trở lại với các phần trang ở admin, tìm đến phần trang container của Blog Details và click vào. Lúc này editor sẽ mở ra và chúng ta có thể thay đổi nội dung mặc định thành bất cứ nội dung thymeleaf nào mà bạn muốn, ví dụ:
<div class="container"> <section class="page-header"> <div class="container"> <div class="breadcrumb"> <h3 class="title">[[${blog.title}]]</h3> <a class="link" href="/"> <h5> <i class="fa-solid fa-angles-left"></i> Back To Home </h5> </a> </div> </div> </section> <section class="page-content page-article"> <div class="row"> <div class="col-12"> <span>[[#{posted_by}]]: [[${blog.author.name}]]</span> <span> [[${#strings.toLowerCase(#messages.msg('at'))}]]: <span class="date-time-minute-string">[[${blog.publishedAt}]]</span> </span> </div> <div th:if="${blog.terms.size() > 0}" class="col-12"> <div class="terms-wrapper"> <span>[[#{in}]]:</span> <div class="terms"> <a th:each="term : ${blog.terms}"> [[${term.name}]] </a> </div> </div> </div> <div class="col-lg-2"></div> <div class="col-lg-8"> <div class="row"> <div th:if="${blog.featuredImage != null}"> <img th:src="${blog.featuredImage.getUrlOrNull()}"> </div> <div class="col-md-12 article-content" th:utext="${blog.content}"></div> </div> </div> <div class="col-lg-2"></div> </div> </section> </div>
Sau đó trở lại giao diện chi tiết một bài blog ở web và refresh lại, bạn sẽ nhận được kết quả tương ứng với những thay đổi của mình.