How to import Vue components?

I’ve tried playing with Vue in Frappe and ran into a problem.

How do I import components to extend a new Vue instance for my Page, generated with the Page DocType? I can’t use ES6 imports due to the Page script not being appended as type=module.

I’ve implemented imports with with callbacks and frappe.require(), but it feels like a hacky convoluted way and I can’t seem to understand how existing Vue infrastructure, allowing ES6 imports gets wired to the Frappe instance.

This is what I’ve got for now:
new_page.js:

frappe.provide('frappe.app.export_app');
frappe.pages['new_page'].refresh = function(wrapper) {
	if (!$('#app').length) {
		$(frappe.render_template('new_page_template', page)).prependTo(page.main);
	}
	frappe.require('assets/dc_plc/js/vue/button_counter.js', () => {
		Vue.component('button_counter', button_counter());
		frappe.app.export_app.vue_instance = new Vue({
			el: '#app',
			data: {}
		});
	});
};

button_counter.js:

let button_counter = () => {
	return {
		data: function () {
			return {
				count: 0
			}
		},
		name: 'button_counter',
		template: '<button v-on:click="count++">Click counter — {{ count }}</button>'
	}
};

new_page_template.html:

<div id="export-app">
	<button_counter></button_counter>
</div>
3 Likes

If you want to write single file Vue components (.vue) then you will have to include them in build.json. Files included in build.json are processed through Rollup, so you can use ES6 imports in them.

Take a look at examples:

https://github.com/frappe/frappe/blob/develop/frappe/public/js/frappe/views/modules_home.js

2 Likes

Thanks, this is what I was looking for.

I saw the existing .js files using Vue, but I couldn’t figure out how to correctly add them to the page.

UPD: okay, finally got Vue running properly, will write a short manual later.

Thanks to Faris’s guidance, I managed to bootstrap a custom Vue instance to the page, created via the Page DocType.

This worked for me:

First, create a .js file, which will manage the Vue bootstrapping process (shamelessly stolen from the modules_home.js):

export_tool.js

import ToolRoot from './ToolRoot.vue';

frappe.provide('frappe.your_app');   // create a namespace within the Frappe instance

frappe.your_app.ExportTool = class {   // create a glue class, wich will manage your Vue instance
        constructor({ parent }) {
                this.$parent = $(parent);
                this.page = parent.page;
                this.setup_header();
                this.make_body();
        }
        make_body() {
                this.$export_tool_container = this.$parent.find('.layout-main');   // bind the new Vue instance to the main <div> on the page
                this.vue = new Vue({
                        el: this.$export_tool_container[0],
                        data: {
                        },
                        render: h => h(ToolRoot),
                });
        }
        setup_header() {
        }
};

I think this is possible without a glue class, but I’ve yet to try it.

Next, bootstrap your glue class to the new_page you have created via Page DocType:

new_page.js

frappe.pages['new_page'].on_page_load = function(wrapper) {
        this.page = frappe.ui.make_app_page({
                parent: wrapper,
                title: 'New page title',
                single_column: true,
        });

        this.page.$export_tool = new frappe.your_app.ExportTool(this.page);
};

Then, create your root single file component:

ToolRoot.vue

<template>
        <div>{{ some_data }}<br /><ButtonCounter :count="50"></ButtonCounter></div>
</template>

<script>
        import ButtonCounter from './ButtonCounter.vue'

        export default {
                name: "ToolRoot",
                data: function () {
                        return {
                                some_data: 'sample data',
                        }
                },
                components: {
                        ButtonCounter
                },
        }
</script>

Then, create other Vue components as per the usual Vue development process:

ButtonCounter.vue

<template>
        <button v-on:click="count++">Click count — {{ count }}</button>
</template>

<script>
        export default {
                props: ['count'],
                data: function () {
                        return {
                                count: 0
                        }
                }
        }
</script>

Next, wire the whole setup to the Frappe instance via Rollup:

Your app’s hooks.py:

// ...
app_include_js = "/assets/js/your_app.min.js"
// ...

Your app’s build.json:

{
    "js/your_app.min.js": [
        "public/js/export_tool/export_tool.js",
        "public/js/your_app.js"
    ]
}

This is an importants step, since unbundled .js files will not allow you to use ES6 imports to import Vue single file components

Result:

15 Likes

Does this work in v11?

The method itself should work because AFAIK the Page generation in v11 is mostly the same as in v12. However you might need to yarn add vue to your app directory yourself, since I don’t remember if Vue is being used in v11.

You will then also need to import Vue from 'vue' along with your root component import.

1 Like

It does work in v11

For those that need to do this in Version 14, note that the build system has been changed from rollup to esbuild, hence the build.json file is no longer valid.

See Migrating to Version 14 · frappe/frappe Wiki (github.com) for details.

I could use import my component but tou create it in your js file

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Vue Test</title>

    <!-- This is a development version of Vue.js! -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

    <!-- import es6 module in main.js -->
    <script src="/webbrowser/main.js" type="module"></script>

    <!-- import stylesheet -->
    <link rel="stylesheet" href="/webbrowser/styles.css">

    <!-- Google fonts -->
    <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">

{% raw %}
<!-- Basic usage -->
<div id="app_basic" v-bind:title="timestamp" class="experiment-block">
    <a href="/webbrowser/f1/f2/f3" class="nav-link" active-class="active">
        {{ message }}
    </a>
</div>

<!-- Loops -->
<div id="app_loops" class="experiment-block">
    <ol>
        <li v-for="todo in todos">
            {{ todo.text }}
        </li>
    </ol>
</div>

<!-- OnClick -->
<div id="app_onclick" class="experiment-block">
    <span>{{ message }}</span>
    <button v-on:click="randomGenerate">Click me</button>
</div>

<!-- Data binding -->
<div id="app_model" class="experiment-block">
    <p>{{ value }}</p>
    <input v-model="value">
</div>

<!--  Components -->
<div id="app_component" class="experiment-block">
    <todo_list v-bind:todo_list_prop="todoList"/>
</div>
{% endraw %}

main.js

// List of to do items (accepts an array as props)
Vue.component(
    'todo_list',
    {
        props: ['todo_list_prop'],
        template:
            `<ol>
                <todo_item />
            </ol>`
    },
);
// Renderer for each to do item (accepts one item as props)
Vue.component(
    'todo_item',
    {
        props: ['todo_item_prop'],
        template:
            `<li>
                test 2
            </li>`
    },
);

new Vue(
    {
        el: '#app_basic',
        data: {
            message: '🐵 Hello World 🔮',
        },
    });

new Vue(
    {
        el: '#app_loops',
        data: {
            todos: [{
                text: 'Learn JavaScript',
            },
                {
                    text: 'Learn Vue',
                },
                {
                    text: 'Build something awesome',
                },
            ],
        },
    });

new Vue(
    {
        el: '#app_onclick',
        data: {
            message: "Click me (random number generate)",
        },
        methods: {
            randomGenerate() {
                this.message = Math.random();
            },
        },
    });

new Vue(
    {
        el: '#app_model',
        data: {
            value: "Some string data",
        },
    });
    // Setup the data for the to do list (and and attach to index.html)
new Vue(
    {
        el: '#app_component',
        data: {
            todoList: [
                {id: 0, text: 'Brush teeth', done: true},
                {id: 1, text: 'Buy chocolate', done: false},
                {id: 2, text: 'Sell laptop', done: false},
            ],
        },
    });

it works

2 Likes

This works well on Version 13 still