rynex
Version:
A minimalist TypeScript framework for building reactive web applications with no virtual DOM
248 lines • 7.42 kB
JavaScript
/**
* Rynex Utility Helpers
* Utility functions for conditional rendering, fragments, etc.
*/
import { createElement } from '../dom.js';
import { effect } from '../state.js';
/**
* Fragment - render children without wrapper
*/
export function fragment(...children) {
const flat = children.flat(Infinity).filter(child => child !== null && child !== undefined);
return flat.filter(child => child instanceof HTMLElement);
}
/**
* Conditional rendering - show content when condition is true
*/
export function when(condition, content) {
return condition ? content() : null;
}
/**
* Show/hide based on condition with reactive support
* Usage: show(state.visible, element) or show(() => state.count > 0, element)
* Properly toggles DOM presence (not just display:none)
*/
export function show(condition, content) {
const container = createElement('div', { 'data-conditional': 'true' });
let isAttached = false;
const updateVisibility = (visible) => {
if (visible && !isAttached) {
container.appendChild(content);
isAttached = true;
}
else if (!visible && isAttached) {
if (container.contains(content)) {
container.removeChild(content);
}
isAttached = false;
}
};
if (typeof condition === 'function') {
// Reactive condition
effect(() => {
updateVisibility(condition());
});
}
else {
// Static condition
updateVisibility(condition);
}
return container;
}
/**
* Iterate over array and render items
*/
export function each(items, renderFn, keyFn) {
return items.map((item, index) => {
const element = renderFn(item, index);
if (keyFn) {
element.dataset.key = String(keyFn(item, index));
}
return element;
});
}
/**
* Switch case rendering
*/
export function switchCase(value, cases, defaultCase) {
const key = String(value);
if (cases[key]) {
return cases[key]();
}
return defaultCase ? defaultCase() : null;
}
/**
* Dynamic component - render component based on type
*/
export function dynamic(component, props, ...children) {
if (typeof component === 'function') {
return component(props);
}
return createElement(component, props, ...children);
}
/**
* Portal - render content in a different DOM location
*/
export function portal(children, target) {
const container = typeof target === 'string' ? document.querySelector(target) : target;
if (container) {
const flat = children.flat(Infinity);
flat.forEach(child => {
if (child instanceof HTMLElement) {
container.appendChild(child);
}
else if (typeof child === 'string' || typeof child === 'number') {
container.appendChild(document.createTextNode(String(child)));
}
});
}
return createElement('div', { 'data-portal': true });
}
/**
* CSS helper - add styles to external stylesheet
* Usage: css('.my-class', { color: 'red', fontSize: '16px' })
* Styles are added to a <style> tag in the document head
*/
let styleSheet = null;
let styleElement = null;
export function css(selector, styles) {
// Create style element if it doesn't exist
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.setAttribute('data-rynex-styles', 'true');
document.head.appendChild(styleElement);
styleSheet = styleElement.sheet;
}
if (typeof styles === 'string') {
// Raw CSS string
const rule = `${selector} { ${styles} }`;
if (styleSheet) {
styleSheet.insertRule(rule, styleSheet.cssRules.length);
}
}
else {
// Style object - convert to CSS string
const cssText = Object.entries(styles)
.map(([key, value]) => {
// Convert camelCase to kebab-case
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
return `${cssKey}: ${value}`;
})
.join('; ');
const rule = `${selector} { ${cssText} }`;
if (styleSheet) {
styleSheet.insertRule(rule, styleSheet.cssRules.length);
}
}
}
/**
* Lazy load component
* Loads component dynamically when needed
*/
export function lazy(loader) {
let cached = null;
let loading = null;
return async () => {
if (cached) {
return cached;
}
if (loading) {
return loading.then(() => cached);
}
loading = loader().then(module => {
cached = module.default;
loading = null;
return cached;
});
return loading;
};
}
/**
* Suspense boundary for async components
* Shows fallback while loading
*/
export function suspense(props, children) {
const container = createElement('div', { 'data-suspense': 'true' });
container.appendChild(props.fallback);
const loadContent = async () => {
try {
const content = await Promise.resolve(children());
container.innerHTML = '';
container.appendChild(content);
}
catch (error) {
if (props.onError) {
props.onError(error);
}
else {
console.error('Suspense error:', error);
}
}
};
loadContent();
return container;
}
/**
* Error boundary component
* Catches errors in child components
*/
export function errorBoundary(props, children) {
const container = createElement('div', { 'data-error-boundary': 'true' });
try {
const content = typeof children === 'function' ? children() : children;
container.appendChild(content);
// Add global error listener for this boundary
window.addEventListener('error', (event) => {
if (container.contains(event.target)) {
event.preventDefault();
const error = event.error || new Error(event.message);
if (props.onError) {
props.onError(error);
}
container.innerHTML = '';
container.appendChild(props.fallback(error));
}
});
}
catch (error) {
if (props.onError) {
props.onError(error);
}
container.appendChild(props.fallback(error));
}
return container;
}
/**
* Memoize component to prevent unnecessary re-renders
* Caches result based on props equality
*/
export function memo(component, areEqual) {
let lastProps = null;
let lastResult = null;
return (props) => {
const shouldUpdate = !lastProps ||
(areEqual ? !areEqual(lastProps, props) : !shallowEqual(lastProps, props));
if (shouldUpdate) {
lastProps = { ...props };
lastResult = component(props);
}
return lastResult;
};
}
/**
* Shallow equality check for objects
*/
function shallowEqual(obj1, obj2) {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (obj1[key] !== obj2[key]) {
return false;
}
}
return true;
}
//# sourceMappingURL=utilities.js.map