From 765c4587a16c57fc9a2f5e698939e122e51738a8 Mon Sep 17 00:00:00 2001 From: Jelle De Loecker Date: Mon, 29 Apr 2024 18:53:03 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20initial=20reactive=20variable?= =?UTF-8?q?=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + lib/bootstrap.js | 1 + lib/core/base.js | 2 + lib/core/hawkejs.js | 8 +- lib/core/renderer.js | 104 +++++++++++++++ lib/core/template.js | 86 +++++++++++-- lib/core/templates.js | 8 +- lib/element/custom_element.js | 85 +++++++++---- lib/expression/expression.js | 6 +- lib/parser/builder.js | 113 +++++------------ lib/parser/directives_parser.js | 1 + lib/parser/function_body.js | 120 ++++++++++++++++++ lib/parser/subroutine.js | 26 +++- test/10-expressions.js | 153 +++++++++++++++++++++-- test/templates/simple/reactive_print.hwk | 3 + 15 files changed, 581 insertions(+), 136 deletions(-) create mode 100644 lib/parser/function_body.js create mode 100644 test/templates/simple/reactive_print.hwk diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ae78daf..2da697ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * Only clone certain variables once, when they are initially set * Prepare text nodes during compiling * Clean up custom element `renderCustomTemplate` function +* Add initial reactive variable support ## 2.3.19 (2024-04-13) diff --git a/lib/bootstrap.js b/lib/bootstrap.js index 0359de1c..7e883e01 100644 --- a/lib/bootstrap.js +++ b/lib/bootstrap.js @@ -56,6 +56,7 @@ Blast.requireAll([ ['parser', 'marked'], ['parser', 'markdown_parser'], ['parser', 'builder'], + ['parser', 'function_body'], ['parser', 'subroutine'] ], options); diff --git a/lib/core/base.js b/lib/core/base.js index 2e9a543a..dd97611c 100644 --- a/lib/core/base.js +++ b/lib/core/base.js @@ -464,8 +464,10 @@ function logLog(type, args) { } // Some Symbols +Hawkejs.RENDER_INSTRUCTION = Symbol('render_instruction'); Hawkejs.DELAY_SYNC_RENDER = Symbol('delay_sync_render'); Hawkejs.CREATED_MANUALLY = Symbol('created_manually'); +Hawkejs.REACTIVE_VALUES = Symbol('reactive_values'); Hawkejs.APPLIED_OPTIONS = Symbol('applied_options'); Hawkejs.RENDER_CONTENT = Symbol('render_hawkejs_content'); Hawkejs.SERIALIZE_FORM = Symbol('serialize_form'); diff --git a/lib/core/hawkejs.js b/lib/core/hawkejs.js index d2a9f170..d41dbc2b 100644 --- a/lib/core/hawkejs.js +++ b/lib/core/hawkejs.js @@ -414,7 +414,7 @@ Main.setMethod(function createElement(name, xml) { * * @author Jelle De Loecker * @since 1.0.0 - * @version 2.2.0 + * @version 2.4.0 * * @param {Object} options * @param {String} options.template_name @@ -430,13 +430,15 @@ Main.setMethod(function compile(options) { if (arguments.length == 2) { options = { template_name : options, - template : arguments[1] + template : arguments[1], + is_inline : true, }; } else { options = { template_name : 'inline_' + (counter++), template : options, - cache : false + cache : false, + is_inline : true, }; } } else if (!options) { diff --git a/lib/core/renderer.js b/lib/core/renderer.js index 29d007f6..26aa9d10 100644 --- a/lib/core/renderer.js +++ b/lib/core/renderer.js @@ -159,6 +159,44 @@ Renderer.enforceSingletonElement('html'); Renderer.enforceSingletonElement('head'); Renderer.enforceSingletonElement('body'); +/** + * Split the input into keys & vlaues + * + * @author Jelle De Loecker + * @since 2.4.0 + * @version 2.4.0 + * + * @param {HTMLElement} element + * @param {Develry.Optional[]} values + */ +Renderer.setStatic(function attachReactiveListeners(element, values) { + + if (!element || !values?.length) { + return; + } + + values.forEach(optional => optional.onChange(() => reactiveRerender(element))); +}); + +/** + * Rerender the given element + * + * @author Jelle De Loecker + * @since 2.4.0 + * @version 2.4.0 + * + * @param {HTMLElement} element + */ +function reactiveRerender(element) { + + // Make sure we have all the required info for a rerender + if (!element.is_custom_hawkejs_element && !element[Hawkejs.RENDER_INSTRUCTION]) { + return; + } + + return Hawkejs.Element.Element.prototype.rerender.call(element); +} + /** * The variables to use when rendering * @@ -2282,6 +2320,72 @@ Renderer.setMethod(function applyElementOptions(element, options, for_sync_rende } } } + + // If the element body is rendered using reference variables, + // the element has to be registered so the browser knows about it + if (options.reference_count > 0 && options.references?.length) { + + let values = []; + + for (let name of options.references) { + let value = this.active_variables.get(name); + + if (!value || !(value instanceof Classes.Develry.Optional)) { + continue; + } + + values.push(value); + } + + if (this.attachReactiveReferences(element, values)) { + + if (options.variables && element[Hawkejs.VARIABLES]) { + let new_variables = this.active_variables.overlay(element[Hawkejs.VARIABLES]); + element[Hawkejs.VARIABLES] = new_variables; + } else if (!element[Hawkejs.VARIABLES]) { + element[Hawkejs.VARIABLES] = this.active_variables; + } + + if (!element.is_custom_hawkejs_element && options.body) { + let instruction = {}; + + if (options.body.source_name) { + instruction.template = options.body.source_name; + instruction.function = options.body.name; + } else { + instruction.source = options.body.source_code; + } + + element[Hawkejs.RENDER_INSTRUCTION] = instruction; + } + } + } +}); + +/** + * Attach reactive references + * + * @author Jelle De Loecker + * @since 2.4.0 + * @version 2.4.0 + * + * @param {HTMLElement} element + * @param {Develry.Optional[]} values + */ +Renderer.setMethod(function attachReactiveReferences(element, values) { + + // Only bother registering the element if there are values + if (!element || !values?.length) { + return; + } + + element[Hawkejs.REACTIVE_VALUES] = values; + + this.registerElementInstance(element); + + Hawkejs.Renderer.attachReactiveListeners(element, values); + + return true; }); /** diff --git a/lib/core/template.js b/lib/core/template.js index e71fa651..10eef3b9 100644 --- a/lib/core/template.js +++ b/lib/core/template.js @@ -1,5 +1,6 @@ const TARGET_NAME = Symbol('target_block_name'), - RENDERER = Symbol('renderer'); + RENDERER = Symbol('renderer'), + COMPILED = Symbol('compiled'); /** * The Template class @@ -23,13 +24,12 @@ const Template = Fn.inherits('Hawkejs.Base', function Template(templates, name, // Are we switching out this template? this.switching_template = false; - // Set the name + // Do we want to use a specific subroutine? + // (If not set, the main "compiledView" function will be used) + this.wanted_subroutine = null; + if (name) { - if (typeof name == 'function') { - this.fnc = name; - } else { - this.name = name; - } + this.setTemplateInfo(name); } }); @@ -440,6 +440,46 @@ Template.setStatic(function unDry(obj) { return result; }); +/** + * Set the wanted template info + * + * @author Jelle De Loecker + * @since 2.4.0 + * @version 2.4.0 + * + * @param {string|Function|Object} info + */ +Template.setMethod(function setTemplateInfo(info) { + + if (!info) { + return; + } + + let type = typeof info; + + if (type == 'string') { + this.name = info; + } else if (type == 'function') { + this.fnc = info; + } else if (type == 'object') { + + if (info.template) { + this.name = info.template; + } + + if (info.function) { + this.wanted_subroutine = info.function; + } + + if (info[COMPILED]) { + this.fnc = info[COMPILED]; + } else if (info.source) { + this.fnc = this.hawkejs.compile(info.source); + info[COMPILED] = this.fnc; + } + } +}); + /** * Return an object for json-drying this object * @@ -544,12 +584,40 @@ Template.setMethod(Blast.checksumSymbol, function checksum() { return Obj.checksum([this.name, this.theme, this.source_name, this.fnc]); }); +/** + * Get a subroutine function by name + * + * @author Jelle De Loecker + * @since 2.4.0 + * @version 2.4.0 + * + * @param {string} name + * + * @return {Function} + */ +Template.setMethod(function getSubroutineFunction(name) { + + if (!this.fnc) { + return; + } + + if (arguments.length == 0) { + if (!this.wanted_subroutine) { + return this.fnc; + } + + name = this.wanted_subroutine; + } + + return this.fnc.compiled?.[name]; +}); + /** * Execute the compiled code * * @author Jelle De Loecker * @since 2.1.0 - * @version 2.3.7 + * @version 2.4.0 * * @param {Object} variables */ @@ -582,7 +650,7 @@ Template.setMethod(function evaluate(variables) { renderer.active_variables = variables // Actually call the compiled function - this.fnc.call(renderer, renderer, this, variables, renderer.helpers); + this.getSubroutineFunction().call(renderer, renderer, this, variables, renderer.helpers); // End the template's main block renderer.end(this.main_block_name); diff --git a/lib/core/templates.js b/lib/core/templates.js index 1aaecc2a..ccefbf14 100644 --- a/lib/core/templates.js +++ b/lib/core/templates.js @@ -158,7 +158,7 @@ Templates.setProperty(function name() { * * @author Jelle De Loecker * @since 1.0.0 - * @version 2.2.20 + * @version 2.4.0 * * @type {Array} */ @@ -208,7 +208,7 @@ Templates.enforceProperty(function templates(names) { names[i].main_name_modifier = this.main_name_modifier; } - if (i == 0 && names[i].fnc) { + if (i == 0 && names[i].getSubroutineFunction()) { this.active = names[i]; } } @@ -335,13 +335,13 @@ Templates.setMethod(function toString() { * * @author Jelle De Loecker * @since 1.0.0 - * @version 2.3.15 + * @version 2.4.0 * * @param {Function} callback */ Templates.setMethod(function getCompiled(callback) { - if (this.active && this.active.fnc) { + if (this.active && this.active.getSubroutineFunction()) { return callback(null, this.active); } diff --git a/lib/element/custom_element.js b/lib/element/custom_element.js index 4c0468da..6348687d 100644 --- a/lib/element/custom_element.js +++ b/lib/element/custom_element.js @@ -408,11 +408,17 @@ Element.setStatic(function sceneReady(fnc) { }, false); /** - * Revive this object + * Revive this element. + * This is also used for non-custom HTML elements. * * @author Jelle De Loecker * @since 1.1.0 * @version 2.2.22 + * + * @param {Object} obj + * @param {boolean} force + * + * @return {HTMLElement} */ Element.setStatic(function unDry(obj, force) { @@ -528,6 +534,11 @@ Element.setStatic(function unDry(obj, force) { element[Hawkejs.VARIABLES] = obj.variables; } + if (obj.reactive?.length) { + Hawkejs.Renderer.attachReactiveListeners(element, obj.reactive); + element[Hawkejs.RENDER_INSTRUCTION] = obj.instructions; + } + // Delay this so the object is fully undried Element.sceneReady(function() { if (typeof element.undried == 'function') { @@ -1058,19 +1069,19 @@ function renderCustomTemplate(re_render) { function renderContentsWithTemplate(element, template, variables, slot_data, pledge) { // See if the element has already finished rendering, somehow - const alreadyDone = () => element[CURRENT_RENDER] != pledge || pledge.is_done; + const alreadyDone = () => element[CURRENT_RENDER] && element[CURRENT_RENDER] != pledge || pledge.is_done; if (pledge && alreadyDone()) { pledge.resolve(false); return pledge; - } + } let renderer = Element.prototype.ensureHawkejsRenderer.call(element); if (element.constructor.use_new_renderer_scope || renderer?.dialog_open) { renderer = renderer.createSubRenderer(); renderer.scope_id = renderer.getId(); - } + } if (!pledge) { pledge = new Classes.Pledge.Swift(); @@ -1090,7 +1101,7 @@ function renderContentsWithTemplate(element, template, variables, slot_data, ple variables = element[Hawkejs.VARIABLES].overlay(variables); } else { variables = renderer.prepareVariables(variables); - } + } if (slot_data) { variables.setFromTemplate('self', element); @@ -1122,12 +1133,7 @@ function renderContentsWithTemplate(element, template, variables, slot_data, ple return; } - let nodes = block.toElements(), - i; - - for (i = 0; i < nodes.length; i++) { - element.append(nodes[i]); - } + Hawkejs.replaceChildren(element, block.toElements()); _insertSlotData.call(element, slot_data); @@ -1545,11 +1551,12 @@ Element.setMethod(function dryClone() { }); /** - * Dry this object + * Serialize this element for JSON-Dry. + * This is also used for non-custom HTML elements on the server-side. * * @author Jelle De Loecker * @since 1.1.0 - * @version 2.3.7 + * @version 2.4.0 * * @return {Object} */ @@ -1590,12 +1597,16 @@ Element.setMethod(function toDry() { innerHTML : this.innerHTML, renderer : this.hawkejs_renderer, variables : this[Hawkejs.VARIABLES], + reactive : this[Hawkejs.REACTIVE_VALUES], + instructions : this[Hawkejs.RENDER_INSTRUCTION], }; } else { value = { hawkejs_id : this.hawkejs_id, assigned_data : this.assigned_data, variables : this[Hawkejs.VARIABLES], + reactive : this[Hawkejs.REACTIVE_VALUES], + instructions : this[Hawkejs.RENDER_INSTRUCTION], }; } @@ -2707,15 +2718,18 @@ Element.setMethod(function waitForTasks() { }); /** - * Re-render this element if possible + * Re-render this element if possible. + * This is also used to re-render non-custom elements. * * @author Jelle De Loecker * @since 2.0.0 - * @version 2.3.17 + * @version 2.4.0 */ Element.setMethod(function rerender() { - if (!this.inner_template) { + const IS_CUSTOM_ELEMENT = this.is_custom_hawkejs_element; + + if (IS_CUSTOM_ELEMENT && !this.inner_template) { return; } @@ -2741,6 +2755,8 @@ Element.setMethod(function rerender() { // Get the current dimensions let client_rects = Blast.isBrowser ? this.getClientRects()?.[0] : null, + new_width, + new_height, previous_width, previous_height; @@ -2748,14 +2764,29 @@ Element.setMethod(function rerender() { previous_width = this.style.width; previous_height = this.style.height; - this.style.width = ~~(client_rects.width) + 'px'; - this.style.height = ~~(client_rects.height) + 'px'; + this.style.width = new_width = ~~(client_rects.width) + 'px'; + this.style.height = new_height = ~~(client_rects.height) + 'px'; } - let pledge = renderCustomTemplate.call(this, true); + let pledge; - if (pledge) { - this.delayAssemble(pledge); + if (IS_CUSTOM_ELEMENT) { + pledge = renderCustomTemplate.call(this, true); + + if (pledge) { + this.delayAssemble(pledge); + } + } else { + let instructions = this[Hawkejs.RENDER_INSTRUCTION]; + + if (instructions) { + try { + this[CURRENT_RENDER] = null; + renderContentsWithTemplate(this, instructions, null, null, pledge); + } catch (err) { + pledge = Classes.Pledge.reject(err); + } + } } Classes.Pledge.Swift.done(pledge, err => { @@ -2764,8 +2795,16 @@ Element.setMethod(function rerender() { this.removeAttribute('data-he-rerendering'); if (client_rects?.width) { - this.style.width = previous_width; - this.style.height = previous_height; + + // Reset the original width & height, + // but only if they haven't been changed in the meantime + if (this.style.width == new_width) { + this.style.width = previous_width; + } + + if (this.style.height == new_height) { + this.style.height = previous_height; + } if (this.getAttribute('style') == '') { this.removeAttribute('style'); diff --git a/lib/expression/expression.js b/lib/expression/expression.js index 97892f90..334089eb 100644 --- a/lib/expression/expression.js +++ b/lib/expression/expression.js @@ -426,7 +426,11 @@ Expression.setMethod(function _getValueByPath(path, variables, main_variables) { } } - if (current && current instanceof Classes.Hawkejs.Variables) { + if (!current) { + break; + } + + if (current instanceof Classes.Hawkejs.Variables) { current = current.get(piece); } else { current = current[piece]; diff --git a/lib/parser/builder.js b/lib/parser/builder.js index 4977484c..08a384ee 100644 --- a/lib/parser/builder.js +++ b/lib/parser/builder.js @@ -5,7 +5,7 @@ const R = Hawkejs.Parser.Parser.rawString; * * @author Jelle De Loecker * @since 2.2.0 - * @version 2.2.0 + * @version 2.4.0 */ const Builder = Fn.inherits(null, 'Hawkejs.Parser', function Builder(hawkejs) { @@ -15,8 +15,12 @@ const Builder = Fn.inherits(null, 'Hawkejs.Parser', function Builder(hawkejs) { this.current = null; this.root = this.createFunctionBody('root'); + // The name of the template this.template_name = null; + // Is this an inline template? + this.is_inline = null; + // Properties used during compilation this._functions_to_compile = null; this._root_subroutine = null; @@ -51,42 +55,13 @@ Builder.setProperty(function only_html_allowed() { * @param {string} type * @param {string} info * - * @return {Object} + * @return {Hawkejs.Parser.FunctionBody} */ Builder.setMethod(function createFunctionBody(type, info) { - let entry = { - id : this.functions.length, - parent : this.current, - content : [], - type : type, - info : info || null, - name : null, - }; - - let name; - - if (type == 'root') { - name = 'compiledView'; - } else { - name = ['cpv'] - - if (Blast.isDevelopment) { - name.push(type); - - if (info) { - name.push(Bound.String.slug(info, '_')); - } - } - - name.push(entry.id); - name = name.join('_'); - } - - entry.name = name; + let entry = new Hawkejs.Parser.FunctionBody(this, type, this.functions.length, this.current, info); this.functions.push(entry); - this.current = entry; return entry; @@ -422,7 +397,7 @@ Builder.setMethod(function addError(type, message, entry) { * @since 2.2.0 * @version 2.2.0 * - * @return {*} + * @return {Hawkejs.Parser.FunctionBody} */ Builder.setMethod(function getCurrentBlockOrElement() { @@ -447,21 +422,21 @@ Builder.setMethod(function getCurrentBlockOrElement() { * * @author Jelle De Loecker * @since 2.2.0 - * @version 2.2.0 + * @version 2.4.0 * * @param {Array} path */ Builder.setMethod(function seenReference(path) { + /** @type Hawkejs.Parser.FunctionBody */ let current = this.getCurrentBlockOrElement(); if (!current) { - console.log('Failed to find block or element for reference: ' + path.join('.')); + //console.log('Failed to find block or element for reference: ' + path.join('.')); return; } - current.entry.create_separate_subroutine = true; - + current.containsReference(path); }); /** @@ -616,16 +591,16 @@ Builder.setMethod(function toCode() { * * @author Jelle De Loecker * @since 2.2.0 - * @version 2.2.0 + * @version 2.4.0 * - * @param {Object} body + * @param {Hawkejs.Parser.FunctionBody} body * @param {Array} args * * @return {Hawkejs.Parser.Subroutine} */ Builder.setMethod(function _createSubroutine(body, args) { - let subroutine = new Hawkejs.Parser.Subroutine(this.hawkejs); + let subroutine = new Hawkejs.Parser.Subroutine(this, body); subroutine.name = body.name; subroutine.args = args; @@ -643,7 +618,7 @@ Builder.setMethod(function _createSubroutine(body, args) { * @version 2.2.0 * * @param {Hawkejs.Parser.Subroutine} subroutine - * @param {Object} body + * @param {Hawkejs.Parser.FunctionBody} body */ Builder.setMethod(function _compileBody(subroutine, body) { @@ -696,20 +671,30 @@ Builder.setMethod(function _compileEntry(subroutine, entry) { let create_separate_subroutine = entry.create_separate_subroutine; - if (create_separate_subroutine && entry.function_body && entry.function_body.content.length) { - let sr_body = this._createSubroutine(entry.function_body, ['_$expression']); + // There are several reasons why the element should get a separate subroutine: + // the most probably one is because it contains a reference variable + if (create_separate_subroutine && entry.function_body?.content?.length) { + + const body = entry.function_body; + const subroutine = this._createSubroutine(body, ['_$expression']); if (!options) { options = {body: null}; } - options.body = R(sr_body.name); + options.body = R(subroutine.name); + this._compileBody(subroutine, body); + subroutine.requires(subroutine); - this._compileBody(sr_body, entry.function_body); - - subroutine.requires(sr_body); + // If the body contains reference variables, + // the Renderer needs to know about it + if (body.reference_count > 0) { + options.references = body.references; + options.reference_count = body.reference_count; + } } + // @TODO: DELETE THIS if (store_source) { if (!options) { options = {}; @@ -721,7 +706,7 @@ Builder.setMethod(function _compileEntry(subroutine, entry) { options.properties.push({ name : 'hwk_source', - value : extractSource(entry.function_body), + value : entry.function_body.getSourceCode(), }); } @@ -813,38 +798,6 @@ Builder.setMethod(function _compileEntry(subroutine, entry) { } }); -/** - * Return the original source code for this function body - * - * @author Jelle De Loecker - * @since 2.3.7 - * @version 2.3.7 - * - * @param {Object} function_body - * - * @return {String} - */ -function extractSource(function_body) { - - let result = '', - entry; - - for (entry of function_body.content) { - - if (entry.function_body) { - result += extractSource(entry.function_body); - continue; - } - - if (entry.source) { - result += entry.source; - continue; - } - } - - return result; -} - /** * Compile element options * diff --git a/lib/parser/directives_parser.js b/lib/parser/directives_parser.js index 9723951a..f080b9ba 100644 --- a/lib/parser/directives_parser.js +++ b/lib/parser/directives_parser.js @@ -142,6 +142,7 @@ Dparser.setMethod(function build(options) { if (!builder) { builder = new Hawkejs.Parser.Builder(this.hawkejs); builder.template_name = this.name; + builder.is_inline = (options?.is_inline ?? this.options?.is_inline) || false; } if (!builder.directives_parser) { diff --git a/lib/parser/function_body.js b/lib/parser/function_body.js new file mode 100644 index 00000000..1753a8ea --- /dev/null +++ b/lib/parser/function_body.js @@ -0,0 +1,120 @@ +const R = Hawkejs.Parser.Parser.rawString; + +/** + * The FunctionBody class holds information for creating a Subroutine + * + * @author Jelle De Loecker + * @since 2.4.0 + * @version 2.4.0 + * + * @param {Hawkejs.Parser.Builder} builder + * @param {string} type + * @param {number} id + * @param {Hakejs.Parser.FunctionBody} parent + * @param {string} info + */ +const FunctionBody = Fn.inherits(null, 'Hawkejs.Parser', function FunctionBody(builder, type, id, parent, info) { + + // The builder instance + this.builder = builder; + + // The ID of this body + this.id = id; + + // The parent body + this.parent = parent; + + // The content + this.content = []; + + // The type of body + this.type = type; + + // Any extra info + this.info = info || null; + + // The generated name + this.name = null; + + // The amount of references in this body + this.reference_count = 0; + + // References + this.references = null; + + if (type == 'root') { + this.name = 'compiledView'; + } else { + let name = ['cpv'] + + if (Blast.isDevelopment) { + name.push(type); + + if (info) { + name.push(Bound.String.slug(info, '_')); + } + } + + name.push(this.id); + this.name = name.join('_'); + } +}); + +/** + * Indicate a reference has been seen in this body + * + * @author Jelle De Loecker + * @since 2.4.0 + * @version 2.4.0 + * + * @param {Array} path + */ +FunctionBody.setMethod(function containsReference(path) { + + if (!path?.length) { + return; + } + + if (!this.references) { + this.references = []; + } + + // For now, only the first piece of a path can be a reference. + // In the future, something like `self.&prop` could be allowed + let piece = path[0]; + + this.references.push(piece); + this.reference_count++; + this.entry.create_separate_subroutine = true; + +}); + +/** + * Return the source code of this body + * + * @author Jelle De Loecker + * @since 2.4.0 + * @version 2.4.0 + * + * @return {string} + */ +FunctionBody.setMethod(function getSourceCode() { + + let result = '', + entry; + + for (entry of this.content) { + + if (entry.function_body) { + result += entry.function_body.getSourceCode(); + continue; + } + + if (entry.source) { + result += entry.source; + continue; + } + } + + return result; +}); \ No newline at end of file diff --git a/lib/parser/subroutine.js b/lib/parser/subroutine.js index 2338eb5f..2523e8c4 100644 --- a/lib/parser/subroutine.js +++ b/lib/parser/subroutine.js @@ -3,11 +3,21 @@ * * @author Jelle De Loecker * @since 2.2.0 - * @version 2.2.0 + * @version 2.4.0 + * + * @param {Hawkejs.Parser.Builder} builder + * @param {Hawkejs.Parser.FunctionBody} body */ -const Subroutine = Fn.inherits(null, 'Hawkejs.Parser', function Subroutine(hawkejs) { +const Subroutine = Fn.inherits(null, 'Hawkejs.Parser', function Subroutine(builder, body) { + + // The builder instance + this.builder = builder; + + // The root hawkejs instance + this.hawkejs = builder.hawkejs; - this.hawkejs = hawkejs; + // The original body instance + this.original_body = body; this.name = ''; this.code = ''; @@ -34,7 +44,7 @@ Subroutine.setMethod(function requires(subroutine) { * * @author Jelle De Loecker * @since 2.2.0 - * @version 2.2.0 + * @version 2.4.0 */ Subroutine.setMethod(function toString() { let result = 'function ' + this.name + '('; @@ -51,6 +61,14 @@ Subroutine.setMethod(function toString() { result += this.code; result += '\n}\n'; + if (this.builder.is_inline) { + if (this.original_body?.reference_count > 0) { + result += this.name + '.source_code = ' + JSON.stringify(this.original_body.getSourceCode()) + ';\n'; + } + } else { + result += this.name + '.source_name = ' + JSON.stringify(this.builder.template_name) + ';\n'; + } + return result; }); diff --git a/test/10-expressions.js b/test/10-expressions.js index d3fa72c3..e9126e06 100644 --- a/test/10-expressions.js +++ b/test/10-expressions.js @@ -913,6 +913,32 @@ This should be a converted variable: createTests(tests); }); + describe('Reactive variables', () => { + + let tests = [ + [ + (vars) => vars.set('ref_title', Optional('Original title')), + `{{ &ref_title }}`, + `Original title`, + (vars) => {vars.get('ref_title').value = 'New title'}, + `New title`, + (vars) => {vars.get('ref_title').value = 'Third attempt'}, + `Third attempt`, + ], + [ + (vars) => vars.set('ref_bool', Optional(false)), + `Ref bool is: {% if &ref_bool %}{{ ref_bool }}{% else %}FALSE{% /if %}`, + `Ref bool is: FALSE`, + (vars) => {vars.get('ref_bool').value = 'str'}, + `Ref bool is: str`, + (vars) => {vars.get('ref_bool').value = null}, + `Ref bool is: FALSE`, + ], + ]; + + createReactiveTests(tests); + }); + return; describe('None existing method calls', function() { @@ -925,8 +951,19 @@ This should be a converted variable: }); }); +function Optional(value) { + return new Blast.Classes.Develry.Optional(value); +} + +function createReactiveTests(tests) { + return createTests(tests); +} + function createTests(tests) { + const Blast = __Protoblast, + Classes = Blast.Classes; + const CustomList = function CustomList(records) { this.should_not_be_visible = 'nope'; this.records = records; @@ -943,23 +980,57 @@ function createTests(tests) { return this; }; - let my_deck = new __Protoblast.Classes.Deck(); + let my_deck = new Classes.Deck(); my_deck.set('x', 'X'); my_deck.set('y', 'Y'); my_deck.push('Z'); for (let i = 0; i < tests.length; i++) { - let template, + let setup_tasks = [], + extra_tasks = [], + template, result, title, code, test = tests[i]; if (Array.isArray(test)) { - code = tests[i][0]; - title = tests[i][0].replace(/\r\n/g, '\\n').replace(/\n/g, '\\n').replace(/\t/g, '\\t'); - result = tests[i][1]; + + if (test.length === 2) { + code = tests[i][0]; + result = tests[i][1]; + } else { + + let seen_template = false, + seen_initial_result = false; + + for (let entry of test) { + + if (typeof entry == 'function') { + if (!seen_template) { + setup_tasks.push(entry); + continue; + } + } else if (typeof entry == 'string') { + if (!seen_template) { + code = entry; + seen_template = true; + continue; + } + + if (!seen_initial_result) { + result = entry; + seen_initial_result = true; + continue; + } + } + + extra_tasks.push(entry); + } + } + + title = code.replace(/\r\n/g, '\\n').replace(/\n/g, '\\n').replace(/\t/g, '\\t'); } else { title = test.template; template = test.template; @@ -1117,19 +1188,77 @@ function createTests(tests) { // (and lose the iterator property) variables.set('iterable', iterable); - renderer.renderHTML(compiled, variables).done(function done(err, res) { + let setup_pledges = []; + + if (setup_tasks) { + + for (let task of setup_tasks) { + setup_pledges.push(task(variables)); + } + } + + Blast.Bound.Function.series(setup_pledges, (err) => { if (err) { return next(err); } - try { - assertEqualHtml(res, result); - } catch (e) { - return next(e); - } + let is_reactive = setup_pledges?.length > 0; + + renderer.render(compiled, variables).done(async function done(err, block) { + + if (err) { + return next(err); + } + + let elements = block.toElements(); + let res = block.toHTML(); + + if (is_reactive) { + res = res.replace(/\s+data-hid=["'].*?["']/g, ''); + } + + try { + assertEqualHtml(res, result); + } catch (e) { + return next(e); + } + + if (is_reactive) { + + for (let task of extra_tasks) { + + if (typeof task == 'function') { + + try { + await task(variables); + } catch (err) { + return next(err); + } + continue; + } + + if (typeof task == 'string') { + + // Simple race condition hack + await Classes.Pledge.after(5); + + res = block.toHTML(); + res = res.replace(/\s+data-hid=["'].*?["']/g, ''); + res = res.replace(/\s+he-rendered=["'].*?["']/g, ''); + + try { + assertEqualHtml(res, task); + } catch (e) { + return next(e); + } + } + } + } + + next(); + }); - next(); }); }); } diff --git a/test/templates/simple/reactive_print.hwk b/test/templates/simple/reactive_print.hwk new file mode 100644 index 00000000..2eae8670 --- /dev/null +++ b/test/templates/simple/reactive_print.hwk @@ -0,0 +1,3 @@ +
+ {{ &reactive_value }} +
\ No newline at end of file