QT Quick UI Forms: how expose custom signal from QML component and handle in javascript - qml

The QT Quick UI Forms example describes how to separate the declarative UI from the imperative javascript.
However, it does not describe how to make a custom signal that is exposed (declared?) at the declaritive level and handled in the javascript file.
The form is to be loaded in a shell application that needs to call fire custom signals we are calling: init and shutdown.
It seems I should be able to do this::
// MyComponent.ui.qml
Item {
signal init()
}
// MyComponent.qml javascript file
MyComponent {
onInit : {
// do some initialization
}
}
// Usage in shell
MyComponent {
id: mycomp
}
// somewhere
button.clicked: mycomp.init()

The order of the references was incorrect.
Needs to be:
Loader => MyComponent.qml (javascript) => MyComponentForm.ui.qml
But was:
Loader => MyComponentForm.ui.qml
// MyComponent.qml (javascript) was not referenced
Our loader was configured to load the declarative qml directly. Fixed by loading the javascript qml file. (which in turn referred to the MyComponentForm)
This way, our javascript init function was exposed. The module with imperative javascript extends the declarative qml with javascript functions.
// MyComponentForm.ui.qml
Item {
// property aliases as required by imperative code
// declarative ui stuff
}
// MyComponent.qml javascript file
MyComponentForm {
onInit : {
// do some initialization
}
}
Loader {
anchors.fill: parent
id: pageLoader
}
Button {
onClicked {
source = "qrc://MyComponent.qml";
pageLoader.item.init();
}
}

Related

Nuxt class-based services architecture (register globally; vs manual import)

In my Nuxt app I'm registering app services in a plugin file (e.g. /plugins/services.js) like this...
import FeatureOneService from '#/services/feature-one-service.js'
import FeatureTwoService from '#/services/feature-two-service.js'
import FeatureThreeService from '#/services/feature-three-service.js'
import FeatureFourService from '#/services/feature-four-service.js'
import FeatureFiveService from '#/services/feature-five-service.js'
export default (ctx, inject) => {
inject('feature1', new FeatureOneService(ctx))
inject('feature2', new FeatureTwoService(ctx))
inject('feature3', new FeatureThreeService(ctx))
inject('feature4', new FeatureFourService(ctx))
inject('feature5', new FeatureFiveService(ctx))
}
After doing this I can access any of my service on vue instance like this.$feature1.someMethod()
It works but I've once concern, that is, this approach loads all services globally. So whatever page the user visits all these services must be loaded.
Now I've 20+ such services in my app and this does not seem optimal approach to me.
The other approach I was wondering is to export a singleton instance within each service class and import this class instance in any component which needs that service.
So basically in my service class (e.g. feature-one-service.js) I would do like to do it like this..
export default new FeatureOneService() <---- I'm not sure how to pass nuxt instance in a .js file?
and import it my component where it is required like so...
import FeatureOneService from '#/services/feature-one-service.js'
What approach do you think is most feasible? if its the second one, then how to pass nuxt instance to my singleton class?
Yep, loading everything globally is not optimal in terms of performance.
You will need to either try to use JS files and pass down the Vue instance there.
Or use mixins, this is not optimal but it is pretty much the only solution in terms of reusability with Vue2.
Vue3 (composition API) brings composables, which is a far better approach regarding reusability (thing React hooks).
I've been struggling a lot with it and the only solution is probably to inject services to the global Vue instance at the component/page level during the initialisation (in created hook), another option is to do that in the middleware (or anywhere else where you have access to the nuxt context. Otherwise you won't be able to pass nuxt context to the service.
I usually set up services as classes, call them where necessary, and pass in the properties of the context which the class depends on as constructor arguments.
So for example, a basic MeiliSearchService class might look like:
export class MeilisearchService {
#client: MeiliSearch
constructor($config: NuxtRuntimeConfig) {
super()
this.#client = new MeiliSearch({
host: $config.services.meilisearch.host,
apiKey: $config.services.meilisearch.key
})
}
...
someMethod() {
let doSomething = this.#client.method()
...
}
...
}
Then wherever you need to use the service, just new up an instance (passing in whatever it needs) and make it available to the component.
data() {
const meiliSearchService = new MeiliSearchService(this.$config)
return {
meiliSearchService,
results,
...
}
},
methods: {
search(query) {
...
this.results = this.meiliSearchService.search(query)
...
}
}
As I'm sure you know, some context properties are only available in certain Nuxt life-cycle hooks. I find most of what I need is available everywhere, including $config, store, $route, $router etc.
Don't forget about Vue's reactivity when using this approach. For example, using a getter method on your service class will return the most recent value only when explicitly called. You can't, for example, stick the getter method in a computed() property and expect reactivty.
<div v-for='result in latestSearchResults'>
...
</div>
...
computed: {
latestSearchResults() {
return this.#client.getLatestResults()
}
}
Instead, call the method like:
methods: {
getLatestResults() {
return this.#client.getLatestResults()
}
}

