jadelet
jadelet copied to clipboard
WIP: Add support for a 'Fragment' tag
This PR adds support for a fragments.
Currently Jadelet requires a single root element (e.g., HTML element) in the template.
However, there are cases where HTML root element can be redundant, or might produce invalid HTML.
For example, there might be a case where we would like to output the content in the existing <ul>
element.
With this PR, we would be able to use a (reserved) word "Fragment" or a symbol <>
in the template string.
var template = """
Fragment
li 1
li 2
li 3
"""
var template = """
<>
li 1
li 2
li 3
"""
One challenge with using fragments in that they behave a little strangely when being added to the DOM.
f = document.createDocumentFragment()
f.appendChild(document.createElement('p'))
f.childElementCount // => 1
document.body.appendChild(f)
f.childElementCount // => 0
This means we no longer have a reference to the containing element so dynamic content won't work inside document fragments.
A related feature that I've been thinking about adding is a way to register specific tags to arbitrary handlers. That way someone could register document fragments or other element constructors to take more control over customized behavior.
Something like:
ul
Item(@click @text)
span Neat!
Jadelet.register
Item: ({click, text, children}) ->
el = document.createElement 'li'
el.onclick = click;
el.append(text, children...)
return el
It needs a bit more thought, especially around observable properties and content but I think there might be something to it.
This means we no longer have a reference to the containing element so dynamic content won't work inside document fragments.
Can you show me an example of this?
Below is the simple example I've tried by using observable
.
<!-- html -->
<div id="app"></div>
<ul id="list"></ul>
o = Jadelet.Observable
t = Jadelet.exec
app = document.getElementById("app")
lst = document.getElementById("list")
tpl1 = t(`ul
li @a
li @b
li @c`)
tpl2 = t(`Fragment
li @a
li @b
li @c`)
x = o(1)
obj = {
a: o(() => x() + 1),
b: o(() => x() + 2),
c: o(() => x() + 3)
}
app.appendChild(tpl1(obj))
lst.appendChild(tpl2(obj))
/*
html result:
<div id="app">
<ul>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
</div>
<ul id="list">
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
*/
x(5)
/*
html result:
<div id="app">
<ul>
<li>6</li>
<li>7</li>
<li>8</li>
</ul>
</div>
<ul id="list">
<li>6</li>
<li>7</li>
<li>8</li>
</ul>
*/
Try this one:
tpl = t(`Fragment
@a
li @b
li @c`)
I've got the following result with the same HTML.
tpl1 = t(`li @d`)
tpl2 = t(`Fragment
@a
li @b`)
x = o(1)
objA = {
d: o(() => x() + 1)
}
obj = {
a: o(() => tpl1(objA)),
b: o(() => x() + 2)
}
lst.appendChild(tpl2(obj))
/*
html result:
<div id="app"></div>
<ul id="list">
<li>2</li>
<li>3</li>
</ul>
*/
x(5)
/*
html result:
<div id="app"></div>
<ul id="list">
<li>6</li>
<li>7</li>
</ul>
*/
So far, it looks like it's working.
But once I applied the template to the <div>
element, funny things happened.
tpl3 = t(`ul
@a
li @b`)
app.appendChild(tpl3(obj))
/*
html result:
<div id="app">
<ul>
<li>6</li>
<li>7</li>
</ul>
</div>
<ul id="list">
<li>7</li>
</ul>
*/
So, the @a
from the template disappeared.
I tried to reverse order, and I first appended the template to the <div>
element and then to <ul>
, but I got the same result.
It looks like when both templates use the same subtemplate, it "disappears" from the first element.
I changed the HTML a bit to check if this is the issue with the DocumentFragment
, but the outcome was the same.
<div id="app"></div>
<div id="app2"></div>
app.appendChild(tpl3(obj))
/*
html result
<div id="app">
<ul>
<li>2</li>
<li>3</li>
</ul>
</div>
<div id="app2"></div>
*/
app2.appendChild(tpl3(obj))
/*
html result
<div id="app">
<ul>
<li>3</li>
</ul>
</div>
<div id="app2">
<ul>
<li>2</li>
<li>3</li>
</ul>
</div>
*/
I thought this might be connected to the observable functions, so I tried the code without them, and the result is the same as above.
objA = { d: x() + 1 }
obj = { a: tpl1(objA), b: x() + 2 }
Even without the observables, the behavior stays the same.
objA = { d: 1 }
obj = { a: tpl1(objA), b: 2 }
Did I miss something in my code?
I'm still not sure about the disappearing subtemplate, but here is the working example.
o = Jadelet.Observable
t = Jadelet.exec
app = document.getElementById("app")
lst = document.getElementById("list")
x = o(1)
function objA() {
var tpl = t(`li @d`),
obj = {d: o(() => x() + 1)}
return tpl(obj)
}
function obj() {
return {
a: objA(),
b: o(() => x() + 2)
}
}
function elm1() {
var tpl = t(`ul
@a
li @b`)
return tpl(obj())
}
function elm2() {
var tpl = t(`Fragment
@a
li @b`)
return tpl(obj())
}
function test() {
app.appendChild(elm1())
lst.appendChild(elm2())
}
test()
/*
<div id="app">
<ul>
<li>2</li>
<li>3</li>
</ul>
</div>
<ul id="list">
<li>2</li>
<li>3</li>
</ul>
*/
x(5)
/*
<div id="app">
<ul>
<li>6</li>
<li>7</li>
</ul>
</div>
<ul id="list">
<li>6</li>
<li>7</li>
</ul>
*/
I've been thinking...
There may be a way to do this by instead of tracking the parent node, to track the first child in a fragment and the number of sibling nodes added. This assumes that other nodes aren't inserted in between nodes controlled by Jadelet. In this case wrapping the entire template in a document fragment by default would allow for root level siblings and should still be able to handle reactively inserting/removing.
One additional edge case would be to track the parent/previous sibling where the template is appended to the DOM to handle the case where zero nodes are added, or to insert a comment node as a placeholder.