Advanced Components

Warning

Work in progress. The example works but requires much more elucidation

Introduction

In most cases, components can be defined using Python only as previously described in this tutorial. In some cases however, in parallel to the Python definition, a Vue.js component also needs to be created.

In this chapter, we will create a signing pad component based on signature_pad

Info

If you are not familiar with Vue.js, this chapter will not be very useful to you.

The Components Directory

Under your applications static directory (the default is the directory the application is run form) create another directory called components. Put the your JavaScript component definition there. Files must all have the extension .js.

The application will load these files automatically.

Python

import justpy as jp

class SignaturePad(jp.JustpyBaseComponent):

    vue_type = 'signaturepad'

    def __init__(self, **kwargs):

        self.options = jp.Dict()
        self.classes = ''
        self.style = ''
        self.width = 400
        self.height = 200
        self.clear = False
        self.show = True
        self.event_propagation = True
        self.pages = {}
        kwargs['temp'] = False  # Force an id to be assigned to pad
        super().__init__(**kwargs)
        self.allowed_events = ['onEnd', 'onBegin']
        if type(self.options) != jp.Dict:
            self.options = jp.Dict(self.options)
        self.initialize(**kwargs)


    def add_to_page(self, wp: jp.WebPage):
        wp.add_component(self)

    def react(self, data):
        pass


    def convert_object_to_dict(self):
        d = {}
        d['vue_type'] = self.vue_type
        d['id'] = self.id
        d['show'] = self.show
        d['classes'] = self.classes
        d['style'] = self.style
        d['event_propagation'] = self.event_propagation
        d['def'] = self.options
        d['events'] = self.events
        d['width'] = self.width
        d['height'] = self.height
        d['clear'] = self.clear
        d['options'] = self.options
        return d


def my_end(self, msg):
    print(msg)
    self.data = msg.data

async def clear_pad(self, msg):
    self.pad.clear = True
    await msg.page.update()
    self.pad.clear = False
    return True


def pad_test():
    wp = jp.WebPage()
    wp.head_html = '<script src="https://cdn.jsdelivr.net/npm/signature_pad@2.3.2/dist/signature_pad.min.js"></script>'
    pad = SignaturePad(a=wp, style = 'background-color: white; border: 1px solid;', classes='m-2', onEnd=my_end)
    pad.options = {'penColor': 'blue'}
    clear_btn = jp.Button(text='Clear Pad', classes=jp.Styles.button_simple + ' m-2', a=wp, click=clear_pad)
    clear_btn.pad = pad
    return wp

jp.justpy(pad_test)

Vue.js Component

var signature_pads = {};
    Vue.component('signaturepad', {
        template:
            `<canvas  v-bind:id="jp_props.id" :class="jp_props.classes"  :style="jp_props.style" :width="jp_props.width" height="jp_props.height"></canvas>`,
        methods: {
            pad_change() {
                var id = this.$props.jp_props.id.toString();
                var canvas = document.getElementById(id);
                var signaturePad = new SignaturePad(canvas, this.$props.jp_props.options);
                signature_pads[id] = signaturePad;
                var events = this.$props.jp_props.events;
                var props = this.$props;

                function onEnd() {
                    if (events.includes('onEnd')) {
                        var data = signaturePad.toDataURL('image/png');
                        var point_data = signaturePad.toData();
                        var e = {
                            'event_type': 'onEnd',
                            'id': props.jp_props.id,
                            'class_name': props.jp_props.class_name,
                            'html_tag': props.jp_props.html_tag,
                            'vue_type': props.jp_props.vue_type,
                            'page_id': page_id,
                            'websocket_id': websocket_id,
                            'data': data,
                            'point_data': point_data
                        };
                        send_to_server(e, 'event');
                    }
                }

                signaturePad.onEnd = onEnd;

            }
        },
        mounted() {
            this.pad_change();
        },
        updated() {

            if (this.$props.jp_props.clear) {
                signature_pads[this.$props.jp_props.id.toString()].clear();
            }
        },
        props: {
            jp_props: Object
        }
    });

FullCalendar Example

FullCalendar is a full featured JavaScript Calendar

The following implementation does not support all the features and events but is a sound basis to build on.

Python file:

import justpy as jp


class FullCalendar(jp.JustpyBaseComponent):
    vue_type = 'fullcalendar'

    def __init__(self, **kwargs):
        self.options = jp.Dict()
        self.classes = ''
        self.style = ''
        self.show = True
        self.event_propagation = True
        self.pages = {}
        kwargs['temp'] = False  # Force an id to be assigned
        super().__init__(**kwargs)
        self.allowed_events = ['eventClick', 'eventDrop']
        if type(self.options) != jp.Dict:
            self.options = jp.Dict(self.options)
        self.initialize(**kwargs)

    def add_to_page(self, wp: jp.WebPage):
        wp.add_component(self)

    def react(self, data):
        pass

    async def run_method(self, command, websocket):
        await websocket.send_json({'type': 'run_method', 'data': command, 'id': self.id})
        # So the page itself does not update, return True not None
        return True

    def convert_object_to_dict(self):
        d = {}
        d['vue_type'] = self.vue_type
        d['id'] = self.id
        d['show'] = self.show
        d['classes'] = self.classes
        d['style'] = self.style
        d['event_propagation'] = self.event_propagation
        d['events'] = self.events
        d['options'] = self.options
        return d


