harttle / liquidjs

A simple, expressive, safe and Shopify compatible template engine in pure JavaScript.
https://liquidjs.com
MIT License
1.52k stars 238 forks source link

unexpected behaviour when using DOM content as template input #504

Closed jalberto closed 2 years ago

jalberto commented 2 years ago
{% for item in (1..3) %}
  <div>
    <p>{{item}}</p>
  </div>
{% endfor %}

Works as expected, but

<table>
  {% for item in (1..3) %}
  <tr>
    <td>{{item}}</td>
  </tr>
  {% endfor %}
</table>

Produces this:

image

more context:

document.addEventListener('DOMContentLoaded', () => {
  const template = document.body.innerHTML
  const result = document.body
  const Liquid = window.liquidjs.Liquid
  const engine = new Liquid({
    extname: '.html',
    cache: true
  });

  grist.ready({requiredAccess: 'read table'});

  grist.onRecord(function(record) {
    const ctx = { data: record };

    engine.parseAndRender(template, ctx)
      .then(function(html) {
        result.innerHTML = html
      });
  });
})

I also tried the template and result divs strategy, but I get same result. This is rendered inside an iframe, maybe is there some option I need to enable to make it work?

harttle commented 2 years ago

Try print html and template before set to DOM by console.log

I tried this and it works: https://liquidjs.com/playground.html#PHRhYmxlPgogIHslIGZvciBpdGVtIGluICgxLi4zKSAlfQogIDx0cj4KICAgIDx0ZD57e2l0ZW19fTwvdGQ+CiAgPC90cj4KICB7JSBlbmRmb3IgJX0KPC90YWJsZT4=,e30=

jalberto commented 2 years ago

@harttle do you mean something like this?

    const html = engine.parseAndRender(template, ctx);
    result.innerHTML = html;
jalberto commented 2 years ago

So I tried this and got an even odder result, I added strictVariables: true to catch possible errors earlier.

    const html = engine.parseAndRenderSync(template, ctx);
    result.innerHTML = html;

If the for is outside, a table tag: image image

what happens inside a table tag: image image

harttle commented 2 years ago

It seems complicated, could you create a minimal runnable snippet to demonstrate the problem? Then I can help.

jalberto commented 2 years ago

I am using it inside Grist (https://www.getgrist.com/), as a custom widget, I cannot reproduce it in the playground, so I guess it can be affected by the iframe or Grist own JS, the custom part is the JS I added in the 1st comment.

I could create the same example in Grist and share it with you

That said, I wonder why it behaves different based on which tag is surrounding the code, is that expected?

jalberto commented 2 years ago

Here is the env with public access: https://docs.getgrist.com/a8X5yeTvzGx2/Invoicing-copy

Here is the custom code: https://github.com/jalberto/grist-widget/tree/master/ja-invoices-es

I really appreciate any clue you may have. Thanks!

harttle commented 2 years ago

That said, I wonder why it behaves different based on which tag is surrounding the code, is that expected?

No, LiquidJS is HTML agnostic. It can be used to generate HTMLs and there's people who use LiquidJS to generate C++ code.

harttle commented 2 years ago

Please change this line:

https://github.com/jalberto/grist-widget/blob/383ab426791aa52f1075f2bbca43266bb4592d01/ja-invoices-es/widget.js#L17

to

    console.log('template:', template, 'ctx', ctx);
    const html = engine.parseAndRenderSync(template, ctx);
    console.log('html', html);

and post the console log here.

jalberto commented 2 years ago
template: 

    <h1>Factura</h1>

    <section class="meta">
      <div class="meta--invoice">
        <table>
          <tbody><tr>
            <td>Número:</td>
            <td>{{data.Number}}</td>
          </tr>
          <tr>
            <td>Emitida:</td>
            <td>{{data.Issued | date: "%d-%m-%Y"}}</td>
          </tr>
          <tr>
            <td>Vencimiento:</td>
            <td>{{data.Due | date: "%d-%m-%Y"}}</td>
          </tr>
        </tbody></table>
      </div>

      <div class="meta--client">
        <ul>
          <li>{{data.References.Client.Name}}</li>
          <li>{{data.References.Client.Tax_ID}}</li>
          <li>{{data.References.Client.Street1}}</li>
          <li>{{data.References.Client.Street2}}</li>
          <li>{{data.References.Client.Zip}} {{data.References.Client.City}}</li>
        </ul>
      </div>

      <div class="meta--provider">
        <ul>
          <li>{{data.Invoicer.Name}}</li>
          <li>{{data.Invoicer.Tax_ID}}</li>
          <!-- <li>{{data.Invoicer.Street1}}</li> -->
          <!-- <li>{{data.Invoicer.Zip}} {{data.References.Client.City}}</li> -->
        </ul>
      </div>
    </section>

    <section class="detaills">

        {% for item in (1..3) %}

        {% endfor %}

      <table>
        <tbody><tr>
          <th>Concepto</th>
          <th>Base unit.</th>
          <th>Total</th>
        </tr><tr>
          <td>{{ item }}</td>
        </tr></tbody></table>
    </section>

    <section class="misc">
      <div class="misc--notes">{{data.Note}}</div>
    </section>
  <!-- Code injected by live-server -->
<script type="text/javascript">
    // <![CDATA[  <-- For SVG support
    if ('WebSocket' in window) {
        (function() {
            function refreshCSS() {
                var sheets = [].slice.call(document.getElementsByTagName("link"));
                var head = document.getElementsByTagName("head")[0];
                for (var i = 0; i < sheets.length; ++i) {
                    var elem = sheets[i];
                    head.removeChild(elem);
                    var rel = elem.rel;
                    if (elem.href && typeof rel != "string" || rel.length == 0 || rel.toLowerCase() == "stylesheet") {
                        var url = elem.href.replace(/(&|\?)_cacheOverride=\d+/, '');
                        elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf());
                    }
                    head.appendChild(elem);
                }
            }
            var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://';
            var address = protocol + window.location.host + window.location.pathname + '/ws';
            var socket = new WebSocket(address);
            socket.onmessage = function(msg) {
                if (msg.data == 'reload') window.location.reload();
                else if (msg.data == 'refreshcss') refreshCSS();
            };
            console.log('Live reload enabled.');
        })();
    }
    // ]]>
