rynex
Version:
A minimalist TypeScript framework for building reactive web applications with no virtual DOM
250 lines • 7.36 kB
JavaScript
/**
* Routing Helper Functions
* UI components and utilities for routing
*/
import { createElement } from '../dom.js';
/**
* Link component - creates a router-aware link
*/
export function Link(props, children) {
const link = document.createElement('a');
link.href = props.to;
if (props.class) {
link.className = props.class;
}
if (props.style) {
Object.assign(link.style, props.style);
}
// Add children
const childArray = Array.isArray(children) ? children : [children];
childArray.forEach(child => {
if (typeof child === 'string') {
link.appendChild(document.createTextNode(child));
}
else {
link.appendChild(child);
}
});
// Handle active class
if (props.activeClass) {
const updateActiveClass = () => {
const currentPath = window.location.pathname;
const isActive = props.exact
? currentPath === props.to
: currentPath.startsWith(props.to);
if (isActive) {
link.classList.add(props.activeClass);
}
else {
link.classList.remove(props.activeClass);
}
};
updateActiveClass();
window.addEventListener('popstate', updateActiveClass);
}
return link;
}
/**
* NavLink component - Link with automatic active styling
*/
export function NavLink(props, children) {
return Link({
...props,
activeClass: props.activeClass || 'active'
}, children);
}
/**
* Router outlet component - renders matched route
*/
export function RouterOutlet(router) {
const outlet = createElement('div', { class: 'router-outlet' });
router.mount(outlet);
return outlet;
}
/**
* Route guard component - conditionally render based on route
*/
export function RouteGuard(condition, children, fallback) {
const container = createElement('div', { class: 'route-guard' });
// This would need to be integrated with the router to work properly
// For now, return the children
container.appendChild(children);
return container;
}
/**
* Breadcrumb component
*/
export function Breadcrumb(props = {}) {
const nav = createElement('nav', {
class: props.class || 'breadcrumb',
style: props.style
});
const updateBreadcrumb = () => {
nav.innerHTML = '';
const paths = window.location.pathname.split('/').filter(Boolean);
// Home link
const homeLink = Link({ to: '/', class: 'breadcrumb-item' }, 'Home');
nav.appendChild(homeLink);
// Path segments
let currentPath = '';
paths.forEach((segment, index) => {
currentPath += '/' + segment;
if (props.separator) {
const separator = document.createTextNode(` ${props.separator} `);
nav.appendChild(separator);
}
const isLast = index === paths.length - 1;
if (isLast) {
const span = createElement('span', { class: 'breadcrumb-item active' });
span.textContent = segment;
nav.appendChild(span);
}
else {
const link = Link({ to: currentPath, class: 'breadcrumb-item' }, segment);
nav.appendChild(link);
}
});
};
updateBreadcrumb();
window.addEventListener('popstate', updateBreadcrumb);
return nav;
}
/**
* Back button component
*/
export function BackButton(props = {}) {
const button = createElement('button', {
class: props.class || 'back-button',
style: props.style
});
button.textContent = props.text || '← Back';
button.onclick = () => window.history.back();
return button;
}
/**
* Route params display (for debugging)
*/
export function RouteParamsDebug(router) {
const container = createElement('div', {
class: 'route-params-debug',
style: {
padding: '1rem',
background: '#f0f0f0',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '0.875rem'
}
});
const update = () => {
const route = router.getCurrentRoute();
if (route) {
container.innerHTML = `
<strong>Route Debug:</strong><br>
Path: ${route.path}<br>
Params: ${JSON.stringify(route.params)}<br>
Query: ${JSON.stringify(route.query)}<br>
Hash: ${route.hash}
`;
}
};
update();
return container;
}
/**
* Loading component for lazy routes
*/
export function RouteLoading(props = {}) {
const container = createElement('div', {
class: props.class || 'route-loading',
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2rem',
...props.style
}
});
const spinner = createElement('div', {
class: 'spinner',
style: {
width: '40px',
height: '40px',
border: '4px solid #f3f3f3',
borderTop: '4px solid #3498db',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}
});
container.appendChild(spinner);
if (props.text) {
const text = createElement('span', {
style: { marginLeft: '1rem' }
});
text.textContent = props.text;
container.appendChild(text);
}
// Add keyframes for spinner animation
if (!document.querySelector('#route-loading-styles')) {
const style = document.createElement('style');
style.id = 'route-loading-styles';
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
}
return container;
}
/**
* 404 Not Found component
*/
export function NotFound(props = {}) {
const container = createElement('div', {
class: props.class || 'not-found',
style: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '4rem 2rem',
textAlign: 'center',
...props.style
}
});
const title = createElement('h1', {
style: {
fontSize: '4rem',
margin: '0 0 1rem 0',
color: '#333'
}
});
title.textContent = props.title || '404';
const message = createElement('p', {
style: {
fontSize: '1.25rem',
margin: '0 0 2rem 0',
color: '#666'
}
});
message.textContent = props.message || 'Page not found';
container.appendChild(title);
container.appendChild(message);
if (props.homeLink !== false) {
const homeLink = Link({
to: '/',
style: {
padding: '0.75rem 1.5rem',
background: '#3498db',
color: 'white',
textDecoration: 'none',
borderRadius: '4px',
fontSize: '1rem'
}
}, 'Go Home');
container.appendChild(homeLink);
}
return container;
}
//# sourceMappingURL=routing.js.map