diff --git a/api/src/reportcreator_api/tests/test_rendering.py b/api/src/reportcreator_api/tests/test_rendering.py
index aa8548082..811cc87bd 100644
--- a/api/src/reportcreator_api/tests/test_rendering.py
+++ b/api/src/reportcreator_api/tests/test_rendering.py
@@ -80,6 +80,8 @@ def extract_html_part(self, html, start=None, end=None):
("{{ formatDate('2022-09-21', 'long', 'en-US') }}", "September 21, 2022"),
("{{ formatDate('2022-09-21', 'full', 'en-US') }}", "Wednesday, September 21, 2022"),
("{{ formatDate('2022-09-21', {year: '2-digit', month: 'narrow', day: '2-digit', numberingSystem: 'latn'}, 'en-US') }}", "S 21, 22"),
+ ("""
{{ helperFunction() }}
""", lambda self: f"{self.project.data['title']} function
"),
+ ("""{{ computedVar.value }}
""", lambda self: f"{self.project.data['title']} computed
"),
])
def test_variables_rendering(self, template, html):
if callable(html):
diff --git a/docs/docs/designer/formatting-utils.md b/docs/docs/designer/formatting-utils.md
index 707ecb205..3b792f2ce 100755
--- a/docs/docs/designer/formatting-utils.md
+++ b/docs/docs/designer/formatting-utils.md
@@ -76,4 +76,31 @@ English (default): ...
English (no commas, always "and"): ...
German: ...
French: ...
-```
\ No newline at end of file
+```
+
+
+## Helper Functions
+It is possible to define helper functions and variables inside the Vue template language to reuse logic.
+Setting variables only works for native DOM tag (e.g. ``, `
`, etc.), but not for Vue components (e.g. ``, ``, etc.).
+The name of the `:set` attributes does not matter, but they have to be unique per tag.
+Helper functions are defined at the start of the template, they can be used by following template elements.
+
+```html
+
+
+ Call helper function (without arguments): {{ helperFunction() }}
+ Call helper function (with arguments): {{ calculateCustomScore(report.findings[0]) }}
+ Use computed property: {{ computedProperty.value }}
+
+```
+
+Note that defining variables and helper functions is not officially supported by the Vue template language, but rather a workaround.
+For more details see: https://stackoverflow.com/questions/43999618/how-to-define-a-temporary-variable-in-vue-js-template
diff --git a/rendering/src/main.js b/rendering/src/main.js
index d6db506d9..3da10960c 100644
--- a/rendering/src/main.js
+++ b/rendering/src/main.js
@@ -1,4 +1,4 @@
-import { createApp, compile, nextTick } from 'vue';
+import { createApp, compile, computed, ref } from 'vue';
import { generateCodeFrame } from '@vue/shared';
import ChartJsPluginDataLabels from 'chartjs-plugin-datalabels';
import Pagebreak from './components/Pagebreak.vue';
@@ -13,7 +13,6 @@ import Ref from './components/Ref.vue';
import { callForTicks } from './utils';
import lodash from 'lodash';
-
// injected as global variables
const REPORT_TEMPLATE = '' + (window.REPORT_TEMPLATE || '') + '
';
const REPORT_DATA = window.REPORT_DATA || { report: {}, findings: [] };
@@ -59,20 +58,15 @@ const DEFAULT_COMPUTED = {
count_info: this.findings_info.length,
};
},
- lodash() {
- return lodash;
- },
chartjsPlugins() {
return {
DataLabels: ChartJsPluginDataLabels
};
},
- window() {
- return window;
- },
- document() {
- return document;
- },
+ lodash: () => lodash,
+ window: () => window,
+ document: () => document,
+ computed: () => computed,
};
const DEFAULT_METHODS = {
@@ -144,6 +138,8 @@ if (!window.RENDERING_COMPLETED) {
data: () => ({
data: REPORT_DATA,
_tickCount: 0,
+ _pendingPromises: [],
+ _observer: null,
}),
computed: {
...DEFAULT_COMPUTED,
@@ -153,12 +149,43 @@ if (!window.RENDERING_COMPLETED) {
...DEFAULT_METHODS,
...REPORT_METHODS,
},
+ created() {
+ this._observer = new MutationObserver((mutationList) => {
+ for (const mutation of mutationList) {
+ if (mutation.type === 'childList') {
+ for (const node of mutation.addedNodes) {
+ if (node.nodeType === Node.ELEMENT_NODE && node.nodeName === 'SCRIPT') {
+ this._pendingPromises.push(new Promise((resolve, reject) => {
+ node.addEventListener('load', resolve);
+ node.addEventListener('error', reject);
+ }));
+ }
+ }
+ }
+ }
+ });
+ this._observer.observe(document, { childList: true, subtree: true });
+ },
+ beforeUnmount() {
+ this._observer.disconnect();
+ },
async mounted() {
- // Wait some ticks before rendering is signaled as completed
- // Allow multi-pass rendering (for e.g. table of contents)
- await callForTicks(10, nextTick, () => {
- this._tickCount += 1;
- })
+ const waitUntilFinished = async () => {
+ // Wait some ticks before rendering is signaled as completed
+ // Allow multi-pass rendering (for e.g. table of contents)
+ await callForTicks(10, () => {
+ this._tickCount += 1;
+ });
+ // Wait for pending promises to finish
+ if (this._pendingPromises.length > 0) {
+ await Promise.allSettled(this._pendingPromises);
+ await callForTicks(10, () => {
+ this._tickCount += 1;
+ });
+ }
+ }
+ await waitUntilFinished();
+
window.RENDERING_COMPLETED = true;
},
});