How to subscribe to events without DOM modifications in cyclejs (motorcyclejs)? - cyclejs

How can i subscribe events from some DOM nodes if I do not want to produce changes in DOM?
How can I achive something like this in cyclejs or motorcyclejs?
function main({DOM}) {
DOM.select('button').events('click').forEach(e => console.log(e))
return {}
}
run(main, {DOM: makeDOMDriver('#app')})
updated:
DOM tree already exits before running main function:
<div id="app">
<button>Click</button>
</div>
The example above does not work, the event listener is not attached to DOM node.

Here this should demonstrate what your setup should look like if you wanted the ability to view your DOM clicks:
const view = () => {
return div('app',[
button('Click')
]);
};
const intent = ({DOM}) => {
const intent$ = DOM.select('button').events('click').map(e => console.log(e));
return intent$;
};
const main = (sources) => {
const intent$ = intent(sources);
const view$ = Rx.Observable.just(view);
return {
DOM: view$
};
};
run(main, {DOM: makeDOMDriver('#app')})

Related

reactive object not updating on event emitted from watch

i'm building a complex form using this reactive obj
const formData = reactive({})
provide('formData', formData)
inside the form one of the components is rendered like this:
<ComboZone
v-model:municipality_p="formData.registry.municipality"
v-model:province_p="formData.registry.province"
v-model:region_p="formData.registry.region"
/>
this is the ComboZone render function:
setup(props: any, { emit }) {
const { t } = useI18n()
const { getters } = useStore()
const municipalities = getters['registry/municipalities']
const _provinces = getters['registry/provinces']
const _regions = getters['registry/regions']
const municipality = useModelWrapper(props, emit, 'municipality_p')
const province = useModelWrapper(props, emit, 'province_p')
const region = useModelWrapper(props, emit, 'region_p')
const updateConnectedField = (key: string, collection: ComputedRef<any>) => {
if (collection.value && collection.value.length === 1) {
console.log(`update:${key} => ${collection.value[0].id}`)
emit(`update:${key}`, collection.value[0].id)
} else {
console.log(`update:${key} =>undefined`)
emit(`update:${key}`, undefined)
}
}
const provinces = computed(() => (municipality.value ? _provinces[municipality.value] : []))
const regions = computed(() => (province.value ? _regions[province.value] : []))
watch(municipality, () => updateConnectedField('province_p', provinces))
watch(province, () => updateConnectedField('region_p', regions))
return { t, municipality, province, region, municipalities, provinces, regions }
}
useModelWrapper :
import { computed, WritableComputedRef } from 'vue'
export default function useModelWrapper(props: any, emit: any, name = 'modelValue'): WritableComputedRef<any> {
return computed({
get: () => props[name],
set: (value) => {
console.log(`useModelWrapper update:${name} => ${value}`)
emit(`update:${name}`, value)
}
})
}
problem is that the events emitted from useModelWrapper update the formData in the parent template correctly, the events emitted from inside the watch function are delayed by one render....
TL;DR;
Use watchEffect instead of watch
...with the caveat that I haven't tried to reproduce, my guess is that you're running into this issue because you're using watch which runs lazily.
The lazy nature is a result of deferring execution, which is likely why you're seeing it trigger on the next cycle.
watchEffect
Runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed.
watch
Compared to watchEffect, watch allows us to:
Perform the side effect lazily;
Be more specific about what state should trigger the watcher to re-run;
Access both the previous and current value of the watched state.
found a solution, watchEffect wasn't the way.
Looks like was an issue with multiple events of update in the same tick, worked for me handling the flush of the watch with { flush: 'post' } as option of the watch function.
Try to use the key: prop in components. I think it will solve the issue.

AddEventListener DOM event Jest testing

