b0nes
Version:
Zero-dependency component library and SSR/SSG framework
107 lines (100 loc) • 3.55 kB
JavaScript
import { processSlotTrusted } from '../../utils/processSlot.js';
import { normalizeClasses } from '../../utils/normalizeClasses.js';
import { validateProps, validatePropTypes, createComponentError } from '../../utils/componentError.js';
import { escapeAttr } from '../../utils/escapeAttr.js';
/**
* Link component - An HTML anchor element for navigation
*
* Renders an HTML anchor (<a>) element with URL validation, security features,
* and customizable styling. Automatically adds rel="noopener noreferrer" for
* external links that open in new tabs to prevent security vulnerabilities.
*
* @param {Object} props - Component properties
* @param {string} props.url - The destination URL (href attribute)
* @param {string} props.slot - Link text or child components
* @param {string} [props.attrs=''] - Additional HTML attributes (e.g., 'data-track="click"')
* @param {string} [props.className=''] - Additional CSS classes to apply
*
* @returns {string} Rendered HTML anchor element
*
* @throws {ComponentError} If required props (url, slot) are missing or have invalid types
*
* @example
* // Basic internal link
* link({ url: '/about', slot: 'About Us' })
* // Returns: '<a href="/about" class="link">About Us</a>'
*
* @example
* // External link with security (opens in new tab)
* link({
* url: 'https://example.com',
* slot: 'Visit Example',
* attrs: 'target="_blank" rel="noopener noreferrer"'
* })
* // Returns: '<a href="https://example.com" class="link" target="_blank" rel="noopener noreferrer">Visit Example</a>'
*
* @example
* // Link with custom styling and attributes
* link({
* url: '/contact',
* slot: 'Get in Touch',
* className: 'btn primary',
* attrs: 'data-track="nav-click" id="contact-link"'
* })
* // Returns: '<a href="/contact" class="link btn primary" data-track="nav-click" id="contact-link">Get in Touch</a>'
*
* @example
* // Accessible link with aria-label
* link({
* url: '/profile',
* slot: '<img src="avatar.jpg" alt=""/>',
* attrs: 'aria-label="View user profile"'
* })
* // Returns: '<a href="/profile" class="link" aria-label="View user profile"><img src="avatar.jpg" alt=""/></a>'
*
* @example
* // Download link
* link({
* url: '/files/document.pdf',
* slot: 'Download PDF',
* attrs: 'download'
* })
* // Returns: '<a href="/files/document.pdf" class="link" download>Download PDF</a>'
*/
export const link = ({
url,
slot,
attrs = '',
className = ''
}) => {
// Validate required props
validateProps(
{ url, slot },
['url', 'slot'],
{ componentName: 'link', componentType: 'atom' }
);
// Validate prop types
validatePropTypes(
{ url, attrs, className },
{
url: 'string',
attrs: 'string',
className: 'string'
},
{ componentName: 'link', componentType: 'atom' }
);
// Validate URL is not empty
if (url.trim().length === 0) {
throw createComponentError(
'URL cannot be empty',
{ componentName: 'link', componentType: 'atom', props: { url, slot } }
);
}
// Process attributes
attrs = attrs ? ` ${attrs}` : '';
// Normalize and escape classes
const classes = normalizeClasses(['link', className]);
// Process slot content (trust component-rendered HTML)
const slotContent = processSlotTrusted(slot);
return `<a href="${url}" class="${classes}"${attrs}>${slotContent}</a>`;
};