Difficulty customizing vapps and qapps

This question has reference for something I’m still stuck in

Briefly the problem in qapps and vapps mode is the following:

  • Javascript works
  • HTML works
  • Manipulating the DOM does not work

so if I have an XML screen and part of it is custom made and dynamic, I can easily do the customization in html render-mode, but in vuet and qvt render modes it’s very difficult to achieve the same result. The issues that are stopping me are:

  • I cannot manipulate the DOM directly because of the virtual dom in vue
  • I cannot register new components after instantiation new Vue(...)
  • I cannot “cleanly” extend the capabilities of WebrootVue to add my own custom components.

Is ther anyway that I can customize qvt and vuet renders modes without being 100% client rendered (i.e. js + vuet or js + qvt modes).

Thanks in advance for all the help folks

2 Likes

Just to note another problem, even with 100% client rendered screens I have another problem, I cannot use the same screen in html mode because modes defind render-modes="js,vuet" is required. So it seems difficult to serve the same custom screen in all 3 modes, html, vuet and qvt. Either javascript kicks-in and disables everything, or the screen is server-rendered and I cannot customize the above mentioned modes.

Have you checked GitHub - jonesde/moqui-quasar-custom: Example component for a moqui-quasar based application with custom root template for header, etc?
I have customized my theme like the following screen.

1 Like

So what you’re doing here is essentially overriding the root screen with your own WebrootVue. This might be nice and has its usecase but that’s not where my pain point is.

The real issue for me is the inability to customize qapps and vapps the same way I customize apps. So for the same XML screen, I have to write essentially in two different ways. If I’m on apps, I just write XML and insert my custom code using FreeMarker in render-mode tags and I can use jQuery and ajax etc … to make things dynamic. However, if I’m on qapps and vapps, then the only thing that I can do (for things not supported in the macros / WebrootVue) is to write a 100% client rendered screen (js / qjs) and then use stuff from WebrootVue to save a little bit of time. What would be awesome is if I can write my Vue components right there where I need them or be able to enhance WebrootVue without overriding the root screen.

However, I guess maybe I will create a combo of:

  • custom macros
  • override root screens with my own WebrootVue to allow adding more components

Then proceed to write everything in good old XML for my custom widgets

1 Like

Maybe QuickSearch.qvue can help.

Ahh that’s the best solution so far. Simply enhance the theme from the database such that all my artifacts are loaded on start. Thank you very much for sharing your thoughts

OK on a deeper review the suggestion does not work. The QuickSearch.qvue for example is specifically handled in WebrootVue.qvt.ftl using this statement <#assign navbarCompList = sri.getThemeValues("STRT_HEADER_NAVBAR_COMP")> which is specifically designed for the header that is implemented in simple screens. It’s just impossible to add any vue logic anywhere on the current design except for pure client-rendered screens.

So after investigating all possible options I think the best option is to simply override vapps.xml and qapps.xml with my own screens to be able to add my own scripts to it, which allows me to do Vue.component(...) BEFORE new Vue(...) and hence be able to mix Vue with XML and get the best of both worlds. This solution is a little dangerous because I have to keep my eyes on vapps.xml and qapps.xml to check for any critical changes that I need to incorporate into my files, but this is the only way I can get custom Vue components developed in a mixed screens (XML + vue)

1 Like

Dear Taher,

If what you want to do is to add some parts of a screen with more dynamic js and components with Vue, then the easier way to go is with a <dynamic-container> widget as it was already pointed out.

Then, if the screen that you load is a qvue screen, you can just define your Vue components locally there.

For often used components, then you should define your own Screen Widgets, which is the clean way to go in Moqui.

However, for small one-time-used cases, by using <render-modet><text> you should be able to insert markup using standard HTML and available Moqui Vue components in Vue screens (because that would be rendered inside slots of other OOTB Vue components).

