diff --git a/CHANGELOG.md b/CHANGELOG.md index 2da697f..3d27245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * Prepare text nodes during compiling * Clean up custom element `renderCustomTemplate` function * Add initial reactive variable support +* Allow assigning an element to a reference using `:ref` syntax ## 2.3.19 (2024-04-13) diff --git a/lib/core/renderer.js b/lib/core/renderer.js index 26aa9d1..d90c88b 100644 --- a/lib/core/renderer.js +++ b/lib/core/renderer.js @@ -2321,6 +2321,16 @@ Renderer.setMethod(function applyElementOptions(element, options, for_sync_rende } } + if (options.hooks?.length) { + for (let hook of options.hooks) { + if (hook.name === 'ref') { + if (hook.value) { + hook.value.value = element; + } + } + } + } + // 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) { @@ -2551,10 +2561,12 @@ Renderer.setMethod(function startExpression(name, options, vars, fnc) { * * @author Jelle De Loecker * @since 1.3.3 - * @version 2.0.0 + * @version 2.4.0 */ -Renderer.setMethod(function parseExpression(tokens, vars) { - return Hawkejs.Expression.Expression.parseExpression(this, tokens, vars); +Renderer.setMethod(function parseExpression(tokens, vars, unwrap_optionals = true) { + let expression = new Hawkejs.Expression.Expression(this); + expression.unwrap_optionals = unwrap_optionals; + return expression.parseExpression(tokens, vars); }); /** @@ -2562,10 +2574,11 @@ Renderer.setMethod(function parseExpression(tokens, vars) { * * @author Jelle De Loecker * @since 1.3.3 - * @version 2.0.0 + * @version 2.4.0 */ -Renderer.setMethod(function parseExpressionAsArguments(tokens, vars) { +Renderer.setMethod(function parseExpressionAsArguments(tokens, vars, unwrap_optionals = true) { let expression = new Hawkejs.Expression.Expression(this); + expression.unwrap_optionals = unwrap_optionals; return expression.getTokenValuesArray(tokens, vars); }); diff --git a/lib/expression/expression.js b/lib/expression/expression.js index 334089e..4d1605c 100644 --- a/lib/expression/expression.js +++ b/lib/expression/expression.js @@ -12,6 +12,9 @@ const Expression = Fn.inherits(null, 'Hawkejs.Expression', function Expression(v // The renderer instance this.view = view; + // Should optionals be unwrapped? + this.unwrap_optionals = true; + this.options = null; this.vars = null; this.fnc = null; @@ -430,6 +433,15 @@ Expression.setMethod(function _getValueByPath(path, variables, main_variables) { break; } + // Always unwrap optionals that are not the last part of a path + if (current instanceof Classes.Develry.Optional) { + current = current.value; + } + + if (!current) { + break; + } + if (current instanceof Classes.Hawkejs.Variables) { current = current.get(piece); } else { @@ -766,7 +778,7 @@ const toLowerCase = (input) => input ? input.toLowerCase() : input; * * @author Jelle De Loecker * @since 1.2.9 - * @version 1.4.0 + * @version 2.4.0 * * @param {Object} token * @param {Object} vars An object of variables @@ -796,7 +808,7 @@ Expression.setMethod(function getTokenValue(token, vars) { } // Unwrap optionals - if (result && result instanceof Classes.Develry.Optional) { + if (result && this.unwrap_optionals && result instanceof Classes.Develry.Optional) { result = result.value; } @@ -1051,7 +1063,7 @@ Expression.setMethod(function callPathWithArgs(path, args, vars) { * * @author Jelle De Loecker * @since 1.2.9 - * @version 2.2.0 + * @version 2.4.0 * * @param {Object} token * @param {Object} vars An object of variables diff --git a/lib/parser/base_parser.js b/lib/parser/base_parser.js index f158d7d..ba17803 100644 --- a/lib/parser/base_parser.js +++ b/lib/parser/base_parser.js @@ -105,14 +105,15 @@ Parser.setStatic(function rawString(text) { * * @author Jelle De Loecker * @since 2.2.0 - * @version 2.2.0 + * @version 2.4.0 * * @param {Object} options * @param {boolean} as_args + * @param {boolean} unwrap_optionals * * @return {Object} */ -Parser.setStatic(function wrapExpression(options, as_args) { +Parser.setStatic(function wrapExpression(options, as_args, unwrap_optionals = true) { options = Bound.Array.cast(options); @@ -127,10 +128,16 @@ Parser.setStatic(function wrapExpression(options, as_args) { code += 'AsArguments'; } - return { + let result = { $wrap: '__render.' + code, $args: [options, Parser.rawString('vars')], }; + + if (!unwrap_optionals) { + result.$args.push(false); + } + + return result; }); /** diff --git a/lib/parser/builder.js b/lib/parser/builder.js index 08a384e..b971c74 100644 --- a/lib/parser/builder.js +++ b/lib/parser/builder.js @@ -803,7 +803,7 @@ Builder.setMethod(function _compileEntry(subroutine, entry) { * * @author Jelle De Loecker * @since 2.2.0 - * @version 2.2.0 + * @version 2.4.0 * * @param {Object} entry * @@ -813,7 +813,7 @@ Builder.setMethod(function _compileElementOptions(entry) { let has_attributes = !Obj.isEmpty(entry.attributes); - if (!has_attributes && !entry.directives && !entry.properties && !entry.variables && !entry.codes && !entry.body) { + if (!has_attributes && !entry.directives && !entry.properties && !entry.variables && !entry.codes && !entry.body && !entry.hooks) { return; } @@ -825,6 +825,7 @@ Builder.setMethod(function _compileElementOptions(entry) { directives : null, properties : null, variables : null, + hooks : null, codes : null, body : null, }; @@ -852,6 +853,10 @@ Builder.setMethod(function _compileElementOptions(entry) { data.variables = this._compileDirectiveValues(entry.variables); } + if (entry.hooks) { + data.hooks = this._compileDirectiveValues(entry.hooks, false); + } + if (entry.codes) { data.codes = this._compileCodes(entry.codes); } @@ -868,16 +873,17 @@ Builder.setMethod(function _compileElementOptions(entry) { * * @author Jelle De Loecker * @since 2.2.0 - * @version 2.2.0 + * @version 2.4.0 * * @param {Object} entry + * @param {boolean} unwrap_optionals * * @return {Builder} */ -Builder.setMethod(function _compileRawExpression(entry) { +Builder.setMethod(function _compileRawExpression(entry, unwrap_optionals = true) { let tokens = new Hawkejs.Parser.Expressions(entry.value); let expression = tokens.getExpression(); - return Hawkejs.Parser.Parser.wrapExpression(expression); + return Hawkejs.Parser.Parser.wrapExpression(expression, null, unwrap_optionals); }); /** @@ -887,9 +893,12 @@ Builder.setMethod(function _compileRawExpression(entry) { * @since 2.0.0 * @version 2.2.0 * + * @param {Object} val + * @param {boolean} unwrap_optionals + * * @return {Object} */ -Builder.setMethod(function _compileAttributeValue(val) { +Builder.setMethod(function _compileAttributeValue(val, unwrap_optionals = true) { let result = ''; @@ -918,7 +927,7 @@ Builder.setMethod(function _compileAttributeValue(val) { entry = val.value[i]; if (entry.type == 'expressions') { - result.push(this._compileRawExpression(entry)); + result.push(this._compileRawExpression(entry, unwrap_optionals)); } else if (entry.type == 'code') { let code; @@ -947,7 +956,7 @@ Builder.setMethod(function _compileAttributeValue(val) { val.value = val.value.slice(2, -2); } - result = this._compileRawExpression(val); + result = this._compileRawExpression(val, unwrap_optionals); } else { result = R(val.value); } @@ -961,13 +970,14 @@ Builder.setMethod(function _compileAttributeValue(val) { * * @author Jelle De Loecker * @since 2.0.0 - * @version 2.0.0 + * @version 2.4.0 * - * @param {Array} values + * @param {Object[]} values + * @param {boolean} unwrap_optionals * * @return {String} */ -Builder.setMethod(function _compileDirectiveValues(values) { +Builder.setMethod(function _compileDirectiveValues(values, unwrap_optionals = true) { let result = [], value, @@ -981,7 +991,7 @@ Builder.setMethod(function _compileDirectiveValues(values) { name : value.name, context : value.context || null, method : value.method || null, - value : this._compileAttributeValue(value.value) + value : this._compileAttributeValue(value.value, unwrap_optionals), }; result.push(entry); diff --git a/lib/parser/directives_parser.js b/lib/parser/directives_parser.js index f080b9b..f2ee744 100644 --- a/lib/parser/directives_parser.js +++ b/lib/parser/directives_parser.js @@ -380,7 +380,7 @@ Dparser.setMethod(function parseCurrent() { * * @author Jelle De Loecker * @since 2.0.0 - * @version 2.1.5 + * @version 2.4.0 * * @param {Object} result */ @@ -441,7 +441,7 @@ Dparser.setMethod(function parseAttributes(result) { char = next.value[0]; name = next.value; - if (char === '!' || char === '#' || char === '+') { + if (char === '!' || char === '#' || char === '+' || char === ':') { name = next.value.slice(1); } @@ -493,6 +493,13 @@ Dparser.setMethod(function parseAttributes(result) { } result.variables.push(entry); + } else if (char === ':') { + + if (!result.hooks) { + result.hooks = []; + } + + result.hooks.push(entry); } else { result.attributes[entry.name] = entry; } diff --git a/test/10-expressions.js b/test/10-expressions.js index e9126e0..ab96a67 100644 --- a/test/10-expressions.js +++ b/test/10-expressions.js @@ -934,6 +934,25 @@ This should be a converted variable: (vars) => {vars.get('ref_bool').value = null}, `Ref bool is: FALSE`, ], + [ + (vars) => vars.set('ref_el', Optional()), + `INNER
{{ &ref_el.textContent }}
`, + `INNER
INNER
`, + (vars) => { + // Get the reference again + let ref_el = vars.get('ref_el'); + + // Get the element it is referring + let el = ref_el.value; + + // Change the content of the element directly + el.textContent = 'CHANGED'; + + // Trigger a change + ref_el.value = el; + }, + `CHANGED
CHANGED
`, + ] ]; createReactiveTests(tests);