I have one Users vue component and I am trying to test mounted() with addEventListener.
Users.vue
=========
mounted(){
let viewPort = document.getElementById("Users-list"); ----> Here I am getting null for addEventListener.
viewPort!.addEventListener("scroll", (e: any) => {
let result =
e.target.scrollHeight - e.target.scrollTop - e.target.clientHeight ===
0;
if (result) {
this.test = this.Offset + this.Limit;
this.response = this.GetDetails();
}
});
}
I have written spec for Users component and trying to test mounted() method with addEventListener.
But I am getting an error message cannot read property addEventListener of null.
Users.spec.ts
=============
describe('Users TestSuite', async () => {
let userWrapper: any;
let userObj: any;
beforeEach(() => {
userWrapper = shallowMount(Users, {
// attachTo: document.getElementById('Users-list'),
localVue,
i18n,
router
})
userObj = userWrapper.findComponent(Users).vm;
const mockAddeventListener = jest.fn().mockImplementation((event, fn) => {
fn();
})
document.getElementById = jest.fn().mockReturnValue({
scrollTop: 100,
clientHeight: 200,
scrollHeight: 500,
addEventListener: mockAddeventListener
})
expect(mockAddeventListener).toBeCalledWith('scroll', expect.anything());
});
it('should render Users page', () => {
expect(userObj).toBeTruthy();
});
I think the problem here might be u are creating the mock function after u are creating the component. Mounted method will be called when the wrapper is created so try to move the mock implementation above the wrapper statement.
Another sure way in which to make it work is before u create the wrapper set the body of the document like document.body.innerHTML = <div id="users-list"></div>. This will definitely work.
For both the above solutions make sure that they are above the wrapper statement.

How to send data from one Electron window to another with ipcRenderer using vuejs?

I have in one component this:
openNewWindow() {
let child = new BrowserWindow({
modal: true,
show: false,
});
child.loadURL('http://localhost:9080/#/call/' + this.chatEntityId + '?devices=' + JSON.stringify(data));
child.on('close', function () { child = null; });
child.once('ready-to-show', () => {
child.show();
});
child.webContents.on('did-finish-load', () => {
console.log("done loading");
ipcRenderer.send('chanel', "data");
});
}
And then in child window component:
mounted() {
ipc.on('chanel', (event, message) => {
console.log(message);
console.log(event);
});
}
I tried that .on in created() and beforeCreate() and with this.$nextTick(), withsetTimeout` but nothing works.
I don't want to send some string data but object but as you can see not event simple string "data" works. I am out of ideas.
I can see that this only works in parent component for component where that emit came from if i do this:
send
listen in main process
send back to event.sender
So, question is how to pass any form of data from one window to another?
Ok, after long night, morning after solution.
In some vuejs componenet some action on button click for example
ipcRenderer.send('chanel', someData);
In main process
ipcMain.on('chanel', (event, arg) => {
let child = new BrowserWindow()
// other stuff here
child.loadURL(arg.url)
child.on('show', () => {
console.log("done loading");
child.webContents.send('data', arg);
});
})
In vuejs component for other route arg.url
mounted() {
ipc.on('chanel', (event, message) => {
console.log(message);
console.log(event);
});
}

Open a VueJS component on a new window

I have a basic VueJS application with only one page.
It's not a SPA, and I do not use vue-router.
I would like to implement a button that when clicked executes the window.open() function with content from one of my Vue Components.
Looking at the documentation from window.open() I saw the following statement for URL:
URL accepts a path or URL to an HTML page, image file, or any other resource which is supported by the browser.
Is it possible to pass a component as an argument for window.open()?
I was able to use some insights from an article about Portals in React to create a Vue component which is able to mount its children in a new window, while preserving reactivity! It's as simple as:
<window-portal>
I appear in a new window!
</window-portal>
Try it in this codesandbox!
The code for this component is as follows:
<template>
<div v-if="open">
<slot />
</div>
</template>
<script>
export default {
name: 'window-portal',
props: {
open: {
type: Boolean,
default: false,
}
},
data() {
return {
windowRef: null,
}
},
watch: {
open(newOpen) {
if(newOpen) {
this.openPortal();
} else {
this.closePortal();
}
}
},
methods: {
openPortal() {
this.windowRef = window.open("", "", "width=600,height=400,left=200,top=200");
this.windowRef.addEventListener('beforeunload', this.closePortal);
// magic!
this.windowRef.document.body.appendChild(this.$el);
},
closePortal() {
if(this.windowRef) {
this.windowRef.close();
this.windowRef = null;
this.$emit('close');
}
}
},
mounted() {
if(this.open) {
this.openPortal();
}
},
beforeDestroy() {
if (this.windowRef) {
this.closePortal();
}
}
}
</script>
The key is the line this.windowRef.document.body.appendChild(this.$el); this line effectively removes the DOM element associated with the Vue component (the top-level <div>) from the parent window and inserts it into the body of the child window. Since this element is the same reference as the one Vue would normally update, just in a different place, everything Just Works - Vue continues to update the element in response to databinding changes, despite it being mounted in a new window. I was actually quite surprised at how simple this was!
You cannot pass a Vue component, because window.open doesn't know about Vue. What you can do, however, is to create a route which displays your component and pass this route's URL to window.open, giving you a new window with your component. Communication between the components in different windows might get tricky though.
For example, if your main vue is declared like so
var app = new Vue({...});
If you only need to render a few pieces of data in the new window, you could just reference the data model from the parent window.
var app1 = window.opener.app;
var title = app.title;
var h1 = document.createElement("H1");
h1.innerHTML = title;
document.body.appendChild(h1);
I ported the Alex contribution to Composition API and works pretty well.
The only annoyance is that the created window ignores size and position, maybe because it is launched from a Chrome application that is fullscreen. Any idea?
<script setup lang="ts">
import {ref, onMounted, onBeforeUnmount, watch, nextTick} from "vue";
const props = defineProps<{modelValue: boolean;}>();
const emit = defineEmits(["update:modelValue"]);
let windowRef: Window | null = null;
const portal = ref(null);
const copyStyles = (sourceDoc: Document, targetDoc: Document): void => {
// eslint-disable-next-line unicorn/prefer-spread
for(const styleSheet of Array.from(sourceDoc.styleSheets)) {
if(styleSheet.cssRules) {
// for <style> elements
const nwStyleElement = sourceDoc.createElement("style");
// eslint-disable-next-line unicorn/prefer-spread
for(const cssRule of Array.from(styleSheet.cssRules)) {
// write the text of each rule into the body of the style element
nwStyleElement.append(sourceDoc.createTextNode(cssRule.cssText));
}
targetDoc.head.append(nwStyleElement);
}
else if(styleSheet.href) {
// for <link> elements loading CSS from a URL
const nwLinkElement = sourceDoc.createElement("link");
nwLinkElement.rel = "stylesheet";
nwLinkElement.href = styleSheet.href;
targetDoc.head.append(nwLinkElement);
}
}
};
const openPortal = (): void => {
nextTick().then((): void => {
windowRef = window.open("", "", "width=600,height=400,left=200,top=200");
if(!windowRef || !portal.value) return;
windowRef.document.body.append(portal.value);
copyStyles(window.document, windowRef.document);
windowRef.addEventListener("beforeunload", closePortal);
})
.catch((error: Error) => console.error("Cannot instantiate portal", error.message));
};
const closePortal = (): void => {
if(windowRef) {
windowRef.close();
windowRef = null;
emit("update:modelValue", false);
}
};
watch(props, () => {
if(props.modelValue) {
openPortal();
}
else {
closePortal();
}
});
onMounted(() => {
if(props.modelValue) {
openPortal();
}
});
onBeforeUnmount(() => {
if(windowRef) {
closePortal();
}
});
</script>
<template>
<div v-if="props.modelValue" ref="portal">
<slot />
</div>
</template>

Testing Methods within Vue Components using Jasmine

I have the following test which works great
it('does not render chapter div or error div', () => {
const payLoad = chapter;
const switcher = 'guild';
var vm = getComponent(payLoad, switcher).$mount();
expect(vm.$el.querySelector('#chapter-card')).toBeNull();
expect(vm.$el.querySelector('#error-card')).toBeNull();
});
To do this I wrote a helper method that mounts a component:
const getComponent = (prop1) => {
let vm = new Vue({
template: '<div><compd :payLoad="group" :index="index" "></compd ></div></div>',
components: {
compd,
},
data: {
payLoad: prop1,
},
})
return vm;
}
however, I have a method within my vue component compd. For simplicitys sake, lets call it
add(num,num){
return num+num;
}
I want to be able to write a test case similar to the following:
it('checks the add method works', () => {
expect(compd.add(1,2).toBe(3));
});
I cannot figure out how to do this. Has anyone any suggestions?
The documentation here:
https://v2.vuejs.org/v2/guide/unit-testing.html
Does not cover testing methods.
Source code from vue repo
As you can see the method gets called simply on the instance
const vm = new Vue({
data: {
a: 1
},
methods: {
plus () {
this.a++
}
}
})
vm.plus()
expect(vm.a).toBe(2)
You can also access the method via $options like in this case (vue source code)
const A = Vue.extend({
methods: {
a () {}
}
})
const vm = new A({
methods: {
b () {}
}
})
expect(typeof vm.$options.methods.a).toBe('function')
Update:
To test child components use $children to access the necessary child. Example
var childToTest = vm.$children.find((comp)=>comp.$options.name === 'accordion')` assuming name is set to `accordion`
After that you can
childToTest.plus();
vm.$nextTick(()=>{
expect(childToTest.someData).toBe(someValue)
done(); //call test done callback here
})
If you have a single child component and not a v-for put a ref on it
`
vm.$refs.mycomponent.myMethod()