Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/demo-zk-books/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
<timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyyMMdd</maven.build.timestamp.format>

<tools.version>26.6.0</tools.version>
<tools.version>26.7.0</tools.version>

</properties>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package mybookstore.providers;

import tools.dynamia.integration.sterotypes.Component;
import tools.dynamia.zk.ui.MicroFrontendHostContextProvider;

import java.util.Map;

/**
* Demonstrates tools.dynamia.zk.ui.MicroFrontendHostContextProvider: every value returned here
* reaches every mounted microfrontend on this app automatically as the "dynamiaHost" prop, without
* any ZUL author having to bind it by hand. See microfrontend-demo.zul.
*/
@Component
public class DemoHostContextProvider implements MicroFrontendHostContextProvider {

@Override
public Map<String, Object> getHostContext() {
return Map.of(
"tenantId", "demo-bookstore",
"locale", "es_CO",
"apiBaseUrl", "/api"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public Module getModule() { //<2>
.addPage(
new Page("components", "Custom Components", "classpath:/pages/custom-components.zul"),
new Page("vue", "Vue Example", "classpath:/pages/vue-integration.zul"),
new Page("microfrontend", "MicroFrontend", "classpath:/pages/microfrontend-demo.zul"),
new Page("mvvm", "Standard ZK MVVM", "classpath:/pages/standard-mvvm.zul"),
new Page("chartjs", "Charts for ZK", "classpath:/pages/chartjs.zul"),
new Page("aceditor", "Ace Code Editor", "classpath:/pages/aceditor.zul"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package mybookstore.viewmodel;

import org.zkoss.bind.annotation.BindingParam;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Random;

/**
* Backs the "MVVM mode" panel of the MicroFrontend demo (microfrontend-demo.zul). Every bound
* value below is a plain bean property, so it works with {@code @bind(vm.xxx)} with zero extra
* code in tools.dynamia.zk.ui.MicroFrontend. {@link #getUser()} in particular proves that binding
* a Java object (not just Strings/numbers) reaches the browser as real JSON: MicroFrontend's
* buildConfig() serializes the whole props map through StringPojoParser.convertMapToJson, which is
* backed by Jackson and recursively serializes nested POJOs, not toString().
*/
public class MicroFrontendDemoVM {

private static final String[] ROLES = {"admin", "editor", "viewer"};
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
private final Random random = new Random();

private String theme = "dark";
private boolean shadow;
private DemoUser user = new DemoUser(1L, "Ada Lovelace", "admin");
private String lastEvent = "(none yet)";

public String getTheme() {
return theme;
}

public boolean isShadow() {
return shadow;
}

public DemoUser getUser() {
return user;
}

public String getLastEvent() {
return lastEvent;
}

@Command
@NotifyChange("theme")
public void toggleTheme() {
theme = "dark".equals(theme) ? "light" : "dark";
}

@Command
@NotifyChange("shadow")
public void toggleShadow() {
shadow = !shadow;
}

@Command
@NotifyChange("user")
public void randomizeUser() {
user = new DemoUser(random.nextLong(1000), "User " + random.nextInt(1000), ROLES[random.nextInt(ROLES.length)]);
}

@Command
@NotifyChange("lastEvent")
public void handleMicrofrontendEvent(@BindingParam("data") Object data) {
lastEvent = LocalTime.now().format(TIME_FORMAT) + " -> " + data;
}

/** Bound as a whole to the "user" prop, proving nested POJO-to-JSON serialization. */
public static class DemoUser {

private final Long id;
private final String name;
private final String role;

public DemoUser(Long id, String name, String role) {
this.id = id;
this.name = name;
this.role = role;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public String getRole() {
return role;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<zk xmlns:n="native">

<zscript><![CDATA[
void toggle() {
if (wrapper.getParent() != null) {
wrapper.detach();
}
}

void handleMicrofrontendEvent(org.zkoss.zk.ui.event.Event event) {
Clients.showNotification("onMicrofrontendEvent: " + event.getData());
}

void handleMicrofrontendReady(org.zkoss.zk.ui.event.Event event) {
System.out.println("[MicroFrontend] onMicrofrontendReady: " + event.getTarget());
}

void handleMicrofrontendError(org.zkoss.zk.ui.event.Event event) {
Clients.showNotification("onMicrofrontendError: " + event.getData(), "error", null, "top_center", 4000);
}

void randomizeMountFnUserId() {
mountFnDemo.addProp("userId", (int) (Math.random() * 1000));
}

void breakBundle() {
brokenDemo.setSrc("/static/js/does-not-exist.js");
}
]]></zscript>

<div hflex="1" vflex="1">
<n:h3>MicroFrontend Component Demo</n:h3>

<div id="wrapper" hflex="1">
<groupbox width="50%">
<caption label="custom-element mode (see server console for onMicrofrontendReady)"/>
<microfrontend src="/static/js/microfrontend-demo.js" css="/static/css/microfrontend-demo.css"
tag="demo-widget" mode="custom-element" userId="42" theme="dark"
onMicrofrontendEvent="handleMicrofrontendEvent(event)"
onMicrofrontendReady="handleMicrofrontendReady(event)"/>
</groupbox>
<groupbox width="50%">
<caption label="mount-fn mode, shadow DOM isolated, update in place"/>
<microfrontend id="mountFnDemo" src="/static/js/microfrontend-demo.js" css="/static/css/microfrontend-demo.css"
mode="mount-fn" mountFn="mountDemoApp" unmountFn="unmountDemoApp" updateFn="updateDemoApp"
userId="99" shadow="true" onMicrofrontendEvent="handleMicrofrontendEvent(event)"/>
<button label="Change userId (updates in place, no remount)" onClick="randomizeMountFnUserId()"/>
</groupbox>
<groupbox width="50%">
<caption label="app auto-discovery mode, self-mounting (no tag/mountFn)"/>
<microfrontend app="/static/next/demo-app"/>
</groupbox>
<groupbox width="50%" viewModel="@id('vm') @init('mybookstore.viewmodel.MicroFrontendDemoVM')">
<caption label="MVVM mode: @bind, @command, POJO bound as JSON"/>
<microfrontend src="/static/js/microfrontend-demo.js" css="/static/css/microfrontend-demo.css"
tag="demo-widget" theme="@bind(vm.theme)" shadow="@bind(vm.shadow)"
user="@bind(vm.user)"
onMicrofrontendEvent="@command('handleMicrofrontendEvent', data=event.data)"/>
<hbox>
<button label="Toggle theme" onClick="@command('toggleTheme')"/>
<button label="Toggle shadow" onClick="@command('toggleShadow')"/>
<button label="Randomize user" onClick="@command('randomizeUser')"/>
</hbox>
<label value="@load(vm.lastEvent)"/>
</groupbox>
<groupbox width="50%">
<caption label="onMicrofrontendError demo"/>
<microfrontend id="brokenDemo" src="/static/js/microfrontend-demo.js" tag="demo-widget"
onMicrofrontendError="handleMicrofrontendError(event)"/>
<button label="Point src at a missing file (fires onMicrofrontendError)" onClick="breakBundle()"/>
</groupbox>
</div>

<button label="Detach wrapper (verify unmount in console)" onClick="toggle()"/>
</div>
</zk>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* Loaded via the MicroFrontend "css" attribute, proves external stylesheets ship with a bundle. */
.demo-widget-emit-btn {
margin-top: .5rem;
background: #6c5ce7;
color: #fff;
border: none;
border-radius: 6px;
padding: .4rem .8rem;
cursor: pointer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Fake "microfrontend" bundle used to manually verify tools.dynamia.zk.ui.MicroFrontend.
* Simulates what a Vue/React build would ship: a custom element AND a mount/unmount pair.
*/
(function () {
class DemoWidget extends HTMLElement {
connectedCallback() {
console.log('[demo-widget] connectedCallback', {
userId: this.userId, theme: this.theme, user: this.user, dynamiaHost: this.dynamiaHost
});
var userLine = '';
if (this.user && typeof this.user === 'object') {
userLine = 'user: ' + JSON.stringify(this.user) + ' (typeof ' + typeof this.user + ')<br/>';
}
var hostLine = '';
if (this.dynamiaHost && typeof this.dynamiaHost === 'object') {
hostLine = 'dynamiaHost: ' + JSON.stringify(this.dynamiaHost) + '<br/>';
}
this.innerHTML =
'<div style="padding:1rem;border:2px dashed #6c5ce7;border-radius:8px">' +
'<strong>custom-element mode</strong><br/>' +
'userId: ' + this.userId + '<br/>' +
'theme: ' + this.theme + '<br/>' +
userLine + hostLine +
'<button id="inc">clicks: 0</button><br/>' +
'<button id="emit" class="demo-widget-emit-btn">emit event to server</button>' +
'</div>';
var btn = this.querySelector('#inc');
var clicks = 0;
btn.addEventListener('click', function () {
clicks++;
btn.textContent = 'clicks: ' + clicks;
});
var dynamiaEmit = this.dynamiaEmit;
this.querySelector('#emit').addEventListener('click', function () {
if (typeof dynamiaEmit === 'function') {
dynamiaEmit({from: 'custom-element', clicks: clicks});
}
});
}

disconnectedCallback() {
console.log('[demo-widget] disconnectedCallback (auto cleanup by browser)');
}
}

if (!customElements.get('demo-widget')) {
customElements.define('demo-widget', DemoWidget);
}

function renderMountDemoApp(container, props) {
var hostLine = props.dynamiaHost ? ('dynamiaHost: ' + JSON.stringify(props.dynamiaHost) + '<br/>') : '';
container.innerHTML =
'<div style="padding:1rem;border:2px solid #00b894;border-radius:8px">' +
'<strong>mount-fn mode</strong><br/>userId: ' + props.userId + '<br/>' +
hostLine +
'in-place updates: ' + (container._dynamiaUpdateCount || 0) + '<br/>' +
'<button id="emit" class="demo-widget-emit-btn">emit event to server</button></div>';
container.querySelector('#emit').addEventListener('click', function () {
if (typeof props.dynamiaEmit === 'function') {
props.dynamiaEmit({from: 'mount-fn', userId: props.userId});
}
});
}

window.mountDemoApp = function (container, props) {
container._dynamiaUpdateCount = 0;
renderMountDemoApp(container, props);
console.log('[demo-app] mounted via mount-fn', props);
};

/**
* Optional single-spa-style "update" hook (MicroFrontend's updateFn=): called instead of
* unmount+mount when only props changed, so the bundle can preserve its own state. Here we
* just bump a counter on the container to prove in the UI/console that no full remount happened.
*/
window.updateDemoApp = function (container, props) {
container._dynamiaUpdateCount = (container._dynamiaUpdateCount || 0) + 1;
renderMountDemoApp(container, props);
console.log('[demo-app] updated in place via updateFn (#' + container._dynamiaUpdateCount + ')', props);
};

window.unmountDemoApp = function (container) {
container.innerHTML = '';
console.log('[demo-app] unmounted via mount-fn');
};
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Fake hashed Vite chunk used to manually verify MicroFrontend's "app" auto-discovery + "auto"
* mount mode: a plain self-mounting SPA bundle, like `createApp(App).mount('#demo-app-root')`
* would produce, with no custom element and no window mount/unmount functions.
*/
(function () {
var root = document.getElementById('demo-app-root');
if (!root) {
console.error('[demo-app] #demo-app-root not found, was the index.html body replicated?');
return;
}
console.log('[demo-app] self-mounted into #demo-app-root');
root.innerHTML =
'<div class="demo-app-widget-box">' +
'<strong>app auto-discovery mode</strong><br/>' +
'self-mounted, no tag/mountFn needed' +
'</div>';
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* Fake hashed Vite CSS chunk, loaded automatically via MicroFrontend's "app" discovery. */
.demo-app-widget-box {
padding: 1rem;
border: 2px dotted #e17055;
border-radius: 8px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!-- Mimics a Vite `dist/index.html` output for a self-mounting SPA (not a custom-element library
build), used to manually verify MicroFrontend's "app" auto-discovery + "auto" mount mode. -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>demo-app</title>
<script type="module" crossorigin src="/static/next/demo-app/assets/index-a1b2c3d4.js"></script>
<link rel="stylesheet" crossorigin href="/static/next/demo-app/assets/index-e5f6g7h8.css">
</head>
<body>
<div id="demo-app-root"></div>
</body>
</html>
Loading
Loading