There is a reliable way to check from QML/JavaScript if a component has a signal?
At the moment I found that this code work as intended:
if (myCustomComponent.myCustomSignal)
myCustomComponent.myCustomSignal();
Where myCustomComponent is even as simple as this:
Item {
id: root
signal myCustomSignal()
}
The signal is executed only if myCustomComponent has it but I'm not sure if is the correct way.
No, more reliable way should be to check if component has a signal could be:
if (typeof root.myCustomSignal !== 'undefined' && typeof root.myCustomSignal === 'function') { ... }
or
if (typeof root.myCustomSignal === 'function') { ... }
Using boolean is not realiable. Change your sample target item to:
Item {
id: root
property bool myCustomSignal: true
}
In this case your condition will be true and you will try invoke a property what will lead to crash.
This approach is less error prone and it ensure that signal exists and is invokable/function.
Related
I have the following code in a Nuxtjs app in SSR mode.
<Component
:is="author.linkUrl ? 'a' : 'div'"
v-bind="!author.linkUrl && { href: author.linkUrl, target: '_blank' }"
#click="author.linkUrl ? handleAnalytics() : null"
>
The click event in case it's an a tag, will only fire if it's written as handleAnalytics(), but handleAnalytics will not work.
Don't get me wrong the code is working, but I don't understand why.
With classical event binding (#click="handleAnalytics), Vue will auto bind it for you because it sees it's a function.
But when provided a ternary condition, it's not auto binded but wrapped into a anonymous function instead. So you have to call it with parenthesis otherwise you're just returning the function without executing it.
To be clearer, you can write it this way: #click="() => author.linkUrl ? handleAnalytics() : null"
Note: when having a dynamic tag component, I'd suggest to use the render function instead.
This is an advanced technique, but this way you won't bind things to an element that doesn't need it (without having the kind of hack to return null).
Example:
export default {
props: {
author: { type: Object, required: true },
},
render (h: CreateElement) {
const renderLink = () => {
return h('a', {
attrs: {
href: author.linkUrl,
target: '_blank',
},
on: {
click: this.handleAnalytics
},
)
}
const renderDiv = () => {
return h('div')
}
return this.author.linkUrl ? renderLink() : renderDiv()
}
}
Documention: Vue2, Vue3
In javascript functions are a reference to an object. Just like in any other language you need to store this reference in memory.
Here are a few examples that might help you understand on why its not working:
function handleAnalytics() { return 'bar' };
const resultFromFunction = handleAnalytics();
const referenceFn = handleAnalytics;
resultFromFunction will have bar as it's value, while referenceFn will have the reference to the function handleAnalytics allowing you to do things like:
if (someCondition) {
referenceFn();
}
A more practical example:
function callEuropeanUnionServers() { ... }
function callAmericanServers() { ... }
// Where would the user like for his data to be stored
const callAPI = user.preferesDataIn === 'europe'
? callEuropeanUnionServers
: callEuropeanUnionServers;
// do some logic
// ...
// In this state you won't care which servers the data is stored.
// You will only care that you need to make a request to store the user data.
callAPI();
In your example what happens is that you are doing:
#click="author.linkUrl ? handleAnalytics() : null"
What happens in pseudo code is:
Check the author has a linkUrl
If yes, then EXECUTE handleAnalytics first and then the result of it pass to handler #click
If not, simply pass null
Why it works when you use handleAnalytics and not handleAnalytics()?
Check the author has a linkUrl
If yes, then pass the REFERENCE handleAnalytics to handler #click
If not, simply pass null
Summary
When using handleAnalytics you are passing a reference to #click. When using handleAnalytics() you are passing the result returned from handleAnalytics to #click handler.
I have a keyboard navigation system. When you press ArrowUp or ArrowDown, an event is emitted FROM app.js (best place I found to listen to these keypresses since they need to be system-wide) TO the mounted() in the component.
The Event.$on() INSIDE the mounted() part of the component then calls a function that uses $refs to identify the currently selected item and, when ENTER is pressed, show it's modal.
app.js code (listen to the keypresses):
else if (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'Enter') {
event.preventDefault()
switch (this.$router.currentRoute.path) {
case "/pedidos":
Event.$emit('navegarSetasPedidos', event.key)
break;
case "/clientes":
Event.$emit('navegarSetasClientes', event.key)
break;
}
}
mounted() section of the component in question:
mounted() {
Event.$on('navegarSetasPedidos', (key) => {this.navegarSetas(key)})
}
function responsible for the navigation (sorry for bad formating, haven't figured how stackoverflow's codeblock thing works yet):
navegarSetas(key) {
if (this.navegacaoSetasAtiva == false) {
this.navegacaoSetasAtiva = true
this.navegacaoAtual = 0
} else if (this.modalAtivado == false && this.navegacaoSetasAtiva == true) {
if (key == 'ArrowDown' && this.navegacaoAtual < this.pedidos.length - 1) {
this.navegacaoAtual++
let elementoSelecionado = this.$refs['pedido'+this.navegacaoAtual][0].$el
let boundaries = elementoSelecionado.getBoundingClientRect()
if (boundaries.top < 0 || boundaries.top > (window.innerHeight || document.documentElement.clientHeight)){
elementoSelecionado.scrollIntoView({behavior: 'smooth'})
}
} else if (key == 'ArrowUp' && this.navegacaoAtual <= this.pedidos.length && this.navegacaoAtual > 0) {
this.navegacaoAtual--
let elementoSelecionado = this.$refs['pedido'+this.navegacaoAtual][0].$el
let boundaries = elementoSelecionado.getBoundingClientRect()
if (boundaries.top < 0 || boundaries.top > (window.innerHeight || document.documentElement.clientHeight)){
elementoSelecionado.scrollIntoView({behavior: 'smooth'})
}
} else if (key == 'Enter') {
let pedidoSelecionado = this.pedidos[this.navegacaoAtual].id
Event.$emit('changeShow', pedidoSelecionado)
}
}
This works very well the first time it is acessed. The problem is, if I change the current route to show another component and then return to the previous component, I get a lot of "this.$refs['pedido'+this.navegacaoAtual][0].$el is undefined" errors, but the system still works normally, albeit erratically.
The funny thing is: if I console log "this.$refs['pedido'+this.navegacaoAtual][0].$el is undefined", I'll get an EMPTY log before the errors, then ANOTHER one right below it, this time, not empty.
Everywhere else I've searched this says that the problem is due to how Vue re-renders things, and that I'm calling this event BEFORE it's rendered, which shouldn't be possible since I'm calling it inside mounted().
Any help is greatly appreciated, thank you!
Turns out, after a LOT of searching, the Event.$on event setters also work as the normal JavaScript ones (which makes a lot of sense now that I'm thinking about it)—meaning that you have to destroy them whenever your component is unmounted (aka Destroyed).
Even though VUE Dev Tools was picking only one event after the re-route, it was still firing two (seen through console.log() returning one empty value, a bunch of errors, and another value with filled array AFTER the errors).
The solution to this was simply adding Event.$off('eventName') on the destroyed() function of the component.
I try to setState in compenetDidUpdate but it shows error infinite loop. Got any solution? Originally I put the setState in a function but also face this error. I am using class component code
componentDidUpdate(){
if(isEmpty(this.props.AESDetail) == false){
if(this.props.AESDetail.length != 0){
if(this.props.APIESDetail.length != 0){
if(this.props.APIESDetail.Focus != null){
this.setState({
gotFocusApies: true
})
}
}
}
}
}
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
It looks like you might just need one additional test to make sure gotFocusApies isn't already true.
These if statements are also probably better off combined into one.
Note: list.length != 0 can typically be replaced with list.length or !!list.length.
componentDidUpdate() {
if (
isEmpty(this.props.AESDetail) == false &&
this.props.AESDetail.length &&
this.props.APIESDetail.length &&
this.props.APIESDetail.Focus != null &&
!this.state.gotFocusApies
) {
this.setState({ gotFocusApies: true });
}
}
I'm building an auto-complete menu in Vue.js backed by Firebase (using vue-fire). The aim is to start typing a user's display name and having match records show up in the list of divs below.
The template looks like this:
<b-form-input id="toUser"
type="text"
v-model="selectedTo"
#change="searcher">
</b-form-input>
<div v-on:click="selectToUser(user)" class="userSearchDropDownResult" v-for="user in searchResult" v-if="showSearcherDropdown">{{ user.name }}</div>
Upon clicking a potential match the intention is to set the value of the field and clear away the list of matches.
Here is the code portion of the component:
computed: {
/* method borrowed from Reddit user imGnarly: https://www.reddit.com/r/vuejs/comments/63w65c/client_side_autocomplete_search_with_vuejs/ */
searcher() {
let self = this;
let holder = [];
let rx = new RegExp(this.selectedTo, 'i');
this.users.forEach(function (val, key) {
if (rx.test(val.name) || rx.test(val.email)) {
let obj = {}
obj = val;
holder.push(obj);
} else {
self.searchResult = 'No matches found';
}
})
this.searchResult = holder;
return this.selectedTo;
},
showSearcherDropdown() {
if(this.searchResult == null) return false;
if(this.selectedTo === '') return false;
return true;
}
},
methods: {
selectToUser: function( user ) {
this.newMessage.to = user['.key'];
this.selectedTo = user.name;
this.searchResult = null;
}
}
Typeahead works well, on each change to the input field the searcher() function is called and populates the searchResult with the correct values. The v-for works and a list of divs is shown.
Upon clicking a div, I call selectToUser( user ). This correctly reports details from the user object to the console.
However, on first click I get an exception in the console and the divs don't clear away (I expect them to disappear because I'm setting searchResults to null).
[Vue warn]: Error in event handler for "change": "TypeError: fns.apply is not a function"
found in
---> <BFormInput>
<BFormGroup>
<BTab>
TypeError: fns.apply is not a function
at VueComponent.invoker (vue.esm.js?efeb:2004)
at VueComponent.Vue.$emit (vue.esm.js?efeb:2515)
at VueComponent.onChange (form-input.js?1465:138)
at boundFn (vue.esm.js?efeb:190)
at invoker (vue.esm.js?efeb:2004)
at HTMLInputElement.fn._withTask.fn._withTask (vue.esm.js?efeb:1802)
If I click the div a second time then there's no error, the input value is set and the divs disappear.
So I suspect that writing a value to this.selectedTo (which is also the v-model object for the element is triggering a #change event. On the second click the value of doesn't actually change because it's already set, so no call to searcher() and no error.
I've noticed this also happens if the element loses focus.
Question: how to prevent an #change event when changing v-model value via a method?
(other info: according to package.json I'm on vue 2.5.2)
On:
<b-form-input id="toUser"
type="text"
v-model="selectedTo"
#change="searcher">
The "searcher" should be a method. A method that will be called whenever that b-component issues a change event.
But looking at your code, it is not a method, but a computed:
computed: {
searcher() {
...
},
showSearcherDropdown() {
...
}
},
methods: {
selectToUser: function( user ) {
...
}
}
So when the change event happens, it tries to call something that is not a method (or, in other words, it tries to call a method that doesn't exist). That's why you get the error.
Now, since what you actually want is to update searcher whenever this.selectedTo changes, to get that, it is actually not needed to have that #change handler. This is due to the code of computed: { searcher() { already depending on this.selectedTo. Whenever this.selectedTo changes, Vue will calculate searcher again.
Solution: simply remove #change="searcher" from b-form. Everything else will work.
#acdcjunior, thanks for your answer.
Of course just removing the reference to searcher() just means no action is taken upon field value change so the field won’t work at all.
Moving the searcher() function into methods: {} instead of computed: {} means that it will be called on an input event and not a change even (another mystery but not one for today). A subtle difference that takes away the typeahead feature I’m aiming at.
However, it did make me remember that the result of computed: {} functions are cached and will be re-computed when any parameters change. In this case I realised that the searcher() function is dependent upon the this.selectedTo variable. So when the selectToUser() function sets this.selectedTo it triggers another call to searcher().
Fixed now. In case anyone has a similar problem in the future, I resolved this by turning to old fashioned semaphore by adding another variable.
var userMadeSelection: false
Now, searcher() begins with a check for this scenario:
computed: {
searcher() {
if(this.userMadeSelection) {
this.userMadeSelection = false;
return this.selectedTo;
}
…
and then in selectToUser():
this.userMadeSelection = true;
Having first read this http://www.danyow.net/jquery-ui-datepicker-with-aurelia/ and with inspiration from https://gist.github.com/charlespockert/6a1fef3f546f6d37d1dc here follows my attempt to implement the https://github.com/eternicode/bootstrap-datepicker version of bootstrap datepicker:
http://plnkr.co/edit/TkbT6E?p=preview
I'm getting self.datePicker.datepicker is not a function(…) although I've checked that bootstrap-datepicker is correctly installed with jspm and that the .js is loaded. The datepicker does show up (which confirms that the js is loaded correctly), and I can select a date, but the value is not set.
What am I doing wrong here?
Update:
A friendly soul ,#SamDeBlock, in gitter.im/Aurelia put together this http://plnkr.co/edit/hKit8pigwL1ijr2DmbGP?p=preview with the dependencies so it'll run. I keep getting the above error however, when running this in my own application. I'm gonna investigate it problem further and update here, if I get to the bottom of this.
Update 2:
I've now located the problem down to being an issue with system.js/jspm. That's also why the above plunkr works, since it just reference the files directly instead of defining them in config.js.
If I add the file manually like proposed in the above plunkr - AND if I change the following in moment.js file from:
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
global.moment = factory()
}(this, function () { 'use strict';
...
to:
(function (global = typeof window !== "undefined" ? window : this, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
global.moment = factory()
}(this, function () { 'use strict';
...
the global = typeof window !== "undefined" ? window : this part..
THEN it seems to work, with the Aurelia navigation skeleton as well.
But why is this necessary? And how can I get this to work, without all the hacks? I've built my application on the Aurelia Navigation Starter project
https://github.com/aurelia/skeleton-navigation
Chances are that jspm caused this when doing jspm install -y npm:bootstrap-datepickerso check in config.js
Look for this:
"npm:bootstrap-datepicker#1.5.0": {
"fs": "github:jspm/nodelibs-fs#0.1.2",
"jquery": "npm:jquery#2.1.4"
},
And change it to this:
"npm:bootstrap-datepicker#1.5.0": {
"fs": "github:jspm/nodelibs-fs#0.1.2",
"jquery": "github:components/jquery#2.1.4"
},
The important part being: "jquery": "github:components/jquery#2.1.4"
Then everything should work through System.config without the need for the moment.js workaround or anything else.
http://plnkr.co/edit/OeVeiLXwfpTlorDZjBQe?p=preview