calendar_options = {
    'plugins': ['dayGrid', 'interaction'],
    'editable': True,
    'header': {'left': '',
               'center': 'title',
               'right': 'today prev,next'},
    'defaultDate': '2020-05-15',
    'events': [
        {
            'title': 'This is an event',
            'start': '2020-05-02',
            'end': '2020-05-12',
            'color': 'red',
            'editable': True
        },
        {
            'title': 'event with a URL',
            'url': 'https://www.google.com/',
            'start': '2020-05-03'
        }
    ]
}
# https://fullcalendar.io/docs/plugin-index
head_html = """
<link rel="stylesheet" href="https://unpkg.com/@fullcalendar/core@4.4.0/main.min.css">
<link rel="stylesheet" href="https://unpkg.com/@fullcalendar/daygrid@4.4.0/main.min.css">
<script src="https://unpkg.com/@fullcalendar/core@4.4.0/main.min.js"></script>
<script src="https://unpkg.com/@fullcalendar/daygrid@4.4.0/main.min.js"></script>
<script src="https://unpkg.com/@fullcalendar/interaction@4.4.0/main.min.js"></script>
"""

async def add_event(self, msg):
    print(msg)
    self.calendar.options['events'].append({'title': 'Very new event', 'start': '2020-05-02', 'end': '2020-05-05', 'color': 'green', 'editable': True})

def event_click(self, msg):
    print(msg)

def event_drop(self, msg):
    print(msg)
    print(msg.all_events)
    self.options['events'] = msg.all_events

def calendar_test():
    wp = jp.QuasarPage()
    wp.head_html = head_html
    calendar = FullCalendar(a=wp, classes='q-ma-lg', style='width: 700px;')
    calendar.options = calendar_options
    calendar.on('eventClick', event_click)
    calendar.on('eventDrop', event_drop)
    b = jp.QBtn(label='Add Event', a=wp, classes='q-ma-lg', click=add_event)
    b.calendar = calendar
    return wp


jp.justpy(calendar_test)

Vue.js component:

var full_calendars = {};
Vue.component('fullcalendar', {
    template:
        `<div  v-bind:id="jp_props.id" :class="jp_props.classes"  :style="jp_props.style" ></div>`,

    methods: {
        get_all_events(calendar) {
            let all_events = [];
            for (let event_obj of calendar.getEvents()) {
                all_events.push(this.create_object_from_event(event_obj))
            }
            return all_events;
        },
        create_object_from_event(event_obj) {
            // https://fullcalendar.io/docs/event-object
            const event_properties = ['id', 'groupId', 'allDay', 'start', 'end', 'title', 'url', 'classNames',
                'editable', 'startEditable', 'durationEditable', 'resourceEditable', 'rendering', 'overlap',
                'constraint', 'backgroundColor', 'borderColor', 'textColor', 'extendedProps'];
            let event_data = {};
            for (let i of event_properties) {
                event_data[i] =event_obj[i];
            }
            return event_data;
        },
        calendar_change() {
            var id = this.$props.jp_props.id.toString();
            var events = this.$props.jp_props.events;
            var props = this.$props;
            var calendarEl = document.getElementById(id);
            var calendar = new FullCalendar.Calendar(calendarEl, this.$props.jp_props.options);
            const parent_comp = this;
            if (events.includes('eventClick'))
                // https://fullcalendar.io/docs/eventClick
                calendar.on('eventClick', function (info) {
                    var e = {
                        'event_type': 'eventClick',
                        'id': props.jp_props.id,
                        'class_name': props.jp_props.class_name,
                        'html_tag': props.jp_props.html_tag,
                        'vue_type': props.jp_props.vue_type,
                        'page_id': page_id,
                        'websocket_id': websocket_id,
                        'event_data': parent_comp.create_object_from_event(info.event),
                        'all_events': parent_comp.get_all_events(calendar)
                    };
                    send_to_server(e, 'event');
                });

            if (events.includes('eventDrop'))
                // https://fullcalendar.io/docs/eventDrop
                calendar.on('eventDrop', function (info) {
                    var e = {
                        'event_type': 'eventDrop',
                        'id': props.jp_props.id,
                        'class_name': props.jp_props.class_name,
                        'html_tag': props.jp_props.html_tag,
                        'vue_type': props.jp_props.vue_type,
                        'page_id': page_id,
                        'websocket_id': websocket_id,
                        'event_data': parent_comp.create_object_from_event(info.event),
                        'old_event_data': parent_comp.create_object_from_event(info.oldEvent),
                        'delta': info.delta,
                        'all_events': parent_comp.get_all_events(calendar)
                    };
                    send_to_server(e, 'event');
                });
            full_calendars[id] = calendar;
            comp_dict[this.$props.jp_props.id] = calendar;

            calendar.render();
        },

    },

    mounted() {
        this.calendar_change();
    },
    updated() {
        var calendar = comp_dict[this.$props.jp_props.id];
        calendar.destroy();
        this.calendar_change();

    },
    props: {
        jp_props: Object
    }
});