Access data model in VueJS with Cypress (application actions)

I recently came across this blog post: Stop using Page Objects and Start using App Actions. It describes an approach where the application exposes its model so that Cypress can access it in order to setup certain states for testing.
Example code from the link:
// app.jsx code
var model = new app.TodoModel('react-todos');
if (window.Cypress) {
window.model = model
}
I'd like to try this approach in my VueJS application but I'm struggling with how to expose "the model".
I'm aware that it's possible to expose the Vuex store as described here: Exposing vuex store to Cypress but I'd need access to the component's data().
So, how could I expose e.g. HelloWorld.data.message for being accessible from Cypress?
Demo application on codesandbox.io
Would it be possible via Options/Data API?
Vue is pretty good at providing it's internals for plugins, etc. Just console.log() to discover where the data sits at runtime.
For example, to read internal Vue data,
either from the app level (main.js)
const Vue = new Vue({...
if (window.Cypress) {
window.Vue = Vue;
}
then in the test
cy.window().then(win => {
const message = win.Vue.$children[0].$children[0].message;
}
or from the component level
mounted() {
if (window.Cypress) {
window.HelloWorld = this;
}
}
then in the test
cy.window().then(win => {
const message = win.HelloWorld.message;
}
But actions in the referenced article implies setting data, and in Vue that means you should use Vue.set() to maintain observability.
Since Vue is exposed on this.$root,
cy.window().then(win => {
const component = win.HelloWorld;
const Vue = component.$root;
Vue.$set(component, 'message', newValue);
}
P.S. The need to use Vue.set() may go away in v3, since they are implementing observability via proxies - you may just be able to assign the value.
Experimental App Action for Vue HelloWorld component.
You could expose a setter within the Vue component in the mounted hook
mounted() {
this.$root.setHelloWorldMessage = this.setMessage;
},
methods: {
setMessage: function (newValue) {
this.message = newValue;
}
}
But now we are looking at a situation where the Cypress test is looking like another component of the app that needs access to state of the HelloWorld.
In this case the Vuex approach you referenced seems the cleaner way to handle things.

How to get SPA navigation working with external framework that uses innerHTML for content

In my Vue.js app, I am using a bootstrap-based framework that generates the html for my header and a menu nav with links, which is then inserted into the page by assigning innerHTML to a mount point.
But when I use the generated content to navigate, the entire page reloads since the links aren't using <router-link>.
One attempt at a fix:
In the Vue app, I assigned a method called goto on the window object that would perform programmatic router navigation.
I was then able to pass javascript:window.goto("myPageName"); as the href attribute, but this comes with many undesirable side-effects.
How can I cleanly make the links navigate without reloading the page?
(The framework needs jQuery as a dependency, so that is able to be used in a solution.)
I was able to use a MutationObserver that watches for subtree changes and adds a custom click handler when it detects the links being added via .innerHTML.
With this method, I specify vue-goto:myPageName as the href attribute, and then the handler will take care of making it an SPA link.
import { router } from "#/router";
import { store } from "#/store";
export const attrib = "vue-goto";
export const prefix = attrib + ":";
function onChange() {
// find all links matching our custom prefix to which we have not yet added our custom handler
const links = window.$(`a[href^='${prefix}']`).not(`[${attrib}]`);
// add custom attribute for us to grab later
links.attr(attrib, function() {
// jQuery doesn't like arrow functions
return window
.$(this)
.attr("href")
.substr(prefix.length)
.trim();
});
// Update href on the link to one that makes sense
links.attr("href", function() {
return router.resolve({
name: window.$(this).attr(attrib), // grab attribute we saved earlier
params: { lang: store.state.language }, // in our case, our pages are qualified by a language parameter
}).href;
});
// Override default click navigation behaviour to use vue-router programmatic navigation
links.click(function(e) {
e.preventDefault(); // prevent default click
const routeName = window.$(this).attr(attrib);
const goto = {
name: routeName,
lang: store.state.language,
};
router.push(goto).catch(ex => {
// add catch here so navigation promise errors aren't lost to the void causing headaches later
// eslint-disable-next-line no-console
console.error(
`Error occurred during navigation from injected [${prefix}${routeName}] link`,
"\n",
ex,
);
});
});
}
let observer;
export function init() {
if (observer) observer.unobserve(document.body);
observer = new MutationObserver(onChange);
observer.observe(document.body, {
characterData: false,
childList: true,
subtree: true, // important, we want to see all changes not just at toplevel
attributes: false,
});
}
init();

How I can make a function Global in Aurelia

Here is a scenario. I want to access Aurelia function, outside the Aurelia Application. Like while running my application if I call a method "GetNotification(string Message)" through browser Console, then it should get called.
The reason is that my Aurelia Application will run in a .Net Application browser. So I want to communicate between my native application(.Net) and Aurelia application. As in .Net browser control, we can call any Javascript function. But I am unable to call Aurelia Function, as it is not exposed externally.
I wouldn't recommend exposing your methods directly to global namespace. What you could do would be to register a custom event handler from within your viewmodel class and then trigger it from the .net site like ...
// ViewModel within aurelia
export class MyViewModel {
attached(){
document.body.addEventListener('custom-event', event => {
this.myViewModelMethod(event.detail); // just keep in mind the scope
}, false);
}
myViewModelMethod(data) {
console.log('data', data);
}
}
// .NET (outside the aurelia app)
// keep in mind CustomEvent is supported by most browsers but for IE it's only IE11
// see: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent
document.body.dispatchEvent(new CustomEvent('custom-event', {
detail: {
myData: {
prop1:'prop1'
}
}
}));

How to dynamically mock ES6 modules with SystemJS?

I have a single-page application written in ES6. The code in transpiled server-side into classic javascript by babelJs, then loaded by SystemJs.
Javascript present in my html file:
System.config({
baseURL: '/js',
meta: {
'/js/*': { format: 'cjs' }
}});
System.defaultJSExtensions = true;
System.import("index.js")
.catch(function (error) {
console.error(error)
});
index.js:
import f1 from 'file1';
import f2 from 'file2';
// code here ...
Everything works fine. index.js is loaded, and all import statements are correctly executed.
Now, I want to create some pages with mocked ES6 modules, for testing purpose. My goal is to display pages by replacing model classes (contained in ES6 modules) with other static test classes.
Let's say I have 3 files: real_model.js, fake_model.js and component.js. component.js import the real model (import Model from 'real_model';).
How can I replace the real model by the fake one (in the component) dynamically ?
It's been a while since this question was posted, but maybe this solution might still be of help to anyone else.
With SystemJS it is possible to create a module on-the-fly using System.newModule. Then you can use System.set to overwrite existing modules with the new one. In our tests we use the following helper function to mock existing modules:
function mockModule(name, value) {
const normalizedName = System.normalizeSync(name);
System.delete(normalizedName);
System.set(normalizedName, System.newModule(Object.assign({ default: value }, value)));
}
Then, e.g. inside the beforeEach callback, we assign the mock and then import the module to be tested using System.import:
let [component, fake_model] = [];
beforeEach(() => {
// define mock
fake_model = { foo: 'bar' };
// overwrite module with mock
mockModule('real_model', fake_model);
// delete and reimport module
System.delete(System.normalizeSync('component'));
return System.import('src/testing').then((m) => {
component = m.default;
}).catch(err => console.error(err));
});
// test your component here ...
A big advantage of this approach is that you don't need an additional mocking library and it works solely with SystemJS.