If what you want to do is to define a custom component on the fly, first think twice if this is what you really need and if there may be a more clean way to achieve what you are trying to do. Anyway, you can do it easily with Vue dynamic components with:

<component :is="my component definition here"></component>

Please see:

The special attribute “is” allows you to provide a component’s options object instead of a registered name. This is how <dynamic-container> does it behind scenes (after loading the component definition with an AJAX request).

If the problem is to use your own custom Vue components globally, without modifying anything of the Moqui source code, perhaps this would work for you (not tested):

In your XML screen, within <pre-action>, add your .js with your custom Vue component definitions with something like:

<pre-actions><script><![CDATA[

footer_scripts.add('/my-path/my-vue-components.js')

</pre-actions>

However you should check if footer_scripts already contains “/js/WebrootVue.qvt.js”. In this case, ensure that you insert your js just before it (that is, after vue, quasar, etc. but before WebrootVue).

Please, see moqui/runtime/base-component/webroot/screen/webroot/qapps.xml file.

In your my-vue-components.js files you would define and register your custom components with Vue. Then, they would be globally available.

Francisco Faes
www.blurbiness.com

2 Likes

Hello @franciscofaes

First of all, thank you so much for putting your thinking energy into this. Much appreciated.

So let me give you one example of my requirements. I need a “specialized” dropdown that tries to search for something, and if it doesn’t exist, shows a “create new” button which opens a pop-up dialog to create a new “thing” and place it as the selected value in my dropdown.

Now lets investigate your suggested solutions against that:

  1. dynamic-container does not provide the ability for me to communicate between my dropdown and that container to tell it where to come back to or any other attributes that I need to pass. So to do any communication, I need to select the dialog from the DOM, and boom, you’re stuck on vue.
  2. the <component :is=...> is an interesting solution, but that component cannot communicate with others components, so I’m back to square one, I have to define my full logic in one stand-alone component like <drop-down-dialog> or something like that.
  3. the <pre-actions> does not work, I already tried it. It doesn’t work because you cannot get your pre-actions to run before webrootvue, the only way to do that is to override qapps.xml and vapps.xml. Therefore your suggestion for my-vue-components.js will ONLY work if I FIRST override qapps.xml and vapps.xml so that I control the order sequence of what scripts to load first
  4. I agree with you completely that I should design my own custom widgets and I already did that for some simple scenarios. But for a complex scenario like this one, even BEFORE attempting to create a custom widget I need something that works first, THEN I can abstract it away behind my custom widget and we’re back to good-ol XML. So it is my intention to eventually create my own XML widgets, but I need a solution that works first.

Because of all of that I think the first step is to override qapps.xml and vapps.xml. I see no way around that to get to at least start building my own components.

If you have any additional thoughts for my above usecase, I would appreciate it. I thank you very much for the all the input insofar.

1 Like

Dear Taher,

Yes, the <pre-actions> approach will not work in a subscreen because <subscreens-active> is loaded dynamically and the footer_scripts will be already there at runtime.

However, you can override qapps.xml screen without modifying anything in the Moqui source code.

In the MoquiConf.xml file of your own Moqui component, add the following (tested):

<screen-facade>
  <screen location="component://webroot/screen/webroot.xml">
      <subscreens-item name="qapps" location="component://my-component/screen/MyQapps.xml" />
  </screen>
</screen-facade>

Then, your MyQapps.xml screen would be:

<?xml version="1.0" encoding="UTF-8"?>
<screen xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-2.1.xsd"
        require-authentication="false" screen-theme-type-enum-id="STT_INTERNAL_QUASAR" default-menu-title="Applications" allow-extra-path="true">
    <!-- NOTE: require-authentication=false so no permission required but in pre-actions if no user logged in goes to login screen -->

    <!-- pre-actions copied from original qapps.xml, then insert the scripts that you need -->
    
    <pre-actions><script><![CDATA[
        // if user not logged in save current path and params then redirect to Login
        if (!ec.user.userId) { ec.web.saveScreenLastInfo(null, null); sri.sendRedirectAndStopRender('/Login') }

        html_scripts.add('/libs/moment.js/moment-with-locales.min.js')
        html_scripts.add('/libs/jquery/jquery.min.js')

        String instancePurpose = System.getProperty("instance_purpose")
        if (!instancePurpose || instancePurpose == 'production') {
            /* ========== Production Mode ========== */
            html_scripts.add('/js/MoquiLib.min.js')
            // Vue JS
            footer_scripts.add('/libs/vue/vue.min.js')
            // http-vue-loader
            footer_scripts.add('/js/http-vue-loader/httpVueLoader.js')
            // Quasar
            footer_scripts.add("/libs/quasar/quasar.umd.min.js")

            footer_scripts.add("/js/my-components.js")

            // Webroot Quasar-Vue instance, in footer so runs after page loaded
            footer_scripts.add('/js/WebrootVue.qvt.min.js')
        } else {
            /* ========== Dev Mode ========== */
            html_scripts.add('/js/MoquiLib.js')
            // Vue JS
            footer_scripts.add('/libs/vue/vue.js')
            // http-vue-loader
            footer_scripts.add('/js/http-vue-loader/httpVueLoader.js')
            // Quasar
            footer_scripts.add("/libs/quasar/quasar.umd.min.js")

            footer_scripts.add("/js/my-components.js")

            // Webroot Quasar-Vue instance, in footer so runs after page loaded
            footer_scripts.add('/js/WebrootVue.qvt.js')
        }

        // conditional QZ Tray scripts, only include if enabled
        if (ec.user.getPreference("qz.print.enabled") == "true") {
            footer_scripts.add('/js/qz-tray/sha-256.min.js')
            footer_scripts.add('/js/qz-tray/qz-tray.min.js')
            // TODO migrate MoquiQzComponent.js to MoquiQzComponent.qvt.js
            footer_scripts.add('/js/qz-tray/MoquiQzComponent.js')
        }
    ]]></script></pre-actions>

    <!-- No need to copy/modify qapps.xml screen content, just include it -->
    <widgets>
        <include-screen location="component://webroot/screen/webroot/qapps.xml" share-scope="true"/>
    </widgets>
</screen>

Regarding strategies for communication between Vue components in a more generic way, the Vue official approaches are inject/provide and an eventbus.

With inject/provide you can provide data and functions to children components at any nested sub-level (direct children, grand-children, gran-grand-children, etc).
Please see:

So, basically, you could create your own generic Moqui widget which could be used as:

<provider-container what-to-provide="{myData:'something', myFunc: () => { /* something to do */}}"/>

Or design it as you needed. The attribute what-to-provide could then be specified in freemarker, so you would have all the freedom.
Then, macro implementation would be pretty straight forward with a <component :is="/* component definition here */">
with no need of external .js. Alternatively, go with a predefined component if you had included my-component.js in your HTML.
In the freemarker part, you will generate the Vue markup and then you will call <#recurse> inside the Vue slot.

This means that you could share data and functions (which are just a special type of data) between all Vue components which would be rendered
inside your <provider-container> widget. No matter if they come from a <dynmaic-container>, a dialog or whatever.

provide/inject is very useful in a decoupled scenario like Moqui screens, where different parts fo the screen will come from different screen xml definitions, loaded dynamically at runtime.

And, finally, if you still need an event-like communication between Vue components, then follow the event bus approach.
Please, note that the way to do it in Vue 2.x (version used by Moqui) is different than in Vue 3.x.

Please see:

2 Likes

Yup I already did the stuff you suggested in vapps / qapps override (basically copy-paste into my own screens). I also am aware of vue component bus comunication, however the components must be already designed to accept that communication. So to summarize and to keep this for record here is a summary of what I did / will do:

  • override vapps and qapps to allow me to add my own scripts (esssentally the top level screens)
  • create generic logic for the thing I’m trying to implement using components for vapps / qapps and html/css/js for apps
  • use that implemented logic mentioned above in my screen macros (that enhance existing macros with my new widgets)

So I guess I’m on target, I’m doing as much overriding as necessary.

Thank you very much for your help. Highly appreciated!

1 Like

Dear Taher,

Since all suggestions in previous messages were conceptual, perhaps they were not made clear enough.

Of course that if you use provide you will have to use inject at the other end. But once you start to add custom scripts inline in a screen, you may need to coordinate them. For example, things from a form, with things from a dialog, and a subscreen, and a dynamic container, etc. Here, provide/inject and the event bus would be useful.

Going back to the origin of the discussion, you wanted to interact with Vue directly from a screen XML.
So let us see a working example (a proof of concept) which does not need to override anything in Moqui, just markup and scripts directly in the screen XML.

Let us take the Moqui Example component. Then, replace the EditExample.xml screen with the one below.

Basically, as it was exposed in a previous message, you can define Vue components inline with <component :is="/* options object */"></component>.

In the following example, a custom component is defined at the beginning of the screen widgets. Then, all the original screen content is inside this component’s slot.
This way, you can expose slot props for the rest of the screen. The mySlotProps in the example.

The tricky part is that if you define a component on the fly inside the form’s slot, then it will be destoyed and created again every time the form is rendered, because you are using a object definition directly in the :is attribute. This can be solved just reusing a component object. Instead of registering a component in a external js, this component options object is defined in the top custom component and exposed in mySlotProps. This is the sampleComp in the example.

Then, inside the form, an additional field is defined with <render-mode>. Because now we are inside the Moqui Vue form’s slot, we have the formProps available, which is the slot props provided by the mForm component. The formProps has a fields property with the current values of all fields in the form.

In this example, the component sampComp, just as a proof of concept, allows to bind a value with v-model. We use this for binding the Example entity comments field, so that we will be able to set its value programmatically. Then, we bind the field exampleTypeEnumId that we watch with a watcher, and every time it changes we set a value in comments.
This just demonstrates that we can react to changes in form fields and to modify their values programmatically from a screen XML.
Just for completion, mySlotProps exposes a method too, demonstrating how we can provide a method to other parts of the screen.

Essentially, mySlotProps is a simplied way to achieve a provide/inject effect.

As you see, inside <render-mode> we can use Freemarker, Quasar components and Moqui Vue components. So there is a lot of flexibility here.

To sum up, you can define Vue components on the fly in a screen XML, without previous registration and without external js files. Just as you requested.
Of course, if what you need is to interact more deeply with Moqui components (such as reacting to autocomplete events, etc.) you may require to redefine your own components anyway, but perhaps with simple wrapper components defined inline and Freemarker inside <render-mode> you may solve several of your use cases.

<?xml version="1.0" encoding="UTF-8"?>
<!--
This software is in the public domain under CC0 1.0 Universal plus a
Grant of Patent License.

To the extent possible under law, the author(s) have dedicated all
copyright and related and neighboring rights to this software to the
public domain worldwide. This software is distributed without any
warranty.

You should have received a copy of the CC0 Public Domain Dedication
along with this software (see the LICENSE.md file). If not, see
<http://creativecommons.org/publicdomain/zero/1.0/>.
-->
<screen xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-2.1.xsd"
        default-menu-title="Example" default-menu-index="1">

    <parameter name="exampleId" required="true"/>

    <transition name="updateExample"><service-call name="moqui.example.ExampleServices.updateExample"/>
        <default-response url="."/></transition>
    <transition name="edit"><path-parameter name="exampleId"/><default-response url=".">
        <parameter name="exampleId" from="exampleId"/></default-response></transition>
    <transition name="features"><path-parameter name="exampleId"/>
        <default-response url="../EditExampleFeatureAppls">
            <parameter name="exampleId" from="exampleId"/>
            <parameter name="test" value="foo"/>
        </default-response>
    </transition>

    <actions>
        <entity-find-one entity-name="moqui.example.Example" value-field="example"/>
        <log message="EditExample exampleId [${exampleId}] example [${example}]"/>
    </actions>
    <widgets>


        <!-- Opening of a custom component for providing slot data.
             Note that with text template="true" we can use FreeMarker here.
             You can also use Quasar and Moqui Vue components -->
        <render-mode>
            <text template="true" type="qvt"><![CDATA[ 
                <component :is="{
                    data: function() { return { 
                        message: 'This is the Example ${exampleId}',
                        // An options object for defining a component.
                        // We do this here so we can reuse the object. See more comments below.
                        // Just a proof of concept. 
                        sampleComponent: {
                            props: ['value', 'fieldToWatch', 'doSomething'],
                            data: function() { return { 
                                sample: 'This is the sample component.',
                                counter: 0
                            } },
                            watch: {
                                fieldToWatch: function(newVal,oldVal) { 
                                    this.counter++;
                                    this.$emit('input', 'Hello, this field is filled dynamically after changing other field from '
                                        + oldVal + ' to ' + newVal);
                                    moqui.notifyGrowl({type:'info', title:'Field filled in dynamically!'});    
                                }
                            },
                            // created() just to check that this component is created only once.
                            created: function() { this.sample = this.sample + ' Created ' + Date.now() },
                            template: `<div>
                                <p>{{sample}}</p>
                                <p>Watched field has changed {{counter}} times, now it is {{fieldToWatch}}</p>
                                <p><q-btn @click=&quot;doSomething&quot;>Click me for doing something!</q-btn></p>
                                </div>`
                        }
                    } },
                    methods: {
                        doSomething: function() { window.alert('This works!') }
                    },
                    template: `<div><slot :message=&quot;message&quot; :sampleComp=&quot;sampleComponent&quot; :method=&quot;doSomething&quot;></slot></div>`
                }" v-slot="mySlotProps">
            ]]></text>
        </render-mode>



        <section name="ExampleMenu" condition="example"><widgets>
            <label text="Test/Example Links:"/>
            <link url="edit" text="Edit" link-type="anchor"/>
            <link url="features" text="Features" link-type="anchor"/>
            <link url="/apps/example/Example/EditExample/features/${exampleId}" text="Features - Path Parameter" link-type="anchor"/>
        </widgets></section>

        <form-single name="UpdateExample" transition="updateExample" map="example">
            <auto-fields-service service-name="moqui.example.ExampleServices.create#Example"/>
            <!-- for the auto-service, basically the entity and operation: <auto-fields-service service-name="create#Example"/> -->

            <field name="exampleId"><default-field><display/></default-field></field>
            <field name="exampleTypeEnumId">
                <default-field title="Type" tooltip="This is the type of example">
                    <auto-widget-entity entity-name="Example" field-type="edit"/>
                    <!-- the auto-widget-entity element will basically produce this:
                    <drop-down allow-empty="false">
                        <entity-options text="${description}">
                            <entity-find entity-name="moqui.basic.Enumeration">
                                <econdition field-name="enumTypeId" value="ExampleType"/>
                                <order-by field-name="description"/>
                            </entity-find>
                        </entity-options>
                    </drop-down> -->
                </default-field>
            </field>
            <field name="statusId">
                <default-field title="Status" tooltip="This is the status of the example">
                        <drop-down allow-empty="false" current-description="${example?.'Example#moqui.basic.StatusItem'?.description}">
                            <entity-options key="${toStatusId}" text="StatusTransitionNameTemplate">
                                <entity-find entity-name="moqui.basic.StatusFlowTransitionToDetail">
                                    <econdition field-name="statusId" from="example.statusId"/>
                                </entity-find>
                            </entity-options>
                        </drop-down>    
                </default-field>
            </field>
            <field name="exampleName"><default-field tooltip="The name of the example"><text-line/></default-field></field>
            <field name="description"><default-field tooltip="The description of the example"><text-line/></default-field></field>
            <!-- uncomment to see example of a render-mode embedded in a field:
            <field name="testRenderMode"><default-field>
                <render-mode><text type="html,vuet,qvt"><![CDATA[<span><div>This is test HTML text.</div><div>This is another line.</div></span>]]></text></render-mode>
            </default-field></field>
            -->

            <!-- A field in a form to demonstrate how to interact with Vue components from a Moqui screen -->
            <field name="myExperiment">
                <default-field>
                    <render-mode>
                        <text type="qvt"><![CDATA[ 
                            <!-- Now we are including markup inside the Moqui Vue form slot, therefore,
                                 we have formProps available as slot props. It has a 'field' property
                                 with the current values of all fields in the form -->

                            <hr>
                            <p>Let's try mySlotProps: {{mySlotProps.message}}</p>
                            <hr>

                            <!-- Defining a component directly with an options object inline is handy, but doing it
                                inside a form slot will make it to be destroyed and created again every time
                                the form is rendered (which happens when a field changes). This is not ideal and we will
                                not be able to use watchers, etc. -->    
                            <keep-alive> <!-- keep-alive has nothing to do in this case, just for demonstrating it -->
                            <component :is="{
                                data: function() { return { 
                                    myStuff: 'Hey, I\'m a component defined on the fly, inside a mForm slot. Type in a form field and you will see that I am created again.' 
                                }},
                                created: function() { this.myStuff = this.myStuff + ' Created ' + Date.now()},
                                template: `<p>{{myStuff}}</p>`
                            }"></component>
                            </keep-alive>

                            <hr>
                            <p>Try to change the value of the "Type" field in the dropdown and you will see what happens.</p> 

                            <!-- The destroy/create problem does not happen with registered components -->
                            <!-- And it does not happen either if we reuse an object --> 
                            <component :is="mySlotProps.sampleComp"
                                       v-model="formProps.fields.comments"
                                       :fieldToWatch="formProps.fields.exampleTypeEnumId"
                                       :doSomething="mySlotProps.method"></component>
                            
                            <hr>
                            <p>This is what you have in formProps:</p>
                            <pre>{{formProps.fields}}</pre>

                        ]]></text>
                    </render-mode>    

                </default-field>
            </field>

            <field name="submitButton"><default-field title="Update"><submit/></default-field></field>

            <field-layout>
                <field-ref name="exampleId"/>
                <fields-not-referenced/>
                <field-row><field-ref name="exampleSize"/><field-ref name="exampleDate"/></field-row>
                <field-row><field-ref name="testDate"/><field-ref name="testTime"/></field-row>

                <field-accordion active="1">
                    <field-group title="Special Fields">
                        <field-ref name="auditedField"/>
                        <field-ref name="encryptedField"/>
                    </field-group>
                    <field-group title="Validated Fields">
                        <field-ref name="exampleEmail"/>
                        <field-ref name="exampleUrl"/>
                    </field-group>
                    <field-group title="My experiment">
                        <field-ref name="myExperiment"/>
                    </field-group>
                </field-accordion>

                <!-- <field-ref name="testRenderMode"/> -->
                <field-ref name="submitButton"/>
            </field-layout>
        </form-single>

        <!-- Closing custom component -->
        <render-mode>
            <text type="qvt"><![CDATA[ 
                </component>
            ]]></text>
        </render-mode>

    </widgets>
</screen>

Best,

Francisco Faes
www.blurbiness.com

3 Likes

@franciscofaes Excellent examples. This is so good that I wish we can incorporate into a new screen in the example component.

I get it fully now, customization can go in many different directions depending on needs. But your file example makes it clear at least what are the available options. Thank you again for your help, you’ve been kind with your time and attention.

1 Like