Compilation Directives
Directives are core components of qingkuai, represented as special attributes prefixed with #
that instruct the qingkuai compiler how to generate corresponding JavaScript code. qingkuai provides a rich set of built-in directives covering multiple aspects including flow control, rendering control, and asynchronous processing:
Rendering control directives: target, html, show - control content insertion position and display
Flow control directives: for, if, el-if, else - control structural rendering logic
Async processing directives: await, then, catch - respond to asynchronous data changes
Additionally, there's a slot
directive for receiving component slot parameters, which we'll introduce after explaining the component concept.
Conditional Rendering
In qingkuai, we implement conditional rendering logic by combining if, elif and else directives, similar to Javascript's if, else if and else keywords. Consider this scenario: showing login prompts when users aren't logged in, and displaying user information after login - a very common frontend requirement. We can easily implement this logic using qingkuai's conditional rendering:
<qk:spread #if={userInfo}>
<p>View after logging in.</p>
<button
class="login-btn"
@click={handleLogin}
>
Login
</button>
</qk:spread>
<p #else>Hello {userInfo.name}!</p>
qk:spread
tag serves as a virtual mounting point for directives. It won't be rendered to the page - you can understand that directives on this element will be applied to all child nodes. This design avoids introducing unnecessary meaningless elements, and also makes it possible to add directives to text nodes. We'll explain more usage details in Built-in Elements.
We can also insert elif
directives between if
and else
as branch nodes:
<p #if={language === "qk"}>QingKuai</p>
<p #elif={language === "js"}>Javascrip</p>
<p #elif={language === "ts"}>Typescrip</p>
<p #else>Language is not Qingkuai, Javascript, or Typescript.</p>
Alternatively, we can use the show
directive to control element visibility. Unlike the three directives mentioned above, show doesn't unmount elements from the page - it simply controls whether to add the display: none;
style rule. Therefore, for elements requiring frequent visibility toggling, the show directive is preferable due to its lower overhead:
<div #show={visiable}>
<!-- some contents> -->
</div>
List Rendering
List rendering is very convenient in qingkuai. Here's a basic usage example, commonly used during development testing to quickly create list rendering:
<p #for={3}>Paragraph in list rendering.<p>
This will render three consecutive p tags:
<p>Paragraph in list rendering.</p>
<p>Paragraph in list rendering.</p>
<p>Paragraph in list rendering.</p>
The for
directive's value can be not just a number, but also arrays, objects, strings, Sets (Set), Maps (Map), or expressions evaluating to these types. Additionally, you can use for...of
-like syntax to name each iteration's item and index.
<p #for={item, index of [1, 2 , 3]}>{index}: {item}</p>
The rendered result will be:
<p>0: 1</p>
<p>1: 2</p>
<p>2: 3</p>
List rendering with Map:
<lang-js>
const languages = new Map([
["qk", "Qingkuai"],
["js", "Javascript"],
["ts", "Typescript"]
])
</lang-js>
<p #for={item, index of languages}>{index}: {item}</p>
The rendered result will be:
<p>qk: QingKuai</p>
<p>js: Javascript</p>
<p>ts: Typescript</p>
When naming for directive iteration items and indexes, you can also use destructuring syntax at the item or index identifier name:
<lang-js>
const languageInfos = {
qk: {
age: 1,
name: "QingKuai"
},
js: {
age: 30,
name: "Javascript"
},
ts: {
age: 13,
name: "Typescript"
}
}
</lang-js>
<p #for={{ name, age }, extension of languageInfos}>
{name}: file extension is {extension}, released in {2025 - age}.
</p>
The rendered result will be:
<p>QingKuai: file extension is qk, released in 2024.</p>
<p>Javascript: file extension is js, released in 1995.</p>
<p>Typescript: file extension is ts, released in 2012.</p>
If you've used vue, you might wonder why the for
directive uses of
rather than in
as the iteration keyword. This is because the in keyword can appear in Javascript expressions while of cannot. For example, if we used in, cases like this would be hard to handle:
<p #for={prop in obj ? 3 : 2}>...</p>
key Directive
-
When using the
for
directive to create list rendering, if the directive's dependent reactive variable changes, the list updates with this logic:If the new list is longer, new elements are created and appended; if shorter, extra elements are removed from the end
During qingkuai's scheduled updates, each list element's attributes and textContent are updated
-
This causes an issue: if nodes have their own state (typically form elements), the state may become disordered since list changes don't always just append/remove at the end. The
#key
directive solves this by assigning each element a unique key for identification. The update logic then becomes:Check if keys exist in the new list, and position their corresponding elements correctly
During updates, each element's attributes and textContent are updated
Therefore, when list-rendered elements have state, it's recommended to add the key directive to elements using the for directive:
<form>
<input
#for={user of users}
#key={user.id}
!value={user.name}
placeholder="user name"
/>
</form>
"" + [Interpolation expression]
, and checks for duplicate key values in the list - duplicates will throw runtime errors! Each item's key directive value should be unique within a single list.
Async Processing
In some scenarios, you may need to asynchronously wait for a state in embedded scripts before rendering. qingkuai's async processing directives make this easy. The await directive accepts a Promise - after resolution, then and catch directives can render different content for success/failure cases:
<p #await={pms}>waiting...</p>
<p #then>pms is resolved.</p>
<p #catch>pms is rejected.</p>
To access Promise resolution/rejection values, simply set the then/catch directive value to a Javascript identifier:
<p #await={pms}>waiting...</p>
<p #then={res}>pms is resolved and received {res}.</p>
<p #catch={err}>pms is rejected and received {err}.</p>
then/catch directive values also support destructuring syntax:
<p #await={pms}>waiting...</p>
<p
#then={
{
id: userId,
name: userName
}
}
>
pms is resolved and user id is {userId}, user name is {userName}.
</p>
<p #catch={{msg, code}}>pms is rejected and the error code is {code}, msg: {msg}.</p>
If no rendering is needed during waiting, just write await with then/catch directives on the same tag:
<p
#await={pms}
#then={res}
>
pms is resolved with: {res}
</p>
html Directive
Sometimes we need to render text as HTML fragments, while regular interpolation only modifies textContent and escapes HTML. The html directive fulfills this need:
<div class="dynamic-html-content" #html>{htmlStr}</div>
While the above code works, the outer element isn't always necessary. To avoid meaningless elements, use qk:spread
as a virtual mounting point:
<qk:spread #html>{htmlStr}</qk:spread>
We can also pass html directive a value specifying which tags to keep escaped, preventing xss attacks when handling untrusted HTML fragments. The html directive value type is:
type HTMLDirectiveValueType = Partial<{
// List of tags that need to be preserved with escaping
escapeTags: string[]
// Whether to preserve escaping for <style> tags
escapeStyle: boolean
// Whether to preserve escaping for <script> tags
escapeScript: boolean
}>
We recommend this pattern for handling semi-trusted content:
<lang-js>
const htmlDireciveConf = {
escapeStyle: true,
escapeScript: true,
escapeTags: ["link", "iframe", "form"]
}
</lang-js>
<p #html={htmlDireciveConf}>{htmlStr}</p>
target Directive
Some scenarios require manually controlling an element's parent, like full-screen modals. The target directive handles this easily, accepting either a CSS selector string or HTMLElement. Both examples below mount the div to the body:
<div
class="page-modal"
#target={"body"}
></div>
<div
class="page-modal"
#target={document.body}
></div>