core_View.js

import { compile, render } from "./../_external/ejs/ejs.js";

/**
 * View
 * Template rendering system using EJS (Embedded JavaScript) templates.
 * Provides automatic injection of localization helpers and efficient DOM updates.
 *
 * @class
 *
 * @example
 * // Basic template rendering
 * app.view.render(container, `
 *     <h1>Welcome <%= username %></h1>
 *     <p><%= _lcs('greeting') %></p>
 * `, { username: 'John' });
 *
 * @example
 * // With localization helpers
 * app.view.render(container, `
 *     <h1><%= _lcs('welcome', { name: username }) %></h1>
 *     <p>Price: <%= _lcn(price, { style: 'currency', currency: 'USD' }) %></p>
 *     <time><%= _lcd(date, { dateStyle: 'long' }) %></time>
 * `, { username: 'John', price: 29.99, date: new Date() });
 *
 * @example
 * // Pre-compile templates for performance
 * const template = app.view.compile(`<h1><%= title %></h1>`);
 * // Later, use compiled template multiple times
 * const html1 = template({ title: 'Page 1' });
 * const html2 = template({ title: 'Page 2' });
 */
class View
{
    /**
     * Create a new View instance
     *
     * @constructor
     * @param {App} appInstance - Reference to the parent App instance
     */
    constructor( appInstance )
    {
        /**
         * Parent app instance
         * @type {App}
         */
        this.app = appInstance;

        /**
         * Global data available to all templates
         * @type {Object}
         */
        this.globalViewData = {};
    }

    /**
     * Set the browser window title
     *
     * @param {string} title - Title to set
     *
     * @example
     * app.view.setWindowTitle('My App - Dashboard');
     *
     * @example
     * // In state onEnter
     * async onEnter() {
     *     this.app.view.setWindowTitle(`User Profile - ${userName}`);
     * }
     */
    setWindowTitle( title )
    {
        if ( window && window.document && window.document.title )
        {
            window.document.title = title;
        }
    }

    /**
     * Compile an EJS template into a reusable function
     * Useful for templates that will be rendered multiple times.
     *
     * @param {string} template - EJS template string
     * @param {object} [opts=null] - EJS compiler options
     * @param {boolean} [opts.client=false] - Generate standalone client function
     * @param {string} [opts.filename] - Used by cache to key caches
     * @param {boolean} [opts.strict=false] - Use strict mode
     * @returns {Function} Compiled template function that accepts data object
     *
     * @see {@link https://ejs.co/#docs}
     *
     * @example
     * // Compile once, use many times
     * const userCard = app.view.compile(`
     *     <div class="user-card">
     *         <h3><%= name %></h3>
     *         <p><%= email %></p>
     *     </div>
     * `);
     *
     * const html1 = userCard({ name: 'John', email: 'john@example.com' });
     * const html2 = userCard({ name: 'Jane', email: 'jane@example.com' });
     */
    compile( template, opts )
    {
        return compile( template, opts );
    }

    /**
     * Generate HTML string from template and data without rendering to DOM
     *
     * @param {string} tmpl - EJS template string
     * @param {object} [data={}] - Template data
     * @param {object} [tmplOptions=null] - EJS rendering options
     * @returns {string} Rendered HTML string
     *
     * @example
     * // Get HTML for manual insertion
     * const html = app.view.getHtml(`<h1><%= title %></h1>`, { title: 'Hello' });
     * console.log(html); // '<h1>Hello</h1>'
     *
     * @example
     * // Use in AJAX response
     * const itemHtml = app.view.getHtml(itemTemplate, itemData);
     * fetch('/api/items', {
     *     method: 'POST',
     *     body: JSON.stringify({ html: itemHtml })
     * });
     */
    getHtml( tmpl, data = {}, tmplOptions = null )
    {
        return render( tmpl, this.createData( data ), tmplOptions );
    }

    /**
     * Render template with data directly to a DOM container
     * Automatically injects localization helpers (_lcs, _lcn, _lcd) into template data.
     *
     * @param {HTMLElement} container - Container element to render into
     * @param {string} tmpl - EJS template string
     * @param {object} [data={}] - Template data variables
     * @param {object} [tmplOptions=null] - EJS rendering options
     *
     * @throws {Error} If container is not an HTMLElement
     *
     * @example
     * // Simple rendering
     * app.view.render(this.app.container, `
     *     <h1>Hello <%= name %></h1>
     * `, { name: 'World' });
     *
     * @example
     * // With loops and conditions
     * app.view.render(container, `
     *     <ul>
     *     <% users.forEach(user => { %>
     *         <li><%= user.name %> - <%= user.email %></li>
     *     <% }); %>
     *     </ul>
     *     <% if (users.length === 0) { %>
     *         <p>No users found</p>
     *     <% } %>
     * `, { users: userList });
     *
     * @example
     * // Using localization helpers (auto-injected)
     * app.view.render(container, `
     *     <h1><%= _lcs('welcome') %></h1>
     *     <p><%= _lcs('greeting', { name: userName }) %></p>
     *     <span><%= _lcn(price, { style: 'currency', currency: 'EUR' }) %></span>
     *     <time><%= _lcd(date, { dateStyle: 'medium' }) %></time>
     * `, { userName: 'John', price: 99.99, date: new Date() });
     */
    render( container, tmpl, data = {}, tmplOptions = null )
    {
        const html = this.getHtml( tmpl, data, tmplOptions );
        this.renderHtml( container, html );
    }

    /**
     * Render pre-generated HTML string directly to a container
     * Uses replaceChildren() for efficient DOM updates.
     *
     * @param {HTMLElement} container - Container element to render into
     * @param {string} html - Pre-generated HTML string
     *
     * @throws {Error} If container is not an HTMLElement
     *
     * @example
     * // Render pre-generated HTML
     * const html = '<h1>Hello</h1><p>World</p>';
     * app.view.renderHtml(container, html);
     *
     * @example
     * // Combined with getHtml
     * const html = app.view.getHtml(template, data);
     * app.view.renderHtml(container, html);
     */
    renderHtml( container, html )
    {
        if ( !container || false === ( container instanceof HTMLElement ) )
        {
            throw new Error( 'Invalid container. Given container must be an instance of an HTMLElement.' );
        }

        const temp = document.createElement('div');
        temp.innerHTML = html;
        container.replaceChildren(...temp.childNodes);
    }

    createData( data = {} )
    {
        if ( data.hasOwnProperty( '_lcs' ) )
        {
            console.warn( '_lcs already exists in template data.' );
        }
        else
        {
            data[ '_lcs' ] = this.app.l18n.t.bind( this.app.l18n );
        }

        if ( data.hasOwnProperty( '_lcn' ) )
        {
            console.warn( '_lcn already exists in template data.' );
        }
        else
        {
            data[ '_lcn' ] = this.app.l18n.n.bind( this.app.l18n );
        }

        if ( data.hasOwnProperty( '_lcd' ) )
        {
            console.warn( '_lcd already exists in template data.' );
        }
        else
        {
            data[ '_lcd' ] = this.app.l18n.d.bind( this.app.l18n );
        }

        const gvdKeys = Object.keys( this.globalViewData );
        for ( let gi = 0; gi < gvdKeys.length; gi++ )
        {
            if ( data.hasOwnProperty( gvdKeys[ gi ] ) )
            {
                console.warn( `The globalViewData entry ${gvdKeys[ gi ]} already exists in template data.` );
            }
            else
            {
                data[ gvdKeys[ gi ] ] = this.globalViewData[ gvdKeys[ gi ] ];
            }
        }

        return data;
    }
}

export { View };