Third-Party Library Integration
WildflowerJS works seamlessly with third-party JavaScript libraries because it has no virtual DOM. Libraries that manipulate the real DOM directly - like Leaflet, FullCalendar, SortableJS, Chart.js, and many others - can be integrated without wrappers, adapters, or special plugins.
- No Virtual DOM - Libraries can read from and write to the real DOM without conflicts
- Simple Scanning - Call
wildflower.scan()after any library renders new content - Data Attributes - Use
data-bind-attrto set IDs that libraries need - No Framework Lock-in - Use any JavaScript library without "wrapper" packages
The Integration Pattern
Integrating third-party libraries follows a consistent pattern:
- Initialize the library - Create the library instance on your DOM element
- Scan after rendering - If the library renders content containing WildflowerJS components, call
wildflower.scan() - Sync state changes - Use watchers or subscriptions to keep the library in sync with your state
- Use data-bind-attr - Set data attributes that the library needs to identify elements
Loading Libraries Securely (Subresource Integrity)
When you pull a library from a CDN, lock it to a known-good hash with Subresource Integrity (SRI). The browser hashes the downloaded file and refuses to run it if the bytes do not match, so a tampered or swapped-out CDN file is blocked instead of executed. This is the runtime equivalent of how WildflowerJS pins its own build tools - rollup and terser are fetched as SHA-512-verified tarballs - so apply the same discipline to the libraries you load at runtime.
<!-- Pin the exact version, then lock the file with its hash -->
<script
src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"
integrity="sha512-..."
crossorigin="anonymous"></script>
Generate the hash from the exact file you intend to ship:
curl -s https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js \
| openssl dgst -sha512 -binary | openssl base64 -A
# prefix the result with "sha512-" for the integrity attribute
- Pin the version. The URL must point at an exact version (
sortablejs@1.15.2), never@latestor an unversioned path. The hash is tied to specific bytes, so an auto-updating URL breaks on every release - which is the point: an unexpected change is blocked, not run. - Add
crossorigin="anonymous". SRI on a cross-origin script requires CORS; without it the browser will not run the script even when the hash matches. - Hash the file you actually load. Minified vs unminified, or a different version, yields a different hash. jsDelivr and unpkg also expose a copy-paste SRI hash in their UIs, or use srihash.org.
WildflowerJS itself can be pinned the same way when loaded from a CDN: generate the hash for the exact wildflower.min.js version you reference.
Dynamic Action Binding with rebindActions()
When a component renders content via innerHTML that includes data-action attributes, those actions need to be bound to the component. Use this.rebindActions() after updating the DOM:
// After dynamically adding content with data-action attributes
this.element.querySelector('.container').innerHTML = `
<button data-action="handleClick">Click Me</button>
<button data-action="handleSubmit">Submit</button>
`;
// Bind the new action handlers
this.rebindActions();
rebindActions() is safe to call multiple times - already-bound elements are skipped. This is the recommended pattern for dynamic content rendering within components.
Example: SortableJS
SortableJS enables drag-and-drop reordering of lists. Use data-list for declarative rendering and initialize SortableJS in init(). When the user drags, read the new order from the DOM and update your state array.
<style>
.task-list { list-style: none; margin: 0; padding: 0; }
.task-item { display: flex; align-items: center; gap: 12px; padding: 10px 14px; margin-bottom: 4px; background: #888; color: #fff; border-radius: 6px; cursor: grab; }
.task-item .drag-handle { color: #ccc; user-select: none; }
.task-item .task-text { flex: 1; }
.task-item .remove-btn { margin-left: auto; flex-shrink: 0; }
</style>
<div data-component="task-manager">
<h5>Drag to Reorder</h5>
<ul class="task-list" data-list="tasks" data-key="id">
<template>
<li class="task-item" data-bind-attr="({ 'data-task-id': id })">
<span class="drag-handle">⋮⋮</span>
<span class="task-text" data-bind="text"></span>
<button class="btn btn-sm btn-danger remove-btn" data-action="removeTask">×</button>
</li>
</template>
</ul>
</div>
wildflower.component('task-manager', {
state: {
tasks: [
{ id: 1, text: 'Book flights' },
{ id: 2, text: 'Reserve hotel' },
{ id: 3, text: 'Pack bags' },
{ id: 4, text: 'Get travel insurance' },
{ id: 5, text: 'Print boarding passes' }
]
},
init() {
const self = this;
const list = this.element.querySelector('.task-list');
// Load SortableJS dynamically if not already available
if (window.Sortable) {
this.initSortable(list);
} else {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js';
script.onload = function() { self.initSortable(list); };
document.head.appendChild(script);
}
},
initSortable(list) {
new Sortable(list, {
animation: 150,
handle: '.drag-handle',
ghostClass: 'sortable-ghost',
onEnd: () => {
const items = list.querySelectorAll('[data-task-id]');
const newOrder = Array.from(items).map(
el => parseInt(el.dataset.taskId)
);
this.tasks = newOrder.map(id =>
this.tasks.find(t => t.id === id)
);
}
});
},
removeTask(event, element) {
const id = parseInt(
element.closest('[data-task-id]').dataset.taskId
);
this.tasks = this.tasks.filter(
t => t.id !== id
);
}
});
data-list with data-key for declarative rendering. In the SortableJS onEnd callback, read the reordered data-task-id attributes from the DOM and rebuild the state array to match. WildflowerJS reconciles the rest.
SortableJS with data-list (Cross-List Drag-and-Drop)
For kanban-style interfaces where items move between multiple lists, you can use SortableJS directly with data-list. WildflowerJS automatically detects when SortableJS moves elements between lists and reconciles the DOM correctly.
<div data-component="kanban-board">
<div style="display:flex; gap:12px;">
<div style="flex:1;">
<h6>To Do</h6>
<div class="card card-body card-list" data-list="todoCards" data-key="id"
style="min-height:80px;">
<template>
<div data-bind-attr="({ 'data-card-id': id })"
style="padding:8px 12px; margin-bottom:4px; background:#888; color:#fff; border-radius:4px; cursor:grab;">
<span data-bind="title"></span>
</div>
</template>
</div>
</div>
<div style="flex:1;">
<h6>In Progress</h6>
<div class="card card-body card-list" data-list="inProgressCards" data-key="id"
style="min-height:80px;">
<template>
<div data-bind-attr="({ 'data-card-id': id })"
style="padding:8px 12px; margin-bottom:4px; background:#888; color:#fff; border-radius:4px; cursor:grab;">
<span data-bind="title"></span>
</div>
</template>
</div>
</div>
<div style="flex:1;">
<h6>Done</h6>
<div class="card card-body card-list" data-list="doneCards" data-key="id"
style="min-height:80px;">
<template>
<div data-bind-attr="({ 'data-card-id': id })"
style="padding:8px 12px; margin-bottom:4px; background:#888; color:#fff; border-radius:4px; cursor:grab;">
<span data-bind="title"></span>
</div>
</template>
</div>
</div>
</div>
</div>
wildflower.component('kanban-board', {
state: {
todoCards: [
{ id: 1, title: 'Design mockups' },
{ id: 2, title: 'Write specs' }
],
inProgressCards: [
{ id: 3, title: 'Build API' }
],
doneCards: [
{ id: 4, title: 'Setup project' }
]
},
init() {
const self = this;
function setup() {
self.element.querySelectorAll('.card-list').forEach(
function(list) {
new Sortable(list, {
group: 'kanban-cards',
animation: 150,
ghostClass: 'sortable-ghost',
onEnd: function(evt) {
const cardId = parseInt(
evt.item.dataset.cardId
);
const fromList = evt.from.dataset.list;
const toList = evt.to.dataset.list;
if (fromList !== toList) {
self.moveCard(
cardId, fromList,
toList, evt.newIndex
);
}
}
});
}
);
}
if (window.Sortable) {
setup();
} else {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js';
script.onload = setup;
document.head.appendChild(script);
}
},
moveCard(cardId, fromName, toName, newIndex) {
const fromList = this[fromName];
const card = fromList.find(c => c.id === cardId);
const newFrom = fromList.filter(c => c.id !== cardId);
const newTo = [...this[toName]];
newTo.splice(newIndex, 0, card);
this[fromName] = newFrom;
this[toName] = newTo;
}
});
data-bind-attr="({ 'data-card-id': id })" on each card is essential. It sets a data-card-id attribute that we read in onEnd to identify which card was moved.
Example: Leaflet Maps
Leaflet is a popular mapping library. It manages its own DOM for markers and popups, but you can embed WildflowerJS components inside popups.
Setup
<!-- Include Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Map container -->
<div data-component="map-demo">
<div id="my-map" style="height: 400px;"></div>
</div>
JavaScript
// Popup component - will be initialized inside Leaflet popups
wildflower.component('location-popup', {
state: {
location: null
},
init() {
// Get location ID from data attribute
const locationId = parseInt(this.element.dataset.locationId);
const store = wildflower.getStore('locations');
this.location = store.getLocation(locationId);
},
remove() {
const store = wildflower.getStore('locations');
store.map.closePopup();
store.removeLocation(this.location.id);
}
});
// Main map component
wildflower.component('map-demo', {
state: {
map: null,
markers: {}
},
init() {
this.initMap();
this.syncMarkers();
},
initMap() {
this.map = L.map('my-map').setView([51.505, -0.09], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(this.map);
// Click to add locations
this.map.on('click', (e) => {
this.addLocation(e.latlng.lat, e.latlng.lng);
});
},
syncMarkers() {
const store = wildflower.getStore('locations');
store.locations.forEach(loc => {
const marker = L.marker([loc.lat, loc.lng]).addTo(this.map);
// Popup contains a WildflowerJS component
const popupHtml = `
<div data-component="location-popup" data-location-id="${loc.id}">
<h4 data-bind="location.name"></h4>
<button data-action="remove">Remove</button>
</div>
`;
marker.bindPopup(popupHtml);
// KEY: Scan popup content when it opens
marker.on('popupopen', () => {
wildflower.scan('.leaflet-popup-content');
});
this.markers[loc.id] = marker;
});
}
});
marker.on('popupopen', () => wildflower.scan('.leaflet-popup-content')) initializes the WildflowerJS component inside the Leaflet popup after Leaflet renders it to the DOM.
Example: FullCalendar
FullCalendar is a full-featured calendar component. State changes in WildflowerJS can drive calendar events.
wildflower.component('calendar-demo', {
state: {
calendar: null,
events: [
{ id: '1', title: 'Meeting', date: '2025-03-15' },
{ id: '2', title: 'Conference', date: '2025-03-18' }
]
},
init() {
this.initCalendar();
// Watch for event changes
this.watch = {
events: () => this.syncCalendar()
};
},
initCalendar() {
this.calendar = new FullCalendar.Calendar(
document.getElementById('calendar'),
{
initialView: 'dayGridMonth',
events: this.events,
editable: true,
// Drag event updates state
eventDrop: (info) => {
this.updateEventDate(info.event.id, info.event.startStr);
},
// Click event triggers action
eventClick: (info) => {
this.selectEvent(info.event.id);
}
}
);
this.calendar.render();
},
syncCalendar() {
if (!this.calendar) return;
// Remove all events and re-add from state
this.calendar.removeAllEvents();
this.events.forEach(evt => {
this.calendar.addEvent(evt);
});
},
updateEventDate(id, newDate) {
this.events = this.events.map(e =>
e.id === id ? { ...e, date: newDate } : e
);
},
addEvent(title, date) {
const newEvent = {
id: String(Date.now()),
title,
date
};
this.events = [...this.events, newEvent];
}
});
Example: Chart.js
Chart.js renders charts to a canvas element. When your data changes, update the chart.
wildflower.component('chart-demo', {
state: {
chart: null,
data: [12, 19, 3, 5, 2, 3]
},
init() {
this.initChart();
},
initChart() {
const ctx = this.element.querySelector('#myChart').getContext('2d');
this.chart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Sales',
data: this.data,
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
}
});
},
updateData(newData) {
this.data = newData;
// Update chart directly
this.chart.data.datasets[0].data = newData;
this.chart.update();
},
destroy() {
if (this.chart) {
this.chart.destroy();
}
}
});
Using Stores for Shared State
When multiple components need to interact with a third-party library, use a store for centralized state:
// Centralized store for map state
wildflower.store('mapData', {
state: {
locations: [],
map: null,
nextId: 1
},
addLocation(lat, lng) {
const location = {
id: this.nextId++,
lat,
lng,
name: `Location ${this.nextId - 1}`
};
this.locations = [...this.locations, location];
},
removeLocation(id) {
this.locations = this.locations.filter(l => l.id !== id);
},
getLocation(id) {
return this.locations.find(l => l.id === id);
}
});
// Any component can access the store
wildflower.component('location-popup', {
remove() {
const store = wildflower.getStore('mapData');
store.removeLocation(this.locationId);
}
});
wildflower.getComponents('parent')[0]. Any component can access the store directly via wildflower.getStore('storeName').
Common Patterns Summary
| Scenario | Pattern |
|---|---|
| Component renders innerHTML with data-action | this.rebindActions() after innerHTML update |
| Library renders content with WF components | library.on('render', () => wildflower.scan(container)) |
| Library needs element IDs after manipulation | data-bind-attr="{ 'data-id': id }" |
| State change should update library | Use watch or store.subscribe() |
| Multiple components share library state | Use a store: wildflower.store() |
| Library creates/destroys DOM elements | Scan after creation, no action needed on destruction |
Libraries Known to Work Well
These libraries have been tested and work well with WildflowerJS:
- SortableJS - Drag-and-drop sorting
- Leaflet - Interactive maps
- FullCalendar - Calendar components
- Chart.js - Charts and graphs
- Swiper - Touch sliders
- Tippy.js - Tooltips and popovers
- Flatpickr - Date pickers
- Quill / TinyMCE - Rich text editors
- Prism.js / Highlight.js - Code syntax highlighting
Troubleshooting
Components Not Initializing
If components inside library-rendered content aren't working:
- Ensure you're calling
wildflower.scan()after the library renders - Check the selector passed to scan - it should contain the new components
- Verify the content actually has
data-componentattributes
State Not Syncing
If library state and WildflowerJS state get out of sync:
- Use watchers or store subscriptions to react to state changes
- Call the library's update/refresh method when state changes
- Consider if the library should be the source of truth for certain data
Memory Leaks
To prevent memory leaks:
- Destroy library instances in your component's
destroy()method - Remove event listeners you've added
- Clear any references stored in state