UNPKG

v0-rails

Version:

Convert React/JSX + Tailwind UI code from v0.dev to Rails ViewComponent classes and ERB templates with automatic slot detection, icon handling, and route generation

236 lines (213 loc) 6.22 kB
/** * Generate Stimulus controller for component * @param {Object} ir - Intermediate Representation * @returns {string} - Stimulus controller code */ function generateStimulusController(ir) { // Generate handler methods based on the component's JSX const handlers = extractEventHandlers(ir); const hasHandlers = handlers.length > 0; // Check if this is an interactive component that needs special handling const specialInteractions = generateSpecialInteractions(ir); const controllerName = ir.snakeCaseName; return `import { Controller } from "@hotwired/stimulus" /** * ${ir.name} component controller * * This controller handles the interactive behavior for the ${ir.name} component. */ export default class extends Controller { connect() { // Controller connected to the DOM console.log("${controllerName} controller connected") } disconnect() { // Controller disconnected from the DOM console.log("${controllerName} controller disconnected") } ${hasHandlers ? handlers.join('\n') : ''} ${specialInteractions} } `; } /** * Extract event handlers from component JSX * @param {Object} ir - Intermediate Representation * @returns {Array} - Array of handler method strings */ function extractEventHandlers(ir) { if (!ir.jsx) return []; const handlers = []; const handlerSet = new Set(); // Common React event handler patterns const eventHandlers = [{ pattern: /onClick\s*=\s*\{([^}]+)\}/g, name: 'click' }, { pattern: /onChange\s*=\s*\{([^}]+)\}/g, name: 'change' }, { pattern: /onSubmit\s*=\s*\{([^}]+)\}/g, name: 'submit' }, { pattern: /onBlur\s*=\s*\{([^}]+)\}/g, name: 'blur' }, { pattern: /onFocus\s*=\s*\{([^}]+)\}/g, name: 'focus' }, { pattern: /onKeyDown\s*=\s*\{([^}]+)\}/g, name: 'keydown' }, { pattern: /onKeyUp\s*=\s*\{([^}]+)\}/g, name: 'keyup' }, { pattern: /onMouseOver\s*=\s*\{([^}]+)\}/g, name: 'mouseover' }, { pattern: /onMouseOut\s*=\s*\{([^}]+)\}/g, name: 'mouseout' }]; for (const { pattern, name } of eventHandlers) { let match; while ((match = pattern.exec(ir.jsx)) !== null) { const handlerName = match[1].trim().replace(/[()]/g, ''); // Skip if we've already processed this handler if (handlerSet.has(handlerName)) continue; handlerSet.add(handlerName); handlers.push(generateHandlerMethod(handlerName, name)); } } // Handle interactive components even if no explicit handlers if (ir.isInteractive && handlers.length === 0) { if (ir.name.toLowerCase().includes('button')) { handlers.push(generateHandlerMethod('click', 'click')); } } return handlers; } /** * Generate a handler method for the given handler name and event * @param {string} handlerName - The name of the handler * @param {string} eventName - The event name * @returns {string} - Handler method string */ function generateHandlerMethod(handlerName, eventName) { return ` /** * Handles the ${eventName} event * @param {Event} event - The DOM event */ ${handlerName}(event) { // Handle ${eventName} event console.log("${handlerName} called with", event) // Example: Prevent default behavior if needed // event.preventDefault() // Example: Access dataset from element // const { myValue } = event.currentTarget.dataset // Example: Dispatch custom event // const customEvent = new CustomEvent("${handlerName}:success", { // detail: { value: someValue }, // bubbles: true // }) // this.element.dispatchEvent(customEvent) }`; } /** * Generate special interactions for specific component types * @param {Object} ir - Intermediate Representation * @returns {string} - Special interaction methods */ function generateSpecialInteractions(ir) { if (!ir.isInteractive) return ''; let specialMethods = ''; // Button specific interactions if (ir.name.toLowerCase().includes('button')) { specialMethods += ` /** * Toggle active state * @param {Event} event - The DOM event */ toggleActive(event) { this.element.classList.toggle('active') } /** * Set loading state * @param {Boolean} isLoading - Whether the button is loading */ setLoading(isLoading = true) { if (isLoading) { this.element.classList.add('loading') this.element.setAttribute('disabled', 'disabled') } else { this.element.classList.remove('loading') this.element.removeAttribute('disabled') } }`; } // Form input specific interactions if (ir.name.toLowerCase().includes('input') || ir.name.toLowerCase().includes('field')) { specialMethods += ` /** * Clear the input */ clear() { const inputElement = this.element.querySelector('input') if (inputElement) { inputElement.value = '' } } /** * Set validation state * @param {String} state - The validation state ('valid', 'invalid', null) */ setValidationState(state) { this.element.classList.remove('is-valid', 'is-invalid') if (state === 'valid') { this.element.classList.add('is-valid') } else if (state === 'invalid') { this.element.classList.add('is-invalid') } }`; } // Dropdown/menu specific interactions if (ir.name.toLowerCase().includes('dropdown') || ir.name.toLowerCase().includes('menu')) { specialMethods += ` /** * Toggle the dropdown * @param {Event} event - The DOM event */ toggle(event) { event.preventDefault() const isOpen = this.element.classList.contains('is-open') if (isOpen) { this.close() } else { this.open() } } /** * Open the dropdown */ open() { this.element.classList.add('is-open') // Example: Close when clicking outside // document.addEventListener('click', this.outsideClickHandler) } /** * Close the dropdown */ close() { this.element.classList.remove('is-open') // Example: Remove outside click handler // document.removeEventListener('click', this.outsideClickHandler) }`; } return specialMethods; } module.exports = { generateStimulusController };