javascript

examples

examples.js
/**
 * 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,
  };
}
Examples - JavaScript Tutorial | DeepML