javascript
exercises
exercises.js⚡javascript
/**
* 19.4 Resize Observer - Exercises
*
* Practice implementing ResizeObserver patterns
*/
// ============================================
// EXERCISE 1: Container Query Polyfill
// ============================================
/**
* Create a container query polyfill that:
* - Applies styles based on container size
* - Supports multiple breakpoints
* - Uses data attributes for configuration
*
* Requirements:
* - Parse breakpoints from data-container-query attribute
* - Apply classes based on container width
* - Support min/max width conditions
*/
class ContainerQueryPolyfill {
// Your implementation here
}
/*
// SOLUTION:
class ContainerQueryPolyfill {
constructor(container, options = {}) {
this.container = container;
this.options = options;
this.queries = this.parseQueries();
this.children = [];
this.observer = new ResizeObserver(entries => {
const width = entries[0].contentRect.width;
const height = entries[0].contentRect.height;
this.evaluateQueries(width, height);
});
this.init();
}
init() {
// Find all children with container queries
this.children = Array.from(
this.container.querySelectorAll('[data-container-query]')
).map(element => ({
element,
queries: this.parseElementQueries(element)
}));
this.observer.observe(this.container);
}
parseQueries() {
const queryAttr = this.container.dataset.containerBreakpoints || '';
const queries = {};
// Parse format: "small:300,medium:600,large:900"
queryAttr.split(',').forEach(part => {
const [name, size] = part.split(':');
if (name && size) {
queries[name.trim()] = parseInt(size);
}
});
return queries;
}
parseElementQueries(element) {
const queryAttr = element.dataset.containerQuery || '';
const queries = [];
// Parse format: "min-width:300:class-name max-width:600:other-class"
queryAttr.split(/\s+/).forEach(part => {
const [condition, value, className] = part.split(':');
if (condition && value && className) {
queries.push({
condition: condition.trim(),
value: parseInt(value),
className: className.trim()
});
}
});
return queries;
}
evaluateQueries(width, height) {
// Set container size custom properties
this.container.style.setProperty('--container-width', `${width}px`);
this.container.style.setProperty('--container-height', `${height}px`);
// Apply breakpoint classes to container
Object.entries(this.queries).forEach(([name, breakpoint]) => {
const className = `container-${name}`;
if (width >= breakpoint) {
this.container.classList.add(className);
} else {
this.container.classList.remove(className);
}
});
// Evaluate child element queries
this.children.forEach(({ element, queries }) => {
queries.forEach(query => {
const matches = this.evaluateCondition(query.condition, query.value, width, height);
if (matches) {
element.classList.add(query.className);
} else {
element.classList.remove(query.className);
}
});
});
}
evaluateCondition(condition, value, width, height) {
switch (condition) {
case 'min-width': return width >= value;
case 'max-width': return width <= value;
case 'min-height': return height >= value;
case 'max-height': return height <= value;
case 'width': return width === value;
case 'height': return height === value;
default: return false;
}
}
addBreakpoint(name, value) {
this.queries[name] = value;
const rect = this.container.getBoundingClientRect();
this.evaluateQueries(rect.width, rect.height);
}
removeBreakpoint(name) {
delete this.queries[name];
this.container.classList.remove(`container-${name}`);
}
refresh() {
this.children = Array.from(
this.container.querySelectorAll('[data-container-query]')
).map(element => ({
element,
queries: this.parseElementQueries(element)
}));
const rect = this.container.getBoundingClientRect();
this.evaluateQueries(rect.width, rect.height);
}
destroy() {
this.observer.disconnect();
}
}
*/
// ============================================
// EXERCISE 2: Adaptive Grid System
// ============================================
/**
* Create an adaptive grid system that:
* - Automatically calculates optimal column count
* - Maintains minimum item width
* - Handles gaps and padding
*
* Requirements:
* - Calculate columns based on container width
* - Support minimum and maximum item widths
* - Emit events when layout changes
*/
class AdaptiveGrid {
// Your implementation here
}
/*
// SOLUTION:
class AdaptiveGrid {
constructor(container, options = {}) {
this.container = container;
this.options = {
minItemWidth: options.minItemWidth || 200,
maxItemWidth: options.maxItemWidth || 400,
gap: options.gap || 16,
padding: options.padding || 16
};
this.columns = 0;
this.itemWidth = 0;
this.listeners = new Map();
this.observer = new ResizeObserver(entries => {
const width = entries[0].contentRect.width;
this.calculateLayout(width);
});
this.init();
}
init() {
this.container.style.display = 'grid';
this.container.style.gap = `${this.options.gap}px`;
this.container.style.padding = `${this.options.padding}px`;
this.observer.observe(this.container);
}
calculateLayout(containerWidth) {
const availableWidth = containerWidth - (this.options.padding * 2);
// Calculate optimal number of columns
let columns = Math.floor(
(availableWidth + this.options.gap) /
(this.options.minItemWidth + this.options.gap)
);
// Ensure at least 1 column
columns = Math.max(1, columns);
// Calculate actual item width
let itemWidth = (availableWidth - (this.options.gap * (columns - 1))) / columns;
// Clamp to max width if needed
if (itemWidth > this.options.maxItemWidth && columns > 1) {
columns--;
itemWidth = (availableWidth - (this.options.gap * (columns - 1))) / columns;
}
// Only update if changed
if (columns !== this.columns || Math.abs(itemWidth - this.itemWidth) > 1) {
const previousColumns = this.columns;
this.columns = columns;
this.itemWidth = itemWidth;
this.applyLayout();
this.emit('layoutChange', {
columns,
itemWidth,
previousColumns,
containerWidth
});
}
}
applyLayout() {
this.container.style.gridTemplateColumns =
`repeat(${this.columns}, ${this.itemWidth}px)`;
}
getLayout() {
return {
columns: this.columns,
itemWidth: this.itemWidth,
gap: this.options.gap,
padding: this.options.padding
};
}
setOptions(options) {
Object.assign(this.options, options);
this.container.style.gap = `${this.options.gap}px`;
this.container.style.padding = `${this.options.padding}px`;
const rect = this.container.getBoundingClientRect();
this.calculateLayout(rect.width);
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
return this;
}
emit(event, data) {
const callbacks = this.listeners.get(event) || [];
callbacks.forEach(cb => cb(data));
}
destroy() {
this.observer.disconnect();
this.container.style.display = '';
this.container.style.gridTemplateColumns = '';
this.container.style.gap = '';
this.container.style.padding = '';
}
}
*/
// ============================================
// EXERCISE 3: Responsive Chart Wrapper
// ============================================
/**
* Create a responsive chart wrapper that:
* - Debounces resize events
* - Scales chart appropriately
* - Maintains aspect ratio option
*
* Requirements:
* - Work with any chart library
* - Support aspect ratio lock
* - Handle responsive font sizing
*/
class ResponsiveChartWrapper {
// Your implementation here
}
/*
// SOLUTION:
class ResponsiveChartWrapper {
constructor(container, chartInstance, options = {}) {
this.container = container;
this.chart = chartInstance;
this.options = {
aspectRatio: options.aspectRatio || null,
debounceDelay: options.debounceDelay || 150,
minWidth: options.minWidth || 200,
minHeight: options.minHeight || 150,
responsiveFontSize: options.responsiveFontSize !== false,
baseFontSize: options.baseFontSize || 12,
fontSizeRange: options.fontSizeRange || [10, 18]
};
this.resizeTimeout = null;
this.currentSize = { width: 0, height: 0 };
this.observer = new ResizeObserver(entries => {
const { width, height } = entries[0].contentRect;
this.debouncedResize(width, height);
});
this.init();
}
init() {
// Setup container
this.container.style.position = 'relative';
this.container.style.overflow = 'hidden';
this.observer.observe(this.container);
}
debouncedResize(width, height) {
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
this.resizeTimeout = setTimeout(() => {
this.handleResize(width, height);
}, this.options.debounceDelay);
}
handleResize(containerWidth, containerHeight) {
let width = Math.max(containerWidth, this.options.minWidth);
let height = containerHeight;
// Apply aspect ratio if set
if (this.options.aspectRatio) {
height = width / this.options.aspectRatio;
// If height exceeds container, fit to height instead
if (height > containerHeight) {
height = Math.max(containerHeight, this.options.minHeight);
width = height * this.options.aspectRatio;
}
} else {
height = Math.max(height, this.options.minHeight);
}
// Skip if no change
if (Math.abs(width - this.currentSize.width) < 1 &&
Math.abs(height - this.currentSize.height) < 1) {
return;
}
this.currentSize = { width, height };
// Calculate font size
let fontSize = this.options.baseFontSize;
if (this.options.responsiveFontSize) {
fontSize = this.calculateFontSize(width);
}
// Update chart
this.updateChart(width, height, fontSize);
}
calculateFontSize(width) {
const [minSize, maxSize] = this.options.fontSizeRange;
const minWidth = this.options.minWidth;
const maxWidth = 1200;
// Linear interpolation
const ratio = Math.max(0, Math.min(1, (width - minWidth) / (maxWidth - minWidth)));
return Math.round(minSize + (maxSize - minSize) * ratio);
}
updateChart(width, height, fontSize) {
// Generic chart update - adapt based on library
if (this.chart.resize) {
this.chart.resize({ width, height });
} else if (this.chart.setSize) {
this.chart.setSize(width, height);
} else if (this.chart.update) {
this.chart.update({
chart: { width, height }
});
}
// Update font size if chart supports it
if (this.chart.setFontSize) {
this.chart.setFontSize(fontSize);
}
console.log(`Chart resized to ${width}x${height}, font: ${fontSize}px`);
}
setAspectRatio(ratio) {
this.options.aspectRatio = ratio;
const rect = this.container.getBoundingClientRect();
this.handleResize(rect.width, rect.height);
}
getSize() {
return { ...this.currentSize };
}
forceResize() {
const rect = this.container.getBoundingClientRect();
this.handleResize(rect.width, rect.height);
}
destroy() {
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
this.observer.disconnect();
}
}
*/
// ============================================
// EXERCISE 4: Element Size Monitor
// ============================================
/**
* Create an element size monitoring system:
* - Track multiple elements
* - Generate size change events
* - Provide size history
*
* Requirements:
* - Support size thresholds for significant changes
* - Track size over time
* - Calculate size change velocity
*/
class ElementSizeMonitor {
// Your implementation here
}
/*
// SOLUTION:
class ElementSizeMonitor {
constructor(options = {}) {
this.options = {
significantChangeThreshold: options.significantChangeThreshold || 10,
historyLength: options.historyLength || 100,
sampleInterval: options.sampleInterval || 16
};
this.elements = new Map();
this.listeners = new Map();
this.observer = new ResizeObserver(entries => {
const timestamp = Date.now();
entries.forEach(entry => this.handleResize(entry, timestamp));
});
}
monitor(element, id = null) {
const elementId = id || element.id || `element-${this.elements.size}`;
this.elements.set(element, {
id: elementId,
history: [],
lastWidth: 0,
lastHeight: 0,
velocity: { width: 0, height: 0 },
isAnimating: false
});
this.observer.observe(element);
}
handleResize(entry, timestamp) {
const data = this.elements.get(entry.target);
if (!data) return;
const { width, height } = entry.contentRect;
// Calculate change
const deltaWidth = width - data.lastWidth;
const deltaHeight = height - data.lastHeight;
// Update history
const historyEntry = {
timestamp,
width,
height,
deltaWidth,
deltaHeight
};
data.history.push(historyEntry);
// Trim history
if (data.history.length > this.options.historyLength) {
data.history.shift();
}
// Calculate velocity (pixels per second)
if (data.history.length >= 2) {
const prev = data.history[data.history.length - 2];
const timeDelta = (timestamp - prev.timestamp) / 1000;
if (timeDelta > 0) {
data.velocity = {
width: deltaWidth / timeDelta,
height: deltaHeight / timeDelta
};
}
}
// Detect animation
const isAnimating = Math.abs(data.velocity.width) > 50 ||
Math.abs(data.velocity.height) > 50;
if (isAnimating !== data.isAnimating) {
data.isAnimating = isAnimating;
this.emit(isAnimating ? 'animationStart' : 'animationEnd', {
element: entry.target,
id: data.id
});
}
// Emit change events
const significantChange =
Math.abs(deltaWidth) >= this.options.significantChangeThreshold ||
Math.abs(deltaHeight) >= this.options.significantChangeThreshold;
if (significantChange) {
this.emit('significantChange', {
element: entry.target,
id: data.id,
width,
height,
deltaWidth,
deltaHeight,
velocity: data.velocity
});
}
this.emit('resize', {
element: entry.target,
id: data.id,
width,
height,
deltaWidth,
deltaHeight
});
data.lastWidth = width;
data.lastHeight = height;
}
getStats(element) {
const data = this.elements.get(element);
if (!data) return null;
const history = data.history;
if (history.length === 0) return null;
const widths = history.map(h => h.width);
const heights = history.map(h => h.height);
return {
id: data.id,
current: {
width: data.lastWidth,
height: data.lastHeight
},
velocity: data.velocity,
isAnimating: data.isAnimating,
stats: {
minWidth: Math.min(...widths),
maxWidth: Math.max(...widths),
avgWidth: widths.reduce((a, b) => a + b, 0) / widths.length,
minHeight: Math.min(...heights),
maxHeight: Math.max(...heights),
avgHeight: heights.reduce((a, b) => a + b, 0) / heights.length
},
historyLength: history.length
};
}
getHistory(element, limit = null) {
const data = this.elements.get(element);
if (!data) return [];
const history = [...data.history];
return limit ? history.slice(-limit) : history;
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
return this;
}
off(event, callback) {
const callbacks = this.listeners.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) callbacks.splice(index, 1);
}
return this;
}
emit(event, data) {
const callbacks = this.listeners.get(event) || [];
callbacks.forEach(cb => cb(data));
}
unmonitor(element) {
this.observer.unobserve(element);
this.elements.delete(element);
}
destroy() {
this.observer.disconnect();
this.elements.clear();
this.listeners.clear();
}
}
*/
// ============================================
// EXERCISE 5: Fluid Typography System
// ============================================
/**
* Create a fluid typography system:
* - Scale fonts based on container width
* - Support multiple text elements
* - Handle minimum and maximum sizes
*
* Requirements:
* - Calculate font sizes using CSS clamp-like logic
* - Support different scaling for headings vs body
* - Provide presets for common use cases
*/
class FluidTypography {
// Your implementation here
}
/*
// SOLUTION:
class FluidTypography {
constructor(container, options = {}) {
this.container = container;
this.options = {
minWidth: options.minWidth || 320,
maxWidth: options.maxWidth || 1200,
baseFontSize: options.baseFontSize || 16,
scaleRatio: options.scaleRatio || 1.25
};
this.presets = {
h1: { minSize: 24, maxSize: 48, lineHeight: 1.1 },
h2: { minSize: 20, maxSize: 36, lineHeight: 1.2 },
h3: { minSize: 18, maxSize: 28, lineHeight: 1.3 },
h4: { minSize: 16, maxSize: 22, lineHeight: 1.4 },
body: { minSize: 14, maxSize: 18, lineHeight: 1.5 },
small: { minSize: 12, maxSize: 14, lineHeight: 1.4 },
...options.presets
};
this.elements = [];
this.observer = new ResizeObserver(entries => {
const width = entries[0].contentRect.width;
this.updateTypography(width);
});
this.init();
}
init() {
// Find all elements with data-fluid-type attribute
this.elements = Array.from(
this.container.querySelectorAll('[data-fluid-type]')
).map(element => ({
element,
preset: element.dataset.fluidType,
customConfig: this.parseCustomConfig(element)
}));
// Also auto-detect headings if enabled
if (this.options.autoDetect !== false) {
['h1', 'h2', 'h3', 'h4', 'p'].forEach(tag => {
this.container.querySelectorAll(tag).forEach(element => {
if (!element.dataset.fluidType) {
this.elements.push({
element,
preset: tag === 'p' ? 'body' : tag,
customConfig: null
});
}
});
});
}
this.observer.observe(this.container);
}
parseCustomConfig(element) {
const min = element.dataset.fluidMin;
const max = element.dataset.fluidMax;
const lineHeight = element.dataset.fluidLineHeight;
if (min || max || lineHeight) {
return {
minSize: min ? parseFloat(min) : null,
maxSize: max ? parseFloat(max) : null,
lineHeight: lineHeight ? parseFloat(lineHeight) : null
};
}
return null;
}
updateTypography(containerWidth) {
// Calculate scale factor (0 to 1)
const scale = Math.max(0, Math.min(1,
(containerWidth - this.options.minWidth) /
(this.options.maxWidth - this.options.minWidth)
));
this.elements.forEach(({ element, preset, customConfig }) => {
const config = this.getConfig(preset, customConfig);
if (!config) return;
// Calculate fluid font size
const fontSize = this.interpolate(
config.minSize,
config.maxSize,
scale
);
// Apply styles
element.style.fontSize = `${fontSize}px`;
element.style.lineHeight = String(config.lineHeight);
});
// Set CSS custom property for width-based calculations
this.container.style.setProperty('--fluid-scale', scale);
this.container.style.setProperty('--container-width', `${containerWidth}px`);
}
getConfig(preset, customConfig) {
const base = this.presets[preset] || this.presets.body;
if (customConfig) {
return {
minSize: customConfig.minSize ?? base.minSize,
maxSize: customConfig.maxSize ?? base.maxSize,
lineHeight: customConfig.lineHeight ?? base.lineHeight
};
}
return base;
}
interpolate(min, max, scale) {
return min + (max - min) * scale;
}
addPreset(name, config) {
this.presets[name] = {
minSize: config.minSize || 14,
maxSize: config.maxSize || 18,
lineHeight: config.lineHeight || 1.5
};
}
applyTo(element, preset) {
this.elements.push({
element,
preset,
customConfig: null
});
const rect = this.container.getBoundingClientRect();
this.updateTypography(rect.width);
}
generateCSS() {
// Generate CSS custom properties for use without JS
let css = `:root {\n`;
css += ` --fluid-min-width: ${this.options.minWidth}px;\n`;
css += ` --fluid-max-width: ${this.options.maxWidth}px;\n\n`;
Object.entries(this.presets).forEach(([name, config]) => {
css += ` /* ${name} */\n`;
css += ` --fluid-${name}-min: ${config.minSize}px;\n`;
css += ` --fluid-${name}-max: ${config.maxSize}px;\n`;
css += ` --fluid-${name}-lh: ${config.lineHeight};\n\n`;
});
css += `}\n`;
return css;
}
destroy() {
this.observer.disconnect();
this.elements.forEach(({ element }) => {
element.style.fontSize = '';
element.style.lineHeight = '';
});
}
}
*/
// ============================================
// TEST UTILITIES
// ============================================
console.log('=== ResizeObserver Exercises ===');
console.log('');
console.log('Exercises:');
console.log('1. ContainerQueryPolyfill - Container-based responsive styles');
console.log('2. AdaptiveGrid - Auto-calculating grid columns');
console.log('3. ResponsiveChartWrapper - Chart library resize handling');
console.log('4. ElementSizeMonitor - Size tracking with velocity');
console.log('5. FluidTypography - Container-based font scaling');
console.log('');
console.log('These exercises require a browser environment.');
console.log('Uncomment solutions to see implementations.');
// Export for browser use
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
ContainerQueryPolyfill,
AdaptiveGrid,
ResponsiveChartWrapper,
ElementSizeMonitor,
FluidTypography
};
}