2

I am currently working on a .NET Core application based on a CMS Framework named PiranhaCMS. This framework allows the definition of configurable "Blocks", basically widgets, that can be added by the users on their pages. The configuration page of the blocks is realized as a Vue.js component, and code is then compiled via gulp in a standard JS format (from the .vue file to the Vue.component(...) syntax) for the Piranha framework to read and render. The author of Piranha confirmed that this is the only way to define new blocks.

In one of our custom blocks, we are trying to implement a DevExpress Web Dashboard. I have tried following the steps outlined at https://docs.devexpress.com/Dashboard/401150/web-dashboard/dashboard-component-for-vue, but to no avail, since the compiler throws an Exception stating that the top-level declaration should be an export default { ... }, and not an import statement.

I came up with a workaround in which I dynamically load the required scripts and styles on the created() method of the component, and then define the dashboard in the same way I would in a classic javascript case (https://docs.devexpress.com/Dashboard/119158/web-dashboard/dashboard-control-for-javascript-applications-jquery-knockout-etc/add-web-dashboard-to-a-javascript-application);;) however, I am sure there is a more elegant solution to this problem.

Below is the code relevant to the problem. Here is the custom block itools-dashboard.vue:

<template>
    <div class="form-group block-body">
        <div :id="'dashboard-designer-' + uid" class="dashboard-designer">
            <div :id="'dashboard_' + uid" style="height: 100%;">
            </div>
        </div>
        <div class="row">
            <div class="col-sm-6" style="padding:10px; margin-top: 0px;vertical-align: top;">
                <fieldset>
                    <legend>Dashboard</legend>
                    <div class="form-group">
                        <label>Dashboard name</label>
                        <select class="form-control small" :id="'dashboard-names-' + uid" v-model="model.dashboardName.value">
                            <option v-for="dash in dashboardNames">{{ dash }}</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label>Update time</label>
                        <input class="form-control small" type="number" v-model="model.updateTime.value">
                    </div>
                    <div class="form-group">
                        <label>Width</label>
                        <input class="form-control small" type="text" v-model="model.width.value">
                    </div>
                    <div class="form-group">
                        <label>Height</label>
                        <input class="form-control small" type="text" v-model="model.height.value">
                    </div>
                </fieldset>
            </div>
            <div class="col-sm-6" style="padding:10px; margin-top: 0px; background-color: #fcfcfc; border:1px dotted lightgray; vertical-align: top;">
                <itools-base :model="model"></itools-base>
            </div>
        </div>
    </div>
</template>
<script>
    export default {
        props: ["uid", "toolbar", "model"],
        data: function () {
            return {
                dashboardNames: [],
                dahsboardConfig: null,
                updateModes: ["period", "realtime"],
                basePath: "../../../../assets/",
                // define all the css and js files paths
                cssResources: [
                    "devextreme/dist/css/light.css",
                    "@devexpress/analytics-core/dist/css/dx-analytics.common.css",
                    "@devexpress/analytics-core/dist/css/dx-analytics.light.css",
                    "@devexpress/analytics-core/dist/css/dx-querybuilder.css",
                    "devexpress-dashboard/dist/css/dx-dashboard.light.min.css"
                ],
                jsResources: [
                    "js/jquery/jquery-3.3.1.min.js",
                    "jquery-ui-dist/jquery-ui.js",
                    "knockout/build/output/knockout-latest.js",
                    "ace-builds/src-min-noconflict/ace.js",
                    "ace-builds/src-min-noconflict/ext-language_tools.js",
                    "ace-builds/src-min-noconflict/theme-dreamweaver.js",
                    "ace-builds/src-min-noconflict/theme-ambiance.js",
                    "devextreme/dist/js/dx.all.js",
                    "devextreme/dist/js/dx.aspnet.mvc.js",
                    "devextreme-aspnet-data/js/dx.aspnet.data.js",
                    "@devexpress/analytics-core/dist/js/dx-analytics-core.min.js",
                    "@devexpress/analytics-core/dist/js/dx-querybuilder.min.js",
                    "devexpress-dashboard/dist/js/dx-dashboard.min.js"
                ]
            }
        },
        created: function () {
            // dynamically add the required css
            this.cssResources.forEach(x => {
                let link = document.createElement("link");
                link.setAttribute("href", this.basePath + x);
                link.setAttribute("rel", "stylesheet");
                document.head.appendChild(link);
            });
            // dynamically add the js files. 
            // It needs to be a synchronous ajax call so that the exports are visible in the code
            // (eg the new DevExpress call)
            this.jsResources.forEach(x => {
                $.ajax({
                    async: false,
                    url: this.basePath + x,
                    dataType: "script"
                })
            });
            this.model.width.value = this.model.width.value || "100%";
            this.model.height.value = this.model.height.value || "300";
            this.model.updateTime.value = this.model.updateTime.value || 5000;

        },
        mounted: function () {
            var h = document.getElementById("dashboard-designer-" + this.uid).clientHeight;

            DevExpress.Dashboard.ResourceManager.embedBundledResources();
            var dashboardControl = new DevExpress.Dashboard.DashboardControl(document.getElementById("dashboard_" + this.uid), {
                endpoint: "/api/dashboard",
                workingMode: "Designer",
                width: "100%",
                height: "100%",
                initialDashboardId: this.model.dashboardName.value,
            });

            dashboardControl.render();
        },
        beforeCreate: function () {
            fetch("/api/Dashboards/GetDashboardNames")
                .then(response => response.json())
                .then(data => {
                    this.dashboardNames = data;
                });
        },
    }
</script>

which is then compiled via gulp task to

Vue.component("itools-dashboard", {
  props: ["uid", "toolbar", "model"],
  data: function () {
    return {
      dashboardNames: [],
      dahsboardConfig: null,
      updateModes: ["period", "realtime"],
      basePath: "../../../../assets/",
      cssResources: ["devextreme/dist/css/light.css", "@devexpress/analytics-core/dist/css/dx-analytics.common.css", "@devexpress/analytics-core/dist/css/dx-analytics.light.css", "@devexpress/analytics-core/dist/css/dx-querybuilder.css", "devexpress-dashboard/dist/css/dx-dashboard.light.min.css"],
      jsResources: ["js/jquery/jquery-3.3.1.min.js", "jquery-ui-dist/jquery-ui.js", "knockout/build/output/knockout-latest.js", "ace-builds/src-min-noconflict/ace.js", "ace-builds/src-min-noconflict/ext-language_tools.js", "ace-builds/src-min-noconflict/theme-dreamweaver.js", "ace-builds/src-min-noconflict/theme-ambiance.js", "devextreme/dist/js/dx.all.js", "devextreme/dist/js/dx.aspnet.mvc.js", "devextreme-aspnet-data/js/dx.aspnet.data.js", "@devexpress/analytics-core/dist/js/dx-analytics-core.min.js", "@devexpress/analytics-core/dist/js/dx-querybuilder.min.js", "devexpress-dashboard/dist/js/dx-dashboard.min.js"]
    };
  },
  created: function () {
    this.cssResources.forEach(x => {
      let link = document.createElement("link");
      link.setAttribute("href", this.basePath + x);
      link.setAttribute("rel", "stylesheet");
      document.head.appendChild(link);
    });
    this.jsResources.forEach(x => {
      $.ajax({
        async: false,
        url: this.basePath + x,
        dataType: "script"
      });
    });
    this.model.width.value = this.model.width.value || "100%";
    this.model.height.value = this.model.height.value || "300";
    this.model.updateTime.value = this.model.updateTime.value || 5000;
  },
  mounted: function () {
    DevExpress.Dashboard.ResourceManager.embedBundledResources();
    var dashboardControl = new DevExpress.Dashboard.DashboardControl(document.getElementById("dashboard_" + this.uid), {
      endpoint: "/api/dashboard",
      workingMode: "Designer",
      width: "100%",
      height: "100%",
      initialDashboardId: this.model.dashboardName.value
    });
    dashboardControl.render();
  },
  beforeCreate: function () {
    fetch("/api/Dashboards/GetDashboardNames").then(response => response.json()).then(data => {
      this.dashboardNames = data;
    });
  },
  template: "\n<div class=\"form-group block-body\">\n    <div :id=\"'dashboard-designer-' + uid\" class=\"dashboard-designer\">\n        <div :id=\"'dashboard_' + uid\" style=\"height: 100%;\">\n        </div>\n    </div>\n    <div class=\"row\">\n        <div class=\"col-sm-6\" style=\"padding:10px; margin-top: 0px;vertical-align: top;\">\n            <fieldset>\n                <legend>Dashboard</legend>\n                <div class=\"form-group\">\n                    <label>Dashboard name</label>\n                    <select class=\"form-control small\" :id=\"'dashboard-names-' + uid\" v-model=\"model.dashboardName.value\">\n                        <option v-for=\"dash in dashboardNames\">{{ dash }}</option>\n                    </select>\n                </div>\n                <div class=\"form-group\">\n                    <label>Update time</label>\n                    <input class=\"form-control small\" type=\"number\" v-model=\"model.updateTime.value\">\n                </div>\n                <div class=\"form-group\">\n                    <label>Width</label>\n                    <input class=\"form-control small\" type=\"text\" v-model=\"model.width.value\">\n                </div>\n                <div class=\"form-group\">\n                    <label>Height</label>\n                    <input class=\"form-control small\" type=\"text\" v-model=\"model.height.value\">\n                </div>\n            </fieldset>\n        </div>\n        <div class=\"col-sm-6\" style=\"padding:10px; margin-top: 0px; background-color: #fcfcfc; border:1px dotted lightgray; vertical-align: top;\">\n            <itools-base :model=\"model\"></itools-base>\n        </div>\n    </div>\n</div>\n"
});  

The gulp task responsible for the compilation, defined by Piranha, is:

var gulp = require('gulp'),
    sass = require('gulp-sass'),
    cssmin = require("gulp-cssmin"),
    uglifyes = require('uglify-es'),
    composer = require('gulp-uglify/composer'),
    uglify = composer(uglifyes, console),
    rename = require("gulp-rename"),
    concat = require("gulp-concat");

var path = require('path'),
    vueCompiler = require('vue-template-compiler'),
    babel = require("@babel/core"),
    babelTemplate = require("@babel/template").default,
    codeFrameColumns = require('@babel/code-frame').codeFrameColumns,
    babelTypes = require("@babel/types"),
    through2 = require('through2');

function vueCompile() {
    return through2.obj(function (file, _, callback) {
        var relativeFile = path.relative(file.cwd, file.path);
        console.log(relativeFile);
        var ext = path.extname(file.path);
        if (ext === '.vue') {
            var getComponent;
            getComponent = function (ast, sourceCode) {
                const ta = ast.program.body[0]
                if (!babelTypes.isExportDefaultDeclaration(ta)) {
                    var msg = 'Top level declaration in file ' + relativeFile + ' must be "export default {" \n' + codeFrameColumns(sourceCode, { start: ta.loc.start }, { highlightCode: true });
                    throw msg;
                }
                return ta.declaration;
            }

            var compile;
            compile = function (componentName, content) {
                var component = vueCompiler.parseComponent(content, []);
                if (component.styles.length > 0) {
                    component.styles.forEach(s => {
                        const linesToStyle = content.substr(0, s.start).split(/\r?\n/).length;
                        var msg = 'WARNING: <style> tag in ' + relativeFile + ' is ignored\n' + codeFrameColumns(content, { start: { line: linesToStyle } }, { highlightCode: true });
                        console.warn(msg);
                    });
                }

                var ast = babel.parse(component.script.content, {
                    parserOpts: {
                        sourceFilename: file.path
                    }
                });

                var vueComponent = getComponent(ast, component.script.content);
                vueComponent.properties.push(babelTypes.objectProperty(babelTypes.identifier('template'), babelTypes.stringLiteral(component.template.content)))

                var wrapInComponent = babelTemplate("Vue.component(NAME, COMPONENT);");
                var componentAst = wrapInComponent({
                    NAME: babelTypes.stringLiteral(componentName),
                    COMPONENT: vueComponent
                })

                ast.program.body = [componentAst]

                babel.transformFromAst(ast, null, null, function (err, result) {
                    if (err) {
                        callback(err, null)
                    }
                    else {
                        file.contents = Buffer.from(result.code);
                        callback(null, file)
                    }
                });
            }
            var componentName = path.basename(file.path, ext);
            if (file.isBuffer()) {
                compile(componentName, file.contents.toString());
            }
            else if (file.isStream()) {
                var chunks = [];
                file.contents.on('data', function (chunk) {
                    chunks.push(chunk);
                });
                file.contents.on('end', function () {
                    compile(componentName, Buffer.concat(chunks).toString());
                });
            }
        } else {
            callback(null, file);
        }
    });
}

var js = {
    name: "itools-blocks.js",
    path: "wwwroot/assets/js/blocks/*.vue"
}

//
// Compile & minimize js files
//
gulp.task("min:js", function (done) {
    gulp.src(js.path, { base: "." })
        .pipe(vueCompile())
        .pipe(concat("wwwroot/assets/js/blocks/" + js.name))
        .pipe(gulp.dest("."))
        .pipe(uglify().on('error', function (e) {
            console.log(e);
        }))
        .pipe(rename({
            suffix: ".min"
        }))
        .pipe(gulp.dest("."));
    done();
});

any kind of help is well appreciated

lupok
  • 1,025
  • 11
  • 15

1 Answers1

0

The gulpfile with the method “vueCompile” that you’re referring to was specifically written to suit the needs of the internal components we provide in the framework, it’s by no means a silver bullet for all Vue component compilation. However I understand your problem, before writing this code we desperately searched for existing npm-packages that would give us the functionality we needed, but this wasn’t that easy to find as we only use a subset of the features available in Vue.js

We’d be more than happy to get feedback or more information on how this could be done, so we’ll be watching this thread

Håkan Edling
  • 2,723
  • 1
  • 12
  • 15
  • Hi Håkan, I have been using piranha since version 6, it seems to me that the introduction of VUE has "stiffened" the framework, wouldn't it be useful to have the possibility to create objects without being bound to VUE? – lupok Jul 30 '21 at 14:41
  • As the editorial interface we wanted to build was almost impossible to create using only server-side rendered asp.net we had to choose a JavaScript framework to add the client side features. Custom pages can be added to the manager using either MVC or Razor Pages with or without Vue.js, but when adding components that should integrate with the existing edit views, like fields or custom blocks it’s necessary to build these with Vue. – Håkan Edling Aug 01 '21 at 06:19