alonrbar/easy-template-x

Nested conditions in a table

Closed this issue · 5 comments

image

In the above image you can see the issue. If the condition in the red square (cond8) is set to false, the entire cell is shown empty, even though cond7 is set to true. If both are set to true, everything shows up.

Unfortunately this is a know issues regarding nested conditions that happens because of the way loops inside tables works (conditions are implemented on top of the same mechanism). It's the same root cause as issue #50 (you can read a bit longer explanation there) and will be fixed when #50 is fixed (can't commit on a timeline for that unfortunately).

@alonrbar I need to solve the same issue and want to dive deeper into the LoopPlugin implementation. Could you, please, explain why conditions are implemented along with loops? Are there any technical restrictions? What if we split this functionality into two different plugins with two different syntaxes?

It seems there is no easy way to extend and override TemplateHandler, TemplateCompiler and all other related classes for a few reasons:

  • they hide implementations in private methods;
  • loops and conditions are united not only in LoopPlugin but also at compiler level via containerContentType. Detecting tag content type relies on tag disposition (but not on any custom prefixes) so, I guess, it will be impossible to split them without rewritting/extending TagParser.

Not sure which thread I should post my research in because a few seem to be related to each other, but let me leave the information here as I've started to struggle with nested conditions in a table.

First, I had no luck with modifying TemplateHandler, TemplateCompiler, etc as I mentioned in my previous comment. There were a lot of incompatibilities with more strict ESLint setup in my project so I left this idea relatively soon.

Fortunately, I've established that LoopPlugin already distinguishes loops and conditions so there is no need (probably) to support different syntaxes for them. I copy-pasted contents of LoopPlugin and started to play with them.

The first problem I met: it was impossible to use two conditions in the same table cell like this (don't be scared by the syntax with : and ~ prefixes, it's my custom scope data resolver...see below):

image

I got the following error:

/node_modules/easy-template-x/dist/cjs/easy-template-x.js:605
if (!referenceNode.parentNode) throw new Error('${"referenceNode"}' has no parent);

which is similar to #85.

Digging into the sources I was unable to figure out why this logic is required to update data path when dealing with conditions. But I believe that it could be potentially one of the reasons of the error mentioned above. So I decided to separate loops and conditions compilation. Loop compilation is left as is in general: data path is updated before and after each compilation step. In case of conditions compilation data path is left untouched. Not sure how conditions should deal with different loop strategies (should they?) but after an attempt to rewrite conditions support by my own I found that LoopParagraphStrategy suits this purpose quite good. As the result, I've managed to compile this simple table:

image

I was also suprized by the fact that my custom syntax implemented few months ago is also supported in conditions. So condition with angular expression also worked for me:

{#~expression:"requisite.id === 'passport'"}Паспорт{/}

Nested conditions also work:

{#~expression:"counterparties.length === 3"}This text is rendered...{#~expression:"counterparties.length === 2"} and this one is not{/}{/}

The next more complicated task was to render properties of few objects in one table (each consisting of a few rows). And it seems that I found a bug in mergeBack implementation of LoopTableStrategy (and probably LoopListStratagey). It doesn't remove all rows of the template, it does remove only the first one and the last one. So I got the following result:

image

Here is the link to source code causing the issue.

A quick fix for me was to pass a whole range received for compilation to mergeBack method:

  public mergeBack(rowGroups: Array<Array<XmlNode>>, firstRow: XmlNode, lastRow: XmlNode, rows: Array<XmlNode>): void {
    for (let i = 0; i < rowGroups.length; i += 1) {
      const curRowsGroup = rowGroups[i];
      for (let j = 0; j < curRowsGroup.length; j += 1) {
        const row = curRowsGroup[j];
        XmlNode.insertBefore(row, lastRow);
      }
    }

    // remove the old rows
    rows.forEach((row) => XmlNode.remove(row));
  }

Another line of code that lead to crash in some scenarios is in LoopParagraphStrategy. Before accessing zero-indexed element of the group we have to check whether it really exists:

      const curParagraphsGroup = middleParagraphs[i];
      if (curParagraphsGroup.length === 0) {
        continue;
      }
      // merge first paragraphs
      docxParser.joinParagraphs(mergeTo, curParagraphsGroup[0]);

Thanks to this, I was lucky to compile the template I needed:

image

The result:

image

For those who are interested I'll leave a link to a gist of my modification of LoopPlugin. I am absolutely not sure that it works as expected in all cases it expected to work. Probably it breaks some features (e.g., didn't test with LoopListStrategy). @alonrbar is the only person who could clarify things. But for now it works for me without need to create nested invisible tables and other similar workarounds. Hope someone will find it useful.

P.S. Here is my custom scope data resolver. Prefixing by : one can access any nested property in the current scope like this:

{:contract.date}

Apply extensions:

{~dateTime:contract.date}

Apply extensions with options:

{~dateTime:contract.date format="DD.MM.YYYY"}

Use angular expression:

{~expression:"counterparties.length > 1 ? 'Counterparties' : 'Counterparty'"}

Use angular expression with extension:

{~dateTime@expression:"counterparties.length === 1 ? counterparties[0].birthDate : '2023-11-24'"}

Fixed in v4.0.0