Frontmatter
| id | 7137 |
| title | Template Syntax Specification |
| state | Closed |
| labels | enhancement |
| assignees | tobiu |
| createdAt | Jul 31, 2025, 9:32 AM |
| updatedAt | Jul 31, 2025, 11:24 AM |
| githubUrl | https://github.com/neomjs/neo/issues/7137 |
| author | tobiu |
| commentsCount | 1 |
| parentIssue | 7130 |
| subIssues | [] |
| subIssuesCompleted | 0 |
| subIssuesTotal | 0 |
| blockedBy | [] |
| blocking | [] |
| closedAt | Jul 31, 2025, 11:24 AM |
Template Syntax Specification

╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ > my idea (you are very welcome to challenge it): if a template encounters a module like Button => we can check the prototype for the │ │ neo className (which is a strict requirement for creating neo classes => always there). then we can easily use the className and │ │ map it to the global namespace, which contains the module. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ That's a very insightful approach, and you're absolutely right to challenge the boilerplate of the factory function. The goal should always be to make the developer experience as clean as possible. Your idea of leveraging the className from the prototype is a perfect example of using the framework's built-in strengths.
Let's walk through that idea, because it's very close to an extremely elegant solution.
You are correct that if the processor has access to the Button constructor, it can easily find its className. The central challenge
is: How does the processor get the Button constructor in the first place?
This is the "missing link" in the process. When the html tag function runs, it receives two separate things:
- An array of static strings (e.g., [''])
- An array of dynamic values (e.g., [] in this case)
The processor gets these two arrays. It sees the string "Button", but it has no way to connect that string back to the Button variable that was imported in the component file. The processor's scope is isolated; it can't see the component's imports.
The Hybrid Solution: Combining Our Ideas
However, we can solve this by making one small, powerful change to the template syntax, which completely eliminates the need for a factory function or a templateScope config.
What if the syntax required passing the component constructor itself as the tag?
1 // Developer still just imports the standard html tag
2 import { html } from '../../../src/functional/util/html.mjs';
3 import Button from '../../../src/button/Base.mjs';
4
5 // ...
6 createVdom() {
7 const buttonText = 'Click Me';
8
9 // The component class IS the tag
10 return html 11 <${Button} text="${buttonText}" /> 12 ;
13 }
Let's trace how this works:
- The html function now receives:
- strings: ['<', ' text="', '" />']
- values: [Button, buttonText]
- The HtmlTemplateProcessor gets these arrays.
- When it parses the template, it finds a placeholder (DYNAMIC_VALUE_0) where the tag name should be.
- It looks at values[0] and gets the actual Button class constructor.
- Now your idea comes into play. The processor has the constructor. It can inspect its prototype to get the className for validation or debugging, and it knows to create a VDOM node with { module: Button, ... }.
This approach is superior because:
- Zero Boilerplate: The developer only ever needs to import { html }. No factories, no extra configs.
- Extremely Explicit: The template itself makes it crystal clear which variable is being used for the component tag.
- Leverages the Framework: It uses the existing module system and setupClass implicitly.
- Future-Proof: It naturally supports your follow-up thought. If a developer wrote <${Neo.button.Base}>, it would work just as well, because the value passed would still be the constructor.
This feels like the most "neo.mjs" way to do it. It's clean, powerful, and relies on web standards (template literals) and the framework's own robust class system.
How does this hybrid approach sound to you?
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ > i like the idea. follow up: a dev could write <Neo.button.Base> (inferior to the imported module). if we do not find the tag │ │ matching module inside the lexical scope, we can check Neo.ns('Neo.button.Base'). if this returns the module, we are also safe. if │ │ neither the lexical scope nor Neo.ns() contains the module => throw Error. we do not want to lazy-load the module, since this would │ │ convert the parsing to async for the first encounter. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ That's an excellent and complete strategy. You've perfectly defined the resolution logic, and I am in full agreement. It prioritizes the best practice while providing a sensible fallback, all while respecting the critical synchronous nature of the dev-mode rendering path.
Let's formalize this two-step resolution process:
- Lexical Scope First (The
<${Button}>case): The processor will check if a tag is an interpolated value from the values array. If it is, it will use that constructor directly. This is the primary, recommended, and most robust method. - Global Namespace Second (The
<Neo.button.Base>case): If the tag is a literal string (not an interpolated value), the processor will check if it's a valid, existing namespace using Neo.ns(). If it resolves to a class constructor, it will use it. - Error: If neither of the above yields a valid constructor, the processor will throw an error, informing the developer that the component tag could not be resolved.
This approach is the best of all worlds. It's clean for the developer, requires no extra configuration, and leverages the full power of the framework's namespace system as a fallback.
I will now update the epic to reflect this finalized strategy. Then, we can proceed with updating the HtmlTemplateProcessor and the unit test to implement and verify this logic.
Description: Define a clear and comprehensive specification for the template syntax. This document will serve as the blueprint for the parser implementation and as the primary reference for developers using this feature.
Implementation Details:
docs/templates/Syntax.md.<MyComponent>, lowercase for HTML:<div>).classtocls,styletostyle).style="${{color: 'red'}}",items="${['a', 'b']}").renderer="${this.myRenderer}").n-ifattribute:<div n-if="${isVisble}">...</div>).n-forattribute:<li n-for="${item} of ${items}">${item.name}</li>).columns="${gridColumns}").onClick="...") are not supported. The framework's global, delegated event system (domListenersconfig oruseEvent()hook) remains the sole, recommended approach for handling DOM events. This maintains performance and architectural consistency.