</script>

 ctx {data: {…}}
widget.js?ver=1:21 html 

    <h1>Factura</h1>

    <section class="meta">
      <div class="meta--invoice">
        <table>
          <tbody><tr>
            <td>Número:</td>
            <td>JA-001</td>
          </tr>
          <tr>
            <td>Emitida:</td>
            <td>12-05-2022</td>
          </tr>
          <tr>
            <td>Vencimiento:</td>
            <td>12-06-2022</td>
          </tr>
        </tbody></table>
      </div>

      <div class="meta--client">
        <ul>
          <li>foo</li>
          <li>foo</li>
          <li>foo</li>
          <li>foo</li>
          <li>foo foo</li>
        </ul>
      </div>

      <div class="meta--provider">
        <ul>
          <li>bar</li>
          <li>fff</li>
          <!-- <li>bar 9</li> -->
          <!-- <li>333 foo</li> -->
        </ul>
      </div>
    </section>

    <section class="detaills">

      <table>
        <tbody><tr>
          <th>Concepto</th>
          <th>Base unit.</th>
          <th>Total</th>
        </tr><tr>
          <td></td>
        </tr></tbody></table>
    </section>

    <section class="misc">
      <div class="misc--notes"></div>
    </section>
  <!-- Code injected by live-server -->
<script type="text/javascript">
    // <![CDATA[  <-- For SVG support
    if ('WebSocket' in window) {
        (function() {
            function refreshCSS() {
                var sheets = [].slice.call(document.getElementsByTagName("link"));
                var head = document.getElementsByTagName("head")[0];
                for (var i = 0; i < sheets.length; ++i) {
                    var elem = sheets[i];
                    head.removeChild(elem);
                    var rel = elem.rel;
                    if (elem.href && typeof rel != "string" || rel.length == 0 || rel.toLowerCase() == "stylesheet") {
                        var url = elem.href.replace(/(&|\?)_cacheOverride=\d+/, '');
                        elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf());
                    }
                    head.appendChild(elem);
                }
            }
            var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://';
            var address = protocol + window.location.host + window.location.pathname + '/ws';
            var socket = new WebSocket(address);
            socket.onmessage = function(msg) {
                if (msg.data == 'reload') window.location.reload();
                else if (msg.data == 'refreshcss') refreshCSS();
            };
            console.log('Live reload enabled.');
        })();
    }
    // ]]>
</script>
harttle commented 2 years ago

Note this part:

<section class="detaills">

        {% for item in (1..3) %}

        {% endfor %}

      <table>
        <tbody><tr>
          <th>Concepto</th>
          <th>Base unit.</th>
          <th>Total</th>
        </tr><tr>
          <td>{{ item }}</td>
        </tr></tbody></table>
    </section>

{{ item}} is outside of {% for %}. I guess that's because when your HTML is rendered into DOM, it's normalized and additional <tbody> element is added. So the template you get from DOM is no longer the one you set. Try add tbody in your source code.

jalberto commented 2 years ago

Thanks! So yes, now the output is in the correct place, but the item still empty when called within table tags

jalberto commented 2 years ago

