STRd6 / jadelet

Pure and simple clientside templates
https://jadelet.com
MIT License
378 stars 11 forks source link

WIP: Add support for a 'Fragment' tag #38

Open zdenko opened 3 years ago

zdenko commented 3 years ago

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
"""
STRd6 commented 3 years ago

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.

zdenko commented 3 years ago

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>
*/
STRd6 commented 3 years ago

Try this one:

tpl = t(`Fragment
  @a
  li @b
  li @c`)
zdenko commented 3 years ago

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?

zdenko commented 3 years ago

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>
*/
STRd6 commented 2 years ago

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.