javascript
examples
examples.js⚡javascript
/**
* 19.4 Resize Observer - Examples
*
* Watch for element size changes efficiently
*/
// ============================================
// BASIC RESIZE OBSERVER
// ============================================
/**
* Simple size monitoring
*/
function basicResizeObserver() {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`Element resized: ${width}x${height}`);
// Access different box sizes
if (entry.contentBoxSize) {
const contentBox = entry.contentBoxSize[0];
console.log(
`Content box: ${contentBox.inlineSize}x${contentBox.blockSize}`
);
}
if (entry.borderBoxSize) {
const borderBox = entry.borderBoxSize[0];
console.log(
`Border box: ${borderBox.inlineSize}x${borderBox.blockSize}`
);
}
}
});
const element = document.getElementById('observed');
observer.observe(element);
return observer;
}
// ============================================
// RESPONSIVE COMPONENT
// ============================================
/**
* Component that adapts based on its own size
*/
class ResponsiveComponent {
constructor(element, breakpoints = {}) {
this.element = element;
this.breakpoints = {
small: breakpoints.small || 300,
medium: breakpoints.medium || 600,
large: breakpoints.large || 900,
};
this.currentSize = null;
this.observer = new ResizeObserver((entries) => {
const entry = entries[0];
this.handleResize(entry.contentRect.width);
});
this.observer.observe(element);
}
handleResize(width) {
let newSize;
if (width < this.breakpoints.small) {
newSize = 'xs';
} else if (width < this.breakpoints.medium) {
newSize = 'small';
} else if (width < this.breakpoints.large) {
newSize = 'medium';
} else {
newSize = 'large';
}
if (newSize !== this.currentSize) {
this.currentSize = newSize;
this.updateLayout(newSize);
}
}
updateLayout(size) {
// Remove all size classes
this.element.classList.remove(
'size-xs',
'size-small',
'size-medium',
'size-large'
);
// Add current size class
this.element.classList.add(`size-${size}`);
console.log(`Component switched to ${size} layout`);
// Emit custom event
this.element.dispatchEvent(
new CustomEvent('sizechange', {
detail: { size },
})
);
}
destroy() {
this.observer.disconnect();
}
}
// ============================================
// CANVAS RESIZING
// ============================================
/**
* Keep canvas size synced with container
*/
class ResizableCanvas {
constructor(canvas, options = {}) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.dpr = options.dpr || window.devicePixelRatio || 1;
this.onResize = options.onResize || null;
this.observer = new ResizeObserver((entries) => {
const entry = entries[0];
// Use devicePixelContentBoxSize if available for crisp rendering
if (entry.devicePixelContentBoxSize) {
const size = entry.devicePixelContentBoxSize[0];
this.resize(size.inlineSize, size.blockSize, false);
} else {
const { width, height } = entry.contentRect;
this.resize(width, height, true);
}
});
this.observer.observe(canvas);
}
resize(width, height, applyDpr = true) {
const scale = applyDpr ? this.dpr : 1;
// Set canvas buffer size
this.canvas.width = width * scale;
this.canvas.height = height * scale;
// Set display size
if (applyDpr) {
this.canvas.style.width = `${width}px`;
this.canvas.style.height = `${height}px`;
}
// Scale context for DPR
if (applyDpr) {
this.ctx.scale(scale, scale);
}
console.log(
`Canvas resized to ${width}x${height} (buffer: ${this.canvas.width}x${this.canvas.height})`
);
if (this.onResize) {
this.onResize(width, height);
}
}
destroy() {
this.observer.disconnect();
}
}
// ============================================
// ASPECT RATIO CONTAINER
// ============================================
/**
* Maintain aspect ratio within container
*/
class AspectRatioContainer {
constructor(container, aspectRatio = 16 / 9) {
this.container = container;
this.aspectRatio = aspectRatio;
// Create inner element for content
this.inner = container.querySelector('.aspect-inner') || this.createInner();
this.observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
this.adjustSize(width, height);
});
this.observer.observe(container);
}
createInner() {
const inner = document.createElement('div');
inner.className = 'aspect-inner';
inner.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`;
// Move existing children to inner
while (this.container.firstChild) {
inner.appendChild(this.container.firstChild);
}
this.container.style.position = 'relative';
this.container.appendChild(inner);
return inner;
}
adjustSize(containerWidth, containerHeight) {
const containerRatio = containerWidth / containerHeight;
let width, height;
if (containerRatio > this.aspectRatio) {
// Container is wider - fit to height
height = containerHeight;
width = height * this.aspectRatio;
} else {
// Container is taller - fit to width
width = containerWidth;
height = width / this.aspectRatio;
}
this.inner.style.width = `${width}px`;
this.inner.style.height = `${height}px`;
}
setAspectRatio(ratio) {
this.aspectRatio = ratio;
// Trigger recalculation
const rect = this.container.getBoundingClientRect();
this.adjustSize(rect.width, rect.height);
}
destroy() {
this.observer.disconnect();
}
}
// ============================================
// TEXT TRUNCATION
// ============================================
/**
* Auto-truncate text based on container size
*/
class TextTruncator {
constructor(element, options = {}) {
this.element = element;
this.fullText = element.textContent;
this.minChars = options.minChars || 10;
this.ellipsis = options.ellipsis || '...';
this.observer = new ResizeObserver(() => {
this.truncate();
});
this.observer.observe(element);
}
truncate() {
// Reset to full text first
this.element.textContent = this.fullText;
// Check if truncation needed
if (this.element.scrollWidth <= this.element.clientWidth) {
return;
}
// Binary search for optimal length
let low = this.minChars;
let high = this.fullText.length;
while (low < high) {
const mid = Math.floor((low + high + 1) / 2);
this.element.textContent = this.fullText.slice(0, mid) + this.ellipsis;
if (this.element.scrollWidth <= this.element.clientWidth) {
low = mid;
} else {
high = mid - 1;
}
}
this.element.textContent = this.fullText.slice(0, low) + this.ellipsis;
}
setText(text) {
this.fullText = text;
this.truncate();
}
destroy() {
this.observer.disconnect();
}
}
// ============================================
// RESPONSIVE TABLE
// ============================================
/**
* Table that adapts to container width
*/
class ResponsiveTable {
constructor(table, options = {}) {
this.table = table;
this.options = {
stackBreakpoint: options.stackBreakpoint || 600,
hideColumns: options.hideColumns || [],
};
this.mode = null;
this.observer = new ResizeObserver((entries) => {
const width = entries[0].contentRect.width;
this.handleResize(width);
});
// Wrap table if needed
this.wrapper = table.parentElement.classList.contains('table-wrapper')
? table.parentElement
: this.wrapTable();
this.observer.observe(this.wrapper);
}
wrapTable() {
const wrapper = document.createElement('div');
wrapper.className = 'table-wrapper';
this.table.parentNode.insertBefore(wrapper, this.table);
wrapper.appendChild(this.table);
return wrapper;
}
handleResize(width) {
const newMode = width < this.options.stackBreakpoint ? 'stacked' : 'normal';
if (newMode !== this.mode) {
this.mode = newMode;
this.applyMode(newMode);
}
}
applyMode(mode) {
if (mode === 'stacked') {
this.table.classList.add('table-stacked');
this.convertToStacked();
} else {
this.table.classList.remove('table-stacked');
this.convertToNormal();
}
}
convertToStacked() {
const headers = Array.from(this.table.querySelectorAll('thead th')).map(
(th) => th.textContent
);
this.table.querySelectorAll('tbody tr').forEach((row) => {
row.querySelectorAll('td').forEach((cell, index) => {
cell.setAttribute('data-label', headers[index] || '');
});
});
}
convertToNormal() {
this.table.querySelectorAll('td[data-label]').forEach((cell) => {
cell.removeAttribute('data-label');
});
}
destroy() {
this.observer.disconnect();
}
}
// ============================================
// VIRTUAL SCROLL RECALCULATOR
// ============================================
/**
* Recalculate virtual scroll on resize
*/
class VirtualScrollResize {
constructor(container, options) {
this.container = container;
this.itemHeight = options.itemHeight;
this.items = options.items || [];
this.renderItem = options.renderItem;
this.visibleStart = 0;
this.visibleEnd = 0;
this.observer = new ResizeObserver((entries) => {
const height = entries[0].contentRect.height;
this.recalculate(height);
});
this.observer.observe(container);
// Also handle scroll
container.addEventListener('scroll', () => this.onScroll());
}
recalculate(containerHeight) {
const visibleCount = Math.ceil(containerHeight / this.itemHeight) + 1;
console.log(
`Container height: ${containerHeight}, visible items: ${visibleCount}`
);
this.visibleCount = visibleCount;
this.onScroll();
}
onScroll() {
const scrollTop = this.container.scrollTop;
const start = Math.floor(scrollTop / this.itemHeight);
const end = Math.min(start + this.visibleCount, this.items.length);
if (start !== this.visibleStart || end !== this.visibleEnd) {
this.visibleStart = start;
this.visibleEnd = end;
this.render();
}
}
render() {
const visibleItems = this.items.slice(this.visibleStart, this.visibleEnd);
const content = this.container.querySelector('.virtual-content');
content.style.height = `${this.items.length * this.itemHeight}px`;
content.style.paddingTop = `${this.visibleStart * this.itemHeight}px`;
content.innerHTML = visibleItems.map(this.renderItem).join('');
}
setItems(items) {
this.items = items;
const height = this.container.offsetHeight;
this.recalculate(height);
}
destroy() {
this.observer.disconnect();
}
}
// ============================================
// DEBOUNCED RESIZE OBSERVER
// ============================================
/**
* ResizeObserver with debouncing for performance
*/
function createDebouncedResizeObserver(callback, delay = 100) {
let timeoutId = null;
let latestEntries = null;
const observer = new ResizeObserver((entries) => {
latestEntries = entries;
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
callback(latestEntries);
timeoutId = null;
}, delay);
});
return {
observe: (element, options) => observer.observe(element, options),
unobserve: (element) => observer.unobserve(element),
disconnect: () => {
if (timeoutId) clearTimeout(timeoutId);
observer.disconnect();
},
};
}
// ============================================
// NODE.JS SIMULATION
// ============================================
console.log('=== ResizeObserver Examples ===');
console.log('Note: ResizeObserver is a browser API.');
console.log('');
console.log('Example patterns:');
console.log('');
const patterns = [
{
name: 'Responsive Component',
description: 'Adapts layout based on container size',
usage: 'new ResponsiveComponent(element, { small: 300, medium: 600 })',
},
{
name: 'Resizable Canvas',
description: 'Keeps canvas crisp at any size',
usage: 'new ResizableCanvas(canvas, { dpr: 2 })',
},
{
name: 'Aspect Ratio Container',
description: 'Maintains aspect ratio while fitting container',
usage: 'new AspectRatioContainer(container, 16/9)',
},
{
name: 'Text Truncator',
description: 'Auto-truncates text to fit container',
usage: 'new TextTruncator(element, { ellipsis: "..." })',
},
{
name: 'Responsive Table',
description: 'Switches to stacked layout on narrow containers',
usage: 'new ResponsiveTable(table, { stackBreakpoint: 600 })',
},
];
patterns.forEach(({ name, description, usage }) => {
console.log(`${name}:`);
console.log(` ${description}`);
console.log(` Usage: ${usage}`);
console.log('');
});
// Export for browser use
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
basicResizeObserver,
ResponsiveComponent,
ResizableCanvas,
AspectRatioContainer,
TextTruncator,
ResponsiveTable,
VirtualScrollResize,
createDebouncedResizeObserver,
};
}