It seems the problem is the same :(

template: 

    <h1>Factura</h1>

    <section class="meta">
      <div class="meta--invoice">
        <table>
          <tbody><tr>
            <td>Número:</td>
            <td>{{data.Number}}</td>
          </tr>
          <tr>
            <td>Emitida:</td>
            <td>{{data.Issued | date: "%d-%m-%Y"}}</td>
          </tr>
          <tr>
            <td>Vencimiento:</td>
            <td>{{data.Due | date: "%d-%m-%Y"}}</td>
          </tr>
        </tbody></table>
      </div>

      <div class="meta--client">
        <ul>
          <li>{{data.References.Client.Name}}</li>
          <li>{{data.References.Client.Tax_ID}}</li>
          <li>{{data.References.Client.Street1}}</li>
          <li>{{data.References.Client.Street2}}</li>
          <li>{{data.References.Client.Zip}} {{data.References.Client.City}}</li>
        </ul>
      </div>

      <div class="meta--provider">
        <ul>
          <li>{{data.Invoicer.Name}}</li>
          <li>{{data.Invoicer.Tax_ID}}</li>
          <!-- <li>{{data.Invoicer.Street1}}</li> -->
          <!-- <li>{{data.Invoicer.Zip}} {{data.References.Client.City}}</li> -->
        </ul>
      </div>
    </section>

    <section class="detaills">

        {% for item in (1..3) %}

        {% endfor %}
        <table>
        <tbody>
        <tr>
          <th>Concepto</th>
          <th>Base unit.</th>
          <th>Total</th>
        </tr><tr>
          <td>{{ item }}</td>
        </tr></tbody>
      </table>
    </section>

    <section class="misc">
      <div class="misc--notes">{{data.Note}}</div>
    </section>
  <!-- Code injected by live-server -->
<script type="text/javascript">
    // <![CDATA[  <-- For SVG support
    if ('WebSocket' in window) {
        (function() {
            function refreshCSS() {
                var sheets = [].slice.call(document.getElementsByTagName("link"));
                var head = document.getElementsByTagName("head")[0];
                for (var i = 0; i < sheets.length; ++i) {
                    var elem = sheets[i];
                    head.removeChild(elem);
                    var rel = elem.rel;
                    if (elem.href && typeof rel != "string" || rel.length == 0 || rel.toLowerCase() == "stylesheet") {
                        var url = elem.href.replace(/(&|\?)_cacheOverride=\d+/, '');
                        elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf());
                    }
                    head.appendChild(elem);
                }
            }
            var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://';
            var address = protocol + window.location.host + window.location.pathname + '/ws';
            var socket = new WebSocket(address);
            socket.onmessage = function(msg) {
                if (msg.data == 'reload') window.location.reload();
                else if (msg.data == 'refreshcss') refreshCSS();
            };
            console.log('Live reload enabled.');
        })();
    }
    // ]]>
</script>

 ctx {data: {…}}
widget.js?ver=1:21 html 

    <h1>Factura</h1>

    <section class="meta">
      <div class="meta--invoice">
        <table>
          <tbody><tr>
            <td>Número:</td>
            <td>JA-001</td>
          </tr>
          <tr>
            <td>Emitida:</td>
            <td>12-05-2022</td>
          </tr>
          <tr>
            <td>Vencimiento:</td>
            <td>12-06-2022</td>
          </tr>
        </tbody></table>
      </div>

      <div class="meta--client">
        <ul>
          <li>foo</li>
          <li>foo</li>
          <li>foo</li>
          <li>foo</li>
          <li>foo foo</li>
        </ul>
      </div>

      <div class="meta--provider">
        <ul>
          <li>bar</li>
          <li>fff</li>
          <!-- <li>bar 9</li> -->
          <!-- <li>333 foo</li> -->
        </ul>
      </div>
    </section>

    <section class="detaills">

        <table>
        <tbody>
        <tr>
          <th>Concepto</th>
          <th>Base unit.</th>
          <th>Total</th>
        </tr><tr>
          <td></td>
        </tr></tbody>
      </table>
    </section>

    <section class="misc">
      <div class="misc--notes"></div>
    </section>
  <!-- Code injected by live-server -->
<script type="text/javascript">
    // <![CDATA[  <-- For SVG support
    if ('WebSocket' in window) {
        (function() {
            function refreshCSS() {
                var sheets = [].slice.call(document.getElementsByTagName("link"));
                var head = document.getElementsByTagName("head")[0];
                for (var i = 0; i < sheets.length; ++i) {
                    var elem = sheets[i];
                    head.removeChild(elem);
                    var rel = elem.rel;
                    if (elem.href && typeof rel != "string" || rel.length == 0 || rel.toLowerCase() == "stylesheet") {
                        var url = elem.href.replace(/(&|\?)_cacheOverride=\d+/, '');
                        elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf());
                    }
                    head.appendChild(elem);
                }
            }
            var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://';
            var address = protocol + window.location.host + window.location.pathname + '/ws';
            var socket = new WebSocket(address);
            socket.onmessage = function(msg) {
                if (msg.data == 'reload') window.location.reload();
                else if (msg.data == 'refreshcss') refreshCSS();
            };
            console.log('Live reload enabled.');
        })();
    }
    // ]]>
</script>
jalberto commented 2 years ago

The only way to make it work as expected is to wrap all the template in a scrtip tag (it does not work with any other tag)